chamba 0.8.0 → 0.10.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 (73) hide show
  1. package/README.md +14 -3
  2. package/dist/commands/dev.js +11 -11
  3. package/dist/commands/menu.js +1 -1
  4. package/dist/commands/settings.js +9 -6
  5. package/dist/lib/awake-holder.js +3 -0
  6. package/dist/lib/awake-marker.js +4 -0
  7. package/dist/lib/awake.js +1 -0
  8. package/dist/lib/chamba-yaml.js +4 -4
  9. package/dist/lib/constants.js +1 -1
  10. package/dist/lib/global-config.js +3 -3
  11. package/dist/lib/skills.js +2 -2
  12. package/package.json +1 -1
  13. package/templates/Dockerfile +3 -3
  14. package/templates/context/baseline.md +2 -2
  15. package/templates/context/web-pane-craft.md +1 -1
  16. package/templates/pane-apps/client/assets/api-CguEsvNp.js +12 -0
  17. package/templates/pane-apps/client/assets/api-Cyoc3E6K.css +1 -0
  18. package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
  19. package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
  20. package/templates/pane-apps/client/assets/home-CLFnmg7u.css +1 -0
  21. package/templates/pane-apps/client/assets/home-CrhBkqDQ.js +1 -0
  22. package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
  23. package/templates/pane-apps/client/assets/reviews-Bq8EwmG-.js +153 -0
  24. package/templates/pane-apps/client/assets/reviews-DzHtA35Q.css +1 -0
  25. package/templates/pane-apps/client/assets/search-DgFnnu-J.js +1 -0
  26. package/templates/pane-apps/client/assets/specs-JvdNFtXR.js +13 -0
  27. package/templates/pane-apps/client/assets/useNow-Ww-Gffnk.js +1 -0
  28. package/templates/pane-apps/client/home/index.html +16 -0
  29. package/templates/pane-apps/client/reviews/index.html +6 -4
  30. package/templates/pane-apps/client/specs/index.html +4 -3
  31. package/templates/pane-apps/server/home.mjs +11 -0
  32. package/templates/pane-apps/server/reviews.mjs +19 -18
  33. package/templates/pane-apps/server/specs.mjs +4 -4
  34. package/templates/skills/dx-review/SKILL.md +16 -3
  35. package/templates/skills/dx-review/references/acts.md +86 -15
  36. package/templates/skills/dx-spec/SKILL.md +11 -19
  37. package/templates/skills/dx-spec/references/review-guide.md +15 -9
  38. package/templates/skills/dx-spec/references/spec-guide.md +12 -5
  39. package/templates/skills/dx-spec/references/stages.md +44 -24
  40. package/templates/skills/dx-spec-execute/SKILL.md +78 -28
  41. package/templates/startup.mjs +13 -13
  42. package/templates/tool-helper.sh +4 -2
  43. package/templates/webterm/README.md +41 -17
  44. package/templates/webterm/awake.js +27 -0
  45. package/templates/webterm/config.js +30 -0
  46. package/templates/webterm/public/app/alerts.js +92 -11
  47. package/templates/webterm/public/app/composer.js +37 -3
  48. package/templates/webterm/public/app/connection.js +14 -0
  49. package/templates/webterm/public/app/dom.js +1 -0
  50. package/templates/webterm/public/app/frames.js +1 -1
  51. package/templates/webterm/public/app/home-host.js +16 -0
  52. package/templates/webterm/public/app/main.js +4 -2
  53. package/templates/webterm/public/app/pane-shape.js +5 -3
  54. package/templates/webterm/public/app/pane-shell.js +45 -5
  55. package/templates/webterm/public/app/pane.js +8 -2
  56. package/templates/webterm/public/app/reviews-host.js +5 -1
  57. package/templates/webterm/public/app/state.js +4 -0
  58. package/templates/webterm/public/app/tabs.js +37 -3
  59. package/templates/webterm/public/app/tool-host.js +19 -3
  60. package/templates/webterm/public/app/workspace-color.js +1 -1
  61. package/templates/webterm/public/index.html +5 -1
  62. package/templates/webterm/public/styles.css +65 -0
  63. package/templates/webterm/server.js +24 -1
  64. package/templates/webterm/tool-document.js +3 -2
  65. package/templates/webterm/tools/home.js +80 -0
  66. package/templates/webterm/tools/index.js +5 -4
  67. package/templates/webterm/tools/reviews.js +7 -3
  68. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +0 -12
  69. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +0 -1
  70. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +0 -1
  71. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +0 -153
  72. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +0 -13
  73. package/templates/skills/dx-spec/references/principles-template.md +0 -2
package/README.md CHANGED
@@ -33,17 +33,27 @@ The optional fields - `shadow_paths`, `env`, `ports`, `profiles` - are all descr
33
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
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.
35
35
  - **A web interface.** The browser front-end relays the real agent TUI over a websocket - every session on one page, several agents at once, images and dictation. It starts with the container. Claude sessions also get a status strip above the composer: model, context used, quota left.
36
- - **A pane of tools beside the terminal.** Pages, where an agent publishes a page for you to read or a form for you to answer; Specs, where a piece of work is read, annotated and steered; and Code Reviews, where a changeset is reviewed hunk by hunk with the agent on the other side of it. An agent reaches each one with a helper on its path - `specs` and `reviews` - and the `dx-spec` and `dx-review` skills teach it the craft.
36
+ - **A pane of tools beside the terminal.** Home, the pulse of the repository you are working in; 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 Specs and Code Reviews with a helper on its path - `specs` and `reviews` - and the `dx-spec` and `dx-review` skills teach it the craft. The agent cannot reach Home at all.
37
37
 
38
38
  The menu has two doors into that container.
39
39
  **Web interface** brings it up, opens the interface in your browser, and hands your terminal straight back - the work happens in the tab.
40
40
  **Container shell** drops you into a shell inside the container and opens nothing; the greeting there still prints the interface's URL, because it is running either way.
41
41
 
42
+ Both doors can hold your machine awake while the container is up, so an agent left running keeps working.
43
+ **Settings > Stay awake** is where you turn it on or off, and a machine that was never asked is asked once at its first session.
44
+ The display still sleeps, a closed lid still sends the machine to sleep, and the hold ends the moment the container stops.
45
+ It works on macOS and on Linux with systemd; elsewhere you are never asked, **Settings > Stay awake** says there is nothing to turn on, and your machine sleeps as it always did.
46
+
42
47
  ## The web pane
43
48
 
44
49
  Beside the terminal is a pane, there from the first second of a session, and the pane holds tools.
45
50
  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.
46
51
 
52
+ **Home** is the tab a session opens on, and it is the repository you are in, read from git.
53
+ The header row says the branch, what is uncommitted and when the last commit landed, and under it the work of the repository is drawn: the days you committed on, the shape of your week, the branches, the tree and the milestones.
54
+ It reads when you open the tab and when you press Refresh, and never on its own, so nothing here is watching you type.
55
+ Nothing it draws touches the repository, and its numbers are cached outside the workspace, so `git status` never shows it.
56
+
47
57
  **Pages** is where an agent publishes.
48
58
  `webpane report.html --title "Port rework"` puts a standalone HTML file in the pane as a chip: a plan, a comparison table, a long answer, anything worth reading at your own pace instead of scrolling back for.
49
59
  A page can also ask - a form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
@@ -54,11 +64,12 @@ They live under `~/.chamba/workspaces/<id>/agents/webpane/` on the host, and not
54
64
  It renders the specs directory of your repository: the work, its artifacts, and where each piece stands.
55
65
  Read an artifact as one continuous document, select any part of it to comment on or ask for its removal, and answer what the run asks - a gate on a screen, a round of questions, a decision that needs your judgment - as a form beside the thing it is about.
56
66
  Everything is a file in your repository, so two windows agree, a session that ends loses nothing, and git sees the work.
57
- The agent's half is `specs` on its path inside the container, and the `dx-spec` skills carry the craft: one builds a specification and one runs its plan, both honoring the engineering principles you keep in `principles.md` beside the work.
67
+ 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 rules your repository states in its own `AGENTS.md` or `CLAUDE.md`.
58
68
  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
69
 
60
70
  **Code Reviews** is where a changeset is read hunk by hunk.
61
71
  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.
72
+ The main line has no base of its own, so on `main` or `master` the form opens on the last commit and says why.
62
73
  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
74
  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
75
  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.
@@ -67,7 +78,7 @@ The agent's half is `reviews` on its path, and the `dx-review` skill carries the
67
78
  ## Where state lives
68
79
 
69
80
  `chamba.yaml` lives in the workspace and can be committed.
70
- Everything else is on your machine under `~/.chamba/`: one directory per workspace holding its lock file, agent config, and shadow overlays, plus a host-global `config` for settings that are yours rather than the project's.
81
+ Everything else is on your machine under `~/.chamba/`: one directory per workspace holding its lock file, agent config, and shadow overlays, plus a host-global `config` for settings that are yours rather than the project's - the web interface's port range, and whether chamba keeps this machine awake.
71
82
 
72
83
  ## Developing chamba
73
84
 
@@ -1,11 +1,11 @@
1
- import{spawnSync as A}from"node:child_process";import{createHash as ve}from"node:crypto";import{existsSync as Le}from"node:fs";import{join as j,relative as Ce}from"node:path";import{cancel as G,confirm as O,isCancel as W,log as r,outro as E,select as Ee}from"@clack/prompts";import{buildAgentContextDocs as Se,buildAgentMountArgs as ye,injectAgentContext as Pe}from"../lib/agent-context.js";import{openInBrowser as Re}from"../lib/browser.js";import{readChambaYaml as xe}from"../lib/chamba-yaml.js";import{CONTAINER_KEEP_ALIVE as Me,CONTAINER_STARTUP as We,CONTAINER_WORKSPACE as X,DEFAULT_PROFILE as re,GIT_MODE as _e,LABEL_ENV as ie,LABEL_GIT_MODE as se,LABEL_MANAGED as Ie,LABEL_PORTS as ae,LABEL_PROFILE as ce,LABEL_RUNTIME_ENV as le,LABEL_SHADOWS as de,LABEL_TIMEZONE as ue,RUNTIME_ENV as fe,WEB_CONTAINER_PORT as pe}from"../lib/constants.js";import{buildDockerfile as me,buildImageWithTempfile as Te,computeBuildHash as Ne,isImageStale as Oe}from"../lib/dockerfile-builder.js";import{envLabel as De,envRunArgs as Be,envWarnings as He,validateEnvConfig as ze}from"../lib/env.js";import{readWebRange as Fe}from"../lib/global-config.js";import{buildPnpmStoreMountArgs as Ue}from"../lib/pnpm-store.js";import{assertHostPortsAvailable as Ve,formatPortNotice as je,formatWebRange as Ge,portEnvArgs as Ze,portPublishArgs as Ke,portsLabel as he,validatePortsConfig as Ye}from"../lib/ports.js";import{containerSessionCount as qe,loginShellExecArgs as Je}from"../lib/sessions.js";import{buildShadowMountArgs as Qe,ensureShadowsInSync as Xe,expandShadowPatterns as et}from"../lib/shadows.js";import{buildSkillsMountArgs as tt}from"../lib/skills.js";import{buildWebpaneMountArgs as nt,buildWebtermStateMountArgs as ot,isWebKey as rt,readWebAgent as it,readWebKey as ee,resolveWebPort as st,setWebDefaultCwd as at,startWebtermAndVerify as ct,webInterfaceAnswers as lt,webInterfaceUrl as ge,webPortUsable as be,webSessionInfo as dt}from"../lib/webterm.js";import{readActiveProfile as ut,readGitMode as ft,writeActiveProfile as pt}from"../lib/workspace-identity.js";function mt(n,t){return t===n?X:`${X}/${Ce(n,t)}`}async function ht(n,t){const s=Object.keys(t);if(s.length<=1)return s[0]??re;const o=ut(n.workspaceId)??re,$=await Ee({message:"Profile:",options:s.map(i=>{const f=t[i]?.description,g=i===o,m=f&&g?`${f} \xB7 current`:f??(g?"current":void 0),d={value:i,label:i};return m&&(d.hint=m),d}),initialValue:o});W($)&&(G("Cancelled."),process.exit(0));const l=$;return l!==o&&pt(n.workspaceId,l),l}function gt(n){const t=`{{.State.Status}}|{{index .Config.Labels "${de}"}}|{{index .Config.Labels "${ce}"}}|{{index .Config.Labels "${le}"}}|{{index .Config.Labels "${se}"}}|{{index .Config.Labels "${ue}"}}|{{index .Config.Labels "${ae}"}}|{{index .Config.Labels "${ie}"}}`,s=A("docker",["inspect","--format",t,n],{encoding:"utf8",stdio:"pipe"});if(s.status!==0)return null;const o=y=>y==="<no value>"?"":y,[$="",l="",i="",f="",g="",m="",d="",S=""]=s.stdout.trim().split("|");return{status:$,shadowLabel:o(l),profileLabel:o(i),runtimeEnvLabel:o(f),gitModeLabel:o(g),timezoneLabel:o(m),portsLabel:o(d),envLabel:o(S)}}function we(n){return n.length===0?"":[...n].sort().join(",")}const bt=/^[A-Za-z][A-Za-z0-9_+-]*(\/[A-Za-z0-9_+-]+){1,2}$/;function wt(){let n;try{n=Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}return typeof n!="string"||!bt.test(n)||n==="UTC"||n==="Etc/UTC"?null:n}function Ae(){const n=Object.entries(fe);if(n.length===0)return"";const t=n.map(([s,o])=>`${s}=${o}`).sort().join(",");return ve("sha256").update(t).digest("hex").slice(0,12)}function te(n){A("docker",["stop",n],{stdio:"pipe"}),A("docker",["rm",n],{stdio:"pipe"})}function At(n,t){return A("docker",["exec","-u","root",...t?[]:["-i","-t"],n,"node",We,"--summary"],{stdio:t?"pipe":"inherit"}).status===0}async function $e(n){const{containerName:t,workspaceRoot:s,cacheDir:o,templatesDir:$,activeProfile:l,profileHook:i,expandedShadows:f,envConfig:g,hasGit:m,gitMode:d,shadowPatterns:S,workspaceName:y,portMappings:_,webPort:h,noCache:a,quiet:c=!1}=n,D=c?"pipe":["ignore","ignore","inherit"],B=he(_),Z=De(g),H=Qe(o,f),z=Se(m,S,d),I=Be(g),F=ye(o),T=tt(o),K=Ue(o),Y=ot(o),U=nt(o),P=["-v",`${s}:${X}`,...H,...F,...T,...K,...Y,...U],C=wt(),q=["--label",`${Ie}=true`,"--label",`${de}=${we(f)}`,"--label",`${ce}=${l}`,"--label",`${le}=${Ae()}`,"--label",`${se}=${d}`,"--label",`${ue}=${C??""}`],R=[...Object.entries(fe).flatMap(([p,b])=>["-e",`${p}=${b}`]),...C===null?[]:["-e",`TZ=${C}`],"-e",`CHAMBA_WORKSPACE=${y}`,"-e",`CHAMBA_GIT_MODE=${d}`],u=gt(t);let N=u?.status??null;if(u!==null){const p=we(f),b=u.shadowLabel!==p,w=u.profileLabel!==l,J=u.runtimeEnvLabel!==Ae(),V=u.gitModeLabel!==d,Q=u.timezoneLabel!==(C??""),M=u.portsLabel!==B,L=u.envLabel!==Z;if(b||w||J||V||Q||M||L){let v;w?v=`Profile changed (${u.profileLabel} -> ${l})`:b?v="Shadow paths changed":V?v=`Git mode changed (${u.gitModeLabel||"<unset>"} -> ${d})`:Q?v=`Timezone changed (${u.timezoneLabel||"<unset>"} -> ${C??"<unset>"})`:M?v="Ports changed":L?v="Environment variables changed":v="Runtime environment updated";const ne=!c&&u.status==="running";if(ne){r.warn(`${v} - applying this needs to recreate the running container, which stops any open sessions.
2
- Agent memory, settings, and workspace data are preserved.`);const oe=await O({message:"Recreate the container now?",initialValue:!0});(W(oe)||!oe)&&(G("Session cancelled - container left running."),process.exit(0))}te(t),N=null,w&&A("docker",["rmi",t],{stdio:"pipe"}),!c&&!ne&&r.info(`${v} - ${w?"rebuilding":"recreating container"}...`)}}Xe(o,f,s),Pe(o,z);let e=h;const k=async()=>{const p=me(j($,"Dockerfile"),i),b=await Te(p,$,t,a,c);b.status!==0&&(c||E("Failed to build container image."),process.exit(b.status));let w=_;h!==void 0&&!await be(h,t)&&(c||r.warn(`Web interface: host port ${h} was taken before the container could start - continuing without it.`),w=_.filter(L=>L.host!==h),e=void 0);try{await Ve(w,e)}catch(L){c||E(L instanceof Error?L.message:String(L)),process.exit(1)}c||r.info("Starting dev container...");const J=e!==void 0?["-e",`CHAMBA_WEB_URL=${ge(e,null)}`]:[],V=["run","-d","--name",t,...P,...I,...R,...J,...Ze(w),...Ke(w),"--security-opt","no-new-privileges:true",...q,"--label",`${ae}=${he(w)}`,"--label",`${ie}=${Z}`,t,...Me],M=A("docker",V,{stdio:c?"pipe":["ignore","ignore","pipe"]});M.status!==0&&(c||(process.stderr.write(M.stderr?.toString()??""),E("Failed to start dev container.")),process.exit(M.status??1))},x=p=>({status:p,webPort:e??null});if(N===null)return await k(),x("created");if(N==="exited"){c||r.info("Resuming dev container...");const p=A("docker",["start",t],{stdio:D});if(p.status!==0){c&&process.exit(p.status??1),r.warn(`This container could not start - a host path it was created against has likely moved or been removed
3
- (for example the workspace directory was renamed).`);const b=await O({message:"Recreate it now? Your agent memory, settings, and workspace data are preserved.",initialValue:!0});return(W(b)||!b)&&(E("Failed to start dev container."),process.exit(p.status??1)),te(t),await k(),x("created")}return x("resumed")}else return x("connected")}async function ke(n,t,s,o){await ct(n,t,s,o)||r.warn(`Web interface: the server did not come up on port ${s}, so the URL in the greeting will not answer.
4
- Run \`webterm ${t}\` in the container to see why, or just run \`${t}\` in the terminal.`)}async function Bt(n,t,s){const o=s?.mode??"web",$=process.cwd(),l=t.workspaceRoot,i=t.containerName,f=t.workspaceDir,g=j(n,"templates"),m=xe(l);m||(r.error("chamba.yaml not found or invalid."),process.exit(1));let d;try{d=Ye(m.ports??[])}catch(e){r.error(e instanceof Error?e.message:String(e)),process.exit(1)}const S=Fe(),y=new Set(d.map(e=>e.host)),_=async e=>!y.has(e)&&await be(e,i),h=await st(t.workspaceId,S,_);let a=null;h.ok?(h.movedFrom!==void 0&&r.info(`Web interface: port ${h.movedFrom} was not available - moved to ${h.port} and kept there.`),a=h.port,d.push({host:a,container:pe})):h.reason==="exhausted"?r.warn(`Web interface: no free port left in ${Ge(S)} - web interface skipped this session.
5
- Widen the range in Settings > Ports.`):r.warn(`Web interface: the port assignment could not be recorded for this workspace - web interface skipped this session.
6
- Check that the workspace cache dir is writable.`);const c=mt(l,$),D=m.profiles??{},B=await ht(t,D),H=D[B]?.dockerfile_hook,z=m.shadow_paths??[],{paths:I,skippedTracked:F}=et(z,l);I.length>0&&r.info(`Shadow paths active: ${I.join(", ")} (Settings > Shadow paths)`),F.length>0&&r.warn(`Skipped ${F.length} shadow path(s) tracked by git`);let T;try{T=ze(m.env,l)}catch(e){r.error(e instanceof Error?e.message:String(e)),process.exit(1)}for(const e of He(T))r.warn(e);const K=Le(j(l,".git")),Y=ft(t.workspaceId)??_e.local,U={containerName:i,workspaceRoot:l,cacheDir:f,templatesDir:g,activeProfile:B,profileHook:H,expandedShadows:I,envConfig:T,hasGit:K,gitMode:Y,shadowPatterns:z,workspaceName:t.workspaceId,portMappings:d,...a!==null&&{webPort:a},...s?.noCache!==void 0&&{noCache:s.noCache}};let P=await $e(U);a=P.webPort;const C=me(j(g,"Dockerfile"),H),q=Ne(C,g);let R=Oe(i,q);if(R){r.warn(`This container's image is out of date.
7
- Please rebuild to update - this will not affect agent memory, settings, or your data.`);const e=await O({message:"Rebuild now? (Recommended)",initialValue:!0});W(e)&&(G("Session cancelled."),process.exit(0)),e&&(te(i),A("docker",["rmi",i],{stdio:"pipe"}),P=await $e(U),a=P.webPort,R=!1)}for(const e of d)e.container!==pe&&r.info(je(e));if(!At(i,R))if(R){const e=await O({message:"Startup checks failed (likely due to outdated image). Connect anyway?",initialValue:!0});(!e||W(e))&&(G("Session cancelled."),process.exit(0))}else E("Startup checks failed."),process.exit(1);const u=it(f);if(P.status!=="connected"?a!==null&&await ke(i,u,a,c):a!==null&&(await lt(a)?await at(a,ee(i),c):await ke(i,u,a,c)),o==="web"){const e=a===null?null:ee(i);if(a!==null&&rt(e)){const k=ge(a,e);Re(k)?r.info(`Web interface: opened in your browser
8
- ${k}`):r.info(`Web interface: open this in your browser
9
- ${k}`),E("The container and the interface keep running. Run chamba again for the menu.")}else r.warn(`Web interface: there is nothing to open (see above).
10
- Pick "Container shell" from the menu to work in the terminal instead.`),E("The container is running.");return}const N=A("docker",Je(c,i),{stdio:"inherit"});if(qe(i)===0){const e=(a===null?null:await dt(a,ee(i)))?.sessions??0;if(e>0){const x=e===1?"1 agent session is":`${e} agent sessions are`,p=e===1?"it":"them";r.warn(`Web interface: ${x} open in the browser - stopping the container ends ${p}.`)}const k=await O({message:"Last session to this container closed. Stop it? (resumes fast)",initialValue:e===0});!W(k)&&k&&(r.info("Stopping container..."),A("docker",["stop",i],{stdio:"pipe"}),r.info("Container stopped."))}process.stdout.write(`
11
- `),process.exit(N.status??0)}export{wt as hostTimezone,mt as resolveWorkdir,Bt as run,$e as startContainer};
1
+ import{spawnSync as k}from"node:child_process";import{createHash as $e}from"node:crypto";import{existsSync as ve}from"node:fs";import{join as j,relative as Le}from"node:path";import{cancel as G,confirm as T,isCancel as y,log as i,outro as C,select as ye}from"@clack/prompts";import{buildAgentContextDocs as Ce,buildAgentMountArgs as Ee,injectAgentContext as Pe}from"../lib/agent-context.js";import{ensureAwakeHolder as Re,isAwakeSupported as xe}from"../lib/awake.js";import{openInBrowser as Me}from"../lib/browser.js";import{readChambaYaml as We}from"../lib/chamba-yaml.js";import{CONTAINER_KEEP_ALIVE as Te,CONTAINER_STARTUP as _e,CONTAINER_WORKSPACE as X,DEFAULT_PROFILE as ie,GIT_MODE as Ie,LABEL_ENV as se,LABEL_GIT_MODE as re,LABEL_MANAGED as Ne,LABEL_PORTS as ae,LABEL_PROFILE as ce,LABEL_RUNTIME_ENV as le,LABEL_SHADOWS as de,LABEL_TIMEZONE as ue,RUNTIME_ENV as fe,WEB_CONTAINER_PORT as pe}from"../lib/constants.js";import{buildDockerfile as me,buildImageWithTempfile as Oe,computeBuildHash as De,isImageStale as Be}from"../lib/dockerfile-builder.js";import{envLabel as He,envRunArgs as Ve,envWarnings as ze,validateEnvConfig as Fe}from"../lib/env.js";import{readStayAwake as Ue,readWebRange as je,writeStayAwake as Ge}from"../lib/global-config.js";import{buildPnpmStoreMountArgs as Ze}from"../lib/pnpm-store.js";import{assertHostPortsAvailable as Ke,formatPortNotice as Ye,formatWebRange as qe,portEnvArgs as Je,portPublishArgs as Qe,portsLabel as he,validatePortsConfig as Xe}from"../lib/ports.js";import{containerSessionCount as et,loginShellExecArgs as tt}from"../lib/sessions.js";import{buildShadowMountArgs as nt,ensureShadowsInSync as ot,expandShadowPatterns as it}from"../lib/shadows.js";import{buildSkillsMountArgs as st}from"../lib/skills.js";import{buildWebpaneMountArgs as rt,buildWebtermStateMountArgs as at,isWebKey as ct,readWebAgent as lt,readWebKey as ee,resolveWebPort as dt,setWebDefaultCwd as ut,startWebtermAndVerify as ft,webInterfaceAnswers as pt,webInterfaceUrl as we,webPortUsable as ge,webSessionInfo as mt}from"../lib/webterm.js";import{readActiveProfile as ht,readGitMode as wt,writeActiveProfile as gt}from"../lib/workspace-identity.js";function bt(t,n){return n===t?X:`${X}/${Le(t,n)}`}async function kt(t,n){const r=Object.keys(n);if(r.length<=1)return r[0]??ie;const o=ht(t.workspaceId)??ie,A=await ye({message:"Profile:",options:r.map(s=>{const f=n[s]?.description,w=s===o,m=f&&w?`${f} \xB7 current`:f??(w?"current":void 0),d={value:s,label:s};return m&&(d.hint=m),d}),initialValue:o});y(A)&&(G("Cancelled."),process.exit(0));const l=A;return l!==o&&gt(t.workspaceId,l),l}function At(t){const n=`{{.State.Status}}|{{index .Config.Labels "${de}"}}|{{index .Config.Labels "${ce}"}}|{{index .Config.Labels "${le}"}}|{{index .Config.Labels "${re}"}}|{{index .Config.Labels "${ue}"}}|{{index .Config.Labels "${ae}"}}|{{index .Config.Labels "${se}"}}`,r=k("docker",["inspect","--format",n,t],{encoding:"utf8",stdio:"pipe"});if(r.status!==0)return null;const o=P=>P==="<no value>"?"":P,[A="",l="",s="",f="",w="",m="",d="",E=""]=r.stdout.trim().split("|");return{status:A,shadowLabel:o(l),profileLabel:o(s),runtimeEnvLabel:o(f),gitModeLabel:o(w),timezoneLabel:o(m),portsLabel:o(d),envLabel:o(E)}}function be(t){return t.length===0?"":[...t].sort().join(",")}const St=/^[A-Za-z][A-Za-z0-9_+-]*(\/[A-Za-z0-9_+-]+){1,2}$/;function $t(){let t;try{t=Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}return typeof t!="string"||!St.test(t)||t==="UTC"||t==="Etc/UTC"?null:t}function ke(){const t=Object.entries(fe);if(t.length===0)return"";const n=t.map(([r,o])=>`${r}=${o}`).sort().join(",");return $e("sha256").update(n).digest("hex").slice(0,12)}function te(t){k("docker",["stop",t],{stdio:"pipe"}),k("docker",["rm",t],{stdio:"pipe"})}function vt(t,n){return k("docker",["exec","-u","root",...n?[]:["-i","-t"],t,"node",_e,"--summary"],{stdio:n?"pipe":"inherit"}).status===0}async function Ae(t){const{containerName:n,workspaceRoot:r,cacheDir:o,templatesDir:A,activeProfile:l,profileHook:s,expandedShadows:f,envConfig:w,hasGit:m,gitMode:d,shadowPatterns:E,workspaceName:P,portMappings:_,webPort:h,noCache:a,quiet:c=!1}=t,D=c?"pipe":["ignore","ignore","inherit"],B=he(_),Z=He(w),H=nt(o,f),V=Ce(m,E,d),I=Ve(w),z=Ee(o),N=st(o),K=Ze(o),Y=at(o),F=rt(o),R=["-v",`${r}:${X}`,...H,...z,...N,...K,...Y,...F],L=$t(),q=["--label",`${Ne}=true`,"--label",`${de}=${be(f)}`,"--label",`${ce}=${l}`,"--label",`${le}=${ke()}`,"--label",`${re}=${d}`,"--label",`${ue}=${L??""}`],x=[...Object.entries(fe).flatMap(([p,g])=>["-e",`${p}=${g}`]),...L===null?[]:["-e",`TZ=${L}`],"-e",`CHAMBA_WORKSPACE=${P}`,"-e",`CHAMBA_GIT_MODE=${d}`],u=At(n);let O=u?.status??null;if(u!==null){const p=be(f),g=u.shadowLabel!==p,b=u.profileLabel!==l,J=u.runtimeEnvLabel!==ke(),U=u.gitModeLabel!==d,Q=u.timezoneLabel!==(L??""),W=u.portsLabel!==B,v=u.envLabel!==Z;if(g||b||J||U||Q||W||v){let $;b?$=`Profile changed (${u.profileLabel} -> ${l})`:g?$="Shadow paths changed":U?$=`Git mode changed (${u.gitModeLabel||"<unset>"} -> ${d})`:Q?$=`Timezone changed (${u.timezoneLabel||"<unset>"} -> ${L??"<unset>"})`:W?$="Ports changed":v?$="Environment variables changed":$="Runtime environment updated";const ne=!c&&u.status==="running";if(ne){i.warn(`${$} - applying this needs to recreate the running container, which stops any open sessions.
2
+ Agent memory, settings, and workspace data are preserved.`);const oe=await T({message:"Recreate the container now?",initialValue:!0});(y(oe)||!oe)&&(G("Session cancelled - container left running."),process.exit(0))}te(n),O=null,b&&k("docker",["rmi",n],{stdio:"pipe"}),!c&&!ne&&i.info(`${$} - ${b?"rebuilding":"recreating container"}...`)}}ot(o,f,r),Pe(o,V);let e=h;const S=async()=>{const p=me(j(A,"Dockerfile"),s),g=await Oe(p,A,n,a,c);g.status!==0&&(c||C("Failed to build container image."),process.exit(g.status));let b=_;h!==void 0&&!await ge(h,n)&&(c||i.warn(`Web interface: host port ${h} was taken before the container could start - continuing without it.`),b=_.filter(v=>v.host!==h),e=void 0);try{await Ke(b,e)}catch(v){c||C(v instanceof Error?v.message:String(v)),process.exit(1)}c||i.info("Starting dev container...");const J=e!==void 0?["-e",`CHAMBA_WEB_URL=${we(e,null)}`]:[],U=["run","-d","--name",n,...R,...I,...x,...J,...Je(b),...Qe(b),"--security-opt","no-new-privileges:true",...q,"--label",`${ae}=${he(b)}`,"--label",`${se}=${Z}`,n,...Te],W=k("docker",U,{stdio:c?"pipe":["ignore","ignore","pipe"]});W.status!==0&&(c||(process.stderr.write(W.stderr?.toString()??""),C("Failed to start dev container.")),process.exit(W.status??1))},M=p=>({status:p,webPort:e??null});if(O===null)return await S(),M("created");if(O==="exited"){c||i.info("Resuming dev container...");const p=k("docker",["start",n],{stdio:D});if(p.status!==0){c&&process.exit(p.status??1),i.warn(`This container could not start - a host path it was created against has likely moved or been removed
3
+ (for example the workspace directory was renamed).`);const g=await T({message:"Recreate it now? Your agent memory, settings, and workspace data are preserved.",initialValue:!0});return(y(g)||!g)&&(C("Failed to start dev container."),process.exit(p.status??1)),te(n),await S(),M("created")}return M("resumed")}else return M("connected")}async function Se(t,n,r,o){await ft(t,n,r,o)||i.warn(`Web interface: the server did not come up on port ${r}, so the URL in the greeting will not answer.
4
+ Run \`webterm ${n}\` in the container to see why, or just run \`${n}\` in the terminal.`)}async function Lt(){if(Ue()!==null||!xe())return;const t=await T({message:"Keep this machine awake while a chamba container is up?",initialValue:!0});y(t)||(Ge(t?"on":"off"),i.info(t?"Stay awake is on. Change it under Settings > Stay awake.":"Stay awake is off. Turn it on under Settings > Stay awake."))}async function yt(t,n){const r=await Re({cacheDir:t,container:n});r==="unsupported"?i.info("Stay awake: not available on this platform. The session goes on, and the machine sleeps as it always did."):r==="failed"&&i.warn("Stay awake: the hold did not start, so this machine may sleep while the container is up.")}async function Gt(t,n,r){const o=r?.mode??"web",A=process.cwd(),l=n.workspaceRoot,s=n.containerName,f=n.workspaceDir,w=j(t,"templates"),m=We(l);m||(i.error("chamba.yaml not found or invalid."),process.exit(1));let d;try{d=Xe(m.ports??[])}catch(e){i.error(e instanceof Error?e.message:String(e)),process.exit(1)}const E=je(),P=new Set(d.map(e=>e.host)),_=async e=>!P.has(e)&&await ge(e,s),h=await dt(n.workspaceId,E,_);let a=null;h.ok?(h.movedFrom!==void 0&&i.info(`Web interface: port ${h.movedFrom} was not available - moved to ${h.port} and kept there.`),a=h.port,d.push({host:a,container:pe})):h.reason==="exhausted"?i.warn(`Web interface: no free port left in ${qe(E)} - web interface skipped this session.
5
+ Widen the range in Settings > Ports.`):i.warn(`Web interface: the port assignment could not be recorded for this workspace - web interface skipped this session.
6
+ Check that the workspace cache dir is writable.`);const c=bt(l,A),D=m.profiles??{},B=await kt(n,D),H=D[B]?.dockerfile_hook,V=m.shadow_paths??[],{paths:I,skippedTracked:z}=it(V,l);I.length>0&&i.info(`Shadow paths active: ${I.join(", ")} (Settings > Shadow paths)`),z.length>0&&i.warn(`Skipped ${z.length} shadow path(s) tracked by git`);let N;try{N=Fe(m.env,l)}catch(e){i.error(e instanceof Error?e.message:String(e)),process.exit(1)}for(const e of ze(N))i.warn(e);const K=ve(j(l,".git")),Y=wt(n.workspaceId)??Ie.local,F={containerName:s,workspaceRoot:l,cacheDir:f,templatesDir:w,activeProfile:B,profileHook:H,expandedShadows:I,envConfig:N,hasGit:K,gitMode:Y,shadowPatterns:V,workspaceName:n.workspaceId,portMappings:d,...a!==null&&{webPort:a},...r?.noCache!==void 0&&{noCache:r.noCache}};let R=await Ae(F);a=R.webPort;const L=me(j(w,"Dockerfile"),H),q=De(L,w);let x=Be(s,q);if(x){i.warn(`This container's image is out of date.
7
+ Please rebuild to update - this will not affect agent memory, settings, or your data.`);const e=await T({message:"Rebuild now? (Recommended)",initialValue:!0});y(e)&&(G("Session cancelled."),process.exit(0)),e&&(te(s),k("docker",["rmi",s],{stdio:"pipe"}),R=await Ae(F),a=R.webPort,x=!1)}for(const e of d)e.container!==pe&&i.info(Ye(e));if(!vt(s,x))if(x){const e=await T({message:"Startup checks failed (likely due to outdated image). Connect anyway?",initialValue:!0});(!e||y(e))&&(G("Session cancelled."),process.exit(0))}else C("Startup checks failed."),process.exit(1);await Lt(),await yt(f,s);const u=lt(f);if(R.status!=="connected"?a!==null&&await Se(s,u,a,c):a!==null&&(await pt(a)?await ut(a,ee(s),c):await Se(s,u,a,c)),o==="web"){const e=a===null?null:ee(s);if(a!==null&&ct(e)){const S=we(a,e);Me(S)?i.info(`Web interface: opened in your browser
8
+ ${S}`):i.info(`Web interface: open this in your browser
9
+ ${S}`),C("The container and the interface keep running. Run chamba again for the menu.")}else i.warn(`Web interface: there is nothing to open (see above).
10
+ Pick "Container shell" from the menu to work in the terminal instead.`),C("The container is running.");return}const O=k("docker",tt(c,s),{stdio:"inherit"});if(et(s)===0){const e=(a===null?null:await mt(a,ee(s)))?.sessions??0;if(e>0){const M=e===1?"1 agent session is":`${e} agent sessions are`,p=e===1?"it":"them";i.warn(`Web interface: ${M} open in the browser - stopping the container ends ${p}.`)}const S=await T({message:"Last session to this container closed. Stop it? (resumes fast)",initialValue:e===0});!y(S)&&S&&(i.info("Stopping container..."),k("docker",["stop",s],{stdio:"pipe"}),i.info("Container stopped."))}process.stdout.write(`
11
+ `),process.exit(O.status??0)}export{$t as hostTimezone,bt as resolveWorkdir,Gt as run,Ae as startContainer};
@@ -1 +1 @@
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
+ import{existsSync as y}from"node:fs";import{join as S}from"node:path";import{styleText as e}from"node:util";import{cancel as $,isCancel as A,log as P,select as j}from"@clack/prompts";import{isAwakeSupported as x}from"../lib/awake.js";import{readChambaYaml as C}from"../lib/chamba-yaml.js";import{DEFAULT_PROFILE as r}from"../lib/constants.js";import{readStayAwake as I}from"../lib/global-config.js";import{readActiveProfile as R}from"../lib/workspace-identity.js";async function N(u){const{ctx:o,activeCount:l,workspaceRunning:s,version:m}=u,d=y(S(o.workspaceRoot,".git"));let t=[];try{t=Object.keys(C(o.workspaceRoot)?.profiles??{})}catch{}const i=R(o.workspaceId),f=t.includes(r)?r:t[0]??r,b=i&&t.includes(i)?i:f,w=e(["bold","gray"]," \xB7 "),g=s?`${e("bold",`${o.workspaceId} container`)} ${e(["bold","green"],"up")}`:e("bold",o.workspaceId),c=[e("bold",`chamba v${m}`),g];t.length>1&&c.push(e("bold",`profile: ${b}`));const k=c.join(w),n=[],a=l-(s?1:0);a>0&&n.push(`${a} other container${a===1?"":"s"} up`),d||n.push("no git");const p=n.length>0?[e("gray",n.join(" \xB7 "))]:[];I()==="on"&&x()&&l>0&&p.push(e("gray","Stay awake is on: with the lid open, this computer does not sleep while a container is running, so agents keep working.")),P.message([k,...p]);const v=[{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"}],h=await j({message:"Menu:",options:v});return A(h)?($(),"quit"):h}export{N as run};
@@ -1,15 +1,18 @@
1
- import{spawnSync as l}from"node:child_process";import{relative as I}from"node:path";import{cancel as v,confirm as h,isCancel as c,log as n,multiselect as A,note as w,outro as T,path as W,select as g,text as R}from"@clack/prompts";import{buildDefaultChambaYaml as U,readChambaYaml as b,writeChambaYaml as y}from"../lib/chamba-yaml.js";import{GIT_MODE as d}from"../lib/constants.js";import{readWebRange as S,writeWebRange as O}from"../lib/global-config.js";import{formatWebRange as p,PORT_MAX as Y,PORT_MIN as E,parseWebRange as $}from"../lib/ports.js";import{countPatternHits as P}from"../lib/shadows.js";import{readWebKey as L,reassignOutOfRangeWebPorts as D,webInterfaceUrl as _}from"../lib/webterm.js";import{readGitMode as C,readWebPort as V,writeGitMode as j}from"../lib/workspace-identity.js";async function B(e){const a=b(e.workspaceRoot);if(!a){n.error("chamba.yaml not found or invalid.");return}const t=a.shadow_paths??[];if(t.length>0){const i=t.map(r=>{const m=P(r,e.workspaceRoot);return` ${r} (${m} ${m===1?"match":"matches"})`});w(i.join(`
1
+ import{spawnSync as l}from"node:child_process";import{relative as I}from"node:path";import{cancel as v,confirm as f,isCancel as i,log as n,multiselect as O,note as m,outro as W,path as D,select as w,text as S}from"@clack/prompts";import{endAwakeHolder as U,ensureAwakeHolder as E,isAwakeSupported as R,otherHeldWorkspaces as L}from"../lib/awake.js";import{buildDefaultChambaYaml as Y,readChambaYaml as b,writeChambaYaml as y}from"../lib/chamba-yaml.js";import{GIT_MODE as d}from"../lib/constants.js";import{readStayAwake as $,readWebRange as P,writeStayAwake as H,writeWebRange as V}from"../lib/global-config.js";import{formatWebRange as p,PORT_MAX as j,PORT_MIN as B,parseWebRange as _}from"../lib/ports.js";import{countPatternHits as A}from"../lib/shadows.js";import{readWebKey as K,reassignOutOfRangeWebPorts as q,webInterfaceUrl as T}from"../lib/webterm.js";import{readGitMode as C,readWebPort as z,writeGitMode as X}from"../lib/workspace-identity.js";async function F(e){const a=b(e.workspaceRoot);if(!a){n.error("chamba.yaml not found or invalid.");return}const t=a.shadow_paths??[];if(t.length>0){const c=t.map(r=>{const h=A(r,e.workspaceRoot);return` ${r} (${h} ${h===1?"match":"matches"})`});m(c.join(`
2
2
  `),"Shadow patterns")}else n.info("No shadow patterns configured.");n.message(`Shadow patterns block the agent from seeing matching host paths -
3
3
  the container gets an empty, isolated copy instead.
4
4
  Supports gitignore-style patterns (e.g. node_modules, .env*).
5
- Git-tracked paths are skipped to avoid worktree diversions.`);const o=[{value:"add",label:"Add pattern or path"}];t.length>0&&o.push({value:"remove",label:"Remove patterns"}),o.push({value:"back",label:"\u2190 Back"});const s=await g({message:"Shadow paths:",options:o});c(s)||s==="back"||(s==="add"?await K(e):s==="remove"&&await q(e))}async function K(e){const a=b(e.workspaceRoot);if(!a)return;const t=[...a.shadow_paths??[]],o=await g({message:"Add:",options:[{value:"pattern",label:"Pattern",hint:"gitignore-style (e.g. .env*, *.log)"},{value:"path",label:"Specific path",hint:"pick a file or directory"}]});if(!c(o)){if(o==="pattern"){const s=await R({message:"Pattern:",placeholder:"e.g. .env* or node_modules",validate:m=>{const u=(m??"").trim();if(u.length===0)return"Pattern cannot be empty";if(t.includes(u))return"Pattern already exists"}});if(c(s))return;const i=s.trim(),r=P(i,e.workspaceRoot);t.push(i),n.info(`Added: ${i} (${r} ${r===1?"match":"matches"})`)}else{const s=await W({message:"Path to shadow:",root:e.workspaceRoot,directory:!0});if(c(s))return;const i=s.trim(),r=I(e.workspaceRoot,i);if(!r||r.startsWith("..")){n.warn("Path must be inside the workspace directory. Skipped.");return}if(t.includes(r)){n.warn(`"${r}" is already in shadow patterns. Skipped.`);return}t.push(r),n.info(`Added: ${r}`)}a.shadow_paths=t,y(e.workspaceRoot,a),await f(e)}}async function q(e){const a=b(e.workspaceRoot);if(!a?.shadow_paths?.length)return;const t=await A({message:"Select patterns to remove: (space to toggle, enter to confirm)",options:a.shadow_paths.map(s=>({value:s,label:s}))});if(c(t))return;const o=new Set(t);a.shadow_paths=a.shadow_paths.filter(s=>!o.has(s)),y(e.workspaceRoot,a),n.success(`Removed ${o.size} pattern(s).`),await f(e)}async function z(e){const a=C(e.workspaceId)??d.local;w(`Local - local mutations allowed; remote blocked
5
+ Git-tracked paths are skipped to avoid worktree diversions.`);const o=[{value:"add",label:"Add pattern or path"}];t.length>0&&o.push({value:"remove",label:"Remove patterns"}),o.push({value:"back",label:"\u2190 Back"});const s=await w({message:"Shadow paths:",options:o});i(s)||s==="back"||(s==="add"?await J(e):s==="remove"&&await N(e))}async function J(e){const a=b(e.workspaceRoot);if(!a)return;const t=[...a.shadow_paths??[]],o=await w({message:"Add:",options:[{value:"pattern",label:"Pattern",hint:"gitignore-style (e.g. .env*, *.log)"},{value:"path",label:"Specific path",hint:"pick a file or directory"}]});if(!i(o)){if(o==="pattern"){const s=await S({message:"Pattern:",placeholder:"e.g. .env* or node_modules",validate:h=>{const u=(h??"").trim();if(u.length===0)return"Pattern cannot be empty";if(t.includes(u))return"Pattern already exists"}});if(i(s))return;const c=s.trim(),r=A(c,e.workspaceRoot);t.push(c),n.info(`Added: ${c} (${r} ${r===1?"match":"matches"})`)}else{const s=await D({message:"Path to shadow:",root:e.workspaceRoot,directory:!0});if(i(s))return;const c=s.trim(),r=I(e.workspaceRoot,c);if(!r||r.startsWith("..")){n.warn("Path must be inside the workspace directory. Skipped.");return}if(t.includes(r)){n.warn(`"${r}" is already in shadow patterns. Skipped.`);return}t.push(r),n.info(`Added: ${r}`)}a.shadow_paths=t,y(e.workspaceRoot,a),await g(e)}}async function N(e){const a=b(e.workspaceRoot);if(!a?.shadow_paths?.length)return;const t=await O({message:"Select patterns to remove: (space to toggle, enter to confirm)",options:a.shadow_paths.map(s=>({value:s,label:s}))});if(i(t))return;const o=new Set(t);a.shadow_paths=a.shadow_paths.filter(s=>!o.has(s)),y(e.workspaceRoot,a),n.success(`Removed ${o.size} pattern(s).`),await g(e)}async function Q(e){const a=C(e.workspaceId)??d.local;m(`Local - local mutations allowed; remote blocked
6
6
  Strict - read-only; mutations and remote blocked
7
- Unrestricted - no chamba-enforced restrictions`,"Git mode");const t=await g({message:"Git mode:",options:[{value:d.local,label:"Local",hint:a===d.local?"current \xB7 default":"default"},{value:d.strict,label:"Strict",...a===d.strict?{hint:"current"}:{}},{value:d.unrestricted,label:"Unrestricted",...a===d.unrestricted?{hint:"current"}:{}}],initialValue:a});if(!c(t)&&t!==a){if(t===d.unrestricted){const o=await h({message:"Unrestricted mode disables chamba's built-in git restrictions (allows remote push/pull/fetch). Continue?",initialValue:!1});if(c(o)||!o){n.info("Git mode unchanged.");return}}j(e.workspaceId,t),n.success(`Git mode set to ${t}.`),await f(e)}}async function H(e){const a=S(),t=V(e.workspaceId),o=t===null?null:L(e.containerName),s=t===null?"no port assigned yet":o!==null?_(t,o):`${_(t,null)} (the key comes with the interface when it starts)`;w(`web interface range: ${p(a)} (all workspaces)
7
+ Unrestricted - no chamba-enforced restrictions`,"Git mode");const t=await w({message:"Git mode:",options:[{value:d.local,label:"Local",hint:a===d.local?"current \xB7 default":"default"},{value:d.strict,label:"Strict",...a===d.strict?{hint:"current"}:{}},{value:d.unrestricted,label:"Unrestricted",...a===d.unrestricted?{hint:"current"}:{}}],initialValue:a});if(!i(t)&&t!==a){if(t===d.unrestricted){const o=await f({message:"Unrestricted mode disables chamba's built-in git restrictions (allows remote push/pull/fetch). Continue?",initialValue:!1});if(i(o)||!o){n.info("Git mode unchanged.");return}}X(e.workspaceId,t),n.success(`Git mode set to ${t}.`),await g(e)}}async function Z(e){if(!R()){m("chamba holds a machine awake with `caffeinate` on macOS and `systemd-inhibit` on Linux.\nNeither is available here, so there is nothing to turn on.","Stay awake");return}const a=$();m(`On - while a chamba container is up, this computer does not sleep, so agents keep working
8
+ Off - the computer sleeps as it always did
9
+
10
+ The display sleeps either way, and a closed lid still sends the computer to sleep.`,"Stay awake");const t=await w({message:"Stay awake:",options:[{value:"on",label:"On",...a==="on"?{hint:"current"}:{}},{value:"off",label:"Off",...a==="off"?{hint:"current"}:{}}],initialValue:a??"on"});if(i(t))return;if(H(t),t==="off"){await U({cacheDir:e.workspaceDir,container:e.containerName}),n.success("Stay awake is off. This computer sleeps as it always did.");const s=L(e.workspaceId);s.length>0&&n.info(`Still held for ${s.join(", ")}. Each of those ends with its own container.`);return}const o=await E({cacheDir:e.workspaceDir,container:e.containerName});o==="failed"?n.warn("Stay awake is on, and the hold did not start. This machine may sleep while the container is up."):o==="no-container"?n.success("Stay awake is on. The hold starts with this workspace's container."):n.success("Stay awake is on. This computer does not sleep while a container is up.")}async function x(e){const a=P(),t=z(e.workspaceId),o=t===null?null:K(e.containerName),s=t===null?"no port assigned yet":o!==null?T(t,o):`${T(t,null)} (the key comes with the interface when it starts)`;m(`web interface range: ${p(a)} (all workspaces)
8
11
  this workspace: ${s}`,"Ports"),n.message(`The host port range the web interface draws from.
9
12
  Each workspace keeps one sticky port from it, so its URL never changes.
10
13
  Loopback-only, never reachable off this machine, and the URL carries a key
11
14
  that is minted fresh every time the interface starts.
12
- Ports your own project publishes are set in chamba.yaml, not here.`);const i=await R({message:"Port range (START-END):",placeholder:p(a),initialValue:p(a),validate:u=>{const k=(u??"").trim();if(!/^\d+-\d+$/.test(k))return"Use START-END, e.g. 3900-3999";if(!$(k)){const[G=0,M=0]=k.split("-").map(Number);return G>=M?"Start must be below end":`Ports must be between ${E} and ${Y}`}}});if(c(i))return;const r=$(i.trim());if(!r||p(r)===p(a))return;O(r);const m=D(r);for(const u of m)n.info(`${u.workspaceId}: port ${u.from} -> ${u.to}`);n.success(`Port range set to ${p(r)}.`),await f(e)}async function f(e){const a=e.containerName,t=l("docker",["inspect","--format","{{.State.Status}}",a],{encoding:"utf8",stdio:"pipe"});if((t.status===0?t.stdout.trim():null)!=="running"){n.info("Changes will apply on next session.");return}const s=await h({message:"Stop the running container so changes apply on next session?"});if(c(s)||!s){n.warn("Container still running with old config - it will be recreated on next session.");return}n.info("Stopping container..."),l("docker",["stop",a],{stdio:"pipe"}),l("docker",["rm",a],{stdio:"pipe"}),n.info("Container removed. Open a new session to apply changes.")}async function X(e){const a=b(e.workspaceRoot);if(!a){n.error("chamba.yaml not found or invalid.");return}w(`This will reset chamba.yaml to the minimal default (workspace_id + shadow_paths).
15
+ Ports your own project publishes are set in chamba.yaml, not here.`);const c=await S({message:"Port range (START-END):",placeholder:p(a),initialValue:p(a),validate:u=>{const k=(u??"").trim();if(!/^\d+-\d+$/.test(k))return"Use START-END, e.g. 3900-3999";if(!_(k)){const[M=0,G=0]=k.split("-").map(Number);return M>=G?"Start must be below end":`Ports must be between ${B} and ${j}`}}});if(i(c))return;const r=_(c.trim());if(!r||p(r)===p(a))return;V(r);const h=q(r);for(const u of h)n.info(`${u.workspaceId}: port ${u.from} -> ${u.to}`);n.success(`Port range set to ${p(r)}.`),await g(e)}async function g(e){const a=e.containerName,t=l("docker",["inspect","--format","{{.State.Status}}",a],{encoding:"utf8",stdio:"pipe"});if((t.status===0?t.stdout.trim():null)!=="running"){n.info("Changes will apply on next session.");return}const s=await f({message:"Stop the running container so changes apply on next session?"});if(i(s)||!s){n.warn("Container still running with old config - it will be recreated on next session.");return}n.info("Stopping container..."),l("docker",["stop",a],{stdio:"pipe"}),l("docker",["rm",a],{stdio:"pipe"}),n.info("Container removed. Open a new session to apply changes.")}async function ee(e){const a=b(e.workspaceRoot);if(!a){n.error("chamba.yaml not found or invalid.");return}m(`This will reset chamba.yaml to the minimal default (workspace_id + shadow_paths).
13
16
  Your workspace_id is preserved; shadow_paths reset to the defaults (node_modules, .env*).
14
- Any env, profiles, or ports settings will be removed - re-add them with your editor, which reads the schema the file points at.`,"Reset chamba.yaml");const t=await h({message:"Reset chamba.yaml to defaults?"});if(c(t)||!t)return;const o=U(a.workspace_id);y(e.workspaceRoot,o),n.success("chamba.yaml reset to defaults."),await f(e)}async function se(e){for(;;){const t=[{value:"git-mode",label:"Git mode",hint:`current: ${C(e.workspaceId)??d.local}`},{value:"shadow-paths",label:"Shadow paths",hint:"manage shadow patterns"},{value:"ports",label:"Ports",hint:`web interface range: ${p(S())}`},{value:"rebuild",label:"Rebuild container",hint:"force a fresh image build"},{value:"clean-rebuild",label:"Clean rebuild",hint:"fresh build, no cache"},{value:"reset",label:"Reset config",hint:"restore chamba.yaml to defaults"},{value:"back",label:"\u2190 Back"}],o=await g({message:"Settings:",options:t});if(c(o)||o==="back")return"back";switch(o){case"git-mode":await z(e);break;case"shadow-paths":await B(e);break;case"ports":await H(e);break;case"rebuild":return"rebuild";case"clean-rebuild":return"clean-rebuild";case"reset":await X(e);break}}}async function re(e){if(l("docker",["inspect","--type","container",e],{encoding:"utf8"}).status!==0){n.info(`Container ${e} is not running.`);return}const t=await h({message:`Stop ${e}?`});if(c(t)||!t){v();return}n.info(`Stopping ${e}...`),l("docker",["stop",e],{stdio:"pipe"}),T(`${e} stopped.`)}async function ie(e){const a=l("docker",["inspect","--type","container","--format","{{.State.Status}}",e],{encoding:"utf8",stdio:"pipe"}),t=a.status===0;if(t&&a.stdout.trim()==="running"){n.warn(`Rebuilding stops and removes the running container.
15
- Agent memory, settings, and workspace data are preserved.`);const i=await h({message:"Rebuild now?",initialValue:!0});if(c(i)||!i)return v("Rebuild cancelled - container left running."),!1}return t&&(n.info(`Stopping container ${e}...`),l("docker",["stop",e],{stdio:"pipe"}),l("docker",["rm",e],{stdio:"pipe"})),(l("docker",["images","-q",e],{encoding:"utf8",stdio:"pipe"}).stdout??"").trim().length>0&&(n.info(`Removing image ${e}...`),l("docker",["rmi",e],{stdio:"pipe"})),n.info("Image removed - starting fresh build\u2026"),!0}export{ie as resetImage,se as run,re as stop};
17
+ Any env, profiles, or ports settings will be removed - re-add them with your editor, which reads the schema the file points at.`,"Reset chamba.yaml");const t=await f({message:"Reset chamba.yaml to defaults?"});if(i(t)||!t)return;const o=Y(a.workspace_id);y(e.workspaceRoot,o),n.success("chamba.yaml reset to defaults."),await g(e)}function te(){if(!R())return"not available on this platform";const e=$();return e===null?"not set":`current: ${e}`}async function me(e){for(;;){const t=[{value:"git-mode",label:"Git mode",hint:`current: ${C(e.workspaceId)??d.local}`},{value:"shadow-paths",label:"Shadow paths",hint:"manage shadow patterns"},{value:"ports",label:"Ports",hint:`web interface range: ${p(P())}`},{value:"stay-awake",label:"Stay awake",hint:te()},{value:"rebuild",label:"Rebuild container",hint:"force a fresh image build"},{value:"clean-rebuild",label:"Clean rebuild",hint:"fresh build, no cache"},{value:"reset",label:"Reset config",hint:"restore chamba.yaml to defaults"},{value:"back",label:"\u2190 Back"}],o=await w({message:"Settings:",options:t});if(i(o)||o==="back")return"back";switch(o){case"git-mode":await Q(e);break;case"shadow-paths":await F(e);break;case"ports":await x(e);break;case"stay-awake":await Z(e);break;case"rebuild":return"rebuild";case"clean-rebuild":return"clean-rebuild";case"reset":await ee(e);break}}}async function fe(e){if(l("docker",["inspect","--type","container",e],{encoding:"utf8"}).status!==0){n.info(`Container ${e} is not running.`);return}const t=await f({message:`Stop ${e}?`});if(i(t)||!t){v();return}n.info(`Stopping ${e}...`),l("docker",["stop",e],{stdio:"pipe"}),W(`${e} stopped.`)}async function we(e){const a=l("docker",["inspect","--type","container","--format","{{.State.Status}}",e],{encoding:"utf8",stdio:"pipe"}),t=a.status===0;if(t&&a.stdout.trim()==="running"){n.warn(`Rebuilding stops and removes the running container.
18
+ Agent memory, settings, and workspace data are preserved.`);const c=await f({message:"Rebuild now?",initialValue:!0});if(i(c)||!c)return v("Rebuild cancelled - container left running."),!1}return t&&(n.info(`Stopping container ${e}...`),l("docker",["stop",e],{stdio:"pipe"}),l("docker",["rm",e],{stdio:"pipe"})),(l("docker",["images","-q",e],{encoding:"utf8",stdio:"pipe"}).stdout??"").trim().length>0&&(n.info(`Removing image ${e}...`),l("docker",["rmi",e],{stdio:"pipe"})),n.info("Image removed - starting fresh build\u2026"),!0}export{we as resetImage,me as run,fe as stop};
@@ -0,0 +1,3 @@
1
+ import{spawn as m}from"node:child_process";import{utimesSync as w}from"node:fs";import{awakeMarkerPath as d,claimAwakeMarker as g,dockerWaitArgv as M,isAwakeMarkerLive as I,readAwakeMarker as p,removeAwakeMarker as u,writeAwakeMarker as x}from"./awake-marker.js";const[r="",n="",o="",S="",...l]=process.argv.slice(2),s=Number(S),v={cacheDir:r,container:n};(!r||!n||!o||!Number.isFinite(s)||s<=0||l.length===0)&&(process.stderr.write(`awake-holder: needs a cache dir, a container name, a container id, a touch interval and an inhibitor command
2
+ `),process.exit(1));let t=null,a=null,f=!1;for(const e of["SIGTERM","SIGINT","SIGHUP"])process.on(e,()=>i(!0));process.on("uncaughtException",()=>i(!0)),process.on("unhandledRejection",()=>i(!0));const c=p(r);c!==null&&I(v,c,o)&&process.exit(0),c!==null&&u(r);const h=new Date().toISOString();try{g(r,{pid:process.pid,container:n,id:o,started:h})||process.exit(0)}catch(e){process.stderr.write(`awake-holder: cannot write ${d(r)} - ${e instanceof Error?e.message:String(e)}
3
+ `),process.exit(1)}const[b="",...A]=l;if(t=m(b,[...A,...M(n)],{detached:!0,stdio:"ignore"}),t.on("exit",()=>i(!0)),t.on("error",()=>i(!0)),t.pid!==void 0)try{x(r,{pid:process.pid,container:n,id:o,started:h,inhibitor:t.pid})}catch{}a=setInterval(()=>{if(!k())return i(!1);try{const e=new Date;w(d(r),e,e)}catch{i(!0)}},s),a.unref();function k(){return p(r)?.pid===process.pid}function i(e){if(!f){if(f=!0,a!==null&&clearInterval(a),t?.pid!==void 0)try{process.kill(-t.pid,"SIGTERM")}catch{}e&&k()&&u(r),process.exit(0)}}
@@ -0,0 +1,4 @@
1
+ import{spawnSync as x}from"node:child_process";import{readFileSync as w,renameSync as b,writeFileSync as l}from"node:fs";import{join as m}from"node:path";import{WEBTERM_AWAKE_FILE as g,WEBTERM_STATE_DIR as A}from"./constants.js";import{safeRmSync as E}from"./safe-rm.js";function M(t){return["docker","wait",t]}const k="awake-holder";function o(t){return m(t,A,g)}function y(t){let n;try{n=w(o(t),"utf8")}catch{return null}const e=new Map;for(const u of n.split(`
2
+ `)){const s=u.indexOf("=");s!==-1&&e.set(u.slice(0,s).trim(),u.slice(s+1).trim())}const r=Number(e.get("pid")),i=e.get("container"),f=e.get("id"),a=e.get("started"),c=Number(e.get("inhibitor"));return!Number.isInteger(r)||r<=0||!i||!f||!a?null:{pid:r,container:i,id:f,started:a,...Number.isInteger(c)&&c>0?{inhibitor:c}:{}}}function d(t){const n=[`pid=${t.pid}`,`container=${t.container}`,`id=${t.id}`,`started=${t.started}`];return t.inhibitor!==void 0&&n.push(`inhibitor=${t.inhibitor}`),`${n.join(`
3
+ `)}
4
+ `}function W(t,n){try{return l(o(t),d(n),{flag:"wx"}),!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function F(t,n){const e=o(t),r=`${e}.${n.pid}`;l(r,d(n)),b(r,e)}function S(t){try{E(o(t),{force:!0})}catch{}}function p(t){const n=x("ps",["-ww","-o","args=","-p",String(t)],{encoding:"utf8"});return n.error!==void 0||n.status!==0?null:n.stdout.trim()||null}function h(t,...n){return n.every(e=>$(t,e))}function $(t,n){if(n==="")return!1;for(let e=t.indexOf(n);e!==-1;e=t.indexOf(n,e+1)){const r=e===0?" ":t[e-1],i=e+n.length>=t.length?" ":t[e+n.length];if(/\s/.test(r)&&/\s/.test(i))return!0}return!1}function I(t,n,e,r){return n.id===e&&R(t,n,r)}function R(t,n,e=p){if(n.container!==t.container)return!1;const r=e(n.pid);return r===null?!1:r.includes(k)&&h(r,t.cacheDir,t.container)}function j(t,n,e){const r=y(t.cacheDir);return r===null?null:I(t,r,n,e)?r:(T(t,r,e),S(t.cacheDir),null)}function T(t,n,e=p){if(n.inhibitor===void 0)return;const r=e(n.inhibitor);r===null||!h(r,...M(t.container))||v(-n.inhibitor,"SIGTERM")}function v(t,n){try{return process.kill(t,n),!0}catch{return!1}}export{k as HOLDER_NAME,o as awakeMarkerPath,W as claimAwakeMarker,M as dockerWaitArgv,T as endInhibitor,I as isAwakeMarkerLive,R as isHolderRunning,y as readAwakeMarker,S as removeAwakeMarker,v as signal,j as takeLiveMarker,F as writeAwakeMarker};
@@ -0,0 +1 @@
1
+ import{spawn as k,spawnSync as A}from"node:child_process";import{accessSync as h,constants as D}from"node:fs";import{delimiter as x,dirname as g,extname as S,join as s}from"node:path";import{setTimeout as u}from"node:timers/promises";import{fileURLToPath as y}from"node:url";import{endInhibitor as H,HOLDER_NAME as _,isHolderRunning as f,readAwakeMarker as i,removeAwakeMarker as l,signal as o,takeLiveMarker as E}from"./awake-marker.js";import{AWAKE_END_GRACE_MS as M,AWAKE_START_WAIT_MS as I,AWAKE_TOUCH_MS as T}from"./constants.js";import{readStayAwake as b}from"./global-config.js";import{listWorkspaces as v}from"./workspace-identity.js";function p(r,n=process.platform){return n==="darwin"?["caffeinate","-is"]:n==="linux"?["systemd-inhibit","--what=idle:sleep","--who=chamba",`--why=${r} is up`,"--mode=block"]:null}function m(r=process.platform){const n=p("chamba",r);if(n===null)return!1;const[e=""]=n;return R(e)}function R(r){return r.includes("/")?d(r):(process.env.PATH??"").split(x).filter(Boolean).some(e=>d(s(e,r)))}function d(r){try{return h(r,D.X_OK),!0}catch{return!1}}function W(){const r=y(import.meta.url);return s(g(r),`${_}${S(r)}`)}async function K(r,n,e=process.platform){const t=p(r.container,e);if(t===null||!m(e))return!1;const c=k(process.execPath,P(r,n,t),{detached:!0,stdio:"ignore"});c.on("error",()=>{}),c.unref();const w=Date.now()+I;for(;Date.now()<w;){if(i(r.cacheDir)!==null)return!0;await u(50)}return!1}async function z(r,n=process.platform,e=L){if(b()!=="on")return"off";if(!m(n))return"unsupported";const t=e(r.container);return t===null?"no-container":E(r,t)!==null?"already":await K(r,t,n)?"held":"failed"}function L(r){const n=A("docker",["inspect","--format","{{.Id}}|{{.State.Running}}",r],{encoding:"utf8",stdio:"pipe"});if(n.status!==0)return null;const[e="",t=""]=n.stdout.trim().split("|");return e&&t==="true"?e:null}function P(r,n,e,t=T){return[...process.execArgv,W(),r.cacheDir,r.container,n,String(t),...e]}function C(r,n){return r.filter(e=>{const t=i(e.cacheDir);return t!==null&&f(e,t,n)}).map(e=>e.container)}function F(r){const n=v().filter(e=>e.workspaceId!==r).map(e=>({cacheDir:e.workspaceDir,container:e.containerName}));return C(n)}async function J(r,n){const e=i(r.cacheDir);if(e===null)return l(r.cacheDir);if(!f(r,e,n)||!o(e.pid,"SIGTERM"))return a(r,e,n);const t=Date.now()+M;for(;Date.now()<t;){if(!o(e.pid,0))return a(r,e,n);await u(50)}o(e.pid,"SIGKILL"),a(r,e,n)}function a(r,n,e){H(r,i(r.cacheDir)??n,e),l(r.cacheDir)}export{p as awakeArgv,P as awakeHolderArgv,J as endAwakeHolder,z as ensureAwakeHolder,C as heldWorkspaces,m as isAwakeSupported,F as otherHeldWorkspaces,K as startAwakeHolder};
@@ -1,10 +1,10 @@
1
- import{existsSync as w,readFileSync as h,writeFileSync as v}from"node:fs";import{basename as $,join as p}from"node:path";import y from"ajv";import{dump as C,load as _}from"js-yaml";import{CHAMBA_SCHEMA_DIR as S,CHAMBA_SCHEMA_FILE as g,CHAMBA_YAML as o,DEFAULT_SHADOW_PATHS as j,PACKAGE_ROOT as b,WORKSPACE_ID_MAX as m,WORKSPACE_ID_MIN as k}from"./constants.js";const M=/^[a-z0-9][a-z0-9-]*[a-z0-9]$/;function B(e){if(e.length<k)return`Workspace ID must be at least ${k} characters`;if(e.length>m)return`Workspace ID must be at most ${m} characters`;if(!M.test(e))return"Workspace ID must be lowercase alphanumeric with hyphens, not starting or ending with a hyphen"}function Y(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,m)||"my-workspace"}const I=p(b,S,g),O=y.default??y;let u=null;function A(){if(!u){const e=JSON.parse(h(I,"utf8"));u=new O({allErrors:!0,allowUnionTypes:!0}).compile(e)}return u}function E(e){return(e??[]).map(t=>{if(t.keyword==="additionalProperties"&&t.params?.additionalProperty)return`unknown property "${t.params.additionalProperty}"`;const a=t.instancePath?`"${t.instancePath.slice(1).replace(/\//g,".")}"`:"";return a?`${a} ${t.message}`:t.message??"validation error"}).join("; ")}function V(e){const t=p(e,o);if(!w(t))return null;const a=_(h(t,"utf8"));if(typeof a!="object"||a===null)throw new Error(`${o} is empty or not a valid YAML object`);const r=A();if(!r(a))throw new Error(`Invalid ${o}: ${E(r.errors)}`);return a}const{version:P}=JSON.parse(h(p(b,"package.json"),"utf8")),D=`https://unpkg.com/chamba@${P}/schema/${g}`,W=`# yaml-language-server: $schema=${D}`,x={workspace_id:`# chamba workspace config - run 'npx chamba' from anywhere under this directory tree to start your dev container.
1
+ import{existsSync as w,readFileSync as h,writeFileSync as A}from"node:fs";import{basename as $,join as p}from"node:path";import y from"ajv";import{dump as C,load as _}from"js-yaml";import{CHAMBA_SCHEMA_DIR as S,CHAMBA_SCHEMA_FILE as g,CHAMBA_YAML as o,DEFAULT_SHADOW_PATHS as j,PACKAGE_ROOT as b,WORKSPACE_ID_MAX as m,WORKSPACE_ID_MIN as k}from"./constants.js";const M=/^[a-z0-9][a-z0-9-]*[a-z0-9]$/;function B(e){if(e.length<k)return`Workspace ID must be at least ${k} characters`;if(e.length>m)return`Workspace ID must be at most ${m} characters`;if(!M.test(e))return"Workspace ID must be lowercase alphanumeric with hyphens, not starting or ending with a hyphen"}function Y(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,m)||"my-workspace"}const O=p(b,S,g),P=y.default??y;let u=null;function E(){if(!u){const e=JSON.parse(h(O,"utf8"));u=new P({allErrors:!0,allowUnionTypes:!0}).compile(e)}return u}function v(e){return(e??[]).map(t=>{if(t.keyword==="additionalProperties"&&t.params?.additionalProperty)return`unknown property "${t.params.additionalProperty}"`;const a=t.instancePath?`"${t.instancePath.slice(1).replace(/\//g,".")}"`:"";return a?`${a} ${t.message}`:t.message??"validation error"}).join("; ")}function V(e){const t=p(e,o);if(!w(t))return null;const a=_(h(t,"utf8"));if(typeof a!="object"||a===null)throw new Error(`${o} is empty or not a valid YAML object`);const r=E();if(!r(a))throw new Error(`Invalid ${o}: ${v(r.errors)}`);return a}const{version:I}=JSON.parse(h(p(b,"package.json"),"utf8")),D=`https://unpkg.com/chamba@${I}/schema/${g}`,W=`# yaml-language-server: $schema=${D}`,x={workspace_id:`# chamba workspace config - run 'npx chamba' from anywhere under this directory tree to start your dev container.
2
2
  # Every field is documented in the schema above, which your editor reads for completion and validation.
3
3
  # This file may be rewritten by chamba (repair, reset, settings changes). Custom comments will not be preserved.`,env:`# Environment injected into the container - a single value or a list.
4
- # Each entry is an env-file path relative to this directory (e.g. '.env') or an inline 'KEY=VALUE' variable.`,shadow_paths:"# .gitignore-style patterns - agents see an empty, isolated copy instead of the real host data.",profiles:`# Dockerfile profiles - each adds on top of the chamba base image (Debian + Node.js + git + AI CLIs).
4
+ # Each entry is an env-file path relative to this directory (e.g. '.env') or an inline 'KEY=VALUE' variable.`,shadow_paths:"# .gitignore-style patterns - agents see an empty, isolated copy instead of the real host data.",profiles:`# Dockerfile profiles - each adds on top of the chamba base image (Debian + Node.js + git + agent CLIs).
5
5
  # Useful for teams: each person can have a lean profile for their stack instead of one large shared image.
6
6
  # When multiple profiles exist, chamba prompts you to pick one on session start.`};function R(e,t){const a=p(e,o),n=C(t,{lineWidth:-1,quotingType:'"',forceQuotes:!1}).split(`
7
7
  `),s=[];for(const c of n){const i=c.match(/^(\w[\w_]*):/)?.[1],f=i&&x[i];f&&(s.push(""),s.push(f)),s.push(c)}const l=s.join(`
8
8
  `).trimEnd(),d=t.profiles&&Object.keys(t.profiles).length>0?`
9
- ${T}`:"";v(a,`${W}${l}${d}
10
- `)}const T=" # Add more profiles here - or ask the agent inside the container to set one up for you.";function H(e){return{workspace_id:e,shadow_paths:[...j]}}const L=new Set(["workspace_id","env","shadow_paths","profiles","ports"]);function J(e){const t=p(e,o);if(!w(t))return{repairedYaml:null};try{const a=_(h(t,"utf8"));if(typeof a!="object"||a===null)return{repairedYaml:null};const r=a,n=[];for(const i of Object.keys(r))L.has(i)||(delete r[i],n.push(`removed unknown field "${i}"`));const s=Y($(e)),l=H(r.workspace_id||s);if("workspace_id"in r||(r.workspace_id=l.workspace_id,n.push(`added missing workspace_id ("${l.workspace_id}")`)),"shadow_paths"in r||(r.shadow_paths=l.shadow_paths,n.push("added default shadow_paths")),n.length===0)return{repairedYaml:null};const d=A();if(!d(r))return{repairedYaml:null,error:`${o} could not be repaired: ${E(d.errors)}`};const c=r;return R(e,c),{repairedYaml:c,message:`Repaired ${o}: ${n.join(", ")}`}}catch(a){return{repairedYaml:null,error:`${o} repair failed: ${a instanceof Error?a.message:a}`}}}export{D as SCHEMA_URL,H as buildDefaultChambaYaml,V as readChambaYaml,J as repairChambaYaml,Y as slugifyForWorkspaceId,B as validateWorkspaceId,R as writeChambaYaml};
9
+ ${T}`:"";A(a,`${W}${l}${d}
10
+ `)}const T=" # Add more profiles here - or ask the agent inside the container to set one up for you.";function H(e){return{workspace_id:e,shadow_paths:[...j]}}const L=new Set(["workspace_id","env","shadow_paths","profiles","ports"]);function J(e){const t=p(e,o);if(!w(t))return{repairedYaml:null};try{const a=_(h(t,"utf8"));if(typeof a!="object"||a===null)return{repairedYaml:null};const r=a,n=[];for(const i of Object.keys(r))L.has(i)||(delete r[i],n.push(`removed unknown field "${i}"`));const s=Y($(e)),l=H(r.workspace_id||s);if("workspace_id"in r||(r.workspace_id=l.workspace_id,n.push(`added missing workspace_id ("${l.workspace_id}")`)),"shadow_paths"in r||(r.shadow_paths=l.shadow_paths,n.push("added default shadow_paths")),n.length===0)return{repairedYaml:null};const d=E();if(!d(r))return{repairedYaml:null,error:`${o} could not be repaired: ${v(d.errors)}`};const c=r;return R(e,c),{repairedYaml:c,message:`Repaired ${o}: ${n.join(", ")}`}}catch(a){return{repairedYaml:null,error:`${o} repair failed: ${a instanceof Error?a.message:a}`}}}export{D as SCHEMA_URL,H as buildDefaultChambaYaml,V as readChambaYaml,J as repairChambaYaml,Y as slugifyForWorkspaceId,B as validateWorkspaceId,R as writeChambaYaml};
@@ -1 +1 @@
1
- import{dirname as E}from"node:path";import{fileURLToPath as e}from"node:url";const T=E(E(E(e(import.meta.url)))),I=".chamba",R="workspaces",a="global",O="agents",N="shadows",D="pnpm-store",x="webterm",C="chamba.yaml",S="schema",m="chamba.schema.json",B=".lock",P="config",M="1",h=2,U=48,b=["node_modules",".env*"],A="devuser",o=`/home/${A}`,l="/workspace",F=`${o}/startup.mjs`,t=`${o}/.local/share/pnpm/store`,i=["bash","-c",'trap "exit 0" TERM INT; while :; do sleep 86400 & wait $!; done'],W="/usr/local/share/chamba/claude-statusline.sh",d="chamba-",G="chamba.managed",H="chamba.shadows",K="chamba.profile",u="chamba.runtime-env",w="chamba.git-mode",f="chamba.timezone",g="chamba.build-hash",$="chamba.ports",V="chamba.env",v="default";import{GIT_MODE as _,GIT_WRAPPER_PATH as c,GIT_WRAPPER_SOURCE as r}from"../../templates/runtime-constants.mjs";const Y=Object.values(_),j={CLAUDE_AFK_TIMEOUT_MS:"2147483647",CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY:"1",CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:"1",CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL:"1",CLAUDE_CODE_NO_FLICKER:"1",DISABLE_AUTOUPDATER:"1",DISABLE_ERROR_REPORTING:"1",DISABLE_INSTALLATION_CHECKS:"1",DISABLE_TELEMETRY:"1",DISABLE_UPGRADE_COMMAND:"1",DO_NOT_TRACK:"1",OPENCODE_DISABLE_CLAUDE_CODE_SKILLS:"1",npm_config_store_dir:t,pnpm_config_store_dir:t},y=["claude","opencode","codex"],X="claude",z=3899,Z="3900-3999",q="/tmp/webterm.key",s=`${o}/.chamba-webterm`,n="agent",J=`${s}/${n}`,Q="webpane",oo=`${o}/.webpane`;export{O as AGENTS_DIR,I as CHAMBA_DIR,S as CHAMBA_SCHEMA_DIR,m as CHAMBA_SCHEMA_FILE,C as CHAMBA_YAML,W as CLAUDE_STATUSLINE_PATH,o as CONTAINER_HOME,i as CONTAINER_KEEP_ALIVE,d as CONTAINER_NAME_PREFIX,t as CONTAINER_PNPM_STORE,F as CONTAINER_STARTUP,A as CONTAINER_USER,oo as CONTAINER_WEBPANE_DIR,s as CONTAINER_WEBTERM_STATE_DIR,l as CONTAINER_WORKSPACE,v as DEFAULT_PROFILE,b as DEFAULT_SHADOW_PATHS,X as DEFAULT_WEB_AGENT,_ as GIT_MODE,Y as GIT_MODES,c as GIT_WRAPPER_PATH,r as GIT_WRAPPER_SOURCE,P as GLOBAL_CONFIG_FILE,a as GLOBAL_DIR,g as LABEL_BUILD_HASH,V as LABEL_ENV,w as LABEL_GIT_MODE,G as LABEL_MANAGED,$ as LABEL_PORTS,K as LABEL_PROFILE,u as LABEL_RUNTIME_ENV,H as LABEL_SHADOWS,f as LABEL_TIMEZONE,B as LOCK_FILE,M as LOCK_VERSION,T as PACKAGE_ROOT,D as PNPM_STORE_DIR,j as RUNTIME_ENV,N as SHADOWS_DIR,Q as WEBPANE_DIR,n as WEBTERM_AGENT_FILE,J as WEBTERM_AGENT_FILE_PATH,x as WEBTERM_STATE_DIR,y as WEB_AGENTS,z as WEB_CONTAINER_PORT,q as WEB_KEY_FILE_PATH,Z as WEB_RANGE_DEFAULT,R as WORKSPACES_DIR,U as WORKSPACE_ID_MAX,h as WORKSPACE_ID_MIN};
1
+ import{dirname as E}from"node:path";import{fileURLToPath as A}from"node:url";const I=E(E(E(A(import.meta.url)))),R=".chamba",a="workspaces",O="global",x="agents",N="shadows",D="pnpm-store",S="webterm",C="chamba.yaml",m="schema",B="chamba.schema.json",M=".lock",P="config",h="1",W=2,U=48,b=["node_modules",".env*"],e="devuser",o=`/home/${e}`,F="/workspace",l=`${o}/startup.mjs`,t=`${o}/.local/share/pnpm/store`,K=["bash","-c",'trap "exit 0" TERM INT; while :; do sleep 86400 & wait $!; done'],i="/usr/local/share/chamba/claude-statusline.sh",d="chamba-",G="chamba.managed",H="chamba.shadows",u="chamba.profile",w="chamba.runtime-env",f="chamba.git-mode",g="chamba.timezone",$="chamba.build-hash",V="chamba.ports",k="chamba.env",v="default";import{GIT_MODE as _,GIT_WRAPPER_PATH as c,GIT_WRAPPER_SOURCE as r}from"../../templates/runtime-constants.mjs";const j=Object.values(_),y={CLAUDE_AFK_TIMEOUT_MS:"2147483647",CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY:"1",CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:"1",CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL:"1",CLAUDE_CODE_NO_FLICKER:"1",DISABLE_AUTOUPDATER:"1",DISABLE_ERROR_REPORTING:"1",DISABLE_INSTALLATION_CHECKS:"1",DISABLE_TELEMETRY:"1",DISABLE_UPGRADE_COMMAND:"1",DO_NOT_TRACK:"1",OPENCODE_DISABLE_CLAUDE_CODE_SKILLS:"1",npm_config_store_dir:t,pnpm_config_store_dir:t},X=["claude","opencode","codex"],z="claude",Z=3899,q="3900-3999",J="/tmp/webterm.key",s=`${o}/.chamba-webterm`,n="agent",Q=`${s}/${n}`,oo="awake",p=3e4,Eo=p*3,to=2e3,_o=2e3,Ao="webpane",eo=`${o}/.webpane`;export{x as AGENTS_DIR,to as AWAKE_END_GRACE_MS,Eo as AWAKE_STALE_MS,_o as AWAKE_START_WAIT_MS,p as AWAKE_TOUCH_MS,R as CHAMBA_DIR,m as CHAMBA_SCHEMA_DIR,B as CHAMBA_SCHEMA_FILE,C as CHAMBA_YAML,i as CLAUDE_STATUSLINE_PATH,o as CONTAINER_HOME,K as CONTAINER_KEEP_ALIVE,d as CONTAINER_NAME_PREFIX,t as CONTAINER_PNPM_STORE,l as CONTAINER_STARTUP,e as CONTAINER_USER,eo as CONTAINER_WEBPANE_DIR,s as CONTAINER_WEBTERM_STATE_DIR,F as CONTAINER_WORKSPACE,v as DEFAULT_PROFILE,b as DEFAULT_SHADOW_PATHS,z as DEFAULT_WEB_AGENT,_ as GIT_MODE,j as GIT_MODES,c as GIT_WRAPPER_PATH,r as GIT_WRAPPER_SOURCE,P as GLOBAL_CONFIG_FILE,O as GLOBAL_DIR,$ as LABEL_BUILD_HASH,k as LABEL_ENV,f as LABEL_GIT_MODE,G as LABEL_MANAGED,V as LABEL_PORTS,u as LABEL_PROFILE,w as LABEL_RUNTIME_ENV,H as LABEL_SHADOWS,g as LABEL_TIMEZONE,M as LOCK_FILE,h as LOCK_VERSION,I as PACKAGE_ROOT,D as PNPM_STORE_DIR,y as RUNTIME_ENV,N as SHADOWS_DIR,Ao as WEBPANE_DIR,n as WEBTERM_AGENT_FILE,Q as WEBTERM_AGENT_FILE_PATH,oo as WEBTERM_AWAKE_FILE,S as WEBTERM_STATE_DIR,X as WEB_AGENTS,Z as WEB_CONTAINER_PORT,J as WEB_KEY_FILE_PATH,q as WEB_RANGE_DEFAULT,a as WORKSPACES_DIR,U as WORKSPACE_ID_MAX,W as WORKSPACE_ID_MIN};
@@ -1,4 +1,4 @@
1
- import{mkdirSync as p,readFileSync as g,writeFileSync as u}from"node:fs";import{homedir as r}from"node:os";import{join as i}from"node:path";import{CHAMBA_DIR as c,GLOBAL_CONFIG_FILE as b,GLOBAL_DIR as a,WEB_RANGE_DEFAULT as d}from"./constants.js";import{formatWebRange as R,parseWebRange as f}from"./ports.js";const s={webRange:"web_range"};function l(){return i(r(),c,a,b)}function m(){const n=new Map;try{const e=g(l(),"utf8").trimEnd().split(`
2
- `).map(o=>o.trim()).filter(Boolean);for(const o of e){const t=o.indexOf("=");t!==-1&&n.set(o.slice(0,t),o.slice(t+1))}}catch{}return n}function _(n){p(i(r(),c,a),{recursive:!0});const e=`${[...n].map(([o,t])=>`${o}=${t}`).join(`
1
+ import{mkdirSync as g,readFileSync as m,writeFileSync as A}from"node:fs";import{homedir as a}from"node:os";import{join as c}from"node:path";import{CHAMBA_DIR as f,GLOBAL_CONFIG_FILE as w,GLOBAL_DIR as s,WEB_RANGE_DEFAULT as d}from"./constants.js";import{formatWebRange as _,parseWebRange as l}from"./ports.js";const r={webRange:"web_range",stayAwake:"stay_awake"},b=["on","off"];function p(){return c(a(),f,s,w)}function i(){const e=new Map;try{const n=m(p(),"utf8").trimEnd().split(`
2
+ `).map(t=>t.trim()).filter(Boolean);for(const t of n){const o=t.indexOf("=");o!==-1&&e.set(t.slice(0,o),t.slice(o+1))}}catch{}return e}function u(e){g(c(a(),f,s),{recursive:!0});const n=`${[...e].map(([t,o])=>`${t}=${o}`).join(`
3
3
  `)}
4
- `;u(l(),e)}function C(){const n=m().get(s.webRange);return(n!==void 0?f(n):null)??f(d)}function E(n){const e=m();e.set(s.webRange,R(n)),_(e)}export{s as GLOBAL_CONFIG_KEYS,l as globalConfigPath,C as readWebRange,E as writeWebRange};
4
+ `;A(p(),n)}function G(){const e=i().get(r.webRange);return(e!==void 0?l(e):null)??l(d)}function S(e){const n=i();n.set(r.webRange,_(e)),u(n)}function k(){const e=i().get(r.stayAwake);return b.find(n=>n===e)??null}function B(e){const n=i();n.set(r.stayAwake,e),u(n)}export{r as GLOBAL_CONFIG_KEYS,b as STAY_AWAKE_VALUES,p as globalConfigPath,k as readStayAwake,G as readWebRange,B as writeStayAwake,S as writeWebRange};
@@ -1,2 +1,2 @@
1
- import{copyFileSync as b,existsSync as f,mkdirSync as d,readdirSync as h,readFileSync as m,renameSync as w,statSync as k,writeFileSync as M}from"node:fs";import{dirname as y,join as s}from"node:path";import{load as T}from"js-yaml";import{AGENTS_DIR as p,CONTAINER_HOME as g,PACKAGE_ROOT as O,WEB_AGENTS as $}from"./constants.js";import{safeRmSync as I}from"./safe-rm.js";import{renderTemplate as L,writeFileEnsuringDir as N}from"./templates.js";const A=[{audience:["claude"],hostSubpath:s("claude","skills"),container:`${g}/.claude/skills`,insideAgentMount:!0},{audience:["codex","opencode"],hostSubpath:s("shared","skills"),container:`${g}/.agents/skills`,insideAgentMount:!1}],F="skills-manifest.json",_=/^---\r?\n([\s\S]*?)\r?\n---/;function S(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function j(e){const r=_.exec(m(e,"utf8"));if(!r)return null;const t=T(r[1]??""),o=S(t)?t.metadata:void 0,n=S(o)?o.audience:void 0;if(n==null)return null;const c=typeof n=="string"?n.split(","):n;if(!Array.isArray(c)||c.some(a=>typeof a!="string"))throw new Error(`${e}: metadata.audience is neither a name nor a list of names`);const i=c.map(a=>String(a).trim()).filter(a=>a.length>0),l=i.filter(a=>!$.includes(a));if(l.length>0)throw new Error(`${e}: metadata.audience names ${l.join(", ")}, which chamba does not run`);return i.length>0?i:null}function x(e,r){return e===null||r.audience.some(t=>e.includes(t))}function B(e){return A.flatMap(r=>{const t=s(e,p,r.hostSubpath);return d(t,{recursive:!0}),["-v",`${t}:${r.container}:ro`]})}function Y(e,r){const t=s(O,"templates","skills");if(!f(t))return;const o=h(t,{withFileTypes:!0}).filter(i=>i.isDirectory()&&f(s(t,i.name,"SKILL.md"))).map(i=>({name:i.name,audience:j(s(t,i.name,"SKILL.md"))})),n=s(e,p,F),c=K(n);for(const i of A){const l=s(e,p,i.hostSubpath),a=o.filter(u=>x(u.audience,i)).map(u=>u.name);R(l,c[i.hostSubpath]??[],a);for(const u of a)E(s(t,u),s(l,u),r);c[i.hostSubpath]=[...a].sort()}G(n,c)}function E(e,r,t){for(const o of h(e,{withFileTypes:!0})){const n=s(e,o.name),c=s(r,o.name),i=k(n);i.isDirectory()?E(n,c,t):i.isFile()&&(o.name.endsWith(".md")?N(c,L(m(n,"utf8"),t)):(d(y(c),{recursive:!0}),b(n,c)))}}function R(e,r,t){for(const o of r){if(t.includes(o))continue;const n=s(e,o);f(n)&&I(n,{recursive:!0,force:!0})}}const D=/^[A-Za-z0-9._-]+$/;function K(e){try{const r=JSON.parse(m(e,"utf8"));if(S(r))return Object.fromEntries(Object.entries(r).map(([t,o])=>[t,(Array.isArray(o)?o:[]).filter(n=>typeof n=="string"&&D.test(n))]))}catch{}return{}}function G(e,r){d(y(e),{recursive:!0});const t=`${e}.writing`;M(t,`${JSON.stringify(r,null,2)}
2
- `),w(t,e)}export{A as SKILL_HOMES,B as buildSkillsMountArgs,Y as injectSkills,j as readSkillAudience};
1
+ import{copyFileSync as T,existsSync as p,mkdirSync as f,readdirSync as g,readFileSync as S,renameSync as E,statSync as I,writeFileSync as M}from"node:fs";import{dirname as h,join as o}from"node:path";import{load as N}from"js-yaml";import{AGENTS_DIR as d,CONTAINER_HOME as b,PACKAGE_ROOT as O,WEB_AGENTS as $}from"./constants.js";import{safeRmSync as y}from"./safe-rm.js";import{renderTemplate as L,writeFileEnsuringDir as _}from"./templates.js";const k=[{audience:["claude"],hostSubpath:o("claude","skills"),container:`${b}/.claude/skills`,insideAgentMount:!0},{audience:["codex","opencode"],hostSubpath:o("shared","skills"),container:`${b}/.agents/skills`,insideAgentMount:!1}],F="skills-manifest.json",R=".skill-staging",j=/^---\r?\n([\s\S]*?)\r?\n---/;function A(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function x(e){const n=j.exec(S(e,"utf8"));if(!n)return null;const t=N(n[1]??""),i=A(t)?t.metadata:void 0,r=A(i)?i.audience:void 0;if(r==null)return null;const c=typeof r=="string"?r.split(","):r;if(!Array.isArray(c)||c.some(u=>typeof u!="string"))throw new Error(`${e}: metadata.audience is neither a name nor a list of names`);const a=c.map(u=>String(u).trim()).filter(u=>u.length>0),s=a.filter(u=>!$.includes(u));if(s.length>0)throw new Error(`${e}: metadata.audience names ${s.join(", ")}, which chamba does not run`);return a.length>0?a:null}function D(e,n){return e===null||n.audience.some(t=>e.includes(t))}function q(e){return k.flatMap(n=>{const t=o(e,d,n.hostSubpath);return f(t,{recursive:!0}),["-v",`${t}:${n.container}:ro`]})}function P(e,n){const t=o(O,"templates","skills");if(!p(t))return;const i=g(t,{withFileTypes:!0}).filter(s=>s.isDirectory()&&p(o(t,s.name,"SKILL.md"))).map(s=>({name:s.name,audience:x(o(t,s.name,"SKILL.md"))})),r=o(e,d,F),c=o(e,d,R),a=H(r);for(const s of k){const u=o(e,d,s.hostSubpath),m=i.filter(l=>D(l.audience,s)).map(l=>l.name);G(u,a[s.hostSubpath]??[],m);for(const l of m)v(o(t,l),o(u,l),c,n);a[s.hostSubpath]=[...m].sort()}C(r,a)}function v(e,n,t,i){y(t,{recursive:!0,force:!0}),w(e,t,i),f(h(n),{recursive:!0}),y(n,{recursive:!0,force:!0}),E(t,n)}function w(e,n,t){for(const i of g(e,{withFileTypes:!0})){const r=o(e,i.name),c=o(n,i.name),a=I(r);a.isDirectory()?w(r,c,t):a.isFile()&&(i.name.endsWith(".md")?_(c,L(S(r,"utf8"),t)):(f(h(c),{recursive:!0}),T(r,c)))}}function G(e,n,t){for(const i of n){if(t.includes(i))continue;const r=o(e,i);p(r)&&y(r,{recursive:!0,force:!0})}}const K=/^[A-Za-z0-9._-]+$/;function H(e){try{const n=JSON.parse(S(e,"utf8"));if(A(n))return Object.fromEntries(Object.entries(n).map(([t,i])=>[t,(Array.isArray(i)?i:[]).filter(r=>typeof r=="string"&&K.test(r))]))}catch{}return{}}function C(e,n){f(h(e),{recursive:!0});const t=`${e}.writing`;M(t,`${JSON.stringify(n,null,2)}
2
+ `),E(t,e)}export{k as SKILL_HOMES,q as buildSkillsMountArgs,P as injectSkills,x as readSkillAudience};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chamba",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Run AI coding agents in a container, from your browser",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,13 +14,13 @@
14
14
  # fresh and --no-cache builds stay reproducible; bumping a pin edits this file,
15
15
  # busts the build hash, and auto-prompts a rebuild. Deliberately left floating:
16
16
  # apt packages (pinned by the Debian trixie release), Node (major-pinned via
17
- # setup_24.x; minors carry security fixes), and the AI CLIs (always latest).
17
+ # setup_24.x; minors carry security fixes), and the agent CLIs (always latest).
18
18
  # ---------------------------------------------------------------------------
19
19
 
20
20
  FROM debian:trixie-slim
21
21
  LABEL chamba.managed=true
22
22
 
23
- # Enable 24-bit color support for AI CLIs (suppresses color upgrade prompts)
23
+ # Enable 24-bit color support for agent CLIs (suppresses color upgrade prompts)
24
24
  ENV COLORTERM=truecolor
25
25
 
26
26
  # ---------------------------------------------------------------------------
@@ -78,7 +78,7 @@ RUN git config --system protocol.allow never && \
78
78
  git config --system protocol.file.allow always
79
79
 
80
80
  # ---------------------------------------------------------------------------
81
- # Layer 7 - Global npm tools (pnpm pinned; AI CLIs always latest)
81
+ # Layer 7 - Global npm tools (pnpm pinned; agent CLIs always latest)
82
82
  # pnpm is pinned exact: pnpm 11 changed where global settings are read from
83
83
  # (~/.config/pnpm/config.yaml, no longer ~/.npmrc), so an unpinned major bump
84
84
  # can silently break the baked store-dir config.
@@ -6,9 +6,9 @@ You are running inside a chamba dev container.
6
6
  - `/workspace/chamba.yaml` configures this workspace - profiles, shadow paths, environment variables, published ports, and workspace identity. Only modify it when the user explicitly asks. Every field is described in the JSON schema the file's header points at.
7
7
  - Files outside mounted paths cannot be read, written, or executed. If a command fails because of missing files or permissions, tell the user: "This requires running on the host - please run `<command>` outside the container."
8
8
  - This file (`{{tool_path}}`) is managed by chamba and overwritten on every session start. Do not edit it.
9
- - At the start of every session, greet the user with one short line noting the chamba sandbox and the current git mode. When `CHAMBA_WEB_SESSION=1` is in your environment you are being reached through the web interface, so add that anything long or structured can go in the web pane beside the terminal. For example: "You're in a chamba sandbox - git mode: local, filesystem limited to /workspace. Ask for anything as a page and it opens in the web pane."
9
+ - At the start of every session, greet the user with one short line noting the chamba sandbox and the current git mode. When `CHAMBA_WEB_SESSION=1` is in your environment you are being reached through the web interface, so add that anything long or structured can go in the web pane beside the terminal. For example: "You're in a chamba sandbox - git mode: local, filesystem limited to /workspace. Ask for anything as a page and it lands on the Pages tab of the web pane."
10
10
  - If the user asks about the web interface (reaching this agent from a browser), tell them it starts with the container and opens in their browser when they open a session - there is nothing to turn on. `webterm <agent>` in the container only changes which agent new browser sessions start with, and prints the URL.
11
- - **The web pane.** Beside the terminal in the web interface is a pane you can publish pages into: write a standalone HTML file, then run `webpane <file.html> --title "A title"`. Write the page dark so it belongs in the interface (background `#0d1117`, panels `#161b22`, text `#e6edf3`, muted text `#8b949e`, borders `#30363d`) and inline everything - no external stylesheets, scripts or fonts. The page's own `<title>` is what the pane shows. Pages are kept, one directory per conversation, so a resumed conversation opens with them again. In a plain terminal session there is no pane and the helper says so.
11
+ - **The web pane.** Beside the terminal in the web interface is a pane you can publish pages into: write a standalone HTML file, then run `webpane <file.html> --title "A title"`. Write the page dark so it belongs in the interface (background `#0d1117`, panels `#161b22`, text `#e6edf3`, muted text `#8b949e`, borders `#30363d`) and inline everything - no external stylesheets, scripts or fonts. The page's own `<title>` is what the pane shows. Pages live on the pane's Pages tab, which the user may not be looking at: a page you publish is selected there and the tab says so, and it opens in front only where that tab is already the open one. Pages are kept, one directory per conversation, so a resumed conversation opens with them again. In a plain terminal session there is no pane and the helper says so.
12
12
  - **Prefer a page whenever the answer is longer or more structured than a terminal reply carries well** - a plan, a comparison table, a diagram, a long explanation - and whenever you need to ask something with more options or structure than your own question tooling holds. Then say in one line what you published. The page is the answer, not a summary of it.
13
13
  - **Spec-driven work has a tab of its own, and `specs` is your half of it.** The helper is on your path in every session. `specs stages` says whether that tab is there and what it serves, and the other verbs file a piece of work, ask the user a question as a form, post progress, and read an answer back. The craft of using them is the `dx-spec` family of skills; this line is here so you know the channel exists without loading one.
14
14
  - **Code review has a tab of its own too, and `reviews` is your half of that one.** `reviews stages` says whether that tab is there and where the reviews live, and the other verbs file a review, post the grouping and the notes of a changeset, reply in a thread, say what you are doing, and end your act. Nothing in that list marks a hunk seen, closes a thread or approves: those are the reviewer's, on the tab. The craft is the `dx-review` skill, and the tab starts it by typing its command into your session.
@@ -1,6 +1,6 @@
1
1
  ## Publishing a page
2
2
 
3
- The web pane is the panel beside the terminal in the chamba web interface. You publish a standalone HTML file into it and it opens at once, in front of the user, with a chip in the bar above the pane that they can come back to. A user who is looking at another session sees their tab marked instead, and reads the page when they get there.
3
+ The web pane is the panel beside the terminal in the chamba web interface. You publish a standalone HTML file into it, and the page is there immediately. A chip in the bar above the pane lets the user come back to it. If the user has the Pages tab open, the page opens in front of them. If the user has a different tool open, the pane does not change the tool. The Pages tab then shows that a page arrived, and it shows how many pages the user did not open. If the user looks at a different session, the tab of that session gets the mark, and the user reads the page after they go there.
4
4
 
5
5
  Publish when the answer is bigger than the terminal carries well, and when the question is.
6
6