daimon 0.4.3 → 0.5.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 (50) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/cli.js +67 -58
  3. package/dist/dashboard/3rdpartylicenses.txt +461 -0
  4. package/dist/dashboard/browser/chunk-3TYCIBMV.js +1 -0
  5. package/dist/dashboard/browser/chunk-5UAN6ETO.js +1 -0
  6. package/dist/dashboard/browser/chunk-AEERNAF7.js +1 -0
  7. package/dist/dashboard/browser/chunk-AX3RJNG4.js +1 -0
  8. package/dist/dashboard/browser/chunk-BADBUP5C.js +3 -0
  9. package/dist/dashboard/browser/chunk-BF6RQFHS.js +2 -0
  10. package/dist/dashboard/browser/chunk-C65CUT7O.js +4 -0
  11. package/dist/dashboard/browser/chunk-CNIZYK4A.js +1 -0
  12. package/dist/dashboard/browser/chunk-D4BFRQ63.js +4 -0
  13. package/dist/dashboard/browser/chunk-E235WGFQ.js +3 -0
  14. package/dist/dashboard/browser/chunk-F2EDJ6FT.js +4 -0
  15. package/dist/dashboard/browser/chunk-HFAARBWL.js +1 -0
  16. package/dist/dashboard/browser/chunk-HFJ25UTJ.js +4 -0
  17. package/dist/dashboard/browser/chunk-JX3IOOXU.js +4 -0
  18. package/dist/dashboard/browser/chunk-LQNYSOSZ.js +1 -0
  19. package/dist/dashboard/browser/chunk-MBVVV35N.js +1 -0
  20. package/dist/dashboard/browser/chunk-NC2VPB4Y.js +2 -0
  21. package/dist/dashboard/browser/chunk-NXNVIINH.js +1 -0
  22. package/dist/dashboard/browser/chunk-Q7R63OUT.js +3 -0
  23. package/dist/dashboard/browser/chunk-QLKOKZDG.js +9 -0
  24. package/dist/dashboard/browser/chunk-QQSPJIPQ.js +1 -0
  25. package/dist/dashboard/browser/chunk-SCAIGUJL.js +6 -0
  26. package/dist/dashboard/browser/chunk-SLQ2WBUA.js +1 -0
  27. package/dist/dashboard/browser/chunk-TSB6OOH2.js +6 -0
  28. package/dist/dashboard/browser/chunk-WAN7TQQW.js +1 -0
  29. package/dist/dashboard/browser/chunk-WWUKM5OG.js +1 -0
  30. package/dist/dashboard/browser/chunk-ZVU34B5S.js +1 -0
  31. package/dist/dashboard/browser/chunk-ZYE3XQS4.js +2 -0
  32. package/dist/dashboard/browser/index.html +15 -0
  33. package/dist/dashboard/browser/main-Z6L5VPBT.js +4 -0
  34. package/dist/dashboard/browser/styles-SIPYJLMG.css +1 -0
  35. package/dist/dashboard/prerendered-routes.json +3 -0
  36. package/dist/main.js +49 -44
  37. package/dist/mcp.js +2 -2
  38. package/package.json +5 -4
  39. package/src/templates/claude/skill.md.tmpl +23 -31
  40. package/src/dashboard.html +0 -451
  41. package/src/templates/claude/commands/doctor.md.tmpl +0 -10
  42. package/src/templates/claude/commands/errors.md.tmpl +0 -10
  43. package/src/templates/claude/commands/logs.md.tmpl +0 -10
  44. package/src/templates/claude/commands/restart.md.tmpl +0 -10
  45. package/src/templates/claude/commands/start.md.tmpl +0 -12
  46. package/src/templates/claude/commands/status.md.tmpl +0 -10
  47. package/src/templates/claude/commands/stop.md.tmpl +0 -10
  48. package/src/templates/claude/commands/up.md.tmpl +0 -10
  49. package/src/templates/claude/commands/wait.md.tmpl +0 -10
  50. package/src/templates/claude/commands/why.md.tmpl +0 -10
package/dist/mcp.js CHANGED
@@ -1,3 +1,3 @@
1
- import{McpServer as V}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as W}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";import f from"node:fs";import c from"node:path";import m from"node:os";import{fileURLToPath as I}from"node:url";var $=I(import.meta.url),A=c.dirname($);function j(){return{searchRoots:[],portRange:[4200,4299],apiPort:4999,overrides:{},autoStart:[],profiles:{},tags:{},autoRestart:{enabled:!1,maxAttempts:5,windowMs:3e5},healthProbe:{enabled:!0,intervalMs:3e4,timeoutMs:2e3,path:"/",host:null,scheme:null,rejectUnauthorized:!1,fallbackHosts:["127.0.0.1","::1"]},logs:{enabled:!1,dir:c.join(m.homedir(),".daimon","logs"),maxFiles:5,maxBytesPerFile:1e7},depends:{},cascadeRestart:!1,history:{enabled:!0,path:c.join(m.homedir(),".daimon","history.db"),retentionDays:30},notifications:{enabled:!0,onError:!0,onUnhealthy:!0,tray:!1},staleDetect:{enabled:!0,silentMs:3e4},headless:!1,envFiles:{},requestLog:{enabled:!1,portOffset:1e3},metrics:{enabled:!1},editor:{scheme:"vscode"},apiToken:null}}function P(r){return r.startsWith("~/")||r.startsWith("~\\")?c.join(m.homedir(),r.slice(2)):r==="~"?m.homedir():r}function x(r,n){if(!r||typeof r!="object")throw new Error(`Config at ${n} is not a JSON object`);let t=r,e=j();if(t.searchRoots!==void 0){if(!Array.isArray(t.searchRoots)||!t.searchRoots.every(o=>typeof o=="string"||o&&typeof o=="object"&&typeof o.path=="string"))throw new Error(`Config "searchRoots" must be an array of strings or { path, viteSubfolders? } objects (${n})`);e.searchRoots=t.searchRoots}if(t.portRange!==void 0){if(!Array.isArray(t.portRange)||t.portRange.length!==2||typeof t.portRange[0]!="number"||typeof t.portRange[1]!="number"||t.portRange[0]>t.portRange[1])throw new Error(`Config "portRange" must be [min, max] numbers (${n})`);e.portRange=[t.portRange[0],t.portRange[1]]}if(t.apiPort!==void 0){if(typeof t.apiPort!="number")throw new Error(`Config "apiPort" must be a number (${n})`);e.apiPort=t.apiPort}if(t.overrides!==void 0){if(typeof t.overrides!="object"||t.overrides===null||Array.isArray(t.overrides))throw new Error(`Config "overrides" must be an object (${n})`);e.overrides=t.overrides}if(t.autoStart!==void 0){if(!Array.isArray(t.autoStart)||!t.autoStart.every(o=>typeof o=="string"))throw new Error(`Config "autoStart" must be an array of strings (${n})`);e.autoStart=t.autoStart}if(t.profiles!==void 0){if(typeof t.profiles!="object"||t.profiles===null||Array.isArray(t.profiles))throw new Error(`Config "profiles" must be an object (${n})`);for(let[o,i]of Object.entries(t.profiles))if(!Array.isArray(i)||!i.every(s=>typeof s=="string"))throw new Error(`Config "profiles.${o}" must be an array of strings (${n})`);e.profiles=t.profiles}if(t.tags!==void 0){if(typeof t.tags!="object"||t.tags===null||Array.isArray(t.tags))throw new Error(`Config "tags" must be an object (${n})`);e.tags=t.tags}if(t.autoRestart&&typeof t.autoRestart=="object"&&(e.autoRestart={...e.autoRestart,...t.autoRestart}),t.healthProbe&&typeof t.healthProbe=="object"&&(e.healthProbe={...e.healthProbe,...t.healthProbe}),t.logs&&typeof t.logs=="object"&&(e.logs={...e.logs,...t.logs},e.logs.dir=P(e.logs.dir)),t.depends&&typeof t.depends=="object"&&!Array.isArray(t.depends)){for(let[o,i]of Object.entries(t.depends))if(!Array.isArray(i)||!i.every(s=>typeof s=="string"))throw new Error(`Config "depends.${o}" must be an array of strings (${n})`);e.depends=t.depends}if(typeof t.cascadeRestart=="boolean"&&(e.cascadeRestart=t.cascadeRestart),t.history&&typeof t.history=="object"&&(e.history={...e.history,...t.history},e.history.path=P(e.history.path)),t.notifications&&typeof t.notifications=="object"&&(e.notifications={...e.notifications,...t.notifications}),t.staleDetect&&typeof t.staleDetect=="object"&&(e.staleDetect={...e.staleDetect,...t.staleDetect}),typeof t.headless=="boolean"&&(e.headless=t.headless),t.envFiles&&typeof t.envFiles=="object"&&!Array.isArray(t.envFiles)&&(e.envFiles=t.envFiles),t.requestLog&&typeof t.requestLog=="object"&&(e.requestLog={...e.requestLog,...t.requestLog}),t.metrics&&typeof t.metrics=="object"&&(e.metrics={...e.metrics,...t.metrics}),t.editor&&typeof t.editor=="object"){let o=t.editor.scheme;typeof o=="string"&&o.trim()&&(e.editor={scheme:o.trim()})}return(typeof t.apiToken=="string"||t.apiToken===null)&&(e.apiToken=t.apiToken),e}function E(){return{local:c.join(process.cwd(),"daimon.config.json"),user:c.join(m.homedir(),".daimon","config.json")}}function C(){let{local:r,user:n}=E();if(f.existsSync(r)){let o=JSON.parse(f.readFileSync(r,"utf8"));return{kind:"loaded",config:x(o,r),path:r}}if(f.existsSync(n)){let o=JSON.parse(f.readFileSync(n,"utf8"));return{kind:"loaded",config:x(o,n),path:n}}let e=[c.resolve(A,"..","daimon.config.example.json"),c.resolve(A,"..","..","daimon.config.example.json")].find(o=>f.existsSync(o));return f.mkdirSync(c.dirname(n),{recursive:!0}),e?f.copyFileSync(e,n):f.writeFileSync(n,JSON.stringify(j(),null,2)+`
2
- `,"utf8"),{kind:"stub-created",path:n}}import L from"node:fs";import h from"node:path";import U from"node:os";import{spawn as q}from"node:child_process";import{fileURLToPath as J}from"node:url";import _ from"node:fs";import R from"node:path";import{fileURLToPath as F}from"node:url";var O=R.dirname(F(import.meta.url));function M(){let r=[R.resolve(O,"..","package.json"),R.resolve(O,"..","..","package.json")];for(let n of r)try{return JSON.parse(_.readFileSync(n,"utf8"))}catch{}return{}}var S=M().version||"0.0.0";var G=h.join(U.homedir(),".daimon"),N=h.join(G,"daemon.lock");function B(r){try{return process.kill(r,0),!0}catch(n){return n&&n.code==="EPERM"}}function b(){try{let r=L.readFileSync(N,"utf8"),n=JSON.parse(r);if(!n||typeof n.pid!="number")return null;if(!B(n.pid)){try{L.unlinkSync(N)}catch{}return null}return n}catch{return null}}function H(){let r=h.dirname(J(import.meta.url));return h.join(r,"main.js")}async function D(r={}){let n={...process.env};r.port&&(n.DAIMON_PORT=String(r.port)),q(process.execPath,[H(),"--headless"],{detached:!0,stdio:"ignore",env:n,windowsHide:!0}).unref();let e=Date.now();for(;Date.now()-e<5e3;){let o=b();if(o&&(!r.port||o.apiPort===r.port))return o;await new Promise(i=>setTimeout(i,100))}throw new Error("daemon failed to start within 5s")}function z(){if(process.env.DAIMON_PORT){let n=Number(process.env.DAIMON_PORT);if(Number.isFinite(n)&&n>0)return n}let r=b();if(r)return r.apiPort;try{let n=C();if(n.kind==="loaded")return n.config.apiPort}catch{}return 4999}var K=()=>`http://127.0.0.1:${z()}`,T=!1;async function Q(){if(!T&&(T=!0,process.env.DAIMON_NO_SPAWN!=="1"&&!b()))try{let r=process.env.DAIMON_PORT?Number(process.env.DAIMON_PORT):void 0;await D({port:Number.isFinite(r)&&r>0?r:void 0})}catch{}}async function l(r,n="GET"){await Q();try{let t=await fetch(K()+r,{method:n}),e=await t.text();try{return{status:t.status,body:JSON.parse(e)}}catch{return{status:t.status,body:e}}}catch{return{status:0,body:{error:"daimon is not running \u2014 start it with: daimon daemon start --detach"}}}}function d(r){return{content:[{type:"text",text:JSON.stringify(r)}]}}function p(r){return{content:[{type:"text",text:JSON.stringify({error:r})}],isError:!0}}async function X(){let r=new V({name:"daimon",version:S});r.registerTool("list_apps",{description:"List all known apps with current status, port, health, etc.",inputSchema:{}},async()=>{let t=await l("/api/apps");return t.status===0?p(t.body?.error||"unknown"):d(t.body)}),r.registerTool("get_status",{description:"Get the current status of one app.",inputSchema:{name:a.string()}},async({name:t})=>{let e=await l(`/api/apps/${encodeURIComponent(t)}`);return e.status===0?p(e.body?.error||"unknown"):e.status===404?p("unknown app"):d(e.body)}),r.registerTool("get_errors",{description:"Get errors for an app. Supports --since duration, --since-last cursor, optional structured form.",inputSchema:{name:a.string(),since:a.string().optional(),sinceLast:a.boolean().optional(),client:a.string().optional(),structured:a.boolean().optional()}},async({name:t,since:e,sinceLast:o,client:i,structured:s})=>{let u=`/api/apps/${encodeURIComponent(t)}/errors`,w=new URLSearchParams;o?(u+="/since-last",i&&w.set("client",i)):e&&w.set("since",e);let v=w.toString(),g=await l(u+(v?"?"+v:""));if(g.status===0)return p(g.body?.error||"unknown");if(g.status===404)return p("unknown app");let y=g.body;return s&&Array.isArray(y)&&(y=y.map(k=>k.parsed??{message:k.message})),d(y)}),r.registerTool("get_logs",{description:"Get recent log lines for an app.",inputSchema:{name:a.string(),tail:a.number().int().positive().optional(),since:a.string().optional()}},async({name:t,tail:e,since:o})=>{let i=new URLSearchParams;e&&i.set("tail",String(e)),o&&i.set("since",o);let s=i.toString(),u=await l(`/api/apps/${encodeURIComponent(t)}/logs${s?"?"+s:""}`);return u.status===0?p(u.body?.error||"unknown"):u.status===404?p("unknown app"):d(u.body)});for(let t of["start","stop","restart"])r.registerTool(`${t}_app`,{description:`${t} an app.`,inputSchema:{name:a.string()}},async({name:e})=>{let o=await l(`/api/apps/${encodeURIComponent(e)}/${t}`,"POST");return o.status===0?p(o.body?.error||"unknown"):d(o.body)});r.registerTool("wait_for_app",{description:"Block until app reaches the given state or timeout (max 600s).",inputSchema:{name:a.string(),until:a.enum(["serving","healthy","stopped","error"]).optional(),timeout:a.number().int().positive().max(600).optional()}},async({name:t,until:e,timeout:o})=>{let i=new URLSearchParams;i.set("until",e||"serving"),i.set("timeout",String(Math.min(o??120,600)));let s=await l(`/api/apps/${encodeURIComponent(t)}/wait?${i.toString()}`);return s.status===0?p(s.body?.error||"unknown"):s.status===404?p("unknown app"):d(s.body)});let n=new W;await r.connect(n)}X().catch(r=>{process.stderr.write(`[daimon-mcp] fatal: ${r?.stack||r}
1
+ import{McpServer as B}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as W}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";import l from"node:fs";import f from"node:path";import m from"node:os";import{fileURLToPath as M}from"node:url";var D=M(import.meta.url),P=f.dirname(D);function x(){return{searchRoots:[],portRange:[4200,4299],apiPort:4999,overrides:{},autoStart:[],profiles:{},tags:{},autoRestart:{enabled:!1,maxAttempts:5,windowMs:3e5},healthProbe:{enabled:!0,intervalMs:3e4,timeoutMs:2e3,path:"/",host:null,scheme:null,rejectUnauthorized:!1,fallbackHosts:["127.0.0.1","::1"]},logs:{enabled:!1,dir:f.join(m.homedir(),".daimon","logs"),maxFiles:5,maxBytesPerFile:1e7},depends:{},cascadeRestart:!1,history:{enabled:!0,path:f.join(m.homedir(),".daimon","history.db"),retentionDays:30},notifications:{enabled:!0,onError:!0,onUnhealthy:!0,tray:!1},staleDetect:{enabled:!0,silentMs:3e4},headless:!1,envFiles:{},requestLog:{enabled:!1,portOffset:1e3},metrics:{enabled:!1},editor:{scheme:"vscode"},apiToken:null,output:{format:"compact",ndjson:!1},doctor:{autoFix:{onInit:!1,permitted:["orphan-daemon","stale-lock","missing-search-root","corrupt-history-db"]}},dashboard:{theme:"auto",density:"comfortable"}}}function A(o){return o.startsWith("~/")||o.startsWith("~\\")?f.join(m.homedir(),o.slice(2)):o==="~"?m.homedir():o}function C(o,n){if(!o||typeof o!="object")throw new Error(`Config at ${n} is not a JSON object`);let t=o,e=x();if(t.searchRoots!==void 0){if(!Array.isArray(t.searchRoots)||!t.searchRoots.every(r=>typeof r=="string"||r&&typeof r=="object"&&typeof r.path=="string"))throw new Error(`Config "searchRoots" must be an array of strings or { path, viteSubfolders? } objects (${n})`);e.searchRoots=t.searchRoots}if(t.portRange!==void 0){if(!Array.isArray(t.portRange)||t.portRange.length!==2||typeof t.portRange[0]!="number"||typeof t.portRange[1]!="number"||t.portRange[0]>t.portRange[1])throw new Error(`Config "portRange" must be [min, max] numbers (${n})`);e.portRange=[t.portRange[0],t.portRange[1]]}if(t.apiPort!==void 0){if(typeof t.apiPort!="number")throw new Error(`Config "apiPort" must be a number (${n})`);e.apiPort=t.apiPort}if(t.overrides!==void 0){if(typeof t.overrides!="object"||t.overrides===null||Array.isArray(t.overrides))throw new Error(`Config "overrides" must be an object (${n})`);e.overrides=t.overrides}if(t.autoStart!==void 0){if(!Array.isArray(t.autoStart)||!t.autoStart.every(r=>typeof r=="string"))throw new Error(`Config "autoStart" must be an array of strings (${n})`);e.autoStart=t.autoStart}if(t.profiles!==void 0){if(typeof t.profiles!="object"||t.profiles===null||Array.isArray(t.profiles))throw new Error(`Config "profiles" must be an object (${n})`);for(let[r,s]of Object.entries(t.profiles))if(!Array.isArray(s)||!s.every(i=>typeof i=="string"))throw new Error(`Config "profiles.${r}" must be an array of strings (${n})`);e.profiles=t.profiles}if(t.tags!==void 0){if(typeof t.tags!="object"||t.tags===null||Array.isArray(t.tags))throw new Error(`Config "tags" must be an object (${n})`);e.tags=t.tags}if(t.autoRestart&&typeof t.autoRestart=="object"&&(e.autoRestart={...e.autoRestart,...t.autoRestart}),t.healthProbe&&typeof t.healthProbe=="object"&&(e.healthProbe={...e.healthProbe,...t.healthProbe}),t.logs&&typeof t.logs=="object"&&(e.logs={...e.logs,...t.logs},e.logs.dir=A(e.logs.dir)),t.depends&&typeof t.depends=="object"&&!Array.isArray(t.depends)){for(let[r,s]of Object.entries(t.depends))if(!Array.isArray(s)||!s.every(i=>typeof i=="string"))throw new Error(`Config "depends.${r}" must be an array of strings (${n})`);e.depends=t.depends}if(typeof t.cascadeRestart=="boolean"&&(e.cascadeRestart=t.cascadeRestart),t.history&&typeof t.history=="object"&&(e.history={...e.history,...t.history},e.history.path=A(e.history.path)),t.notifications&&typeof t.notifications=="object"&&(e.notifications={...e.notifications,...t.notifications}),t.staleDetect&&typeof t.staleDetect=="object"&&(e.staleDetect={...e.staleDetect,...t.staleDetect}),typeof t.headless=="boolean"&&(e.headless=t.headless),t.envFiles&&typeof t.envFiles=="object"&&!Array.isArray(t.envFiles)&&(e.envFiles=t.envFiles),t.requestLog&&typeof t.requestLog=="object"&&(e.requestLog={...e.requestLog,...t.requestLog}),t.metrics&&typeof t.metrics=="object"&&(e.metrics={...e.metrics,...t.metrics}),t.editor&&typeof t.editor=="object"){let r=t.editor.scheme;typeof r=="string"&&r.trim()&&(e.editor={scheme:r.trim()})}if((typeof t.apiToken=="string"||t.apiToken===null)&&(e.apiToken=t.apiToken),t.output&&typeof t.output=="object"){let r=t.output;(r.format==="compact"||r.format==="full")&&(e.output.format=r.format),typeof r.ndjson=="boolean"&&(e.output.ndjson=r.ndjson)}if(t.doctor&&typeof t.doctor=="object"){let r=t.doctor.autoFix;r&&typeof r=="object"&&(typeof r.onInit=="boolean"&&(e.doctor.autoFix.onInit=r.onInit),Array.isArray(r.permitted)&&(e.doctor.autoFix.permitted=r.permitted.filter(s=>typeof s=="string")))}if(t.dashboard&&typeof t.dashboard=="object"){let r=t.dashboard;(r.theme==="auto"||r.theme==="light"||r.theme==="dark")&&(e.dashboard.theme=r.theme),(r.density==="comfortable"||r.density==="compact")&&(e.dashboard.density=r.density)}return e}function N(){return{local:f.join(process.cwd(),"daimon.config.json"),user:f.join(m.homedir(),".daimon","config.json")}}function j(){let{local:o,user:n}=N();if(l.existsSync(o)){let r=JSON.parse(l.readFileSync(o,"utf8"));return{kind:"loaded",config:C(r,o),path:o}}if(l.existsSync(n)){let r=JSON.parse(l.readFileSync(n,"utf8"));return{kind:"loaded",config:C(r,n),path:n}}let e=[f.resolve(P,"..","daimon.config.example.json"),f.resolve(P,"..","..","daimon.config.example.json")].find(r=>l.existsSync(r));return l.mkdirSync(f.dirname(n),{recursive:!0}),e?l.copyFileSync(e,n):l.writeFileSync(n,JSON.stringify(x(),null,2)+`
2
+ `,"utf8"),{kind:"stub-created",path:n}}import O from"node:fs";import y from"node:path";import U from"node:os";import{spawn as q}from"node:child_process";import{fileURLToPath as J}from"node:url";import $ from"node:fs";import v from"node:path";import{fileURLToPath as E}from"node:url";var _=v.dirname(E(import.meta.url));function F(){let o=[v.resolve(_,"..","package.json"),v.resolve(_,"..","..","package.json")];for(let n of o)try{return JSON.parse($.readFileSync(n,"utf8"))}catch{}return{}}var S=F().version||"0.0.0";var G=y.join(U.homedir(),".daimon"),T=y.join(G,"daemon.lock");function H(o){try{return process.kill(o,0),!0}catch(n){return n&&n.code==="EPERM"}}function b(){try{let o=O.readFileSync(T,"utf8"),n=JSON.parse(o);if(!n||typeof n.pid!="number")return null;if(!H(n.pid)){try{O.unlinkSync(T)}catch{}return null}return n}catch{return null}}function V(){let o=y.dirname(J(import.meta.url));return y.join(o,"main.js")}async function I(o={}){let n={...process.env};o.port&&(n.DAIMON_PORT=String(o.port)),q(process.execPath,[V(),"--headless"],{detached:!0,stdio:"ignore",env:n,windowsHide:!0}).unref();let e=Date.now();for(;Date.now()-e<5e3;){let r=b();if(r&&(!o.port||r.apiPort===o.port))return r;await new Promise(s=>setTimeout(s,100))}throw new Error("daemon failed to start within 5s")}function z(){if(process.env.DAIMON_PORT){let n=Number(process.env.DAIMON_PORT);if(Number.isFinite(n)&&n>0)return n}let o=b();if(o)return o.apiPort;try{let n=j();if(n.kind==="loaded")return n.config.apiPort}catch{}return 4999}var K=()=>`http://127.0.0.1:${z()}`,L=!1;async function Q(){if(!L&&(L=!0,process.env.DAIMON_NO_SPAWN!=="1"&&!b()))try{let o=process.env.DAIMON_PORT?Number(process.env.DAIMON_PORT):void 0;await I({port:Number.isFinite(o)&&o>0?o:void 0})}catch{}}async function c(o,n="GET"){await Q();try{let t=await fetch(K()+o,{method:n}),e=await t.text();try{return{status:t.status,body:JSON.parse(e)}}catch{return{status:t.status,body:e}}}catch{return{status:0,body:{error:"daimon is not running \u2014 start it with: daimon daemon start --detach"}}}}function u(o){return{content:[{type:"text",text:JSON.stringify(o)}]}}function p(o){return{content:[{type:"text",text:JSON.stringify({error:o})}],isError:!0}}async function X(){let o=new B({name:"daimon",version:S});o.registerTool("list_apps",{description:"List apps in compact form: name, status, port, health, errCount, lastChangeMs. Use list_apps_full for the verbose v0.4 shape.",inputSchema:{}},async()=>{let t=await c("/api/apps?format=compact");return t.status===0?p(t.body?.error||"unknown"):u(t.body)}),o.registerTool("list_apps_full",{description:"List apps in the verbose v0.4 form (uptimeMs, lastCompileMs, metrics, etc.). Heavier \u2014 prefer list_apps unless you need extra fields.",inputSchema:{}},async()=>{let t=await c("/api/apps?format=full");return t.status===0?p(t.body?.error||"unknown"):u(t.body)}),o.registerTool("get_status",{description:"Compact status: name, status, port, url, health, errCount, lastChangeMs, uptime. Use get_status_full for the verbose v0.4 shape.",inputSchema:{name:a.string()}},async({name:t})=>{let e=await c(`/api/apps/${encodeURIComponent(t)}?format=compact`);return e.status===0?p(e.body?.error||"unknown"):e.status===404?p("unknown app"):u(e.body)}),o.registerTool("get_status_full",{description:"Verbose v0.4 status form including events, compile history, metrics. Prefer get_status unless you need extra fields.",inputSchema:{name:a.string()}},async({name:t})=>{let e=await c(`/api/apps/${encodeURIComponent(t)}?format=full`);return e.status===0?p(e.body?.error||"unknown"):e.status===404?p("unknown app"):u(e.body)}),o.registerTool("get_errors",{description:"Get errors for an app. Supports --since duration, --since-last cursor, optional structured form.",inputSchema:{name:a.string(),since:a.string().optional(),sinceLast:a.boolean().optional(),client:a.string().optional(),structured:a.boolean().optional()}},async({name:t,since:e,sinceLast:r,client:s,structured:i})=>{let d=`/api/apps/${encodeURIComponent(t)}/errors`,w=new URLSearchParams;r?(d+="/since-last",s&&w.set("client",s)):e&&w.set("since",e);let k=w.toString(),g=await c(d+(k?"?"+k:""));if(g.status===0)return p(g.body?.error||"unknown");if(g.status===404)return p("unknown app");let h=g.body;return i&&Array.isArray(h)&&(h=h.map(R=>R.parsed??{message:R.message})),u(h)}),o.registerTool("get_logs",{description:"Get recent log lines for an app.",inputSchema:{name:a.string(),tail:a.number().int().positive().optional(),since:a.string().optional()}},async({name:t,tail:e,since:r})=>{let s=new URLSearchParams;e&&s.set("tail",String(e)),r&&s.set("since",r);let i=s.toString(),d=await c(`/api/apps/${encodeURIComponent(t)}/logs${i?"?"+i:""}`);return d.status===0?p(d.body?.error||"unknown"):d.status===404?p("unknown app"):u(d.body)});for(let t of["start","stop","restart"])o.registerTool(`${t}_app`,{description:`${t} an app.`,inputSchema:{name:a.string()}},async({name:e})=>{let r=await c(`/api/apps/${encodeURIComponent(e)}/${t}`,"POST");return r.status===0?p(r.body?.error||"unknown"):u(r.body)});o.registerTool("overview",{description:'Decision-ready snapshot of the workspace: totals, byStatus, needsAttention (with first parsed error per failing app), recentlyChanged. The recommended first call in a session \u2014 answers "what is going on right now?" in one round-trip.',inputSchema:{workspace:a.string().optional(),profile:a.string().optional()}},async({workspace:t,profile:e})=>{let r=new URLSearchParams;t&&r.set("workspace",t),e&&r.set("profile",e);let s=r.toString(),i=await c("/api/overview"+(s?"?"+s:""));return i.status===0?p(i.body?.error||"unknown"):u(i.body)}),o.registerTool("ensure",{description:'One-call lifecycle: if the app is stopped/crashed/error, start it; then block until it reaches the target state. Idempotent \u2014 returns immediately on already-terminal apps. Replaces the list\u2192status\u2192start\u2192wait\u2192status sequence. Returns compact AppSummary plus _meta.startedFromState / waitedMs. On timeout the body is { error: "timeout", state, _meta: { timedOut: true } } \u2014 treat as exit 2 equivalent.',inputSchema:{name:a.string(),until:a.enum(["serving","healthy"]).optional(),timeoutMs:a.number().int().positive().max(6e5).optional()}},async({name:t,until:e,timeoutMs:r})=>{let s=new URLSearchParams;s.set("until",e||"healthy"),s.set("timeoutMs",String(Math.min(r??18e4,6e5)));let i=await c(`/api/apps/${encodeURIComponent(t)}/ensure?${s.toString()}`,"POST");return i.status===0?p(i.body?.error||"unknown"):i.status===404?p("unknown app"):u(i.body)}),o.registerTool("ensure_up",{description:"One-call profile bring-up: cascade-start every app in the profile (resolving deps) and block until each reaches the target. Returns per-app terminal state plus _meta.totalMs. Use this instead of daimon up + per-app waits.",inputSchema:{profile:a.string(),until:a.enum(["serving","healthy"]).optional(),timeoutMs:a.number().int().positive().max(12e5).optional()}},async({profile:t,until:e,timeoutMs:r})=>{let s=new URLSearchParams;s.set("until",e||"healthy"),s.set("timeoutMs",String(Math.min(r??3e5,12e5)));let i=await c(`/api/profiles/${encodeURIComponent(t)}/ensure-up?${s.toString()}`,"POST");return i.status===0?p(i.body?.error||"unknown"):i.status===404?p("unknown profile"):u(i.body)}),o.registerTool("wait_for_app",{description:"Block until app reaches the given state or timeout (max 600s).",inputSchema:{name:a.string(),until:a.enum(["serving","healthy","stopped","error"]).optional(),timeout:a.number().int().positive().max(600).optional()}},async({name:t,until:e,timeout:r})=>{let s=new URLSearchParams;s.set("until",e||"serving"),s.set("timeout",String(Math.min(r??120,600)));let i=await c(`/api/apps/${encodeURIComponent(t)}/wait?${s.toString()}`);return i.status===0?p(i.body?.error||"unknown"):i.status===404?p("unknown app"):u(i.body)});let n=new W;await o.connect(n)}X().catch(o=>{process.stderr.write(`[daimon-mcp] fatal: ${o?.stack||o}
3
3
  `),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daimon",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Local dev-server manager for Angular/Nx/Vite/Storybook — TUI, loopback HTTP API, JSON CLI, and MCP server for Claude Code",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Yosi Azulay (https://flycotech.com)",
@@ -31,10 +31,10 @@
31
31
  },
32
32
  "files": [
33
33
  "dist",
34
- "src/dashboard.html",
35
34
  "src/templates",
36
35
  "README.md",
37
- "LICENSE"
36
+ "LICENSE",
37
+ "CHANGELOG.md"
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "tsc",
@@ -44,7 +44,8 @@
44
44
  "test": "node --test test/depends.test.mjs test/bundle.test.mjs test/notifier.test.mjs test/regression.test.mjs",
45
45
  "clean:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
46
46
  "build:bundle": "npm run clean:dist && esbuild src/cli.ts src/main.ts src/mcp.ts --bundle --platform=node --target=node20 --format=esm --minify --legal-comments=none --outdir=dist --packages=external",
47
- "prepublishOnly": "npm run build && npm test && npm run build:bundle"
47
+ "build:dashboard": "node -e \"const fs=require('fs');if(fs.existsSync('dashboard/node_modules')){require('child_process').execSync('npm run build',{cwd:'dashboard',stdio:'inherit'});}else{console.log('[daimon] dashboard/node_modules missing skipping ng build. Run: (cd dashboard && npm install) to enable the Angular SPA bundle.');}\"",
48
+ "prepublishOnly": "npm run build && npm test && npm run build:bundle && npm run build:dashboard"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=20"
@@ -1,44 +1,36 @@
1
1
  ---
2
2
  name: daimon
3
- description: Manage local Angular/Nx/Vite dev servers via the daimon CLI on the loopback API. Use for starting, stopping, inspecting status, surfacing dedup'd errors, tailing logs, and blocking until a target state is reached. Local-only; no remote access; no source-code mutation.
3
+ description: Local-only manager for Angular/Nx/Vite/Storybook dev servers via the `daimon` CLI on 127.0.0.1. Start/stop/inspect dev servers, surface dedup'd errors, tail logs, and block until a target state is reached. Read-only outside `~/.daimon/*` and daimon's own config. Never bind non-loopback. Never mutate user source.
4
4
  daimon-version: {{daimon_version}}
5
5
  generated-at: {{generated_at}}
6
6
  ---
7
7
 
8
- # daimon skill
8
+ # daimon
9
9
 
10
- You are operating inside a repo managed by **daimon** a local daemon that owns the lifecycle of multiple dev-server processes. Use `daimon` instead of running `npm start`, `nx serve`, or `ng serve` yourself.
10
+ Loopback CLI at `http://127.0.0.1:{{api_port}}`. Compact JSON on stdout, errors as compact JSON on stderr (exit 1; `daimon wait` timeout exits 2). Daemon auto-spawns on first call (set `DAIMON_NO_SPAWN=1` to skip).
11
11
 
12
- ## Available CLI surface
12
+ ## Verbs
13
13
 
14
- The daemon auto-spawns the first time you call any CLI command. Suppress with `DAIMON_NO_SPAWN=1` for read-only scripts.
14
+ - `daimon overview` workspace snapshot (totals, needs-attention, recently-changed). **Start here.**
15
+ - `daimon list` → `{name,status,port,health,errCount,lastChangeMs}` per app. `--full` for v0.4 form.
16
+ - `daimon status <name>` → compact `{name,status,port,url,health,errCount,lastChangeMs,uptime}`. `--full` for events/metrics.
17
+ - `daimon ensure <name> [--until serving|healthy] [--timeout 180]` → one call: starts if stopped, blocks to target, returns terminal state.
18
+ - `daimon ensure-up <profile>` → same for an entire profile; cascades dependencies.
19
+ - `daimon errors <name> [--since 5m|--since-last --client claude] [--structured]` → dedup'd errors, compact `{file,line,col,code,message}`.
20
+ - `daimon logs <name> [--tail N] [--since 30s]` → recent log lines.
21
+ - `daimon start|stop|restart <name>` · `daimon up|down [<profile>]` · `daimon wait <name> --until healthy --timeout 60s`.
22
+ - `daimon why <name>` → last status transition + 5 events. `daimon why-empty` → explain an empty `list`.
23
+ - `daimon doctor [--auto-fix] [--dry-run]` → config + env sanity check; auto-fix repairs orphan daemon, stale lock, missing search root, corrupt history DB.
15
24
 
16
- ```
17
- {{commands_table}}
18
- ```
25
+ ## Patterns
19
26
 
20
- API base: `http://127.0.0.1:{{api_port}}`. All output is compact JSON.
27
+ - **First call in a session:** `daimon overview`. If `totals.apps === 0`, the response includes `_meta.suggestion` — follow it.
28
+ - **Bring up one app idempotently:** `daimon ensure <name>`. Replaces the old `status → start → wait → status` sequence.
29
+ - **Diff-style error checks:** `daimon errors <name> --since-last --client claude` returns only what's new since the last call.
21
30
 
22
- ## When to use what
31
+ ## Do not
23
32
 
24
- - **Find out what's running**: `daimon list`
25
- - **Check one app**: `daimon status <name>`
26
- - **Block until ready** (preferred over polling): `daimon wait <name> --until healthy --timeout 60s`
27
- - **Show errors**: `daimon errors <name> --since 5m` (or `--structured` for parsed TS errors, or `--since-last --client claude` for diff-mode)
28
- - **Tail logs**: `daimon logs <name> --tail 100`
29
- - **Investigate failure**: `daimon why <name>` returns the last status transition plus 5 preceding events
30
- - **Bring up a profile**: `daimon up <profile>` — starts deps in order, blocks until healthy
31
- - **Sanity-check setup**: `daimon doctor` (does not need the daemon)
32
-
33
- ## What not to do
34
-
35
- - Do not start `nx serve` / `ng serve` directly.
36
- - Do not parse log files — `daimon errors` is already dedup'd and structured.
37
- - Do not assume an app is healthy because it's "serving"; use `--until healthy`.
38
- - Do not retry on a timeout from `daimon wait` without first calling `daimon errors` and `daimon why`.
39
-
40
- ## Exit codes
41
-
42
- - `0` success
43
- - `1` generic error (unknown app, daemon down, etc.)
44
- - `2` `daimon wait` timed out
33
+ - Do not run `nx serve`, `ng serve`, or `npm start` directly — go through `daimon ensure`.
34
+ - Do not parse log files; `daimon errors` is already dedup'd and parsed.
35
+ - Do not retry blindly on `daimon wait` timeout (exit 2) read `daimon errors` and `daimon why` first.
36
+ - Do not bind non-loopback ports. Do not modify user source files via daimon.
@@ -1,451 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <title>daimon</title>
6
- <style>
7
- body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; background: #0f1115; color: #e6e6e6; margin: 0; padding: 20px; }
8
- h1 { font-size: 18px; font-weight: 600; margin: 0 0 8px; letter-spacing: 0.5px; }
9
- .tabs { display: flex; gap: 4px; margin-bottom: 12px; }
10
- .tab { padding: 6px 12px; background: #1a1d24; color: #9aa3b2; border: 1px solid #232730; border-radius: 4px; cursor: pointer; font-size: 13px; }
11
- .tab.active { background: #2c313b; color: #e6e6e6; border-color: #353a45; }
12
- table { width: 100%; border-collapse: collapse; font-size: 14px; }
13
- th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #232730; vertical-align: middle; }
14
- th { color: #9aa3b2; font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
15
- .badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: 500; }
16
- .s-stopped { background: #2b2f38; color: #9aa3b2; }
17
- .s-starting, .s-compiling { background: #423a17; color: #e0c060; }
18
- .s-serving { background: #19402a; color: #62d28a; }
19
- .s-error { background: #4a1f23; color: #ef6f74; }
20
- .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
21
- .h-healthy { background: #62d28a; }
22
- .h-unhealthy { background: #ef6f74; }
23
- .h-unknown { background: #555a66; }
24
- a { color: #8ab4ff; text-decoration: none; }
25
- a:hover { text-decoration: underline; }
26
- .err { color: #ef6f74; }
27
- .meta { color: #8a93a2; font-size: 12px; margin-bottom: 12px; }
28
- .num { text-align: right; font-variant-numeric: tabular-nums; color: #c8cdd6; }
29
- button { background: #232730; color: #e6e6e6; border: 1px solid #353a45; padding: 3px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; margin-right: 4px; }
30
- button:hover:not(:disabled) { background: #2c313b; }
31
- button:disabled { opacity: 0.4; cursor: default; }
32
- .chev { display: inline-block; width: 12px; cursor: pointer; user-select: none; color: #9aa3b2; }
33
- .drawer { background: #14171d; padding: 10px 18px 12px 36px; border-bottom: 1px solid #232730; }
34
- .errrow { display: grid; grid-template-columns: 12px 1fr 90px 90px auto; gap: 10px; padding: 4px 0; font-size: 13px; align-items: baseline; border-bottom: 1px solid #1a1d24; }
35
- .errrow .file { color: #8ab4ff; font-family: ui-monospace, Consolas, monospace; }
36
- .errrow .code { color: #d0a060; font-family: ui-monospace, Consolas, monospace; font-size: 12px; }
37
- .errrow .msg { color: #d8d8d8; }
38
- .errrow .count { color: #c8cdd6; font-variant-numeric: tabular-nums; text-align: right; }
39
- .errrow .seen { color: #6c757d; font-variant-numeric: tabular-nums; text-align: right; font-size: 11px; }
40
- .sev { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #ef6f74; }
41
- .errrow button { font-size: 10px; padding: 2px 6px; }
42
- .panel { background: #14171d; padding: 14px; border-radius: 6px; border: 1px solid #232730; margin-top: 12px; }
43
- .toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
44
- .toolbar input[type=text] { background: #0f1115; color: #e6e6e6; border: 1px solid #353a45; padding: 4px 8px; border-radius: 4px; font-size: 13px; min-width: 240px; }
45
- .toolbar select { background: #0f1115; color: #e6e6e6; border: 1px solid #353a45; padding: 4px 8px; border-radius: 4px; font-size: 13px; }
46
- .group-h { color: #9aa3b2; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; margin: 12px 0 4px; }
47
- </style>
48
- </head>
49
- <body>
50
- <h1 style="display:flex;align-items:center;justify-content:space-between">daimon <button id="gear" title="Configuration" style="font-size:14px;padding:4px 10px;margin:0">⚙ config</button></h1>
51
- <div class="tabs">
52
- <div class="tab active" data-tab="apps">Apps</div>
53
- <div class="tab" data-tab="all-errors">All errors</div>
54
- </div>
55
-
56
- <div id="config-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:100;align-items:flex-start;justify-content:center;padding:40px 20px;overflow:auto">
57
- <div style="background:#14171d;border:1px solid #353a45;border-radius:8px;padding:18px;max-width:900px;width:100%;color:#e6e6e6">
58
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
59
- <div style="font-weight:600;font-size:15px">Configuration</div>
60
- <div><button id="cfg-reload">soft reload</button><button id="cfg-save">save</button><button id="cfg-close">close</button></div>
61
- </div>
62
- <div id="cfg-toast" style="margin-bottom:8px;font-size:12px;color:#d0a060;min-height:14px"></div>
63
- <div class="tabs" style="margin-bottom:8px">
64
- <div class="tab active" data-cfgtab="perapp">Per-app</div>
65
- <div class="tab" data-cfgtab="global">Global</div>
66
- </div>
67
- <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
68
- <label style="color:#9aa3b2;font-size:12px">apply preset:</label>
69
- <select id="preset-select" style="background:#0f1115;color:#e6e6e6;border:1px solid #353a45;padding:3px 8px;border-radius:3px"><option value="">—</option></select>
70
- <button id="preset-apply">load into patch</button>
71
- </div>
72
- <div id="cfg-perapp"></div>
73
- <div id="cfg-global" style="display:none">
74
- <div style="color:#9aa3b2;font-size:12px;margin-bottom:6px">JSON merge patch. Saving sends only this object to the server.</div>
75
- <textarea id="cfg-textarea" style="width:100%;height:380px;background:#0f1115;color:#e6e6e6;border:1px solid #353a45;padding:8px;border-radius:4px;font-family:ui-monospace,Consolas,monospace;font-size:12px"></textarea>
76
- </div>
77
- </div>
78
- </div>
79
- <div class="meta" id="meta">loading…</div>
80
-
81
- <div id="view-apps">
82
- <table>
83
- <thead>
84
- <tr>
85
- <th style="width:18px"></th>
86
- <th>name</th><th>status</th><th>health</th><th>port</th><th>url</th>
87
- <th>errors</th><th>uptime</th><th class="num">cpu%</th><th class="num">mem MB</th>
88
- <th>recent compile</th><th>bundle</th><th>actions</th>
89
- </tr>
90
- </thead>
91
- <tbody id="rows"></tbody>
92
- </table>
93
- </div>
94
-
95
- <div id="view-all-errors" style="display:none">
96
- <div class="toolbar">
97
- <input type="text" id="all-err-search" placeholder="search (substring)…" />
98
- <label style="color:#9aa3b2;font-size:12px">group:</label>
99
- <select id="all-err-group">
100
- <option value="app">by app</option>
101
- <option value="file">by file</option>
102
- <option value="code">by code</option>
103
- </select>
104
- <span style="color:#6c757d;font-size:12px" id="all-err-count"></span>
105
- </div>
106
- <div id="all-err-body" class="panel"></div>
107
- </div>
108
-
109
- <script>
110
- let editorScheme = 'vscode';
111
- const expanded = new Set();
112
- const errorCache = new Map();
113
- let currentTab = 'apps';
114
- let configCache = null;
115
- let configEtag = '';
116
- const tokenKey = 'daimon.token.' + location.port;
117
-
118
- function loadToken() { try { return localStorage.getItem(tokenKey) || ''; } catch { return ''; } }
119
- function saveToken(t) { try { if (t) localStorage.setItem(tokenKey, t); else localStorage.removeItem(tokenKey); } catch {} }
120
- function authedHeaders(extra = {}) {
121
- const tok = loadToken();
122
- return tok ? { ...extra, 'Authorization': 'Bearer ' + tok } : extra;
123
- }
124
- async function authedFetch(url, init = {}) {
125
- init.headers = authedHeaders(init.headers || {});
126
- let res = await fetch(url, init);
127
- if (res.status === 401) {
128
- const t = prompt('daimon API token:');
129
- if (t) {
130
- saveToken(t);
131
- init.headers = authedHeaders((init.headers && init.headers['Content-Type']) ? { 'Content-Type': init.headers['Content-Type'] } : {});
132
- res = await fetch(url, init);
133
- }
134
- }
135
- return res;
136
- }
137
- function setCfgToast(msg, color) { const t = document.getElementById('cfg-toast'); t.style.color = color || '#d0a060'; t.textContent = msg; }
138
-
139
- document.querySelectorAll('.tab').forEach(t => {
140
- t.addEventListener('click', () => {
141
- currentTab = t.dataset.tab;
142
- document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x === t));
143
- document.getElementById('view-apps').style.display = currentTab === 'apps' ? '' : 'none';
144
- document.getElementById('view-all-errors').style.display = currentTab === 'all-errors' ? '' : 'none';
145
- tick();
146
- });
147
- });
148
-
149
- function fmtUptime(ms) {
150
- if (ms == null) return '';
151
- const s = Math.floor(ms / 1000);
152
- if (s < 60) return s + 's';
153
- const m = Math.floor(s / 60);
154
- if (m < 60) return m + 'm ' + (s % 60) + 's';
155
- const h = Math.floor(m / 60);
156
- return h + 'h ' + (m % 60) + 'm';
157
- }
158
- function sparkline(values) {
159
- if (!values || !values.length) return '';
160
- const w = 60, h = 16, max = Math.max(...values, 1);
161
- const step = values.length > 1 ? w / (values.length - 1) : 0;
162
- const pts = values.map((v, i) => `${(i * step).toFixed(1)},${(h - (v / max) * (h - 2) - 1).toFixed(1)}`).join(' ');
163
- return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><polyline fill="none" stroke="#8ab4ff" stroke-width="1.2" points="${pts}"/></svg>`;
164
- }
165
- function editorUrl(file, line, col) {
166
- if (!file) return '#';
167
- const safe = String(file).replace(/\\/g, '/');
168
- const suffix = (line ? ':' + line : '') + (col ? ':' + col : '');
169
- if (editorScheme.includes('://')) return editorScheme + safe + suffix;
170
- return editorScheme + '://file/' + safe + suffix;
171
- }
172
- function escapeHtml(s) {
173
- return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
174
- }
175
- async function action(name, kind, btn) {
176
- const buttons = btn.parentNode.querySelectorAll('button');
177
- buttons.forEach(b => b.disabled = true);
178
- try { await authedFetch('/api/apps/' + encodeURIComponent(name) + '/' + kind, { method: 'POST' }); } catch {}
179
- tick();
180
- }
181
- window.action = action;
182
- async function copyError(err) {
183
- const payload = { file: err.parsed?.file ?? null, line: err.parsed?.line ?? null, col: err.parsed?.col ?? null, code: err.parsed?.code ?? null, message: err.parsed?.message ?? err.message };
184
- try { await navigator.clipboard.writeText(JSON.stringify(payload)); } catch {}
185
- }
186
- window.copyError = copyError;
187
- const logStreams = new Map();
188
- const logBuf = new Map();
189
- function startLogStream(name) {
190
- if (logStreams.has(name)) return;
191
- try {
192
- const es = new EventSource('/api/apps/' + encodeURIComponent(name) + '/logs/stream');
193
- logStreams.set(name, es);
194
- if (!logBuf.has(name)) logBuf.set(name, []);
195
- es.onmessage = ev => {
196
- try {
197
- const d = JSON.parse(ev.data);
198
- const arr = logBuf.get(name);
199
- arr.push(d.line);
200
- if (arr.length > 200) arr.splice(0, arr.length - 200);
201
- const slot = document.querySelector(`[data-livelog="${name}"]`);
202
- if (slot) slot.textContent = arr.slice(-12).join('\n');
203
- } catch {}
204
- };
205
- es.onerror = () => { es.close(); logStreams.delete(name); };
206
- } catch {}
207
- }
208
- function stopLogStream(name) {
209
- const es = logStreams.get(name);
210
- if (es) { try { es.close(); } catch {}; logStreams.delete(name); }
211
- }
212
- async function toggleDrawer(name) {
213
- if (expanded.has(name)) { expanded.delete(name); stopLogStream(name); }
214
- else { expanded.add(name); await fetchErrors(name); startLogStream(name); }
215
- tick();
216
- }
217
- window.toggleDrawer = toggleDrawer;
218
- async function fetchErrors(name) {
219
- try {
220
- const r = await fetch('/api/apps/' + encodeURIComponent(name) + '/errors');
221
- if (r.ok) errorCache.set(name, await r.json());
222
- } catch {}
223
- }
224
- function renderDrawer(name) {
225
- const errs = errorCache.get(name) || [];
226
- const liveLogHtml = `<pre data-livelog="${name}" style="background:#0c0e13;color:#a7b1c2;padding:8px;border-radius:4px;font-size:11px;margin:0 0 8px;max-height:160px;overflow:auto"></pre>`;
227
- if (errs.length === 0) return '<div class="drawer">' + liveLogHtml + '<div style="color:#6c757d">no errors recorded</div></div>';
228
- const rows = errs.map((e, i) => {
229
- const p = e.parsed || {};
230
- const fileTxt = p.file ? `${p.file}${p.line ? ':' + p.line : ''}${p.col ? ':' + p.col : ''}` : '(unparsed)';
231
- const link = p.file ? `<a href="${editorUrl(p.file, p.line, p.col)}">${escapeHtml(fileTxt)}</a>` : escapeHtml(fileTxt);
232
- const code = escapeHtml(p.code || '');
233
- const msg = escapeHtml((p.message || e.message || '').slice(0, 200));
234
- const seen = new Date(e.lastSeen).toLocaleTimeString();
235
- return `<div class="errrow"><span class="sev"></span><div><div class="file">${link}</div><div class="msg" title="${escapeHtml(e.message)}">${msg}</div></div><div class="code">${code}</div><div class="count">×${e.count}</div><div><button onclick='copyError(${JSON.stringify(JSON.stringify(e))})'>copy</button>${p.file ? `<a href="${editorUrl(p.file, p.line, p.col)}"><button>open</button></a>` : ''}<div class="seen">${seen}</div></div></div>`;
236
- }).join('');
237
- return `<div class="drawer">${liveLogHtml}${rows}</div>`;
238
- }
239
- async function loadEditorScheme() {
240
- try {
241
- const r = await fetch('/api/config');
242
- if (r.ok) {
243
- const cfg = await r.json();
244
- configCache = cfg.config;
245
- configEtag = cfg.etag;
246
- editorScheme = cfg?.config?.editor?.scheme || 'vscode';
247
- }
248
- } catch {}
249
- }
250
-
251
- async function openConfigPanel() {
252
- const r = await fetch('/api/config');
253
- if (!r.ok) { setCfgToast('failed to load config: ' + r.status, '#ef6f74'); return; }
254
- const j = await r.json();
255
- configCache = j.config;
256
- configEtag = j.etag;
257
- document.getElementById('cfg-textarea').value = JSON.stringify({}, null, 2);
258
- renderPerAppEditor();
259
- document.getElementById('config-modal').style.display = 'flex';
260
- setCfgToast('');
261
- }
262
- function closeConfigPanel() { document.getElementById('config-modal').style.display = 'none'; }
263
- function renderPerAppEditor() {
264
- const wrap = document.getElementById('cfg-perapp');
265
- if (!configCache) { wrap.innerHTML = ''; return; }
266
- const overrides = configCache.overrides || {};
267
- const apps = Array.from(new Set([...Object.keys(overrides), ...(window.__appNames || [])])).sort();
268
- if (apps.length === 0) { wrap.innerHTML = '<div style="color:#6c757d;font-size:12px">no apps yet — open the global tab to set searchRoots</div>'; return; }
269
- wrap.innerHTML = apps.map(name => {
270
- const ov = overrides[name] || {};
271
- return `<div style="border-bottom:1px solid #232730;padding:10px 0">
272
- <div style="font-weight:600;margin-bottom:6px">${name}</div>
273
- <div style="display:grid;grid-template-columns:90px 1fr;gap:6px 12px;font-size:12px">
274
- <label>port</label><input data-app="${name}" data-key="port" type="number" value="${ov.port ?? ''}" style="background:#0f1115;color:#e6e6e6;border:1px solid #353a45;padding:3px 6px;border-radius:3px" />
275
- <label>command</label><input data-app="${name}" data-key="command" type="text" value="${(ov.command ?? '').replace(/"/g, '&quot;')}" style="background:#0f1115;color:#e6e6e6;border:1px solid #353a45;padding:3px 6px;border-radius:3px" />
276
- <label>url</label><input data-app="${name}" data-key="url" type="text" value="${(ov.url ?? '').replace(/"/g, '&quot;')}" style="background:#0f1115;color:#e6e6e6;border:1px solid #353a45;padding:3px 6px;border-radius:3px" />
277
- <label>hidden</label><input data-app="${name}" data-key="hidden" type="checkbox" ${ov.hidden ? 'checked' : ''} />
278
- </div>
279
- </div>`;
280
- }).join('');
281
- }
282
- function collectPatch() {
283
- let textPatch = {};
284
- try { textPatch = JSON.parse(document.getElementById('cfg-textarea').value || '{}'); }
285
- catch (e) { throw new Error('invalid JSON in global tab: ' + e.message); }
286
- const overridesPatch = {};
287
- document.querySelectorAll('#cfg-perapp [data-app]').forEach(inp => {
288
- const name = inp.dataset.app;
289
- const key = inp.dataset.key;
290
- let v;
291
- if (inp.type === 'checkbox') v = inp.checked;
292
- else if (inp.type === 'number') v = inp.value === '' ? null : Number(inp.value);
293
- else v = inp.value === '' ? null : inp.value;
294
- const current = (configCache.overrides || {})[name]?.[key] ?? null;
295
- const norm = v === '' ? null : v;
296
- if (JSON.stringify(norm) !== JSON.stringify(current)) {
297
- overridesPatch[name] = overridesPatch[name] || {};
298
- overridesPatch[name][key] = norm;
299
- }
300
- });
301
- const out = { ...textPatch };
302
- if (Object.keys(overridesPatch).length) {
303
- out.overrides = { ...(textPatch.overrides || {}), ...overridesPatch };
304
- }
305
- return out;
306
- }
307
- async function saveConfig() {
308
- let patch;
309
- try { patch = collectPatch(); } catch (e) { setCfgToast(e.message, '#ef6f74'); return; }
310
- if (!Object.keys(patch).length) { setCfgToast('nothing to save', '#9aa3b2'); return; }
311
- const res = await authedFetch('/api/config', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'If-Match': configEtag }, body: JSON.stringify(patch) });
312
- if (res.status === 412) {
313
- setCfgToast('config was changed elsewhere — click Reload to refresh', '#ef6f74');
314
- return;
315
- }
316
- if (!res.ok) {
317
- let msg = 'save failed: ' + res.status;
318
- try { const e = await res.json(); if (e?.error) msg += ' — ' + e.error; } catch {}
319
- setCfgToast(msg, '#ef6f74');
320
- return;
321
- }
322
- const j = await res.json();
323
- configEtag = j.etag;
324
- let msg = 'saved · applied: ' + (j.applied || []).join(', ');
325
- if (j.restartRequired && j.restartRequired.length) msg += ' · applies on next restart of: ' + j.restartRequired.join(', ');
326
- setCfgToast(msg, '#62d28a');
327
- await loadEditorScheme();
328
- await openConfigPanel();
329
- }
330
- async function reloadConfig() {
331
- const res = await authedFetch('/api/config/reload', { method: 'POST' });
332
- if (!res.ok) { setCfgToast('reload failed: ' + res.status, '#ef6f74'); return; }
333
- const j = await res.json();
334
- setCfgToast('reloaded · added: ' + (j.addedApps || []).join(',') + ' · removed: ' + (j.removedApps || []).join(','), '#62d28a');
335
- await loadEditorScheme();
336
- await openConfigPanel();
337
- }
338
- async function loadPresets() {
339
- try {
340
- const r = await fetch('/api/presets');
341
- if (!r.ok) return;
342
- const list = await r.json();
343
- const sel = document.getElementById('preset-select');
344
- sel.innerHTML = '<option value="">—</option>' + list.map((p, i) => `<option value="${i}">${escapeHtml(p.name)} — ${escapeHtml(p.description || '')}</option>`).join('');
345
- sel._list = list;
346
- } catch {}
347
- }
348
- document.getElementById('preset-apply').addEventListener('click', () => {
349
- const sel = document.getElementById('preset-select');
350
- const list = sel._list || [];
351
- const idx = sel.value;
352
- if (idx === '') return;
353
- const preset = list[Number(idx)];
354
- if (!preset) return;
355
- document.getElementById('cfg-textarea').value = JSON.stringify(preset.patch, null, 2);
356
- document.querySelectorAll('[data-cfgtab]').forEach(x => { x.classList.toggle('active', x.dataset.cfgtab === 'global'); });
357
- document.getElementById('cfg-perapp').style.display = 'none';
358
- document.getElementById('cfg-global').style.display = '';
359
- setCfgToast('preset loaded into Global tab — click Save to apply', '#62d28a');
360
- });
361
- document.getElementById('gear').addEventListener('click', () => { loadPresets(); openConfigPanel(); });
362
- document.getElementById('cfg-close').addEventListener('click', closeConfigPanel);
363
- document.getElementById('cfg-save').addEventListener('click', saveConfig);
364
- document.getElementById('cfg-reload').addEventListener('click', reloadConfig);
365
- document.querySelectorAll('[data-cfgtab]').forEach(t => t.addEventListener('click', () => {
366
- document.querySelectorAll('[data-cfgtab]').forEach(x => x.classList.toggle('active', x === t));
367
- document.getElementById('cfg-perapp').style.display = t.dataset.cfgtab === 'perapp' ? '' : 'none';
368
- document.getElementById('cfg-global').style.display = t.dataset.cfgtab === 'global' ? '' : 'none';
369
- }));
370
- async function renderAllErrors() {
371
- const r = await fetch('/api/apps');
372
- if (!r.ok) return;
373
- const apps = await r.json();
374
- await Promise.all(apps.map(a => fetchErrors(a.name)));
375
- const allRows = [];
376
- for (const a of apps) {
377
- const errs = errorCache.get(a.name) || [];
378
- for (const e of errs) allRows.push({ app: a.name, err: e });
379
- }
380
- const filter = (document.getElementById('all-err-search').value || '').toLowerCase();
381
- const group = document.getElementById('all-err-group').value;
382
- const filtered = filter ? allRows.filter(r => (r.err.parsed?.message || r.err.message || '').toLowerCase().includes(filter) || (r.err.parsed?.file || '').toLowerCase().includes(filter)) : allRows;
383
- document.getElementById('all-err-count').textContent = filtered.length + ' entr' + (filtered.length === 1 ? 'y' : 'ies');
384
- const buckets = new Map();
385
- for (const row of filtered) {
386
- const key = group === 'app' ? row.app : group === 'file' ? (row.err.parsed?.file || '(unparsed)') : (row.err.parsed?.code || '(no code)');
387
- if (!buckets.has(key)) buckets.set(key, []);
388
- buckets.get(key).push(row);
389
- }
390
- const html = [...buckets.entries()].map(([k, rows]) => {
391
- const inner = rows.map(({ app, err }) => {
392
- const p = err.parsed || {};
393
- const fileTxt = p.file ? `${p.file}${p.line ? ':' + p.line : ''}${p.col ? ':' + p.col : ''}` : '(unparsed)';
394
- const link = p.file ? `<a href="${editorUrl(p.file, p.line, p.col)}">${escapeHtml(fileTxt)}</a>` : escapeHtml(fileTxt);
395
- const code = escapeHtml(p.code || '');
396
- const msg = escapeHtml((p.message || err.message || '').slice(0, 200));
397
- return `<div class="errrow"><span class="sev"></span><div><div><span style="color:#9aa3b2;font-size:11px">${escapeHtml(app)}</span> · <span class="file">${link}</span></div><div class="msg">${msg}</div></div><div class="code">${code}</div><div class="count">×${err.count}</div><div><button onclick='copyError(${JSON.stringify(JSON.stringify(err))})'>copy</button>${p.file ? `<a href="${editorUrl(p.file, p.line, p.col)}"><button>open</button></a>` : ''}</div></div>`;
398
- }).join('');
399
- return `<div class="group-h">${escapeHtml(k)} (${rows.length})</div>${inner}`;
400
- }).join('');
401
- document.getElementById('all-err-body').innerHTML = html || '<div style="color:#6c757d">no errors</div>';
402
- }
403
-
404
- async function tick() {
405
- if (currentTab === 'all-errors') { await renderAllErrors(); document.getElementById('meta').textContent = 'all errors — last updated ' + new Date().toLocaleTimeString(); return; }
406
- try {
407
- const res = await fetch('/api/apps');
408
- const apps = await res.json();
409
- window.__appNames = apps.map(a => a.name);
410
- const tbody = document.getElementById('rows');
411
- const refreshDrawers = [];
412
- tbody.innerHTML = apps.map(a => {
413
- const isOpen = expanded.has(a.name);
414
- const chev = isOpen ? '▾' : '▸';
415
- const rowHtml = `
416
- <tr>
417
- <td><span class="chev" onclick="toggleDrawer('${a.name}')">${chev}</span></td>
418
- <td onclick="toggleDrawer('${a.name}')" style="cursor:pointer">${a.name}</td>
419
- <td><span class="badge s-${a.status}">${a.status}</span></td>
420
- <td><span class="dot h-${a.health || 'unknown'}"></span>${a.health || 'unknown'}</td>
421
- <td>${a.port ?? ''}</td>
422
- <td>${a.url ? `<a href="${a.url}" target="_blank">${a.url}</a>` : ''}</td>
423
- <td class="${a.errorCount ? 'err' : ''}">${a.errorCount}</td>
424
- <td>${fmtUptime(a.uptimeMs)}</td>
425
- <td class="num">${a.cpu == null ? '' : (a.cpu.toFixed ? a.cpu.toFixed(1) : a.cpu)}</td>
426
- <td class="num">${a.memMB == null ? '' : a.memMB}</td>
427
- <td>${sparkline((a.compileHistoryMs || []).slice(-10))}</td>
428
- <td>${a.bundle ? `${a.bundle.initialKB}KB initial · ${a.bundle.lazyKB}KB lazy${a.bundleRegressionPct != null && a.bundleRegressionPct > 10 ? ` <span class="err">(+${a.bundleRegressionPct}%)</span>` : ''}` : ''}</td>
429
- <td>
430
- <button onclick="action('${a.name}','start',this)">start</button>
431
- <button onclick="action('${a.name}','stop',this)">stop</button>
432
- <button onclick="action('${a.name}','restart',this)">restart</button>
433
- </td>
434
- </tr>`;
435
- const drawer = isOpen ? `<tr><td colspan="13" style="padding:0">${renderDrawer(a.name)}</td></tr>` : '';
436
- if (isOpen) refreshDrawers.push(a.name);
437
- return rowHtml + drawer;
438
- }).join('');
439
- document.getElementById('meta').textContent = apps.length + ' app' + (apps.length === 1 ? '' : 's') + ' — last updated ' + new Date().toLocaleTimeString();
440
- for (const n of refreshDrawers) void fetchErrors(n);
441
- } catch (e) {
442
- document.getElementById('meta').textContent = 'error: ' + e.message;
443
- }
444
- }
445
- document.getElementById('all-err-search').addEventListener('input', () => { if (currentTab === 'all-errors') renderAllErrors(); });
446
- document.getElementById('all-err-group').addEventListener('change', () => { if (currentTab === 'all-errors') renderAllErrors(); });
447
- loadEditorScheme().then(tick);
448
- setInterval(tick, 2000);
449
- </script>
450
- </body>
451
- </html>