@zuzuucodes/cli 1.3.0 → 1.4.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/bin/zuzuu.mjs CHANGED
@@ -61,7 +61,9 @@ function help() {
61
61
  usage: zuzuu <command> [options]
62
62
 
63
63
  code [dir] launch OpenCode as the bundled default host (faculty home + capture + gate + digest)
64
- web [dir] launch the visual workbench (installs @zuzuucodes/web on demand)
64
+ web [dir] [--stop|--status]
65
+ launch the visual workbench (reuses a running one;
66
+ --stop ends it, --status reports it)
65
67
  init scaffold the faculty home (.zuzuu/) — git-style, idempotent
66
68
  status detected hosts + recorded sessions
67
69
  capture [--host NAME] capture a session → .zuzuu/.traces + .zuzuu/sessions.json
@@ -108,7 +110,7 @@ const args = parseArgs(rest);
108
110
 
109
111
  switch (cmd) {
110
112
  case 'code': process.exit(code(args)); break;
111
- case 'web': web(args); break;
113
+ case 'web': await web(args); break;
112
114
  case 'init': init(args); break;
113
115
  case 'remember': remember(args); break;
114
116
  case 'recall': await recall(args); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuzuucodes/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import crypto from "node:crypto";
8
8
  import { WebcodeServer } from "./server.js";
9
9
  import { addRecent } from "./config.js";
10
+ import { writeInstanceFile, removeInstanceFile } from "./instance-file.js";
10
11
  const HERE = path.dirname(fileURLToPath(import.meta.url));
11
12
  const DEFAULT_PORT = 7770;
12
13
  function parseArgs(argv) {
@@ -147,14 +148,32 @@ async function main() {
147
148
  console.log(`\n zuzuu-web v${pkg.version}`);
148
149
  console.log(` workspace ${root}`);
149
150
  console.log(` url ${url}\n`);
151
+ // Singleton contract: record this instance so `zuzuu web` can reuse it
152
+ // instead of spawning a duplicate (never in hosted mode; never fatal).
153
+ if (!hosted) {
154
+ writeInstanceFile({
155
+ root,
156
+ port: boundPort,
157
+ pid: process.pid,
158
+ token,
159
+ startedAt: new Date().toISOString(),
160
+ version: pkg.version,
161
+ });
162
+ }
150
163
  if (args.open)
151
164
  openBrowser(url);
152
165
  });
153
166
  const shutdown = () => {
167
+ if (!hosted)
168
+ removeInstanceFile(root);
154
169
  server.stop();
155
170
  process.exit(0);
156
171
  };
157
172
  process.on("SIGINT", shutdown);
158
173
  process.on("SIGTERM", shutdown);
174
+ process.on("exit", () => {
175
+ if (!hosted)
176
+ removeInstanceFile(root); // best-effort; idempotent after shutdown()
177
+ });
159
178
  }
160
179
  void main();
@@ -0,0 +1,62 @@
1
+ // Per-workspace daemon instance state — the singleton contract with `zuzuu web`.
2
+ //
3
+ // After a successful listen the daemon writes
4
+ // ~/.webcode/instances/<sha256(realpath-root).slice(0,16)>.json
5
+ // and removes it on clean shutdown. The zuzuu CLI computes the same path for a
6
+ // workspace to discover (and reuse / stop) an already-running daemon instead of
7
+ // spawning a fresh one — so the port + token stay stable across `zuzuu web` runs.
8
+ //
9
+ // Security note: the file contains the auth token. That's acceptable here —
10
+ // it's 0600 in the user's own home directory, and the same token already
11
+ // appears in the daemon's own stdout URL. Hosted mode never writes this file.
12
+ import crypto from "node:crypto";
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ export function instancesDir() {
17
+ return path.join(os.homedir(), ".webcode", "instances");
18
+ }
19
+ /** Deterministic per-workspace file path. `root` must already be realpath'd. */
20
+ export function instancePath(root, dir = instancesDir()) {
21
+ const id = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
22
+ return path.join(dir, `${id}.json`);
23
+ }
24
+ /** Write the instance file (0600). Never throws — a failed write only costs reuse. */
25
+ export function writeInstanceFile(info, dir = instancesDir()) {
26
+ const file = instancePath(info.root, dir);
27
+ try {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ fs.writeFileSync(file, JSON.stringify(info, null, 2) + "\n", { mode: 0o600 });
30
+ fs.chmodSync(file, 0o600); // mode option only applies on create; enforce on overwrite too
31
+ return file;
32
+ }
33
+ catch (err) {
34
+ console.warn(`zuzuu-web: could not write instance state (${String(err)}) — continuing without it`);
35
+ return null;
36
+ }
37
+ }
38
+ /** Best-effort read; null on missing/corrupt. */
39
+ export function readInstanceFile(root, dir = instancesDir()) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(instancePath(root, dir), "utf8"));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Best-effort removal on shutdown. Only removes the file if it still belongs
49
+ * to `pid` — if another daemon raced us and overwrote it, leave theirs alone.
50
+ */
51
+ export function removeInstanceFile(root, pid = process.pid, dir = instancesDir()) {
52
+ const file = instancePath(root, dir);
53
+ try {
54
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
55
+ if (typeof parsed.pid === "number" && parsed.pid !== pid)
56
+ return; // not ours anymore
57
+ fs.unlinkSync(file);
58
+ }
59
+ catch {
60
+ /* already gone or unreadable — nothing to do */
61
+ }
62
+ }
@@ -128,6 +128,16 @@ function binAvailable(binary) {
128
128
  return false;
129
129
  }
130
130
  }
131
+ /** Count item FILES in a faculty dir — knowledge items are .md, others .json/.md. */
132
+ async function countItemFiles(dir) {
133
+ try {
134
+ const names = (await fsp.readdir(dir)).filter((n) => n.endsWith(".json") || n.endsWith(".md"));
135
+ return { count: names.length, names };
136
+ }
137
+ catch {
138
+ return { count: 0, names: [] };
139
+ }
140
+ }
131
141
  /** Read every *.json in a dir into objects; missing dir → [], corrupt file → skipped. */
132
142
  async function readJsonDir(dir) {
133
143
  let names = [];
@@ -181,7 +191,7 @@ export function createZuzuuApi(getRoot, opts = {}) {
181
191
  const faculties = [];
182
192
  for (const key of FACULTIES) {
183
193
  const itemsDir = itemsDirOf(agent, key);
184
- const count = itemsDir ? (await readJsonDir(itemsDir)).length : 0;
194
+ const count = itemsDir ? (await countItemFiles(itemsDir)).count : 0;
185
195
  const pending = (await proposalsOf(agent, key)).length;
186
196
  faculties.push({ key, count, pending });
187
197
  }
@@ -194,7 +204,7 @@ export function createZuzuuApi(getRoot, opts = {}) {
194
204
  const agent = await agentDir();
195
205
  const itemsDir = itemsDirOf(agent, key);
196
206
  const items = itemsDir
197
- ? (await readJsonDir(itemsDir)).map((it) => ({ id: String(it.id ?? "?"), title: firstLine(it.body ?? it.id) }))
207
+ ? (await countItemFiles(itemsDir)).names.map((n) => ({ id: n.replace(/\.(json|md)$/, ""), title: n.replace(/\.(json|md)$/, "") }))
198
208
  : [];
199
209
  const proposals = (await proposalsOf(agent, key)).map((p) => ({ id: String(p.id ?? "?"), faculty: key, title: proposalTitle(p) }));
200
210
  return c.json({ key, items, proposals });
@@ -1 +1 @@
1
- import{d as e,f as t,u as n}from"./index-Ye54YyTn.js";import{i as r,n as i,t as a}from"./monaco-setup-Dszx738Y.js";var o=t();function s({path:t,name:s}){let{data:l,isLoading:u,error:d}=e({queryKey:[`git`,`diff`,t],queryFn:async()=>{let[e,r]=await Promise.all([n.gitDiff(t),n.readFile(t).catch(()=>``)]);return{original:e.original,working:r}}});return d?(0,o.jsx)(c,{danger:!0,children:d.message}):u||!l?(0,o.jsx)(c,{children:`loading diff…`}):(0,o.jsx)(r,{original:l.original,modified:l.working,language:i(s),theme:a(),options:{readOnly:!0,renderSideBySide:!0,fontFamily:`"JetBrains Mono Variable", ui-monospace, monospace`,fontSize:13,minimap:{enabled:!1},automaticLayout:!0}})}function c({children:e,danger:t}){return(0,o.jsx)(`div`,{className:`flex h-full items-center justify-center text-ui ${t?`text-danger`:`text-ink-500`}`,children:e})}export{s as DiffTab,s as default};
1
+ import{d as e,f as t,u as n}from"./index--5yy8RbA.js";import{i as r,n as i,t as a}from"./monaco-setup-CsR6EfHe.js";var o=t();function s({path:t,name:s}){let{data:l,isLoading:u,error:d}=e({queryKey:[`git`,`diff`,t],queryFn:async()=>{let[e,r]=await Promise.all([n.gitDiff(t),n.readFile(t).catch(()=>``)]);return{original:e.original,working:r}}});return d?(0,o.jsx)(c,{danger:!0,children:d.message}):u||!l?(0,o.jsx)(c,{children:`loading diff…`}):(0,o.jsx)(r,{original:l.original,modified:l.working,language:i(s),theme:a(),options:{readOnly:!0,renderSideBySide:!0,fontFamily:`"JetBrains Mono Variable", ui-monospace, monospace`,fontSize:13,minimap:{enabled:!1},automaticLayout:!0}})}function c({children:e,danger:t}){return(0,o.jsx)(`div`,{className:`flex h-full items-center justify-center text-ui ${t?`text-danger`:`text-ink-500`}`,children:e})}export{s as DiffTab,s as default};
@@ -1 +1 @@
1
- import{f as e,l as t}from"./index-Ye54YyTn.js";import{n,r,t as i}from"./monaco-setup-Dszx738Y.js";var a=e();function o({path:e,name:o}){let c=t(t=>t.buffers[e]),l=t(e=>e.setValue),u=t(e=>e.save);return!c||c.loading?(0,a.jsx)(s,{children:`loading…`}):c.error?(0,a.jsx)(s,{danger:!0,children:c.error}):(0,a.jsx)(r,{path:e,language:n(o),theme:i(),value:c.value,onChange:t=>l(e,t??``),onMount:(t,n)=>{t.addCommand(n.KeyMod.CtrlCmd|n.KeyCode.KeyS,()=>{u(e)})},options:{fontFamily:`"JetBrains Mono Variable", ui-monospace, monospace`,fontSize:13,lineHeight:1.5,minimap:{enabled:!0,scale:1},scrollBeyondLastLine:!1,smoothScrolling:!0,renderWhitespace:`selection`,tabSize:2,automaticLayout:!0,padding:{top:8}}})}function s({children:e,danger:t}){return(0,a.jsx)(`div`,{className:`flex h-full items-center justify-center text-ui ${t?`text-danger`:`text-ink-500`}`,children:e})}export{o as MonacoFile,o as default};
1
+ import{f as e,l as t}from"./index--5yy8RbA.js";import{n,r,t as i}from"./monaco-setup-CsR6EfHe.js";var a=e();function o({path:e,name:o}){let c=t(t=>t.buffers[e]),l=t(e=>e.setValue),u=t(e=>e.save);return!c||c.loading?(0,a.jsx)(s,{children:`loading…`}):c.error?(0,a.jsx)(s,{danger:!0,children:c.error}):(0,a.jsx)(r,{path:e,language:n(o),theme:i(),value:c.value,onChange:t=>l(e,t??``),onMount:(t,n)=>{t.addCommand(n.KeyMod.CtrlCmd|n.KeyCode.KeyS,()=>{u(e)})},options:{fontFamily:`"JetBrains Mono Variable", ui-monospace, monospace`,fontSize:13,lineHeight:1.5,minimap:{enabled:!0,scale:1},scrollBeyondLastLine:!1,smoothScrolling:!0,renderWhitespace:`selection`,tabSize:2,automaticLayout:!0,padding:{top:8}}})}function s({children:e,danger:t}){return(0,a.jsx)(`div`,{className:`flex h-full items-center justify-center text-ui ${t?`text-danger`:`text-ink-500`}`,children:e})}export{o as MonacoFile,o as default};
@@ -1 +1 @@
1
- import{h as e}from"./editor.api2-BmGoRSl4.js";import{o as t}from"./monaco-setup-Dszx738Y.js";import{_ as n,a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v,v as y}from"./lspLanguageFeatures-gTnJsses.js";var b=120*1e3,x=class{constructor(e){this._defaults=e,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&=(this._worker.dispose(),null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){this._worker&&Date.now()-this._lastUsedTime>b&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||=(this._worker=t({moduleId:`vs/language/css/cssWorker`,createWorker:()=>new Worker(new URL(`/assets/css.worker-CvXBzhp8.js`,``+import.meta.url),{type:`module`}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._worker.getProxy()),this._client}getLanguageServiceWorker(...e){let t;return this._getClient().then(e=>{t=e}).then(t=>{if(this._worker)return this._worker.withSyncedResources(e)}).then(e=>t)}};function S(t){let n=[],s=[],c=new x(t);n.push(c);let g=(...e)=>c.getLanguageServiceWorker(...e);function y(){let{languageId:n,modeConfiguration:c}=t;w(s),c.completionItems&&s.push(e.registerCompletionItemProvider(n,new _(g,[`/`,`-`,`:`]))),c.hovers&&s.push(e.registerHoverProvider(n,new a(g))),c.documentHighlights&&s.push(e.registerDocumentHighlightProvider(n,new p(g))),c.definitions&&s.push(e.registerDefinitionProvider(n,new f(g))),c.references&&s.push(e.registerReferenceProvider(n,new o(g))),c.documentSymbols&&s.push(e.registerDocumentSymbolProvider(n,new u(g))),c.rename&&s.push(e.registerRenameProvider(n,new m(g))),c.colors&&s.push(e.registerColorProvider(n,new l(g))),c.foldingRanges&&s.push(e.registerFoldingRangeProvider(n,new v(g))),c.diagnostics&&s.push(new h(n,g,t.onDidChange)),c.selectionRanges&&s.push(e.registerSelectionRangeProvider(n,new d(g))),c.documentFormattingEdits&&s.push(e.registerDocumentFormattingEditProvider(n,new r(g))),c.documentRangeFormattingEdits&&s.push(e.registerDocumentRangeFormattingEditProvider(n,new i(g)))}return y(),n.push(C(s)),C(n)}function C(e){return{dispose:()=>w(e)}}function w(e){for(;e.length;)e.pop().dispose()}export{_ as CompletionAdapter,f as DefinitionAdapter,h as DiagnosticsAdapter,l as DocumentColorAdapter,r as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,g as DocumentLinkAdapter,i as DocumentRangeFormattingEditProvider,u as DocumentSymbolAdapter,v as FoldingRangeAdapter,a as HoverAdapter,o as ReferenceAdapter,m as RenameAdapter,d as SelectionRangeAdapter,x as WorkerManager,c as fromPosition,s as fromRange,S as setupMode,n as toRange,y as toTextEdit};
1
+ import{h as e}from"./editor.api2-BmGoRSl4.js";import{o as t}from"./monaco-setup-CsR6EfHe.js";import{_ as n,a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v,v as y}from"./lspLanguageFeatures-gTnJsses.js";var b=120*1e3,x=class{constructor(e){this._defaults=e,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&=(this._worker.dispose(),null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){this._worker&&Date.now()-this._lastUsedTime>b&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||=(this._worker=t({moduleId:`vs/language/css/cssWorker`,createWorker:()=>new Worker(new URL(`/assets/css.worker-CvXBzhp8.js`,``+import.meta.url),{type:`module`}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._worker.getProxy()),this._client}getLanguageServiceWorker(...e){let t;return this._getClient().then(e=>{t=e}).then(t=>{if(this._worker)return this._worker.withSyncedResources(e)}).then(e=>t)}};function S(t){let n=[],s=[],c=new x(t);n.push(c);let g=(...e)=>c.getLanguageServiceWorker(...e);function y(){let{languageId:n,modeConfiguration:c}=t;w(s),c.completionItems&&s.push(e.registerCompletionItemProvider(n,new _(g,[`/`,`-`,`:`]))),c.hovers&&s.push(e.registerHoverProvider(n,new a(g))),c.documentHighlights&&s.push(e.registerDocumentHighlightProvider(n,new p(g))),c.definitions&&s.push(e.registerDefinitionProvider(n,new f(g))),c.references&&s.push(e.registerReferenceProvider(n,new o(g))),c.documentSymbols&&s.push(e.registerDocumentSymbolProvider(n,new u(g))),c.rename&&s.push(e.registerRenameProvider(n,new m(g))),c.colors&&s.push(e.registerColorProvider(n,new l(g))),c.foldingRanges&&s.push(e.registerFoldingRangeProvider(n,new v(g))),c.diagnostics&&s.push(new h(n,g,t.onDidChange)),c.selectionRanges&&s.push(e.registerSelectionRangeProvider(n,new d(g))),c.documentFormattingEdits&&s.push(e.registerDocumentFormattingEditProvider(n,new r(g))),c.documentRangeFormattingEdits&&s.push(e.registerDocumentRangeFormattingEditProvider(n,new i(g)))}return y(),n.push(C(s)),C(n)}function C(e){return{dispose:()=>w(e)}}function w(e){for(;e.length;)e.pop().dispose()}export{_ as CompletionAdapter,f as DefinitionAdapter,h as DiagnosticsAdapter,l as DocumentColorAdapter,r as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,g as DocumentLinkAdapter,i as DocumentRangeFormattingEditProvider,u as DocumentSymbolAdapter,v as FoldingRangeAdapter,a as HoverAdapter,o as ReferenceAdapter,m as RenameAdapter,d as SelectionRangeAdapter,x as WorkerManager,c as fromPosition,s as fromRange,S as setupMode,n as toRange,y as toTextEdit};