diff --git a/plugins/batch-start/.gitignore b/plugins/batch-start/.gitignore new file mode 100644 index 000000000..e755a50cc --- /dev/null +++ b/plugins/batch-start/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +src-ztools/dist/ +src-ztools/preload/services.js +batch-start-plugin.zip +.DS_Store +*.local +.env* +.worktrees/ + diff --git a/plugins/batch-start/CHANGELOG.md b/plugins/batch-start/CHANGELOG.md index 298ad26fc..1566f2f7b 100644 --- a/plugins/batch-start/CHANGELOG.md +++ b/plugins/batch-start/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.3 - 2026-11-11 + +- 管理界面三栏重构:跟随主机主题,分类/启动组侧栏可折叠 +- 分类筛选与左侧选中目标解耦;分类操作改为箭头弹出菜单 +- 应用支持多分类归属,列表展示分类/启动组标签并可一键移除 +- 应用行增加启动按钮,支持直接打开系统应用或 ZTools 指令 + ## 0.1.2 - 2026-10-16 - 修复启动组包含 ZTools 指令时,`outPlugin(true)` 过早退出导致目标插件装配被中止的问题 diff --git a/plugins/batch-start/dist/assets/index-CiJ97cWo.js b/plugins/batch-start/dist/assets/index-CiJ97cWo.js deleted file mode 100644 index 1e6344714..000000000 --- a/plugins/batch-start/dist/assets/index-CiJ97cWo.js +++ /dev/null @@ -1,17 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function s(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(r){if(r.ep)return;r.ep=!0;const i=s(r);fetch(r.href,i)}})();/** -* @vue/shared v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/function Ws(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const Q={},_t=[],Ne=()=>{},Vn=()=>!1,us=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),fs=e=>e.startsWith("onUpdate:"),oe=Object.assign,ks=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Yr=Object.prototype.hasOwnProperty,V=(e,t)=>Yr.call(e,t),R=Array.isArray,tt=e=>Ut(e)==="[object Map]",ts=e=>Ut(e)==="[object Set]",pn=e=>Ut(e)==="[object Date]",D=e=>typeof e=="function",ee=e=>typeof e=="string",He=e=>typeof e=="symbol",G=e=>e!==null&&typeof e=="object",Wn=e=>(G(e)||D(e))&&D(e.then)&&D(e.catch),kn=Object.prototype.toString,Ut=e=>kn.call(e),Zr=e=>Ut(e).slice(8,-1),Gn=e=>Ut(e)==="[object Object]",Gs=e=>ee(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,It=Ws(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),as=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},Xr=/-\w/g,Se=as(e=>e.replace(Xr,t=>t.slice(1).toUpperCase())),ei=/\B([A-Z])/g,ft=as(e=>e.replace(ei,"-$1").toLowerCase()),qn=as(e=>e.charAt(0).toUpperCase()+e.slice(1)),xs=as(e=>e?`on${qn(e)}`:""),De=(e,t)=>!Object.is(e,t),ws=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},ti=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let gn;const ds=()=>gn||(gn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function qs(e){if(R(e)){const t={};for(let s=0;s{if(s){const n=s.split(ni);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function st(e){let t="";if(ee(e))t=e;else if(R(e))for(let s=0;s!!(e&&e.__v_isRef===!0),ve=e=>ee(e)?e:e==null?"":R(e)||G(e)&&(e.toString===kn||!D(e.toString))?Qn(e)?ve(e.value):JSON.stringify(e,Yn,2):String(e),Yn=(e,t)=>Qn(t)?Yn(e,t.value):tt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,r],i)=>(s[Ss(n,i)+" =>"]=r,s),{})}:ts(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>Ss(s))}:He(t)?Ss(t):G(t)&&!R(t)&&!Gn(t)?String(t):t,Ss=(e,t="")=>{var s;return He(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};/** -* @vue/reactivity v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/let ie;class ui{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&ie&&(ie.active?(this.parent=ie,this.index=(ie.scopes||(ie.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes){const n=this.scopes.slice();for(t=0,s=n.length;t0&&--this._on===0){if(ie===this)ie=this.prevScope;else{let t=ie;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let s,n;for(s=0,n=this.effects.length;s0)return;if(Pt){let t=Pt;for(Pt=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;$t;){let t=$t;for($t=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function tr(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function sr(e){let t,s=e.depsTail,n=s;for(;n;){const r=n.prevDep;n.version===-1?(n===s&&(s=r),Qs(n),ai(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=r}e.deps=t,e.depsTail=s}function Fs(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(nr(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function nr(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Dt)||(e.globalVersion=Dt,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Fs(e))))return;e.flags|=2;const t=e.dep,s=J,n=Ce;J=e,Ce=!0;try{tr(e);const r=e.fn(e._value);(t.version===0||De(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{J=s,Ce=n,sr(e),e.flags&=-3}}function Qs(e,t=!1){const{dep:s,prevSub:n,nextSub:r}=e;if(n&&(n.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)Qs(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function ai(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let Ce=!0;const rr=[];function qe(){rr.push(Ce),Ce=!1}function ze(){const e=rr.pop();Ce=e===void 0?!0:e}function _n(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=J;J=void 0;try{t()}finally{J=s}}}let Dt=0;class di{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Ys{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!J||!Ce||J===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==J)s=this.activeLink=new di(J,this),J.deps?(s.prevDep=J.depsTail,J.depsTail.nextDep=s,J.depsTail=s):J.deps=J.depsTail=s,ir(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=J.depsTail,s.nextDep=void 0,J.depsTail.nextDep=s,J.depsTail=s,J.deps===s&&(J.deps=n)}return s}trigger(t){this.version++,Dt++,this.notify(t)}notify(t){zs();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{Js()}}}function ir(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)ir(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const Ds=new WeakMap,lt=Symbol(""),js=Symbol(""),jt=Symbol("");function ce(e,t,s){if(Ce&&J){let n=Ds.get(e);n||Ds.set(e,n=new Map);let r=n.get(s);r||(n.set(s,r=new Ys),r.map=n,r.key=s),r.track()}}function Ge(e,t,s,n,r,i){const o=Ds.get(e);if(!o){Dt++;return}const l=u=>{u&&u.trigger()};if(zs(),t==="clear")o.forEach(l);else{const u=R(e),f=u&&Gs(s);if(u&&s==="length"){const a=Number(n);o.forEach((h,v)=>{(v==="length"||v===jt||!He(v)&&v>=a)&&l(h)})}else switch((s!==void 0||o.has(void 0))&&l(o.get(s)),f&&l(o.get(jt)),t){case"add":u?f&&l(o.get("length")):(l(o.get(lt)),tt(e)&&l(o.get(js)));break;case"delete":u||(l(o.get(lt)),tt(e)&&l(o.get(js)));break;case"set":tt(e)&&l(o.get(lt));break}}Js()}function gt(e){const t=U(e);return t===e?t:(ce(t,"iterate",jt),be(e)?t:t.map(Te))}function ps(e){return ce(e=U(e),"iterate",jt),e}function Me(e,t){return Je(e)?bt(ct(e)?Te(t):t):Te(t)}const hi={__proto__:null,[Symbol.iterator](){return Ts(this,Symbol.iterator,e=>Me(this,e))},concat(...e){return gt(this).concat(...e.map(t=>R(t)?gt(t):t))},entries(){return Ts(this,"entries",e=>(e[1]=Me(this,e[1]),e))},every(e,t){return Ve(this,"every",e,t,void 0,arguments)},filter(e,t){return Ve(this,"filter",e,t,s=>s.map(n=>Me(this,n)),arguments)},find(e,t){return Ve(this,"find",e,t,s=>Me(this,s),arguments)},findIndex(e,t){return Ve(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ve(this,"findLast",e,t,s=>Me(this,s),arguments)},findLastIndex(e,t){return Ve(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ve(this,"forEach",e,t,void 0,arguments)},includes(...e){return As(this,"includes",e)},indexOf(...e){return As(this,"indexOf",e)},join(e){return gt(this).join(e)},lastIndexOf(...e){return As(this,"lastIndexOf",e)},map(e,t){return Ve(this,"map",e,t,void 0,arguments)},pop(){return Tt(this,"pop")},push(...e){return Tt(this,"push",e)},reduce(e,...t){return yn(this,"reduce",e,t)},reduceRight(e,...t){return yn(this,"reduceRight",e,t)},shift(){return Tt(this,"shift")},some(e,t){return Ve(this,"some",e,t,void 0,arguments)},splice(...e){return Tt(this,"splice",e)},toReversed(){return gt(this).toReversed()},toSorted(e){return gt(this).toSorted(e)},toSpliced(...e){return gt(this).toSpliced(...e)},unshift(...e){return Tt(this,"unshift",e)},values(){return Ts(this,"values",e=>Me(this,e))}};function Ts(e,t,s){const n=ps(e),r=n[t]();return n!==e&&!be(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=s(i.value)),i}),r}const pi=Array.prototype;function Ve(e,t,s,n,r,i){const o=ps(e),l=o!==e&&!be(e),u=o[t];if(u!==pi[t]){const h=u.apply(e,i);return l?Te(h):h}let f=s;o!==e&&(l?f=function(h,v){return s.call(this,Me(e,h),v,e)}:s.length>2&&(f=function(h,v){return s.call(this,h,v,e)}));const a=u.call(o,f,n);return l&&r?r(a):a}function yn(e,t,s,n){const r=ps(e),i=r!==e&&!be(e);let o=s,l=!1;r!==e&&(i?(l=n.length===0,o=function(f,a,h){return l&&(l=!1,f=Me(e,f)),s.call(this,f,Me(e,a),h,e)}):s.length>3&&(o=function(f,a,h){return s.call(this,f,a,h,e)}));const u=r[t](o,...n);return l?Me(e,u):u}function As(e,t,s){const n=U(e);ce(n,"iterate",jt);const r=n[t](...s);return(r===-1||r===!1)&&tn(s[0])?(s[0]=U(s[0]),n[t](...s)):r}function Tt(e,t,s=[]){qe(),zs();const n=U(e)[t].apply(e,s);return Js(),ze(),n}const gi=Ws("__proto__,__v_isRef,__isVue"),or=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(He));function mi(e){He(e)||(e=String(e));const t=U(this);return ce(t,"has",e),t.hasOwnProperty(e)}class lr{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!r;if(s==="__v_isReadonly")return r;if(s==="__v_isShallow")return i;if(s==="__v_raw")return n===(r?i?Ai:ar:i?fr:ur).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const o=R(t);if(!r){let u;if(o&&(u=hi[s]))return u;if(s==="hasOwnProperty")return mi}const l=Reflect.get(t,s,ue(t)?t:n);if((He(s)?or.has(s):gi(s))||(r||ce(t,"get",s),i))return l;if(ue(l)){const u=o&&Gs(s)?l:l.value;return r&&G(u)?Ns(u):u}return G(l)?r?Ns(l):Xs(l):l}}class cr extends lr{constructor(t=!1){super(!1,t)}set(t,s,n,r){let i=t[s];const o=R(t)&&Gs(s);if(!this._isShallow){const f=Je(i);if(!be(n)&&!Je(n)&&(i=U(i),n=U(n)),!o&&ue(i)&&!ue(n))return f||(i.value=n),!0}const l=o?Number(s)e,Jt=e=>Reflect.getPrototypeOf(e);function xi(e,t,s){return function(...n){const r=this.__v_raw,i=U(r),o=tt(i),l=e==="entries"||e===Symbol.iterator&&o,u=e==="keys"&&o,f=r[e](...n),a=s?Ls:t?bt:Te;return!t&&ce(i,"iterate",u?js:lt),oe(Object.create(f),{next(){const{value:h,done:v}=f.next();return v?{value:h,done:v}:{value:l?[a(h[0]),a(h[1])]:a(h),done:v}}})}}function Qt(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function wi(e,t){const s={get(r){const i=this.__v_raw,o=U(i),l=U(r);e||(De(r,l)&&ce(o,"get",r),ce(o,"get",l));const{has:u}=Jt(o),f=t?Ls:e?bt:Te;if(u.call(o,r))return f(i.get(r));if(u.call(o,l))return f(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&ce(U(r),"iterate",lt),r.size},has(r){const i=this.__v_raw,o=U(i),l=U(r);return e||(De(r,l)&&ce(o,"has",r),ce(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,u=U(l),f=t?Ls:e?bt:Te;return!e&&ce(u,"iterate",lt),l.forEach((a,h)=>r.call(i,f(a),f(h),o))}};return oe(s,e?{add:Qt("add"),set:Qt("set"),delete:Qt("delete"),clear:Qt("clear")}:{add(r){const i=U(this),o=Jt(i),l=U(r),u=!t&&!be(r)&&!Je(r)?l:r;return o.has.call(i,u)||De(r,u)&&o.has.call(i,r)||De(l,u)&&o.has.call(i,l)||(i.add(u),Ge(i,"add",u,u)),this},set(r,i){!t&&!be(i)&&!Je(i)&&(i=U(i));const o=U(this),{has:l,get:u}=Jt(o);let f=l.call(o,r);f||(r=U(r),f=l.call(o,r));const a=u.call(o,r);return o.set(r,i),f?De(i,a)&&Ge(o,"set",r,i):Ge(o,"add",r,i),this},delete(r){const i=U(this),{has:o,get:l}=Jt(i);let u=o.call(i,r);u||(r=U(r),u=o.call(i,r)),l&&l.call(i,r);const f=i.delete(r);return u&&Ge(i,"delete",r,void 0),f},clear(){const r=U(this),i=r.size!==0,o=r.clear();return i&&Ge(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{s[r]=xi(r,e,t)}),s}function Zs(e,t){const s=wi(e,t);return(n,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?n:Reflect.get(V(s,r)&&r in n?s:n,r,i)}const Si={get:Zs(!1,!1)},Ci={get:Zs(!1,!0)},Ti={get:Zs(!0,!1)};const ur=new WeakMap,fr=new WeakMap,ar=new WeakMap,Ai=new WeakMap;function Ei(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Xs(e){return Je(e)?e:en(e,!1,yi,Si,ur)}function Oi(e){return en(e,!1,bi,Ci,fr)}function Ns(e){return en(e,!0,vi,Ti,ar)}function en(e,t,s,n,r){if(!G(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=r.get(e);if(i)return i;const o=Ei(Zr(e));if(o===0)return e;const l=new Proxy(e,o===2?n:s);return r.set(e,l),l}function ct(e){return Je(e)?ct(e.__v_raw):!!(e&&e.__v_isReactive)}function Je(e){return!!(e&&e.__v_isReadonly)}function be(e){return!!(e&&e.__v_isShallow)}function tn(e){return e?!!e.__v_raw:!1}function U(e){const t=e&&e.__v_raw;return t?U(t):e}function Ii(e){return!V(e,"__v_skip")&&Object.isExtensible(e)&&zn(e,"__v_skip",!0),e}const Te=e=>G(e)?Xs(e):e,bt=e=>G(e)?Ns(e):e;function ue(e){return e?e.__v_isRef===!0:!1}function me(e){return $i(e,!1)}function $i(e,t){return ue(e)?e:new Pi(e,t)}class Pi{constructor(t,s){this.dep=new Ys,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:U(t),this._value=s?t:Te(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,n=this.__v_isShallow||be(t)||Je(t);t=n?t:U(t),De(t,s)&&(this._rawValue=t,this._value=n?t:Te(t),this.dep.trigger())}}function Mi(e){return ue(e)?e.value:e}const Ri={get:(e,t,s)=>t==="__v_raw"?e:Mi(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const r=e[t];return ue(r)&&!ue(s)?(r.value=s,!0):Reflect.set(e,t,s,n)}};function dr(e){return ct(e)?e:new Proxy(e,Ri)}class Fi{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new Ys(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Dt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&J!==this)return er(this,!0),!0}get value(){const t=this.dep.track();return nr(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Di(e,t,s=!1){let n,r;return D(e)?n=e:(n=e.get,r=e.set),new Fi(n,r,s)}const Yt={},ss=new WeakMap;let ot;function ji(e,t=!1,s=ot){if(s){let n=ss.get(s);n||ss.set(s,n=[]),n.push(e)}}function Li(e,t,s=Q){const{immediate:n,deep:r,once:i,scheduler:o,augmentJob:l,call:u}=s,f=I=>r?I:be(I)||r===!1||r===0?et(I,1):et(I);let a,h,v,S,B=!1,M=!1;if(ue(e)?(h=()=>e.value,B=be(e)):ct(e)?(h=()=>f(e),B=!0):R(e)?(M=!0,B=e.some(I=>ct(I)||be(I)),h=()=>e.map(I=>{if(ue(I))return I.value;if(ct(I))return f(I);if(D(I))return u?u(I,2):I()})):D(e)?t?h=u?()=>u(e,2):e:h=()=>{if(v){qe();try{v()}finally{ze()}}const I=ot;ot=a;try{return u?u(e,3,[S]):e(S)}finally{ot=I}}:h=Ne,t&&r){const I=h,Z=r===!0?1/0:r;h=()=>et(I(),Z)}const X=fi(),z=()=>{a.stop(),X&&X.active&&ks(X.effects,a)};if(i&&t){const I=t;t=(...Z)=>{const ye=I(...Z);return z(),ye}}let F=M?new Array(e.length).fill(Yt):Yt;const H=I=>{if(!(!(a.flags&1)||!a.dirty&&!I))if(t){const Z=a.run();if(I||r||B||(M?Z.some((ye,te)=>De(ye,F[te])):De(Z,F))){v&&v();const ye=ot;ot=a;try{const te=[Z,F===Yt?void 0:M&&F[0]===Yt?[]:F,S];F=Z,u?u(t,3,te):t(...te)}finally{ot=ye}}}else a.run()};return l&&l(H),a=new Zn(h),a.scheduler=o?()=>o(H,!1):H,S=I=>ji(I,!1,a),v=a.onStop=()=>{const I=ss.get(a);if(I){if(u)u(I,4);else for(const Z of I)Z();ss.delete(a)}},t?n?H(!0):F=a.run():o?o(H.bind(null,!0),!0):a.run(),z.pause=a.pause.bind(a),z.resume=a.resume.bind(a),z.stop=z,z}function et(e,t=1/0,s){if(t<=0||!G(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,ue(e))et(e.value,t,s);else if(R(e))for(let n=0;n{et(n,t,s)});else if(Gn(e)){for(const n in e)et(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&et(e[n],t,s)}return e}/** -* @vue/runtime-core v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/function Vt(e,t,s,n){try{return n?e(...n):e()}catch(r){gs(r,t,s)}}function Ae(e,t,s,n){if(D(e)){const r=Vt(e,t,s,n);return r&&Wn(r)&&r.catch(i=>{gs(i,t,s)}),r}if(R(e)){const r=[];for(let i=0;i>>1,r=ae[n],i=Lt(r);i=Lt(s)?ae.push(e):ae.splice(Ki(t),0,e),e.flags|=1,pr()}}function pr(){ns||(ns=hr.then(mr))}function Bi(e){if(!R(e))Xe&&e.id===-1?Xe.splice(mt+1,0,e):e.flags&1||(yt.push(e),e.flags|=1);else for(let t=0;tLt(s)-Lt(n));if(yt.length=0,Xe){for(let s=0;se.id==null?e.flags&2?-1:1/0:e.id;function mr(e){try{for(Pe=0;Pe{n._d&&$n(-1);const i=rs(t),o=ut.length;let l;try{l=e(...r)}finally{for(let u=ut.length;u>o;u--)Ur();rs(i),n._d&&$n(1)}return l};return n._n=!0,n._c=!0,n._d=!0,n}function rt(e,t,s,n){const r=e.dirs,i=t&&t.dirs;for(let o=0;o1)return s&&D(t)?t.call(n&&n.proxy):t}}const Wi=Symbol.for("v-scx"),ki=()=>Xt(Wi);function Es(e,t,s){return yr(e,t,s)}function yr(e,t,s=Q){const{immediate:n,deep:r,flush:i,once:o}=s,l=oe({},s),u=t&&n||!t&&i!=="post";let f;if(Bt){if(i==="sync"){const S=ki();f=S.__watcherHandles||(S.__watcherHandles=[])}else if(!u){const S=()=>{};return S.stop=Ne,S.resume=Ne,S.pause=Ne,S}}const a=he;l.call=(S,B,M)=>Ae(S,a,B,M);let h=!1;i==="post"?l.scheduler=S=>{pe(S,a&&a.suspense)}:i!=="sync"&&(h=!0,l.scheduler=(S,B)=>{B?S():sn(S)}),l.augmentJob=S=>{t&&(S.flags|=4),h&&(S.flags|=2,a&&(S.id=a.uid,S.i=a))};const v=Li(e,t,l);return Bt&&(f?f.push(v):u&&v()),v}function Gi(e,t,s){const n=this.proxy,r=ee(e)?e.includes(".")?vr(n,e):()=>n[e]:e.bind(n,n);let i;D(t)?i=t:(i=t.handler,s=t);const o=kt(this),l=yr(r,i.bind(n),s);return o(),l}function vr(e,t){const s=t.split(".");return()=>{let n=e;for(let r=0;re.__isTeleport,Os=Symbol("_leaveCb");function zi(e){let t=e[0];if(e.length>1){for(const s of e)if(s.type!==Qe){t=s;break}}return t}function br(e){if(!rn(e))return ms(e.type)&&e.children?zi(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:s}=e;if(s){if(t&16)return s[0];if(t&32&&D(s.default))return s.default()}}function nn(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;const s=e.component.subTree;nn(ms(s.type)&&br(s)||s,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Wt(e,t){return D(e)?oe({name:e.name},t,{setup:e}):e}function xr(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function bn(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const is=new WeakMap;function Mt(e,t,s,n,r=!1){if(R(e)){e.forEach((M,X)=>Mt(M,t&&(R(t)?t[X]:t),s,n,r));return}if(Rt(n)&&!r){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&Mt(e,t,s,n.component.subTree);return}const i=n.shapeFlag&4?un(n.component):n.el,o=r?null:i,{i:l,r:u}=e,f=t&&t.r,a=l.refs===Q?l.refs={}:l.refs,h=l.setupState,v=U(h),S=h===Q?Vn:M=>bn(a,M)?!1:V(v,M),B=(M,X)=>!(X&&bn(a,X));if(f!=null&&f!==u){if(xn(t),ee(f))a[f]=null,S(f)&&(h[f]=null);else if(ue(f)){const M=t;B(f,M.k)&&(f.value=null),M.k&&(a[M.k]=null)}}if(D(u))Vt(u,l,12,[o,a]);else{const M=ee(u),X=ue(u);if(M||X){const z=()=>{if(e.f){const F=M?S(u)?h[u]:a[u]:B()||!e.k?u.value:a[e.k];if(r)R(F)&&ks(F,i);else if(R(F))F.includes(i)||F.push(i);else if(M)a[u]=[i],S(u)&&(h[u]=a[u]);else{const H=[i];B(u,e.k)&&(u.value=H),e.k&&(a[e.k]=H)}}else M?(a[u]=o,S(u)&&(h[u]=o)):X&&(B(u,e.k)&&(u.value=o),e.k&&(a[e.k]=o))};if(o){const F=()=>{z(),is.delete(e)};F.id=-1,is.set(e,F),pe(F,s)}else xn(e),z()}}}function xn(e){const t=is.get(e);t&&(t.flags|=8,is.delete(e))}ds().requestIdleCallback;ds().cancelIdleCallback;const Rt=e=>!!e.type.__asyncLoader,rn=e=>e.type.__isKeepAlive;function Ji(e,t){wr(e,"a",t)}function Qi(e,t){wr(e,"da",t)}function wr(e,t,s=he){const n=e.__wdc||(e.__wdc=()=>{let r=s;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(_s(t,n,s),s){let r=s.parent;for(;r&&r.parent;)rn(r.parent.vnode)&&Yi(n,t,s,r),r=r.parent}}function Yi(e,t,s,n){const r=_s(t,e,n,!0);Cr(()=>{ks(n[t],r)},s)}function _s(e,t,s=he,n=!1){if(s){const r=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...o)=>{qe();const l=kt(s),u=Ae(t,s,e,o);return l(),ze(),u});return n?r.unshift(i):r.push(i),i}}const Ye=e=>(t,s=he)=>{(!Bt||e==="sp")&&_s(e,(...n)=>t(...n),s)},Zi=Ye("bm"),Sr=Ye("m"),Xi=Ye("bu"),eo=Ye("u"),to=Ye("bum"),Cr=Ye("um"),so=Ye("sp"),no=Ye("rtg"),ro=Ye("rtc");function io(e,t=he){_s("ec",e,t)}const oo=Symbol.for("v-ndc");function Nt(e,t,s,n){let r;const i=s,o=R(e);if(o||ee(e)){const l=o&&ct(e);let u=!1,f=!1;l&&(u=!be(e),f=Je(e),e=ps(e)),r=new Array(e.length);for(let a=0,h=e.length;at(l,u,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let u=0,f=l.length;ue?qr(e)?un(e):Hs(e.parent):null,Ft=oe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Hs(e.parent),$root:e=>Hs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Ar(e),$forceUpdate:e=>e.f||(e.f=()=>{sn(e.update)}),$nextTick:e=>e.n||(e.n=Hi.bind(e.proxy)),$watch:e=>Gi.bind(e)}),Is=(e,t)=>e!==Q&&!e.__isScriptSetup&&V(e,t),lo={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:r,props:i,accessCache:o,type:l,appContext:u}=e;if(t[0]!=="$"){const v=o[t];if(v!==void 0)switch(v){case 1:return n[t];case 2:return r[t];case 4:return s[t];case 3:return i[t]}else{if(Is(n,t))return o[t]=1,n[t];if(r!==Q&&V(r,t))return o[t]=2,r[t];if(V(i,t))return o[t]=3,i[t];if(s!==Q&&V(s,t))return o[t]=4,s[t];Ks&&(o[t]=0)}}const f=Ft[t];let a,h;if(f)return t==="$attrs"&&ce(e.attrs,"get",""),f(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(s!==Q&&V(s,t))return o[t]=4,s[t];if(h=u.config.globalProperties,V(h,t))return h[t]},set({_:e},t,s){const{data:n,setupState:r,ctx:i}=e;return Is(r,t)?(r[t]=s,!0):n!==Q&&V(n,t)?(n[t]=s,!0):V(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:r,props:i,type:o}},l){let u;return!!(s[l]||e!==Q&&l[0]!=="$"&&V(e,l)||Is(t,l)||V(i,l)||V(n,l)||V(Ft,l)||V(r.config.globalProperties,l)||(u=o.__cssModules)&&u[l])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:V(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function wn(e){return R(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let Ks=!0;function co(e){const t=Ar(e),s=e.proxy,n=e.ctx;Ks=!1,t.beforeCreate&&Sn(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:u,inject:f,created:a,beforeMount:h,mounted:v,beforeUpdate:S,updated:B,activated:M,deactivated:X,beforeDestroy:z,beforeUnmount:F,destroyed:H,unmounted:I,render:Z,renderTracked:ye,renderTriggered:te,errorCaptured:we,serverPrefetch:at,expose:Ke,inheritAttrs:nt,components:dt,directives:ht,filters:wt}=t;if(f&&uo(f,n,null),o)for(const Y in o){const W=o[Y];D(W)&&(n[Y]=W.bind(s))}if(r){const Y=r.call(s,s);G(Y)&&(e.data=Xs(Y))}if(Ks=!0,i)for(const Y in i){const W=i[Y],Be=D(W)?W.bind(s,s):D(W.get)?W.get.bind(s,s):Ne,pt=!D(W)&&D(W.set)?W.set.bind(s):Ne,Ue=Le({get:Be,set:pt});Object.defineProperty(n,Y,{enumerable:!0,configurable:!0,get:()=>Ue.value,set:b=>Ue.value=b})}if(l)for(const Y in l)Tr(l[Y],n,s,Y);if(u){const Y=D(u)?u.call(s):u;Reflect.ownKeys(Y).forEach(W=>{Vi(W,Y[W])})}a&&Sn(a,e,"c");function ne(Y,W){R(W)?W.forEach(Be=>Y(Be.bind(s))):W&&Y(W.bind(s))}if(ne(Zi,h),ne(Sr,v),ne(Xi,S),ne(eo,B),ne(Ji,M),ne(Qi,X),ne(io,we),ne(ro,ye),ne(no,te),ne(to,F),ne(Cr,I),ne(so,at),R(Ke))if(Ke.length){const Y=e.exposed||(e.exposed={});Ke.forEach(W=>{Object.defineProperty(Y,W,{get:()=>s[W],set:Be=>s[W]=Be,enumerable:!0})})}else e.exposed||(e.exposed={});Z&&e.render===Ne&&(e.render=Z),nt!=null&&(e.inheritAttrs=nt),dt&&(e.components=dt),ht&&(e.directives=ht),at&&xr(e)}function uo(e,t,s=Ne){R(e)&&(e=Bs(e));for(const n in e){const r=e[n];let i;G(r)?"default"in r?i=Xt(r.from||n,r.default,!0):i=Xt(r.from||n):i=Xt(r),ue(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[n]=i}}function Sn(e,t,s){Ae(R(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function Tr(e,t,s,n){let r=n.includes(".")?vr(s,n):()=>s[n];if(ee(e)){const i=t[e];D(i)&&Es(r,i)}else if(D(e))Es(r,e.bind(s));else if(G(e))if(R(e))e.forEach(i=>Tr(i,t,s,n));else{const i=D(e.handler)?e.handler.bind(s):t[e.handler];D(i)&&Es(r,i,e)}}function Ar(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let u;return l?u=l:!r.length&&!s&&!n?u=t:(u={},r.length&&r.forEach(f=>os(u,f,o,!0)),os(u,t,o)),G(t)&&i.set(t,u),u}function os(e,t,s,n=!1){const{mixins:r,extends:i}=t;i&&os(e,i,s,!0),r&&r.forEach(o=>os(e,o,s,!0));for(const o in t)if(!(n&&o==="expose")){const l=fo[o]||s&&s[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const fo={data:Cn,props:Tn,emits:Tn,methods:Et,computed:Et,beforeCreate:fe,created:fe,beforeMount:fe,mounted:fe,beforeUpdate:fe,updated:fe,beforeDestroy:fe,beforeUnmount:fe,destroyed:fe,unmounted:fe,activated:fe,deactivated:fe,errorCaptured:fe,serverPrefetch:fe,components:Et,directives:Et,watch:ho,provide:Cn,inject:ao};function Cn(e,t){return t?e?function(){return oe(D(e)?e.call(this,this):e,D(t)?t.call(this,this):t)}:t:e}function ao(e,t){return Et(Bs(e),Bs(t))}function Bs(e){if(R(e)){const t={};for(let s=0;st==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Se(t)}Modifiers`]||e[`${ft(t)}Modifiers`];function _o(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||Q;let r=s;const i=t.startsWith("update:"),o=i&&mo(n,t.slice(7));o&&(o.trim&&(r=s.map(a=>ee(a)?a.trim():a)),o.number&&(r=r.map(ti)));let l,u=n[l=xs(t)]||n[l=xs(Se(t))];!u&&i&&(u=n[l=xs(ft(t))]),u&&Ae(u,e,6,r);const f=n[l+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ae(f,e,6,r)}}const yo=new WeakMap;function Or(e,t,s=!1){const n=s?yo:t.emitsCache,r=n.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!D(e)){const u=f=>{const a=Or(f,t,!0);a&&(l=!0,oe(o,a))};!s&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!i&&!l?(G(e)&&n.set(e,null),null):(R(i)?i.forEach(u=>o[u]=null):oe(o,i),G(e)&&n.set(e,o),o)}function ys(e,t){return!e||!us(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),V(e,t[0].toLowerCase()+t.slice(1))||V(e,ft(t))||V(e,t))}function An(e){const{type:t,vnode:s,proxy:n,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:u,render:f,renderCache:a,props:h,data:v,setupState:S,ctx:B,inheritAttrs:M}=e,X=rs(e);let z,F;try{if(s.shapeFlag&4){const I=r||n,Z=I;z=Fe(f.call(Z,I,a,h,S,v,B)),F=l}else{const I=t;z=Fe(I.length>1?I(h,{attrs:l,slots:o,emit:u}):I(h,null)),F=t.props?l:vo(l)}}catch(I){ut.length=0,gs(I,e,1),z=xe(Qe)}let H=z;if(F&&M!==!1){const I=Object.keys(F),{shapeFlag:Z}=H;I.length&&Z&7&&(i&&I.some(fs)&&(F=bo(F,i)),H=xt(H,F,!1,!0))}if(s.dirs&&(H=xt(H,null,!1,!0),H.dirs=H.dirs?H.dirs.concat(s.dirs):s.dirs),s.transition){const I=ms(H.type)&&br(H)||H;nn(I,s.transition)}return z=H,rs(X),z}const vo=e=>{let t;for(const s in e)(s==="class"||s==="style"||us(s))&&((t||(t={}))[s]=e[s]);return t},bo=(e,t)=>{const s={};for(const n in e)(!fs(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function xo(e,t,s){const{props:n,children:r,component:i}=e,{props:o,children:l,patchFlag:u}=t,f=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&u>=0){if(u&1024)return!0;if(u&16)return n?En(n,o,f):!!o;if(u&8){const a=t.dynamicProps;for(let h=0;hObject.create($r),Mr=e=>Object.getPrototypeOf(e)===$r;function So(e,t,s,n=!1){const r={},i=Pr();e.propsDefaults=Object.create(null),Rr(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);s?e.props=n?r:Oi(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Co(e,t,s,n){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=U(r),[u]=e.propsOptions;let f=!1;if((n||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let h=0;h{u=!0;const[v,S]=Fr(h,t,!0);oe(o,v),S&&l.push(...S)};!s&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!u)return G(e)&&n.set(e,_t),_t;if(R(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",ln=e=>R(e)?e.map(Fe):[Fe(e)],Ao=(e,t,s)=>{if(t._n)return t;const n=Ui((...r)=>ln(t(...r)),s);return n._c=!1,n},Dr=(e,t,s)=>{const n=e._ctx;for(const r in e){if(on(r))continue;const i=e[r];if(D(i))t[r]=Ao(r,i,n);else if(i!=null){const o=ln(i);t[r]=()=>o}}},jr=(e,t)=>{const s=ln(t);e.slots.default=()=>s},Lr=(e,t,s)=>{for(const n in t)(s||!on(n))&&(e[n]=t[n])},Eo=(e,t,s)=>{const n=e.slots=Pr();if(e.vnode.shapeFlag&32){const r=t._;r?(Lr(n,t,s),s&&zn(n,"_",r,!0)):Dr(t,n)}else t&&jr(e,t)},Oo=(e,t,s)=>{const{vnode:n,slots:r}=e;let i=!0,o=Q;if(n.shapeFlag&32){const l=t._;l?s&&l===1?i=!1:Lr(r,t,s):(i=!t.$stable,Dr(t,r)),o=t}else t&&(jr(e,t),o={default:1});if(i)for(const l in r)!on(l)&&o[l]==null&&delete r[l]},pe=Ro;function Io(e){return $o(e)}function $o(e,t){const s=ds();s.__VUE__=!0;const{insert:n,remove:r,patchProp:i,createElement:o,createText:l,createComment:u,setText:f,setElementText:a,parentNode:h,nextSibling:v,setScopeId:S=Ne,insertStaticContent:B}=e,M=(c,d,p,y=null,_=null,g=null,C=void 0,w=null,x=!!d.dynamicChildren)=>{if(c===d)return;c&&!At(c,d)&&(y=zt(c),b(c,_,g,!0),c=null),d.patchFlag===-2&&(x=!1,d.dynamicChildren=null);const{type:m,ref:$,shapeFlag:T}=d;switch(m){case vs:X(c,d,p,y);break;case Qe:z(c,d,p,y);break;case Ps:c==null&&F(d,p,y,C);break;case de:dt(c,d,p,y,_,g,C,w,x);break;default:T&1?Z(c,d,p,y,_,g,C,w,x):T&6?ht(c,d,p,y,_,g,C,w,x):(T&64||T&128)&&m.process(c,d,p,y,_,g,C,w,x,St)}$!=null&&_?Mt($,c&&c.ref,g,d||c,!d):$==null&&c&&c.ref!=null&&Mt(c.ref,null,g,c,!0)},X=(c,d,p,y)=>{if(c==null)n(d.el=l(d.children),p,y);else{const _=d.el=c.el;d.children!==c.children&&f(_,d.children)}},z=(c,d,p,y)=>{c==null?n(d.el=u(d.children||""),p,y):d.el=c.el},F=(c,d,p,y)=>{[c.el,c.anchor]=B(c.children,d,p,y,c.el,c.anchor)},H=({el:c,anchor:d},p,y)=>{let _;for(;c&&c!==d;)_=v(c),n(c,p,y),c=_;n(d,p,y)},I=({el:c,anchor:d})=>{let p;for(;c&&c!==d;)p=v(c),r(c),c=p;r(d)},Z=(c,d,p,y,_,g,C,w,x)=>{if(d.type==="svg"?C="svg":d.type==="math"&&(C="mathml"),c==null)ye(d,p,y,_,g,C,w,x);else{const m=c.el&&c.el._isVueCE?c.el:null;try{m&&m._beginPatch(),at(c,d,_,g,C,w,x)}finally{m&&m._endPatch()}}},ye=(c,d,p,y,_,g,C,w)=>{let x,m;const{props:$,shapeFlag:T,transition:O,dirs:P}=c;if(x=c.el=o(c.type,g,$&&$.is,$),T&8?a(x,c.children):T&16&&we(c.children,x,null,y,_,$s(c,g),C,w),P&&rt(c,null,y,"created"),te(x,c,c.scopeId,C,y),$){for(const q in $)q!=="value"&&!It(q)&&i(x,q,null,$[q],g,y);"value"in $&&i(x,"value",null,$.value,g),(m=$.onVnodeBeforeMount)&&$e(m,y,c)}P&&rt(c,null,y,"beforeMount");const N=Po(_,O);N&&O.beforeEnter(x),n(x,d,p),((m=$&&$.onVnodeMounted)||N||P)&&pe(()=>{try{m&&$e(m,y,c),N&&O.enter(x),P&&rt(c,null,y,"mounted")}finally{}},_)},te=(c,d,p,y,_)=>{if(p&&S(c,p),y)for(let g=0;g{for(let m=x;m{const w=d.el=c.el;let{patchFlag:x,dynamicChildren:m,dirs:$}=d;x|=c.patchFlag&16;const T=c.props||Q,O=d.props||Q;let P;if(p&&it(p,!1),(P=O.onVnodeBeforeUpdate)&&$e(P,p,d,c),$&&rt(d,c,p,"beforeUpdate"),p&&it(p,!0),m&&(!c.dynamicChildren||c.dynamicChildren.length!==m.length)&&(x=0,C=!1,m=null),(T.innerHTML&&O.innerHTML==null||T.textContent&&O.textContent==null)&&a(w,""),m?Ke(c.dynamicChildren,m,w,p,y,$s(d,_),g):C||W(c,d,w,null,p,y,$s(d,_),g,!1),x>0){if(x&16)nt(w,T,O,p,_);else if(x&2&&T.class!==O.class&&i(w,"class",null,O.class,_),x&4&&i(w,"style",T.style,O.style,_),x&8){const N=d.dynamicProps;for(let q=0;q{P&&$e(P,p,d,c),$&&rt(d,c,p,"updated")},y)},Ke=(c,d,p,y,_,g,C)=>{for(let w=0;w{if(d!==p){if(d!==Q)for(const g in d)!It(g)&&!(g in p)&&i(c,g,d[g],null,_,y);for(const g in p){if(It(g))continue;const C=p[g],w=d[g];C!==w&&g!=="value"&&i(c,g,w,C,_,y)}"value"in p&&i(c,"value",d.value,p.value,_)}},dt=(c,d,p,y,_,g,C,w,x)=>{const m=d.el=c?c.el:l(""),$=d.anchor=c?c.anchor:l("");let{patchFlag:T,dynamicChildren:O,slotScopeIds:P}=d;P&&(w=w?w.concat(P):P),c==null?(n(m,p,y),n($,p,y),we(d.children||[],p,$,_,g,C,w,x)):T>0&&T&64&&O&&c.dynamicChildren&&c.dynamicChildren.length===O.length?(Ke(c.dynamicChildren,O,p,_,g,C,w),(d.key!=null||_&&d===_.subTree)&&Nr(c,d,!0)):W(c,d,p,$,_,g,C,w,x)},ht=(c,d,p,y,_,g,C,w,x)=>{d.slotScopeIds=w,c==null?d.shapeFlag&512?_.ctx.activate(d,p,y,C,x):wt(d,p,y,_,g,C,x):qt(c,d,x)},wt=(c,d,p,y,_,g,C)=>{const w=c.component=Ho(c,y,_);if(rn(c)&&(w.ctx.renderer=St),Bo(w,!1,C),w.asyncDep){if(_&&_.registerDep(w,ne,C),!c.el){const x=w.subTree=xe(Qe);z(null,x,d,p),c.placeholder=x.el}}else ne(w,c,d,p,_,g,C)},qt=(c,d,p)=>{const y=d.component=c.component;if(xo(c,d,p))if(y.asyncDep&&!y.asyncResolved){Y(y,d,p);return}else y.next=d,y.update();else d.el=c.el,y.vnode=d},ne=(c,d,p,y,_,g,C)=>{const w=()=>{if(c.isMounted){let{next:T,bu:O,u:P,parent:N,vnode:q}=c;{const Oe=Hr(c);if(Oe){T&&(T.el=q.el,Y(c,T,C)),Oe.asyncDep.then(()=>{pe(()=>{c.isUnmounted||m()},_)});return}}let k=T,se;it(c,!1),T?(T.el=q.el,Y(c,T,C)):T=q,O&&ws(O),(se=T.props&&T.props.onVnodeBeforeUpdate)&&$e(se,N,T,q),it(c,!0);const re=An(c),Ee=c.subTree;c.subTree=re,M(Ee,re,h(Ee.el),zt(Ee),c,_,g),T.el=re.el,k===null&&wo(c,re.el),P&&pe(P,_),(se=T.props&&T.props.onVnodeUpdated)&&pe(()=>$e(se,N,T,q),_)}else{let T;const{el:O,props:P}=d,{bm:N,m:q,parent:k,root:se,type:re}=c,Ee=Rt(d);it(c,!1),N&&ws(N),!Ee&&(T=P&&P.onVnodeBeforeMount)&&$e(T,k,d),it(c,!0);{se.ce&&se.ce._hasShadowRoot()&&se.ce._injectChildStyle(re,c.parent?c.parent.type:void 0);const Oe=c.subTree=An(c);M(null,Oe,p,y,c,_,g),d.el=Oe.el}if(q&&pe(q,_),!Ee&&(T=P&&P.onVnodeMounted)){const Oe=d;pe(()=>$e(T,k,Oe),_)}(d.shapeFlag&256||k&&Rt(k.vnode)&&k.vnode.shapeFlag&256)&&c.a&&pe(c.a,_),c.isMounted=!0,d=p=y=null}};c.scope.on();const x=c.effect=new Zn(w);c.scope.off();const m=c.update=x.run.bind(x),$=c.job=x.runIfDirty.bind(x);$.i=c,$.id=c.uid,x.scheduler=()=>sn($),it(c,!0),m()},Y=(c,d,p)=>{d.component=c;const y=c.vnode.props;c.vnode=d,c.next=null,Co(c,d.props,y,p),Oo(c,d.children,p),qe(),vn(c),ze()},W=(c,d,p,y,_,g,C,w,x=!1)=>{const m=c&&c.children,$=c?c.shapeFlag:0,T=d.children,{patchFlag:O,shapeFlag:P}=d;if(O>0){if(O&128){pt(m,T,p,y,_,g,C,w,x);return}else if(O&256){Be(m,T,p,y,_,g,C,w,x);return}}P&8?($&16&&Ze(m,_,g),T!==m&&a(p,T)):$&16?P&16?pt(m,T,p,y,_,g,C,w,x):Ze(m,_,g,!0):($&8&&a(p,""),P&16&&we(T,p,y,_,g,C,w,x))},Be=(c,d,p,y,_,g,C,w,x)=>{c=c||_t,d=d||_t;const m=c.length,$=d.length,T=Math.min(m,$);let O;for(O=0;O$?Ze(c,_,g,!0,!1,T):we(d,p,y,_,g,C,w,x,T)},pt=(c,d,p,y,_,g,C,w,x)=>{let m=0;const $=d.length;let T=c.length-1,O=$-1;for(;m<=T&&m<=O;){const P=c[m],N=d[m]=x?ke(d[m]):Fe(d[m]);if(At(P,N))M(P,N,p,null,_,g,C,w,x);else break;m++}for(;m<=T&&m<=O;){const P=c[T],N=d[O]=x?ke(d[O]):Fe(d[O]);if(At(P,N))M(P,N,p,null,_,g,C,w,x);else break;T--,O--}if(m>T){if(m<=O){const P=O+1,N=P<$?d[P].el:y;for(;m<=O;)M(null,d[m]=x?ke(d[m]):Fe(d[m]),p,N,_,g,C,w,x),m++}}else if(m>O)for(;m<=T;)b(c[m],_,g,!0),m++;else{const P=m,N=m,q=new Map;for(m=N;m<=O;m++){const ge=d[m]=x?ke(d[m]):Fe(d[m]);ge.key!=null&&q.set(ge.key,m)}let k,se=0;const re=O-N+1;let Ee=!1,Oe=0;const Ct=new Array(re);for(m=0;m=re){b(ge,_,g,!0);continue}let Ie;if(ge.key!=null)Ie=q.get(ge.key);else for(k=N;k<=O;k++)if(Ct[k-N]===0&&At(ge,d[k])){Ie=k;break}Ie===void 0?b(ge,_,g,!0):(Ct[Ie-N]=m+1,Ie>=Oe?Oe=Ie:Ee=!0,M(ge,d[Ie],p,null,_,g,C,w,x),se++)}const an=Ee?Mo(Ct):_t;for(k=an.length-1,m=re-1;m>=0;m--){const ge=N+m,Ie=d[ge],dn=d[ge+1],hn=ge+1<$?dn.el||Kr(dn):y;Ct[m]===0?M(null,Ie,p,hn,_,g,C,w,x):Ee&&(k<0||m!==an[k]?Ue(Ie,p,hn,2):k--)}}},Ue=(c,d,p,y,_=null)=>{const{el:g,type:C,transition:w,children:x,shapeFlag:m}=c;if(m&6){Ue(c.component.subTree,d,p,y);return}if(m&128){c.suspense.move(d,p,y);return}if(m&64){C.move(c,d,p,St);return}if(C===de){n(g,d,p);for(let T=0;Tw.enter(g),_));else{const{leave:T,delayLeave:O,afterLeave:P}=w,N=()=>{c.ctx.isUnmounted?r(g):n(g,d,p)},q=()=>{const k=g._isLeaving||!!g[Os];g._isLeaving&&g[Os](!0),w.persisted&&!k?N():T(g,()=>{N(),P&&P()})};O?O(g,N,q):q()}else n(g,d,p)},b=(c,d,p,y=!1,_=!1)=>{const{type:g,props:C,ref:w,children:x,dynamicChildren:m,shapeFlag:$,patchFlag:T,dirs:O,cacheIndex:P,memo:N}=c;if(T===-2&&(_=!1),w!=null&&(qe(),Mt(w,null,p,c,!0),ze()),P!=null&&(d.renderCache[P]=void 0),$&256){d.ctx.deactivate(c);return}const q=$&1&&O,k=!Rt(c);let se;if(k&&(se=C&&C.onVnodeBeforeUnmount)&&$e(se,d,c),$&6)le(c.component,p,y);else{if($&128){c.suspense.unmount(p,y);return}q&&rt(c,null,d,"beforeUnmount"),$&64?c.type.remove(c,d,p,St,y):m&&!m.hasOnce&&(g!==de||T>0&&T&64)?Ze(m,d,p,!1,!0):(g===de&&T&384||!_&&$&16)&&Ze(x,d,p),y&&E(c)}const re=N!=null&&P==null;(k&&(se=C&&C.onVnodeUnmounted)||q||re)&&pe(()=>{se&&$e(se,d,c),q&&rt(c,null,d,"unmounted"),re&&(c.el=null)},p)},E=c=>{const{type:d,el:p,anchor:y,transition:_}=c;if(d===de){j(p,y);return}if(d===Ps){I(c);return}const g=()=>{r(p),_&&!_.persisted&&_.afterLeave&&_.afterLeave()};if(c.shapeFlag&1&&_&&!_.persisted){const{leave:C,delayLeave:w}=_,x=()=>C(p,g);w?w(c.el,g,x):x()}else g()},j=(c,d)=>{let p;for(;c!==d;)p=v(c),r(c),c=p;r(d)},le=(c,d,p)=>{const{bum:y,scope:_,job:g,subTree:C,um:w,m:x,a:m}=c;In(x),In(m),y&&ws(y),_.stop(),g&&(g.flags|=8,b(C,c,d,p)),w&&pe(w,d),pe(()=>{c.isUnmounted=!0},d)},Ze=(c,d,p,y=!1,_=!1,g=0)=>{for(let C=g;C{if(c.shapeFlag&6)return zt(c.component.subTree);if(c.shapeFlag&128)return c.suspense.next();const d=v(c.anchor||c.el),p=d&&d[qi];return p?v(p):d};let bs=!1;const fn=(c,d,p)=>{let y;c==null?d._vnode&&(b(d._vnode,null,null,!0),y=d._vnode.component):M(d._vnode||null,c,d,null,null,null,p),d._vnode=c,bs||(bs=!0,vn(y),gr(),bs=!1)},St={p:M,um:b,m:Ue,r:E,mt:wt,mc:we,pc:W,pbc:Ke,n:zt,o:e};return{render:fn,hydrate:void 0,createApp:go(fn)}}function $s({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function it({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Po(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Nr(e,t,s=!1){const n=e.children,r=t.children;if(R(n)&&R(r))for(let i=0;i>1,e[s[l]]0&&(t[n]=s[i-1]),s[i]=n)}}for(i=s.length,o=s[i-1];i-- >0;)s[i]=o,o=t[o];return s}function Hr(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Hr(t)}function In(e){if(e)for(let t=0;te.__isSuspense;function Ro(e,t){t&&t.pendingBranch?R(e)?t.effects.push(...e):t.effects.push(e):Bi(e)}const de=Symbol.for("v-fgt"),vs=Symbol.for("v-txt"),Qe=Symbol.for("v-cmt"),Ps=Symbol.for("v-stc"),ut=[];let _e=null;function L(e=!1){ut.push(_e=e?null:[])}function Ur(){ut.pop(),_e=ut[ut.length-1]||null}let Ht=1;function $n(e,t=!1){Ht+=e,e<0&&_e&&t&&(_e.hasOnce=!0)}function Vr(e){return e.dynamicChildren=Ht>0?_e||_t:null,Ur(),Ht>0&&_e&&_e.push(e),e}function K(e,t,s,n,r,i){return Vr(A(e,t,s,n,r,i,!0))}function Wr(e,t,s,n,r){return Vr(xe(e,t,s,n,r,!0))}function kr(e){return e?e.__v_isVNode===!0:!1}function At(e,t){return e.type===t.type&&e.key===t.key}const Gr=({key:e})=>e??null,es=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?ee(e)||ue(e)||D(e)?{i:je,r:e,k:t,f:!!s}:e:null);function A(e,t=null,s=null,n=0,r=null,i=e===de?0:1,o=!1,l=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Gr(t),ref:t&&es(t),scopeId:_r,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:n,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:je};return l?(ls(u,s),i&128&&e.normalize(u)):s&&(u.shapeFlag|=ee(s)?8:16),Ht>0&&!o&&_e&&(u.patchFlag>0||i&6)&&u.patchFlag!==32&&_e.push(u),u}const xe=Fo;function Fo(e,t=null,s=null,n=0,r=null,i=!1){if((!e||e===oo)&&(e=Qe),kr(e)){const l=xt(e,t,!0);return s&&ls(l,s),Ht>0&&!i&&_e&&(l.shapeFlag&6?_e[_e.indexOf(e)]=l:_e.push(l)),l.patchFlag=-2,l}if(ko(e)&&(e=e.__vccOpts),t){t=Do(t);let{class:l,style:u}=t;l&&!ee(l)&&(t.class=st(l)),G(u)&&(tn(u)&&!R(u)&&(u=oe({},u)),t.style=qs(u))}const o=ee(e)?1:Br(e)?128:ms(e)?64:G(e)?4:D(e)?2:0;return A(e,t,s,n,r,o,i,!0)}function Do(e){return e?tn(e)||Mr(e)?oe({},e):e:null}function xt(e,t,s=!1,n=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:u}=e,f=t?jo(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:f,key:f&&Gr(f),ref:t&&t.ref?s&&i?R(i)?i.concat(es(t)):[i,es(t)]:es(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==de?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&xt(e.ssContent),ssFallback:e.ssFallback&&xt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return u&&n&&nn(a,u.clone(a)),a}function cn(e=" ",t=0){return xe(vs,null,e,t)}function Re(e="",t=!1){return t?(L(),Wr(Qe,null,e)):xe(Qe,null,e)}function Fe(e){return e==null||typeof e=="boolean"?xe(Qe):R(e)?xe(de,null,e.slice()):kr(e)?ke(e):xe(vs,null,String(e))}function ke(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:xt(e)}function ls(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(R(t))s=16;else if(typeof t=="object")if(n&65){const r=t.default;r&&(r._c&&(r._d=!1),ls(e,r()),r._c&&(r._d=!0));return}else{s=32;const r=t._;!r&&!Mr(t)?t._ctx=je:r===3&&je&&(je.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(D(t)){if(n&65){ls(e,{default:t});return}t={default:t,_ctx:je},s=32}else t=String(t),n&64?(s=16,t=[cn(t)]):s=8;e.children=t,e.shapeFlag|=s}function jo(...e){const t={};for(let s=0;she||je;let cs,Kt;{const e=ds(),t=(s,n)=>{let r;return(r=e[s])||(r=e[s]=[]),r.push(n),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};cs=t("__VUE_INSTANCE_SETTERS__",s=>he=s),Kt=t("__VUE_SSR_SETTERS__",s=>Bt=s)}const kt=e=>{const t=he;return cs(e),e.scope.on(),()=>{e.scope.off(),cs(t)}},Pn=()=>{he&&he.scope.off(),cs(null)};function qr(e){return e.vnode.shapeFlag&4}let Bt=!1;function Bo(e,t=!1,s=!1){t&&Kt(t);const{props:n,children:r}=e.vnode,i=qr(e);So(e,n,i,t),Eo(e,r,s||t);const o=i?Uo(e,t):void 0;return t&&Kt(!1),o}function Uo(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,lo);const{setup:n}=s;if(n){qe();const r=e.setupContext=n.length>1?Wo(e):null,i=kt(e),o=Vt(n,e,0,[e.props,r]),l=Wn(o);if(ze(),i(),(l||e.sp)&&!Rt(e)&&xr(e),l){if(o.then(Pn,Pn),t)return o.then(u=>{Kt(!0);try{Mn(e,u,t)}finally{Kt(!1)}}).catch(u=>{gs(u,e,0)});e.asyncDep=o}else Mn(e,o)}else zr(e)}function Mn(e,t,s){D(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:G(t)&&(e.setupState=dr(t)),zr(e)}function zr(e,t,s){const n=e.type;e.render||(e.render=n.render||Ne);{const r=kt(e);qe();try{co(e)}finally{ze(),r()}}}const Vo={get(e,t){return ce(e,"get",""),e[t]}};function Wo(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,Vo),slots:e.slots,emit:e.emit,expose:t}}function un(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(dr(Ii(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Ft)return Ft[s](e)},has(t,s){return s in t||s in Ft}})):e.proxy}function ko(e){return D(e)&&"__vccOpts"in e}const Le=(e,t)=>Di(e,t,Bt),Go="3.5.42";/** -* @vue/runtime-dom v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/let Vs;const Rn=typeof window<"u"&&window.trustedTypes;if(Rn)try{Vs=Rn.createPolicy("vue",{createHTML:e=>e})}catch{}const Jr=Vs?e=>Vs.createHTML(e):e=>e,qo="http://www.w3.org/2000/svg",zo="http://www.w3.org/1998/Math/MathML",We=typeof document<"u"?document:null,Fn=We&&We.createElement("template"),Jo={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const r=t==="svg"?We.createElementNS(qo,e):t==="mathml"?We.createElementNS(zo,e):s?We.createElement(e,{is:s}):We.createElement(e);return e==="select"&&n&&n.multiple!=null&&r.setAttribute("multiple",n.multiple),r},createText:e=>We.createTextNode(e),createComment:e=>We.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>We.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,r,i){const o=s?s.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),s),!(r===i||!(r=r.nextSibling)););else{Fn.innerHTML=Jr(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const l=Fn.content;if(n==="svg"||n==="mathml"){const u=l.firstChild;for(;u.firstChild;)l.appendChild(u.firstChild);l.removeChild(u)}t.insertBefore(l,s)}return[o?o.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Qo=Symbol("_vtc");function Yo(e,t,s){const n=e[Qo];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const Dn=Symbol("_vod"),Zo=Symbol("_vsh"),Xo=Symbol(""),el=/(?:^|;)\s*display\s*:/;function tl(e,t,s){const n=e.style,r=ee(s);let i=!1;if(s&&!r){if(t)if(ee(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();s[l]==null&&Ot(n,l,"")}else for(const o in t)s[o]==null&&Ot(n,o,"");for(const o in s){o==="display"&&(i=!0);const l=s[o];l!=null?nl(e,o,!ee(t)&&t?t[o]:void 0,l)||Ot(n,o,l):Ot(n,o,"")}}else if(r){if(t!==s){const o=n[Xo];o&&(s+=";"+o),n.cssText=s,i=el.test(s)}}else t&&e.removeAttribute("style");Dn in e&&(e[Dn]=i?n.display:"",e[Zo]&&(n.display="none"))}const Zt=/\s*!important$/;function Ot(e,t,s){if(R(s))s.forEach(n=>Ot(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))Zt.test(s)?e.setProperty(t,s.replace(Zt,""),"important"):e.setProperty(t,s);else{const n=sl(e,t);Zt.test(s)?e.setProperty(ft(n),s.replace(Zt,""),"important"):e[n]=s}}const jn=["Webkit","Moz","ms"],Ms={};function sl(e,t){const s=Ms[t];if(s)return s;let n=Se(t);if(n!=="filter"&&n in e)return Ms[t]=n;n=qn(n);for(let r=0;rRs||(fl.then(()=>Rs=0),Rs=Date.now());function dl(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;const r=s.value;if(R(r)){const i=n.stopImmediatePropagation;n.stopImmediatePropagation=()=>{i.call(n),n._stopped=!0};const o=r.slice(),l=[n];for(let u=0;ue.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,hl=(e,t,s,n,r,i)=>{const o=r==="svg";t==="class"?Yo(e,n,o):t==="style"?tl(e,s,n):us(t)?fs(t)||ol(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):pl(e,t,n,o))?(Hn(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Nn(e,t,n,o,i,t!=="value")):e._isVueCE&&(gl(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!ee(n)))?Hn(e,Se(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Nn(e,t,n,o))};function pl(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Bn(t)&&D(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Bn(t)&&ee(s)?!1:t in e}function gl(e,t){const s=e._def.props;if(!s)return!1;const n=Se(t);return Array.isArray(s)?s.some(r=>Se(r)===n):Object.keys(s).some(r=>Se(r)===n)}const ml=["ctrl","shift","alt","meta"],_l={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>ml.some(s=>e[`${s}Key`]&&!t.includes(s))},Qr=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=((r,...i)=>{for(let o=0;o{const t=vl().createApp(...e),{mount:s}=t;return t.mount=n=>{const r=wl(n);if(!r)return;const i=t._component;!D(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=s(r,!1,xl(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t});function xl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function wl(e){return ee(e)?document.querySelector(e):e}const Sl={class:"panel app-list"},Cl={class:"panel-header"},Tl={class:"toolbar-row"},Al=["value"],El=["value"],Ol=["disabled"],Il={class:"panel-body"},$l={key:0,class:"empty-state"},Pl=["checked","onChange"],Ml=["src"],Rl={class:"app-meta"},Fl={class:"app-name"},Dl={key:0,class:"source-badge"},jl={class:"app-path muted"},Ll=["value","onChange"],Nl=["value"],Hl=Wt({__name:"AppList",props:{apps:{},categories:{},selectedCategoryId:{},selectedAppIds:{},searchQuery:{}},emits:["update:searchQuery","toggleSelect","assignCategory","addSelectedToGroup"],setup(e,{emit:t}){const s=e,n=t,r=me("all");function i(a){return a.source==="ztools"||a.source==="plugin"}const o=Le(()=>{const a=s.searchQuery.trim().toLowerCase();return s.apps.filter(h=>!(h.categoryId===s.selectedCategoryId)||r.value==="system"&&i(h)||r.value==="command"&&!i(h)?!1:a?[h.name,h.path,h.pluginTitle,h.pluginName].filter(Boolean).join(" ").toLowerCase().includes(a):!0)}),l=Le(()=>new Set(s.selectedAppIds));function u(a,h){const v=h.target.value;n("assignCategory",a,v===""?null:v)}function f(a){r.value=a.target.value}return(a,h)=>(L(),K("section",Sl,[A("div",Cl,[h[4]||(h[4]=A("span",null,"应用",-1)),A("div",Tl,[A("select",{class:"source-filter",value:r.value,"aria-label":"按来源筛选",onChange:f},[...h[3]||(h[3]=[A("option",{value:"all"},"全部",-1),A("option",{value:"system"},"系统应用",-1),A("option",{value:"command"},"指令",-1)])],40,Al),A("input",{class:"search-input",type:"search",placeholder:"搜索名称或路径",value:e.searchQuery,onInput:h[0]||(h[0]=v=>n("update:searchQuery",v.target.value))},null,40,El),A("button",{class:"btn btn-sm btn-primary",type:"button",disabled:e.selectedAppIds.length===0,onClick:h[1]||(h[1]=v=>n("addSelectedToGroup"))}," 加入当前组 ",8,Ol)])]),A("div",Il,[o.value.length===0?(L(),K("div",$l,[...h[5]||(h[5]=[A("div",null,"当前筛选条件下没有应用",-1)])])):Re("",!0),(L(!0),K(de,null,Nt(o.value,v=>(L(),K("div",{key:v._id,class:st(["list-item app-row",{active:l.value.has(v._id)}])},[A("input",{type:"checkbox",checked:l.value.has(v._id),onChange:S=>n("toggleSelect",v._id)},null,40,Pl),v.icon?(L(),K("img",{key:0,class:"app-icon",src:v.icon,alt:""},null,8,Ml)):Re("",!0),A("div",Rl,[A("div",Fl,[cn(ve(v.name)+" ",1),i(v)?(L(),K("span",Dl,"指令")):Re("",!0)]),A("div",jl,ve(i(v)&&(v.pluginTitle||v.pluginName)||v.path),1)]),A("select",{class:"cat-select",value:v.categoryId??"",onChange:S=>u(v._id,S),onClick:h[2]||(h[2]=Qr(()=>{},["stop"]))},[h[6]||(h[6]=A("option",{value:""},"未分类",-1)),(L(!0),K(de,null,Nt(e.categories,S=>(L(),K("option",{key:S._id,value:S._id},ve(S.name),9,Nl))),128))],40,Ll)],2))),128))])]))}}),Gt=(e,t)=>{const s=e.__vccOpts||e;for(const[n,r]of t)s[n]=r;return s},Kl=Gt(Hl,[["__scopeId","data-v-93525532"]]),Bl={class:"panel category-sidebar"},Ul={class:"panel-header"},Vl={class:"panel-body"},Wl=["onClick"],kl={class:"cat-name"},Gl=["onClick"],ql=["onClick"],zl=Wt({__name:"CategorySidebar",props:{categories:{},selectedCategoryId:{}},emits:["select","create","rename","delete"],setup(e,{emit:t}){const s=t;return(n,r)=>(L(),K("aside",Bl,[A("div",Ul,[r[3]||(r[3]=A("span",null,"分类",-1)),A("button",{class:"btn btn-sm",type:"button",onClick:r[0]||(r[0]=i=>s("create"))},"新建")]),A("div",Vl,[A("div",{class:st(["list-item",{active:e.selectedCategoryId===null}]),onClick:r[1]||(r[1]=i=>s("select",null))},[...r[4]||(r[4]=[A("span",null,"未分类",-1)])],2),(L(!0),K(de,null,Nt(e.categories,i=>(L(),K("div",{key:i._id,class:st(["list-item",{active:e.selectedCategoryId===i._id}]),onClick:o=>s("select",i._id)},[A("span",kl,ve(i.name),1),A("span",{class:"cat-actions",onClick:r[2]||(r[2]=Qr(()=>{},["stop"]))},[A("button",{class:"btn btn-sm",type:"button",onClick:o=>s("rename",i._id)},"重命名",8,Gl),A("button",{class:"btn btn-sm btn-danger",type:"button",onClick:o=>s("delete",i._id)}," 删除 ",8,ql)])],10,Wl))),128))])]))}}),Jl=Gt(zl,[["__scopeId","data-v-a0ce77b5"]]),Ql={class:"panel group-panel"},Yl={class:"panel-header"},Zl=["disabled"],Xl={class:"group-list"},ec={key:0,class:"empty-hint muted"},tc=["onClick"],sc={class:"group-name"},nc={key:0,class:"badge-unsynced",title:"Feature 未同步"},rc={class:"editor"},ic={key:0,class:"empty-state"},oc={key:0,class:"sync-row"},lc=["disabled"],cc={key:1,class:"error-banner"},uc={class:"field"},fc=["value"],ac={class:"field"},dc=["value"],hc={class:"field"},pc={key:0,class:"muted"},gc={key:1,class:"member-list"},mc={class:"member-meta"},_c={key:0,class:"source-badge"},yc={class:"muted member-path"},vc=["onClick"],bc={class:"editor-actions"},xc=["disabled"],wc=["disabled"],Sc=Wt({__name:"GroupPanel",props:{groups:{},apps:{},selectedGroupId:{},draft:{},saveError:{},busy:{type:Boolean}},emits:["select","create","update:draft","save","delete","retrySync","removeMember"],setup(e,{emit:t}){const s=e,n=t,r=Le(()=>new Map(s.apps.map(u=>[u._id,u]))),i=Le(()=>s.draft?s.draft.appIds.map(u=>{const f=r.value.get(u);return{id:u,name:(f==null?void 0:f.name)??u,path:(f==null?void 0:f.source)==="ztools"||(f==null?void 0:f.source)==="plugin"?f.pluginTitle||f.pluginName||f.path:(f==null?void 0:f.path)??"",isZtools:(f==null?void 0:f.source)==="ztools"||(f==null?void 0:f.source)==="plugin",missing:!f}}):[]),o=Le(()=>{var u;return((u=s.draft)==null?void 0:u.id)!=null&&s.draft.featureSynced===!1});function l(u){s.draft&&n("update:draft",{...s.draft,...u})}return(u,f)=>(L(),K("aside",Ql,[A("div",Yl,[f[7]||(f[7]=A("span",null,"启动组",-1)),A("button",{class:"btn btn-sm",type:"button",disabled:e.busy,onClick:f[0]||(f[0]=a=>n("create"))}," 新建 ",8,Zl)]),A("div",Xl,[e.groups.length===0?(L(),K("div",ec,"暂无启动组")):Re("",!0),(L(!0),K(de,null,Nt(e.groups,a=>(L(),K("div",{key:a._id,class:st(["list-item",{active:e.selectedGroupId===a._id}]),onClick:h=>n("select",a._id)},[A("span",sc,ve(a.name),1),a.featureSynced?Re("",!0):(L(),K("span",nc,"未同步"))],10,tc))),128))]),A("div",rc,[e.draft?(L(),K(de,{key:1},[o.value?(L(),K("div",oc,[f[9]||(f[9]=A("span",{class:"badge-unsynced"},"未同步",-1)),A("button",{class:"btn btn-sm",type:"button",disabled:e.busy,onClick:f[2]||(f[2]=a=>n("retrySync"))}," 重试同步 ",8,lc)])):Re("",!0),e.saveError?(L(),K("div",cc,ve(e.saveError),1)):Re("",!0),A("div",uc,[f[10]||(f[10]=A("label",null,"名称",-1)),A("input",{type:"text",value:e.draft.name,placeholder:"启动组名称",onInput:f[3]||(f[3]=a=>l({name:a.target.value}))},null,40,fc)]),A("div",ac,[f[11]||(f[11]=A("label",null,"指令(逗号分隔)",-1)),A("input",{type:"text",value:e.draft.cmdsText,placeholder:"默认使用名称",onInput:f[4]||(f[4]=a=>l({cmdsText:a.target.value}))},null,40,dc)]),A("div",hc,[A("label",null,"成员("+ve(i.value.length)+")",1),i.value.length===0?(L(),K("div",pc,"从中间应用列表勾选后点「加入当前组」")):(L(),K("div",gc,[(L(!0),K(de,null,Nt(i.value,a=>(L(),K("div",{key:a.id,class:"member-row"},[A("div",mc,[A("div",{class:st({missing:a.missing})},[cn(ve(a.name)+" ",1),a.isZtools?(L(),K("span",_c,"指令")):Re("",!0)],2),A("div",yc,ve(a.path||a.id),1)]),A("button",{class:"btn btn-sm",type:"button",onClick:h=>n("removeMember",a.id)}," 移除 ",8,vc)]))),128))]))]),A("div",bc,[A("button",{class:"btn btn-primary",type:"button",disabled:e.busy,onClick:f[5]||(f[5]=a=>n("save"))}," 保存 ",8,xc),e.draft.id?(L(),K("button",{key:0,class:"btn btn-danger",type:"button",disabled:e.busy,onClick:f[6]||(f[6]=a=>n("delete"))}," 删除 ",8,wc)):Re("",!0)])],64)):(L(),K("div",ic,[f[8]||(f[8]=A("div",null,"选择或创建一个启动组进行编辑",-1)),A("button",{class:"btn btn-primary",type:"button",onClick:f[1]||(f[1]=a=>n("create"))},"创建启动组")]))])]))}}),Cc=Gt(Sc,[["__scopeId","data-v-981ef1d9"]]),Tc={class:"top-bar"},Ac={class:"toolbar-row"},Ec=["disabled"],Oc=["disabled"],Ic=["disabled"],$c={class:"top-bar__meta muted"},Pc=Wt({__name:"TopBar",props:{lastScanAt:{},canTrialRun:{type:Boolean},busy:{type:Boolean}},emits:["scan","addDir","trialRun"],setup(e,{emit:t}){const s=t;function n(r){if(r==null)return"尚未扫描";try{return new Date(r).toLocaleString()}catch{return"尚未扫描"}}return(r,i)=>(L(),K("header",Tc,[i[3]||(i[3]=A("div",{class:"top-bar__title"},"批量启动",-1)),A("div",Ac,[A("button",{class:"btn btn-primary",type:"button",disabled:e.busy,onClick:i[0]||(i[0]=o=>s("scan"))}," 扫描 ",8,Ec),A("button",{class:"btn",type:"button",disabled:e.busy,onClick:i[1]||(i[1]=o=>s("addDir"))}," 添加目录 ",8,Oc),A("button",{class:"btn",type:"button",disabled:e.busy||!e.canTrialRun,title:"试跑当前选中的启动组",onClick:i[2]||(i[2]=o=>s("trialRun"))}," 试跑当前组 ",8,Ic)]),A("div",$c,"上次扫描:"+ve(n(e.lastScanAt)),1)]))}}),Mc=Gt(Pc,[["__scopeId","data-v-396c1c2f"]]),Rc={class:"app-shell"},Fc={key:0,class:"shell-banner error-banner"},Dc={key:1,class:"empty-state empty-page"},jc=["disabled"],Lc={key:2,class:"main-layout"},Nc={class:"right-col"},Hc={key:0,class:"panel empty-groups"},Kc=Wt({__name:"App",setup(e){const t=me([]),s=me([]),n=me([]),r=me(null),i=me(null),o=me(null),l=me([]),u=me(""),f=me(null),a=me(null),h=me(!1),v=me(null),S=()=>window.batchStart,B=Le(()=>{var b;return((b=r.value)==null?void 0:b.lastScanAt)??null}),M=Le(()=>t.value.length>0),X=Le(()=>n.value.length>0),z=Le(()=>{var b;return o.value!=null||!!((b=f.value)!=null&&b.id)});async function F(){v.value=null;try{if(!window.batchStart){v.value="preload 未注入 window.batchStart,请确认安装的是 src-ztools 目录且已重新构建";return}const[b,E,j,le]=await Promise.all([S().listApps(),S().listCategories(),S().listGroups(),S().getSettings()]);t.value=b,s.value=E,n.value=j,r.value=le}catch(b){v.value=b instanceof Error?b.message:String(b)}}function H(b){f.value={id:b._id,name:b.name,cmdsText:b.cmds.join(", "),appIds:[...b.appIds],order:b.order,featureSynced:b.featureSynced},o.value=b._id,a.value=null}function I(b){const E=n.value.find(j=>j._id===b);E&&H(E)}function Z(){o.value=null,f.value={name:"",cmdsText:"",appIds:[],order:n.value.length},a.value=null}function ye(b,E){const j=E.split(/[,,]/).map(le=>le.trim()).filter(Boolean);if(j.length===0){const le=b.trim();return le?[le]:[]}return j}async function te(b){if(!h.value){h.value=!0;try{return await b()}finally{h.value=!1}}}async function we(){await te(async()=>{var le;await S().runScan(),await F();const b=t.value.filter(Ze=>Ze.source==="ztools"||Ze.source==="plugin").length,E=b>0?`扫描完成:共 ${t.value.length} 个应用(含 ${b} 条 ZTools 指令)`:`扫描完成:共 ${t.value.length} 个应用`,j=(le=window.ztools)==null?void 0:le.showToast;typeof j=="function"&&Promise.resolve(j(E))})}async function at(){await te(async()=>{const b=await S().addCustomScanDir();b&&(r.value=b)})}function Ke(b){var le;const E=`试跑完成:成功 ${b.success} / 失败 ${b.failed}`,j=(le=window.ztools)==null?void 0:le.showToast;if(typeof j=="function"){Promise.resolve(j(E));return}window.alert(E)}async function nt(){var E;const b=((E=f.value)==null?void 0:E.id)??o.value;b&&await te(async()=>{const j=await S().launchGroup(b);Ke(j)})}async function dt(){const b=window.prompt("新分类名称");b!=null&&b.trim()&&await te(async()=>{await S().createCategory(b.trim()),await F()})}async function ht(b){const E=s.value.find(le=>le._id===b),j=window.prompt("重命名分类",(E==null?void 0:E.name)??"");j!=null&&j.trim()&&await te(async()=>{await S().renameCategory(b,j.trim()),await F()})}async function wt(b){window.confirm("删除该分类?其下应用将回到未分类。")&&await te(async()=>{await S().deleteCategory(b),i.value===b&&(i.value=null),await F()})}function qt(b){const E=new Set(l.value);E.has(b)?E.delete(b):E.add(b),l.value=[...E]}async function ne(b,E){await te(async()=>{await S().assignCategory(b,E),await F()})}function Y(){f.value||Z();const b=f.value,E=new Set(b.appIds);for(const j of l.value)E.add(j);f.value={...b,appIds:[...E]}}function W(b){f.value&&(f.value={...f.value,appIds:f.value.appIds.filter(E=>E!==b)})}async function Be(){if(!f.value)return;a.value=null;const b=f.value.name.trim(),E=ye(b,f.value.cmdsText);await te(async()=>{try{const j=await S().saveGroup({name:b,cmds:E,appIds:[...f.value.appIds],order:f.value.order},f.value.id);await F(),H(j)}catch(j){a.value=j instanceof Error?j.message:String(j)}})}async function pt(){var E;if(!((E=f.value)!=null&&E.id)||!window.confirm(`删除启动组「${f.value.name}」?`))return;const b=f.value.id;await te(async()=>{await S().deleteGroup(b),f.value=null,o.value=null,a.value=null,await F()})}async function Ue(){var b;(b=f.value)!=null&&b.id&&await te(async()=>{const E=await S().retrySyncGroup(f.value.id);await F(),E&&H(E)})}return Sr(async()=>{var b,E;try{(E=(b=window.ztools)==null?void 0:b.setExpendHeight)==null||E.call(b,560)}catch{}await F()}),(b,E)=>(L(),K("div",Rc,[xe(Mc,{"last-scan-at":B.value,"can-trial-run":z.value,busy:h.value,onScan:we,onAddDir:at,onTrialRun:nt},null,8,["last-scan-at","can-trial-run","busy"]),v.value?(L(),K("div",Fc,"加载失败:"+ve(v.value),1)):Re("",!0),M.value?(L(),K("div",Lc,[xe(Jl,{categories:s.value,"selected-category-id":i.value,onSelect:E[0]||(E[0]=j=>i.value=j),onCreate:dt,onRename:ht,onDelete:wt},null,8,["categories","selected-category-id"]),xe(Kl,{apps:t.value,categories:s.value,"selected-category-id":i.value,"selected-app-ids":l.value,"search-query":u.value,"onUpdate:searchQuery":E[1]||(E[1]=j=>u.value=j),onToggleSelect:qt,onAssignCategory:ne,onAddSelectedToGroup:Y},null,8,["apps","categories","selected-category-id","selected-app-ids","search-query"]),A("div",Nc,[!X.value&&!f.value?(L(),K("div",Hc,[A("div",{class:"empty-state"},[E[4]||(E[4]=A("div",null,"还没有启动组",-1)),A("button",{class:"btn btn-primary",type:"button",onClick:Z},"创建启动组")])])):(L(),Wr(Cc,{key:1,groups:n.value,apps:t.value,"selected-group-id":o.value,draft:f.value,"save-error":a.value,busy:h.value,onSelect:I,onCreate:Z,"onUpdate:draft":E[2]||(E[2]=j=>f.value=j),onSave:Be,onDelete:pt,onRetrySync:Ue,onRemoveMember:W},null,8,["groups","apps","selected-group-id","draft","save-error","busy"]))])])):(L(),K("div",Dc,[E[3]||(E[3]=A("div",null,"还没有应用,先扫描一次吧",-1)),A("button",{class:"btn btn-primary",type:"button",disabled:h.value,onClick:we}," 扫描应用 ",8,jc)]))]))}}),Bc=Gt(Kc,[["__scopeId","data-v-856e7a20"]]);bl(Bc).mount("#app"); diff --git a/plugins/batch-start/dist/assets/index-DdXE4yVZ.css b/plugins/batch-start/dist/assets/index-DdXE4yVZ.css deleted file mode 100644 index 4e0043427..000000000 --- a/plugins/batch-start/dist/assets/index-DdXE4yVZ.css +++ /dev/null @@ -1 +0,0 @@ -.app-list[data-v-93525532]{flex:1;min-width:0}.toolbar-row[data-v-93525532]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.source-filter[data-v-93525532],.search-input[data-v-93525532],.cat-select[data-v-93525532]{border:1px solid var(--border);border-radius:var(--radius);background:#fff}.source-filter[data-v-93525532]{padding:4px 8px;max-width:110px}.search-input[data-v-93525532]{padding:4px 8px;min-width:160px}.app-row[data-v-93525532]{cursor:default}.app-icon[data-v-93525532]{width:20px;height:20px;object-fit:contain;flex-shrink:0}.app-meta[data-v-93525532]{flex:1;min-width:0}.app-name[data-v-93525532]{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:6px}.source-badge[data-v-93525532]{flex-shrink:0;font-size:10px;font-weight:600;line-height:1;padding:2px 5px;border-radius:4px;color:#1d4ed8;background:#dbeafe}.app-path[data-v-93525532]{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cat-select[data-v-93525532]{max-width:120px;padding:3px 6px}.category-sidebar[data-v-a0ce77b5]{width:220px;flex-shrink:0}.cat-name[data-v-a0ce77b5]{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cat-actions[data-v-a0ce77b5]{display:none;gap:4px}.list-item:hover .cat-actions[data-v-a0ce77b5],.list-item.active .cat-actions[data-v-a0ce77b5]{display:inline-flex}.group-panel[data-v-981ef1d9]{width:300px;flex-shrink:0}.group-list[data-v-981ef1d9]{max-height:160px;overflow:auto;border-bottom:1px solid var(--border);padding:8px}.empty-hint[data-v-981ef1d9]{padding:8px}.group-name[data-v-981ef1d9]{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.editor[data-v-981ef1d9]{flex:1;overflow:auto;padding:12px}.sync-row[data-v-981ef1d9]{display:flex;align-items:center;gap:8px;margin-bottom:10px}.member-list[data-v-981ef1d9]{display:flex;flex-direction:column;gap:6px}.member-row[data-v-981ef1d9]{display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid var(--border);border-radius:var(--radius)}.member-meta[data-v-981ef1d9]{flex:1;min-width:0}.member-path[data-v-981ef1d9]{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-badge[data-v-981ef1d9]{margin-left:6px;font-size:10px;font-weight:600;padding:2px 5px;border-radius:4px;color:#1d4ed8;background:#dbeafe}.missing[data-v-981ef1d9]{color:var(--danger)}.editor-actions[data-v-981ef1d9]{display:flex;gap:8px;margin-top:12px}.top-bar[data-v-396c1c2f]{display:flex;align-items:center;gap:16px;padding:10px 14px;background:var(--surface);border-bottom:1px solid var(--border)}.top-bar__title[data-v-396c1c2f]{font-weight:700;font-size:15px;white-space:nowrap}.top-bar__meta[data-v-396c1c2f]{margin-left:auto;white-space:nowrap}.app-shell[data-v-856e7a20]{display:flex;flex-direction:column;height:100%;min-height:0}.shell-banner[data-v-856e7a20]{margin:8px 12px 0}.empty-page[data-v-856e7a20]{flex:1}.main-layout[data-v-856e7a20]{display:flex;gap:10px;padding:10px;flex:1;min-height:0}.right-col[data-v-856e7a20]{width:300px;flex-shrink:0;display:flex;min-height:0}.right-col>.panel[data-v-856e7a20],.right-col[data-v-856e7a20]>.group-panel{flex:1;width:100%}.empty-groups[data-v-856e7a20]{width:100%}:root{--bg: #f5f6f8;--surface: #ffffff;--border: #e2e5ea;--text: #1f2937;--muted: #6b7280;--accent: #2563eb;--accent-hover: #1d4ed8;--danger: #dc2626;--danger-bg: #fef2f2;--selected: #eff6ff;--radius: 6px;--font: "Segoe UI", system-ui, -apple-system, sans-serif}*,*:before,*:after{box-sizing:border-box}html,body,#app{margin:0;height:100%;font-family:var(--font);font-size:13px;color:var(--text);background:var(--bg)}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.5}.btn{border:1px solid var(--border);background:var(--surface);border-radius:var(--radius);padding:6px 12px;line-height:1.3}.btn:hover:not(:disabled){background:#f3f4f6}.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){background:var(--accent-hover);border-color:var(--accent-hover)}.btn-danger{color:var(--danger);border-color:#fecaca;background:var(--danger-bg)}.btn-sm{padding:3px 8px;font-size:12px}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);display:flex;flex-direction:column;min-height:0;overflow:hidden}.panel-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);font-weight:600}.panel-body{flex:1;overflow:auto;padding:8px}.field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}.field label{color:var(--muted);font-size:12px}.field input,.field select,.field textarea{border:1px solid var(--border);border-radius:var(--radius);padding:6px 8px;background:#fff}.field textarea{min-height:64px;resize:vertical}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 16px;color:var(--muted);text-align:center}.list-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--radius);cursor:pointer;border:1px solid transparent}.list-item:hover{background:#f9fafb}.list-item.active{background:var(--selected);border-color:#bfdbfe}.muted{color:var(--muted)}.error-banner{background:var(--danger-bg);color:var(--danger);border:1px solid #fecaca;border-radius:var(--radius);padding:8px 10px;margin:0 0 8px}.badge-unsynced{display:inline-flex;align-items:center;gap:4px;background:var(--danger-bg);color:var(--danger);border:1px solid #fecaca;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600}.toolbar-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap} diff --git a/plugins/batch-start/docs/screenshots/manage-ui-0.1.3.jpg b/plugins/batch-start/docs/screenshots/manage-ui-0.1.3.jpg new file mode 100644 index 000000000..e770104e8 Binary files /dev/null and b/plugins/batch-start/docs/screenshots/manage-ui-0.1.3.jpg differ diff --git a/plugins/batch-start/docs/superpowers/plans/2026-09-14-ztools-batch-start.md b/plugins/batch-start/docs/superpowers/plans/2026-09-14-ztools-batch-start.md new file mode 100644 index 000000000..04fe04462 --- /dev/null +++ b/plugins/batch-start/docs/superpowers/plans/2026-09-14-ztools-batch-start.md @@ -0,0 +1,779 @@ +# ZTools Batch Start Plugin Implementation Plan + +> **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. + +**Goal:** Build a Vue 3 ZTools plugin that scans apps (system + custom dirs), supports manual categories, and registers launch groups as dynamic ZTools Features for near-simultaneous batch start. + +**Architecture:** Preload hosts pure services (AppLibrary, Category, LaunchGroup, FeatureSync, Launcher, platform Scanners). Vue UI manages data via exposed preload APIs. Static Feature `manage` opens the UI; dynamic Features `group:` launch a group then `outPlugin`. Persistence uses `ztools.db`. + +**Tech Stack:** Vue 3, TypeScript, Vite, Vitest, `@ztools-center/plugin-cli` conventions, Node `fs`/`path`/`child_process` in preload, `ztools` host APIs. + +**Spec:** `docs/superpowers/specs/2026-09-14-ztools-batch-start-design.md` + +--- + +## File map + +| Path | Responsibility | +|------|----------------| +| `plugin.json` | Plugin id, logo, preload, static `manage` feature, platforms | +| `package.json` | Scripts: `dev`, `build`, `test` | +| `vite.config.ts` | UI + preload build | +| `src/types.ts` | Shared domain types | +| `src/services/db.ts` | Thin `ztools.db.promises` helpers + id factories | +| `src/services/appLibrary.ts` | App CRUD, merge-on-scan, assign category | +| `src/services/category.ts` | Category CRUD; delete clears app.categoryId | +| `src/services/scanner/types.ts` | `ScannedApp`, `PlatformScanner` | +| `src/services/scanner/win.ts` | Start Menu `.lnk` + custom dirs `.exe/.bat/.cmd/.lnk` | +| `src/services/scanner/mac.ts` | `/Applications`, `~/Applications` `.app` + custom | +| `src/services/scanner/linux.ts` | `.desktop` + custom executables | +| `src/services/scanner/index.ts` | Pick scanner by `process.platform`, run + merge | +| `src/services/launcher.ts` | Parallel `shellOpenPath`, success/fail summary | +| `src/services/launchGroup.ts` | Group CRUD + validation | +| `src/services/featureSync.ts` | `setFeature` / `removeFeature` / reconcile | +| `src/services/enterRouter.ts` | Map `onPluginEnter` code → manage vs launch | +| `src/preload.ts` | Wire services, expose API, enter routing | +| `src/main.ts` / `src/App.vue` | Mount UI | +| `src/components/TopBar.vue` | Scan, add dir, trial launch | +| `src/components/CategorySidebar.vue` | Categories + 未分类 | +| `src/components/AppList.vue` | Search, select, change category | +| `src/components/GroupPanel.vue` | Groups edit + sync status | +| `tests/**/*.test.ts` | Unit tests with mocked `ztools` / fs | + +--- + +### Task 1: Scaffold project in current repo + +**Files:** +- Create: `package.json`, `plugin.json`, `vite.config.ts`, `tsconfig.json`, `tsconfig.node.json`, `index.html`, `src/main.ts`, `src/App.vue`, `src/env.d.ts`, `public/logo.png`, `vitest.config.ts`, `.gitignore` +- Keep: existing `docs/` and git remote `origin` + +- [ ] **Step 1: Create `package.json`** + +```json +{ + "name": "ztools-batch-start", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3", + "vitest": "^2.1.8", + "vue-tsc": "^2.1.10" + } +} +``` + +- [ ] **Step 2: Create `plugin.json`** + +```json +{ + "name": "batch-start", + "title": "批量启动", + "description": "扫描应用、手动分类,并将启动组注册为 ZTools 指令一键批量启动", + "version": "0.1.0", + "platform": ["win32", "darwin", "linux"], + "main": "index.html", + "logo": "logo.png", + "preload": "preload.js", + "features": [ + { + "code": "manage", + "explain": "管理应用分类与启动组", + "cmds": ["批量启动", "应用启动组"] + } + ], + "development": { + "main": "http://localhost:5173" + } +} +``` + +- [ ] **Step 3: Create Vite + TS + Vitest config and minimal Vue entry** + +`vite.config.ts` must build UI to `dist/` and compile `src/preload.ts` to `dist/preload.js`. Copy `plugin.json` + `logo.png` into `dist` on build so `dist/` is the installable plugin app folder. + +Minimal `src/App.vue`: + +```vue + + + +``` + +Use a simple 128×128 PNG as `public/logo.png`. + +`.gitignore`: + +``` +node_modules/ +dist/ +.DS_Store +*.local +.env* +``` + +- [ ] **Step 4: Install and verify** + +Run: `npm install` +Expected: lockfile created, no errors. + +Run: `npm run build` +Expected: exit 0; `dist/` contains `index.html` and `preload.js`. + +- [ ] **Step 5: Commit** + +```bash +git add package.json plugin.json vite.config.ts tsconfig.json tsconfig.node.json index.html src public vitest.config.ts .gitignore package-lock.json +git commit -m "chore: scaffold Vue ZTools batch-start plugin" +``` + +--- + +### Task 2: Domain types + db helpers + +**Files:** +- Create: `src/types.ts`, `src/services/db.ts`, `tests/db.test.ts` + +- [ ] **Step 1: Write `src/types.ts`** + +```typescript +export type Platform = 'win32' | 'darwin' | 'linux' +export type AppSource = 'scan' | 'manual' + +export interface AppDoc { + _id: string + _rev?: string + name: string + path: string + icon?: string | null + source: AppSource + categoryId: string | null + platform: Platform +} + +export interface CategoryDoc { + _id: string + _rev?: string + name: string + order: number +} + +export interface GroupDoc { + _id: string + _rev?: string + name: string + cmds: string[] + appIds: string[] + order: number + featureSynced: boolean +} + +export interface SettingsDoc { + _id: 'settings' + _rev?: string + customScanDirs: string[] + lastScanAt: number | null +} + +export interface LaunchResult { + success: number + failed: number + errors: Array<{ path: string; error: string }> +} +``` + +- [ ] **Step 2: Write failing test for id helpers** + +`tests/db.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest' +import { makeAppId, makeCategoryId, makeGroupId } from '../src/services/db' + +describe('id factories', () => { + it('prefixes ids', () => { + expect(makeAppId('abc')).toBe('app:abc') + expect(makeCategoryId('abc')).toBe('category:abc') + expect(makeGroupId('abc')).toBe('group:abc') + }) +}) +``` + +- [ ] **Step 3: Run test — expect FAIL** + +Run: `npm test -- tests/db.test.ts` +Expected: FAIL (module not found / export missing). + +- [ ] **Step 4: Implement `src/services/db.ts`** + +```typescript +import type { SettingsDoc } from '../types' + +export function makeAppId(uuid: string): string { + return `app:${uuid}` +} +export function makeCategoryId(uuid: string): string { + return `category:${uuid}` +} +export function makeGroupId(uuid: string): string { + return `group:${uuid}` +} + +export function newId(): string { + return crypto.randomUUID() +} + +export type ZtoolsDb = { + put: (doc: object) => Promise + get: (id: string) => Promise + remove: (docOrId: object | string) => Promise + allDocs: (key?: string) => Promise +} + +export function getDb(): ZtoolsDb { + // @ts-expect-error host injects ztools in plugin runtime + const z = globalThis.ztools ?? (window as any).ztools + return z.db.promises +} + +export async function getSettings(db: ZtoolsDb = getDb()): Promise { + const existing = (await db.get('settings')) as SettingsDoc | null + if (existing) return existing + const doc: SettingsDoc = { + _id: 'settings', + customScanDirs: [], + lastScanAt: null, + } + await db.put(doc) + return (await db.get('settings')) as SettingsDoc +} +``` + +- [ ] **Step 5: Run test — expect PASS** + +Run: `npm test -- tests/db.test.ts` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/types.ts src/services/db.ts tests/db.test.ts +git commit -m "feat: add domain types and db id helpers" +``` + +--- + +### Task 3: Category + AppLibrary services + +**Files:** +- Create: `src/services/category.ts`, `src/services/appLibrary.ts`, `tests/mocks/memoryDb.ts`, `tests/category.test.ts`, `tests/appLibrary.test.ts` + +- [ ] **Step 1: Create in-memory db mock** + +`tests/mocks/memoryDb.ts`: + +```typescript +import type { ZtoolsDb } from '../../src/services/db' + +export function createMemoryDb(): ZtoolsDb { + const map = new Map() + return { + async put(doc: any) { + const prev = map.get(doc._id) + const revNum = prev?._rev ? Number(String(prev._rev).split('-')[1]) + 1 : 1 + const next = { ...doc, _rev: `rev-${revNum}` } + map.set(doc._id, next) + return next + }, + async get(id: string) { + return map.get(id) ?? null + }, + async remove(docOrId: any) { + const id = typeof docOrId === 'string' ? docOrId : docOrId._id + map.delete(id) + return { ok: true, id } + }, + async allDocs(key?: string) { + const all = [...map.values()] + if (!key) return all + return all.filter((d) => String(d._id).startsWith(key)) + }, + } +} +``` + +- [ ] **Step 2: Write failing category tests** + +```typescript +import { describe, it, expect } from 'vitest' +import { createMemoryDb } from './mocks/memoryDb' +import { createCategory, deleteCategory, listCategories } from '../src/services/category' +import { upsertApp, listApps } from '../src/services/appLibrary' +import { makeAppId } from '../src/services/db' + +describe('category', () => { + it('creates and lists categories', async () => { + const db = createMemoryDb() + const c = await createCategory(db, '开发') + expect(c.name).toBe('开发') + expect(c._id.startsWith('category:')).toBe(true) + expect((await listCategories(db)).length).toBe(1) + }) + + it('deleteCategory clears app.categoryId', async () => { + const db = createMemoryDb() + const c = await createCategory(db, '办公') + await upsertApp(db, { + _id: makeAppId('1'), + name: 'A', + path: 'C:/a.exe', + source: 'manual', + categoryId: c._id, + platform: 'win32', + }) + await deleteCategory(db, c._id) + const apps = await listApps(db) + expect(apps[0].categoryId).toBeNull() + }) +}) +``` + +- [ ] **Step 3: Run — expect FAIL** + +Run: `npm test -- tests/category.test.ts` +Expected: FAIL (missing modules). + +- [ ] **Step 4: Implement `category.ts` and `appLibrary.ts`** + +`src/services/category.ts` exports: `createCategory`, `renameCategory`, `deleteCategory`, `listCategories` (sorted by `order`). + +`src/services/appLibrary.ts` exports: `upsertApp`, `listApps`, `assignCategory`, `removeApp`, `mergeScannedApps(db, scanned[])`: +- Match existing by case-normalized `path` +- On match: keep existing `categoryId` and `name`; refresh `platform` / optional icon +- On insert: `categoryId: null`, `source: 'scan'` + +- [ ] **Step 5: Add merge test in `tests/appLibrary.test.ts`** + +```typescript +it('mergeScannedApps keeps categoryId on path match', async () => { + const db = createMemoryDb() + await upsertApp(db, { + _id: makeAppId('1'), + name: 'Old', + path: 'C:/Tool/a.exe', + source: 'scan', + categoryId: 'category:x', + platform: 'win32', + }) + await mergeScannedApps(db, [ + { name: 'New', path: 'C:/Tool/a.exe', platform: 'win32' }, + ]) + const apps = await listApps(db) + expect(apps).toHaveLength(1) + expect(apps[0].categoryId).toBe('category:x') + expect(apps[0].name).toBe('Old') +}) +``` + +Implement until all category/appLibrary tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add src/services/category.ts src/services/appLibrary.ts tests +git commit -m "feat: add category and app library services" +``` + +--- + +### Task 4: Platform scanners + +**Files:** +- Create: `src/services/scanner/types.ts`, `win.ts`, `mac.ts`, `linux.ts`, `index.ts` +- Create: `tests/scanner-win.test.ts`, `tests/scanner-index.test.ts` + +- [ ] **Step 1: Define scanner types** + +```typescript +import type { Platform } from '../../types' + +export interface ScannedApp { + name: string + path: string + platform: Platform +} + +export interface PlatformScanner { + scanSystem(): Promise + scanCustomDir(dir: string): Promise +} +``` + +- [ ] **Step 2: Write Windows custom-dir test with injectable fs** + +`createWinScanner({ readdir, stat, resolveLnk })` for testability. + +```typescript +it('scanCustomDir includes exe bat cmd lnk', async () => { + const files = ['a.exe', 'b.bat', 'c.cmd', 'd.lnk', 'e.txt'] + const scanner = createWinScanner({ + readdir: async () => files, + stat: async () => ({ isFile: () => true, isDirectory: () => false }), + resolveLnk: async (p: string) => ({ + name: 'D', + target: p.replace(/\.lnk$/i, '.exe'), + }), + }) + const apps = await scanner.scanCustomDir('C:/Apps') + const names = apps.map((a) => a.path.toLowerCase()) + expect(names.some((p) => p.endsWith('a.exe'))).toBe(true) + expect(names.some((p) => p.endsWith('b.bat'))).toBe(true) + expect(names.some((p) => p.endsWith('c.cmd'))).toBe(true) + expect(names.some((p) => p.endsWith('.exe'))).toBe(true) // resolved lnk + expect(names.some((p) => p.endsWith('e.txt'))).toBe(false) +}) +``` + +- [ ] **Step 3: Run — expect FAIL, then implement Win/Mac/Linux** + +**Windows:** +- System: `%AppData%\Microsoft\Windows\Start Menu\Programs` and `%ProgramData%\Microsoft\Windows\Start Menu\Programs`, recursive `.lnk` +- Resolve shortcut via PowerShell COM `WScript.Shell.CreateShortcut` +- Custom dirs: recursive depth ≤ 4 for `.exe|.bat|.cmd|.lnk` +- Missing dirs: skip, do not throw + +**macOS:** +- `/Applications`, `~/Applications` — each `.app` → path = bundle, name = basename without `.app` +- Custom: `.app` + files with execute bit + +**Linux:** +- `/usr/share/applications`, `~/.local/share/applications` — parse `Name=` / `Exec=` (strip `%u` etc., first token) +- Custom: execute-bit files + +**`index.ts` `runFullScan`:** pick scanner → system + `settings.customScanDirs` → `mergeScannedApps` → update `lastScanAt`. + +- [ ] **Step 4: Tests pass + commit** + +```bash +git add src/services/scanner tests/scanner-win.test.ts tests/scanner-index.test.ts +git commit -m "feat: add win/mac/linux app scanners" +``` + +--- + +### Task 5: Launcher (parallel open) + +**Files:** +- Create: `src/services/launcher.ts`, `tests/launcher.test.ts` + +- [ ] **Step 1: Failing test** + +```typescript +import { describe, it, expect, vi } from 'vitest' +import { launchPaths } from '../src/services/launcher' + +describe('launchPaths', () => { + it('opens all paths in parallel and summarizes failures', async () => { + const open = vi.fn(async (p: string) => { + if (p.includes('bad')) return { success: false, error: 'missing' } + return { success: true } + }) + const result = await launchPaths(['/a', '/bad', '/c'], open) + expect(open).toHaveBeenCalledTimes(3) + expect(result.success).toBe(2) + expect(result.failed).toBe(1) + expect(result.errors[0].path).toBe('/bad') + }) +}) +``` + +- [ ] **Step 2: Implement** + +```typescript +import type { LaunchResult } from '../types' + +export type OpenPath = ( + fullPath: string, +) => Promise<{ success: boolean; error?: string }> | { success: boolean; error?: string } + +export async function launchPaths(paths: string[], openPath: OpenPath): Promise { + const settled = await Promise.all( + paths.map(async (path) => { + try { + const res = await Promise.resolve(openPath(path)) + if (!res?.success) return { path, ok: false as const, error: res?.error || 'open failed' } + return { path, ok: true as const, error: '' } + } catch (e: any) { + return { path, ok: false as const, error: String(e?.message ?? e) } + } + }), + ) + const errors = settled.filter((s) => !s.ok).map((s) => ({ path: s.path, error: s.error })) + return { + success: settled.filter((s) => s.ok).length, + failed: errors.length, + errors, + } +} + +export async function launchGroupApps( + paths: string[], + openPath: OpenPath = (p) => { + // @ts-expect-error host + return ztools.shellOpenPath(p) + }, +): Promise { + return launchPaths(paths, openPath) +} +``` + +- [ ] **Step 3: Tests pass + commit** + +```bash +git add src/services/launcher.ts tests/launcher.test.ts +git commit -m "feat: add parallel launcher with failure summary" +``` + +--- + +### Task 6: LaunchGroup validation + FeatureSync + +**Files:** +- Create: `src/services/launchGroup.ts`, `src/services/featureSync.ts`, `tests/launchGroup.test.ts`, `tests/featureSync.test.ts` + +- [ ] **Step 1: Failing validation tests** + +```typescript +it('rejects empty name', async () => { + await expect( + saveGroup(db, { name: ' ', cmds: [], appIds: ['app:1'], order: 0 }), + ).rejects.toThrow(/name/i) +}) + +it('rejects zero apps', async () => { + await expect( + saveGroup(db, { name: 'Dev', cmds: ['Dev'], appIds: [], order: 0 }), + ).rejects.toThrow(/app/i) +}) + +it('rejects duplicate cmd against other groups', async () => { + await saveGroup(db, { name: 'A', cmds: ['开工'], appIds: ['app:1'], order: 0 }) + await expect( + saveGroup(db, { name: 'B', cmds: ['开工'], appIds: ['app:2'], order: 1 }), + ).rejects.toThrow(/conflict|冲突|cmd/i) +}) +``` + +- [ ] **Step 2: Implement `launchGroup.ts`** + +Exports: `saveGroup`, `deleteGroup`, `listGroups`, `getGroup`. + +Rules: +- `name.trim()` required +- `appIds.length >= 1` +- default `cmds` to `[name.trim()]` if empty +- cmd conflict: any overlapping cmd string with another group (case-sensitive) +- new docs use `makeGroupId(newId())`, `featureSynced: false` + +- [ ] **Step 3: FeatureSync tests + implement** + +```typescript +it('setFeature on syncGroup and marks featureSynced', async () => { + const setFeature = vi.fn(() => true) + const removeFeature = vi.fn(() => true) + const group = await saveGroup(db, { + name: '开工', + cmds: ['开工'], + appIds: ['app:1'], + order: 0, + }) + await syncGroupFeature(group, { setFeature, removeFeature }, db) + expect(setFeature).toHaveBeenCalledWith( + expect.objectContaining({ code: group._id, cmds: ['开工'] }), + ) + expect((await getGroup(db, group._id))!.featureSynced).toBe(true) +}) + +it('reconcile removes orphan features and restores missing', async () => { + const setFeature = vi.fn(() => true) + const removeFeature = vi.fn(() => true) + const keep = await saveGroup(db, { + name: 'Keep', + cmds: ['Keep'], + appIds: ['app:1'], + order: 0, + }) + const getFeatures = () => [{ code: 'group:orphan' }, { code: keep._id }] + await reconcileFeatures(db, { setFeature, removeFeature, getFeatures }) + expect(removeFeature).toHaveBeenCalledWith('group:orphan') +}) +``` + +`syncGroupFeature` / `removeGroupFeature` / `reconcileFeatures` as in design: `setFeature({ code: group._id, explain: \`启动组:${group.name}\`, cmds })`; on failure set `featureSynced: false` but keep local doc. + +- [ ] **Step 4: Pass + commit** + +```bash +git add src/services/launchGroup.ts src/services/featureSync.ts tests/launchGroup.test.ts tests/featureSync.test.ts +git commit -m "feat: add launch groups and feature sync" +``` + +--- + +### Task 7: Preload wiring + onPluginEnter routing + +**Files:** +- Create: `src/services/enterRouter.ts`, `tests/preload-routing.test.ts` +- Modify: `src/preload.ts` + +- [ ] **Step 1: Router + test** + +```typescript +export type EnterAction = + | { type: 'manage' } + | { type: 'launch-group'; groupId: string } + +export function routePluginEnter(code: string | undefined): EnterAction { + if (!code || code === 'manage') return { type: 'manage' } + if (code.startsWith('group:')) return { type: 'launch-group', groupId: code } + return { type: 'manage' } +} +``` + +```typescript +expect(routePluginEnter('manage')).toEqual({ type: 'manage' }) +expect(routePluginEnter('group:abc')).toEqual({ + type: 'launch-group', + groupId: 'group:abc', +}) +``` + +- [ ] **Step 2: Implement `src/preload.ts`** + +Expose `window.batchStart` with: +`listApps`, `listCategories`, `listGroups`, `getSettings`, `createCategory`, `renameCategory`, `deleteCategory`, `assignCategory`, `addManualApp`, `addCustomScanDir`, `removeCustomScanDir`, `runScan`, `saveGroup`, `deleteGroup`, `retrySyncGroup`, `reconcileFeatures`, `launchGroup(groupId)`. + +On `ztools.onPluginEnter`: +1. `reconcileFeatures` +2. If `launch-group`: resolve app paths → `launchGroupApps` → `showToast(\`成功 ${n} / 失败 ${m}\`)` → `outPlugin()` +3. If `manage`: UI only + +Manual add filters: win `exe,bat,cmd,lnk`; mac/linux permissive. Custom dir via `showOpenDialog({ properties: ['openDirectory'] })`. + +- [ ] **Step 3: Manual smoke checklist in ZTools** + +- 「批量启动」opens UI +- Save group → cmd appears in search +- Trigger group → apps open, plugin exits + +- [ ] **Step 4: Commit** + +```bash +git add src/preload.ts src/services/enterRouter.ts tests/preload-routing.test.ts +git commit -m "feat: wire preload APIs and plugin enter routing" +``` + +--- + +### Task 8: Management UI + +**Files:** +- Modify: `src/App.vue`, `src/main.ts` +- Create: `src/components/TopBar.vue`, `CategorySidebar.vue`, `AppList.vue`, `GroupPanel.vue`, `src/styles.css` + +- [ ] **Step 1: Layout shell** + +Top bar + left categories + center apps + right groups. Load data on mount from `window.batchStart`. + +Empty states: no apps → CTA 扫描应用; no groups → CTA 创建启动组. + +- [ ] **Step 2: TopBar** + +扫描 / 添加目录 / 试跑当前组; show `lastScanAt`; disable 试跑 when no selection. + +- [ ] **Step 3: CategorySidebar + AppList** + +Filter by category (`null` = 未分类); search; multi-select for join group; category dropdown; red badge + 重试同步 when `featureSynced === false`. + +- [ ] **Step 4: GroupPanel** + +CRUD groups; edit cmds; member list; save with validation error toast. + +- [ ] **Step 5: Build + commit** + +```bash +npm run build +git add src +git commit -m "feat: add management UI for scan, categories, and groups" +``` + +--- + +### Task 9: End-to-end hardening + +**Files:** +- Modify: as needed from QA +- Create: `README.md` + +- [ ] **Step 1: QA checklist** + +- [ ] Windows `.bat` in custom dir / manual add launches in group +- [ ] Duplicate cmd rejected +- [ ] Delete group removes Feature +- [ ] Invalid path: partial success toast +- [ ] `npm test` green +- [ ] `npm run build` succeeds + +- [ ] **Step 2: README** + +Document: Node requirement, `npm i && npm run build`, load plugin in ZTools, scan → categorize → create group → trigger cmd. + +- [ ] **Step 3: Final commit** + +```bash +git add README.md +git commit -m "docs: add usage README and finish MVP hardening" +``` + +- [ ] **Step 4: Push only if user explicitly asks** + +```bash +git push -u origin master +``` + +--- + +## Spec coverage self-check + +| Spec requirement | Task | +|------------------|------| +| System + custom dir scan | 4 | +| Manual categories | 3, 8 | +| Launch groups → dynamic Features | 6, 7 | +| Full manage UI | 8 | +| Win/Mac/Linux | 4, `plugin.json` platforms | +| Parallel launch | 5 | +| Win exe/bat/cmd/lnk | 4, 7 | +| Errors / toast / sync retry | 5, 6, 8 | +| Core unit tests | 2–7 | + +## Consistency check + +- Feature codes: static `manage`, dynamic `group:` (= `GroupDoc._id`) across Tasks 6–8. +- Windows extensions: `.exe|.bat|.cmd|.lnk` only. +- No TBD / placeholder steps. diff --git a/plugins/batch-start/docs/superpowers/plans/2026-09-17-batch-start-ztools-plugins.md b/plugins/batch-start/docs/superpowers/plans/2026-09-17-batch-start-ztools-plugins.md new file mode 100644 index 000000000..145342644 --- /dev/null +++ b/plugins/batch-start/docs/superpowers/plans/2026-09-17-batch-start-ztools-plugins.md @@ -0,0 +1,37 @@ +# BatchStart ZTools Plugins Inclusion — Implementation Plan + +> **For agentic workers:** Implement task-by-task. Steps use checkbox syntax. + +**Goal:** Include installed ZTools plugins in the app list and launch them via `redirect`. + +**Architecture:** Scan `~/.ztools/plugins/*/plugin.json`, merge as `source:'plugin'` apps with `ztools-plugin://{name}` paths; launcher branches to `ztools.redirect`. + +**Tech Stack:** Vue 3 + TS + Vite (existing), Node fs in preload/scanner. + +## Global Constraints + +- Exclude plugins: `setting`, `system`, `batch-start` +- Launch: first usable string cmd via `redirect([title, cmd])` +- Sync on full scan; remove orphan plugin apps + strip from groups + +--- + +### Task 1: Types + plugin scanner + merge/reconcile + +- Extend `AppSource` / `AppDoc` +- Add `src/services/scanner/ztoolsPlugins.ts` +- Extend `mergeScannedApps` for plugin source + stable ids +- Reconcile orphans in `runFullScan` +- Tests + +### Task 2: Launcher + preload wiring + +- Launch targets support plugin redirect +- Update `launchGroupById` +- Tests + +### Task 3: UI + version bump + +- AppList badge for plugins +- CHANGELOG 0.1.1 / plugin.json versions +- Build + test diff --git a/plugins/batch-start/docs/superpowers/specs/2026-09-14-ztools-batch-start-design.md b/plugins/batch-start/docs/superpowers/specs/2026-09-14-ztools-batch-start-design.md new file mode 100644 index 000000000..6e087b5c7 --- /dev/null +++ b/plugins/batch-start/docs/superpowers/specs/2026-09-14-ztools-batch-start-design.md @@ -0,0 +1,187 @@ +# ZTools 应用扫描 / 分类 / 批量启动插件 — 设计文档 + +**日期:** 2026-09-14 +**仓库:** https://github.com/ArthurYang999/zTools--.git +**参考文档:** [第一个插件](https://ztoolscenter.github.io/ZTools-doc/first-plugin.html)、[插件 API](https://ztoolscenter.github.io/ZTools-doc/plugin-api.html) + +## 1. 目标 + +做一个 ZTools 插件,支持: + +1. **扫描应用**:系统已安装应用 + 用户自定义目录 +2. **手动分类**:用户自建分类并分配应用 +3. **启动组**:勾选应用组成启动组;保存后注册为 ZTools Feature/指令,主搜索点选即可批量启动 + +成功标准:用户配置好启动组后,在 ZTools 输入组名 → 点击指令 → 组内应用几乎同时启动。 + +## 2. 非目标(本期不做) + +- 自动按规则分类 +- 组内启动间隔配置(本期固定「几乎同时」) +- `.ps1` / `.vbs` 等脚本类型(可后续扩展) +- 云同步 / 多设备共享配置 + +## 3. 技术选型 + +| 项 | 选择 | +|----|------| +| 模板 | Vue 3 + TypeScript + Vite(`@ztools-center/plugin-cli`) | +| 持久化 | `ztools.db` | +| 启动 | `ztools.shellOpenPath` | +| 图标 | `ztools.getFileIcon` | +| 指令 | `ztools.setFeature` / `removeFeature` + `onPluginEnter` | +| 架构 | 模块化:平台扫描器 → 应用库/分类 → 启动组 → Feature 同步 | + +## 4. 整体架构 + +``` +Vue UI(管理页) + │ +Preload 服务层 + AppLibrary · Category · LaunchGroup · FeatureSync · Launcher + │ + ScannerWin / ScannerMac / ScannerLinux + │ + ztools.db + setFeature + shellOpenPath +``` + +- **管理入口**:主 Feature(如「批量启动」)打开完整管理 UI。 +- **启动入口**:每个启动组一个独立 Feature;`onPluginEnter` 按 `code` 批量启动后 `outPlugin`,不打开管理页。 + +## 5. 数据模型 + +### 5.1 `app:` + +| 字段 | 说明 | +|------|------| +| `_id` | `app:` | +| `name` | 显示名 | +| `path` | 可启动路径 | +| `icon` | 可选,缓存的 icon data URL | +| `source` | `scan` \| `manual` | +| `categoryId` | 分类 id,未分类为 `null` | +| `platform` | `win32` \| `darwin` \| `linux` | + +### 5.2 `category:` + +| 字段 | 说明 | +|------|------| +| `_id` | `category:` | +| `name` | 分类名 | +| `order` | 排序 | + +### 5.3 `group:` + +| 字段 | 说明 | +|------|------| +| `_id` | `group:` | +| `name` | 显示名 / 默认指令文本 | +| `cmds` | 触发指令列表(至少含 `name`) | +| `appIds` | 应用 id 数组 | +| `order` | 排序 | +| `featureSynced` | 是否已成功同步 Feature | + +### 5.4 `settings` + +| 字段 | 说明 | +|------|------| +| `_id` | `settings` | +| `customScanDirs` | 自定义扫描目录字符串数组 | +| `lastScanAt` | 上次扫描时间戳 | + +## 6. 核心流程 + +### 6.1 扫描 + +1. 按 `process.platform` 选择适配器。 +2. 扫描系统源 + `settings.customScanDirs`。 +3. 按 `path` 去重合并;已有记录保留 `categoryId` 与手动改名。 +4. 新发现的应用写入 `app:*`,默认 `categoryId = null`。 + +**Windows 系统源:** 开始菜单 `.lnk`(解析后记录目标路径与显示名)。 +**Windows 自定义目录 / 可加入启动组的类型:** `.exe`、`.bat`、`.cmd`、`.lnk`(不限 exe)。 +**macOS:** `/Applications`、`~/Applications` 的 `.app` + 自定义目录。 +**Linux:** `.desktop`(`/usr/share/applications`、`~/.local/share/applications`)+ 自定义目录可执行文件。 + +手动添加:文件选择器可选上述平台对应类型,写入 `source: manual`。 + +### 6.2 分类 + +- UI 新建 / 重命名 / 删除分类。 +- 将应用分配到分类;「未分类」为虚拟视图。 +- 删除分类时,其下应用回到未分类(不删应用)。 + +### 6.3 启动组与 Feature 同步 + +保存启动组时: + +1. 校验:名称非空、至少 1 个应用、指令不与其他组冲突。 +2. 写入 `group:`。 +3. `FeatureSync`: + - `ztools.setFeature({ code: 'group:', explain: '...', cmds: group.cmds })` + - 删除组:`ztools.removeFeature('group:')` +4. 同步失败:本地仍保存,`featureSynced = false`,UI 标红并提供重试。 + +插件加载 / 进入管理页时:用本地 `group:*` 对账,补齐缺失 Feature、移除孤儿 Feature。 + +### 6.4 批量启动 + +1. 解析组内 `appIds` → 路径列表。 +2. **几乎同时**对每个路径调用 `ztools.shellOpenPath`(不设间隔)。 +3. 汇总成功 / 失败;`showToast` 提示。 +4. 若由 Feature 指令触发:启动后 `outPlugin()`。 + +路径失效或打开失败:跳过该项,不中断整组。 + +## 7. UI 结构 + +单页管理界面: + +1. **顶栏**:扫描、添加自定义目录、试跑当前组 +2. **左栏**:分类列表(含未分类) +3. **中栏**:应用列表(搜索、勾选、改分类) +4. **右栏**:启动组列表与编辑(成员、指令名) + +空状态:引导首次扫描;无启动组时引导创建第一组。 + +## 8. 插件 Feature 规划 + +| code | 用途 | +|------|------| +| `manage` | 打开管理 UI;`cmds`:`批量启动`、`应用启动组` | +| `group:` | 动态;启动对应组 | + +`plugin.json` 静态声明主管理 Feature;组 Feature 全部运行时 `setFeature`。 + +## 9. 错误处理 + +| 场景 | 处理 | +|------|------| +| 路径失效 / 启动失败 | 跳过,Toast「成功 n / 失败 m」 | +| 组名空 / 指令冲突 | 禁止保存并提示 | +| 扫描目录不存在 / 无权限 | 跳过该源,继续其余 | +| Feature 同步失败 | 本地保存 + 标红重试 | +| 组内 0 应用 | 禁止注册 Feature | + +## 10. 测试要点 + +- 启动组 CRUD 与 Feature 增删一致(含重启后对账)。 +- Windows:`.exe` / `.bat` / `.cmd` / `.lnk` 均可进组并启动。 +- 三端扫描适配器至少各有一条冒烟路径(真机或 mock)。 +- 批量启动部分失败时仍启动有效项并正确汇总。 + +## 11. 仓库与工程 + +- 工作区:`zTools-BatchStart` +- Git remote:`origin` → `https://github.com/ArthurYang999/zTools--.git` +- 用 `ztools create` 脚手架后,在本仓库实现上述模块,而非另起无关目录结构。 + +## 12. 实现顺序(供后续 plan 使用) + +1. CLI 脚手架 + remote + 主 Feature 空壳 UI +2. db 模型与 AppLibrary / Category CRUD +3. 三端 Scanner + 自定义目录 + Windows 多扩展名 +4. LaunchGroup + Launcher(并行 shellOpenPath) +5. FeatureSync + `onPluginEnter` 分流(管理 vs 组启动) +6. 管理页联调与空状态 / 错误态 +``` diff --git a/plugins/batch-start/docs/superpowers/specs/2026-09-17-batch-start-ztools-plugins-design.md b/plugins/batch-start/docs/superpowers/specs/2026-09-17-batch-start-ztools-plugins-design.md new file mode 100644 index 000000000..e27c50657 --- /dev/null +++ b/plugins/batch-start/docs/superpowers/specs/2026-09-17-batch-start-ztools-plugins-design.md @@ -0,0 +1,119 @@ +# BatchStart: Include Installed ZTools Plugins — Design Spec + +**Date:** 2026-09-17 +**Status:** Approved for planning +**Project:** `zTools-BatchStart` + +## 1. Goal + +Extend the application list so that **installed ZTools plugins** appear alongside scanned/manual apps, can be categorized and added to launch groups, and open via ZTools redirect when a group runs. + +## 2. Decisions (locked) + +| Topic | Choice | +|-------|--------| +| Discovery | Scan `~/.ztools/plugins/*/plugin.json` (no internal API) | +| Launch | Open plugin default entry via `ztools.redirect` using first usable text command | +| Inclusion | All installed plugins **except** `setting`, `system`, and `batch-start` | +| Sync timing | During existing full scan (same 「扫描」 action) | + +## 3. Data model + +### 3.1 `AppSource` + +```ts +type AppSource = 'scan' | 'manual' | 'plugin' +``` + +### 3.2 Plugin app document + +Reuse `AppDoc` with: + +| Field | Value | +|-------|--------| +| `_id` | Stable `app:plugin:{pluginName}` (or existing id helper + plugin prefix) | +| `name` | `plugin.json.title` (fallback `name`) | +| `path` | Pseudo URI `ztools-plugin://{pluginName}` | +| `source` | `'plugin'` | +| `icon` | Resolved logo path or data URL when readable; else `null` | +| `categoryId` | Unchanged merge rules (preserve user category on rescan) | +| `platform` | Current host platform | + +Optional persisted meta (either embed on doc or derive at launch from path + cached fields): + +- `pluginName`: string +- `launchCmd`: first divertible text cmd string +- `pluginTitle`: title used for `redirect([title, cmd])` + +If a plugin has **no** usable string command on any feature, still list it but mark launch as unavailable (skip with error on group launch). + +### 3.3 Divertible command + +A cmd is usable when it is a non-empty `string`. Object cmds (`regex`, `img`, etc.) are skipped for default launch. + +Selection order: first feature in `features[]`, then first usable string in that feature’s `cmds[]`. If none, try subsequent features. + +## 4. Discovery & merge + +### 4.1 Roots + +- Primary: `path.join(os.homedir(), '.ztools', 'plugins')` +- Each immediate child directory: if `plugin.json` exists and parses, treat as one plugin + +### 4.2 Filters + +Skip when `plugin.json.name` is in: + +- `setting` +- `system` +- `batch-start` + +Also skip unreadable / invalid JSON (log once, continue). + +### 4.3 Merge with `AppLibrary` + +- Upsert plugin apps like scanned apps (case-insensitive path / stable id) +- On full scan completion: remove DB docs with `source === 'plugin'` whose plugin is no longer present +- When removing, also strip those `appIds` from all `GroupDoc`s and re-sync features for affected groups + +## 5. Launch path + +Update `launchGroupApps` / group launch: + +1. Resolve each app id → `AppDoc` +2. If `path.startsWith('ztools-plugin://')` (or `source === 'plugin'`): + - Call `ztools.redirect([pluginTitle, launchCmd])` or `ztools.redirect(launchCmd)` if title missing + - Treat falsy return as failure for that item +3. Else: existing `ztools.shellOpenPath(path)` + +Batch launch remains near-simultaneous (`Promise.all`); redirect and shell opens run in parallel. + +「试跑当前组」 uses the same launcher. + +## 6. UI + +- App list: small source badge/label for plugin rows (e.g. 「插件」) +- Path column may show plugin id / title instead of filesystem path +- Scan toast/summary may include plugin count: e.g. scanned N apps including M plugins +- No new tab required + +## 7. Out of scope + +- Privilege / `ztools.internal.getAllPlugins` +- Per-plugin custom command picker in UI (v1 uses first string cmd only) +- Opening a specific non-default feature from the group editor +- Watching plugins folder live without scan (rescan refreshes) + +## 8. Acceptance + +1. After scan, installed third-party plugins appear in the app list (minus exclusions). +2. Plugin can be categorized and added to a launch group. +3. Running the group opens the plugin UI via redirect for plugin members and native apps via shell for others. +4. Uninstalling a plugin + rescan removes it from the list and from groups. +5. Existing scan/manual apps behavior unchanged. + +## 9. Implementation notes + +- Add `src/services/scanner/ztoolsPlugins.ts` (or similar) returning `ScannedApp[]`-compatible shapes with `source: 'plugin'` once types allow — or a dedicated merge step after filesystem scan +- Unit tests: filter exclusions, first-cmd selection, merge/remove orphan plugins, launcher branch for `ztools-plugin://` +- Bump patch version + CHANGELOG when shipping diff --git a/plugins/batch-start/docs/superpowers/specs/2026-11-11-batch-start-ui-refresh-design.md b/plugins/batch-start/docs/superpowers/specs/2026-11-11-batch-start-ui-refresh-design.md new file mode 100644 index 000000000..5d8bf1000 --- /dev/null +++ b/plugins/batch-start/docs/superpowers/specs/2026-11-11-batch-start-ui-refresh-design.md @@ -0,0 +1,46 @@ +# BatchStart UI 焕新设计 + +日期:2026-11-11 +状态:已确认,直接实施 + +## 目标 + +在不改变核心流程(扫描 → 选应用 → 启动组 → 试跑/指令启动)的前提下: + +1. **跟随 ZTools 宿主主题**(亮暗 + 主色),插件不抢戏 +2. **保留三栏**,强化中间应用区为主工作面 +3. **分类弱化**:默认收起为窄轨,可展开;数据与 API 保留 + +## 布局 + +| 区域 | 默认 | 说明 | +|------|------|------| +| 顶栏 | 全宽 | 标题 + 扫描(主)/加目录/试跑 + 上次扫描 | +| 左:分类 | 收起 ~48px | 展开 ~168px;状态持久化 | +| 中:应用 | flex:1 | 主表面、搜索、来源筛选、列表 | +| 右:启动组 | ~280px | 可折叠窄条;空状态轻量 CTA | + +## 主题 Token + +- `ztools.getThemeInfo()` / `onThemeChange` → CSS 变量 +- `--bg` `--surface` `--surface-2` `--text` `--muted` `--border` `--accent` `--selected` `--danger` +- 无宿主 API 时浅色中性降级 +- 主色优先用宿主注入的 `--plugin-primary-color`(若有)或 themeInfo 解析 + +## 视觉 + +- 平面 + 1px 分割,少阴影;圆角 6–8px +- 系统字体栈;动效 150–200ms;`prefers-reduced-motion` +- 列表选中:`--selected` + 左侧 accent 细条 +- 分段筛选低调,不大胶囊堆 + +## 空状态 / 反馈 + +- 无应用:整页扫描 CTA +- 有应用无组:右栏空状态 +- 筛选无结果:列表内提示 +- busy 防连点;试跑 toast;保存错误行内 + +## 非目标 + +- 移除分类数据模型、拖拽排序、新手引导、自建主题页 diff --git a/plugins/batch-start/dist/index.html b/plugins/batch-start/index.html similarity index 53% rename from plugins/batch-start/dist/index.html rename to plugins/batch-start/index.html index 16c6a57a0..2730397e7 100644 --- a/plugins/batch-start/dist/index.html +++ b/plugins/batch-start/index.html @@ -1,13 +1,12 @@ - - - - - - 批量启动 - - - - -
- - + + + + + + 批量启动 + + +
+ + + diff --git a/plugins/batch-start/package-lock.json b/plugins/batch-start/package-lock.json new file mode 100644 index 000000000..3ea344942 --- /dev/null +++ b/plugins/batch-start/package-lock.json @@ -0,0 +1,2974 @@ +{ + "name": "ztools-batch-start", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ztools-batch-start", + "version": "0.1.0", + "dependencies": { + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3", + "vitest": "^2.1.8", + "vue-tsc": "^2.1.10" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz", + "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz", + "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz", + "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz", + "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz", + "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz", + "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz", + "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz", + "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz", + "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz", + "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz", + "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz", + "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz", + "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz", + "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz", + "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz", + "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz", + "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz", + "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz", + "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz", + "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz", + "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz", + "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz", + "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz", + "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz", + "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.2.tgz", + "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.2.tgz", + "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.2", + "@rollup/rollup-android-arm64": "4.63.2", + "@rollup/rollup-darwin-arm64": "4.63.2", + "@rollup/rollup-darwin-x64": "4.63.2", + "@rollup/rollup-freebsd-arm64": "4.63.2", + "@rollup/rollup-freebsd-x64": "4.63.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.2", + "@rollup/rollup-linux-arm-musleabihf": "4.63.2", + "@rollup/rollup-linux-arm64-gnu": "4.63.2", + "@rollup/rollup-linux-arm64-musl": "4.63.2", + "@rollup/rollup-linux-loong64-gnu": "4.63.2", + "@rollup/rollup-linux-loong64-musl": "4.63.2", + "@rollup/rollup-linux-ppc64-gnu": "4.63.2", + "@rollup/rollup-linux-ppc64-musl": "4.63.2", + "@rollup/rollup-linux-riscv64-gnu": "4.63.2", + "@rollup/rollup-linux-riscv64-musl": "4.63.2", + "@rollup/rollup-linux-s390x-gnu": "4.63.2", + "@rollup/rollup-linux-x64-gnu": "4.63.2", + "@rollup/rollup-linux-x64-musl": "4.63.2", + "@rollup/rollup-openbsd-x64": "4.63.2", + "@rollup/rollup-openharmony-arm64": "4.63.2", + "@rollup/rollup-win32-arm64-msvc": "4.63.2", + "@rollup/rollup-win32-ia32-msvc": "4.63.2", + "@rollup/rollup-win32-x64-gnu": "4.63.2", + "@rollup/rollup-win32-x64-msvc": "4.63.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/plugins/batch-start/package.json b/plugins/batch-start/package.json new file mode 100644 index 000000000..187adc995 --- /dev/null +++ b/plugins/batch-start/package.json @@ -0,0 +1,23 @@ +{ + "name": "ztools-batch-start", + "version": "0.1.3", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "vue": "^3.5.13" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.2", + "vite": "^6.0.3", + "vitest": "^2.1.8", + "vue-tsc": "^2.1.10" + } +} diff --git a/plugins/batch-start/plugin.json b/plugins/batch-start/plugin.json index b09a88077..fbf14398c 100644 --- a/plugins/batch-start/plugin.json +++ b/plugins/batch-start/plugin.json @@ -3,7 +3,7 @@ "title": "批量启动", "description": "扫描应用、手动分类,并将启动组注册为 ZTools 指令一键批量启动", "author": "ArthurYang999", - "version": "0.1.2", + "version": "0.1.3", "platform": ["win32", "darwin", "linux"], "main": "dist/index.html", "logo": "logo.png", diff --git a/plugins/batch-start/preload/services.js b/plugins/batch-start/preload/services.js deleted file mode 100644 index 10d315337..000000000 --- a/plugins/batch-start/preload/services.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict";const h=require("node:path"),v=require("node:fs"),j=require("node:os"),ot=require("node:url"),k=require("node:fs/promises"),rt=require("node:child_process"),st=require("node:util");function N(t){return`app:${t}`}function it(t){return`category:${t}`}function at(t){return`group:${t}`}function O(){return crypto.randomUUID()}function J(){return(globalThis.ztools??window.ztools).db.promises}async function $(t=J()){const n=await t.get("settings");if(n)return n;const e={_id:"settings",customScanDirs:[],lastScanAt:null};return await t.put(e),await t.get("settings")}async function D(t){return(await t.allDocs("group:")).sort((e,s)=>e.order-s.order)}async function b(t,n){return await t.get(n)}async function ct(t,n){const e=await t.get(n);e&&await t.remove(e)}async function ut(t,n,e){const s=n.name.trim();if(!s)throw new Error("Group name is required");if(!n.appIds||n.appIds.length<1)throw new Error("Group must include at least one app");const i=!n.cmds||n.cmds.length===0?[s]:[...n.cmds],o=await D(t);for(const r of o)if(!(e&&r._id===e)){for(const a of i)if(r.cmds.includes(a))throw new Error(`cmd conflict: "${a}"`)}if(e){const r=await t.get(e);if(!r)throw new Error(`Group not found: ${e}`);const a={...r,name:s,cmds:i,appIds:[...n.appIds],order:n.order,featureSynced:!1};return await t.put(a),await t.get(e)}const u={_id:at(O()),name:s,cmds:i,appIds:[...n.appIds],order:n.order,featureSynced:!1};return await t.put(u),await t.get(u._id)}const X="ztools-cmd://",lt="ztools-plugin://",ft=new Set(["setting","system","batch-start"]),Y=new Set(["批量启动","应用启动组"]);function R(t,n){const e=t.trim().toLowerCase();return!!(e==="setting"||e==="system"||e==="batch-start"||e.startsWith("batch-start")||ft.has(e)||(n==null?void 0:n.trim())==="批量启动")}function pt(t,n){return`${X}${t}/${encodeURIComponent(n)}`}function U(t){const n=t.toLowerCase();return n.startsWith(X)||n.startsWith(lt)}function mt(t,n={}){if(!Array.isArray(t))return[];const e=n.excludeCmds,s=n.skipGroupFeatures!==!1,i=new Set,o=[];for(const u of t){const r=u==null?void 0:u.code;if(s&&typeof r=="string"&&r.startsWith("group:"))continue;const a=u==null?void 0:u.cmds;if(Array.isArray(a))for(const c of a){if(typeof c!="string")continue;const l=c.trim();!l||i.has(l)||Y.has(l)||e!=null&&e.has(l)||(i.add(l),o.push(l))}}return o}async function K(t,n,e){if(!n||n.includes("://"))return null;const s=h.isAbsolute(n)?n:h.join(t,n);try{return await e(s),ot.pathToFileURL(s).href}catch{return null}}function dt(t,n){return`cmd:${t}:${encodeURIComponent(n)}`}function ht(t,n){const e=t.split(".").map(o=>parseInt(o,10)||0),s=n.split(".").map(o=>parseInt(o,10)||0),i=Math.max(e.length,s.length);for(let o=0;o({name:o,path:pt(n.name,o),platform:t,icon:s,source:"ztools",pluginName:n.name,pluginTitle:n.title,launchCmd:o,stableIdHint:dt(n.name,o)}))}async function Q(t,n){try{const e=await n(h.join(t,"plugin.json"),"utf-8");return JSON.parse(e)}catch{return null}}async function yt(t,n={}){const e=n.pluginsRoot??h.join(j.homedir(),".ztools","plugins"),s=n.readDir??(c=>v.promises.readdir(c)),i=n.readFile??((c,l)=>v.promises.readFile(c,l)),o=n.access??(c=>v.promises.access(c));let u;try{u=await s(e)}catch{return[]}const r=new Map;for(const c of u){if(c===".installing"||c.endsWith(".asar.unpacked"))continue;const l=h.join(e,c),f=await Q(l,i);if(!f)continue;const m=typeof f.name=="string"?f.name.trim():"";if(!m||R(m,f.title))continue;const d=typeof f.title=="string"&&f.title.trim()?f.title.trim():m,w=typeof f.version=="string"&&f.version.trim()||wt(c)||"0.0.0",y=r.get(m);y&&ht(w,y.version)<0||r.set(m,{name:m,title:d,version:w,root:l,logo:typeof f.logo=="string"?f.logo:void 0,features:f.features})}const a=[];for(const c of r.values()){const l=await K(c.root,c.logo,o);a.push(...V(t,{name:c.name,title:c.title,path:c.root,logo:c.logo,features:c.features},n.excludeCmds,l))}return a}async function kt(t,n={}){const s=await(n.listInstalledPlugins??gt)();if(s!==null){const i=[],o=n.readFile??((r,a)=>v.promises.readFile(r,a)),u=n.access??(r=>v.promises.access(r));for(const r of s){if(R(r.name,r.title))continue;let a=r.features,c=r.logo;if(!Array.isArray(a)||a.length===0){if(!r.path)continue;const f=await Q(r.path,o);if(!f)continue;a=f.features,!c&&typeof f.logo=="string"&&(c=f.logo)}const l=c&&c.includes("://")?c:r.path?await K(r.path,c,u):null;i.push(...V(t,{name:r.name,title:r.title,path:r.path,features:a},n.excludeCmds,l))}return i}return yt(t,n)}function _(t){return t.toLowerCase()}function A(t){return t==="ztools"||t==="plugin"}async function W(t,n){return await t.put(n),await t.get(n._id)}async function x(t){return await t.allDocs("app:")}async function St(t,n,e){const s=await t.get(n);if(!s)throw new Error(`App not found: ${n}`);return W(t,{...s,categoryId:e})}async function Ct(t,n){const e=await t.get(n);e&&await t.remove(e)}async function Ft(t,n){if(n.length===0)return;if(t.bulkDocs){await t.bulkDocs(n);return}const e=40;for(let s=0;st.put(o)))}}async function xt(t,n){const e=await x(t),s=new Map(e.map(o=>[_(o.path),o])),i=[];for(const o of n){const u=_(o.path),r=s.get(u),a=o.source??"scan";if(r){const c={...r,platform:o.platform,...o.icon!==void 0?{icon:o.icon}:{}};(r.source==="scan"||A(r.source))&&o.name&&(c.name=o.name),A(a)&&(c.source="ztools",o.pluginName&&(c.pluginName=o.pluginName),o.pluginTitle&&(c.pluginTitle=o.pluginTitle),o.launchCmd!==void 0&&(c.launchCmd=o.launchCmd)),i.push(c),s.set(u,c)}else{const l={_id:o.stableIdHint?N(o.stableIdHint):A(a)&&o.pluginName&&o.launchCmd?N(`cmd:${o.pluginName}:${encodeURIComponent(o.launchCmd)}`):N(O()),name:o.name,path:o.path,icon:o.icon??null,source:A(a)?"ztools":a,categoryId:null,platform:o.platform,...A(a)?{pluginName:o.pluginName,pluginTitle:o.pluginTitle,launchCmd:o.launchCmd??null}:{}};i.push(l),s.set(u,l)}}return await Ft(t,i),x(t)}async function Et(t,n){const e=new Set(n.map(_)),i=(await x(t)).filter(r=>!A(r.source)&&!U(r.path)?!1:!e.has(_(r.path)));if(i.length===0)return[];const o=new Set(i.map(r=>r._id));for(const r of i)await Ct(t,r._id);const u=await D(t);for(const r of u){const a=r.appIds.filter(c=>!o.has(c));a.length!==r.appIds.length&&await t.put({...r,appIds:a,featureSynced:!1})}return[...o]}async function tt(t){return(await t.allDocs("category:")).sort((e,s)=>e.order-s.order)}async function $t(t,n){const s=(await tt(t)).reduce((o,u)=>Math.max(o,u.order),-1),i={_id:it(O()),name:n,order:s+1};return await t.put(i),await t.get(i._id)}async function At(t,n,e){const s=await t.get(n);if(!s)throw new Error(`Category not found: ${n}`);const i={...s,name:e};return await t.put(i),await t.get(n)}async function It(t,n){const e=await t.get(n);e&&await t.remove(e);const s=await x(t);for(const i of s)i.categoryId===n&&await W(t,{...i,categoryId:null})}function Pt(t){return!t||t==="manage"?{type:"manage"}:t.startsWith("group:")?{type:"launch-group",groupId:t}:{type:"manage"}}async function L(t,n,e){let s=!1;try{s=!!await n.setFeature({code:t._id,explain:`启动组:${t.name}`,cmds:t.cmds})}catch{s=!1}const i={...t,featureSynced:s};return await e.put(i),await b(e,t._id)}async function vt(t,n){await n.removeFeature(t)}async function nt(t,n){const e=await D(t),s=new Set(e.map(r=>r._id)),o=(n.getFeatures?await n.getFeatures():[]).filter(r=>String(r.code).startsWith("group:"));for(const r of o)s.has(r.code)||await n.removeFeature(r.code);const u=new Set(o.map(r=>r.code));for(const r of e)(!u.has(r._id)||!r.featureSynced)&&await L(r,n,t)}const Dt=2e3;function Nt(t){return t.filter(n=>n.ok&&n.kind==="ztools").length}async function H(t,n){var e,s;try{if(t.kind==="ztools"){const u=(e=t.launchCmd)==null?void 0:e.trim();if(!u)return{path:t.key,ok:!1,error:"command has no divertible text",kind:"ztools"};const r=n.redirect;if(!r)return{path:t.key,ok:!1,error:"redirect API unavailable",kind:"ztools"};const a=(s=t.pluginTitle)==null?void 0:s.trim();return r(a?[a,u]:u)?{path:t.key,ok:!0,error:"",kind:"ztools"}:{path:t.key,ok:!1,error:"redirect failed",kind:"ztools"}}const i=t.path;if(!i)return{path:t.key,ok:!1,error:"missing path",kind:"path"};const o=await Promise.resolve(n.openPath(i));return o!=null&&o.success?{path:t.key,ok:!0,error:"",kind:"path"}:{path:t.key,ok:!1,error:(o==null?void 0:o.error)||"open failed",kind:"path"}}catch(i){const o=i instanceof Error?i.message:String(i);return{path:t.key,ok:!1,error:o,kind:t.kind}}}async function Tt(t,n){const e=t.filter(a=>a.kind==="path"),s=t.filter(a=>a.kind==="ztools"),i=await Promise.all(e.map(a=>H(a,n))),o=[];for(const a of s)o.push(await H(a,n));const u=[...i,...o],r=u.filter(a=>!a.ok).map(a=>({path:a.path,error:a.error}));return{success:u.filter(a=>a.ok).length,failed:r.length,errors:r,outcomes:u}}async function _t(t,n=s=>ztools.shellOpenPath(s),e=(s,i)=>ztools.redirect(s,i)){const s=t.map(i=>typeof i=="string"?U(i)?{key:i,kind:"ztools",launchCmd:null}:{key:i,kind:"path",path:i}:i);return Tt(s,{openPath:n,redirect:e})}function T(t,n=""){const e=`${t} ${n}`.toLowerCase();return!!(e.includes("uninstall")||e.includes("卸载")||/unins\d{0,3}\.exe$/i.test(n)||/(^|[\\/])uninstall\.exe$/i.test(n))}function M(t){return t.filter(n=>!T(n.name,n.path))}function E(t){if(!t||typeof t!="object"||!("code"in t))return!1;const n=t.code;return n==="ENOENT"||n==="EACCES"||n==="EPERM"}function jt(t){return typeof t.mode=="number"&&(t.mode&73)!==0}function Ot(t){const n=t.replace(/%[fFuUdDnNickvm]/g,"").trim();if(!n)return null;if(n.startsWith('"')){const s=n.indexOf('"',1);if(s>1)return n.slice(1,s)}return n.split(/\s+/)[0]||null}function zt(t){const n=t.split(/\r?\n/);let e=!1,s,i;for(const u of n){const r=u.trim();if(!r||r.startsWith("#"))continue;if(r.startsWith("[")){e=r==="[Desktop Entry]";continue}if(!e)continue;const a=r.indexOf("=");if(a<=0)continue;const c=r.slice(0,a),l=r.slice(a+1);c==="Name"&&s===void 0&&(s=l),c==="Exec"&&i===void 0&&(i=l)}if(!s||!i)return null;const o=Ot(i);return o?{name:s,exec:o}:null}function Gt(t={}){const n=t.readdir??(a=>k.readdir(a)),e=t.stat??(a=>k.stat(a)),s=t.readFile??((a,c="utf8")=>k.readFile(a,c)),i=t.homedir??(()=>j.homedir()),o=t.join??h.join;async function u(a){const c=[];let l;try{l=await n(a)}catch(f){if(E(f))return[];throw f}for(const f of l){if(!f.toLowerCase().endsWith(".desktop"))continue;const m=o(a,f);let d;try{d=await e(m)}catch(C){if(E(C))continue;throw C}if(!d.isFile())continue;let w;try{w=await s(m,"utf8")}catch(C){if(E(C))continue;throw C}const y=zt(w);y&&c.push({name:y.name,path:y.exec,platform:"linux"})}return c}async function r(a){const c=[];let l;try{l=await n(a)}catch(f){if(E(f))return[];throw f}for(const f of l){const m=o(a,f);let d;try{d=await e(m)}catch(w){if(E(w))continue;throw w}!d.isFile()||!jt(d)||c.push({name:f,path:m,platform:"linux"})}return c}return{async scanSystem(){const a=["/usr/share/applications",o(i(),".local","share","applications")],c=[];for(const l of a)c.push(...await u(l));return c},async scanCustomDir(a){try{return await r(a)}catch(c){if(E(c))return[];throw c}}}}function z(t){if(!t||typeof t!="object"||!("code"in t))return!1;const n=t.code;return n==="ENOENT"||n==="EACCES"||n==="EPERM"}function Lt(t){return typeof t.mode=="number"&&(t.mode&73)!==0}function Mt(t){return h.basename(t,".app")}function bt(t={}){const n=t.readdir??(u=>k.readdir(u)),e=t.stat??(u=>k.stat(u)),s=t.homedir??(()=>j.homedir()),i=t.join??h.join;async function o(u,r){const a=[];let c;try{c=await n(u)}catch(l){if(z(l))return[];throw l}for(const l of c){const f=i(u,l);let m;try{m=await e(f)}catch(d){if(z(d))continue;throw d}if(m.isDirectory()&&l.toLowerCase().endsWith(".app")){a.push({name:Mt(f),path:f,platform:"darwin"});continue}r.includeExecutables&&m.isFile()&&Lt(m)&&a.push({name:l,path:f,platform:"darwin"})}return a}return{async scanSystem(){const u=["/Applications",i(s(),"Applications")],r=[];for(const a of u)r.push(...await o(a,{includeExecutables:!1}));return r},async scanCustomDir(u){try{return await o(u,{includeExecutables:!0})}catch(r){if(z(r))return[];throw r}}}}const Rt=st.promisify(rt.execFile),Ut=4,Wt=new Set([".exe",".bat",".cmd",".lnk"]),q=80;function G(t){return h.basename(t,h.extname(t))}function B(t){if(!t||typeof t!="object"||!("code"in t))return!1;const n=t.code;return n==="ENOENT"||n==="EACCES"||n==="EPERM"}async function Zt(t){const n=new Map;if(t.length===0)return n;for(let e=0;e{})}return n}async function qt(t){return(await k.readdir(t,{withFileTypes:!0})).map(e=>({name:e.name,isFile:()=>e.isFile(),isDirectory:()=>e.isDirectory()}))}function Bt(t={}){const n=t.readdir??qt,e=t.resolveLnks??Zt,s=t.env??process.env,i=t.join??h.join;async function o(r,a){const c=[],l=[];async function f(m,d){if(d>a.maxDepth)return;let w;try{w=await n(m)}catch(g){if(B(g))return;throw g}const y=[];for(const g of w){const F=i(m,g.name);if(g.isDirectory()){y.push(F);continue}if(!g.isFile())continue;const I=h.extname(g.name).toLowerCase();if(a.lnkOnly){if(I!==".lnk")continue}else if(!Wt.has(I))continue;if(I===".lnk"){if(T(G(g.name),F))continue;l.push(F)}else{const Z=G(g.name);if(T(Z,F))continue;c.push({name:Z,path:F,platform:"win32"})}}const C=8;for(let g=0;gf(I,d+1)))}}return await f(r,0),{files:c,lnkPaths:l}}async function u(r,a){const{files:c,lnkPaths:l}=await o(r,a);if(l.length===0)return c;const f=await e(l),m=[];for(const d of l){const w=f.get(d);if(!w)continue;const y=G(d);T(y,w)||m.push({name:y,path:w,platform:"win32"})}return M([...c,...m])}return{async scanSystem(){const r=[];s.APPDATA&&r.push(i(s.APPDATA,"Microsoft","Windows","Start Menu","Programs"));const a=s.ProgramData??s.PROGRAMDATA;return a&&r.push(i(a,"Microsoft","Windows","Start Menu","Programs")),(await Promise.all(r.map(l=>u(l,{maxDepth:Number.POSITIVE_INFINITY,lnkOnly:!0})))).flat()},async scanCustomDir(r){try{return await u(r,{maxDepth:Ut,lnkOnly:!1})}catch(a){if(B(a))return[];throw a}}}}function Jt(t=process.platform){switch(t){case"darwin":return bt();case"linux":return Gt();case"win32":default:return Bt()}}function Xt(t){return t==="darwin"||t==="linux"||t==="win32"?t:"win32"}async function Yt(t){const n=new Set(Y),e=await D(t);for(const s of e)for(const i of s.cmds){const o=String(i).trim();o&&n.add(o)}return n}async function Kt(t,n=process.platform,e={}){var l;const s=e.scanner??Jt(n),i=await $(t),o=Xt(n),u=M([...await s.scanSystem()]);for(const f of i.customScanDirs)u.push(...M(await s.scanCustomDir(f)));const r=await Yt(t);let a;e.scanZtoolsCommands?a=await e.scanZtoolsCommands(o,r):e.scanZtoolsPlugins?a=await e.scanZtoolsPlugins(o):a=await kt(o,{excludeCmds:r}),u.push(...a),await xt(t,u),await Et(t,a.map(f=>f.path));const c=((l=e.now)==null?void 0:l.call(e))??Date.now();return await t.put({...i,lastScanAt:c}),x(t)}function S(){return globalThis.ztools??window.ztools}function p(){return J()}function P(){const t=S();return{setFeature:n=>t.setFeature(n),removeFeature:n=>t.removeFeature(n),getFeatures:()=>{var n;return((n=t.getFeatures)==null?void 0:n.call(t))??[]}}}function Vt(){const t=process.platform;return t==="darwin"||t==="linux"||t==="win32"?t:"win32"}function Qt(){if(process.platform==="win32")return[{name:"Apps",extensions:["exe","bat","cmd","lnk"]}]}async function tn(t){const n=await x(p()),e=new Map(n.map(i=>[i._id,i])),s=[];for(const i of t.appIds){const o=e.get(i);o&&(o.source==="ztools"||o.source==="plugin"||U(o.path)?s.push({key:o.path,kind:"ztools",pluginTitle:o.pluginTitle||void 0,launchCmd:o.launchCmd}):o.path&&s.push({key:o.path,kind:"path",path:o.path}))}return s}async function et(t){const n=await b(p(),t);if(!n)return{success:0,failed:0,errors:[]};const e=await tn(n),s=S();return _t(e,i=>s.shellOpenPath(i),(i,o)=>s.redirect(i,o))}const nn={listApps:()=>x(p()),listCategories:()=>tt(p()),listGroups:()=>D(p()),getSettings:()=>$(p()),createCategory:t=>$t(p(),t),renameCategory:(t,n)=>At(p(),t,n),deleteCategory:t=>It(p(),t),assignCategory:(t,n)=>St(p(),t,n),async addManualApp(){var o;const t=S(),n=t.showOpenDialog({properties:["openFile"],filters:Qt()});if(!(n!=null&&n.length))return null;const e=n[0],s=h.basename(e,h.extname(e)),i=((o=t.getFileIcon)==null?void 0:o.call(t,e))??null;return W(p(),{_id:N(O()),name:s,path:e,icon:i,source:"manual",categoryId:null,platform:Vt()})},async addCustomScanDir(){const n=S().showOpenDialog({properties:["openDirectory"]});if(!(n!=null&&n.length))return null;const e=n[0],s=await $(p());return s.customScanDirs.includes(e)?s:(await p().put({...s,customScanDirs:[...s.customScanDirs,e]}),$(p()))},async removeCustomScanDir(t){const n=await $(p());return await p().put({...n,customScanDirs:n.customScanDirs.filter(e=>e!==t)}),$(p())},runScan:()=>Kt(p()),async saveGroup(t,n){const e=await ut(p(),t,n);return L(e,P(),p())},async deleteGroup(t){await ct(p(),t),await vt(t,P())},async retrySyncGroup(t){const n=await b(p(),t);return n?L(n,P(),p()):null},reconcileFeatures:()=>nt(p(),P()),launchGroup:t=>et(t)};window.batchStart=nn;S().onPluginOut(t=>{t||Promise.resolve(S().outPlugin(!0))});S().onPluginEnter(async t=>{try{await nt(p(),P())}catch{}const n=Pt(t==null?void 0:t.code);if(n.type==="launch-group"){const e=await et(n.groupId);await Promise.resolve(S().showToast(`成功 ${e.success} / 失败 ${e.failed}`)),Nt(e.outcomes??[])>0&&await new Promise(s=>setTimeout(s,Dt)),await Promise.resolve(S().outPlugin(!0));return}}); diff --git a/plugins/batch-start/dist/logo.png b/plugins/batch-start/public/logo.png similarity index 100% rename from plugins/batch-start/dist/logo.png rename to plugins/batch-start/public/logo.png diff --git a/plugins/batch-start/logo.png b/plugins/batch-start/src-ztools/logo.png similarity index 100% rename from plugins/batch-start/logo.png rename to plugins/batch-start/src-ztools/logo.png diff --git a/plugins/batch-start/src-ztools/plugin.json b/plugins/batch-start/src-ztools/plugin.json new file mode 100644 index 000000000..73507e2e1 --- /dev/null +++ b/plugins/batch-start/src-ztools/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "batch-start", + "title": "批量启动", + "description": "扫描应用、手动分类,并将启动组注册为 ZTools 指令一键批量启动", + "author": "ArthurYang999", + "version": "0.1.3", + "platform": ["win32", "darwin", "linux"], + "main": "dist/index.html", + "logo": "logo.png", + "preload": "preload/services.js", + "features": [ + { + "code": "manage", + "explain": "管理应用分类与启动组", + "icon": "logo.png", + "cmds": ["批量启动", "应用启动组"] + } + ], + "development": { + "main": "http://localhost:5173" + } +} diff --git a/plugins/batch-start/preload/package.json b/plugins/batch-start/src-ztools/preload/package.json similarity index 89% rename from plugins/batch-start/preload/package.json rename to plugins/batch-start/src-ztools/preload/package.json index 140154e8d..5bbefffba 100644 --- a/plugins/batch-start/preload/package.json +++ b/plugins/batch-start/src-ztools/preload/package.json @@ -1,3 +1,3 @@ -{ - "type": "commonjs" -} +{ + "type": "commonjs" +} diff --git a/plugins/batch-start/src/App.vue b/plugins/batch-start/src/App.vue new file mode 100644 index 000000000..a41e3f28d --- /dev/null +++ b/plugins/batch-start/src/App.vue @@ -0,0 +1,587 @@ + + + + + diff --git a/plugins/batch-start/src/components/AppList.vue b/plugins/batch-start/src/components/AppList.vue new file mode 100644 index 000000000..3f7860f4f --- /dev/null +++ b/plugins/batch-start/src/components/AppList.vue @@ -0,0 +1,587 @@ + + + + + diff --git a/plugins/batch-start/src/components/CategorySidebar.vue b/plugins/batch-start/src/components/CategorySidebar.vue new file mode 100644 index 000000000..883500f4f --- /dev/null +++ b/plugins/batch-start/src/components/CategorySidebar.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/plugins/batch-start/src/components/GroupPanel.vue b/plugins/batch-start/src/components/GroupPanel.vue new file mode 100644 index 000000000..f688cea11 --- /dev/null +++ b/plugins/batch-start/src/components/GroupPanel.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/plugins/batch-start/src/components/TopBar.vue b/plugins/batch-start/src/components/TopBar.vue new file mode 100644 index 000000000..0d6c4e8bc --- /dev/null +++ b/plugins/batch-start/src/components/TopBar.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/plugins/batch-start/src/composables/useHostTheme.ts b/plugins/batch-start/src/composables/useHostTheme.ts new file mode 100644 index 000000000..4c7a65171 --- /dev/null +++ b/plugins/batch-start/src/composables/useHostTheme.ts @@ -0,0 +1,149 @@ +import { onMounted, onUnmounted } from 'vue' + +export type HostThemeInfo = { + isDark?: boolean + primaryColor?: string + customColor?: string + windowMaterial?: string +} + +const PRIMARY_LIGHT: Record = { + blue: '#0284c7', + purple: '#7c3aed', + green: '#059669', + orange: '#ea580c', + red: '#dc2626', + pink: '#db2777', +} + +const PRIMARY_DARK: Record = { + blue: '#38bdf8', + purple: '#a78bfa', + green: '#34d399', + orange: '#fb923c', + red: '#f87171', + pink: '#f472b6', +} + +function resolveAccent(info: HostThemeInfo): string { + const root = getComputedStyle(document.documentElement) + const injected = root.getPropertyValue('--plugin-primary-color').trim() + if (injected) return injected + + if (info.primaryColor === 'custom' && info.customColor && /^#[\da-f]{6}$/i.test(info.customColor)) { + return info.customColor + } + const map = info.isDark ? PRIMARY_DARK : PRIMARY_LIGHT + return map[info.primaryColor || 'green'] || map.green +} + +function hexToRgb(hex: string): { r: number; g: number; b: number } | null { + const m = hex.replace('#', '').match(/^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i) + if (!m) return null + return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) } +} + +function withAlpha(hex: string, alpha: number): string { + const rgb = hexToRgb(hex) + if (!rgb) return hex + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` +} + +export function applyHostTheme(info: HostThemeInfo = {}): void { + const root = document.documentElement + const dark = Boolean(info.isDark) + const accent = resolveAccent(info) + + root.dataset.theme = dark ? 'dark' : 'light' + root.style.setProperty('--accent', accent) + root.style.setProperty('--accent-hover', accent) + root.style.setProperty('--selected', withAlpha(accent, dark ? 0.18 : 0.12)) + root.style.setProperty('--selected-border', withAlpha(accent, dark ? 0.35 : 0.28)) + root.style.setProperty('--badge-bg', withAlpha(accent, dark ? 0.22 : 0.14)) + root.style.setProperty('--badge-fg', accent) + + if (dark) { + root.style.setProperty('--bg', '#12141a') + root.style.setProperty('--surface', '#1a1d24') + root.style.setProperty('--surface-2', '#22262f') + root.style.setProperty('--text', '#e8eaed') + root.style.setProperty('--muted', '#9aa3b2') + root.style.setProperty('--border', '#2e3440') + root.style.setProperty('--danger', '#f87171') + root.style.setProperty('--danger-bg', 'rgba(248, 113, 113, 0.12)') + root.style.setProperty('--danger-border', 'rgba(248, 113, 113, 0.35)') + root.style.setProperty('--hover', 'rgba(255, 255, 255, 0.04)') + root.style.setProperty('--input-bg', '#161920') + root.style.setProperty('--tag-group-fg', '#7dd3fc') + root.style.setProperty('--tag-group-bg', 'rgba(14, 165, 233, 0.18)') + root.style.setProperty('--tag-group-border', 'rgba(125, 211, 252, 0.28)') + } else { + root.style.setProperty('--bg', '#f3f4f6') + root.style.setProperty('--surface', '#ffffff') + root.style.setProperty('--surface-2', '#f8fafc') + root.style.setProperty('--text', '#1f2937') + root.style.setProperty('--muted', '#6b7280') + root.style.setProperty('--border', '#e5e7eb') + root.style.setProperty('--danger', '#dc2626') + root.style.setProperty('--danger-bg', '#fef2f2') + root.style.setProperty('--danger-border', '#fecaca') + root.style.setProperty('--hover', 'rgba(15, 23, 42, 0.04)') + root.style.setProperty('--input-bg', '#ffffff') + root.style.setProperty('--tag-group-fg', '#0369a1') + root.style.setProperty('--tag-group-bg', 'rgba(14, 165, 233, 0.12)') + root.style.setProperty('--tag-group-border', 'rgba(14, 165, 233, 0.28)') + } +} + +type ThemeHost = { + getThemeInfo?: () => HostThemeInfo + onThemeChange?: (cb: (info: HostThemeInfo) => void) => void + isDarkColors?: () => boolean +} + +function readHost(): ThemeHost | null { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((globalThis as any).ztools ?? (typeof window !== 'undefined' ? (window as any).ztools : null)) as + | ThemeHost + | null +} + +/** Bind host theme to CSS variables for the plugin lifetime. */ +export function useHostTheme(): void { + let attached = false + + const refresh = (info?: HostThemeInfo) => { + const z = readHost() + if (info) { + applyHostTheme(info) + return + } + try { + const fromApi = z?.getThemeInfo?.() + if (fromApi) { + applyHostTheme(fromApi) + return + } + } catch { + // fall through + } + applyHostTheme({ isDark: Boolean(z?.isDarkColors?.()) }) + } + + onMounted(() => { + refresh() + const z = readHost() + if (z?.onThemeChange && !attached) { + attached = true + try { + z.onThemeChange((info) => refresh(info)) + } catch { + // optional + } + } + }) + + onUnmounted(() => { + attached = false + }) +} diff --git a/plugins/batch-start/src/env.d.ts b/plugins/batch-start/src/env.d.ts new file mode 100644 index 000000000..3f7c9d50d --- /dev/null +++ b/plugins/batch-start/src/env.d.ts @@ -0,0 +1,73 @@ +/// + +import type { + AppDoc, + CategoryDoc, + GroupDoc, + LaunchResult, + SettingsDoc, + UiPrefs, +} from './types' + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} + +export type GroupInput = { + name: string + cmds: string[] + appIds: string[] + order: number +} + +export interface BatchStartApi { + listApps: () => Promise + listCategories: () => Promise + listGroups: () => Promise + getSettings: () => Promise + patchUiPrefs: (patch: UiPrefs) => Promise + createCategory: (name: string) => Promise + renameCategory: (id: string, name: string) => Promise + deleteCategory: (id: string) => Promise + assignCategory: (appId: string, categoryId: string | null) => Promise + removeCategory: (appId: string, categoryId: string) => Promise + addManualApp: () => Promise + addCustomScanDir: () => Promise + removeCustomScanDir: (dir: string) => Promise + runScan: () => Promise + saveGroup: (input: GroupInput, id?: string) => Promise + deleteGroup: (groupId: string) => Promise + retrySyncGroup: (groupId: string) => Promise + reconcileFeatures: () => Promise + launchGroup: (groupId: string) => Promise + launchApp: (appId: string) => Promise +} + +declare global { + interface Window { + batchStart: BatchStartApi + ztools?: { + showToast?: (message: string, options?: Record) => unknown + setExpendHeight?: (height: number) => void + getThemeInfo?: () => { + isDark?: boolean + primaryColor?: string + customColor?: string + windowMaterial?: string + } + onThemeChange?: ( + callback: (info: { + isDark?: boolean + primaryColor?: string + customColor?: string + windowMaterial?: string + }) => void, + ) => void + isDarkColors?: () => boolean + } + } +} + +export {} diff --git a/plugins/batch-start/src/main.ts b/plugins/batch-start/src/main.ts new file mode 100644 index 000000000..fdbdce560 --- /dev/null +++ b/plugins/batch-start/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './styles.css' + +createApp(App).mount('#app') diff --git a/plugins/batch-start/src/preload.ts b/plugins/batch-start/src/preload.ts new file mode 100644 index 000000000..605966788 --- /dev/null +++ b/plugins/batch-start/src/preload.ts @@ -0,0 +1,288 @@ +import path from 'node:path' +import { + assignCategory as assignCategorySvc, + removeCategory as removeCategorySvc, + listApps as listAppsSvc, + upsertApp, +} from './services/appLibrary' +import { + createCategory as createCategorySvc, + deleteCategory as deleteCategorySvc, + listCategories as listCategoriesSvc, + renameCategory as renameCategorySvc, +} from './services/category' +import { + getDb, + getSettings as getSettingsSvc, + makeAppId, + newId, + patchUiPrefs as patchUiPrefsSvc, + type ZtoolsDb, +} from './services/db' +import { routePluginEnter } from './services/enterRouter' +import { + reconcileFeatures, + removeGroupFeature, + syncGroupFeature, + type FeatureApi, +} from './services/featureSync' +import { + deleteGroup as deleteGroupSvc, + getGroup, + listGroups as listGroupsSvc, + saveGroup as saveGroupSvc, + type GroupInput, +} from './services/launchGroup' +import { + countSuccessfulZtoolsLaunches, + launchGroupApps, + ZTOOLS_REDIRECT_SETTLE_MS, + type LaunchTarget, +} from './services/launcher' +import { runFullScan } from './services/scanner' +import { isZtoolsCommandPath } from './services/scanner/ztoolsPlugins' +import type { + AppDoc, + CategoryDoc, + GroupDoc, + LaunchResult, + Platform, + SettingsDoc, + UiPrefs, +} from './types' + +type ZtoolsHost = { + db: { promises: ZtoolsDb } + setFeature: FeatureApi['setFeature'] + removeFeature: FeatureApi['removeFeature'] + getFeatures: FeatureApi['getFeatures'] + showOpenDialog: (options: Record) => string[] | undefined + showToast: (message: string, options?: Record) => Promise | unknown + outPlugin: (isKill?: boolean) => Promise | unknown + onPluginEnter: (callback: (param: { code?: string }) => void) => void + onPluginOut: (callback: (isKill: boolean) => void) => void + shellOpenPath: (fullPath: string) => + | Promise<{ success: boolean; error?: string }> + | { success: boolean; error?: string } + redirect: (label: string | [string, string], payload?: string) => boolean + getFileIcon?: (filePath: string) => string | null +} + +function host(): ZtoolsHost { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (globalThis as any).ztools ?? (window as any).ztools +} + +function db(): ZtoolsDb { + return getDb() +} + +function featureApi(): FeatureApi { + const z = host() + return { + setFeature: (feature) => z.setFeature(feature), + removeFeature: (code) => z.removeFeature(code), + getFeatures: () => z.getFeatures?.() ?? [], + } +} + +function currentPlatform(): Platform { + const p = process.platform + if (p === 'darwin' || p === 'linux' || p === 'win32') return p + return 'win32' +} + +function manualOpenFilters(): Array<{ name: string; extensions: string[] }> | undefined { + if (process.platform === 'win32') { + return [{ name: 'Apps', extensions: ['exe', 'bat', 'cmd', 'lnk'] }] + } + return undefined +} + +function appToLaunchTarget(app: AppDoc): LaunchTarget | null { + if (app.source === 'ztools' || app.source === 'plugin' || isZtoolsCommandPath(app.path)) { + return { + key: app.path, + kind: 'ztools', + pluginTitle: app.pluginTitle || undefined, + launchCmd: app.launchCmd, + } + } + if (app.path) return { key: app.path, kind: 'path', path: app.path } + return null +} + +async function resolveGroupTargets(group: GroupDoc): Promise { + const apps = await listAppsSvc(db()) + const byId = new Map(apps.map((a) => [a._id, a])) + const targets: LaunchTarget[] = [] + for (const id of group.appIds) { + const app = byId.get(id) + if (!app) continue + const target = appToLaunchTarget(app) + if (target) targets.push(target) + } + return targets +} + +async function executeLaunch(targets: LaunchTarget[]): Promise { + const z = host() + return launchGroupApps( + targets, + (p) => z.shellOpenPath(p), + (label, payload) => z.redirect(label, payload), + ) +} + +async function launchGroupById(groupId: string): Promise { + const group = await getGroup(db(), groupId) + if (!group) { + return { success: 0, failed: 0, errors: [] } + } + return executeLaunch(await resolveGroupTargets(group)) +} + +async function launchAppById(appId: string): Promise { + const app = (await db().get(appId)) as AppDoc | null + if (!app) { + return { success: 0, failed: 1, errors: [{ path: appId, error: 'app not found' }] } + } + const target = appToLaunchTarget(app) + if (!target) { + return { success: 0, failed: 1, errors: [{ path: app.path || appId, error: 'not launchable' }] } + } + const result = await executeLaunch([target]) + // 管理页启动指令会 redirect;延迟退出以免打断目标插件,且不阻塞 toast + if (countSuccessfulZtoolsLaunches(result.outcomes ?? []) > 0) { + void (async () => { + await new Promise((r) => setTimeout(r, ZTOOLS_REDIRECT_SETTLE_MS)) + await Promise.resolve(host().outPlugin(true)) + })() + } + return result +} + +const batchStart = { + listApps: (): Promise => listAppsSvc(db()), + listCategories: (): Promise => listCategoriesSvc(db()), + listGroups: (): Promise => listGroupsSvc(db()), + getSettings: (): Promise => getSettingsSvc(db()), + patchUiPrefs: (patch: UiPrefs): Promise => patchUiPrefsSvc(patch, db()), + + createCategory: (name: string): Promise => createCategorySvc(db(), name), + renameCategory: (id: string, name: string): Promise => + renameCategorySvc(db(), id, name), + deleteCategory: (id: string): Promise => deleteCategorySvc(db(), id), + assignCategory: (appId: string, categoryId: string | null): Promise => + assignCategorySvc(db(), appId, categoryId), + removeCategory: (appId: string, categoryId: string): Promise => + removeCategorySvc(db(), appId, categoryId), + + async addManualApp(): Promise { + const z = host() + const selected = z.showOpenDialog({ + properties: ['openFile'], + filters: manualOpenFilters(), + }) + if (!selected?.length) return null + + const filePath = selected[0] + const name = path.basename(filePath, path.extname(filePath)) + const icon = z.getFileIcon?.(filePath) ?? null + return upsertApp(db(), { + _id: makeAppId(newId()), + name, + path: filePath, + icon, + source: 'manual', + categoryIds: [], + platform: currentPlatform(), + }) + }, + + async addCustomScanDir(): Promise { + const z = host() + const selected = z.showOpenDialog({ + properties: ['openDirectory'], + }) + if (!selected?.length) return null + + const dir = selected[0] + const settings = await getSettingsSvc(db()) + if (settings.customScanDirs.includes(dir)) return settings + + await db().put({ + ...settings, + customScanDirs: [...settings.customScanDirs, dir], + }) + return getSettingsSvc(db()) + }, + + async removeCustomScanDir(dir: string): Promise { + const settings = await getSettingsSvc(db()) + await db().put({ + ...settings, + customScanDirs: settings.customScanDirs.filter((d) => d !== dir), + }) + return getSettingsSvc(db()) + }, + + runScan: (): Promise => runFullScan(db()), + + async saveGroup(input: GroupInput, id?: string): Promise { + const group = await saveGroupSvc(db(), input, id) + return syncGroupFeature(group, featureApi(), db()) + }, + + async deleteGroup(groupId: string): Promise { + await deleteGroupSvc(db(), groupId) + await removeGroupFeature(groupId, featureApi()) + }, + + async retrySyncGroup(groupId: string): Promise { + const group = await getGroup(db(), groupId) + if (!group) return null + return syncGroupFeature(group, featureApi(), db()) + }, + + reconcileFeatures: (): Promise => reconcileFeatures(db(), featureApi()), + + launchGroup: (groupId: string): Promise => launchGroupById(groupId), + launchApp: (appId: string): Promise => launchAppById(appId), +} + +window.batchStart = batchStart + +// Do not keep the plugin resident in background after dismiss / group launch. +host().onPluginOut((isKill) => { + if (!isKill) { + void Promise.resolve(host().outPlugin(true)) + } +}) + +host().onPluginEnter(async (param) => { + try { + await reconcileFeatures(db(), featureApi()) + } catch { + // feature sync failure should not block launch / manage + } + const action = routePluginEnter(param?.code) + + if (action.type === 'launch-group') { + const result = await launchGroupById(action.groupId) + await Promise.resolve( + host().showToast(`成功 ${result.success} / 失败 ${result.failed}`), + ) + // redirect() switches the host "current" plugin to the target; calling + // outPlugin(true) immediately aborts that assembly (see main.log hide-view-abort-assembly). + if (countSuccessfulZtoolsLaunches(result.outcomes ?? []) > 0) { + await new Promise((r) => setTimeout(r, ZTOOLS_REDIRECT_SETTLE_MS)) + } + await Promise.resolve(host().outPlugin(true)) + return + } + + // manage: leave UI open until user exits (then onPluginOut kills) +}) + +export type BatchStartApi = typeof batchStart diff --git a/plugins/batch-start/src/services/appLibrary.ts b/plugins/batch-start/src/services/appLibrary.ts new file mode 100644 index 000000000..c171963d4 --- /dev/null +++ b/plugins/batch-start/src/services/appLibrary.ts @@ -0,0 +1,200 @@ +import type { AppDoc, AppSource, Platform } from '../types' +import { makeAppId, newId, type ZtoolsDb } from './db' +import { listGroups } from './launchGroup' +import { isZtoolsCommandPath } from './scanner/ztoolsPlugins' + +function normalizePath(path: string): string { + return path.toLowerCase() +} + +function isZtoolsSource(source: AppSource): boolean { + return source === 'ztools' || source === 'plugin' +} + +/** Migrate legacy single `categoryId` → `categoryIds` */ +export function normalizeApp(raw: AppDoc & { categoryId?: string | null }): AppDoc { + const legacyId = raw.categoryId + const fromArray = Array.isArray(raw.categoryIds) ? raw.categoryIds.filter(Boolean) : [] + const categoryIds = + fromArray.length > 0 + ? [...new Set(fromArray)] + : legacyId + ? [legacyId] + : [] + const { categoryId: _legacy, ...rest } = raw as AppDoc & { categoryId?: string | null } + return { ...rest, categoryIds } +} + +export async function upsertApp(db: ZtoolsDb, app: AppDoc): Promise { + const normalized = normalizeApp(app) + await db.put(normalized) + return (await db.get(normalized._id)) as AppDoc +} + +export async function listApps(db: ZtoolsDb): Promise { + const docs = (await db.allDocs('app:')) as Array + return docs.map(normalizeApp) +} + +/** + * Add app to a category (multi-membership). Pass `null` to clear all categories. + */ +export async function assignCategory( + db: ZtoolsDb, + appId: string, + categoryId: string | null, +): Promise { + const existing = (await db.get(appId)) as (AppDoc & { categoryId?: string | null }) | null + if (!existing) throw new Error(`App not found: ${appId}`) + const current = normalizeApp(existing) + if (categoryId == null) { + return upsertApp(db, { ...current, categoryIds: [] }) + } + if (current.categoryIds.includes(categoryId)) return current + return upsertApp(db, { + ...current, + categoryIds: [...current.categoryIds, categoryId], + }) +} + +/** Remove one category membership from an app. */ +export async function removeCategory( + db: ZtoolsDb, + appId: string, + categoryId: string, +): Promise { + const existing = (await db.get(appId)) as (AppDoc & { categoryId?: string | null }) | null + if (!existing) throw new Error(`App not found: ${appId}`) + const current = normalizeApp(existing) + if (!current.categoryIds.includes(categoryId)) return current + return upsertApp(db, { + ...current, + categoryIds: current.categoryIds.filter((id) => id !== categoryId), + }) +} + +export async function removeApp(db: ZtoolsDb, appId: string): Promise { + const existing = await db.get(appId) + if (existing) await db.remove(existing) +} + +export type ScannedApp = { + name: string + path: string + platform: Platform + icon?: string | null + source?: AppSource + pluginName?: string + pluginTitle?: string + launchCmd?: string | null + stableIdHint?: string +} + +async function writeDocs(db: ZtoolsDb, docs: AppDoc[]): Promise { + if (docs.length === 0) return + if (db.bulkDocs) { + await db.bulkDocs(docs) + return + } + const CHUNK = 40 + for (let i = 0; i < docs.length; i += CHUNK) { + const chunk = docs.slice(i, i + CHUNK) + await Promise.all(chunk.map((doc) => db.put(doc))) + } +} + +export async function mergeScannedApps( + db: ZtoolsDb, + scanned: ScannedApp[], +): Promise { + const existing = await listApps(db) + const byPath = new Map(existing.map((a) => [normalizePath(a.path), a])) + const toWrite: AppDoc[] = [] + + for (const item of scanned) { + const key = normalizePath(item.path) + const match = byPath.get(key) + const source: AppSource = item.source ?? 'scan' + + if (match) { + const updated: AppDoc = { + ...match, + platform: item.platform, + ...(item.icon !== undefined ? { icon: item.icon } : {}), + } + if ((match.source === 'scan' || isZtoolsSource(match.source)) && item.name) { + updated.name = item.name + } + if (isZtoolsSource(source)) { + updated.source = 'ztools' + if (item.pluginName) updated.pluginName = item.pluginName + if (item.pluginTitle) updated.pluginTitle = item.pluginTitle + if (item.launchCmd !== undefined) updated.launchCmd = item.launchCmd + } + toWrite.push(normalizeApp(updated)) + byPath.set(key, updated) + } else { + const id = item.stableIdHint + ? makeAppId(item.stableIdHint) + : isZtoolsSource(source) && item.pluginName && item.launchCmd + ? makeAppId(`cmd:${item.pluginName}:${encodeURIComponent(item.launchCmd)}`) + : makeAppId(newId()) + const doc: AppDoc = { + _id: id, + name: item.name, + path: item.path, + icon: item.icon ?? null, + source: isZtoolsSource(source) ? 'ztools' : source, + categoryIds: [], + platform: item.platform, + ...(isZtoolsSource(source) + ? { + pluginName: item.pluginName, + pluginTitle: item.pluginTitle, + launchCmd: item.launchCmd ?? null, + } + : {}), + } + toWrite.push(doc) + byPath.set(key, doc) + } + } + + await writeDocs(db, toWrite) + return listApps(db) +} + +/** + * Remove ZTools command apps no longer present; strip their ids from groups. + * Also drops legacy per-plugin (`source:'plugin'` / ztools-plugin://) rows. + */ +export async function reconcileMissingPluginApps( + db: ZtoolsDb, + presentCommandPaths: string[], +): Promise { + const present = new Set(presentCommandPaths.map(normalizePath)) + const apps = await listApps(db) + const orphans = apps.filter((a) => { + if (!isZtoolsSource(a.source) && !isZtoolsCommandPath(a.path)) return false + return !present.has(normalizePath(a.path)) + }) + if (orphans.length === 0) return [] + + const orphanIds = new Set(orphans.map((o) => o._id)) + for (const orphan of orphans) { + await removeApp(db, orphan._id) + } + + const groups = await listGroups(db) + for (const group of groups) { + const nextIds = group.appIds.filter((id) => !orphanIds.has(id)) + if (nextIds.length === group.appIds.length) continue + await db.put({ + ...group, + appIds: nextIds, + featureSynced: false, + }) + } + + return [...orphanIds] +} diff --git a/plugins/batch-start/src/services/category.ts b/plugins/batch-start/src/services/category.ts new file mode 100644 index 000000000..5fd3b48cb --- /dev/null +++ b/plugins/batch-start/src/services/category.ts @@ -0,0 +1,46 @@ +import type { CategoryDoc } from '../types' +import { makeCategoryId, newId, type ZtoolsDb } from './db' +import { listApps, upsertApp } from './appLibrary' + +export async function listCategories(db: ZtoolsDb): Promise { + const docs = (await db.allDocs('category:')) as CategoryDoc[] + return docs.sort((a, b) => a.order - b.order) +} + +export async function createCategory(db: ZtoolsDb, name: string): Promise { + const existing = await listCategories(db) + const maxOrder = existing.reduce((max, c) => Math.max(max, c.order), -1) + const doc: CategoryDoc = { + _id: makeCategoryId(newId()), + name, + order: maxOrder + 1, + } + await db.put(doc) + return (await db.get(doc._id)) as CategoryDoc +} + +export async function renameCategory( + db: ZtoolsDb, + id: string, + name: string, +): Promise { + const existing = (await db.get(id)) as CategoryDoc | null + if (!existing) throw new Error(`Category not found: ${id}`) + const updated: CategoryDoc = { ...existing, name } + await db.put(updated) + return (await db.get(id)) as CategoryDoc +} + +export async function deleteCategory(db: ZtoolsDb, id: string): Promise { + const existing = await db.get(id) + if (existing) await db.remove(existing) + + const apps = await listApps(db) + for (const app of apps) { + if (!app.categoryIds.includes(id)) continue + await upsertApp(db, { + ...app, + categoryIds: app.categoryIds.filter((cid) => cid !== id), + }) + } +} diff --git a/plugins/batch-start/src/services/db.ts b/plugins/batch-start/src/services/db.ts new file mode 100644 index 000000000..3378b5eeb --- /dev/null +++ b/plugins/batch-start/src/services/db.ts @@ -0,0 +1,72 @@ +import type { SettingsDoc } from '../types' + +export function makeAppId(uuid: string): string { + return `app:${uuid}` +} +export function makeCategoryId(uuid: string): string { + return `category:${uuid}` +} +export function makeGroupId(uuid: string): string { + return `group:${uuid}` +} + +export function newId(): string { + return crypto.randomUUID() +} + +export type ZtoolsDb = { + put: (doc: object) => Promise + get: (id: string) => Promise + remove: (docOrId: object | string) => Promise + allDocs: (key?: string) => Promise + bulkDocs?: (docs: object[]) => Promise +} + +export function getDb(): ZtoolsDb { + // @ts-expect-error host injects ztools in plugin runtime + const z = globalThis.ztools ?? (window as any).ztools + return z.db.promises +} + +const DEFAULT_UI = { + categoryCollapsed: true, + groupCollapsed: false, +} as const + +export async function getSettings(db: ZtoolsDb = getDb()): Promise { + const existing = (await db.get('settings')) as SettingsDoc | null + if (existing) { + return { + ...existing, + ui: { + categoryCollapsed: existing.ui?.categoryCollapsed ?? DEFAULT_UI.categoryCollapsed, + groupCollapsed: existing.ui?.groupCollapsed ?? DEFAULT_UI.groupCollapsed, + }, + } + } + const doc: SettingsDoc = { + _id: 'settings', + customScanDirs: [], + lastScanAt: null, + ui: { ...DEFAULT_UI }, + } + await db.put(doc) + return (await db.get('settings')) as SettingsDoc +} + +export async function patchUiPrefs( + patch: NonNullable, + db: ZtoolsDb = getDb(), +): Promise { + const settings = await getSettings(db) + const next: SettingsDoc = { + ...settings, + ui: { + categoryCollapsed: settings.ui?.categoryCollapsed ?? DEFAULT_UI.categoryCollapsed, + groupCollapsed: settings.ui?.groupCollapsed ?? DEFAULT_UI.groupCollapsed, + ...patch, + }, + } + await db.put(next) + return getSettings(db) +} diff --git a/plugins/batch-start/src/services/enterRouter.ts b/plugins/batch-start/src/services/enterRouter.ts new file mode 100644 index 000000000..44aed6c3f --- /dev/null +++ b/plugins/batch-start/src/services/enterRouter.ts @@ -0,0 +1,9 @@ +export type EnterAction = + | { type: 'manage' } + | { type: 'launch-group'; groupId: string } + +export function routePluginEnter(code: string | undefined): EnterAction { + if (!code || code === 'manage') return { type: 'manage' } + if (code.startsWith('group:')) return { type: 'launch-group', groupId: code } + return { type: 'manage' } +} diff --git a/plugins/batch-start/src/services/featureSync.ts b/plugins/batch-start/src/services/featureSync.ts new file mode 100644 index 000000000..26b99637d --- /dev/null +++ b/plugins/batch-start/src/services/featureSync.ts @@ -0,0 +1,65 @@ +import type { GroupDoc } from '../types' +import type { ZtoolsDb } from './db' +import { getGroup, listGroups } from './launchGroup' + +export type FeatureApi = { + setFeature: (feature: { + code: string + explain: string + cmds: string[] + }) => boolean | Promise + removeFeature: (code: string) => boolean | Promise + getFeatures?: () => Array<{ code: string }> | Promise> +} + +export async function syncGroupFeature( + group: GroupDoc, + api: FeatureApi, + db: ZtoolsDb, +): Promise { + let ok = false + try { + ok = !!(await api.setFeature({ + code: group._id, + explain: `启动组:${group.name}`, + cmds: group.cmds, + })) + } catch { + ok = false + } + + const updated: GroupDoc = { ...group, featureSynced: ok } + await db.put(updated) + return (await getGroup(db, group._id)) as GroupDoc +} + +export async function removeGroupFeature( + groupId: string, + api: FeatureApi, +): Promise { + await api.removeFeature(groupId) +} + +export async function reconcileFeatures( + db: ZtoolsDb, + api: FeatureApi, +): Promise { + const groups = await listGroups(db) + const groupIds = new Set(groups.map((g) => g._id)) + + const features = api.getFeatures ? await api.getFeatures() : [] + const groupFeatures = features.filter((f) => String(f.code).startsWith('group:')) + + for (const feature of groupFeatures) { + if (!groupIds.has(feature.code)) { + await api.removeFeature(feature.code) + } + } + + const featureCodes = new Set(groupFeatures.map((f) => f.code)) + for (const group of groups) { + if (!featureCodes.has(group._id) || !group.featureSynced) { + await syncGroupFeature(group, api, db) + } + } +} diff --git a/plugins/batch-start/src/services/launchGroup.ts b/plugins/batch-start/src/services/launchGroup.ts new file mode 100644 index 000000000..0800a12e6 --- /dev/null +++ b/plugins/batch-start/src/services/launchGroup.ts @@ -0,0 +1,75 @@ +import type { GroupDoc } from '../types' +import { makeGroupId, newId, type ZtoolsDb } from './db' + +export type GroupInput = { + name: string + cmds: string[] + appIds: string[] + order: number +} + +export async function listGroups(db: ZtoolsDb): Promise { + const docs = (await db.allDocs('group:')) as GroupDoc[] + return docs.sort((a, b) => a.order - b.order) +} + +export async function getGroup(db: ZtoolsDb, id: string): Promise { + return (await db.get(id)) as GroupDoc | null +} + +export async function deleteGroup(db: ZtoolsDb, id: string): Promise { + const existing = await db.get(id) + if (existing) await db.remove(existing) +} + +export async function saveGroup( + db: ZtoolsDb, + input: GroupInput, + id?: string, +): Promise { + const name = input.name.trim() + if (!name) throw new Error('Group name is required') + + if (!input.appIds || input.appIds.length < 1) { + throw new Error('Group must include at least one app') + } + + const cmds = + !input.cmds || input.cmds.length === 0 ? [name] : [...input.cmds] + + const existing = await listGroups(db) + for (const group of existing) { + if (id && group._id === id) continue + for (const cmd of cmds) { + if (group.cmds.includes(cmd)) { + throw new Error(`cmd conflict: "${cmd}"`) + } + } + } + + if (id) { + const prev = (await db.get(id)) as GroupDoc | null + if (!prev) throw new Error(`Group not found: ${id}`) + const updated: GroupDoc = { + ...prev, + name, + cmds, + appIds: [...input.appIds], + order: input.order, + featureSynced: false, + } + await db.put(updated) + return (await db.get(id)) as GroupDoc + } + + const doc: GroupDoc = { + _id: makeGroupId(newId()), + name, + cmds, + appIds: [...input.appIds], + order: input.order, + featureSynced: false, + } + await db.put(doc) + return (await db.get(doc._id)) as GroupDoc +} diff --git a/plugins/batch-start/src/services/launcher.ts b/plugins/batch-start/src/services/launcher.ts new file mode 100644 index 000000000..45593219a --- /dev/null +++ b/plugins/batch-start/src/services/launcher.ts @@ -0,0 +1,138 @@ +import type { LaunchResult } from '../types' +import { isZtoolsCommandPath } from './scanner/ztoolsPlugins' + +export type OpenPath = ( + fullPath: string, +) => Promise<{ success: boolean; error?: string }> | { success: boolean; error?: string } + +export type RedirectFn = ( + label: string | [string, string], + payload?: string, +) => boolean + +export type LaunchTarget = { + /** Used in error reporting */ + key: string + kind: 'path' | 'ztools' + path?: string + pluginTitle?: string + launchCmd?: string | null +} + +export async function launchPaths(paths: string[], openPath: OpenPath): Promise { + return launchTargets( + paths.map((path) => + isZtoolsCommandPath(path) + ? { key: path, kind: 'ztools' as const, launchCmd: null } + : { key: path, kind: 'path' as const, path }, + ), + { openPath }, + ) +} + +export type LaunchOutcome = { + path: string + ok: boolean + error: string + kind: LaunchTarget['kind'] +} + +/** After redirect, host needs a beat before outPlugin or it aborts the target plugin mid-assembly. */ +export const ZTOOLS_REDIRECT_SETTLE_MS = 2000 + +export function countSuccessfulZtoolsLaunches(outcomes: LaunchOutcome[]): number { + return outcomes.filter((o) => o.ok && o.kind === 'ztools').length +} + +async function launchOne( + target: LaunchTarget, + deps: { openPath: OpenPath; redirect?: RedirectFn }, +): Promise { + try { + if (target.kind === 'ztools') { + const cmd = target.launchCmd?.trim() + if (!cmd) { + return { + path: target.key, + ok: false, + error: 'command has no divertible text', + kind: 'ztools', + } + } + const redirect = deps.redirect + if (!redirect) { + return { path: target.key, ok: false, error: 'redirect API unavailable', kind: 'ztools' } + } + const title = target.pluginTitle?.trim() + const label: string | [string, string] = title ? [title, cmd] : cmd + const ok = redirect(label) + if (!ok) { + return { path: target.key, ok: false, error: 'redirect failed', kind: 'ztools' } + } + return { path: target.key, ok: true, error: '', kind: 'ztools' } + } + + const fullPath = target.path + if (!fullPath) { + return { path: target.key, ok: false, error: 'missing path', kind: 'path' } + } + const res = await Promise.resolve(deps.openPath(fullPath)) + if (!res?.success) { + return { path: target.key, ok: false, error: res?.error || 'open failed', kind: 'path' } + } + return { path: target.key, ok: true, error: '', kind: 'path' } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + return { path: target.key, ok: false, error: message, kind: target.kind } + } +} + +export async function launchTargets( + targets: LaunchTarget[], + deps: { + openPath: OpenPath + redirect?: RedirectFn + }, +): Promise { + // Paths can run in parallel; ZTools redirects must be sequential — each redirect + // becomes the host "current" plugin view and would otherwise race. + const pathTargets = targets.filter((t) => t.kind === 'path') + const ztoolsTargets = targets.filter((t) => t.kind === 'ztools') + + const pathOutcomes = await Promise.all(pathTargets.map((t) => launchOne(t, deps))) + const ztoolsOutcomes: LaunchOutcome[] = [] + for (const target of ztoolsTargets) { + ztoolsOutcomes.push(await launchOne(target, deps)) + } + + const outcomes = [...pathOutcomes, ...ztoolsOutcomes] + const errors = outcomes.filter((s) => !s.ok).map((s) => ({ path: s.path, error: s.error })) + return { + success: outcomes.filter((s) => s.ok).length, + failed: errors.length, + errors, + outcomes, + } +} + +export async function launchGroupApps( + targets: LaunchTarget[] | string[], + openPath: OpenPath = (p) => { + // @ts-expect-error host + return ztools.shellOpenPath(p) + }, + redirect: RedirectFn = (label, payload) => { + // @ts-expect-error host + return ztools.redirect(label, payload) + }, +): Promise { + const normalized: LaunchTarget[] = targets.map((t) => { + if (typeof t === 'string') { + return isZtoolsCommandPath(t) + ? { key: t, kind: 'ztools', launchCmd: null } + : { key: t, kind: 'path', path: t } + } + return t + }) + return launchTargets(normalized, { openPath, redirect }) +} diff --git a/plugins/batch-start/src/services/scanner/filter.ts b/plugins/batch-start/src/services/scanner/filter.ts new file mode 100644 index 000000000..01df1edca --- /dev/null +++ b/plugins/batch-start/src/services/scanner/filter.ts @@ -0,0 +1,16 @@ +/** + * Start Menu / installers often ship "Uninstall Xxx" shortcuts. + * These should not appear in the app library. + */ +export function isUninstallEntry(name: string, appPath: string = ''): boolean { + const text = `${name} ${appPath}`.toLowerCase() + if (text.includes('uninstall') || text.includes('卸载')) return true + // Inno Setup / common uninstaller executables + if (/unins\d{0,3}\.exe$/i.test(appPath)) return true + if (/(^|[\\/])uninstall\.exe$/i.test(appPath)) return true + return false +} + +export function rejectUninstallApps(apps: T[]): T[] { + return apps.filter((a) => !isUninstallEntry(a.name, a.path)) +} diff --git a/plugins/batch-start/src/services/scanner/index.ts b/plugins/batch-start/src/services/scanner/index.ts new file mode 100644 index 000000000..2cc6194c3 --- /dev/null +++ b/plugins/batch-start/src/services/scanner/index.ts @@ -0,0 +1,108 @@ +import { listApps, mergeScannedApps, reconcileMissingPluginApps } from '../appLibrary' +import { getSettings, type ZtoolsDb } from '../db' +import { listGroups } from '../launchGroup' +import type { Platform } from '../../types' +import { rejectUninstallApps } from './filter' +import { createLinuxScanner } from './linux' +import { createMacScanner } from './mac' +import type { PlatformScanner, ScannedApp } from './types' +import { createWinScanner } from './win' +import { + BATCH_START_MANAGE_CMDS, + scanZtoolsCommands, +} from './ztoolsPlugins' + +export type { PlatformScanner, ScannedApp } from './types' +export { createWinScanner } from './win' +export { createMacScanner } from './mac' +export { createLinuxScanner } from './linux' +export { isUninstallEntry, rejectUninstallApps } from './filter' +export { + scanZtoolsCommands, + scanZtoolsPlugins, + listTextCmds, + pickFirstLaunchCmd, + commandAppPath, + pluginAppPath, + EXCLUDED_PLUGIN_NAMES, + BATCH_START_MANAGE_CMDS, + shouldSkipPlugin, +} from './ztoolsPlugins' + +export function getScanner(platform: string = process.platform): PlatformScanner { + switch (platform) { + case 'darwin': + return createMacScanner() + case 'linux': + return createLinuxScanner() + case 'win32': + default: + return createWinScanner() + } +} + +function asPlatform(platform: string): Platform { + if (platform === 'darwin' || platform === 'linux' || platform === 'win32') return platform + return 'win32' +} + +/** Manage cmds + cmds from launch groups registered by this plugin */ +export async function collectBatchStartOwnedCmds(db: ZtoolsDb): Promise> { + const owned = new Set(BATCH_START_MANAGE_CMDS) + const groups = await listGroups(db) + for (const group of groups) { + for (const cmd of group.cmds) { + const text = String(cmd).trim() + if (text) owned.add(text) + } + } + return owned +} + +export type RunFullScanDeps = { + scanner?: PlatformScanner + now?: () => number + /** Injected for tests; defaults to scanning plugin manifests for text cmds */ + scanZtoolsCommands?: ( + platform: Platform, + excludeCmds?: Set, + ) => Promise + /** @deprecated use scanZtoolsCommands */ + scanZtoolsPlugins?: (platform: Platform) => Promise +} + +export async function runFullScan( + db: ZtoolsDb, + platform: string = process.platform, + deps: RunFullScanDeps = {}, +): Promise>> { + const scanner = deps.scanner ?? getScanner(platform) + const settings = await getSettings(db) + const hostPlatform = asPlatform(platform) + + const scanned: ScannedApp[] = rejectUninstallApps([...(await scanner.scanSystem())]) + for (const dir of settings.customScanDirs) { + scanned.push(...rejectUninstallApps(await scanner.scanCustomDir(dir))) + } + + const excludeCmds = await collectBatchStartOwnedCmds(db) + let commands: ScannedApp[] + if (deps.scanZtoolsCommands) { + commands = await deps.scanZtoolsCommands(hostPlatform, excludeCmds) + } else if (deps.scanZtoolsPlugins) { + commands = await deps.scanZtoolsPlugins(hostPlatform) + } else { + commands = await scanZtoolsCommands(hostPlatform, { excludeCmds }) + } + scanned.push(...commands) + + await mergeScannedApps(db, scanned) + await reconcileMissingPluginApps( + db, + commands.map((c) => c.path), + ) + + const lastScanAt = deps.now?.() ?? Date.now() + await db.put({ ...settings, lastScanAt }) + return listApps(db) +} diff --git a/plugins/batch-start/src/services/scanner/linux.ts b/plugins/batch-start/src/services/scanner/linux.ts new file mode 100644 index 000000000..8bf00f7ef --- /dev/null +++ b/plugins/batch-start/src/services/scanner/linux.ts @@ -0,0 +1,177 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import type { PlatformScanner, ScannedApp } from './types' + +export type LinuxStatLike = { + isFile(): boolean + isDirectory(): boolean + mode?: number +} + +export type LinuxScannerDeps = { + readdir?: (dir: string) => Promise + stat?: (filePath: string) => Promise + readFile?: (filePath: string, encoding?: BufferEncoding) => Promise + homedir?: () => string + join?: (...parts: string[]) => string +} + +function isSkippableScanError(err: unknown): boolean { + if (!err || typeof err !== 'object' || !('code' in err)) return false + const code = (err as { code?: string }).code + return code === 'ENOENT' || code === 'EACCES' || code === 'EPERM' +} + +function isExecutable(st: LinuxStatLike): boolean { + return typeof st.mode === 'number' && (st.mode & 0o111) !== 0 +} + +/** Strip desktop Exec field codes and return the first command token. */ +export function parseDesktopExec(execLine: string): string | null { + const stripped = execLine.replace(/%[fFuUdDnNickvm]/g, '').trim() + if (!stripped) return null + + if (stripped.startsWith('"')) { + const end = stripped.indexOf('"', 1) + if (end > 1) return stripped.slice(1, end) + } + + const token = stripped.split(/\s+/)[0] + return token || null +} + +export function parseDesktopFile(content: string): { name: string; exec: string } | null { + const lines = content.split(/\r?\n/) + let inEntry = false + let name: string | undefined + let exec: string | undefined + + for (const raw of lines) { + const line = raw.trim() + if (!line || line.startsWith('#')) continue + if (line.startsWith('[')) { + inEntry = line === '[Desktop Entry]' + continue + } + if (!inEntry) continue + + const eq = line.indexOf('=') + if (eq <= 0) continue + const key = line.slice(0, eq) + const value = line.slice(eq + 1) + if (key === 'Name' && name === undefined) name = value + if (key === 'Exec' && exec === undefined) exec = value + } + + if (!name || !exec) return null + const parsedExec = parseDesktopExec(exec) + if (!parsedExec) return null + return { name, exec: parsedExec } +} + +export function createLinuxScanner(deps: LinuxScannerDeps = {}): PlatformScanner { + const readdir = deps.readdir ?? ((dir) => fs.readdir(dir)) + const stat = deps.stat ?? ((filePath) => fs.stat(filePath)) + const readFile = + deps.readFile ?? ((filePath, encoding = 'utf8') => fs.readFile(filePath, encoding)) + const homedir = deps.homedir ?? (() => os.homedir()) + const join = deps.join ?? path.join + + async function scanDesktopDir(dir: string): Promise { + const out: ScannedApp[] = [] + + let entries: string[] + try { + entries = await readdir(dir) + } catch (err) { + if (isSkippableScanError(err)) return [] + throw err + } + + for (const entry of entries) { + if (!entry.toLowerCase().endsWith('.desktop')) continue + const full = join(dir, entry) + let st: LinuxStatLike + try { + st = await stat(full) + } catch (err) { + if (isSkippableScanError(err)) continue + throw err + } + if (!st.isFile()) continue + + let content: string + try { + content = await readFile(full, 'utf8') + } catch (err) { + if (isSkippableScanError(err)) continue + throw err + } + + const parsed = parseDesktopFile(content) + if (!parsed) continue + out.push({ + name: parsed.name, + path: parsed.exec, + platform: 'linux', + }) + } + + return out + } + + async function scanExecutables(dir: string): Promise { + const out: ScannedApp[] = [] + + let entries: string[] + try { + entries = await readdir(dir) + } catch (err) { + if (isSkippableScanError(err)) return [] + throw err + } + + for (const entry of entries) { + const full = join(dir, entry) + let st: LinuxStatLike + try { + st = await stat(full) + } catch (err) { + if (isSkippableScanError(err)) continue + throw err + } + if (!st.isFile() || !isExecutable(st)) continue + out.push({ + name: entry, + path: full, + platform: 'linux', + }) + } + + return out + } + + return { + async scanSystem() { + const roots = [ + '/usr/share/applications', + join(homedir(), '.local', 'share', 'applications'), + ] + const apps: ScannedApp[] = [] + for (const root of roots) { + apps.push(...(await scanDesktopDir(root))) + } + return apps + }, + + async scanCustomDir(dir: string) { + try { + return await scanExecutables(dir) + } catch (err) { + if (isSkippableScanError(err)) return [] + throw err + } + }, + } +} diff --git a/plugins/batch-start/src/services/scanner/mac.ts b/plugins/batch-start/src/services/scanner/mac.ts new file mode 100644 index 000000000..b560566a0 --- /dev/null +++ b/plugins/batch-start/src/services/scanner/mac.ts @@ -0,0 +1,103 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import type { PlatformScanner, ScannedApp } from './types' + +export type MacStatLike = { + isFile(): boolean + isDirectory(): boolean + mode?: number +} + +export type MacScannerDeps = { + readdir?: (dir: string) => Promise + stat?: (filePath: string) => Promise + homedir?: () => string + join?: (...parts: string[]) => string +} + +function isSkippableScanError(err: unknown): boolean { + if (!err || typeof err !== 'object' || !('code' in err)) return false + const code = (err as { code?: string }).code + return code === 'ENOENT' || code === 'EACCES' || code === 'EPERM' +} + +function isExecutable(st: MacStatLike): boolean { + return typeof st.mode === 'number' && (st.mode & 0o111) !== 0 +} + +function appNameFromBundle(bundlePath: string): string { + return path.basename(bundlePath, '.app') +} + +export function createMacScanner(deps: MacScannerDeps = {}): PlatformScanner { + const readdir = deps.readdir ?? ((dir) => fs.readdir(dir)) + const stat = deps.stat ?? ((filePath) => fs.stat(filePath)) + const homedir = deps.homedir ?? (() => os.homedir()) + const join = deps.join ?? path.join + + async function collectApps( + dir: string, + options: { includeExecutables: boolean }, + ): Promise { + const out: ScannedApp[] = [] + + let entries: string[] + try { + entries = await readdir(dir) + } catch (err) { + if (isSkippableScanError(err)) return [] + throw err + } + + for (const entry of entries) { + const full = join(dir, entry) + let st: MacStatLike + try { + st = await stat(full) + } catch (err) { + if (isSkippableScanError(err)) continue + throw err + } + + if (st.isDirectory() && entry.toLowerCase().endsWith('.app')) { + out.push({ + name: appNameFromBundle(full), + path: full, + platform: 'darwin', + }) + continue + } + + if (options.includeExecutables && st.isFile() && isExecutable(st)) { + out.push({ + name: entry, + path: full, + platform: 'darwin', + }) + } + } + + return out + } + + return { + async scanSystem() { + const roots = ['/Applications', join(homedir(), 'Applications')] + const apps: ScannedApp[] = [] + for (const root of roots) { + apps.push(...(await collectApps(root, { includeExecutables: false }))) + } + return apps + }, + + async scanCustomDir(dir: string) { + try { + return await collectApps(dir, { includeExecutables: true }) + } catch (err) { + if (isSkippableScanError(err)) return [] + throw err + } + }, + } +} diff --git a/plugins/batch-start/src/services/scanner/types.ts b/plugins/batch-start/src/services/scanner/types.ts new file mode 100644 index 000000000..6a03fcb18 --- /dev/null +++ b/plugins/batch-start/src/services/scanner/types.ts @@ -0,0 +1,19 @@ +import type { AppSource, Platform } from '../../types' + +export interface ScannedApp { + name: string + path: string + platform: Platform + icon?: string | null + source?: AppSource + pluginName?: string + pluginTitle?: string + launchCmd?: string | null + /** When set, used as makeAppId(...) for stable ids */ + stableIdHint?: string +} + +export interface PlatformScanner { + scanSystem(): Promise + scanCustomDir(dir: string): Promise +} diff --git a/plugins/batch-start/src/services/scanner/win.ts b/plugins/batch-start/src/services/scanner/win.ts new file mode 100644 index 000000000..757e267b9 --- /dev/null +++ b/plugins/batch-start/src/services/scanner/win.ts @@ -0,0 +1,238 @@ +import { execFile } from 'node:child_process' +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { promisify } from 'node:util' +import type { PlatformScanner, ScannedApp } from './types' +import { isUninstallEntry, rejectUninstallApps } from './filter' + +const execFileAsync = promisify(execFile) + +const CUSTOM_MAX_DEPTH = 4 +const LAUNCH_EXTS = new Set(['.exe', '.bat', '.cmd', '.lnk']) +/** Resolve shortcuts in batches to avoid one PowerShell process per .lnk */ +const LNK_BATCH_SIZE = 80 + +export type WinDirentLike = { + name: string + isFile(): boolean + isDirectory(): boolean +} + +export type WinScannerDeps = { + readdir?: (dir: string) => Promise + resolveLnks?: (lnkPaths: string[]) => Promise> + env?: NodeJS.ProcessEnv + join?: (...parts: string[]) => string +} + +function displayName(filePath: string): string { + return path.basename(filePath, path.extname(filePath)) +} + +function isSkippableScanError(err: unknown): boolean { + if (!err || typeof err !== 'object' || !('code' in err)) return false + const code = (err as { code?: string }).code + return code === 'ENOENT' || code === 'EACCES' || code === 'EPERM' +} + +/** + * Batch-resolve .lnk targets via one PowerShell process. + * Names come from Node paths (correct Unicode); only TargetPath is read from COM. + * Results are written to a UTF-8 file to avoid GBK console mojibake on Chinese Windows. + */ +export async function defaultResolveLnks(lnkPaths: string[]): Promise> { + const map = new Map() + if (lnkPaths.length === 0) return map + + for (let i = 0; i < lnkPaths.length; i += LNK_BATCH_SIZE) { + const chunk = lnkPaths.slice(i, i + LNK_BATCH_SIZE) + const part = await resolveLnkChunk(chunk) + for (const [k, v] of part) map.set(k, v) + } + return map +} + +async function resolveLnkChunk(lnkPaths: string[]): Promise> { + const map = new Map() + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ztools-lnk-')) + const inFile = path.join(tmpDir, 'in.json') + const outFile = path.join(tmpDir, 'out.json') + + try { + await fs.writeFile(inFile, JSON.stringify(lnkPaths), 'utf8') + + const script = ` +$ErrorActionPreference = 'SilentlyContinue' +$inFile = ${JSON.stringify(inFile)} +$outFile = ${JSON.stringify(outFile)} +$utf8 = New-Object System.Text.UTF8Encoding $false +$raw = [System.IO.File]::ReadAllText($inFile, $utf8) +$paths = $raw | ConvertFrom-Json +$shell = New-Object -ComObject WScript.Shell +$list = New-Object System.Collections.Generic.List[object] +foreach ($p in $paths) { + try { + $s = $shell.CreateShortcut([string]$p) + $t = [string]$s.TargetPath + if (-not [string]::IsNullOrWhiteSpace($t)) { + $list.Add([pscustomobject]@{ path = [string]$p; target = $t }) | Out-Null + } + } catch {} +} +$json = ($list | ConvertTo-Json -Compress -Depth 3) +if (-not $json) { $json = '[]' } +if ($json[0] -ne '[') { $json = '[' + $json + ']' } +[System.IO.File]::WriteAllText($outFile, $json, $utf8) +` + + await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], + { windowsHide: true, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, + ) + + const outRaw = await fs.readFile(outFile, 'utf8') + const parsed = JSON.parse(outRaw || '[]') as Array<{ path?: string; target?: string }> + const rows = Array.isArray(parsed) ? parsed : [parsed] + for (const row of rows) { + if (row?.path && row?.target) map.set(row.path, row.target) + } + } catch { + // batch failed — leave map empty for this chunk + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined) + } + + return map +} + +async function defaultReaddir(dir: string): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }) + return entries.map((e) => ({ + name: e.name, + isFile: () => e.isFile(), + isDirectory: () => e.isDirectory(), + })) +} + +export function createWinScanner(deps: WinScannerDeps = {}): PlatformScanner { + const readdir = deps.readdir ?? defaultReaddir + const resolveLnks = deps.resolveLnks ?? defaultResolveLnks + const env = deps.env ?? process.env + const join = deps.join ?? path.join + + async function collectEntries( + dir: string, + options: { maxDepth: number; lnkOnly: boolean }, + ): Promise<{ files: ScannedApp[]; lnkPaths: string[] }> { + const files: ScannedApp[] = [] + const lnkPaths: string[] = [] + + async function walk(current: string, depth: number): Promise { + if (depth > options.maxDepth) return + + let entries: WinDirentLike[] + try { + entries = await readdir(current) + } catch (err) { + if (isSkippableScanError(err)) return + throw err + } + + const subdirs: string[] = [] + for (const entry of entries) { + const full = join(current, entry.name) + if (entry.isDirectory()) { + subdirs.push(full) + continue + } + if (!entry.isFile()) continue + + const ext = path.extname(entry.name).toLowerCase() + if (options.lnkOnly) { + if (ext !== '.lnk') continue + } else if (!LAUNCH_EXTS.has(ext)) { + continue + } + + if (ext === '.lnk') { + // Skip uninstall shortcuts before expensive COM resolve + if (isUninstallEntry(displayName(entry.name), full)) continue + lnkPaths.push(full) + } else { + const name = displayName(entry.name) + if (isUninstallEntry(name, full)) continue + files.push({ + name, + path: full, + platform: 'win32', + }) + } + } + + // Walk subdirectories with limited concurrency + const CONCURRENCY = 8 + for (let i = 0; i < subdirs.length; i += CONCURRENCY) { + const batch = subdirs.slice(i, i + CONCURRENCY) + await Promise.all(batch.map((d) => walk(d, depth + 1))) + } + } + + await walk(dir, 0) + return { files, lnkPaths } + } + + async function collectFromDir( + dir: string, + options: { maxDepth: number; lnkOnly: boolean }, + ): Promise { + const { files, lnkPaths } = await collectEntries(dir, options) + if (lnkPaths.length === 0) return files + + const targets = await resolveLnks(lnkPaths) + const fromLnks: ScannedApp[] = [] + for (const lnk of lnkPaths) { + const target = targets.get(lnk) + if (!target) continue + const name = displayName(lnk) + if (isUninstallEntry(name, target)) continue + // Display name from Node path — preserves Chinese correctly + fromLnks.push({ + name, + path: target, + platform: 'win32', + }) + } + return rejectUninstallApps([...files, ...fromLnks]) + } + + return { + async scanSystem() { + const roots: string[] = [] + if (env.APPDATA) { + roots.push(join(env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs')) + } + const programData = env.ProgramData ?? env.PROGRAMDATA + if (programData) { + roots.push(join(programData, 'Microsoft', 'Windows', 'Start Menu', 'Programs')) + } + + const results = await Promise.all( + roots.map((root) => + collectFromDir(root, { maxDepth: Number.POSITIVE_INFINITY, lnkOnly: true }), + ), + ) + return results.flat() + }, + + async scanCustomDir(dir: string) { + try { + return await collectFromDir(dir, { maxDepth: CUSTOM_MAX_DEPTH, lnkOnly: false }) + } catch (err) { + if (isSkippableScanError(err)) return [] + throw err + } + }, + } +} diff --git a/plugins/batch-start/src/services/scanner/ztoolsPlugins.ts b/plugins/batch-start/src/services/scanner/ztoolsPlugins.ts new file mode 100644 index 000000000..528375ebc --- /dev/null +++ b/plugins/batch-start/src/services/scanner/ztoolsPlugins.ts @@ -0,0 +1,373 @@ +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import type { Platform } from '../../types' +import type { ScannedApp } from './types' + +/** One list item per divertible text command */ +export const CMD_URI_PREFIX = 'ztools-cmd://' +/** Legacy per-plugin URI from 0.1.1 — treated as stale */ +export const LEGACY_PLUGIN_URI_PREFIX = 'ztools-plugin://' + +export const EXCLUDED_PLUGIN_NAMES = new Set(['setting', 'system', 'batch-start']) + +/** Static manage cmds declared in batch-start plugin.json */ +export const BATCH_START_MANAGE_CMDS = new Set(['批量启动', '应用启动组']) + +/** Skip setting/system, batch-start (and variants), and title「批量启动」 */ +export function shouldSkipPlugin(pluginName: string, pluginTitle?: string): boolean { + const name = pluginName.trim().toLowerCase() + if (name === 'setting' || name === 'system') return true + if (name === 'batch-start' || name.startsWith('batch-start')) return true + if (EXCLUDED_PLUGIN_NAMES.has(name)) return true + if (pluginTitle?.trim() === '批量启动') return true + return false +} + +/** @deprecated use shouldSkipPlugin */ +export function isExcludedBatchStartPlugin(pluginName: string, pluginTitle?: string): boolean { + return shouldSkipPlugin(pluginName, pluginTitle) +} + +export function commandAppPath(pluginName: string, cmd: string): string { + return `${CMD_URI_PREFIX}${pluginName}/${encodeURIComponent(cmd)}` +} + +export function isZtoolsCommandPath(appPath: string): boolean { + const lower = appPath.toLowerCase() + return lower.startsWith(CMD_URI_PREFIX) || lower.startsWith(LEGACY_PLUGIN_URI_PREFIX) +} + +/** @deprecated use commandAppPath */ +export function pluginAppPath(pluginName: string): string { + return `${LEGACY_PLUGIN_URI_PREFIX}${pluginName}` +} + +/** @deprecated use isZtoolsCommandPath */ +export function isPluginAppPath(appPath: string): boolean { + return isZtoolsCommandPath(appPath) +} + +export type ListTextCmdsOptions = { + /** Extra cmd strings to skip (e.g. launch-group cmds registered by this plugin) */ + excludeCmds?: Set | ReadonlySet + /** Skip features whose code starts with group: (dynamic launch-group features) */ + skipGroupFeatures?: boolean +} + +export function listTextCmds(features: unknown, options: ListTextCmdsOptions = {}): string[] { + if (!Array.isArray(features)) return [] + const exclude = options.excludeCmds + const skipGroup = options.skipGroupFeatures !== false + const seen = new Set() + const cmds: string[] = [] + for (const feature of features) { + const code = (feature as { code?: unknown })?.code + if (skipGroup && typeof code === 'string' && code.startsWith('group:')) continue + const list = (feature as { cmds?: unknown })?.cmds + if (!Array.isArray(list)) continue + for (const cmd of list) { + if (typeof cmd !== 'string') continue + const text = cmd.trim() + if (!text || seen.has(text)) continue + if (BATCH_START_MANAGE_CMDS.has(text)) continue + if (exclude?.has(text)) continue + seen.add(text) + cmds.push(text) + } + } + return cmds +} + +/** @deprecated use listTextCmds()[0] */ +export function pickFirstLaunchCmd(features: unknown): string | null { + return listTextCmds(features)[0] ?? null +} + +type PluginJson = { + name?: string + title?: string + version?: string + logo?: string + features?: unknown +} + +export type InstalledPluginInfo = { + name: string + title: string + path: string + version?: string + logo?: string + features?: unknown +} + +export type ScanZtoolsCommandsDeps = { + pluginsRoot?: string + readDir?: (dir: string) => Promise + readFile?: (file: string, encoding: 'utf-8') => Promise + access?: (file: string) => Promise + /** Cmds to exclude (batch-start manage cmds + registered launch-group cmds) */ + excludeCmds?: Set | ReadonlySet + /** + * Preferred source of truth: currently installed plugins from ZTools registry. + * When provided (or auto-loaded via ztools.internal), disk leftovers are ignored. + */ + listInstalledPlugins?: () => Promise +} + +async function resolveLogoIcon( + pluginDir: string, + logo: string | undefined, + access: (file: string) => Promise, +): Promise { + if (!logo || logo.includes('://')) return null + const logoPath = path.isAbsolute(logo) ? logo : path.join(pluginDir, logo) + try { + await access(logoPath) + return pathToFileURL(logoPath).href + } catch { + return null + } +} + +function stableCmdId(pluginName: string, cmd: string): string { + return `cmd:${pluginName}:${encodeURIComponent(cmd)}` +} + +function compareSemver(a: string, b: string): number { + const pa = a.split('.').map((x) => parseInt(x, 10) || 0) + const pb = b.split('.').map((x) => parseInt(x, 10) || 0) + const n = Math.max(pa.length, pb.length) + for (let i = 0; i < n; i++) { + const d = (pa[i] ?? 0) - (pb[i] ?? 0) + if (d !== 0) return d + } + return 0 +} + +/** Parse version from artifact name like `foo-1.2.3-abcd1234.asar` */ +export function versionFromArtifactName(entry: string): string | null { + const base = entry.replace(/\.asar$/i, '') + const m = base.match(/-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)-[0-9a-f]{6,}$/i) + return m?.[1] ?? null +} + +/** + * Load installed plugins from ZTools host registry (requires internal API privilege). + * Returns null when unavailable / denied — callers should fall back carefully. + */ +export async function loadInstalledPluginsFromHost(): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const z = (globalThis as any).ztools ?? (typeof window !== 'undefined' ? (window as any).ztools : null) + if (!z?.internal) return null + const raw = + (await z.internal.getPlugins?.()) ?? (await z.internal.getAllPlugins?.()) ?? null + if (!Array.isArray(raw)) return null + const installed: InstalledPluginInfo[] = [] + for (const p of raw as Record[]) { + const name = typeof p.name === 'string' ? p.name.trim() : '' + if (!name) continue + const info: InstalledPluginInfo = { + name, + title: typeof p.title === 'string' && p.title.trim() ? p.title.trim() : name, + path: typeof p.path === 'string' ? p.path : '', + features: p.features, + } + if (typeof p.version === 'string') info.version = p.version + if (typeof p.logo === 'string') info.logo = p.logo + installed.push(info) + } + return installed + } catch { + return null + } +} + +function appsFromPlugin( + platform: Platform, + plugin: { + name: string + title: string + path: string + logo?: string | null + features?: unknown + }, + excludeCmds: Set | ReadonlySet | undefined, + icon: string | null, +): ScannedApp[] { + if (shouldSkipPlugin(plugin.name, plugin.title)) return [] + const cmds = listTextCmds(plugin.features, { + excludeCmds, + skipGroupFeatures: true, + }) + return cmds.map((cmd) => ({ + name: cmd, + path: commandAppPath(plugin.name, cmd), + platform, + icon, + source: 'ztools' as const, + pluginName: plugin.name, + pluginTitle: plugin.title, + launchCmd: cmd, + stableIdHint: stableCmdId(plugin.name, cmd), + })) +} + +async function readPluginJson( + pluginPath: string, + readFile: (file: string, encoding: 'utf-8') => Promise, +): Promise { + try { + const raw = await readFile(path.join(pluginPath, 'plugin.json'), 'utf-8') + return JSON.parse(raw) as PluginJson + } catch { + return null + } +} + +/** + * Disk fallback: read directory plugins + `.asar` packages. + * Skips `.asar.unpacked` / `.installing`. Dedupes by plugin name (newest version wins) + * so upgrade leftovers don't flood the list — but cannot know "uninstalled" without registry. + */ +export async function scanPluginsFromDisk( + platform: Platform, + deps: ScanZtoolsCommandsDeps = {}, +): Promise { + const pluginsRoot = deps.pluginsRoot ?? path.join(os.homedir(), '.ztools', 'plugins') + const readDir = deps.readDir ?? ((dir) => fs.readdir(dir)) + const readFile = deps.readFile ?? ((file, enc) => fs.readFile(file, enc)) + const access = deps.access ?? ((file) => fs.access(file)) + + let entries: string[] + try { + entries = await readDir(pluginsRoot) + } catch { + return [] + } + + type Cand = { + name: string + title: string + version: string + root: string + logo?: string + features?: unknown + } + const bestByName = new Map() + + for (const entry of entries) { + if (entry === '.installing' || entry.endsWith('.asar.unpacked')) continue + const root = path.join(pluginsRoot, entry) + const json = await readPluginJson(root, readFile) + if (!json) continue + + const pluginName = typeof json.name === 'string' ? json.name.trim() : '' + if (!pluginName || shouldSkipPlugin(pluginName, json.title)) continue + + const title = + typeof json.title === 'string' && json.title.trim() ? json.title.trim() : pluginName + const version = + (typeof json.version === 'string' && json.version.trim()) || + versionFromArtifactName(entry) || + '0.0.0' + + const prev = bestByName.get(pluginName) + if (prev && compareSemver(version, prev.version) < 0) continue + + bestByName.set(pluginName, { + name: pluginName, + title, + version, + root, + logo: typeof json.logo === 'string' ? json.logo : undefined, + features: json.features, + }) + } + + const results: ScannedApp[] = [] + for (const cand of bestByName.values()) { + const icon = await resolveLogoIcon(cand.root, cand.logo, access) + results.push( + ...appsFromPlugin( + platform, + { + name: cand.name, + title: cand.title, + path: cand.root, + logo: cand.logo, + features: cand.features, + }, + deps.excludeCmds, + icon, + ), + ) + } + return results +} + +/** + * Discover ZTools text commands for **currently installed** plugins when possible. + * Prefers ZTools registry via `ztools.internal.getPlugins` so uninstalled leftovers + * under ~/.ztools/plugins are ignored. + */ +export async function scanZtoolsCommands( + platform: Platform, + deps: ScanZtoolsCommandsDeps = {}, +): Promise { + const listInstalled = deps.listInstalledPlugins ?? loadInstalledPluginsFromHost + const installed = await listInstalled() + + // Registry is authoritative when available (including empty = no installed plugins). + if (installed !== null) { + const results: ScannedApp[] = [] + const readFile = deps.readFile ?? ((file, enc) => fs.readFile(file, enc)) + const access = deps.access ?? ((file) => fs.access(file)) + + for (const plugin of installed) { + if (shouldSkipPlugin(plugin.name, plugin.title)) continue + + let features = plugin.features + let logo = plugin.logo + // Registry entries usually already include features; refill from disk/asar if missing. + if (!Array.isArray(features) || features.length === 0) { + if (!plugin.path) continue + const json = await readPluginJson(plugin.path, readFile) + if (!json) continue + features = json.features + if (!logo && typeof json.logo === 'string') logo = json.logo + } + + const icon = + logo && logo.includes('://') + ? logo + : plugin.path + ? await resolveLogoIcon(plugin.path, logo, access) + : null + + results.push( + ...appsFromPlugin( + platform, + { + name: plugin.name, + title: plugin.title, + path: plugin.path, + logo, + features, + }, + deps.excludeCmds, + icon, + ), + ) + } + return results + } + + return scanPluginsFromDisk(platform, deps) +} + +/** @deprecated alias */ +export const scanZtoolsPlugins = scanZtoolsCommands +export type ScanZtoolsPluginsDeps = ScanZtoolsCommandsDeps diff --git a/plugins/batch-start/src/styles.css b/plugins/batch-start/src/styles.css new file mode 100644 index 000000000..453d6ed06 --- /dev/null +++ b/plugins/batch-start/src/styles.css @@ -0,0 +1,400 @@ +:root { + --bg: #f3f4f6; + --surface: #ffffff; + --surface-2: #f8fafc; + --border: #e5e7eb; + --text: #1f2937; + --muted: #6b7280; + --accent: #059669; + --accent-hover: #059669; + --selected: rgba(5, 150, 105, 0.12); + --selected-border: rgba(5, 150, 105, 0.28); + --badge-bg: rgba(5, 150, 105, 0.14); + --badge-fg: #059669; + --danger: #dc2626; + --danger-bg: #fef2f2; + --danger-border: #fecaca; + --hover: rgba(15, 23, 42, 0.04); + --input-bg: #ffffff; + --tag-group-fg: #0369a1; + --tag-group-bg: rgba(14, 165, 233, 0.12); + --tag-group-border: rgba(14, 165, 233, 0.28); + --radius: 7px; + --rail: 48px; + --sidebar: 168px; + --group-col: 280px; + --font: 'Segoe UI', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; + --ease: 180ms ease; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + margin: 0; + height: 100%; + font-family: var(--font); + font-size: 13px; + color: var(--text); + background: var(--bg); +} + +button, +input, +select, +textarea { + font: inherit; + color: inherit; +} + +button { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.btn { + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: 6px 12px; + line-height: 1.3; + transition: + background var(--ease), + border-color var(--ease), + color var(--ease), + opacity var(--ease); +} + +.btn:hover:not(:disabled) { + background: var(--hover); +} + +.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.btn-primary:hover:not(:disabled) { + filter: brightness(1.05); + background: var(--accent); + border-color: var(--accent); +} + +.btn-ghost { + background: transparent; + border-color: transparent; +} + +.btn-ghost:hover:not(:disabled) { + background: var(--hover); +} + +.btn-danger { + color: var(--danger); + border-color: var(--danger-border); + background: var(--danger-bg); +} + +.btn-sm { + padding: 3px 8px; + font-size: 12px; +} + +.btn-icon { + width: 28px; + height: 28px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--radius); + border: 1px solid transparent; + background: transparent; + color: var(--muted); +} + +.btn-icon:hover:not(:disabled) { + background: var(--hover); + color: var(--text); +} + +.panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + font-weight: 600; + background: var(--surface-2); +} + +.panel-body { + flex: 1; + overflow: auto; + padding: 8px; +} + +/* Teleported category filter dropdown (Bootstrap-like) */ +.cat-select__menu { + position: fixed; + z-index: 1000; + max-height: 260px; + overflow: auto; + padding: 0.35rem; + margin: 0; + border: 1px solid var(--border); + border-radius: 0.375rem; + background: var(--surface); + box-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.15); + display: flex; + flex-direction: column; + gap: 2px; +} + +.cat-select__option { + border: 0; + background: transparent; + text-align: left; + padding: 0.4rem 0.75rem; + border-radius: 0.25rem; + color: var(--text); + font-size: 13px; + line-height: 1.4; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cat-select__option:hover { + background: var(--hover); +} + +.cat-select__option.active { + background: var(--selected); + color: var(--accent); + font-weight: 600; +} + +/* Teleported category action menu (Bootstrap-like dropdown) */ +.cat-menu__pop { + position: fixed; + z-index: 1000; + min-width: 108px; + padding: 4px; + border: 1px solid var(--border); + border-radius: 0.375rem; + background: var(--surface); + box-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.15); + display: flex; + flex-direction: column; + gap: 2px; +} + +.cat-menu__item { + border: 0; + background: transparent; + text-align: left; + padding: 6px 12px; + border-radius: calc(0.375rem - 2px); + color: var(--text); + font-size: 13px; + line-height: 1.4; + cursor: pointer; +} + +.cat-menu__item:hover { + background: var(--selected); + color: var(--accent); +} + +.cat-menu__item.danger { + color: #dc2626; +} + +.cat-menu__item.danger:hover { + background: rgba(220, 38, 38, 0.1); + color: #dc2626; +} + +.field { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 10px; +} + +.field label { + color: var(--muted); + font-size: 12px; +} + +.field input, +.field select, +.field textarea, +.source-filter, +.search-input, +.cat-select { + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 8px; + background: var(--input-bg); +} + +.field textarea { + min-height: 64px; + resize: vertical; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 32px 16px; + color: var(--muted); + text-align: center; +} + +.list-item { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: var(--radius); + cursor: pointer; + border: 1px solid transparent; + transition: + background var(--ease), + border-color var(--ease); +} + +.list-item:hover { + background: var(--hover); +} + +.list-item.active { + background: var(--selected); + border-color: var(--selected-border); +} + +.list-item.active::before { + content: ''; + position: absolute; + left: 0; + top: 6px; + bottom: 6px; + width: 2px; + border-radius: 2px; + background: var(--accent); +} + +.muted { + color: var(--muted); +} + +.error-banner { + background: var(--danger-bg); + color: var(--danger); + border: 1px solid var(--danger-border); + border-radius: var(--radius); + padding: 8px 10px; + margin: 0 0 8px; +} + +.badge-unsynced { + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--danger-bg); + color: var(--danger); + border: 1px solid var(--danger-border); + border-radius: 999px; + padding: 1px 8px; + font-size: 11px; + font-weight: 600; +} + +.toolbar-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.seg { + display: inline-flex; + padding: 2px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-2); + gap: 2px; +} + +.seg__btn { + border: none; + background: transparent; + color: var(--muted); + border-radius: calc(var(--radius) - 2px); + padding: 4px 10px; + font-size: 12px; + line-height: 1.2; + transition: + background var(--ease), + color var(--ease); +} + +.seg__btn:hover:not(:disabled) { + color: var(--text); + background: var(--hover); +} + +.seg__btn.active { + background: var(--surface); + color: var(--text); + box-shadow: 0 0 0 1px var(--border); +} + +.source-badge { + flex-shrink: 0; + font-size: 10px; + font-weight: 600; + line-height: 1; + padding: 2px 5px; + border-radius: 4px; + color: var(--badge-fg); + background: var(--badge-bg); +} + + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition: none !important; + animation: none !important; + } +} diff --git a/plugins/batch-start/src/types.ts b/plugins/batch-start/src/types.ts new file mode 100644 index 000000000..e0a1665bf --- /dev/null +++ b/plugins/batch-start/src/types.ts @@ -0,0 +1,60 @@ +export type Platform = 'win32' | 'darwin' | 'linux' +/** `ztools` = a divertible ZTools text command (not a desktop app) */ +export type AppSource = 'scan' | 'manual' | 'ztools' | 'plugin' + +export interface AppDoc { + _id: string + _rev?: string + name: string + path: string + icon?: string | null + source: AppSource + /** An app may belong to multiple categories */ + categoryIds: string[] + platform: Platform + /** Present when source is ztools (or legacy plugin) */ + pluginName?: string + pluginTitle?: string + /** Divertible text cmd used with ztools.redirect */ + launchCmd?: string | null +} + +export interface CategoryDoc { + _id: string + _rev?: string + name: string + order: number +} + +export interface GroupDoc { + _id: string + _rev?: string + name: string + cmds: string[] + appIds: string[] + order: number + featureSynced: boolean +} + +export type UiPrefs = { + /** Category sidebar collapsed to icon rail; default true */ + categoryCollapsed?: boolean + /** Group column collapsed to narrow strip; default false */ + groupCollapsed?: boolean +} + +export interface SettingsDoc { + _id: 'settings' + _rev?: string + customScanDirs: string[] + lastScanAt: number | null + ui?: UiPrefs +} + +export interface LaunchResult { + success: number + failed: number + errors: Array<{ path: string; error: string }> + /** Present when launched via launchTargets — used to settle after ZTools redirects */ + outcomes?: Array<{ path: string; ok: boolean; error: string; kind: 'path' | 'ztools' }> +} diff --git a/plugins/batch-start/tests/appLibrary.test.ts b/plugins/batch-start/tests/appLibrary.test.ts new file mode 100644 index 000000000..38117b6a2 --- /dev/null +++ b/plugins/batch-start/tests/appLibrary.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from 'vitest' +import { + upsertApp, + listApps, + mergeScannedApps, + reconcileMissingPluginApps, + normalizeApp, +} from '../src/services/appLibrary' +import { makeAppId } from '../src/services/db' +import { commandAppPath, pluginAppPath } from '../src/services/scanner/ztoolsPlugins' +import { createMemoryDb } from './mocks/memoryDb' + +describe('appLibrary', () => { + it('normalizeApp migrates legacy categoryId', () => { + const app = normalizeApp({ + _id: makeAppId('1'), + name: 'A', + path: 'C:/a.exe', + source: 'manual', + categoryIds: [], + categoryId: 'category:x', + platform: 'win32', + } as Parameters[0]) + expect(app.categoryIds).toEqual(['category:x']) + expect('categoryId' in app).toBe(false) + }) + + it('mergeScannedApps keeps categoryIds on path match', async () => { + const db = createMemoryDb() + await upsertApp(db, { + _id: makeAppId('1'), + name: 'Old', + path: 'C:/Tool/a.exe', + source: 'scan', + categoryIds: ['category:x', 'category:y'], + platform: 'win32', + }) + await mergeScannedApps(db, [ + { name: 'New', path: 'C:/Tool/a.exe', platform: 'win32' }, + ]) + const apps = await listApps(db) + expect(apps).toHaveLength(1) + expect(apps[0].categoryIds).toEqual(['category:x', 'category:y']) + expect(apps[0].name).toBe('New') + }) + + it('mergeScannedApps matches paths case-insensitively', async () => { + const db = createMemoryDb() + await upsertApp(db, { + _id: makeAppId('1'), + name: 'Old', + path: 'C:/Tool/a.exe', + source: 'scan', + categoryIds: ['category:x'], + platform: 'win32', + }) + await mergeScannedApps(db, [ + { name: 'New', path: 'c:/tool/A.exe', platform: 'win32' }, + ]) + const apps = await listApps(db) + expect(apps).toHaveLength(1) + expect(apps[0].categoryIds).toEqual(['category:x']) + expect(apps[0].name).toBe('New') + }) + + it('mergeScannedApps upserts ztools commands with stable ids', async () => { + const db = createMemoryDb() + const path = commandAppPath('colors', '颜色助手') + await mergeScannedApps(db, [ + { + name: '颜色助手', + path, + platform: 'win32', + source: 'ztools', + pluginName: 'colors', + pluginTitle: '调色板', + launchCmd: '颜色助手', + stableIdHint: `cmd:colors:${encodeURIComponent('颜色助手')}`, + }, + ]) + await mergeScannedApps(db, [ + { + name: '颜色助手', + path, + platform: 'win32', + source: 'ztools', + pluginName: 'colors', + pluginTitle: '调色板', + launchCmd: '颜色助手', + stableIdHint: `cmd:colors:${encodeURIComponent('颜色助手')}`, + }, + ]) + const apps = await listApps(db) + expect(apps).toHaveLength(1) + expect(apps[0]._id).toBe(makeAppId(`cmd:colors:${encodeURIComponent('颜色助手')}`)) + expect(apps[0].source).toBe('ztools') + expect(apps[0].launchCmd).toBe('颜色助手') + expect(apps[0].categoryIds).toEqual([]) + }) + + it('reconcile removes missing cmds and legacy plugin rows', async () => { + const db = createMemoryDb() + const keepPath = commandAppPath('colors', '颜色助手') + const gonePath = commandAppPath('colors', 'gone') + await mergeScannedApps(db, [ + { + name: '颜色助手', + path: keepPath, + platform: 'win32', + source: 'ztools', + pluginName: 'colors', + launchCmd: '颜色助手', + }, + { + name: 'gone', + path: gonePath, + platform: 'win32', + source: 'ztools', + pluginName: 'colors', + launchCmd: 'gone', + }, + ]) + // legacy per-plugin row + await upsertApp(db, { + _id: makeAppId('plugin:legacy'), + name: 'Legacy', + path: pluginAppPath('legacy'), + source: 'plugin', + categoryIds: [], + platform: 'win32', + launchCmd: 'x', + }) + + const apps = await listApps(db) + const keep = apps.find((a) => a.launchCmd === '颜色助手')! + const gone = apps.find((a) => a.launchCmd === 'gone')! + const legacy = apps.find((a) => a.source === 'plugin')! + await db.put({ + _id: 'group:1', + name: '工作', + cmds: ['work'], + appIds: [keep._id, gone._id, legacy._id], + order: 0, + featureSynced: true, + }) + + await reconcileMissingPluginApps(db, [keepPath]) + const after = await listApps(db) + expect(after.map((a) => a.launchCmd)).toEqual(['颜色助手']) + const group = (await db.get('group:1')) as { appIds: string[]; featureSynced: boolean } + expect(group.appIds).toEqual([keep._id]) + expect(group.featureSynced).toBe(false) + }) +}) diff --git a/plugins/batch-start/tests/category.test.ts b/plugins/batch-start/tests/category.test.ts new file mode 100644 index 000000000..756abd03b --- /dev/null +++ b/plugins/batch-start/tests/category.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from 'vitest' +import { createMemoryDb } from './mocks/memoryDb' +import { createCategory, deleteCategory, listCategories } from '../src/services/category' +import { + upsertApp, + listApps, + assignCategory, + removeCategory, +} from '../src/services/appLibrary' +import { makeAppId } from '../src/services/db' + +describe('category', () => { + it('creates and lists categories', async () => { + const db = createMemoryDb() + const c = await createCategory(db, '开发') + expect(c.name).toBe('开发') + expect(c._id.startsWith('category:')).toBe(true) + expect((await listCategories(db)).length).toBe(1) + }) + + it('deleteCategory removes id from app.categoryIds', async () => { + const db = createMemoryDb() + const c = await createCategory(db, '办公') + const keep = await createCategory(db, '开发') + await upsertApp(db, { + _id: makeAppId('1'), + name: 'A', + path: 'C:/a.exe', + source: 'manual', + categoryIds: [c._id, keep._id], + platform: 'win32', + }) + await deleteCategory(db, c._id) + const apps = await listApps(db) + expect(apps[0].categoryIds).toEqual([keep._id]) + }) + + it('assignCategory adds without replacing existing', async () => { + const db = createMemoryDb() + const a = await createCategory(db, 'A') + const b = await createCategory(db, 'B') + await upsertApp(db, { + _id: makeAppId('1'), + name: 'App', + path: 'C:/a.exe', + source: 'manual', + categoryIds: [a._id], + platform: 'win32', + }) + await assignCategory(db, makeAppId('1'), b._id) + const apps = await listApps(db) + expect(apps[0].categoryIds).toEqual([a._id, b._id]) + }) + + it('removeCategory drops one membership', async () => { + const db = createMemoryDb() + const a = await createCategory(db, 'A') + const b = await createCategory(db, 'B') + const appId = makeAppId('1') + await upsertApp(db, { + _id: appId, + name: 'App', + path: 'C:/a.exe', + source: 'manual', + categoryIds: [a._id, b._id], + platform: 'win32', + }) + await removeCategory(db, appId, a._id) + const apps = await listApps(db) + expect(apps[0].categoryIds).toEqual([b._id]) + }) +}) diff --git a/plugins/batch-start/tests/db.test.ts b/plugins/batch-start/tests/db.test.ts new file mode 100644 index 000000000..9d448e916 --- /dev/null +++ b/plugins/batch-start/tests/db.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from 'vitest' +import { makeAppId, makeCategoryId, makeGroupId } from '../src/services/db' + +describe('id factories', () => { + it('prefixes ids', () => { + expect(makeAppId('abc')).toBe('app:abc') + expect(makeCategoryId('abc')).toBe('category:abc') + expect(makeGroupId('abc')).toBe('group:abc') + }) +}) diff --git a/plugins/batch-start/tests/featureSync.test.ts b/plugins/batch-start/tests/featureSync.test.ts new file mode 100644 index 000000000..d48ee3f02 --- /dev/null +++ b/plugins/batch-start/tests/featureSync.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createMemoryDb } from './mocks/memoryDb' +import { saveGroup, getGroup } from '../src/services/launchGroup' +import { syncGroupFeature, reconcileFeatures } from '../src/services/featureSync' +import type { ZtoolsDb } from '../src/services/db' + +describe('featureSync', () => { + let db: ZtoolsDb + + beforeEach(() => { + db = createMemoryDb() + }) + + it('setFeature on syncGroup and marks featureSynced', async () => { + const setFeature = vi.fn(() => true) + const removeFeature = vi.fn(() => true) + const group = await saveGroup(db, { + name: '开工', + cmds: ['开工'], + appIds: ['app:1'], + order: 0, + }) + await syncGroupFeature(group, { setFeature, removeFeature }, db) + expect(setFeature).toHaveBeenCalledWith( + expect.objectContaining({ code: group._id, cmds: ['开工'] }), + ) + expect((await getGroup(db, group._id))!.featureSynced).toBe(true) + }) + + it('reconcile removes orphan features and restores missing', async () => { + const setFeature = vi.fn(() => true) + const removeFeature = vi.fn(() => true) + const keep = await saveGroup(db, { + name: 'Keep', + cmds: ['Keep'], + appIds: ['app:1'], + order: 0, + }) + const getFeatures = () => [{ code: 'group:orphan' }, { code: keep._id }] + await reconcileFeatures(db, { setFeature, removeFeature, getFeatures }) + expect(removeFeature).toHaveBeenCalledWith('group:orphan') + }) +}) diff --git a/plugins/batch-start/tests/launchGroup.test.ts b/plugins/batch-start/tests/launchGroup.test.ts new file mode 100644 index 000000000..8ab71e67b --- /dev/null +++ b/plugins/batch-start/tests/launchGroup.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { createMemoryDb } from './mocks/memoryDb' +import { saveGroup } from '../src/services/launchGroup' +import type { ZtoolsDb } from '../src/services/db' + +describe('launchGroup', () => { + let db: ZtoolsDb + + beforeEach(() => { + db = createMemoryDb() + }) + + it('rejects empty name', async () => { + await expect( + saveGroup(db, { name: ' ', cmds: [], appIds: ['app:1'], order: 0 }), + ).rejects.toThrow(/name/i) + }) + + it('rejects zero apps', async () => { + await expect( + saveGroup(db, { name: 'Dev', cmds: ['Dev'], appIds: [], order: 0 }), + ).rejects.toThrow(/app/i) + }) + + it('rejects duplicate cmd against other groups', async () => { + await saveGroup(db, { name: 'A', cmds: ['开工'], appIds: ['app:1'], order: 0 }) + await expect( + saveGroup(db, { name: 'B', cmds: ['开工'], appIds: ['app:2'], order: 1 }), + ).rejects.toThrow(/conflict|冲突|cmd/i) + }) +}) diff --git a/plugins/batch-start/tests/launcher.test.ts b/plugins/batch-start/tests/launcher.test.ts new file mode 100644 index 000000000..8aec92aea --- /dev/null +++ b/plugins/batch-start/tests/launcher.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, vi } from 'vitest' +import { + countSuccessfulZtoolsLaunches, + launchPaths, + launchTargets, +} from '../src/services/launcher' +import { commandAppPath } from '../src/services/scanner/ztoolsPlugins' + +describe('launchPaths', () => { + it('opens all paths in parallel and summarizes failures', async () => { + const open = vi.fn(async (p: string) => { + if (p.includes('bad')) return { success: false, error: 'missing' } + return { success: true } + }) + const result = await launchPaths(['/a', '/bad', '/c'], open) + expect(open).toHaveBeenCalledTimes(3) + expect(result.success).toBe(2) + expect(result.failed).toBe(1) + expect(result.errors[0].path).toBe('/bad') + }) +}) + +describe('launchTargets', () => { + it('redirects ztools command targets and opens filesystem paths', async () => { + const open = vi.fn(async () => ({ success: true })) + const redirect = vi.fn(() => true) + const result = await launchTargets( + [ + { key: '/a.exe', kind: 'path', path: '/a.exe' }, + { + key: commandAppPath('colors', '颜色助手'), + kind: 'ztools', + pluginTitle: '调色板', + launchCmd: '颜色助手', + }, + ], + { openPath: open, redirect }, + ) + expect(open).toHaveBeenCalledWith('/a.exe') + expect(redirect).toHaveBeenCalledWith(['调色板', '颜色助手']) + expect(result.success).toBe(2) + expect(result.failed).toBe(0) + }) + + it('fails command launch when launchCmd missing', async () => { + const result = await launchTargets( + [{ key: commandAppPath('x', 'y'), kind: 'ztools', launchCmd: null }], + { openPath: async () => ({ success: true }), redirect: () => true }, + ) + expect(result.failed).toBe(1) + expect(result.errors[0].error).toMatch(/no divertible/) + }) + + it('runs ztools redirects sequentially after filesystem paths', async () => { + const order: string[] = [] + const open = vi.fn(async (p: string) => { + order.push(`open:${p}`) + return { success: true } + }) + const redirect = vi.fn((label: string | [string, string]) => { + order.push(`redirect:${Array.isArray(label) ? label.join('/') : label}`) + return true + }) + const result = await launchTargets( + [ + { + key: commandAppPath('a', 'cmd-a'), + kind: 'ztools', + pluginTitle: 'A', + launchCmd: 'cmd-a', + }, + { key: '/b.exe', kind: 'path', path: '/b.exe' }, + { + key: commandAppPath('c', 'cmd-c'), + kind: 'ztools', + pluginTitle: 'C', + launchCmd: 'cmd-c', + }, + ], + { openPath: open, redirect }, + ) + expect(order).toEqual(['open:/b.exe', 'redirect:A/cmd-a', 'redirect:C/cmd-c']) + expect(countSuccessfulZtoolsLaunches(result.outcomes)).toBe(2) + }) +}) diff --git a/plugins/batch-start/tests/mocks/memoryDb.ts b/plugins/batch-start/tests/mocks/memoryDb.ts new file mode 100644 index 000000000..fb9095283 --- /dev/null +++ b/plugins/batch-start/tests/mocks/memoryDb.ts @@ -0,0 +1,37 @@ +import type { ZtoolsDb } from '../../src/services/db' + +export function createMemoryDb(): ZtoolsDb { + const map = new Map() + const putOne = async (doc: any) => { + const prev = map.get(doc._id) + const revNum = prev?._rev ? Number(String(prev._rev).split('-')[1]) + 1 : 1 + const next = { ...doc, _rev: `rev-${revNum}` } + map.set(doc._id, next) + return next + } + return { + async put(doc: any) { + return putOne(doc) + }, + async get(id: string) { + return map.get(id) ?? null + }, + async remove(docOrId: any) { + const id = typeof docOrId === 'string' ? docOrId : docOrId._id + map.delete(id) + return { ok: true, id } + }, + async allDocs(key?: string) { + const all = [...map.values()] + if (!key) return all + return all.filter((d) => String(d._id).startsWith(key)) + }, + async bulkDocs(docs: object[]) { + const out: object[] = [] + for (const doc of docs) { + out.push(await putOne(doc)) + } + return out + }, + } +} diff --git a/plugins/batch-start/tests/preload-routing.test.ts b/plugins/batch-start/tests/preload-routing.test.ts new file mode 100644 index 000000000..63ceefb84 --- /dev/null +++ b/plugins/batch-start/tests/preload-routing.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest' +import { routePluginEnter } from '../src/services/enterRouter' + +describe('routePluginEnter', () => { + it('routes manage and group codes', () => { + expect(routePluginEnter('manage')).toEqual({ type: 'manage' }) + expect(routePluginEnter('group:abc')).toEqual({ + type: 'launch-group', + groupId: 'group:abc', + }) + }) + + it('defaults undefined and unknown codes to manage', () => { + expect(routePluginEnter(undefined)).toEqual({ type: 'manage' }) + expect(routePluginEnter('other')).toEqual({ type: 'manage' }) + }) +}) diff --git a/plugins/batch-start/tests/scanner-filter.test.ts b/plugins/batch-start/tests/scanner-filter.test.ts new file mode 100644 index 000000000..615a20d15 --- /dev/null +++ b/plugins/batch-start/tests/scanner-filter.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import { isUninstallEntry, rejectUninstallApps } from '../src/services/scanner/filter' + +describe('uninstall filter', () => { + it('detects uninstall / 卸载 in name or path', () => { + expect(isUninstallEntry('Uninstall WeChat', 'C:/x.exe')).toBe(true) + expect(isUninstallEntry('卸载 微信', 'C:/x.exe')).toBe(true) + expect(isUninstallEntry('微信', 'C:/Program Files/WeChat/Uninstall.exe')).toBe(true) + expect(isUninstallEntry('App', 'C:/app/unins000.exe')).toBe(true) + expect(isUninstallEntry('微信', 'C:/Program Files/WeChat/WeChat.exe')).toBe(false) + }) + + it('rejectUninstallApps drops matching rows', () => { + const kept = rejectUninstallApps([ + { name: '微信', path: 'C:/WeChat.exe' }, + { name: 'Uninstall 微信', path: 'C:/un.exe' }, + { name: '某工具', path: 'C:/tool/卸载.exe' }, + ]) + expect(kept).toEqual([{ name: '微信', path: 'C:/WeChat.exe' }]) + }) +}) diff --git a/plugins/batch-start/tests/scanner-index.test.ts b/plugins/batch-start/tests/scanner-index.test.ts new file mode 100644 index 000000000..072c319e1 --- /dev/null +++ b/plugins/batch-start/tests/scanner-index.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' +import { getSettings } from '../src/services/db' +import { listApps } from '../src/services/appLibrary' +import { runFullScan } from '../src/services/scanner' +import type { PlatformScanner, ScannedApp } from '../src/services/scanner/types' +import { createMemoryDb } from './mocks/memoryDb' + +function stubScanner(apps: ScannedApp[], customByDir: Record = {}): PlatformScanner { + return { + async scanSystem() { + return apps + }, + async scanCustomDir(dir: string) { + if (!(dir in customByDir) && dir.includes('Missing')) return [] + return customByDir[dir] ?? [] + }, + } +} + +describe('runFullScan', () => { + it('merges customScanDirs, sets lastScanAt, skips missing dirs', async () => { + const db = createMemoryDb() + await db.put({ + _id: 'settings', + customScanDirs: ['C:/Apps', 'C:/Missing'], + lastScanAt: null, + }) + + const scanner = stubScanner( + [{ name: 'SystemApp', path: 'C:/System/App.exe', platform: 'win32' }], + { + 'C:/Apps': [{ name: 'CustomApp', path: 'C:/Apps/custom.exe', platform: 'win32' }], + 'C:/Missing': [], + }, + ) + + const now = 1_700_000_000_000 + await runFullScan(db, 'win32', { + scanner, + now: () => now, + scanZtoolsCommands: async () => [], + }) + + const apps = await listApps(db) + const paths = apps.map((a) => a.path.toLowerCase()) + expect(paths.some((p) => p.endsWith('app.exe'))).toBe(true) + expect(paths.some((p) => p.endsWith('custom.exe'))).toBe(true) + + const settings = await getSettings(db) + expect(settings.lastScanAt).toBe(now) + expect(settings.customScanDirs).toEqual(['C:/Apps', 'C:/Missing']) + }) +}) diff --git a/plugins/batch-start/tests/scanner-win.test.ts b/plugins/batch-start/tests/scanner-win.test.ts new file mode 100644 index 000000000..fbdbd3045 --- /dev/null +++ b/plugins/batch-start/tests/scanner-win.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it, vi } from 'vitest' +import { createWinScanner } from '../src/services/scanner/win' + +describe('createWinScanner', () => { + it('scanCustomDir includes exe bat cmd lnk', async () => { + const files = ['a.exe', 'b.bat', 'c.cmd', 'd.lnk', 'e.txt'] + const resolveLnks = vi.fn(async (paths: string[]) => { + const map = new Map() + for (const p of paths) { + map.set(p, p.replace(/\.lnk$/i, '.exe')) + } + return map + }) + const scanner = createWinScanner({ + readdir: async () => + files.map((name) => ({ + name, + isFile: () => true, + isDirectory: () => false, + })), + resolveLnks, + }) + const apps = await scanner.scanCustomDir('C:/Apps') + const names = apps.map((a) => a.path.toLowerCase()) + expect(names.some((p) => p.endsWith('a.exe'))).toBe(true) + expect(names.some((p) => p.endsWith('b.bat'))).toBe(true) + expect(names.some((p) => p.endsWith('c.cmd'))).toBe(true) + expect(names.some((p) => p.endsWith('.exe'))).toBe(true) + expect(names.some((p) => p.endsWith('e.txt'))).toBe(false) + expect(resolveLnks).toHaveBeenCalledTimes(1) + }) + + it('uses Node basename for Chinese shortcut display names', async () => { + const scanner = createWinScanner({ + readdir: async () => [ + { + name: '微信.lnk', + isFile: () => true, + isDirectory: () => false, + }, + ], + resolveLnks: async (paths) => { + const map = new Map() + map.set(paths[0], 'C:/Program Files/Tencent/WeChat/WeChat.exe') + return map + }, + }) + const apps = await scanner.scanCustomDir('C:/Menu') + expect(apps).toHaveLength(1) + expect(apps[0].name).toBe('微信') + expect(apps[0].path).toContain('WeChat.exe') + }) + + it('scanCustomDir returns [] when dir is missing', async () => { + const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + const scanner = createWinScanner({ + readdir: async () => { + throw err + }, + resolveLnks: async () => new Map(), + }) + await expect(scanner.scanCustomDir('C:/Missing')).resolves.toEqual([]) + }) + + it('scanCustomDir returns [] when readdir throws EACCES', async () => { + const err = Object.assign(new Error('EACCES'), { code: 'EACCES' }) + const scanner = createWinScanner({ + readdir: async () => { + throw err + }, + resolveLnks: async () => new Map(), + }) + await expect(scanner.scanCustomDir('C:/Denied')).resolves.toEqual([]) + }) + + it('skips uninstall shortcuts before resolving', async () => { + const resolveLnks = vi.fn(async (paths: string[]) => { + const map = new Map() + for (const p of paths) map.set(p, p.replace(/\.lnk$/i, '.exe')) + return map + }) + const scanner = createWinScanner({ + readdir: async () => + ['Uninstall Foo.lnk', '卸载 Bar.lnk', 'Foo.lnk'].map((name) => ({ + name, + isFile: () => true, + isDirectory: () => false, + })), + resolveLnks, + }) + const apps = await scanner.scanCustomDir('C:/Menu') + expect(apps.map((a) => a.name)).toEqual(['Foo']) + expect(resolveLnks).toHaveBeenCalledTimes(1) + const passed = resolveLnks.mock.calls[0][0] as string[] + expect(passed).toHaveLength(1) + expect(passed[0].endsWith('Foo.lnk')).toBe(true) + }) +}) diff --git a/plugins/batch-start/tests/ztools-plugins.test.ts b/plugins/batch-start/tests/ztools-plugins.test.ts new file mode 100644 index 000000000..b87a0a184 --- /dev/null +++ b/plugins/batch-start/tests/ztools-plugins.test.ts @@ -0,0 +1,116 @@ +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { + EXCLUDED_PLUGIN_NAMES, + commandAppPath, + listTextCmds, + scanZtoolsCommands, + versionFromArtifactName, +} from '../src/services/scanner/ztoolsPlugins' + +describe('ztoolsCommands', () => { + it('listTextCmds skips manage cmds, exclude set, and group: features', () => { + expect( + listTextCmds( + [ + { code: 'manage', cmds: ['批量启动', '颜色助手'] }, + { code: 'group:abc', cmds: ['startWork'] }, + { cmds: ['colors', 'skip-me'] }, + ], + { excludeCmds: new Set(['skip-me']) }, + ), + ).toEqual(['颜色助手', 'colors']) + }) + + it('versionFromArtifactName parses asar install names', () => { + expect(versionFromArtifactName('ztools-kill-process-1.0.2-ef21d1e7.asar')).toBe('1.0.2') + expect(versionFromArtifactName('colors')).toBeNull() + }) + + it('prefers installed registry and ignores disk leftovers', async () => { + const scanned = await scanZtoolsCommands('win32', { + excludeCmds: new Set(), + listInstalledPlugins: async () => [ + { + name: 'colors', + title: '调色板', + path: '/plugins/colors', + features: [{ cmds: ['颜色助手', 'colors'] }], + }, + ], + // Would otherwise pick up uninstalled leftovers if registry ignored: + pluginsRoot: '/plugins', + readDir: async () => ['gone-plugin', 'colors'], + readFile: async (file) => { + if (file.includes('gone-plugin')) { + return JSON.stringify({ + name: 'gone-plugin', + title: '已卸载', + features: [{ cmds: ['残留指令'] }], + }) + } + throw new Error('should not read disk when registry provided') + }, + access: async () => { + throw new Error('no logo') + }, + }) + + expect(scanned.map((s) => s.name)).toEqual(['颜色助手', 'colors']) + expect(scanned.every((s) => s.pluginName === 'colors')).toBe(true) + }) + + it('disk fallback scans asar packages and keeps newest version only', async () => { + const root = path.join('mock-ztools', 'plugins') + const files: Record = { + [path.join(root, 'wechat-multiopen-1.0.0-e2c43859.asar', 'plugin.json')]: JSON.stringify({ + name: 'wechat-multiopen', + title: '微信多开', + version: '1.0.0', + features: [{ cmds: ['微信双开'] }], + }), + [path.join(root, 'ztools-kill-process-1.0.1-ffeb2f13.asar', 'plugin.json')]: JSON.stringify({ + name: 'ztools-kill-process', + title: '杀进程', + version: '1.0.1', + features: [{ cmds: ['杀进程'] }], + }), + [path.join(root, 'ztools-kill-process-1.0.2-ef21d1e7.asar', 'plugin.json')]: JSON.stringify({ + name: 'ztools-kill-process', + title: '杀进程/进程管理', + version: '1.0.2', + features: [{ cmds: ['杀进程', 'kill'] }], + }), + [path.join(root, 'batch-start', 'plugin.json')]: JSON.stringify({ + name: 'batch-start', + title: '批量启动', + features: [{ cmds: ['批量启动'] }], + }), + } + + const scanned = await scanZtoolsCommands('win32', { + pluginsRoot: root, + listInstalledPlugins: async () => null, + readDir: async () => [ + 'wechat-multiopen-1.0.0-e2c43859.asar', + 'ztools-kill-process-1.0.1-ffeb2f13.asar', + 'ztools-kill-process-1.0.2-ef21d1e7.asar', + 'ztools-kill-process-1.0.2-ef21d1e7.asar.unpacked', + 'batch-start', + '.installing', + ], + readFile: async (file) => { + if (!(file in files)) throw new Error(`missing ${file}`) + return files[file] + }, + access: async () => { + throw new Error('no logo') + }, + }) + + expect(scanned.map((s) => s.name).sort()).toEqual(['kill', '微信双开', '杀进程'].sort()) + expect(scanned.find((s) => s.launchCmd === '杀进程')?.pluginTitle).toBe('杀进程/进程管理') + expect(EXCLUDED_PLUGIN_NAMES.has('system')).toBe(true) + expect(scanned.some((s) => s.path === commandAppPath('batch-start', '批量启动'))).toBe(false) + }) +}) diff --git a/plugins/batch-start/tsconfig.json b/plugins/batch-start/tsconfig.json new file mode 100644 index 000000000..1c9add509 --- /dev/null +++ b/plugins/batch-start/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["vite/client", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"] +} diff --git a/plugins/batch-start/tsconfig.node.json b/plugins/batch-start/tsconfig.node.json new file mode 100644 index 000000000..de75e2b15 --- /dev/null +++ b/plugins/batch-start/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} diff --git a/plugins/batch-start/vite.config.ts b/plugins/batch-start/vite.config.ts new file mode 100644 index 000000000..0b3f94c6d --- /dev/null +++ b/plugins/batch-start/vite.config.ts @@ -0,0 +1,79 @@ +import { copyFileSync, existsSync, mkdirSync } from 'node:fs' +import { builtinModules } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig, build as viteBuild, type Plugin } from 'vite' +import vue from '@vitejs/plugin-vue' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const pluginRoot = path.resolve(__dirname, 'src-ztools') + +const nodeExternals = [ + ...builtinModules, + ...builtinModules.map((m) => `node:${m}`), +] + +/** Align with official CLI vue-vite template: UI → src-ztools/dist, preload → src-ztools/preload/services.js */ +function ztoolsPluginBuild(): Plugin { + return { + name: 'ztools-plugin-build', + async closeBundle() { + const preloadDir = path.join(pluginRoot, 'preload') + if (!existsSync(preloadDir)) { + mkdirSync(preloadDir, { recursive: true }) + } + + await viteBuild({ + configFile: false, + publicDir: false, + build: { + outDir: preloadDir, + emptyOutDir: false, + copyPublicDir: false, + lib: { + entry: path.resolve(__dirname, 'src/preload.ts'), + formats: ['cjs'], + fileName: () => 'services.js', + }, + rollupOptions: { + external: nodeExternals, + output: { + entryFileNames: 'services.js', + format: 'cjs', + exports: 'named', + }, + }, + }, + logLevel: 'warn', + }) + + // remove accidental public assets if any + const strayLogo = path.join(preloadDir, 'logo.png') + if (existsSync(strayLogo)) { + try { + const { unlinkSync } = await import('node:fs') + unlinkSync(strayLogo) + } catch { + // ignore + } + } + + const logoSrc = path.resolve(__dirname, 'public/logo.png') + if (existsSync(logoSrc)) { + copyFileSync(logoSrc, path.join(pluginRoot, 'logo.png')) + } + }, + } +} + +export default defineConfig({ + plugins: [vue(), ztoolsPluginBuild()], + // Required for ZTools file:// loading — absolute /assets/* will blank the page + base: './', + publicDir: 'public', + build: { + // Official CLI template outDir + outDir: 'src-ztools/dist', + emptyOutDir: true, + }, +}) diff --git a/plugins/batch-start/vitest.config.ts b/plugins/batch-start/vitest.config.ts new file mode 100644 index 000000000..ae27197be --- /dev/null +++ b/plugins/batch-start/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + exclude: ['**/node_modules/**', '**/dist/**', '**/.worktrees/**'], + }, +})