cligr 1.0.8 → 1.0.10
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/README.md +3 -0
- package/dist/index.js +12 -12
- package/package.json +1 -1
- package/src/commands/groups.ts +2 -2
- package/src/commands/ls.ts +3 -3
- package/src/commands/serve.ts +6 -6
- package/src/commands/up.ts +2 -2
- package/src/config/loader.ts +21 -4
- package/src/config/types.ts +2 -1
- package/tests/integration/commands.test.ts +51 -0
- package/tests/integration/config-loader.test.ts +72 -0
- package/docs/superpowers/plans/2026-04-13-improve-web-ui-console.md +0 -256
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Example config:
|
|
|
31
31
|
tools:
|
|
32
32
|
kubefwd:
|
|
33
33
|
cmd: kubectl port-forward $1 $2:$3
|
|
34
|
+
restart: yes
|
|
34
35
|
|
|
35
36
|
groups:
|
|
36
37
|
myapp:
|
|
@@ -60,6 +61,8 @@ cligr groups -v # List groups with details
|
|
|
60
61
|
|
|
61
62
|
## Restart Policies
|
|
62
63
|
|
|
64
|
+
Restart can be set on a **tool** (as a default) or on a **group** (to override the tool default).
|
|
65
|
+
|
|
63
66
|
- `yes` - Always restart on exit
|
|
64
67
|
- `no` - Never restart
|
|
65
68
|
- `unless-stopped` - Restart unless killed by cligr
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import Q from"fs";import Nn from"os";import X from"path";function Ke(e){return t
|
|
|
15
15
|
`,a+1)):a===0?l&&(e.result+=" "):e.result+=C.repeat(`
|
|
16
16
|
`,a):e.result+=C.repeat(`
|
|
17
17
|
`,l?1+a:a),l=!0,t=!0,a=0,r=e.position;!T(c)&&c!==0;)c=e.input.charCodeAt(++e.position);P(e,r,e.position,!1)}return!0}function De(e,n){var r,i=e.tag,o=e.anchor,l=[],t,s=!1,a;if(e.firstTabInLine!==-1)return!1;for(e.anchor!==null&&(e.anchorMap[e.anchor]=l),a=e.input.charCodeAt(e.position);a!==0&&(e.firstTabInLine!==-1&&(e.position=e.firstTabInLine,m(e,"tab characters must not be used in indentation")),!(a!==45||(t=e.input.charCodeAt(e.position+1),!I(t))));){if(s=!0,e.position++,w(e,!0,-1)&&e.lineIndent<=n){l.push(null),a=e.input.charCodeAt(e.position);continue}if(r=e.line,H(e,n,mn,!1,!0),l.push(e.result),w(e,!0,-1),a=e.input.charCodeAt(e.position),(e.line===r||e.lineIndent>n)&&a!==0)m(e,"bad indentation of a sequence entry");else if(e.lineIndent<n)break}return s?(e.tag=i,e.anchor=o,e.kind="sequence",e.result=l,!0):!1}function ii(e,n,r){var i,o,l,t,s,a,u=e.tag,f=e.anchor,c={},p=Object.create(null),d=null,x=null,h=null,v=!1,y=!1,g;if(e.firstTabInLine!==-1)return!1;for(e.anchor!==null&&(e.anchorMap[e.anchor]=c),g=e.input.charCodeAt(e.position);g!==0;){if(!v&&e.firstTabInLine!==-1&&(e.position=e.firstTabInLine,m(e,"tab characters must not be used in indentation")),i=e.input.charCodeAt(e.position+1),l=e.line,(g===63||g===58)&&I(i))g===63?(v&&(j(e,c,p,d,x,null,t,s,a),d=x=h=null),y=!0,v=!0,o=!0):v?(v=!1,o=!0):m(e,"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line"),e.position+=1,g=i;else{if(t=e.line,s=e.lineStart,a=e.position,!H(e,r,dn,!1,!0))break;if(e.line===l){for(g=e.input.charCodeAt(e.position);R(g);)g=e.input.charCodeAt(++e.position);if(g===58)g=e.input.charCodeAt(++e.position),I(g)||m(e,"a whitespace character is expected after the key-value separator within a block mapping"),v&&(j(e,c,p,d,x,null,t,s,a),d=x=h=null),y=!0,v=!1,o=!1,d=e.tag,x=e.result;else if(y)m(e,"can not read an implicit mapping pair; a colon is missed");else return e.tag=u,e.anchor=f,!0}else if(y)m(e,"can not read a block mapping entry; a multiline key may not be an implicit key");else return e.tag=u,e.anchor=f,!0}if((e.line===l||e.lineIndent>n)&&(v&&(t=e.line,s=e.lineStart,a=e.position),H(e,n,Z,!0,o)&&(v?x=e.result:h=e.result),v||(j(e,c,p,d,x,h,t,s,a),d=x=h=null),w(e,!0,-1),g=e.input.charCodeAt(e.position)),(e.line===l||e.lineIndent>n)&&g!==0)m(e,"bad indentation of a mapping entry");else if(e.lineIndent<n)break}return v&&j(e,c,p,d,x,null,t,s,a),y&&(e.tag=u,e.anchor=f,e.kind="mapping",e.result=c),y}function oi(e){var n,r=!1,i=!1,o,l,t;if(t=e.input.charCodeAt(e.position),t!==33)return!1;if(e.tag!==null&&m(e,"duplication of a tag property"),t=e.input.charCodeAt(++e.position),t===60?(r=!0,t=e.input.charCodeAt(++e.position)):t===33?(i=!0,o="!!",t=e.input.charCodeAt(++e.position)):o="!",n=e.position,r){do t=e.input.charCodeAt(++e.position);while(t!==0&&t!==62);e.position<e.length?(l=e.input.slice(n,e.position),t=e.input.charCodeAt(++e.position)):m(e,"unexpected end of the stream within a verbatim tag")}else{for(;t!==0&&!I(t);)t===33&&(i?m(e,"tag suffix cannot contain exclamation marks"):(o=e.input.slice(n-1,e.position+1),hn.test(o)||m(e,"named tag handle cannot contain such characters"),i=!0,n=e.position+1)),t=e.input.charCodeAt(++e.position);l=e.input.slice(n,e.position),Wr.test(l)&&m(e,"tag suffix cannot contain flow indicator characters")}l&&!gn.test(l)&&m(e,"tag name cannot contain such characters: "+l);try{l=decodeURIComponent(l)}catch{m(e,"tag name is malformed: "+l)}return r?e.tag=l:F.call(e.tagMap,o)?e.tag=e.tagMap[o]+l:o==="!"?e.tag="!"+l:o==="!!"?e.tag="tag:yaml.org,2002:"+l:m(e,'undeclared tag handle "'+o+'"'),!0}function ti(e){var n,r;if(r=e.input.charCodeAt(e.position),r!==38)return!1;for(e.anchor!==null&&m(e,"duplication of an anchor property"),r=e.input.charCodeAt(++e.position),n=e.position;r!==0&&!I(r)&&!G(r);)r=e.input.charCodeAt(++e.position);return e.position===n&&m(e,"name of an anchor node must contain at least one character"),e.anchor=e.input.slice(n,e.position),!0}function li(e){var n,r,i;if(i=e.input.charCodeAt(e.position),i!==42)return!1;for(i=e.input.charCodeAt(++e.position),n=e.position;i!==0&&!I(i)&&!G(i);)i=e.input.charCodeAt(++e.position);return e.position===n&&m(e,"name of an alias node must contain at least one character"),r=e.input.slice(n,e.position),F.call(e.anchorMap,r)||m(e,'unidentified alias "'+r+'"'),e.result=e.anchorMap[r],w(e,!0,-1),!0}function H(e,n,r,i,o){var l,t,s,a=1,u=!1,f=!1,c,p,d,x,h,v;if(e.listener!==null&&e.listener("open",e),e.tag=null,e.anchor=null,e.kind=null,e.result=null,l=t=s=Z===r||mn===r,i&&w(e,!0,-1)&&(u=!0,e.lineIndent>n?a=1:e.lineIndent===n?a=0:e.lineIndent<n&&(a=-1)),a===1)for(;oi(e)||ti(e);)w(e,!0,-1)?(u=!0,s=l,e.lineIndent>n?a=1:e.lineIndent===n?a=0:e.lineIndent<n&&(a=-1)):s=!1;if(s&&(s=u||o),(a===1||Z===r)&&(V===r||dn===r?h=n:h=n+1,v=e.position-e.lineStart,a===1?s&&(De(e,v)||ii(e,v,h))||ni(e,h)?f=!0:(t&&ri(e,h)||Zr(e,h)||ei(e,h)?f=!0:li(e)?(f=!0,(e.tag!==null||e.anchor!==null)&&m(e,"alias node should not have any properties")):Vr(e,h,V===r)&&(f=!0,e.tag===null&&(e.tag="?")),e.anchor!==null&&(e.anchorMap[e.anchor]=e.result)):a===0&&(f=s&&De(e,v))),e.tag===null)e.anchor!==null&&(e.anchorMap[e.anchor]=e.result);else if(e.tag==="?"){for(e.result!==null&&e.kind!=="scalar"&&m(e,'unacceptable node kind for !<?> tag; it should be "scalar", not "'+e.kind+'"'),c=0,p=e.implicitTypes.length;c<p;c+=1)if(x=e.implicitTypes[c],x.resolve(e.result)){e.result=x.construct(e.result),e.tag=x.tag,e.anchor!==null&&(e.anchorMap[e.anchor]=e.result);break}}else if(e.tag!=="!"){if(F.call(e.typeMap[e.kind||"fallback"],e.tag))x=e.typeMap[e.kind||"fallback"][e.tag];else for(x=null,d=e.typeMap.multi[e.kind||"fallback"],c=0,p=d.length;c<p;c+=1)if(e.tag.slice(0,d[c].tag.length)===d[c].tag){x=d[c];break}x||m(e,"unknown tag !<"+e.tag+">"),e.result!==null&&x.kind!==e.kind&&m(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+x.kind+'", not "'+e.kind+'"'),x.resolve(e.result,e.tag)?(e.result=x.construct(e.result,e.tag),e.anchor!==null&&(e.anchorMap[e.anchor]=e.result)):m(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return e.listener!==null&&e.listener("close",e),e.tag!==null||e.anchor!==null||f}function si(e){var n=e.position,r,i,o,l=!1,t;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);(t=e.input.charCodeAt(e.position))!==0&&(w(e,!0,-1),t=e.input.charCodeAt(e.position),!(e.lineIndent>0||t!==37));){for(l=!0,t=e.input.charCodeAt(++e.position),r=e.position;t!==0&&!I(t);)t=e.input.charCodeAt(++e.position);for(i=e.input.slice(r,e.position),o=[],i.length<1&&m(e,"directive name must not be less than one character in length");t!==0;){for(;R(t);)t=e.input.charCodeAt(++e.position);if(t===35){do t=e.input.charCodeAt(++e.position);while(t!==0&&!T(t));break}if(T(t))break;for(r=e.position;t!==0&&!I(t);)t=e.input.charCodeAt(++e.position);o.push(e.input.slice(r,e.position))}t!==0&&Ae(e),F.call(Ne,i)?Ne[i](e,i,o):ee(e,'unknown document directive "'+i+'"')}if(w(e,!0,-1),e.lineIndent===0&&e.input.charCodeAt(e.position)===45&&e.input.charCodeAt(e.position+1)===45&&e.input.charCodeAt(e.position+2)===45?(e.position+=3,w(e,!0,-1)):l&&m(e,"directives end mark is expected"),H(e,e.lineIndent-1,Z,!1,!0),w(e,!0,-1),e.checkLineBreaks&&Kr.test(e.input.slice(n,e.position))&&ee(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&ie(e)){e.input.charCodeAt(e.position)===46&&(e.position+=3,w(e,!0,-1));return}if(e.position<e.length-1)m(e,"end of the stream or a document separator is expected");else return}function Cn(e,n){e=String(e),n=n||{},e.length!==0&&(e.charCodeAt(e.length-1)!==10&&e.charCodeAt(e.length-1)!==13&&(e+=`
|
|
18
|
-
`),e.charCodeAt(0)===65279&&(e=e.slice(1)));var r=new Xr(e,n),i=e.indexOf("\0");for(i!==-1&&(r.position=i,m(r,"null byte is not allowed in input")),r.input+="\0";r.input.charCodeAt(r.position)===32;)r.lineIndent+=1,r.position+=1;for(;r.position<r.length-1;)si(r);return r.documents}function ai(e,n,r){n!==null&&typeof n=="object"&&typeof r>"u"&&(r=n,n=null);var i=Cn(e,r);if(typeof n!="function")return i;for(var o=0,l=i.length;o<l;o+=1)n(i[o])}function ci(e,n){var r=Cn(e,n);if(r.length!==0){if(r.length===1)return r[0];throw new S("expected a single document in the stream, but found more")}}var ui=ai,fi=ci,An={loadAll:ui,load:fi},En=Object.prototype.toString,bn=Object.prototype.hasOwnProperty,be=65279,pi=9,z=10,di=13,mi=32,hi=33,gi=34,ge=35,xi=37,vi=38,yi=39,wi=42,Sn=44,Ci=45,ne=58,Ai=61,Ei=62,bi=63,Si=64,In=91,Tn=93,Ii=96,kn=123,Ti=124,On=125,
|
|
18
|
+
`),e.charCodeAt(0)===65279&&(e=e.slice(1)));var r=new Xr(e,n),i=e.indexOf("\0");for(i!==-1&&(r.position=i,m(r,"null byte is not allowed in input")),r.input+="\0";r.input.charCodeAt(r.position)===32;)r.lineIndent+=1,r.position+=1;for(;r.position<r.length-1;)si(r);return r.documents}function ai(e,n,r){n!==null&&typeof n=="object"&&typeof r>"u"&&(r=n,n=null);var i=Cn(e,r);if(typeof n!="function")return i;for(var o=0,l=i.length;o<l;o+=1)n(i[o])}function ci(e,n){var r=Cn(e,n);if(r.length!==0){if(r.length===1)return r[0];throw new S("expected a single document in the stream, but found more")}}var ui=ai,fi=ci,An={loadAll:ui,load:fi},En=Object.prototype.toString,bn=Object.prototype.hasOwnProperty,be=65279,pi=9,z=10,di=13,mi=32,hi=33,gi=34,ge=35,xi=37,vi=38,yi=39,wi=42,Sn=44,Ci=45,ne=58,Ai=61,Ei=62,bi=63,Si=64,In=91,Tn=93,Ii=96,kn=123,Ti=124,On=125,b={};b[0]="\\0";b[7]="\\a";b[8]="\\b";b[9]="\\t";b[10]="\\n";b[11]="\\v";b[12]="\\f";b[13]="\\r";b[27]="\\e";b[34]='\\"';b[92]="\\\\";b[133]="\\N";b[160]="\\_";b[8232]="\\L";b[8233]="\\P";var ki=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"],Oi=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;function _i(e,n){var r,i,o,l,t,s,a;if(n===null)return{};for(r={},i=Object.keys(n),o=0,l=i.length;o<l;o+=1)t=i[o],s=String(n[t]),t.slice(0,2)==="!!"&&(t="tag:yaml.org,2002:"+t.slice(2)),a=e.compiledTypeMap.fallback[t],a&&bn.call(a.styleAliases,s)&&(s=a.styleAliases[s]),r[t]=s;return r}function Pi(e){var n,r,i;if(n=e.toString(16).toUpperCase(),e<=255)r="x",i=2;else if(e<=65535)r="u",i=4;else if(e<=4294967295)r="U",i=8;else throw new S("code point within a string may not be greater than 0xFFFFFFFF");return"\\"+r+C.repeat("0",i-n.length)+n}var Fi=1,J=2;function Li(e){this.schema=e.schema||Ce,this.indent=Math.max(1,e.indent||2),this.noArrayIndent=e.noArrayIndent||!1,this.skipInvalid=e.skipInvalid||!1,this.flowLevel=C.isNothing(e.flowLevel)?-1:e.flowLevel,this.styleMap=_i(this.schema,e.styles||null),this.sortKeys=e.sortKeys||!1,this.lineWidth=e.lineWidth||80,this.noRefs=e.noRefs||!1,this.noCompatMode=e.noCompatMode||!1,this.condenseFlow=e.condenseFlow||!1,this.quotingType=e.quotingType==='"'?J:Fi,this.forceQuotes=e.forceQuotes||!1,this.replacer=typeof e.replacer=="function"?e.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result="",this.duplicates=[],this.usedDuplicates=null}function Me(e,n){for(var r=C.repeat(" ",n),i=0,o=-1,l="",t,s=e.length;i<s;)o=e.indexOf(`
|
|
19
19
|
`,i),o===-1?(t=e.slice(i),i=s):(t=e.slice(i,o+1),i=o+1),t.length&&t!==`
|
|
20
20
|
`&&(l+=r),l+=t;return l}function xe(e,n){return`
|
|
21
21
|
`+C.repeat(" ",e.indent*n)}function Ni(e,n){var r,i,o;for(r=0,i=e.implicitTypes.length;r<i;r+=1)if(o=e.implicitTypes[r],o.resolve(n))return!0;return!1}function re(e){return e===mi||e===pi}function q(e){return 32<=e&&e<=126||161<=e&&e<=55295&&e!==8232&&e!==8233||57344<=e&&e<=65533&&e!==be||65536<=e&&e<=1114111}function $e(e){return q(e)&&e!==be&&e!==di&&e!==z}function Ge(e,n,r){var i=$e(e),o=i&&!re(e);return(r?i:i&&e!==Sn&&e!==In&&e!==Tn&&e!==kn&&e!==On)&&e!==ge&&!(n===ne&&!o)||$e(n)&&!re(n)&&e===ge||n===ne&&o}function Ri(e){return q(e)&&e!==be&&!re(e)&&e!==Ci&&e!==bi&&e!==ne&&e!==Sn&&e!==In&&e!==Tn&&e!==kn&&e!==On&&e!==ge&&e!==vi&&e!==wi&&e!==hi&&e!==Ti&&e!==Ai&&e!==Ei&&e!==yi&&e!==gi&&e!==xi&&e!==Si&&e!==Ii}function Di(e){return!re(e)&&e!==ne}function K(e,n){var r=e.charCodeAt(n),i;return r>=55296&&r<=56319&&n+1<e.length&&(i=e.charCodeAt(n+1),i>=56320&&i<=57343)?(r-55296)*1024+i-56320+65536:r}function _n(e){var n=/^\n* /;return n.test(e)}var Pn=1,ve=2,Fn=3,Ln=4,$=5;function Mi(e,n,r,i,o,l,t,s){var a,u=0,f=null,c=!1,p=!1,d=i!==-1,x=-1,h=Ri(K(e,0))&&Di(K(e,e.length-1));if(n||t)for(a=0;a<e.length;u>=65536?a+=2:a++){if(u=K(e,a),!q(u))return $;h=h&&Ge(u,f,s),f=u}else{for(a=0;a<e.length;u>=65536?a+=2:a++){if(u=K(e,a),u===z)c=!0,d&&(p=p||a-x-1>i&&e[x+1]!==" ",x=a);else if(!q(u))return $;h=h&&Ge(u,f,s),f=u}p=p||d&&a-x-1>i&&e[x+1]!==" "}return!c&&!p?h&&!t&&!o(e)?Pn:l===J?$:ve:r>9&&_n(e)?$:t?l===J?$:ve:p?Ln:Fn}function $i(e,n,r,i,o){e.dump=function(){if(n.length===0)return e.quotingType===J?'""':"''";if(!e.noCompatMode&&(ki.indexOf(n)!==-1||Oi.test(n)))return e.quotingType===J?'"'+n+'"':"'"+n+"'";var l=e.indent*Math.max(1,r),t=e.lineWidth===-1?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-l),s=i||e.flowLevel>-1&&r>=e.flowLevel;function a(u){return Ni(e,u)}switch(Mi(n,s,e.indent,t,a,e.quotingType,e.forceQuotes&&!i,o)){case Pn:return n;case ve:return"'"+n.replace(/'/g,"''")+"'";case Fn:return"|"+je(n,e.indent)+He(Me(n,l));case Ln:return">"+je(n,e.indent)+He(Me(Gi(n,t),l));case $:return'"'+ji(n)+'"';default:throw new S("impossible error: invalid scalar style")}}()}function je(e,n){var r=_n(e)?String(n):"",i=e[e.length-1]===`
|
|
@@ -29,16 +29,16 @@ import Q from"fs";import Nn from"os";import X from"path";function Ke(e){return t
|
|
|
29
29
|
`:"")+Be(a,n),o=l}return i}function Be(e,n){if(e===""||e[0]===" ")return e;for(var r=/ [^ ]/g,i,o=0,l,t=0,s=0,a="";i=r.exec(e);)s=i.index,s-o>n&&(l=t>o?t:s,a+=`
|
|
30
30
|
`+e.slice(o,l),o=l+1),t=s;return a+=`
|
|
31
31
|
`,e.length-o>n&&t>o?a+=e.slice(o,t)+`
|
|
32
|
-
`+e.slice(t+1):a+=e.slice(o),a.slice(1)}function ji(e){for(var n="",r=0,i,o=0;o<e.length;r>=65536?o+=2:o++)r=K(e,o),i=
|
|
33
|
-
`:""}var Wi=Ki,zi={dump:Wi};function Se(e,n){return function(){throw new Error("Function yaml."+e+" is removed in js-yaml 4. Use yaml."+n+" instead, which is now safe by default.")}}var Ji=A,qi=ze,Qi=Xe,Xi=rn,Vi=on,Zi=Ce,eo=An.load,no=An.loadAll,ro=zi.dump,io=S,oo={binary:cn,float:nn,map:Qe,null:Ve,pairs:fn,set:pn,timestamp:sn,bool:Ze,int:en,merge:an,omap:un,seq:qe,str:Je},to=Se("safeLoad","load"),lo=Se("safeLoadAll","loadAll"),so=Se("safeDump","dump"),Ie={Type:Ji,Schema:qi,FAILSAFE_SCHEMA:Qi,JSON_SCHEMA:Xi,CORE_SCHEMA:Vi,DEFAULT_SCHEMA:Zi,load:eo,loadAll:no,dump:ro,YAMLException:io,types:oo,safeLoad:to,safeLoadAll:lo,safeDump:so};var oe=".cligr.yml",
|
|
32
|
+
`+e.slice(t+1):a+=e.slice(o),a.slice(1)}function ji(e){for(var n="",r=0,i,o=0;o<e.length;r>=65536?o+=2:o++)r=K(e,o),i=b[r],!i&&q(r)?(n+=e[o],r>=65536&&(n+=e[o+1])):n+=i||Pi(r);return n}function Hi(e,n,r){var i="",o=e.tag,l,t,s;for(l=0,t=r.length;l<t;l+=1)s=r[l],e.replacer&&(s=e.replacer.call(r,String(l),s)),(O(e,n,s,!1,!1)||typeof s>"u"&&O(e,n,null,!1,!1))&&(i!==""&&(i+=","+(e.condenseFlow?"":" ")),i+=e.dump);e.tag=o,e.dump="["+i+"]"}function Ye(e,n,r,i){var o="",l=e.tag,t,s,a;for(t=0,s=r.length;t<s;t+=1)a=r[t],e.replacer&&(a=e.replacer.call(r,String(t),a)),(O(e,n+1,a,!0,!0,!1,!0)||typeof a>"u"&&O(e,n+1,null,!0,!0,!1,!0))&&((!i||o!=="")&&(o+=xe(e,n)),e.dump&&z===e.dump.charCodeAt(0)?o+="-":o+="- ",o+=e.dump);e.tag=l,e.dump=o||"[]"}function Bi(e,n,r){var i="",o=e.tag,l=Object.keys(r),t,s,a,u,f;for(t=0,s=l.length;t<s;t+=1)f="",i!==""&&(f+=", "),e.condenseFlow&&(f+='"'),a=l[t],u=r[a],e.replacer&&(u=e.replacer.call(r,a,u)),O(e,n,a,!1,!1)&&(e.dump.length>1024&&(f+="? "),f+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),O(e,n,u,!1,!1)&&(f+=e.dump,i+=f));e.tag=o,e.dump="{"+i+"}"}function Yi(e,n,r,i){var o="",l=e.tag,t=Object.keys(r),s,a,u,f,c,p;if(e.sortKeys===!0)t.sort();else if(typeof e.sortKeys=="function")t.sort(e.sortKeys);else if(e.sortKeys)throw new S("sortKeys must be a boolean or a function");for(s=0,a=t.length;s<a;s+=1)p="",(!i||o!=="")&&(p+=xe(e,n)),u=t[s],f=r[u],e.replacer&&(f=e.replacer.call(r,u,f)),O(e,n+1,u,!0,!0,!0)&&(c=e.tag!==null&&e.tag!=="?"||e.dump&&e.dump.length>1024,c&&(e.dump&&z===e.dump.charCodeAt(0)?p+="?":p+="? "),p+=e.dump,c&&(p+=xe(e,n)),O(e,n+1,f,!0,c)&&(e.dump&&z===e.dump.charCodeAt(0)?p+=":":p+=": ",p+=e.dump,o+=p));e.tag=l,e.dump=o||"{}"}function Ue(e,n,r){var i,o,l,t,s,a;for(o=r?e.explicitTypes:e.implicitTypes,l=0,t=o.length;l<t;l+=1)if(s=o[l],(s.instanceOf||s.predicate)&&(!s.instanceOf||typeof n=="object"&&n instanceof s.instanceOf)&&(!s.predicate||s.predicate(n))){if(r?s.multi&&s.representName?e.tag=s.representName(n):e.tag=s.tag:e.tag="?",s.represent){if(a=e.styleMap[s.tag]||s.defaultStyle,En.call(s.represent)==="[object Function]")i=s.represent(n,a);else if(bn.call(s.represent,a))i=s.represent[a](n,a);else throw new S("!<"+s.tag+'> tag resolver accepts not "'+a+'" style');e.dump=i}return!0}return!1}function O(e,n,r,i,o,l,t){e.tag=null,e.dump=r,Ue(e,r,!1)||Ue(e,r,!0);var s=En.call(e.dump),a=i,u;i&&(i=e.flowLevel<0||e.flowLevel>n);var f=s==="[object Object]"||s==="[object Array]",c,p;if(f&&(c=e.duplicates.indexOf(r),p=c!==-1),(e.tag!==null&&e.tag!=="?"||p||e.indent!==2&&n>0)&&(o=!1),p&&e.usedDuplicates[c])e.dump="*ref_"+c;else{if(f&&p&&!e.usedDuplicates[c]&&(e.usedDuplicates[c]=!0),s==="[object Object]")i&&Object.keys(e.dump).length!==0?(Yi(e,n,e.dump,o),p&&(e.dump="&ref_"+c+e.dump)):(Bi(e,n,e.dump),p&&(e.dump="&ref_"+c+" "+e.dump));else if(s==="[object Array]")i&&e.dump.length!==0?(e.noArrayIndent&&!t&&n>0?Ye(e,n-1,e.dump,o):Ye(e,n,e.dump,o),p&&(e.dump="&ref_"+c+e.dump)):(Hi(e,n,e.dump),p&&(e.dump="&ref_"+c+" "+e.dump));else if(s==="[object String]")e.tag!=="?"&&$i(e,e.dump,n,l,a);else{if(s==="[object Undefined]")return!1;if(e.skipInvalid)return!1;throw new S("unacceptable kind of an object to dump "+s)}e.tag!==null&&e.tag!=="?"&&(u=encodeURI(e.tag[0]==="!"?e.tag.slice(1):e.tag).replace(/!/g,"%21"),e.tag[0]==="!"?u="!"+u:u.slice(0,18)==="tag:yaml.org,2002:"?u="!!"+u.slice(18):u="!<"+u+">",e.dump=u+" "+e.dump)}return!0}function Ui(e,n){var r=[],i=[],o,l;for(ye(e,r,i),o=0,l=i.length;o<l;o+=1)n.duplicates.push(r[i[o]]);n.usedDuplicates=new Array(l)}function ye(e,n,r){var i,o,l;if(e!==null&&typeof e=="object")if(o=n.indexOf(e),o!==-1)r.indexOf(o)===-1&&r.push(o);else if(n.push(e),Array.isArray(e))for(o=0,l=e.length;o<l;o+=1)ye(e[o],n,r);else for(i=Object.keys(e),o=0,l=i.length;o<l;o+=1)ye(e[i[o]],n,r)}function Ki(e,n){n=n||{};var r=new Li(n);r.noRefs||Ui(e,r);var i=e;return r.replacer&&(i=r.replacer.call({"":i},"",i)),O(r,0,i,!0,!0)?r.dump+`
|
|
33
|
+
`:""}var Wi=Ki,zi={dump:Wi};function Se(e,n){return function(){throw new Error("Function yaml."+e+" is removed in js-yaml 4. Use yaml."+n+" instead, which is now safe by default.")}}var Ji=A,qi=ze,Qi=Xe,Xi=rn,Vi=on,Zi=Ce,eo=An.load,no=An.loadAll,ro=zi.dump,io=S,oo={binary:cn,float:nn,map:Qe,null:Ve,pairs:fn,set:pn,timestamp:sn,bool:Ze,int:en,merge:an,omap:un,seq:qe,str:Je},to=Se("safeLoad","load"),lo=Se("safeLoadAll","loadAll"),so=Se("safeDump","dump"),Ie={Type:Ji,Schema:qi,FAILSAFE_SCHEMA:Qi,JSON_SCHEMA:Xi,CORE_SCHEMA:Vi,DEFAULT_SCHEMA:Zi,load:eo,loadAll:no,dump:ro,YAMLException:io,types:oo,safeLoad:to,safeLoadAll:lo,safeDump:so};var oe=".cligr.yml",E=class extends Error{constructor(n){super(n),this.name="ConfigError"}},k=class{configPath;constructor(n){if(n)this.configPath=X.resolve(n);else{let r=X.join(Nn.homedir(),oe),i=X.resolve(oe);Q.existsSync(r)?this.configPath=r:Q.existsSync(i)?this.configPath=i:this.configPath=r}}load(){if(!Q.existsSync(this.configPath))throw new E(`Config file not found. Looking for:
|
|
34
34
|
- ${X.join(Nn.homedir(),oe)}
|
|
35
|
-
- ${X.resolve(oe)}`);let n=Q.readFileSync(this.configPath,"utf-8"),r;try{r=Ie.load(n)}catch(i){throw new
|
|
35
|
+
- ${X.resolve(oe)}`);let n=Q.readFileSync(this.configPath,"utf-8"),r;try{r=Ie.load(n)}catch(i){throw new E(`Invalid YAML: ${i.message}`)}return this.validate(r)}validate(n){if(!n||typeof n!="object")throw new E("Config must be an object");let r=n;if(!r.groups||typeof r.groups!="object")throw new E('Config must have a "groups" object');for(let[i,o]of Object.entries(r.groups))if(o&&typeof o=="object"){let l=o;this.validateItems(l.items,i),this.validateDisabledItems(l.items,l.disabledItems,i)}return r}validateItems(n,r){if(n==null)return;if(typeof n!="object"||Array.isArray(n))throw new E(`Group "${r}": items must be an object with named entries, e.g.:
|
|
36
36
|
items:
|
|
37
|
-
serviceName: "value1,value2"`);let i=new Set;for(let[o,l]of Object.entries(n)){if(typeof l!="string")throw new
|
|
38
|
-
`);for(let p of c)p.length>0&&this.emit("process-log",r,n.name,p,u)};return t.stdout&&t.stdout.on("data",a=>{process.stdout.write(`[${n.name}] ${a}`),s(a,!1)}),t.stderr&&t.stderr.on("data",a=>{process.stderr.write(`[${n.name}] ${a}`),s(a,!0)}),t.on("exit",(a,u)=>{this.handleExit(r,n,i,a,u)}),t}parseCommand(n){let r=[],i="",o=!1,l="";for(let t=0;t<n.length;t++){let s=n[t],a=n[t+1];(s==='"'||s==="'")&&!o?(o=!0,l=s):s===l&&o?(o=!1,l=""):s===" "&&!o?i&&(r.push(i),i=""):i+=s}return i&&r.push(i),{cmd:r[0]||"",args:r.slice(1)}}handleExit(n,r,i,o,l){if(l==="SIGTERM"&&!this.groups.has(n)){this.pidStore.deletePid(n,r.name).catch(()=>{});return}if(i==="unless-stopped"&&l==="SIGTERM"){this.pidStore.deletePid(n,r.name).catch(()=>{});return}if(i==="no"){this.pidStore.deletePid(n,r.name).catch(()=>{});return}let t=`${n}-${r.name}`,s=Date.now(),u=(this.restartTimestamps.get(t)||[]).filter(f=>s-f<this.restartWindow);if(u.push(s),this.restartTimestamps.set(t,u),u.length>this.maxRestarts){console.error(`[${r.name}] Crash loop detected. Stopping restarts.`),this.pidStore.deletePid(n,r.name).catch(()=>{});return}setTimeout(()=>{console.log(`[${r.name}] Restarting... (exit code: ${o})`);let f=this.spawnProcess(r,n,i);this.emit("item-restarted",n,r.name);let c=this.groups.get(n);if(c){let p=c.find(d=>d.item.name===r.name);p&&(p.process=f)}},1e3)}killGroup(n){let r=this.groups.get(n);if(!r)return Promise.resolve();let i=r.map(o=>this.killProcess(o.process));return this.groups.delete(n),Promise.all(i).then(async()=>{await this.pidStore.deleteGroupPids(n),this.emit("group-stopped",n)})}killPid(n){return new Promise((r,i)=>{try{process.kill(n,"SIGTERM");let o=setTimeout(()=>{try{process.kill(n,"SIGKILL")}catch{}},5e3),l=setInterval(()=>{this.pidStore.isPidRunning(n)||(clearTimeout(o),clearInterval(l),r())},100);this.pidStore.isPidRunning(n)||(clearTimeout(o),clearInterval(l),r())}catch(o){i(o)}})}killProcess(n){return new Promise(r=>{n.kill("SIGTERM");let i=setTimeout(()=>{n.killed||n.kill("SIGKILL")},5e3);n.on("exit",()=>{clearTimeout(i),r()}),(n.killed||n.exitCode!==null)&&(clearTimeout(i),r())})}killAll(){let n=[];for(let r of this.groups.keys())n.push(this.killGroup(r));return Promise.all(n).then(()=>{})}async cleanupStalePids(){await this.pidStore.cleanupStalePids()}getGroupStatus(n){let r=this.groups.get(n);return r?r.map(i=>i.status):[]}isGroupRunning(n){return this.groups.has(n)}getRunningGroups(){return Array.from(this.groups.keys())}};async function ke(e){let n=new k,r=new Y,i=new B;try{await i.cleanupStalePids();let{config:o,items:l,tool:t,toolTemplate:s,params:a}=n.getGroup(e),
|
|
39
|
-
Shutting down...`),process.removeListener("SIGINT",
|
|
40
|
-
Group: ${e}`),console.log(`Tool: ${r.tool}`),console.log(`Restart: ${
|
|
41
|
-
Items:`);let
|
|
37
|
+
serviceName: "value1,value2"`);let i=new Set;for(let[o,l]of Object.entries(n)){if(typeof l!="string")throw new E(`Group "${r}": item "${o}" must have a string value`);if(i.has(o))throw new E(`Group "${r}": duplicate item name "${o}". Item names must be unique within a group.`);i.add(o)}}validateDisabledItems(n,r,i){if(r==null)return;if(!Array.isArray(r))throw new E(`Group "${i}": disabledItems must be an array of strings`);let o=new Set,l=n&&typeof n=="object"&&!Array.isArray(n)?new Set(Object.keys(n)):new Set;for(let t of r){if(typeof t!="string")throw new E(`Group "${i}": disabledItems must be an array of strings`);if(o.has(t))throw new E(`Group "${i}": disabledItems contains duplicate "${t}"`);if(o.add(t),!l.has(t))throw new E(`Group "${i}": disabledItems entry "${t}" does not match any item`)}}normalizeItems(n){return Object.entries(n).map(([r,i])=>({name:r,value:i}))}getGroup(n){let r=this.load(),i=r.groups[n];if(!i){let c=Object.keys(r.groups).join(", ");throw new E(`Unknown group: ${n}. Available: ${c}`)}let o=new Set(i.disabledItems||[]),l={};for(let[c,p]of Object.entries(i.items||{}))o.has(c)||(l[c]=p);let t=this.normalizeItems(l),s=null,a=null;r.tools&&r.tools[i.tool]?(s=r.tools[i.tool].cmd,a=i.tool):(a=null,s=null);let u=i.params||{},f=i.restart??r.tools?.[i.tool]?.restart;return{config:i,items:t,tool:a,toolTemplate:s,params:u,restart:f}}getEffectiveRestart(n){let r=this.load(),i=r.groups[n];if(!i){let o=Object.keys(r.groups).join(", ");throw new E(`Unknown group: ${n}. Available: ${o}`)}return i.restart??(r.tools&&r.tools[i.tool]?.restart)??void 0}saveConfig(n){let r=Ie.dump(n,{indent:2,lineWidth:-1});Q.writeFileSync(this.configPath,r,"utf-8")}toggleItem(n,r,i){let o=this.load(),l=o.groups[n];if(!l)throw new E(`Unknown group: ${n}`);if(!Object.hasOwn(l.items||{},r))throw new E(`Item "${r}" not found in group "${n}"`);let t=new Set(l.disabledItems||[]);i?t.delete(r):t.add(r),t.size===0?delete l.disabledItems:l.disabledItems=Array.from(t),this.saveConfig(o)}listGroups(){let n=this.load();return Object.keys(n.groups)}};var D=class{static expandNamedParams(n,r){let i=n;for(let[o,l]of Object.entries(r)){let t=`$${o}`;i=i.replaceAll(t,l)}return i}static expand(n,r,i,o={}){let l=r.value.split(",").map(a=>a.trim()),t=r.name,s=n;for(let a=l.length-1;a>=0;a--){let u=`$${a+1}`;s=s.replaceAll(u,l[a])}return s=this.expandNamedParams(s,o),{name:t,args:l,fullCmd:s}}static parseItem(n,r,i,o,l={}){if(r){let t=this.expand(r,i,o,l),s=r.match(/\$\d+/g)||[],a=0;for(let u of s){let f=parseInt(u.substring(1),10);f>a&&(a=f)}if(a>0&&t.args.length>a){let u=t.args.slice(a);t.fullCmd=`${t.fullCmd} ${u.join(" ")}`}return t}else{let t=i.value.split(",").map(u=>u.trim()),s=i.name,a=n?`${n} ${i.value}`:i.value;return{name:s,args:t,fullCmd:a}}}};import{spawn as co}from"child_process";import{EventEmitter as uo}from"events";import{promises as M}from"fs";import te from"path";import ao from"os";var B=class{pidsDir;constructor(){this.pidsDir=te.join(ao.homedir(),".cligr","pids")}async ensureDir(){try{await M.mkdir(this.pidsDir,{recursive:!0})}catch(n){if(n.code!=="EEXIST")throw n}}sanitizeItemName(n){return n.replace(/[<>:"/\\|?*]/g,"_")}getPidFilePath(n,r){let i=this.sanitizeItemName(r);return te.join(this.pidsDir,`${n}_${i}.pid`)}async writePid(n){await this.ensureDir();let r=this.getPidFilePath(n.groupName,n.itemName);await M.writeFile(r,JSON.stringify(n,null,2),"utf-8")}async readPidsByGroup(n){await this.ensureDir();let r=[];try{let i=await M.readdir(this.pidsDir),o=`${n}_`;for(let l of i)if(l.startsWith(o)&&l.endsWith(".pid"))try{let t=await M.readFile(te.join(this.pidsDir,l),"utf-8");r.push(JSON.parse(t))}catch{continue}}catch{return[]}return r}async readAllPids(){await this.ensureDir();let n=[];try{let r=await M.readdir(this.pidsDir);for(let i of r)if(i.endsWith(".pid"))try{let o=await M.readFile(te.join(this.pidsDir,i),"utf-8");n.push(JSON.parse(o))}catch{continue}}catch{return[]}return n}async deletePid(n,r){let i=this.getPidFilePath(n,r);try{await M.unlink(i)}catch(o){if(o.code!=="ENOENT")throw o}}async deleteGroupPids(n){let r=await this.readPidsByGroup(n);for(let i of r)await this.deletePid(i.groupName,i.itemName)}isPidRunning(n){try{return process.kill(n,0),!0}catch{return!1}}isPidEntryValid(n){if(!this.isPidRunning(n.pid))return!1;let r=Date.now()-5*60*1e3;return n.startTime>r}async cleanupStalePids(){let n=await this.readAllPids(),r=[];for(let i of n)this.isPidEntryValid(i)||(r.push(i),await this.deletePid(i.groupName,i.itemName));return r}async getRunningGroups(){let n=await this.readAllPids(),r=new Set(n.map(i=>i.groupName));return Array.from(r)}};var Te=class{constructor(n,r,i="running"){this.item=n;this.process=r;this.status=i}},Y=class extends uo{groups=new Map;restartTimestamps=new Map;maxRestarts=3;restartWindow=1e4;pidStore=new B;spawnGroup(n,r,i){if(this.groups.has(n))throw new Error(`Group ${n} is already running`);let o=[];for(let l of r){let t=this.spawnProcess(l,n,i);o.push(new Te(l,t))}this.groups.set(n,o),this.emit("group-started",n)}async restartGroup(n,r,i){this.isGroupRunning(n)&&await this.killGroup(n),this.spawnGroup(n,r,i)}spawnProcess(n,r,i){let{cmd:o,args:l}=this.parseCommand(n.fullCmd),t=co(o,l,{stdio:["inherit","pipe","pipe"],shell:!1,windowsHide:!0});if(this.pidStore.deletePid(r,n.name).catch(()=>{}),t.pid){let a={pid:t.pid,groupName:r,itemName:n.name,startTime:Date.now(),restartPolicy:i,fullCmd:n.fullCmd};this.pidStore.writePid(a).catch(u=>{console.error(`[${n.name}] Failed to write PID file:`,u)})}let s=(a,u)=>{let c=a.toString("utf-8").split(`
|
|
38
|
+
`);for(let p of c)p.length>0&&this.emit("process-log",r,n.name,p,u)};return t.stdout&&t.stdout.on("data",a=>{process.stdout.write(`[${n.name}] ${a}`),s(a,!1)}),t.stderr&&t.stderr.on("data",a=>{process.stderr.write(`[${n.name}] ${a}`),s(a,!0)}),t.on("exit",(a,u)=>{this.handleExit(r,n,i,a,u)}),t}parseCommand(n){let r=[],i="",o=!1,l="";for(let t=0;t<n.length;t++){let s=n[t],a=n[t+1];(s==='"'||s==="'")&&!o?(o=!0,l=s):s===l&&o?(o=!1,l=""):s===" "&&!o?i&&(r.push(i),i=""):i+=s}return i&&r.push(i),{cmd:r[0]||"",args:r.slice(1)}}handleExit(n,r,i,o,l){if(l==="SIGTERM"&&!this.groups.has(n)){this.pidStore.deletePid(n,r.name).catch(()=>{});return}if(i==="unless-stopped"&&l==="SIGTERM"){this.pidStore.deletePid(n,r.name).catch(()=>{});return}if(i==="no"){this.pidStore.deletePid(n,r.name).catch(()=>{});return}let t=`${n}-${r.name}`,s=Date.now(),u=(this.restartTimestamps.get(t)||[]).filter(f=>s-f<this.restartWindow);if(u.push(s),this.restartTimestamps.set(t,u),u.length>this.maxRestarts){console.error(`[${r.name}] Crash loop detected. Stopping restarts.`),this.pidStore.deletePid(n,r.name).catch(()=>{});return}setTimeout(()=>{console.log(`[${r.name}] Restarting... (exit code: ${o})`);let f=this.spawnProcess(r,n,i);this.emit("item-restarted",n,r.name);let c=this.groups.get(n);if(c){let p=c.find(d=>d.item.name===r.name);p&&(p.process=f)}},1e3)}killGroup(n){let r=this.groups.get(n);if(!r)return Promise.resolve();let i=r.map(o=>this.killProcess(o.process));return this.groups.delete(n),Promise.all(i).then(async()=>{await this.pidStore.deleteGroupPids(n),this.emit("group-stopped",n)})}killPid(n){return new Promise((r,i)=>{try{process.kill(n,"SIGTERM");let o=setTimeout(()=>{try{process.kill(n,"SIGKILL")}catch{}},5e3),l=setInterval(()=>{this.pidStore.isPidRunning(n)||(clearTimeout(o),clearInterval(l),r())},100);this.pidStore.isPidRunning(n)||(clearTimeout(o),clearInterval(l),r())}catch(o){i(o)}})}killProcess(n){return new Promise(r=>{n.kill("SIGTERM");let i=setTimeout(()=>{n.killed||n.kill("SIGKILL")},5e3);n.on("exit",()=>{clearTimeout(i),r()}),(n.killed||n.exitCode!==null)&&(clearTimeout(i),r())})}killAll(){let n=[];for(let r of this.groups.keys())n.push(this.killGroup(r));return Promise.all(n).then(()=>{})}async cleanupStalePids(){await this.pidStore.cleanupStalePids()}getGroupStatus(n){let r=this.groups.get(n);return r?r.map(i=>i.status):[]}isGroupRunning(n){return this.groups.has(n)}getRunningGroups(){return Array.from(this.groups.keys())}};async function ke(e){let n=new k,r=new Y,i=new B;try{await i.cleanupStalePids();let{config:o,items:l,tool:t,toolTemplate:s,params:a,restart:u}=n.getGroup(e),f=l.map((c,p)=>D.parseItem(t,s,c,p,a));return r.spawnGroup(e,f,u),console.log(`Started group ${e} with ${f.length} process(es)`),console.log("Press Ctrl+C to stop..."),new Promise(c=>{let p=async()=>{console.log(`
|
|
39
|
+
Shutting down...`),process.removeListener("SIGINT",p),process.removeListener("SIGTERM",p),await r.killAll(),c(0)};process.on("SIGINT",p),process.on("SIGTERM",p)})}catch(o){if(o instanceof Error&&o.name==="ConfigError")return console.error(o.message),1;throw o}}async function Rn(e){let n=new k;try{let{config:r,restart:i}=n.getGroup(e);console.log(`
|
|
40
|
+
Group: ${e}`),console.log(`Tool: ${r.tool}`),console.log(`Restart: ${i}`),console.log(`
|
|
41
|
+
Items:`);let o=new Set(r.disabledItems||[]);for(let[l,t]of Object.entries(r.items||{})){let s=o.has(l)?" [disabled]":"";console.log(` ${l}: ${t}${s}`)}return console.log(""),0}catch(r){if(r instanceof Error&&r.name==="ConfigError")return console.error(r.message),1;throw r}}import{spawn as fo,spawnSync as Mn}from"child_process";import U from"fs";import po from"os";import Oe from"path";var Dn=".cligr.yml",mo=`# Cligr Configuration
|
|
42
42
|
|
|
43
43
|
groups:
|
|
44
44
|
web:
|
|
@@ -69,12 +69,12 @@ Install VS Code or set EDITOR environment variable.
|
|
|
69
69
|
|
|
70
70
|
Example:
|
|
71
71
|
export EDITOR=vim
|
|
72
|
-
cligr config`);fo(n,[e],{detached:!0,stdio:"ignore",shell:r==="win32"}).unref()}function xo(e){let n=Oe.dirname(e);U.existsSync(n)||U.mkdirSync(n,{recursive:!0}),U.writeFileSync(e,mo,"utf-8")}async function $n(){try{let e=Oe.join(po.homedir(),Dn),n=Oe.resolve(Dn),r;U.existsSync(e)?r=e:U.existsSync(n)?r=n:r=e,U.existsSync(r)||xo(r);let i=ho();return go(r,i),console.log(`Opening ${r} in ${i}...`),0}catch(e){return console.error(`Error: ${e.message}`),1}}async function Gn(e){let n=new k;try{let r=n.listGroups();if(r.length===0)return 0;if(e){let i=n.load(),o=[];for(let u of r){let f=i.groups[u];o.push({name:u,tool:f.tool||"(none)",restart:
|
|
72
|
+
cligr config`);fo(n,[e],{detached:!0,stdio:"ignore",shell:r==="win32"}).unref()}function xo(e){let n=Oe.dirname(e);U.existsSync(n)||U.mkdirSync(n,{recursive:!0}),U.writeFileSync(e,mo,"utf-8")}async function $n(){try{let e=Oe.join(po.homedir(),Dn),n=Oe.resolve(Dn),r;U.existsSync(e)?r=e:U.existsSync(n)?r=n:r=e,U.existsSync(r)||xo(r);let i=ho();return go(r,i),console.log(`Opening ${r} in ${i}...`),0}catch(e){return console.error(`Error: ${e.message}`),1}}async function Gn(e){let n=new k;try{let r=n.listGroups();if(r.length===0)return 0;if(e){let i=n.load(),o=[];for(let u of r){let f=i.groups[u];o.push({name:u,tool:f.tool||"(none)",restart:n.getEffectiveRestart(u)||"(none)",itemCount:Object.keys(f.items||{}).length})}let l=Math.max(5,...o.map(u=>u.name.length)),t=Math.max(4,...o.map(u=>u.tool.length)),s=Math.max(7,...o.map(u=>u.restart.length)),a="GROUP".padEnd(l)+" "+"TOOL".padEnd(t)+" "+"RESTART".padEnd(s)+" ITEMS";console.log(a);for(let u of o){let f=u.name.padEnd(l)+" "+u.tool.padEnd(t)+" "+u.restart.padEnd(s)+" "+String(u.itemCount);console.log(f)}}else for(let i of r)console.log(i);return 0}catch(r){return console.error(r.message),1}}import vo from"http";async function jn(e){let n=e?parseInt(e,10):7373,r=new k,i=new Y;await i.cleanupStalePids();let o=[],l=(f,c)=>{let p=`event: ${f}
|
|
73
73
|
data: ${JSON.stringify(c)}
|
|
74
74
|
|
|
75
|
-
`;for(let d=o.length-1;d>=0;d--){let x=o[d];try{x.write(p)}catch{o.splice(d,1);try{x.end()}catch{}}}};i.on("group-started",f=>{l("status",{type:"group-started",groupName:f})}),i.on("group-stopped",f=>{l("status",{type:"group-stopped",groupName:f})}),i.on("item-restarted",(f,c)=>{l("status",{type:"item-restarted",groupName:f,itemName:c})}),i.on("process-log",(f,c,p,d)=>{l("log",{group:f,item:c,line:p,isError:d})});let t=vo.createServer((f,c)=>{let p=new URL(f.url||"/",`http://localhost:${n}`);if(c.setHeader("Access-Control-Allow-Origin","*"),c.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),c.setHeader("Access-Control-Allow-Headers","Content-Type"),f.method==="OPTIONS"){c.writeHead(204),c.end();return}if(p.pathname==="/"){c.setHeader("Content-Type","text/html"),c.writeHead(200),c.end(yo());return}if(p.pathname==="/api/groups"){try{let h=r.load(),v=Object.entries(h.groups).map(([y,g])=>({name:y,tool:g.tool,restart:
|
|
75
|
+
`;for(let d=o.length-1;d>=0;d--){let x=o[d];try{x.write(p)}catch{o.splice(d,1);try{x.end()}catch{}}}};i.on("group-started",f=>{l("status",{type:"group-started",groupName:f})}),i.on("group-stopped",f=>{l("status",{type:"group-stopped",groupName:f})}),i.on("item-restarted",(f,c)=>{l("status",{type:"item-restarted",groupName:f,itemName:c})}),i.on("process-log",(f,c,p,d)=>{l("log",{group:f,item:c,line:p,isError:d})});let t=vo.createServer((f,c)=>{let p=new URL(f.url||"/",`http://localhost:${n}`);if(c.setHeader("Access-Control-Allow-Origin","*"),c.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),c.setHeader("Access-Control-Allow-Headers","Content-Type"),f.method==="OPTIONS"){c.writeHead(204),c.end();return}if(p.pathname==="/"){c.setHeader("Content-Type","text/html"),c.writeHead(200),c.end(yo());return}if(p.pathname==="/api/groups"){try{let h=r.load(),v=Object.entries(h.groups).map(([y,g])=>({name:y,tool:g.tool,restart:r.getEffectiveRestart(y),items:Object.entries(g.items||{}).map(([_,L])=>({name:_,value:L,enabled:!(g.disabledItems||[]).includes(_)})),running:i.isGroupRunning(y)}));c.setHeader("Content-Type","application/json"),c.writeHead(200),c.end(JSON.stringify({groups:v}))}catch(h){c.setHeader("Content-Type","application/json"),c.writeHead(500),c.end(JSON.stringify({error:h.message}))}return}if(p.pathname==="/api/events"){c.setHeader("Content-Type","text/event-stream"),c.setHeader("Cache-Control","no-cache"),c.setHeader("Connection","keep-alive"),c.writeHead(200),c.write(`:ok
|
|
76
76
|
|
|
77
|
-
`),o.push(c),f.on("close",()=>{let h=o.indexOf(c);h!==-1&&o.splice(h,1)});return}let d=p.pathname.match(/^\/api\/groups\/([^/]+)\/toggle$/);if(d&&f.method==="POST"){let h=decodeURIComponent(d[1]),v="";f.on("data",y=>v+=y),f.on("end",async()=>{let y;try{y=JSON.parse(v)}catch{c.writeHead(400),c.end(JSON.stringify({error:"Invalid JSON"}));return}let{enabled:g}=y;try{if(g){let{
|
|
77
|
+
`),o.push(c),f.on("close",()=>{let h=o.indexOf(c);h!==-1&&o.splice(h,1)});return}let d=p.pathname.match(/^\/api\/groups\/([^/]+)\/toggle$/);if(d&&f.method==="POST"){let h=decodeURIComponent(d[1]),v="";f.on("data",y=>v+=y),f.on("end",async()=>{let y;try{y=JSON.parse(v)}catch{c.writeHead(400),c.end(JSON.stringify({error:"Invalid JSON"}));return}let{enabled:g}=y;try{if(g){let{items:_,tool:L,toolTemplate:le,params:se,restart:ae}=r.getGroup(h),ce=_.map((ue,fe)=>D.parseItem(L,le,ue,fe,se));i.spawnGroup(h,ce,ae)}else await i.killGroup(h);c.setHeader("Content-Type","application/json"),c.writeHead(200),c.end(JSON.stringify({success:!0}))}catch(_){c.setHeader("Content-Type","application/json"),c.writeHead(500),c.end(JSON.stringify({error:_.message}))}});return}let x=p.pathname.match(/^\/api\/groups\/([^/]+)\/items\/([^/]+)\/toggle$/);if(x&&f.method==="POST"){let h=decodeURIComponent(x[1]),v=decodeURIComponent(x[2]),y="";f.on("data",g=>y+=g),f.on("end",async()=>{let g;try{g=JSON.parse(y)}catch{c.writeHead(400),c.end(JSON.stringify({error:"Invalid JSON"}));return}let{enabled:_}=g;try{if(r.toggleItem(h,v,_),i.isGroupRunning(h)){let{items:L,tool:le,toolTemplate:se,params:ae,restart:ce}=r.getGroup(h),ue=L.map((fe,Bn)=>D.parseItem(le,se,fe,Bn,ae));await i.restartGroup(h,ue,ce)}c.setHeader("Content-Type","application/json"),c.writeHead(200),c.end(JSON.stringify({success:!0}))}catch(L){c.setHeader("Content-Type","application/json"),c.writeHead(500),c.end(JSON.stringify({error:L.message}))}});return}c.writeHead(404),c.end("Not found")});t.on("error",f=>{f.code==="EADDRINUSE"?(console.error(`Port ${n} is already in use`),process.exit(1)):(console.error("Server error:",f),process.exit(2))});let s,a=new Promise(f=>{s=f}),u=async()=>{console.log(`
|
|
78
78
|
Shutting down...`),t.close(),await i.killAll(),s(0)};return process.on("SIGINT",u),process.on("SIGTERM",u),t.listen(n,()=>{console.log(`cligr serve running at http://localhost:${n}`)}),a}function yo(){return`<!DOCTYPE html>
|
|
79
79
|
<html>
|
|
80
80
|
<head>
|
package/package.json
CHANGED
package/src/commands/groups.ts
CHANGED
|
@@ -28,8 +28,8 @@ export async function groupsCommand(verbose: boolean): Promise<number> {
|
|
|
28
28
|
details.push({
|
|
29
29
|
name,
|
|
30
30
|
tool: group.tool || '(none)',
|
|
31
|
-
restart:
|
|
32
|
-
itemCount: Object.keys(group.items).length,
|
|
31
|
+
restart: loader.getEffectiveRestart(name) || '(none)',
|
|
32
|
+
itemCount: Object.keys(group.items || {}).length,
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
package/src/commands/ls.ts
CHANGED
|
@@ -4,15 +4,15 @@ export async function lsCommand(groupName: string): Promise<number> {
|
|
|
4
4
|
const loader = new ConfigLoader();
|
|
5
5
|
|
|
6
6
|
try {
|
|
7
|
-
const { config } = loader.getGroup(groupName);
|
|
7
|
+
const { config, restart } = loader.getGroup(groupName);
|
|
8
8
|
|
|
9
9
|
console.log(`\nGroup: ${groupName}`);
|
|
10
10
|
console.log(`Tool: ${config.tool}`);
|
|
11
|
-
console.log(`Restart: ${
|
|
11
|
+
console.log(`Restart: ${restart}`);
|
|
12
12
|
console.log('\nItems:');
|
|
13
13
|
|
|
14
14
|
const disabled = new Set(config.disabledItems || []);
|
|
15
|
-
for (const [name, value] of Object.entries(config.items)) {
|
|
15
|
+
for (const [name, value] of Object.entries(config.items || {})) {
|
|
16
16
|
const marker = disabled.has(name) ? ' [disabled]' : '';
|
|
17
17
|
console.log(` ${name}: ${value}${marker}`);
|
|
18
18
|
}
|
package/src/commands/serve.ts
CHANGED
|
@@ -69,8 +69,8 @@ export async function serveCommand(portArg?: string): Promise<number> {
|
|
|
69
69
|
const groups = Object.entries(config.groups).map(([name, group]) => ({
|
|
70
70
|
name,
|
|
71
71
|
tool: group.tool,
|
|
72
|
-
restart:
|
|
73
|
-
items: Object.entries(group.items).map(([itemName, value]) => ({
|
|
72
|
+
restart: loader.getEffectiveRestart(name),
|
|
73
|
+
items: Object.entries(group.items || {}).map(([itemName, value]) => ({
|
|
74
74
|
name: itemName,
|
|
75
75
|
value,
|
|
76
76
|
enabled: !(group.disabledItems || []).includes(itemName),
|
|
@@ -119,11 +119,11 @@ export async function serveCommand(portArg?: string): Promise<number> {
|
|
|
119
119
|
const { enabled } = parsed;
|
|
120
120
|
try {
|
|
121
121
|
if (enabled) {
|
|
122
|
-
const {
|
|
122
|
+
const { items, tool, toolTemplate, params, restart } = loader.getGroup(groupName);
|
|
123
123
|
const processItems = items.map((item, index) =>
|
|
124
124
|
TemplateExpander.parseItem(tool, toolTemplate, item, index, params)
|
|
125
125
|
);
|
|
126
|
-
manager.spawnGroup(groupName, processItems,
|
|
126
|
+
manager.spawnGroup(groupName, processItems, restart);
|
|
127
127
|
} else {
|
|
128
128
|
await manager.killGroup(groupName);
|
|
129
129
|
}
|
|
@@ -159,11 +159,11 @@ export async function serveCommand(portArg?: string): Promise<number> {
|
|
|
159
159
|
loader.toggleItem(groupName, itemName, enabled);
|
|
160
160
|
|
|
161
161
|
if (manager.isGroupRunning(groupName)) {
|
|
162
|
-
const {
|
|
162
|
+
const { items, tool, toolTemplate, params, restart } = loader.getGroup(groupName);
|
|
163
163
|
const processItems = items.map((item, index) =>
|
|
164
164
|
TemplateExpander.parseItem(tool, toolTemplate, item, index, params)
|
|
165
165
|
);
|
|
166
|
-
await manager.restartGroup(groupName, processItems,
|
|
166
|
+
await manager.restartGroup(groupName, processItems, restart);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
res.setHeader('Content-Type', 'application/json');
|
package/src/commands/up.ts
CHANGED
|
@@ -13,7 +13,7 @@ export async function upCommand(groupName: string): Promise<number> {
|
|
|
13
13
|
await pidStore.cleanupStalePids();
|
|
14
14
|
|
|
15
15
|
// Load group config
|
|
16
|
-
const { config, items, tool, toolTemplate, params } = loader.getGroup(groupName);
|
|
16
|
+
const { config, items, tool, toolTemplate, params, restart } = loader.getGroup(groupName);
|
|
17
17
|
|
|
18
18
|
// Build process items
|
|
19
19
|
const processItems = items.map((item, index) =>
|
|
@@ -21,7 +21,7 @@ export async function upCommand(groupName: string): Promise<number> {
|
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
// Spawn all processes
|
|
24
|
-
manager.spawnGroup(groupName, processItems,
|
|
24
|
+
manager.spawnGroup(groupName, processItems, restart);
|
|
25
25
|
|
|
26
26
|
console.log(`Started group ${groupName} with ${processItems.length} process(es)`);
|
|
27
27
|
console.log('Press Ctrl+C to stop...');
|
package/src/config/loader.ts
CHANGED
|
@@ -81,7 +81,11 @@ export class ConfigLoader {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
private validateItems(items: unknown, groupName: string): void {
|
|
84
|
-
if (
|
|
84
|
+
if (items === undefined || items === null) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof items !== 'object' || Array.isArray(items)) {
|
|
85
89
|
throw new ConfigError(
|
|
86
90
|
`Group "${groupName}": items must be an object with named entries, e.g.:\n` +
|
|
87
91
|
' items:\n' +
|
|
@@ -143,7 +147,7 @@ export class ConfigLoader {
|
|
|
143
147
|
}));
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
getGroup(name: string): { config: GroupConfig; items: ItemEntry[]; tool: string | null; toolTemplate: string | null; params: Record<string, string
|
|
150
|
+
getGroup(name: string): { config: GroupConfig; items: ItemEntry[]; tool: string | null; toolTemplate: string | null; params: Record<string, string>; restart: GroupConfig['restart'] } {
|
|
147
151
|
const config = this.load();
|
|
148
152
|
const group = config.groups[name];
|
|
149
153
|
|
|
@@ -154,7 +158,7 @@ export class ConfigLoader {
|
|
|
154
158
|
|
|
155
159
|
const disabled = new Set(group.disabledItems || []);
|
|
156
160
|
const enabledItems: Record<string, string> = {};
|
|
157
|
-
for (const [name, value] of Object.entries(group.items)) {
|
|
161
|
+
for (const [name, value] of Object.entries(group.items || {})) {
|
|
158
162
|
if (!disabled.has(name)) {
|
|
159
163
|
enabledItems[name] = value;
|
|
160
164
|
}
|
|
@@ -174,8 +178,21 @@ export class ConfigLoader {
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
const params = group.params || {};
|
|
181
|
+
const restart = group.restart ?? config.tools?.[group.tool]?.restart;
|
|
182
|
+
|
|
183
|
+
return { config: group, items, tool, toolTemplate, params, restart };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getEffectiveRestart(groupName: string): GroupConfig['restart'] {
|
|
187
|
+
const config = this.load();
|
|
188
|
+
const group = config.groups[groupName];
|
|
189
|
+
|
|
190
|
+
if (!group) {
|
|
191
|
+
const available = Object.keys(config.groups).join(', ');
|
|
192
|
+
throw new ConfigError(`Unknown group: ${groupName}. Available: ${available}`);
|
|
193
|
+
}
|
|
177
194
|
|
|
178
|
-
return
|
|
195
|
+
return group.restart ?? (config.tools && config.tools[group.tool]?.restart) ?? undefined;
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
saveConfig(config: CliGrConfig): void {
|
package/src/config/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export interface ToolConfig {
|
|
2
2
|
cmd: string;
|
|
3
|
+
restart?: 'yes' | 'no' | 'unless-stopped';
|
|
3
4
|
}
|
|
4
5
|
|
|
5
6
|
export interface ItemEntry {
|
|
@@ -12,7 +13,7 @@ export interface GroupConfig {
|
|
|
12
13
|
restart?: 'yes' | 'no' | 'unless-stopped';
|
|
13
14
|
params?: Record<string, string>;
|
|
14
15
|
disabledItems?: string[];
|
|
15
|
-
items
|
|
16
|
+
items?: Record<string, string>;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface CliGrConfig {
|
|
@@ -198,6 +198,31 @@ groups:
|
|
|
198
198
|
assert.ok(output.includes('postgres'));
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
it('should show inherited restart from tool in ls output', async () => {
|
|
202
|
+
const configContent = `
|
|
203
|
+
tools:
|
|
204
|
+
docker:
|
|
205
|
+
cmd: docker run -it --rm
|
|
206
|
+
restart: yes
|
|
207
|
+
|
|
208
|
+
groups:
|
|
209
|
+
web:
|
|
210
|
+
tool: docker
|
|
211
|
+
items:
|
|
212
|
+
nginx: nginx
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
216
|
+
resetOutput();
|
|
217
|
+
|
|
218
|
+
const exitCode = await lsCommand('web');
|
|
219
|
+
|
|
220
|
+
assert.strictEqual(exitCode, 0);
|
|
221
|
+
const output = getLogOutput();
|
|
222
|
+
assert.ok(output.includes('Group: web'));
|
|
223
|
+
assert.ok(output.includes('Restart: yes'));
|
|
224
|
+
});
|
|
225
|
+
|
|
201
226
|
it('should list items with correct arguments', async () => {
|
|
202
227
|
const configContent = `
|
|
203
228
|
groups:
|
|
@@ -644,6 +669,32 @@ groups:
|
|
|
644
669
|
assert.ok(output.includes('1')); // item count for direct
|
|
645
670
|
});
|
|
646
671
|
|
|
672
|
+
it('should show inherited restart from tool in verbose mode', async () => {
|
|
673
|
+
const configContent = `
|
|
674
|
+
tools:
|
|
675
|
+
docker:
|
|
676
|
+
cmd: docker run
|
|
677
|
+
restart: yes
|
|
678
|
+
|
|
679
|
+
groups:
|
|
680
|
+
web:
|
|
681
|
+
tool: docker
|
|
682
|
+
items:
|
|
683
|
+
nginx: nginx
|
|
684
|
+
`;
|
|
685
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
686
|
+
resetOutput();
|
|
687
|
+
|
|
688
|
+
const exitCode = await groupsCommand(true);
|
|
689
|
+
|
|
690
|
+
assert.strictEqual(exitCode, 0);
|
|
691
|
+
const output = getLogOutput();
|
|
692
|
+
assert.ok(output.includes('GROUP'));
|
|
693
|
+
assert.ok(output.includes('RESTART'));
|
|
694
|
+
assert.ok(output.includes('web'));
|
|
695
|
+
assert.ok(output.includes('yes'));
|
|
696
|
+
});
|
|
697
|
+
|
|
647
698
|
it('should handle empty groups list', async () => {
|
|
648
699
|
const configContent = `
|
|
649
700
|
groups: {}
|
|
@@ -532,5 +532,77 @@ groups:
|
|
|
532
532
|
assert.strictEqual(loader.getGroup('restart-no').config.restart, 'no');
|
|
533
533
|
assert.strictEqual(loader.getGroup('restart-unless-stopped').config.restart, 'unless-stopped');
|
|
534
534
|
});
|
|
535
|
+
|
|
536
|
+
it('should inherit restart from tool when group has no restart', () => {
|
|
537
|
+
const configContent = `
|
|
538
|
+
tools:
|
|
539
|
+
docker:
|
|
540
|
+
cmd: docker run
|
|
541
|
+
restart: yes
|
|
542
|
+
|
|
543
|
+
groups:
|
|
544
|
+
web:
|
|
545
|
+
tool: docker
|
|
546
|
+
items:
|
|
547
|
+
nginx: nginx
|
|
548
|
+
`;
|
|
549
|
+
|
|
550
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
551
|
+
|
|
552
|
+
const loader = new ConfigLoader();
|
|
553
|
+
const result = loader.getGroup('web');
|
|
554
|
+
|
|
555
|
+
assert.strictEqual(result.config.restart, undefined);
|
|
556
|
+
assert.strictEqual(result.restart, 'yes');
|
|
557
|
+
assert.strictEqual(loader.getEffectiveRestart('web'), 'yes');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should allow group restart to override tool restart', () => {
|
|
561
|
+
const configContent = `
|
|
562
|
+
tools:
|
|
563
|
+
docker:
|
|
564
|
+
cmd: docker run
|
|
565
|
+
restart: yes
|
|
566
|
+
|
|
567
|
+
groups:
|
|
568
|
+
web:
|
|
569
|
+
tool: docker
|
|
570
|
+
restart: no
|
|
571
|
+
items:
|
|
572
|
+
nginx: nginx
|
|
573
|
+
`;
|
|
574
|
+
|
|
575
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
576
|
+
|
|
577
|
+
const loader = new ConfigLoader();
|
|
578
|
+
const result = loader.getGroup('web');
|
|
579
|
+
|
|
580
|
+
assert.strictEqual(result.config.restart, 'no');
|
|
581
|
+
assert.strictEqual(result.restart, 'no');
|
|
582
|
+
assert.strictEqual(loader.getEffectiveRestart('web'), 'no');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should return undefined restart when neither group nor tool defines it', () => {
|
|
586
|
+
const configContent = `
|
|
587
|
+
tools:
|
|
588
|
+
docker:
|
|
589
|
+
cmd: docker run
|
|
590
|
+
|
|
591
|
+
groups:
|
|
592
|
+
web:
|
|
593
|
+
tool: docker
|
|
594
|
+
items:
|
|
595
|
+
nginx: nginx
|
|
596
|
+
`;
|
|
597
|
+
|
|
598
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
599
|
+
|
|
600
|
+
const loader = new ConfigLoader();
|
|
601
|
+
const result = loader.getGroup('web');
|
|
602
|
+
|
|
603
|
+
assert.strictEqual(result.config.restart, undefined);
|
|
604
|
+
assert.strictEqual(result.restart, undefined);
|
|
605
|
+
assert.strictEqual(loader.getEffectiveRestart('web'), undefined);
|
|
606
|
+
});
|
|
535
607
|
});
|
|
536
608
|
});
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# Improve Web UI Console Logging Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Enhance the web UI console panel in `cligr serve` with timestamps, system event logging, color-coded output, and a clear button.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Modify the embedded HTML/CSS/JS inside `src/commands/serve.ts`'s `serveHtml()` function. All changes are client-side: timestamps are generated in `appendLog`, system events are rendered when `status` SSE events arrive, and a clear button empties the log container.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** TypeScript, vanilla HTML/CSS/JS embedded in a string.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: Add console header layout and clear button
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
- Modify: `src/commands/serve.ts:232-234`
|
|
17
|
-
|
|
18
|
-
- [ ] **Step 1: Update `.main h2` CSS to support a flex header with button**
|
|
19
|
-
|
|
20
|
-
Replace:
|
|
21
|
-
```css
|
|
22
|
-
.main h2 { font-size: 1rem; margin: 0; padding: 0.5rem 1rem; border-bottom: 1px solid #ccc; background: #f0f0f0; }
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
With:
|
|
26
|
-
```css
|
|
27
|
-
.main-header { display: flex; align-items: center; justify-content: space-between; font-size: 1rem; margin: 0; padding: 0.5rem 1rem; border-bottom: 1px solid #ccc; background: #f0f0f0; }
|
|
28
|
-
.main-header h2 { font-size: 1rem; margin: 0; }
|
|
29
|
-
.clear-btn { font-size: 0.8rem; padding: 0.25rem 0.6rem; cursor: pointer; }
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
- [ ] **Step 2: Replace the `<h2>Console</h2>` markup with the new header**
|
|
33
|
-
|
|
34
|
-
Replace:
|
|
35
|
-
```html
|
|
36
|
-
<h2>Console</h2>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
With:
|
|
40
|
-
```html
|
|
41
|
-
<div class="main-header">
|
|
42
|
-
<h2>Console</h2>
|
|
43
|
-
<button class="clear-btn" id="clearBtn">Clear</button>
|
|
44
|
-
</div>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
- [ ] **Step 3: Add clear button click handler in the script section**
|
|
48
|
-
|
|
49
|
-
Add after:
|
|
50
|
-
```javascript
|
|
51
|
-
const groupsEl = document.getElementById('groups');
|
|
52
|
-
const logsEl = document.getElementById('logs');
|
|
53
|
-
const resizer = document.getElementById('resizer');
|
|
54
|
-
let autoScroll = true;
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
```javascript
|
|
58
|
-
const clearBtn = document.getElementById('clearBtn');
|
|
59
|
-
clearBtn.addEventListener('click', () => {
|
|
60
|
-
logsEl.innerHTML = '';
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
- [ ] **Step 4: Typecheck**
|
|
65
|
-
|
|
66
|
-
Run: `npm run typecheck`
|
|
67
|
-
Expected: No errors.
|
|
68
|
-
|
|
69
|
-
- [ ] **Step 5: Commit**
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
git add src/commands/serve.ts
|
|
73
|
-
git commit -m "feat(serve): add clear button to console header"
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
### Task 2: Add timestamp and color-coded log styling
|
|
79
|
-
|
|
80
|
-
**Files:**
|
|
81
|
-
- Modify: `src/commands/serve.ts:238-239` and `src/commands/serve.ts:336-342`
|
|
82
|
-
|
|
83
|
-
- [ ] **Step 1: Add CSS classes for log styling**
|
|
84
|
-
|
|
85
|
-
Replace:
|
|
86
|
-
```css
|
|
87
|
-
.logs { flex: 1; background: #111; color: #0f0; font-family: monospace; font-size: 0.85rem; overflow-y: auto; padding: 0.75rem; white-space: pre-wrap; }
|
|
88
|
-
.error { color: #f55; }
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
With:
|
|
92
|
-
```css
|
|
93
|
-
.logs { flex: 1; background: #111; color: #0f0; font-family: monospace; font-size: 0.85rem; overflow-y: auto; padding: 0.75rem; white-space: pre-wrap; }
|
|
94
|
-
.log-line { margin: 0.1rem 0; }
|
|
95
|
-
.log-time { color: #888; }
|
|
96
|
-
.log-system { color: #6cf; }
|
|
97
|
-
.log-error { color: #f55; }
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
- [ ] **Step 2: Rewrite `appendLog` to include timestamps and structured styling**
|
|
101
|
-
|
|
102
|
-
Replace:
|
|
103
|
-
```javascript
|
|
104
|
-
function appendLog(line, isError) {
|
|
105
|
-
const span = document.createElement('div');
|
|
106
|
-
span.textContent = line;
|
|
107
|
-
if (isError) span.className = 'error';
|
|
108
|
-
logsEl.appendChild(span);
|
|
109
|
-
if (autoScroll) logsEl.scrollTop = logsEl.scrollHeight;
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
With:
|
|
114
|
-
```javascript
|
|
115
|
-
function appendLog(line, isError) {
|
|
116
|
-
const div = document.createElement('div');
|
|
117
|
-
div.className = 'log-line';
|
|
118
|
-
|
|
119
|
-
const time = document.createElement('span');
|
|
120
|
-
time.className = 'log-time';
|
|
121
|
-
const now = new Date();
|
|
122
|
-
const hh = String(now.getHours()).padStart(2, '0');
|
|
123
|
-
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
124
|
-
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
125
|
-
time.textContent = `[${hh}:${mm}:${ss}] `;
|
|
126
|
-
div.appendChild(time);
|
|
127
|
-
|
|
128
|
-
const content = document.createElement('span');
|
|
129
|
-
content.textContent = line;
|
|
130
|
-
if (isError) content.className = 'log-error';
|
|
131
|
-
div.appendChild(content);
|
|
132
|
-
|
|
133
|
-
logsEl.appendChild(div);
|
|
134
|
-
if (autoScroll) logsEl.scrollTop = logsEl.scrollHeight;
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
- [ ] **Step 3: Typecheck**
|
|
139
|
-
|
|
140
|
-
Run: `npm run typecheck`
|
|
141
|
-
Expected: No errors.
|
|
142
|
-
|
|
143
|
-
- [ ] **Step 4: Commit**
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
git add src/commands/serve.ts
|
|
147
|
-
git commit -m "feat(serve): add timestamps and color-coded log styling"
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
### Task 3: Log system events to the console
|
|
153
|
-
|
|
154
|
-
**Files:**
|
|
155
|
-
- Modify: `src/commands/serve.ts:344-354`
|
|
156
|
-
|
|
157
|
-
- [ ] **Step 1: Update the `status` SSE handler to append system messages**
|
|
158
|
-
|
|
159
|
-
Replace:
|
|
160
|
-
```javascript
|
|
161
|
-
evtSource.addEventListener('status', (e) => {
|
|
162
|
-
fetchGroups();
|
|
163
|
-
});
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
With:
|
|
167
|
-
```javascript
|
|
168
|
-
evtSource.addEventListener('status', (e) => {
|
|
169
|
-
const data = JSON.parse(e.data);
|
|
170
|
-
if (data.type === 'group-started') {
|
|
171
|
-
appendLog(`[system] Group "${data.groupName}" started`, false);
|
|
172
|
-
const groupTime = document.querySelector('.log-line:last-child span:last-child');
|
|
173
|
-
if (groupTime) groupTime.className = 'log-system';
|
|
174
|
-
} else if (data.type === 'group-stopped') {
|
|
175
|
-
appendLog(`[system] Group "${data.groupName}" stopped`, false);
|
|
176
|
-
const groupTime = document.querySelector('.log-line:last-child span:last-child');
|
|
177
|
-
if (groupTime) groupTime.className = 'log-system';
|
|
178
|
-
} else if (data.type === 'item-restarted') {
|
|
179
|
-
appendLog(`[system] Item "${data.groupName}/${data.itemName}" restarted`, false);
|
|
180
|
-
const groupTime = document.querySelector('.log-line:last-child span:last-child');
|
|
181
|
-
if (groupTime) groupTime.className = 'log-system';
|
|
182
|
-
}
|
|
183
|
-
fetchGroups();
|
|
184
|
-
});
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Wait — the above uses `querySelector` hackily. Better: introduce an `appendSystemLog(message)` helper.
|
|
188
|
-
|
|
189
|
-
Instead, replace the whole block with:
|
|
190
|
-
|
|
191
|
-
```javascript
|
|
192
|
-
function appendSystemLog(message) {
|
|
193
|
-
const div = document.createElement('div');
|
|
194
|
-
div.className = 'log-line';
|
|
195
|
-
|
|
196
|
-
const time = document.createElement('span');
|
|
197
|
-
time.className = 'log-time';
|
|
198
|
-
const now = new Date();
|
|
199
|
-
const hh = String(now.getHours()).padStart(2, '0');
|
|
200
|
-
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
201
|
-
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
202
|
-
time.textContent = `[${hh}:${mm}:${ss}] `;
|
|
203
|
-
div.appendChild(time);
|
|
204
|
-
|
|
205
|
-
const content = document.createElement('span');
|
|
206
|
-
content.className = 'log-system';
|
|
207
|
-
content.textContent = message;
|
|
208
|
-
div.appendChild(content);
|
|
209
|
-
|
|
210
|
-
logsEl.appendChild(div);
|
|
211
|
-
if (autoScroll) logsEl.scrollTop = logsEl.scrollHeight;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
evtSource.addEventListener('status', (e) => {
|
|
215
|
-
const data = JSON.parse(e.data);
|
|
216
|
-
if (data.type === 'group-started') {
|
|
217
|
-
appendSystemLog(`[system] Group "${data.groupName}" started`);
|
|
218
|
-
} else if (data.type === 'group-stopped') {
|
|
219
|
-
appendSystemLog(`[system] Group "${data.groupName}" stopped`);
|
|
220
|
-
} else if (data.type === 'item-restarted') {
|
|
221
|
-
appendSystemLog(`[system] Item "${data.groupName}/${data.itemName}" restarted`);
|
|
222
|
-
}
|
|
223
|
-
fetchGroups();
|
|
224
|
-
});
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
- [ ] **Step 2: Typecheck**
|
|
228
|
-
|
|
229
|
-
Run: `npm run typecheck`
|
|
230
|
-
Expected: No errors.
|
|
231
|
-
|
|
232
|
-
- [ ] **Step 3: Build**
|
|
233
|
-
|
|
234
|
-
Run: `npm run build`
|
|
235
|
-
Expected: Build completes successfully.
|
|
236
|
-
|
|
237
|
-
- [ ] **Step 4: Commit**
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
git add src/commands/serve.ts
|
|
241
|
-
git commit -m "feat(serve): log system events in web UI console"
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## Self-Review
|
|
247
|
-
|
|
248
|
-
**Spec coverage:**
|
|
249
|
-
- Timestamps: Task 2 (`appendLog` adds `[HH:MM:SS]`).
|
|
250
|
-
- System events in console: Task 3 (`appendSystemLog` for `group-started`, `group-stopped`, `item-restarted`).
|
|
251
|
-
- Color coding: Task 2 (`.log-time`, `.log-system`, `.log-error`) and Task 3 (uses `.log-system`).
|
|
252
|
-
- Clear button: Task 1 (Clear button + click handler).
|
|
253
|
-
|
|
254
|
-
**Placeholder scan:** None found. Every step has exact code, file paths, and commands.
|
|
255
|
-
|
|
256
|
-
**Type consistency:** `appendLog` signature unchanged (`line, isError`). `appendSystemLog` is a new helper. All DOM APIs are standard.
|