From 74ef2ca810762847a338e0cde23945e142c31d6f Mon Sep 17 00:00:00 2001 From: ArthurYang999 Date: Wed, 11 Nov 2026 15:12:56 +0800 Subject: [PATCH 1/2] =?UTF-8?q?Update=20plugin=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=20v0.1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add design spec for ZTools batch-start plugin. - Add implementation plan for ZTools batch-start plugin. - chore: ignore .worktrees directory - chore: scaffold Vue ZTools batch-start plugin - feat: add domain types and db id helpers - feat: add category and app library services - test: cover case-insensitive path merge for apps - feat: add win/mac/linux app scanners - fix: skip inaccessible scan sources instead of aborting - feat: add parallel launcher with failure summary - feat: add launch groups and feature sync - feat: wire preload APIs and plugin enter routing - feat: add management UI for scan, categories, and groups - fix: toast trial feedback and join-group UX - docs: add usage README and finish MVP hardening - fix: exclude .worktrees from vitest discovery - fix: align ZTools package layout and relative asset base - fix: decode Chinese app names and speed up Windows scan - fix: ignore uninstall shortcuts during app scan - docs: add logo and align project docs with ZTools handbook - fix: exit plugin process on dismiss — no background residency - docs: design batch-start inclusion of installed ZTools plugins - feat: include ZTools commands in app list with source filter - fix: prefer installed plugin registry when scanning commands - docs: sync manage UI screenshot from open update PR - fix: 启动组 redirect 后延迟退出,避免中止目标插件 - chore: 发布 0.1.2,补充启动组 redirect 修复说明 - feat: 刷新三栏管理界面并支持应用多分类归属 - feat: 应用列表行增加启动按钮 - chore: 发布 0.1.3,补充管理界面与多分类变更说明 --- plugins/batch-start/.gitignore | 10 + plugins/batch-start/CHANGELOG.md | 7 + .../batch-start/dist/assets/index-CiJ97cWo.js | 17 - .../dist/assets/index-DdXE4yVZ.css | 1 - .../plans/2026-09-14-ztools-batch-start.md | 779 +++++ .../2026-09-17-batch-start-ztools-plugins.md | 37 + .../2026-09-14-ztools-batch-start-design.md | 187 ++ ...09-17-batch-start-ztools-plugins-design.md | 119 + ...026-11-11-batch-start-ui-refresh-design.md | 46 + plugins/batch-start/{dist => }/index.html | 25 +- plugins/batch-start/package-lock.json | 2974 +++++++++++++++++ plugins/batch-start/package.json | 23 + plugins/batch-start/plugin.json | 2 +- plugins/batch-start/preload/services.js | 23 - plugins/batch-start/{dist => public}/logo.png | Bin plugins/batch-start/{ => src-ztools}/logo.png | Bin plugins/batch-start/src-ztools/plugin.json | 22 + .../{ => src-ztools}/preload/package.json | 6 +- plugins/batch-start/src/App.vue | 587 ++++ .../batch-start/src/components/AppList.vue | 587 ++++ .../src/components/CategorySidebar.vue | 364 ++ .../batch-start/src/components/GroupPanel.vue | 239 ++ plugins/batch-start/src/components/TopBar.vue | 81 + .../src/composables/useHostTheme.ts | 149 + plugins/batch-start/src/env.d.ts | 73 + plugins/batch-start/src/main.ts | 5 + plugins/batch-start/src/preload.ts | 288 ++ .../batch-start/src/services/appLibrary.ts | 200 ++ plugins/batch-start/src/services/category.ts | 46 + plugins/batch-start/src/services/db.ts | 72 + .../batch-start/src/services/enterRouter.ts | 9 + .../batch-start/src/services/featureSync.ts | 65 + .../batch-start/src/services/launchGroup.ts | 75 + plugins/batch-start/src/services/launcher.ts | 138 + .../src/services/scanner/filter.ts | 16 + .../batch-start/src/services/scanner/index.ts | 108 + .../batch-start/src/services/scanner/linux.ts | 177 + .../batch-start/src/services/scanner/mac.ts | 103 + .../batch-start/src/services/scanner/types.ts | 19 + .../batch-start/src/services/scanner/win.ts | 238 ++ .../src/services/scanner/ztoolsPlugins.ts | 373 +++ plugins/batch-start/src/styles.css | 400 +++ plugins/batch-start/src/types.ts | 60 + plugins/batch-start/tests/appLibrary.test.ts | 154 + plugins/batch-start/tests/category.test.ts | 72 + plugins/batch-start/tests/db.test.ts | 10 + plugins/batch-start/tests/featureSync.test.ts | 43 + plugins/batch-start/tests/launchGroup.test.ts | 31 + plugins/batch-start/tests/launcher.test.ts | 85 + plugins/batch-start/tests/mocks/memoryDb.ts | 37 + .../batch-start/tests/preload-routing.test.ts | 17 + .../batch-start/tests/scanner-filter.test.ts | 21 + .../batch-start/tests/scanner-index.test.ts | 53 + plugins/batch-start/tests/scanner-win.test.ts | 98 + .../batch-start/tests/ztools-plugins.test.ts | 116 + plugins/batch-start/tsconfig.json | 21 + plugins/batch-start/tsconfig.node.json | 13 + plugins/batch-start/vite.config.ts | 79 + plugins/batch-start/vitest.config.ts | 8 + 59 files changed, 9550 insertions(+), 58 deletions(-) create mode 100644 plugins/batch-start/.gitignore delete mode 100644 plugins/batch-start/dist/assets/index-CiJ97cWo.js delete mode 100644 plugins/batch-start/dist/assets/index-DdXE4yVZ.css create mode 100644 plugins/batch-start/docs/superpowers/plans/2026-09-14-ztools-batch-start.md create mode 100644 plugins/batch-start/docs/superpowers/plans/2026-09-17-batch-start-ztools-plugins.md create mode 100644 plugins/batch-start/docs/superpowers/specs/2026-09-14-ztools-batch-start-design.md create mode 100644 plugins/batch-start/docs/superpowers/specs/2026-09-17-batch-start-ztools-plugins-design.md create mode 100644 plugins/batch-start/docs/superpowers/specs/2026-11-11-batch-start-ui-refresh-design.md rename plugins/batch-start/{dist => }/index.html (53%) create mode 100644 plugins/batch-start/package-lock.json create mode 100644 plugins/batch-start/package.json delete mode 100644 plugins/batch-start/preload/services.js rename plugins/batch-start/{dist => public}/logo.png (100%) rename plugins/batch-start/{ => src-ztools}/logo.png (100%) create mode 100644 plugins/batch-start/src-ztools/plugin.json rename plugins/batch-start/{ => src-ztools}/preload/package.json (89%) create mode 100644 plugins/batch-start/src/App.vue create mode 100644 plugins/batch-start/src/components/AppList.vue create mode 100644 plugins/batch-start/src/components/CategorySidebar.vue create mode 100644 plugins/batch-start/src/components/GroupPanel.vue create mode 100644 plugins/batch-start/src/components/TopBar.vue create mode 100644 plugins/batch-start/src/composables/useHostTheme.ts create mode 100644 plugins/batch-start/src/env.d.ts create mode 100644 plugins/batch-start/src/main.ts create mode 100644 plugins/batch-start/src/preload.ts create mode 100644 plugins/batch-start/src/services/appLibrary.ts create mode 100644 plugins/batch-start/src/services/category.ts create mode 100644 plugins/batch-start/src/services/db.ts create mode 100644 plugins/batch-start/src/services/enterRouter.ts create mode 100644 plugins/batch-start/src/services/featureSync.ts create mode 100644 plugins/batch-start/src/services/launchGroup.ts create mode 100644 plugins/batch-start/src/services/launcher.ts create mode 100644 plugins/batch-start/src/services/scanner/filter.ts create mode 100644 plugins/batch-start/src/services/scanner/index.ts create mode 100644 plugins/batch-start/src/services/scanner/linux.ts create mode 100644 plugins/batch-start/src/services/scanner/mac.ts create mode 100644 plugins/batch-start/src/services/scanner/types.ts create mode 100644 plugins/batch-start/src/services/scanner/win.ts create mode 100644 plugins/batch-start/src/services/scanner/ztoolsPlugins.ts create mode 100644 plugins/batch-start/src/styles.css create mode 100644 plugins/batch-start/src/types.ts create mode 100644 plugins/batch-start/tests/appLibrary.test.ts create mode 100644 plugins/batch-start/tests/category.test.ts create mode 100644 plugins/batch-start/tests/db.test.ts create mode 100644 plugins/batch-start/tests/featureSync.test.ts create mode 100644 plugins/batch-start/tests/launchGroup.test.ts create mode 100644 plugins/batch-start/tests/launcher.test.ts create mode 100644 plugins/batch-start/tests/mocks/memoryDb.ts create mode 100644 plugins/batch-start/tests/preload-routing.test.ts create mode 100644 plugins/batch-start/tests/scanner-filter.test.ts create mode 100644 plugins/batch-start/tests/scanner-index.test.ts create mode 100644 plugins/batch-start/tests/scanner-win.test.ts create mode 100644 plugins/batch-start/tests/ztools-plugins.test.ts create mode 100644 plugins/batch-start/tsconfig.json create mode 100644 plugins/batch-start/tsconfig.node.json create mode 100644 plugins/batch-start/vite.config.ts create mode 100644 plugins/batch-start/vitest.config.ts 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/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/**'], + }, +}) From 01e8888410671b5001bd90f7a7cbc040c7a93d41 Mon Sep 17 00:00:00 2001 From: ArthurYang999 Date: Wed, 11 Nov 2026 15:19:25 +0800 Subject: [PATCH 2/2] docs: add manage UI screenshot for v0.1.3 --- .../docs/screenshots/manage-ui-0.1.3.jpg | Bin 0 -> 71008 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/batch-start/docs/screenshots/manage-ui-0.1.3.jpg 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 0000000000000000000000000000000000000000..e770104e88815352b9d25f2c4c8f0c3a7d180540 GIT binary patch literal 71008 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
-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

_{oQ6 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