mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
refactor: modularize preload.ts into domain-specific modules with tests
This commit is contained in:
535
package-lock.json
generated
535
package-lock.json
generated
@@ -78,9 +78,10 @@
|
|||||||
"@vitest/coverage-v8": "^4.0.15",
|
"@vitest/coverage-v8": "^4.0.15",
|
||||||
"@welldone-software/why-did-you-render": "^8.0.3",
|
"@welldone-software/why-did-you-render": "^8.0.3",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
"baseline-browser-mapping": "^2.9.17",
|
||||||
"canvas": "^3.2.0",
|
"canvas": "^3.2.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"electron": "^28.1.0",
|
"electron": "^28.3.3",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-devtools-installer": "^4.0.0",
|
"electron-devtools-installer": "^4.0.0",
|
||||||
"electron-playwright-helpers": "^2.0.1",
|
"electron-playwright-helpers": "^2.0.1",
|
||||||
@@ -91,7 +92,9 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
|
"lint-staged": "^16.2.7",
|
||||||
"lucide-react": "^0.303.0",
|
"lucide-react": "^0.303.0",
|
||||||
"playwright": "^1.57.0",
|
"playwright": "^1.57.0",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
@@ -4914,6 +4917,22 @@
|
|||||||
"ajv": "^6.9.1"
|
"ajv": "^6.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-escapes": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"environment": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@@ -5709,9 +5728,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.30",
|
"version": "2.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz",
|
||||||
"integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
|
"integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -6562,6 +6581,13 @@
|
|||||||
"color-support": "bin.js"
|
"color-support": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "2.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||||
|
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -7890,9 +7916,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
|
||||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
@@ -8764,6 +8790,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/environment": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/err-code": {
|
"node_modules/err-code": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
||||||
@@ -10151,6 +10190,19 @@
|
|||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-east-asian-width": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -10881,6 +10933,22 @@
|
|||||||
"ms": "^2.0.0"
|
"ms": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/husky": {
|
||||||
|
"version": "9.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||||
|
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"husky": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-corefoundation": {
|
"node_modules/iconv-corefoundation": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
|
||||||
@@ -12197,6 +12265,201 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lint-staged": {
|
||||||
|
"version": "16.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
|
||||||
|
"integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^14.0.2",
|
||||||
|
"listr2": "^9.0.5",
|
||||||
|
"micromatch": "^4.0.8",
|
||||||
|
"nano-spawn": "^2.0.0",
|
||||||
|
"pidtree": "^0.6.0",
|
||||||
|
"string-argv": "^0.3.2",
|
||||||
|
"yaml": "^2.8.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"lint-staged": "bin/lint-staged.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cli-truncate": "^5.0.0",
|
||||||
|
"colorette": "^2.0.20",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"log-update": "^6.1.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/cli-truncate": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"slice-ansi": "^7.1.0",
|
||||||
|
"string-width": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/slice-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"is-fullwidth-code-point": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/string-width": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.3.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/wrap-ansi": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"string-width": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/wrap-ansi/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/local-pkg": {
|
"node_modules/local-pkg": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||||
@@ -12311,6 +12574,193 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/log-update": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-escapes": "^7.0.0",
|
||||||
|
"cli-cursor": "^5.0.0",
|
||||||
|
"slice-ansi": "^7.1.0",
|
||||||
|
"strip-ansi": "^7.1.0",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/cli-cursor": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"restore-cursor": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/onetime": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-function": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/restore-cursor": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"onetime": "^7.0.0",
|
||||||
|
"signal-exit": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/slice-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"is-fullwidth-code-point": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/wrap-ansi": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"string-width": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/longest-streak": {
|
"node_modules/longest-streak": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||||
@@ -12826,9 +13276,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdast-util-to-hast": {
|
"node_modules/mdast-util-to-hast": {
|
||||||
"version": "13.2.0",
|
"version": "13.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hast": "^3.0.0",
|
"@types/hast": "^3.0.0",
|
||||||
@@ -13588,6 +14038,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-function": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
@@ -13923,6 +14386,19 @@
|
|||||||
"thenify-all": "^1.0.0"
|
"thenify-all": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nano-spawn": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -14667,6 +15143,19 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pidtree": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"pidtree": "bin/pidtree.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pify": {
|
"node_modules/pify": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
@@ -16939,6 +17428,16 @@
|
|||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-argv": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -19784,6 +20283,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -19,13 +19,14 @@
|
|||||||
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
|
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
|
||||||
"dev:prod-data": "USE_PROD_DATA=1 concurrently \"npm run dev:main:prod-data\" \"npm run dev:renderer\"",
|
"dev:prod-data": "USE_PROD_DATA=1 concurrently \"npm run dev:main:prod-data\" \"npm run dev:renderer\"",
|
||||||
"dev:demo": "MAESTRO_DEMO_DIR=/tmp/maestro-demo npm run dev",
|
"dev:demo": "MAESTRO_DEMO_DIR=/tmp/maestro-demo npm run dev",
|
||||||
"dev:main": "npm run build:prompts && tsc -p tsconfig.main.json && NODE_ENV=development electron .",
|
"dev:main": "npm run build:prompts && tsc -p tsconfig.main.json && npm run build:preload && NODE_ENV=development electron .",
|
||||||
"dev:main:prod-data": "npm run build:prompts && tsc -p tsconfig.main.json && NODE_ENV=development USE_PROD_DATA=1 electron .",
|
"dev:main:prod-data": "npm run build:prompts && tsc -p tsconfig.main.json && npm run build:preload && NODE_ENV=development USE_PROD_DATA=1 electron .",
|
||||||
"dev:renderer": "vite",
|
"dev:renderer": "vite",
|
||||||
"dev:web": "vite --config vite.config.web.mts",
|
"dev:web": "vite --config vite.config.web.mts",
|
||||||
"build": "npm run build:prompts && npm run build:main && npm run build:renderer && npm run build:web && npm run build:cli",
|
"build": "npm run build:prompts && npm run build:main && npm run build:preload && npm run build:renderer && npm run build:web && npm run build:cli",
|
||||||
"build:prompts": "node scripts/generate-prompts.mjs",
|
"build:prompts": "node scripts/generate-prompts.mjs",
|
||||||
"build:main": "tsc -p tsconfig.main.json",
|
"build:main": "tsc -p tsconfig.main.json",
|
||||||
|
"build:preload": "node scripts/build-preload.mjs",
|
||||||
"build:cli": "node scripts/build-cli.mjs",
|
"build:cli": "node scripts/build-cli.mjs",
|
||||||
"build:renderer": "vite build",
|
"build:renderer": "vite build",
|
||||||
"build:web": "vite build --config vite.config.web.mts",
|
"build:web": "vite build --config vite.config.web.mts",
|
||||||
@@ -273,9 +274,10 @@
|
|||||||
"@vitest/coverage-v8": "^4.0.15",
|
"@vitest/coverage-v8": "^4.0.15",
|
||||||
"@welldone-software/why-did-you-render": "^8.0.3",
|
"@welldone-software/why-did-you-render": "^8.0.3",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
"baseline-browser-mapping": "^2.9.17",
|
||||||
"canvas": "^3.2.0",
|
"canvas": "^3.2.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"electron": "^28.1.0",
|
"electron": "^28.3.3",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-devtools-installer": "^4.0.0",
|
"electron-devtools-installer": "^4.0.0",
|
||||||
"electron-playwright-helpers": "^2.0.1",
|
"electron-playwright-helpers": "^2.0.1",
|
||||||
|
|||||||
45
scripts/build-preload.mjs
Normal file
45
scripts/build-preload.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Build script for the Electron preload script using esbuild.
|
||||||
|
*
|
||||||
|
* Bundles the preload script into a single JavaScript file.
|
||||||
|
* This is necessary because Electron's sandboxed preload environment
|
||||||
|
* doesn't support multi-file CommonJS requires the same way Node.js does.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as esbuild from 'esbuild';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const rootDir = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
const outfile = path.join(rootDir, 'dist/main/preload.js');
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
console.log('Building preload script with esbuild...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [path.join(rootDir, 'src/main/preload/index.ts')],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: 'node18', // Match Electron's Node version
|
||||||
|
outfile,
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: false,
|
||||||
|
minify: false, // Keep readable for debugging
|
||||||
|
external: ['electron'], // Don't bundle electron - it's provided by Electron runtime
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = fs.statSync(outfile);
|
||||||
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
||||||
|
console.log(`✓ Built ${outfile} (${sizeKB} KB)`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Preload build failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build();
|
||||||
379
src/__tests__/main/preload/agents.test.ts
Normal file
379
src/__tests__/main/preload/agents.test.ts
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
/**
|
||||||
|
* Tests for agents preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createAgentsApi } from '../../../main/preload/agents';
|
||||||
|
|
||||||
|
describe('Agents Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createAgentsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createAgentsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
it('should invoke agents:detect', async () => {
|
||||||
|
const mockAgents = [
|
||||||
|
{
|
||||||
|
id: 'claude-code',
|
||||||
|
name: 'Claude Code',
|
||||||
|
command: 'claude',
|
||||||
|
available: true,
|
||||||
|
path: '/usr/local/bin/claude',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(mockAgents);
|
||||||
|
|
||||||
|
const result = await api.detect();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:detect', undefined);
|
||||||
|
expect(result).toEqual(mockAgents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agents:detect with SSH remote', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.detect('remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:detect', 'remote-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refresh', () => {
|
||||||
|
it('should invoke agents:refresh without parameters', async () => {
|
||||||
|
const mockResult = { agents: [], debugInfo: {} };
|
||||||
|
mockInvoke.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
const result = await api.refresh();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:refresh', undefined, undefined);
|
||||||
|
expect(result).toEqual(mockResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agents:refresh with agentId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ agents: [], debugInfo: {} });
|
||||||
|
|
||||||
|
await api.refresh('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:refresh', 'claude-code', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agents:refresh with SSH remote', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ agents: [], debugInfo: {} });
|
||||||
|
|
||||||
|
await api.refresh(undefined, 'remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:refresh', undefined, 'remote-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('should invoke agents:get with agentId', async () => {
|
||||||
|
const mockAgent = {
|
||||||
|
id: 'claude-code',
|
||||||
|
name: 'Claude Code',
|
||||||
|
command: 'claude',
|
||||||
|
available: true,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockAgent);
|
||||||
|
|
||||||
|
const result = await api.get('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:get', 'claude-code');
|
||||||
|
expect(result).toEqual(mockAgent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for non-existent agent', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.get('nonexistent');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCapabilities', () => {
|
||||||
|
it('should invoke agents:getCapabilities with agentId', async () => {
|
||||||
|
const mockCapabilities = {
|
||||||
|
supportsResume: true,
|
||||||
|
supportsReadOnlyMode: true,
|
||||||
|
supportsJsonOutput: true,
|
||||||
|
supportsSessionId: true,
|
||||||
|
supportsImageInput: true,
|
||||||
|
supportsImageInputOnResume: false,
|
||||||
|
supportsSlashCommands: true,
|
||||||
|
supportsSessionStorage: true,
|
||||||
|
supportsCostTracking: true,
|
||||||
|
supportsUsageStats: true,
|
||||||
|
supportsBatchMode: true,
|
||||||
|
requiresPromptToStart: false,
|
||||||
|
supportsStreaming: true,
|
||||||
|
supportsResultMessages: true,
|
||||||
|
supportsModelSelection: false,
|
||||||
|
supportsStreamJsonInput: true,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockCapabilities);
|
||||||
|
|
||||||
|
const result = await api.getCapabilities('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getCapabilities', 'claude-code');
|
||||||
|
expect(result).toEqual(mockCapabilities);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfig', () => {
|
||||||
|
it('should invoke agents:getConfig with agentId', async () => {
|
||||||
|
const mockConfig = { theme: 'dark', autoSave: true };
|
||||||
|
mockInvoke.mockResolvedValue(mockConfig);
|
||||||
|
|
||||||
|
const result = await api.getConfig('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getConfig', 'claude-code');
|
||||||
|
expect(result).toEqual(mockConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setConfig', () => {
|
||||||
|
it('should invoke agents:setConfig with agentId and config', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.setConfig('claude-code', { theme: 'light' });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:setConfig', 'claude-code', {
|
||||||
|
theme: 'light',
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigValue', () => {
|
||||||
|
it('should invoke agents:getConfigValue with agentId and key', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('dark');
|
||||||
|
|
||||||
|
const result = await api.getConfigValue('claude-code', 'theme');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getConfigValue', 'claude-code', 'theme');
|
||||||
|
expect(result).toBe('dark');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setConfigValue', () => {
|
||||||
|
it('should invoke agents:setConfigValue with agentId, key, and value', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.setConfigValue('claude-code', 'theme', 'light');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agents:setConfigValue',
|
||||||
|
'claude-code',
|
||||||
|
'theme',
|
||||||
|
'light'
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCustomPath', () => {
|
||||||
|
it('should invoke agents:setCustomPath with agentId and customPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.setCustomPath('claude-code', '/custom/path/claude');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agents:setCustomPath',
|
||||||
|
'claude-code',
|
||||||
|
'/custom/path/claude'
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agents:setCustomPath with null to clear', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.setCustomPath('claude-code', null);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:setCustomPath', 'claude-code', null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCustomPath', () => {
|
||||||
|
it('should invoke agents:getCustomPath with agentId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/custom/path/claude');
|
||||||
|
|
||||||
|
const result = await api.getCustomPath('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getCustomPath', 'claude-code');
|
||||||
|
expect(result).toBe('/custom/path/claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no custom path', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.getCustomPath('claude-code');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllCustomPaths', () => {
|
||||||
|
it('should invoke agents:getAllCustomPaths', async () => {
|
||||||
|
const mockPaths = {
|
||||||
|
'claude-code': '/custom/claude',
|
||||||
|
codex: '/custom/codex',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockPaths);
|
||||||
|
|
||||||
|
const result = await api.getAllCustomPaths();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getAllCustomPaths');
|
||||||
|
expect(result).toEqual(mockPaths);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCustomArgs', () => {
|
||||||
|
it('should invoke agents:setCustomArgs', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.setCustomArgs('claude-code', '--verbose --debug');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agents:setCustomArgs',
|
||||||
|
'claude-code',
|
||||||
|
'--verbose --debug'
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCustomArgs', () => {
|
||||||
|
it('should invoke agents:getCustomArgs', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('--verbose');
|
||||||
|
|
||||||
|
const result = await api.getCustomArgs('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getCustomArgs', 'claude-code');
|
||||||
|
expect(result).toBe('--verbose');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllCustomArgs', () => {
|
||||||
|
it('should invoke agents:getAllCustomArgs', async () => {
|
||||||
|
const mockArgs = { 'claude-code': '--verbose' };
|
||||||
|
mockInvoke.mockResolvedValue(mockArgs);
|
||||||
|
|
||||||
|
const result = await api.getAllCustomArgs();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getAllCustomArgs');
|
||||||
|
expect(result).toEqual(mockArgs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCustomEnvVars', () => {
|
||||||
|
it('should invoke agents:setCustomEnvVars', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const envVars = { CLAUDE_API_KEY: 'test-key' };
|
||||||
|
const result = await api.setCustomEnvVars('claude-code', envVars);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:setCustomEnvVars', 'claude-code', envVars);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCustomEnvVars', () => {
|
||||||
|
it('should invoke agents:getCustomEnvVars', async () => {
|
||||||
|
const envVars = { CLAUDE_API_KEY: 'test-key' };
|
||||||
|
mockInvoke.mockResolvedValue(envVars);
|
||||||
|
|
||||||
|
const result = await api.getCustomEnvVars('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getCustomEnvVars', 'claude-code');
|
||||||
|
expect(result).toEqual(envVars);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllCustomEnvVars', () => {
|
||||||
|
it('should invoke agents:getAllCustomEnvVars', async () => {
|
||||||
|
const allEnvVars = {
|
||||||
|
'claude-code': { CLAUDE_API_KEY: 'key1' },
|
||||||
|
codex: { OPENAI_API_KEY: 'key2' },
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(allEnvVars);
|
||||||
|
|
||||||
|
const result = await api.getAllCustomEnvVars();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getAllCustomEnvVars');
|
||||||
|
expect(result).toEqual(allEnvVars);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getModels', () => {
|
||||||
|
it('should invoke agents:getModels with agentId', async () => {
|
||||||
|
const models = ['gpt-4', 'gpt-3.5-turbo'];
|
||||||
|
mockInvoke.mockResolvedValue(models);
|
||||||
|
|
||||||
|
const result = await api.getModels('opencode');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getModels', 'opencode', undefined);
|
||||||
|
expect(result).toEqual(models);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agents:getModels with forceRefresh', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.getModels('opencode', true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agents:getModels', 'opencode', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('discoverSlashCommands', () => {
|
||||||
|
it('should invoke agents:discoverSlashCommands', async () => {
|
||||||
|
const commands = ['compact', 'help', 'review'];
|
||||||
|
mockInvoke.mockResolvedValue(commands);
|
||||||
|
|
||||||
|
const result = await api.discoverSlashCommands('claude-code', '/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agents:discoverSlashCommands',
|
||||||
|
'claude-code',
|
||||||
|
'/home/user/project',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toEqual(commands);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agents:discoverSlashCommands with customPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['help']);
|
||||||
|
|
||||||
|
await api.discoverSlashCommands('claude-code', '/home/user/project', '/custom/claude');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agents:discoverSlashCommands',
|
||||||
|
'claude-code',
|
||||||
|
'/home/user/project',
|
||||||
|
'/custom/claude'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when discovery fails', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.discoverSlashCommands('unknown-agent', '/home/user/project');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
129
src/__tests__/main/preload/attachments.test.ts
Normal file
129
src/__tests__/main/preload/attachments.test.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Tests for attachments preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createAttachmentsApi } from '../../../main/preload/attachments';
|
||||||
|
|
||||||
|
describe('Attachments Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createAttachmentsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createAttachmentsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('save', () => {
|
||||||
|
it('should invoke attachments:save with sessionId, base64Data, and filename', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
path: '/path/to/file',
|
||||||
|
filename: 'image.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.save('session-123', 'base64data', 'image.png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'attachments:save',
|
||||||
|
'session-123',
|
||||||
|
'base64data',
|
||||||
|
'image.png'
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.path).toBe('/path/to/file');
|
||||||
|
expect(result.filename).toBe('image.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: false, error: 'Save failed' });
|
||||||
|
|
||||||
|
const result = await api.save('session-123', 'base64data', 'image.png');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Save failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('load', () => {
|
||||||
|
it('should invoke attachments:load with sessionId and filename', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
dataUrl: 'data:image/png;base64,abc123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.load('session-123', 'image.png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('attachments:load', 'session-123', 'image.png');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.dataUrl).toBe('data:image/png;base64,abc123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing files', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: false, error: 'File not found' });
|
||||||
|
|
||||||
|
const result = await api.load('session-123', 'nonexistent.png');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('File not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should invoke attachments:delete with sessionId and filename', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.delete('session-123', 'image.png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('attachments:delete', 'session-123', 'image.png');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('should invoke attachments:list with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
files: ['image1.png', 'image2.jpg'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.list('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('attachments:list', 'session-123');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.files).toEqual(['image1.png', 'image2.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when no files exist', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, files: [] });
|
||||||
|
|
||||||
|
const result = await api.list('session-123');
|
||||||
|
|
||||||
|
expect(result.files).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPath', () => {
|
||||||
|
it('should invoke attachments:getPath with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
path: '/home/user/.maestro/attachments/session-123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.getPath('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('attachments:getPath', 'session-123');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.path).toBe('/home/user/.maestro/attachments/session-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
490
src/__tests__/main/preload/autorun.test.ts
Normal file
490
src/__tests__/main/preload/autorun.test.ts
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
/**
|
||||||
|
* Tests for autorun preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAutorunApi,
|
||||||
|
createPlaybooksApi,
|
||||||
|
createMarketplaceApi,
|
||||||
|
} from '../../../main/preload/autorun';
|
||||||
|
|
||||||
|
describe('Autorun Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAutorunApi', () => {
|
||||||
|
let api: ReturnType<typeof createAutorunApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createAutorunApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listDocs', () => {
|
||||||
|
it('should invoke autorun:listDocs', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['doc1.md', 'doc2.md']);
|
||||||
|
|
||||||
|
const result = await api.listDocs('/project/.maestro');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('autorun:listDocs', '/project/.maestro', undefined);
|
||||||
|
expect(result).toEqual(['doc1.md', 'doc2.md']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with sshRemoteId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.listDocs('/project/.maestro', 'ssh-remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:listDocs',
|
||||||
|
'/project/.maestro',
|
||||||
|
'ssh-remote-1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasDocuments', () => {
|
||||||
|
it('should invoke autorun:hasDocuments', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ hasDocuments: true });
|
||||||
|
|
||||||
|
const result = await api.hasDocuments('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('autorun:hasDocuments', '/project');
|
||||||
|
expect(result.hasDocuments).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readDoc', () => {
|
||||||
|
it('should invoke autorun:readDoc', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('# Document Content');
|
||||||
|
|
||||||
|
const result = await api.readDoc('/project/.maestro', 'tasks.md');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:readDoc',
|
||||||
|
'/project/.maestro',
|
||||||
|
'tasks.md',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toBe('# Document Content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('writeDoc', () => {
|
||||||
|
it('should invoke autorun:writeDoc', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.writeDoc('/project/.maestro', 'tasks.md', '# New Content');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:writeDoc',
|
||||||
|
'/project/.maestro',
|
||||||
|
'tasks.md',
|
||||||
|
'# New Content',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveImage', () => {
|
||||||
|
it('should invoke autorun:saveImage', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ path: 'image.png' });
|
||||||
|
|
||||||
|
await api.saveImage('/project/.maestro', 'doc1', 'base64data', 'png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:saveImage',
|
||||||
|
'/project/.maestro',
|
||||||
|
'doc1',
|
||||||
|
'base64data',
|
||||||
|
'png'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteImage', () => {
|
||||||
|
it('should invoke autorun:deleteImage', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteImage('/project/.maestro', 'images/image.png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:deleteImage',
|
||||||
|
'/project/.maestro',
|
||||||
|
'images/image.png'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listImages', () => {
|
||||||
|
it('should invoke autorun:listImages', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['image1.png', 'image2.jpg']);
|
||||||
|
|
||||||
|
const result = await api.listImages('/project/.maestro', 'doc1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('autorun:listImages', '/project/.maestro', 'doc1');
|
||||||
|
expect(result).toEqual(['image1.png', 'image2.jpg']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteFolder', () => {
|
||||||
|
it('should invoke autorun:deleteFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteFolder('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('autorun:deleteFolder', '/project');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('watchFolder', () => {
|
||||||
|
it('should invoke autorun:watchFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({});
|
||||||
|
|
||||||
|
await api.watchFolder('/project/.maestro');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:watchFolder',
|
||||||
|
'/project/.maestro',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle remote folder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
isRemote: true,
|
||||||
|
message: 'File watching not available for remote',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.watchFolder('/project/.maestro', 'ssh-remote-1');
|
||||||
|
|
||||||
|
expect(result.isRemote).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unwatchFolder', () => {
|
||||||
|
it('should invoke autorun:unwatchFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.unwatchFolder('/project/.maestro');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('autorun:unwatchFolder', '/project/.maestro');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onFileChanged', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onFileChanged(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('autorun:fileChanged', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, data: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, data: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onFileChanged(callback);
|
||||||
|
|
||||||
|
const data = { folderPath: '/project', filename: 'tasks.md', eventType: 'change' };
|
||||||
|
registeredHandler!({}, data);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createBackup', () => {
|
||||||
|
it('should invoke autorun:createBackup', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.createBackup('/project/.maestro', 'tasks.md');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:createBackup',
|
||||||
|
'/project/.maestro',
|
||||||
|
'tasks.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('restoreBackup', () => {
|
||||||
|
it('should invoke autorun:restoreBackup', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.restoreBackup('/project/.maestro', 'tasks.md');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:restoreBackup',
|
||||||
|
'/project/.maestro',
|
||||||
|
'tasks.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteBackups', () => {
|
||||||
|
it('should invoke autorun:deleteBackups', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteBackups('/project/.maestro');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('autorun:deleteBackups', '/project/.maestro');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createWorkingCopy', () => {
|
||||||
|
it('should invoke autorun:createWorkingCopy', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
workingCopyPath: '/project/.maestro/tasks.loop-1.md',
|
||||||
|
originalPath: '/project/.maestro/tasks.md',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.createWorkingCopy('/project/.maestro', 'tasks.md', 1);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'autorun:createWorkingCopy',
|
||||||
|
'/project/.maestro',
|
||||||
|
'tasks.md',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(result.workingCopyPath).toContain('loop-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPlaybooksApi', () => {
|
||||||
|
let api: ReturnType<typeof createPlaybooksApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createPlaybooksApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('should invoke playbooks:list', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([{ id: 'pb-1', name: 'Playbook 1' }]);
|
||||||
|
|
||||||
|
const result = await api.list('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('playbooks:list', 'session-123');
|
||||||
|
expect(result).toEqual([{ id: 'pb-1', name: 'Playbook 1' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should invoke playbooks:create', async () => {
|
||||||
|
const playbook = {
|
||||||
|
name: 'New Playbook',
|
||||||
|
documents: [{ filename: 'tasks.md', resetOnCompletion: false }],
|
||||||
|
loopEnabled: false,
|
||||||
|
prompt: 'Run these tasks',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'pb-new' });
|
||||||
|
|
||||||
|
await api.create('session-123', playbook);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('playbooks:create', 'session-123', playbook);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should invoke playbooks:update', async () => {
|
||||||
|
const updates = { name: 'Updated Name' };
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.update('session-123', 'pb-1', updates);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('playbooks:update', 'session-123', 'pb-1', updates);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should invoke playbooks:delete', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.delete('session-123', 'pb-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('playbooks:delete', 'session-123', 'pb-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteAll', () => {
|
||||||
|
it('should invoke playbooks:deleteAll', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteAll('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('playbooks:deleteAll', 'session-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('export', () => {
|
||||||
|
it('should invoke playbooks:export', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, path: '/export/playbook.zip' });
|
||||||
|
|
||||||
|
await api.export('session-123', 'pb-1', '/project/.maestro');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'playbooks:export',
|
||||||
|
'session-123',
|
||||||
|
'pb-1',
|
||||||
|
'/project/.maestro'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('import', () => {
|
||||||
|
it('should invoke playbooks:import', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, playbookId: 'pb-imported' });
|
||||||
|
|
||||||
|
await api.import('session-123', '/project/.maestro');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'playbooks:import',
|
||||||
|
'session-123',
|
||||||
|
'/project/.maestro'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createMarketplaceApi', () => {
|
||||||
|
let api: ReturnType<typeof createMarketplaceApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createMarketplaceApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getManifest', () => {
|
||||||
|
it('should invoke marketplace:getManifest', async () => {
|
||||||
|
const manifest = { playbooks: [{ id: 'mp-1', name: 'Marketplace Playbook' }] };
|
||||||
|
mockInvoke.mockResolvedValue(manifest);
|
||||||
|
|
||||||
|
const result = await api.getManifest();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('marketplace:getManifest');
|
||||||
|
expect(result).toEqual(manifest);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refreshManifest', () => {
|
||||||
|
it('should invoke marketplace:refreshManifest', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.refreshManifest();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('marketplace:refreshManifest');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDocument', () => {
|
||||||
|
it('should invoke marketplace:getDocument', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('# Document content');
|
||||||
|
|
||||||
|
const result = await api.getDocument('playbooks/example', 'tasks.md');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'marketplace:getDocument',
|
||||||
|
'playbooks/example',
|
||||||
|
'tasks.md'
|
||||||
|
);
|
||||||
|
expect(result).toBe('# Document content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getReadme', () => {
|
||||||
|
it('should invoke marketplace:getReadme', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('# README');
|
||||||
|
|
||||||
|
const result = await api.getReadme('playbooks/example');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('marketplace:getReadme', 'playbooks/example');
|
||||||
|
expect(result).toBe('# README');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('importPlaybook', () => {
|
||||||
|
it('should invoke marketplace:importPlaybook', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.importPlaybook('mp-1', 'my-playbook', '/project/.maestro', 'session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'marketplace:importPlaybook',
|
||||||
|
'mp-1',
|
||||||
|
'my-playbook',
|
||||||
|
'/project/.maestro',
|
||||||
|
'session-123',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with sshRemoteId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.importPlaybook(
|
||||||
|
'mp-1',
|
||||||
|
'my-playbook',
|
||||||
|
'/project/.maestro',
|
||||||
|
'session-123',
|
||||||
|
'ssh-remote-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'marketplace:importPlaybook',
|
||||||
|
'mp-1',
|
||||||
|
'my-playbook',
|
||||||
|
'/project/.maestro',
|
||||||
|
'session-123',
|
||||||
|
'ssh-remote-1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onManifestChanged', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onManifestChanged(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('marketplace:manifestChanged', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onManifestChanged(callback);
|
||||||
|
registeredHandler!();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
285
src/__tests__/main/preload/commands.test.ts
Normal file
285
src/__tests__/main/preload/commands.test.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Tests for commands preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createSpeckitApi: getMetadata, getPrompts, getCommand, savePrompt, resetPrompt, refresh
|
||||||
|
* - createOpenspecApi: getMetadata, getPrompts, getCommand, savePrompt, resetPrompt, refresh
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createSpeckitApi, createOpenspecApi } from '../../../main/preload/commands';
|
||||||
|
|
||||||
|
describe('Commands Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSpeckitApi', () => {
|
||||||
|
let api: ReturnType<typeof createSpeckitApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createSpeckitApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMetadata', () => {
|
||||||
|
it('should invoke speckit:getMetadata', async () => {
|
||||||
|
const metadata = {
|
||||||
|
success: true,
|
||||||
|
metadata: {
|
||||||
|
lastRefreshed: '2024-01-01',
|
||||||
|
commitSha: 'abc123',
|
||||||
|
sourceVersion: '1.0.0',
|
||||||
|
sourceUrl: 'https://github.com/example/speckit',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(metadata);
|
||||||
|
|
||||||
|
const result = await api.getMetadata();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('speckit:getMetadata');
|
||||||
|
expect(result).toEqual(metadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPrompts', () => {
|
||||||
|
it('should invoke speckit:getPrompts', async () => {
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: 'cmd-1',
|
||||||
|
command: '/test',
|
||||||
|
description: 'Test command',
|
||||||
|
prompt: 'Test prompt',
|
||||||
|
isCustom: false,
|
||||||
|
isModified: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.getPrompts();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('speckit:getPrompts');
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommand', () => {
|
||||||
|
it('should invoke speckit:getCommand', async () => {
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
command: {
|
||||||
|
id: 'cmd-1',
|
||||||
|
command: '/test',
|
||||||
|
description: 'Test command',
|
||||||
|
prompt: 'Test prompt',
|
||||||
|
isCustom: false,
|
||||||
|
isModified: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.getCommand('/test');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('speckit:getCommand', '/test');
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command not found', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, command: null });
|
||||||
|
|
||||||
|
const result = await api.getCommand('/nonexistent');
|
||||||
|
|
||||||
|
expect(result.command).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('savePrompt', () => {
|
||||||
|
it('should invoke speckit:savePrompt', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.savePrompt('cmd-1', 'Updated prompt content');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'speckit:savePrompt',
|
||||||
|
'cmd-1',
|
||||||
|
'Updated prompt content'
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle save error', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: false, error: 'Failed to save' });
|
||||||
|
|
||||||
|
const result = await api.savePrompt('cmd-1', 'content');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Failed to save');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetPrompt', () => {
|
||||||
|
it('should invoke speckit:resetPrompt', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, prompt: 'Original prompt' });
|
||||||
|
|
||||||
|
const result = await api.resetPrompt('cmd-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('speckit:resetPrompt', 'cmd-1');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.prompt).toBe('Original prompt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refresh', () => {
|
||||||
|
it('should invoke speckit:refresh', async () => {
|
||||||
|
const metadata = {
|
||||||
|
success: true,
|
||||||
|
metadata: {
|
||||||
|
lastRefreshed: '2024-01-02',
|
||||||
|
commitSha: 'def456',
|
||||||
|
sourceVersion: '1.0.1',
|
||||||
|
sourceUrl: 'https://github.com/example/speckit',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(metadata);
|
||||||
|
|
||||||
|
const result = await api.refresh();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('speckit:refresh');
|
||||||
|
expect(result).toEqual(metadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOpenspecApi', () => {
|
||||||
|
let api: ReturnType<typeof createOpenspecApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createOpenspecApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMetadata', () => {
|
||||||
|
it('should invoke openspec:getMetadata', async () => {
|
||||||
|
const metadata = {
|
||||||
|
success: true,
|
||||||
|
metadata: {
|
||||||
|
lastRefreshed: '2024-01-01',
|
||||||
|
commitSha: 'abc123',
|
||||||
|
sourceVersion: '1.0.0',
|
||||||
|
sourceUrl: 'https://github.com/example/openspec',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(metadata);
|
||||||
|
|
||||||
|
const result = await api.getMetadata();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('openspec:getMetadata');
|
||||||
|
expect(result).toEqual(metadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPrompts', () => {
|
||||||
|
it('should invoke openspec:getPrompts', async () => {
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: 'spec-1',
|
||||||
|
command: '/spec',
|
||||||
|
description: 'Spec command',
|
||||||
|
prompt: 'Spec prompt',
|
||||||
|
isCustom: false,
|
||||||
|
isModified: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.getPrompts();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('openspec:getPrompts');
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommand', () => {
|
||||||
|
it('should invoke openspec:getCommand', async () => {
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
command: {
|
||||||
|
id: 'spec-1',
|
||||||
|
command: '/spec',
|
||||||
|
description: 'Spec command',
|
||||||
|
prompt: 'Spec prompt',
|
||||||
|
isCustom: false,
|
||||||
|
isModified: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.getCommand('/spec');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('openspec:getCommand', '/spec');
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('savePrompt', () => {
|
||||||
|
it('should invoke openspec:savePrompt', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.savePrompt('spec-1', 'Updated spec prompt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'openspec:savePrompt',
|
||||||
|
'spec-1',
|
||||||
|
'Updated spec prompt'
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetPrompt', () => {
|
||||||
|
it('should invoke openspec:resetPrompt', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, prompt: 'Original spec prompt' });
|
||||||
|
|
||||||
|
const result = await api.resetPrompt('spec-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('openspec:resetPrompt', 'spec-1');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.prompt).toBe('Original spec prompt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refresh', () => {
|
||||||
|
it('should invoke openspec:refresh', async () => {
|
||||||
|
const metadata = {
|
||||||
|
success: true,
|
||||||
|
metadata: {
|
||||||
|
lastRefreshed: '2024-01-02',
|
||||||
|
commitSha: 'xyz789',
|
||||||
|
sourceVersion: '2.0.0',
|
||||||
|
sourceUrl: 'https://github.com/example/openspec',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(metadata);
|
||||||
|
|
||||||
|
const result = await api.refresh();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('openspec:refresh');
|
||||||
|
expect(result).toEqual(metadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
132
src/__tests__/main/preload/context.test.ts
Normal file
132
src/__tests__/main/preload/context.test.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Tests for context preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createContextApi: getStoredSession, groomContext, cancelGrooming,
|
||||||
|
* createGroomingSession (deprecated), sendGroomingPrompt (deprecated), cleanupGroomingSession
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createContextApi } from '../../../main/preload/context';
|
||||||
|
|
||||||
|
describe('Context Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createContextApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createContextApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStoredSession', () => {
|
||||||
|
it('should invoke context:getStoredSession with correct parameters', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
messages: [{ type: 'user', content: 'Hello', timestamp: '2024-01-01', uuid: '123' }],
|
||||||
|
total: 1,
|
||||||
|
hasMore: false,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await api.getStoredSession('claude-code', '/project', 'session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'context:getStoredSession',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123'
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for non-existent session', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.getStoredSession('claude-code', '/project', 'non-existent');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('groomContext', () => {
|
||||||
|
it('should invoke context:groomContext with correct parameters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('groomed context response');
|
||||||
|
|
||||||
|
const result = await api.groomContext('/project', 'claude-code', 'summarize this');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'context:groomContext',
|
||||||
|
'/project',
|
||||||
|
'claude-code',
|
||||||
|
'summarize this'
|
||||||
|
);
|
||||||
|
expect(result).toBe('groomed context response');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate errors from IPC', async () => {
|
||||||
|
mockInvoke.mockRejectedValue(new Error('Grooming failed'));
|
||||||
|
|
||||||
|
await expect(api.groomContext('/project', 'claude-code', 'prompt')).rejects.toThrow(
|
||||||
|
'Grooming failed'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cancelGrooming', () => {
|
||||||
|
it('should invoke context:cancelGrooming', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.cancelGrooming();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('context:cancelGrooming');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createGroomingSession (deprecated)', () => {
|
||||||
|
it('should invoke context:createGroomingSession', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('grooming-session-id');
|
||||||
|
|
||||||
|
const result = await api.createGroomingSession('/project', 'claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'context:createGroomingSession',
|
||||||
|
'/project',
|
||||||
|
'claude-code'
|
||||||
|
);
|
||||||
|
expect(result).toBe('grooming-session-id');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendGroomingPrompt (deprecated)', () => {
|
||||||
|
it('should invoke context:sendGroomingPrompt', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('response text');
|
||||||
|
|
||||||
|
const result = await api.sendGroomingPrompt('session-123', 'prompt text');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'context:sendGroomingPrompt',
|
||||||
|
'session-123',
|
||||||
|
'prompt text'
|
||||||
|
);
|
||||||
|
expect(result).toBe('response text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanupGroomingSession', () => {
|
||||||
|
it('should invoke context:cleanupGroomingSession', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.cleanupGroomingSession('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('context:cleanupGroomingSession', 'session-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
171
src/__tests__/main/preload/debug.test.ts
Normal file
171
src/__tests__/main/preload/debug.test.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* Tests for debug preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createDebugApi, createDocumentGraphApi } from '../../../main/preload/debug';
|
||||||
|
|
||||||
|
describe('Debug Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDebugApi', () => {
|
||||||
|
let api: ReturnType<typeof createDebugApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createDebugApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPackage', () => {
|
||||||
|
it('should invoke debug:createPackage without options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, path: '/tmp/debug-package.zip' });
|
||||||
|
|
||||||
|
const result = await api.createPackage();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('debug:createPackage', undefined);
|
||||||
|
expect(result).toEqual({ success: true, path: '/tmp/debug-package.zip' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke debug:createPackage with options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, path: '/tmp/debug-package.zip' });
|
||||||
|
const options = {
|
||||||
|
includeLogs: true,
|
||||||
|
includeErrors: true,
|
||||||
|
includeSessions: false,
|
||||||
|
includeGroupChats: false,
|
||||||
|
includeBatchState: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.createPackage(options);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('debug:createPackage', options);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: false, error: 'Failed to create package' });
|
||||||
|
|
||||||
|
const result = await api.createPackage();
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('previewPackage', () => {
|
||||||
|
it('should invoke debug:previewPackage', async () => {
|
||||||
|
const preview = {
|
||||||
|
logs: 150,
|
||||||
|
errors: 5,
|
||||||
|
sessions: 10,
|
||||||
|
groupChats: 2,
|
||||||
|
estimatedSize: '5.2 MB',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(preview);
|
||||||
|
|
||||||
|
const result = await api.previewPackage();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('debug:previewPackage');
|
||||||
|
expect(result).toEqual(preview);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDocumentGraphApi', () => {
|
||||||
|
let api: ReturnType<typeof createDocumentGraphApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createDocumentGraphApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('watchFolder', () => {
|
||||||
|
it('should invoke documentGraph:watchFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.watchFolder('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('documentGraph:watchFolder', '/project');
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unwatchFolder', () => {
|
||||||
|
it('should invoke documentGraph:unwatchFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.unwatchFolder('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('documentGraph:unwatchFolder', '/project');
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onFilesChanged', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onFilesChanged(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('documentGraph:filesChanged', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, data: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, data: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onFilesChanged(callback);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
rootPath: '/project',
|
||||||
|
changes: [
|
||||||
|
{ filePath: '/project/file1.ts', eventType: 'add' as const },
|
||||||
|
{ filePath: '/project/file2.ts', eventType: 'change' as const },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
registeredHandler!({}, data);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, data: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, data: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onFilesChanged(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'documentGraph:filesChanged',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
361
src/__tests__/main/preload/files.test.ts
Normal file
361
src/__tests__/main/preload/files.test.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/**
|
||||||
|
* Tests for files preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createTempfileApi: write, read, delete
|
||||||
|
* - createHistoryApi: getAll, getAllPaginated, add, clear, delete, update, updateSessionName,
|
||||||
|
* getFilePath, listSessions, onExternalChange, reload
|
||||||
|
* - createCliApi: getActivity, onActivityChange
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createTempfileApi, createHistoryApi, createCliApi } from '../../../main/preload/files';
|
||||||
|
|
||||||
|
describe('Files Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTempfileApi', () => {
|
||||||
|
let api: ReturnType<typeof createTempfileApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createTempfileApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('write', () => {
|
||||||
|
it('should invoke tempfile:write with content', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/tmp/tempfile-123.txt');
|
||||||
|
|
||||||
|
const result = await api.write('file content');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tempfile:write', 'file content', undefined);
|
||||||
|
expect(result).toBe('/tmp/tempfile-123.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke tempfile:write with filename', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/tmp/custom-name.txt');
|
||||||
|
|
||||||
|
const result = await api.write('file content', 'custom-name.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'tempfile:write',
|
||||||
|
'file content',
|
||||||
|
'custom-name.txt'
|
||||||
|
);
|
||||||
|
expect(result).toBe('/tmp/custom-name.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read', () => {
|
||||||
|
it('should invoke tempfile:read with path', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('file content');
|
||||||
|
|
||||||
|
const result = await api.read('/tmp/tempfile-123.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tempfile:read', '/tmp/tempfile-123.txt');
|
||||||
|
expect(result).toBe('file content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should invoke tempfile:delete with path', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.delete('/tmp/tempfile-123.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tempfile:delete', '/tmp/tempfile-123.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createHistoryApi', () => {
|
||||||
|
let api: ReturnType<typeof createHistoryApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createHistoryApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should invoke history:getAll without parameters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.getAll();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:getAll', undefined, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke history:getAll with projectPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.getAll('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:getAll', '/project', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke history:getAll with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.getAll('/project', 'session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:getAll', '/project', 'session-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllPaginated', () => {
|
||||||
|
it('should invoke history:getAllPaginated with options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ entries: [], total: 0 });
|
||||||
|
const options = {
|
||||||
|
projectPath: '/project',
|
||||||
|
sessionId: 'session-123',
|
||||||
|
pagination: { limit: 50, offset: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.getAllPaginated(options);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:getAllPaginated', options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('add', () => {
|
||||||
|
it('should invoke history:add with entry', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'entry-123' });
|
||||||
|
const entry = {
|
||||||
|
id: 'entry-123',
|
||||||
|
type: 'USER' as const,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
summary: 'Test entry',
|
||||||
|
projectPath: '/project',
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.add(entry);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:add', entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('should invoke history:clear without projectPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.clear();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:clear', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke history:clear with projectPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.clear('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:clear', '/project');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should invoke history:delete with entryId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.delete('entry-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:delete', 'entry-123', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke history:delete with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.delete('entry-123', 'session-456');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:delete', 'entry-123', 'session-456');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should invoke history:update with updates', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.update('entry-123', { validated: true });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'history:update',
|
||||||
|
'entry-123',
|
||||||
|
{ validated: true },
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke history:update with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.update('entry-123', { validated: false }, 'session-456');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'history:update',
|
||||||
|
'entry-123',
|
||||||
|
{ validated: false },
|
||||||
|
'session-456'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateSessionName', () => {
|
||||||
|
it('should invoke history:updateSessionName', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.updateSessionName('agent-session-123', 'New Session Name');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'history:updateSessionName',
|
||||||
|
'agent-session-123',
|
||||||
|
'New Session Name'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilePath', () => {
|
||||||
|
it('should invoke history:getFilePath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/path/to/history.json');
|
||||||
|
|
||||||
|
const result = await api.getFilePath('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:getFilePath', 'session-123');
|
||||||
|
expect(result).toBe('/path/to/history.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listSessions', () => {
|
||||||
|
it('should invoke history:listSessions', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['session-1', 'session-2']);
|
||||||
|
|
||||||
|
const result = await api.listSessions();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:listSessions');
|
||||||
|
expect(result).toEqual(['session-1', 'session-2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onExternalChange', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onExternalChange(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('history:externalChange', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onExternalChange(callback);
|
||||||
|
registeredHandler!();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = api.onExternalChange(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'history:externalChange',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reload', () => {
|
||||||
|
it('should invoke history:reload', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.reload();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('history:reload');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createCliApi', () => {
|
||||||
|
let api: ReturnType<typeof createCliApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createCliApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getActivity', () => {
|
||||||
|
it('should invoke cli:getActivity', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ active: true, pid: 12345 });
|
||||||
|
|
||||||
|
const result = await api.getActivity();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('cli:getActivity');
|
||||||
|
expect(result).toEqual({ active: true, pid: 12345 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onActivityChange', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onActivityChange(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('cli:activityChange', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onActivityChange(callback);
|
||||||
|
registeredHandler!();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = api.onActivityChange(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith('cli:activityChange', registeredHandler!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
212
src/__tests__/main/preload/fs.test.ts
Normal file
212
src/__tests__/main/preload/fs.test.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* Tests for filesystem preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createFsApi } from '../../../main/preload/fs';
|
||||||
|
|
||||||
|
describe('Filesystem Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createFsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createFsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('homeDir', () => {
|
||||||
|
it('should invoke fs:homeDir', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/home/user');
|
||||||
|
|
||||||
|
const result = await api.homeDir();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:homeDir');
|
||||||
|
expect(result).toBe('/home/user');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readDir', () => {
|
||||||
|
it('should invoke fs:readDir with dirPath', async () => {
|
||||||
|
const mockEntries = [
|
||||||
|
{ name: 'file.txt', isDirectory: false, path: '/home/user/file.txt' },
|
||||||
|
{ name: 'subdir', isDirectory: true, path: '/home/user/subdir' },
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(mockEntries);
|
||||||
|
|
||||||
|
const result = await api.readDir('/home/user');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:readDir', '/home/user', undefined);
|
||||||
|
expect(result).toEqual(mockEntries);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke fs:readDir with SSH remote', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.readDir('/home/user', 'remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:readDir', '/home/user', 'remote-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readFile', () => {
|
||||||
|
it('should invoke fs:readFile with filePath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('file contents');
|
||||||
|
|
||||||
|
const result = await api.readFile('/home/user/file.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:readFile', '/home/user/file.txt', undefined);
|
||||||
|
expect(result).toBe('file contents');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke fs:readFile with SSH remote', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('remote file contents');
|
||||||
|
|
||||||
|
await api.readFile('/home/user/file.txt', 'remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:readFile', '/home/user/file.txt', 'remote-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('writeFile', () => {
|
||||||
|
it('should invoke fs:writeFile with filePath and content', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.writeFile('/home/user/file.txt', 'new contents');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'fs:writeFile',
|
||||||
|
'/home/user/file.txt',
|
||||||
|
'new contents'
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stat', () => {
|
||||||
|
it('should invoke fs:stat with filePath', async () => {
|
||||||
|
const mockStat = {
|
||||||
|
size: 1024,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
modifiedAt: '2024-01-02T00:00:00Z',
|
||||||
|
isDirectory: false,
|
||||||
|
isFile: true,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockStat);
|
||||||
|
|
||||||
|
const result = await api.stat('/home/user/file.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:stat', '/home/user/file.txt', undefined);
|
||||||
|
expect(result).toEqual(mockStat);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('directorySize', () => {
|
||||||
|
it('should invoke fs:directorySize with dirPath', async () => {
|
||||||
|
const mockSize = {
|
||||||
|
totalSize: 10240,
|
||||||
|
fileCount: 10,
|
||||||
|
folderCount: 2,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockSize);
|
||||||
|
|
||||||
|
const result = await api.directorySize('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:directorySize', '/home/user/project', undefined);
|
||||||
|
expect(result).toEqual(mockSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchImageAsBase64', () => {
|
||||||
|
it('should invoke fs:fetchImageAsBase64 with url', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('base64encodedimage');
|
||||||
|
|
||||||
|
const result = await api.fetchImageAsBase64('https://example.com/image.png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'fs:fetchImageAsBase64',
|
||||||
|
'https://example.com/image.png'
|
||||||
|
);
|
||||||
|
expect(result).toBe('base64encodedimage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for failed fetch', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.fetchImageAsBase64('https://example.com/notfound.png');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rename', () => {
|
||||||
|
it('should invoke fs:rename with oldPath and newPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.rename('/home/user/old.txt', '/home/user/new.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'fs:rename',
|
||||||
|
'/home/user/old.txt',
|
||||||
|
'/home/user/new.txt',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke fs:rename with SSH remote', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.rename('/home/user/old.txt', '/home/user/new.txt', 'remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'fs:rename',
|
||||||
|
'/home/user/old.txt',
|
||||||
|
'/home/user/new.txt',
|
||||||
|
'remote-1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should invoke fs:delete with targetPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.delete('/home/user/file.txt');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:delete', '/home/user/file.txt', undefined);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke fs:delete with options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.delete('/home/user/dir', { recursive: true, sshRemoteId: 'remote-1' });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:delete', '/home/user/dir', {
|
||||||
|
recursive: true,
|
||||||
|
sshRemoteId: 'remote-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('countItems', () => {
|
||||||
|
it('should invoke fs:countItems with dirPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ fileCount: 5, folderCount: 2 });
|
||||||
|
|
||||||
|
const result = await api.countItems('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fs:countItems', '/home/user/project', undefined);
|
||||||
|
expect(result.fileCount).toBe(5);
|
||||||
|
expect(result.folderCount).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
324
src/__tests__/main/preload/git.test.ts
Normal file
324
src/__tests__/main/preload/git.test.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/**
|
||||||
|
* Tests for git preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createGitApi } from '../../../main/preload/git';
|
||||||
|
|
||||||
|
describe('Git Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createGitApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createGitApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('status', () => {
|
||||||
|
it('should invoke git:status with cwd', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('M src/file.ts');
|
||||||
|
|
||||||
|
const result = await api.status('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:status',
|
||||||
|
'/home/user/project',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toBe('M src/file.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke git:status with SSH remote parameters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('M src/file.ts');
|
||||||
|
|
||||||
|
await api.status('/home/user/project', 'remote-1', '/remote/cwd');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:status',
|
||||||
|
'/home/user/project',
|
||||||
|
'remote-1',
|
||||||
|
'/remote/cwd'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diff', () => {
|
||||||
|
it('should invoke git:diff with cwd and optional file', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('diff output');
|
||||||
|
|
||||||
|
const result = await api.diff('/home/user/project', 'src/file.ts');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:diff',
|
||||||
|
'/home/user/project',
|
||||||
|
'src/file.ts',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toBe('diff output');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke git:diff without file', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('full diff');
|
||||||
|
|
||||||
|
await api.diff('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:diff',
|
||||||
|
'/home/user/project',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRepo', () => {
|
||||||
|
it('should invoke git:isRepo and return true for git repo', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.isRepo('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:isRepo',
|
||||||
|
'/home/user/project',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-git directory', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await api.isRepo('/home/user/not-a-repo');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('branch', () => {
|
||||||
|
it('should invoke git:branch and return current branch', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ stdout: 'main', stderr: '' });
|
||||||
|
|
||||||
|
const result = await api.branch('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:branch',
|
||||||
|
'/home/user/project',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result.stdout).toBe('main');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('info', () => {
|
||||||
|
it('should invoke git:info and return comprehensive info', async () => {
|
||||||
|
const mockInfo = {
|
||||||
|
branch: 'main',
|
||||||
|
remote: 'origin',
|
||||||
|
behind: 2,
|
||||||
|
ahead: 1,
|
||||||
|
uncommittedChanges: 3,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(mockInfo);
|
||||||
|
|
||||||
|
const result = await api.info('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:info',
|
||||||
|
'/home/user/project',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockInfo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('log', () => {
|
||||||
|
it('should invoke git:log with options', async () => {
|
||||||
|
const mockEntries = [
|
||||||
|
{
|
||||||
|
hash: 'abc123',
|
||||||
|
shortHash: 'abc',
|
||||||
|
author: 'User',
|
||||||
|
date: '2024-01-01',
|
||||||
|
refs: ['HEAD'],
|
||||||
|
subject: 'Initial commit',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue({ entries: mockEntries, error: null });
|
||||||
|
|
||||||
|
const result = await api.log('/home/user/project', { limit: 10, search: 'fix' });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('git:log', '/home/user/project', {
|
||||||
|
limit: 10,
|
||||||
|
search: 'fix',
|
||||||
|
});
|
||||||
|
expect(result.entries).toEqual(mockEntries);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('worktreeInfo', () => {
|
||||||
|
it('should invoke git:worktreeInfo', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
exists: true,
|
||||||
|
isWorktree: true,
|
||||||
|
currentBranch: 'feature-branch',
|
||||||
|
repoRoot: '/home/user/project',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.worktreeInfo('/home/user/worktree');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('git:worktreeInfo', '/home/user/worktree', undefined);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isWorktree).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('worktreeSetup', () => {
|
||||||
|
it('should invoke git:worktreeSetup with all parameters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
created: true,
|
||||||
|
currentBranch: 'feature-branch',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.worktreeSetup(
|
||||||
|
'/home/user/project',
|
||||||
|
'/home/user/worktree',
|
||||||
|
'feature-branch',
|
||||||
|
'remote-1'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:worktreeSetup',
|
||||||
|
'/home/user/project',
|
||||||
|
'/home/user/worktree',
|
||||||
|
'feature-branch',
|
||||||
|
'remote-1'
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.created).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPR', () => {
|
||||||
|
it('should invoke git:createPR', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
prUrl: 'https://github.com/user/repo/pull/123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.createPR('/home/user/worktree', 'main', 'Feature Title', 'PR body');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:createPR',
|
||||||
|
'/home/user/worktree',
|
||||||
|
'main',
|
||||||
|
'Feature Title',
|
||||||
|
'PR body',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.prUrl).toBe('https://github.com/user/repo/pull/123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkGhCli', () => {
|
||||||
|
it('should invoke git:checkGhCli', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ installed: true, authenticated: true });
|
||||||
|
|
||||||
|
const result = await api.checkGhCli();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('git:checkGhCli', undefined);
|
||||||
|
expect(result.installed).toBe(true);
|
||||||
|
expect(result.authenticated).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createGist', () => {
|
||||||
|
it('should invoke git:createGist', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
gistUrl: 'https://gist.github.com/user/abc123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.createGist('file.txt', 'content', 'description', true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'git:createGist',
|
||||||
|
'file.txt',
|
||||||
|
'content',
|
||||||
|
'description',
|
||||||
|
true,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.gistUrl).toBe('https://gist.github.com/user/abc123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listWorktrees', () => {
|
||||||
|
it('should invoke git:listWorktrees', async () => {
|
||||||
|
const mockWorktrees = [
|
||||||
|
{ path: '/home/user/project', head: 'abc123', branch: 'main', isBare: false },
|
||||||
|
{ path: '/home/user/worktree', head: 'def456', branch: 'feature', isBare: false },
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue({ worktrees: mockWorktrees });
|
||||||
|
|
||||||
|
const result = await api.listWorktrees('/home/user/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('git:listWorktrees', '/home/user/project', undefined);
|
||||||
|
expect(result.worktrees).toEqual(mockWorktrees);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onWorktreeDiscovered', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onWorktreeDiscovered(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('worktree:discovered', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with worktree data', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, data: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((channel: string, handler: typeof registeredHandler) => {
|
||||||
|
if (channel === 'worktree:discovered') {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onWorktreeDiscovered(callback);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
sessionId: 'session-123',
|
||||||
|
worktree: { path: '/home/user/worktree', name: 'feature', branch: 'feature-branch' },
|
||||||
|
};
|
||||||
|
registeredHandler!({}, data);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
729
src/__tests__/main/preload/groupChat.test.ts
Normal file
729
src/__tests__/main/preload/groupChat.test.ts
Normal file
@@ -0,0 +1,729 @@
|
|||||||
|
/**
|
||||||
|
* Tests for groupChat preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createGroupChatApi: Storage, chat log, moderator, participant, history, export operations
|
||||||
|
* - Event subscriptions: onMessage, onStateChange, onParticipantsChanged, onModeratorUsage,
|
||||||
|
* onHistoryEntry, onParticipantState, onModeratorSessionIdChanged
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createGroupChatApi } from '../../../main/preload/groupChat';
|
||||||
|
|
||||||
|
describe('GroupChat Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createGroupChatApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createGroupChatApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Storage operations', () => {
|
||||||
|
describe('create', () => {
|
||||||
|
it('should invoke groupChat:create', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'gc-123' });
|
||||||
|
|
||||||
|
const result = await api.create('My Group Chat', 'claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:create',
|
||||||
|
'My Group Chat',
|
||||||
|
'claude-code',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ id: 'gc-123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with moderator config', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'gc-123' });
|
||||||
|
const moderatorConfig = { customPath: '/custom/path' };
|
||||||
|
|
||||||
|
await api.create('My Group Chat', 'claude-code', moderatorConfig);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:create',
|
||||||
|
'My Group Chat',
|
||||||
|
'claude-code',
|
||||||
|
moderatorConfig
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('should invoke groupChat:list', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([{ id: 'gc-1', name: 'Chat 1' }]);
|
||||||
|
|
||||||
|
const result = await api.list();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:list');
|
||||||
|
expect(result).toEqual([{ id: 'gc-1', name: 'Chat 1' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('load', () => {
|
||||||
|
it('should invoke groupChat:load', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'gc-123', name: 'My Chat', participants: [] });
|
||||||
|
|
||||||
|
const result = await api.load('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:load', 'gc-123');
|
||||||
|
expect(result.id).toBe('gc-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should invoke groupChat:delete', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.delete('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:delete', 'gc-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rename', () => {
|
||||||
|
it('should invoke groupChat:rename', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.rename('gc-123', 'New Name');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:rename', 'gc-123', 'New Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should invoke groupChat:update', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
const updates = { name: 'Updated', moderatorAgentId: 'opencode' };
|
||||||
|
|
||||||
|
await api.update('gc-123', updates);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:update', 'gc-123', updates);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Chat log operations', () => {
|
||||||
|
describe('appendMessage', () => {
|
||||||
|
it('should invoke groupChat:appendMessage', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.appendMessage('gc-123', 'Moderator', 'Hello everyone!');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:appendMessage',
|
||||||
|
'gc-123',
|
||||||
|
'Moderator',
|
||||||
|
'Hello everyone!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMessages', () => {
|
||||||
|
it('should invoke groupChat:getMessages', async () => {
|
||||||
|
const messages = [{ timestamp: '2024-01-01', from: 'User', content: 'Hi' }];
|
||||||
|
mockInvoke.mockResolvedValue(messages);
|
||||||
|
|
||||||
|
const result = await api.getMessages('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:getMessages', 'gc-123');
|
||||||
|
expect(result).toEqual(messages);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveImage', () => {
|
||||||
|
it('should invoke groupChat:saveImage', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ path: 'images/img.png' });
|
||||||
|
|
||||||
|
await api.saveImage('gc-123', 'base64data', 'image.png');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:saveImage',
|
||||||
|
'gc-123',
|
||||||
|
'base64data',
|
||||||
|
'image.png'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Moderator operations', () => {
|
||||||
|
describe('startModerator', () => {
|
||||||
|
it('should invoke groupChat:startModerator', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.startModerator('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:startModerator', 'gc-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendToModerator', () => {
|
||||||
|
it('should invoke groupChat:sendToModerator', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ response: 'Moderator response' });
|
||||||
|
|
||||||
|
await api.sendToModerator('gc-123', 'Please coordinate', undefined, false);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:sendToModerator',
|
||||||
|
'gc-123',
|
||||||
|
'Please coordinate',
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with images and readOnly', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ response: 'Response' });
|
||||||
|
|
||||||
|
await api.sendToModerator('gc-123', 'Message', ['image1.png'], true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:sendToModerator',
|
||||||
|
'gc-123',
|
||||||
|
'Message',
|
||||||
|
['image1.png'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopModerator', () => {
|
||||||
|
it('should invoke groupChat:stopModerator', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.stopModerator('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:stopModerator', 'gc-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getModeratorSessionId', () => {
|
||||||
|
it('should invoke groupChat:getModeratorSessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('mod-session-456');
|
||||||
|
|
||||||
|
const result = await api.getModeratorSessionId('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:getModeratorSessionId', 'gc-123');
|
||||||
|
expect(result).toBe('mod-session-456');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Participant operations', () => {
|
||||||
|
describe('addParticipant', () => {
|
||||||
|
it('should invoke groupChat:addParticipant', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.addParticipant('gc-123', 'Agent1', 'claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:addParticipant',
|
||||||
|
'gc-123',
|
||||||
|
'Agent1',
|
||||||
|
'claude-code',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with cwd', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.addParticipant('gc-123', 'Agent1', 'claude-code', '/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:addParticipant',
|
||||||
|
'gc-123',
|
||||||
|
'Agent1',
|
||||||
|
'claude-code',
|
||||||
|
'/project'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendToParticipant', () => {
|
||||||
|
it('should invoke groupChat:sendToParticipant', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ response: 'Participant response' });
|
||||||
|
|
||||||
|
await api.sendToParticipant('gc-123', 'Agent1', 'Do this task');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:sendToParticipant',
|
||||||
|
'gc-123',
|
||||||
|
'Agent1',
|
||||||
|
'Do this task',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with images', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ response: 'Response' });
|
||||||
|
|
||||||
|
await api.sendToParticipant('gc-123', 'Agent1', 'Look at this', ['screenshot.png']);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:sendToParticipant',
|
||||||
|
'gc-123',
|
||||||
|
'Agent1',
|
||||||
|
'Look at this',
|
||||||
|
['screenshot.png']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeParticipant', () => {
|
||||||
|
it('should invoke groupChat:removeParticipant', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.removeParticipant('gc-123', 'Agent1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:removeParticipant', 'gc-123', 'Agent1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetParticipantContext', () => {
|
||||||
|
it('should invoke groupChat:resetParticipantContext', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ newAgentSessionId: 'new-session-789' });
|
||||||
|
|
||||||
|
const result = await api.resetParticipantContext('gc-123', 'Agent1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'groupChat:resetParticipantContext',
|
||||||
|
'gc-123',
|
||||||
|
'Agent1',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result.newAgentSessionId).toBe('new-session-789');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('History operations', () => {
|
||||||
|
describe('getHistory', () => {
|
||||||
|
it('should invoke groupChat:getHistory', async () => {
|
||||||
|
const history = [{ id: 'h-1', timestamp: Date.now(), summary: 'Test' }];
|
||||||
|
mockInvoke.mockResolvedValue(history);
|
||||||
|
|
||||||
|
const result = await api.getHistory('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:getHistory', 'gc-123');
|
||||||
|
expect(result).toEqual(history);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addHistoryEntry', () => {
|
||||||
|
it('should invoke groupChat:addHistoryEntry', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'h-new' });
|
||||||
|
const entry = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
summary: 'Task completed',
|
||||||
|
participantName: 'Agent1',
|
||||||
|
participantColor: '#ff0000',
|
||||||
|
type: 'response' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.addHistoryEntry('gc-123', entry);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:addHistoryEntry', 'gc-123', entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteHistoryEntry', () => {
|
||||||
|
it('should invoke groupChat:deleteHistoryEntry', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteHistoryEntry('gc-123', 'h-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:deleteHistoryEntry', 'gc-123', 'h-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearHistory', () => {
|
||||||
|
it('should invoke groupChat:clearHistory', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.clearHistory('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:clearHistory', 'gc-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getHistoryFilePath', () => {
|
||||||
|
it('should invoke groupChat:getHistoryFilePath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/path/to/history.json');
|
||||||
|
|
||||||
|
const result = await api.getHistoryFilePath('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:getHistoryFilePath', 'gc-123');
|
||||||
|
expect(result).toBe('/path/to/history.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Export operations', () => {
|
||||||
|
describe('getImages', () => {
|
||||||
|
it('should invoke groupChat:getImages', async () => {
|
||||||
|
const images = { 'img1.png': 'base64data1', 'img2.png': 'base64data2' };
|
||||||
|
mockInvoke.mockResolvedValue(images);
|
||||||
|
|
||||||
|
const result = await api.getImages('gc-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groupChat:getImages', 'gc-123');
|
||||||
|
expect(result).toEqual(images);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Event subscriptions', () => {
|
||||||
|
describe('onMessage', () => {
|
||||||
|
it('should register and handle message events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, message: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, message: unknown) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onMessage(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('groupChat:message', expect.any(Function));
|
||||||
|
|
||||||
|
const message = { timestamp: '2024-01-01', from: 'User', content: 'Hi' };
|
||||||
|
registeredHandler!({}, 'gc-123', message);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', message);
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, message: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, message: unknown) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onMessage(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith('groupChat:message', registeredHandler!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onStateChange', () => {
|
||||||
|
it('should register and handle state change events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, state: string) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, state: string) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onStateChange(callback);
|
||||||
|
registeredHandler!({}, 'gc-123', 'moderator-thinking');
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', 'moderator-thinking');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, state: string) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, state: string) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onStateChange(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'groupChat:stateChange',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onParticipantsChanged', () => {
|
||||||
|
it('should register and handle participants changed events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (
|
||||||
|
event: unknown,
|
||||||
|
groupChatId: string,
|
||||||
|
participants: unknown[]
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, participants: unknown[]) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onParticipantsChanged(callback);
|
||||||
|
|
||||||
|
const participants = [{ name: 'Agent1', agentId: 'claude-code' }];
|
||||||
|
registeredHandler!({}, 'gc-123', participants);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', participants);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (
|
||||||
|
event: unknown,
|
||||||
|
groupChatId: string,
|
||||||
|
participants: unknown[]
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, participants: unknown[]) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onParticipantsChanged(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'groupChat:participantsChanged',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onModeratorUsage', () => {
|
||||||
|
it('should register and handle moderator usage events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, usage: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, usage: unknown) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onModeratorUsage(callback);
|
||||||
|
|
||||||
|
const usage = { contextUsage: 50, totalCost: 0.05, tokenCount: 1000 };
|
||||||
|
registeredHandler!({}, 'gc-123', usage);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', usage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, usage: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, usage: unknown) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onModeratorUsage(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'groupChat:moderatorUsage',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onHistoryEntry', () => {
|
||||||
|
it('should register and handle history entry events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, entry: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, entry: unknown) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onHistoryEntry(callback);
|
||||||
|
|
||||||
|
const entry = { id: 'h-1', summary: 'Task done' };
|
||||||
|
registeredHandler!({}, 'gc-123', entry);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, entry: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, entry: unknown) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onHistoryEntry(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'groupChat:historyEntry',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onParticipantState', () => {
|
||||||
|
it('should register and handle participant state events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (
|
||||||
|
event: unknown,
|
||||||
|
groupChatId: string,
|
||||||
|
participantName: string,
|
||||||
|
state: string
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (
|
||||||
|
event: unknown,
|
||||||
|
groupChatId: string,
|
||||||
|
participantName: string,
|
||||||
|
state: string
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onParticipantState(callback);
|
||||||
|
registeredHandler!({}, 'gc-123', 'Agent1', 'working');
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', 'Agent1', 'working');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (
|
||||||
|
event: unknown,
|
||||||
|
groupChatId: string,
|
||||||
|
participantName: string,
|
||||||
|
state: string
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (
|
||||||
|
event: unknown,
|
||||||
|
groupChatId: string,
|
||||||
|
participantName: string,
|
||||||
|
state: string
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onParticipantState(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'groupChat:participantState',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onModeratorSessionIdChanged', () => {
|
||||||
|
it('should register and handle moderator session id changed events', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, sessionId: string) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, sessionId: string) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onModeratorSessionIdChanged(callback);
|
||||||
|
registeredHandler!({}, 'gc-123', 'new-session-id');
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('gc-123', 'new-session-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, groupChatId: string, sessionId: string) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(
|
||||||
|
_channel: string,
|
||||||
|
handler: (event: unknown, groupChatId: string, sessionId: string) => void
|
||||||
|
) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onModeratorSessionIdChanged(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'groupChat:moderatorSessionIdChanged',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
176
src/__tests__/main/preload/leaderboard.test.ts
Normal file
176
src/__tests__/main/preload/leaderboard.test.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Tests for leaderboard preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createLeaderboardApi } from '../../../main/preload/leaderboard';
|
||||||
|
|
||||||
|
describe('Leaderboard Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createLeaderboardApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createLeaderboardApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInstallationId', () => {
|
||||||
|
it('should invoke leaderboard:getInstallationId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('test-installation-id');
|
||||||
|
|
||||||
|
const result = await api.getInstallationId();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:getInstallationId');
|
||||||
|
expect(result).toBe('test-installation-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no installation ID', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.getInstallationId();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
it('should invoke leaderboard:submit with data', async () => {
|
||||||
|
const submitData = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
badgeLevel: 1,
|
||||||
|
badgeName: 'Bronze',
|
||||||
|
cumulativeTimeMs: 10000,
|
||||||
|
totalRuns: 5,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, message: 'Submitted' });
|
||||||
|
|
||||||
|
const result = await api.submit(submitData);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:submit', submitData);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pollAuthStatus', () => {
|
||||||
|
it('should invoke leaderboard:pollAuthStatus with clientToken', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ status: 'confirmed', authToken: 'new-token' });
|
||||||
|
|
||||||
|
const result = await api.pollAuthStatus('client-token');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:pollAuthStatus', 'client-token');
|
||||||
|
expect(result.status).toBe('confirmed');
|
||||||
|
expect(result.authToken).toBe('new-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle pending status', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ status: 'pending' });
|
||||||
|
|
||||||
|
const result = await api.pollAuthStatus('client-token');
|
||||||
|
|
||||||
|
expect(result.status).toBe('pending');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resendConfirmation', () => {
|
||||||
|
it('should invoke leaderboard:resendConfirmation with email and clientToken', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, message: 'Email sent' });
|
||||||
|
|
||||||
|
const result = await api.resendConfirmation({
|
||||||
|
email: 'test@example.com',
|
||||||
|
clientToken: 'token',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:resendConfirmation', {
|
||||||
|
email: 'test@example.com',
|
||||||
|
clientToken: 'token',
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('should invoke leaderboard:get without options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, entries: [] });
|
||||||
|
|
||||||
|
const result = await api.get();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:get', undefined);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke leaderboard:get with limit option', async () => {
|
||||||
|
const mockEntries = [
|
||||||
|
{ rank: 1, displayName: 'User', badgeLevel: 5, cumulativeTimeMs: 100000 },
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, entries: mockEntries });
|
||||||
|
|
||||||
|
const result = await api.get({ limit: 10 });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:get', { limit: 10 });
|
||||||
|
expect(result.entries).toEqual(mockEntries);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLongestRuns', () => {
|
||||||
|
it('should invoke leaderboard:getLongestRuns', async () => {
|
||||||
|
const mockEntries = [
|
||||||
|
{ rank: 1, displayName: 'User', longestRunMs: 50000, runDate: '2024-01-01' },
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, entries: mockEntries });
|
||||||
|
|
||||||
|
const result = await api.getLongestRuns({ limit: 5 });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:getLongestRuns', { limit: 5 });
|
||||||
|
expect(result.entries).toEqual(mockEntries);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sync', () => {
|
||||||
|
it('should invoke leaderboard:sync with email and authToken', async () => {
|
||||||
|
const mockData = {
|
||||||
|
displayName: 'Test User',
|
||||||
|
badgeLevel: 3,
|
||||||
|
badgeName: 'Gold',
|
||||||
|
cumulativeTimeMs: 50000,
|
||||||
|
totalRuns: 25,
|
||||||
|
longestRunMs: 5000,
|
||||||
|
longestRunDate: '2024-01-01',
|
||||||
|
keyboardLevel: 2,
|
||||||
|
coveragePercent: 75,
|
||||||
|
ranking: {
|
||||||
|
cumulative: { rank: 5, total: 100 },
|
||||||
|
longestRun: { rank: 10, total: 100 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, found: true, data: mockData });
|
||||||
|
|
||||||
|
const result = await api.sync({ email: 'test@example.com', authToken: 'token' });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('leaderboard:sync', {
|
||||||
|
email: 'test@example.com',
|
||||||
|
authToken: 'token',
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.found).toBe(true);
|
||||||
|
expect(result.data).toEqual(mockData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle user not found', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, found: false });
|
||||||
|
|
||||||
|
const result = await api.sync({ email: 'test@example.com', authToken: 'token' });
|
||||||
|
|
||||||
|
expect(result.found).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
284
src/__tests__/main/preload/logger.test.ts
Normal file
284
src/__tests__/main/preload/logger.test.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* Tests for logger preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createLoggerApi: log, getLogs, clearLogs, setLogLevel, getLogLevel, setMaxLogBuffer,
|
||||||
|
* getMaxLogBuffer, toast, autorun, onNewLog, getLogFilePath, isFileLoggingEnabled, enableFileLogging
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createLoggerApi } from '../../../main/preload/logger';
|
||||||
|
|
||||||
|
describe('Logger Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createLoggerApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createLoggerApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('log', () => {
|
||||||
|
it('should invoke logger:log with level and message', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.log('info', 'Test message');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'logger:log',
|
||||||
|
'info',
|
||||||
|
'Test message',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke logger:log with context and data', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
const data = { key: 'value' };
|
||||||
|
|
||||||
|
await api.log('error', 'Error occurred', 'MyContext', data);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'logger:log',
|
||||||
|
'error',
|
||||||
|
'Error occurred',
|
||||||
|
'MyContext',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogs', () => {
|
||||||
|
it('should invoke logger:getLogs without filter', async () => {
|
||||||
|
const logs = [{ level: 'info', message: 'Test', timestamp: Date.now() }];
|
||||||
|
mockInvoke.mockResolvedValue(logs);
|
||||||
|
|
||||||
|
const result = await api.getLogs();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:getLogs', undefined);
|
||||||
|
expect(result).toEqual(logs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke logger:getLogs with filter', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
const filter = { level: 'error' as const, context: 'Test', limit: 100 };
|
||||||
|
|
||||||
|
await api.getLogs(filter);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:getLogs', filter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearLogs', () => {
|
||||||
|
it('should invoke logger:clearLogs', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.clearLogs();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:clearLogs');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setLogLevel', () => {
|
||||||
|
it('should invoke logger:setLogLevel', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.setLogLevel('debug');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:setLogLevel', 'debug');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogLevel', () => {
|
||||||
|
it('should invoke logger:getLogLevel', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('info');
|
||||||
|
|
||||||
|
const result = await api.getLogLevel();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:getLogLevel');
|
||||||
|
expect(result).toBe('info');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setMaxLogBuffer', () => {
|
||||||
|
it('should invoke logger:setMaxLogBuffer', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.setMaxLogBuffer(500);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:setMaxLogBuffer', 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMaxLogBuffer', () => {
|
||||||
|
it('should invoke logger:getMaxLogBuffer', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(1000);
|
||||||
|
|
||||||
|
const result = await api.getMaxLogBuffer();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:getMaxLogBuffer');
|
||||||
|
expect(result).toBe(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toast', () => {
|
||||||
|
it('should invoke logger:log with toast level', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.toast('Notification title');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'logger:log',
|
||||||
|
'toast',
|
||||||
|
'Notification title',
|
||||||
|
'Toast',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with data', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
const data = { action: 'clicked' };
|
||||||
|
|
||||||
|
await api.toast('Notification', data);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:log', 'toast', 'Notification', 'Toast', data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autorun', () => {
|
||||||
|
it('should invoke logger:log with autorun level', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.autorun('Task started');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'logger:log',
|
||||||
|
'autorun',
|
||||||
|
'Task started',
|
||||||
|
'AutoRun',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with custom context', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.autorun('Task completed', 'Playbook');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'logger:log',
|
||||||
|
'autorun',
|
||||||
|
'Task completed',
|
||||||
|
'Playbook',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with data', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
const data = { taskIndex: 5 };
|
||||||
|
|
||||||
|
await api.autorun('Processing', 'AutoRun', data);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'logger:log',
|
||||||
|
'autorun',
|
||||||
|
'Processing',
|
||||||
|
'AutoRun',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onNewLog', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onNewLog(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('logger:newLog', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, log: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, log: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onNewLog(callback);
|
||||||
|
|
||||||
|
const logEntry = { level: 'info', message: 'Test', timestamp: Date.now() };
|
||||||
|
registeredHandler!({}, logEntry);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(logEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, log: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, log: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onNewLog(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith('logger:newLog', registeredHandler!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogFilePath', () => {
|
||||||
|
it('should invoke logger:getLogFilePath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/path/to/logs/maestro.log');
|
||||||
|
|
||||||
|
const result = await api.getLogFilePath();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:getLogFilePath');
|
||||||
|
expect(result).toBe('/path/to/logs/maestro.log');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isFileLoggingEnabled', () => {
|
||||||
|
it('should invoke logger:isFileLoggingEnabled', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.isFileLoggingEnabled();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:isFileLoggingEnabled');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enableFileLogging', () => {
|
||||||
|
it('should invoke logger:enableFileLogging', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.enableFileLogging();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('logger:enableFileLogging');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
125
src/__tests__/main/preload/notifications.test.ts
Normal file
125
src/__tests__/main/preload/notifications.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Tests for notifications preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createNotificationApi } from '../../../main/preload/notifications';
|
||||||
|
|
||||||
|
describe('Notification Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createNotificationApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createNotificationApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('show', () => {
|
||||||
|
it('should invoke notification:show with title and body', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.show('Test Title', 'Test Body');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('notification:show', 'Test Title', 'Test Body');
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: false, error: 'Failed to show notification' });
|
||||||
|
|
||||||
|
const result = await api.show('Title', 'Body');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Failed to show notification');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('speak', () => {
|
||||||
|
it('should invoke notification:speak with text', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, ttsId: 123 });
|
||||||
|
|
||||||
|
const result = await api.speak('Hello world');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('notification:speak', 'Hello world', undefined);
|
||||||
|
expect(result).toEqual({ success: true, ttsId: 123 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke notification:speak with custom command', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, ttsId: 456 });
|
||||||
|
|
||||||
|
const result = await api.speak('Hello', 'espeak');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('notification:speak', 'Hello', 'espeak');
|
||||||
|
expect(result.ttsId).toBe(456);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopSpeak', () => {
|
||||||
|
it('should invoke notification:stopSpeak with ttsId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.stopSpeak(123);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('notification:stopSpeak', 123);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onTtsCompleted', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onTtsCompleted(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('tts:completed', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, ttsId: number) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, ttsId: number) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onTtsCompleted(callback);
|
||||||
|
|
||||||
|
// Simulate receiving the event
|
||||||
|
registeredHandler!({}, 789);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(789);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, ttsId: number) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, ttsId: number) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onTtsCompleted(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith('tts:completed', registeredHandler!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
298
src/__tests__/main/preload/process.test.ts
Normal file
298
src/__tests__/main/preload/process.test.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* Tests for process preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
const mockSend = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
send: (...args: unknown[]) => mockSend(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createProcessApi, type ProcessConfig } from '../../../main/preload/process';
|
||||||
|
|
||||||
|
describe('Process Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createProcessApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createProcessApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('spawn', () => {
|
||||||
|
it('should invoke process:spawn with config', async () => {
|
||||||
|
const config: ProcessConfig = {
|
||||||
|
sessionId: 'session-123',
|
||||||
|
toolType: 'claude-code',
|
||||||
|
cwd: '/home/user/project',
|
||||||
|
command: 'claude',
|
||||||
|
args: ['--json'],
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({ pid: 1234, success: true });
|
||||||
|
|
||||||
|
const result = await api.spawn(config);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:spawn', config);
|
||||||
|
expect(result.pid).toBe(1234);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle SSH remote response', async () => {
|
||||||
|
const config: ProcessConfig = {
|
||||||
|
sessionId: 'session-123',
|
||||||
|
toolType: 'claude-code',
|
||||||
|
cwd: '/home/user/project',
|
||||||
|
command: 'claude',
|
||||||
|
args: [],
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
pid: 1234,
|
||||||
|
success: true,
|
||||||
|
sshRemote: { id: 'remote-1', name: 'My Server', host: 'example.com' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.spawn(config);
|
||||||
|
|
||||||
|
expect(result.sshRemote).toEqual({ id: 'remote-1', name: 'My Server', host: 'example.com' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('write', () => {
|
||||||
|
it('should invoke process:write with sessionId and data', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.write('session-123', 'Hello');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:write', 'session-123', 'Hello');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interrupt', () => {
|
||||||
|
it('should invoke process:interrupt with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.interrupt('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:interrupt', 'session-123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('kill', () => {
|
||||||
|
it('should invoke process:kill with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.kill('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:kill', 'session-123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resize', () => {
|
||||||
|
it('should invoke process:resize with sessionId, cols, and rows', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.resize('session-123', 120, 40);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:resize', 'session-123', 120, 40);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runCommand', () => {
|
||||||
|
it('should invoke process:runCommand with config', async () => {
|
||||||
|
const config = {
|
||||||
|
sessionId: 'session-123',
|
||||||
|
command: 'ls -la',
|
||||||
|
cwd: '/home/user',
|
||||||
|
shell: '/bin/bash',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({ exitCode: 0 });
|
||||||
|
|
||||||
|
const result = await api.runCommand(config);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:runCommand', config);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle SSH remote config', async () => {
|
||||||
|
const config = {
|
||||||
|
sessionId: 'session-123',
|
||||||
|
command: 'ls -la',
|
||||||
|
cwd: '/home/user',
|
||||||
|
sessionSshRemoteConfig: {
|
||||||
|
enabled: true,
|
||||||
|
remoteId: 'remote-1',
|
||||||
|
workingDirOverride: '/remote/path',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue({ exitCode: 0 });
|
||||||
|
|
||||||
|
await api.runCommand(config);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:runCommand', config);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getActiveProcesses', () => {
|
||||||
|
it('should invoke process:getActiveProcesses', async () => {
|
||||||
|
const mockProcesses = [
|
||||||
|
{
|
||||||
|
sessionId: 'session-123',
|
||||||
|
toolType: 'claude-code',
|
||||||
|
pid: 1234,
|
||||||
|
cwd: '/home/user',
|
||||||
|
isTerminal: false,
|
||||||
|
isBatchMode: false,
|
||||||
|
startTime: Date.now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(mockProcesses);
|
||||||
|
|
||||||
|
const result = await api.getActiveProcesses();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('process:getActiveProcesses');
|
||||||
|
expect(result).toEqual(mockProcesses);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onData', () => {
|
||||||
|
it('should register event listener for process:data', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onData(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('process:data', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with sessionId and data', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, sessionId: string, data: string) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((channel: string, handler: typeof registeredHandler) => {
|
||||||
|
if (channel === 'process:data') {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onData(callback);
|
||||||
|
registeredHandler!({}, 'session-123', 'output data');
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('session-123', 'output data');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onExit', () => {
|
||||||
|
it('should register event listener for process:exit', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onExit(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('process:exit', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onUsage', () => {
|
||||||
|
it('should register event listener for process:usage', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, sessionId: string, usageStats: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((channel: string, handler: typeof registeredHandler) => {
|
||||||
|
if (channel === 'process:usage') {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onUsage(callback);
|
||||||
|
|
||||||
|
const usageStats = {
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 200,
|
||||||
|
cacheReadInputTokens: 50,
|
||||||
|
cacheCreationInputTokens: 25,
|
||||||
|
totalCostUsd: 0.01,
|
||||||
|
contextWindow: 100000,
|
||||||
|
};
|
||||||
|
registeredHandler!({}, 'session-123', usageStats);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('session-123', usageStats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onAgentError', () => {
|
||||||
|
it('should register event listener for agent:error', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, sessionId: string, error: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((channel: string, handler: typeof registeredHandler) => {
|
||||||
|
if (channel === 'agent:error') {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onAgentError(callback);
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
type: 'auth_expired',
|
||||||
|
message: 'Authentication expired',
|
||||||
|
recoverable: true,
|
||||||
|
agentId: 'claude-code',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
registeredHandler!({}, 'session-123', error);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('session-123', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRemoteNewTabResponse', () => {
|
||||||
|
it('should send response via ipcRenderer.send', () => {
|
||||||
|
api.sendRemoteNewTabResponse('response-channel', { tabId: 'tab-123' });
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledWith('response-channel', { tabId: 'tab-123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send null result', () => {
|
||||||
|
api.sendRemoteNewTabResponse('response-channel', null);
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledWith('response-channel', null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onRemoteCommand', () => {
|
||||||
|
it('should register listener and invoke callback with all parameters', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (
|
||||||
|
event: unknown,
|
||||||
|
sessionId: string,
|
||||||
|
command: string,
|
||||||
|
inputMode?: 'ai' | 'terminal'
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((channel: string, handler: typeof registeredHandler) => {
|
||||||
|
if (channel === 'remote:executeCommand') {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onRemoteCommand(callback);
|
||||||
|
registeredHandler!({}, 'session-123', 'test command', 'ai');
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('session-123', 'test command', 'ai');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
448
src/__tests__/main/preload/sessions.test.ts
Normal file
448
src/__tests__/main/preload/sessions.test.ts
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
/**
|
||||||
|
* Tests for sessions preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createClaudeApi (deprecated): listSessions, listSessionsPaginated, getProjectStats,
|
||||||
|
* readSessionMessages, searchSessions, deleteMessagePair
|
||||||
|
* - createAgentSessionsApi: list, listPaginated, read, search, getPath, deleteMessagePair,
|
||||||
|
* hasStorage, getAvailableStorages, getGlobalStats, getAllNamedSessions, onGlobalStatsUpdate,
|
||||||
|
* registerSessionOrigin, updateSessionName, getOrigins, setSessionName, setSessionStarred
|
||||||
|
*
|
||||||
|
* Note: createClaudeApi is deprecated but still tested for backwards compatibility.
|
||||||
|
* These tests ensure the deprecation warnings are logged correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
// Mock console.warn for deprecation warnings
|
||||||
|
const mockConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createClaudeApi, createAgentSessionsApi } from '../../../main/preload/sessions';
|
||||||
|
|
||||||
|
describe('Sessions Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockConsoleWarn.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mockConsoleWarn.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createClaudeApi (deprecated)', () => {
|
||||||
|
let api: ReturnType<typeof createClaudeApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createClaudeApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listSessions', () => {
|
||||||
|
it('should invoke claude:listSessions and log deprecation warning', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.listSessions('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('claude:listSessions', '/project');
|
||||||
|
expect(mockConsoleWarn).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listSessionsPaginated', () => {
|
||||||
|
it('should invoke claude:listSessionsPaginated', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ sessions: [], cursor: null });
|
||||||
|
|
||||||
|
await api.listSessionsPaginated('/project', { limit: 10 });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('claude:listSessionsPaginated', '/project', {
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProjectStats', () => {
|
||||||
|
it('should invoke claude:getProjectStats', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ totalSessions: 5 });
|
||||||
|
|
||||||
|
await api.getProjectStats('/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('claude:getProjectStats', '/project');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readSessionMessages', () => {
|
||||||
|
it('should invoke claude:readSessionMessages', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ messages: [] });
|
||||||
|
|
||||||
|
await api.readSessionMessages('/project', 'session-123', { limit: 50 });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'claude:readSessionMessages',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
{ limit: 50 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchSessions', () => {
|
||||||
|
it('should invoke claude:searchSessions', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.searchSessions('/project', 'query', 'all');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'claude:searchSessions',
|
||||||
|
'/project',
|
||||||
|
'query',
|
||||||
|
'all'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteMessagePair', () => {
|
||||||
|
it('should invoke claude:deleteMessagePair', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteMessagePair('/project', 'session-123', 'uuid-456', 'fallback');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'claude:deleteMessagePair',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
'uuid-456',
|
||||||
|
'fallback'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAgentSessionsApi', () => {
|
||||||
|
let api: ReturnType<typeof createAgentSessionsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createAgentSessionsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list', () => {
|
||||||
|
it('should invoke agentSessions:list', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.list('claude-code', '/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:list',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with sshRemoteId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.list('claude-code', '/project', 'ssh-remote-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:list',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'ssh-remote-1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listPaginated', () => {
|
||||||
|
it('should invoke agentSessions:listPaginated', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ sessions: [], cursor: null });
|
||||||
|
|
||||||
|
await api.listPaginated('claude-code', '/project', { limit: 20 });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:listPaginated',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
{ limit: 20 },
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read', () => {
|
||||||
|
it('should invoke agentSessions:read', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ messages: [] });
|
||||||
|
|
||||||
|
await api.read('claude-code', '/project', 'session-123', { offset: 0, limit: 50 });
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:read',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
{ offset: 0, limit: 50 },
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
it('should invoke agentSessions:search', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.search('claude-code', '/project', 'search query', 'title');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:search',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'search query',
|
||||||
|
'title',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPath', () => {
|
||||||
|
it('should invoke agentSessions:getPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/path/to/session.json');
|
||||||
|
|
||||||
|
const result = await api.getPath('claude-code', '/project', 'session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:getPath',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toBe('/path/to/session.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteMessagePair', () => {
|
||||||
|
it('should invoke agentSessions:deleteMessagePair', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.deleteMessagePair('claude-code', '/project', 'session-123', 'uuid-456');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:deleteMessagePair',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
'uuid-456',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasStorage', () => {
|
||||||
|
it('should invoke agentSessions:hasStorage', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.hasStorage('claude-code');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agentSessions:hasStorage', 'claude-code');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAvailableStorages', () => {
|
||||||
|
it('should invoke agentSessions:getAvailableStorages', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['claude-code', 'opencode']);
|
||||||
|
|
||||||
|
const result = await api.getAvailableStorages();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agentSessions:getAvailableStorages');
|
||||||
|
expect(result).toEqual(['claude-code', 'opencode']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getGlobalStats', () => {
|
||||||
|
it('should invoke agentSessions:getGlobalStats', async () => {
|
||||||
|
const stats = { totalSessions: 100, totalMessages: 1000 };
|
||||||
|
mockInvoke.mockResolvedValue(stats);
|
||||||
|
|
||||||
|
const result = await api.getGlobalStats();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agentSessions:getGlobalStats');
|
||||||
|
expect(result).toEqual(stats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllNamedSessions', () => {
|
||||||
|
it('should invoke agentSessions:getAllNamedSessions', async () => {
|
||||||
|
const sessions = [
|
||||||
|
{
|
||||||
|
agentId: 'claude-code',
|
||||||
|
agentSessionId: '123',
|
||||||
|
projectPath: '/project',
|
||||||
|
sessionName: 'Test',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(sessions);
|
||||||
|
|
||||||
|
const result = await api.getAllNamedSessions();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agentSessions:getAllNamedSessions');
|
||||||
|
expect(result).toEqual(sessions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onGlobalStatsUpdate', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onGlobalStatsUpdate(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:globalStatsUpdate',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, stats: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, stats: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onGlobalStatsUpdate(callback);
|
||||||
|
|
||||||
|
const stats = { totalSessions: 50 };
|
||||||
|
registeredHandler!({}, stats);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, stats: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, stats: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onGlobalStatsUpdate(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:globalStatsUpdate',
|
||||||
|
registeredHandler!
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registerSessionOrigin', () => {
|
||||||
|
it('should invoke claude:registerSessionOrigin', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.registerSessionOrigin('/project', 'agent-session-123', 'user', 'My Session');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'claude:registerSessionOrigin',
|
||||||
|
'/project',
|
||||||
|
'agent-session-123',
|
||||||
|
'user',
|
||||||
|
'My Session'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateSessionName', () => {
|
||||||
|
it('should invoke claude:updateSessionName', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.updateSessionName('/project', 'agent-session-123', 'New Name');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'claude:updateSessionName',
|
||||||
|
'/project',
|
||||||
|
'agent-session-123',
|
||||||
|
'New Name'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrigins', () => {
|
||||||
|
it('should invoke agentSessions:getOrigins', async () => {
|
||||||
|
const origins = { 'session-1': { origin: 'user', sessionName: 'Test' } };
|
||||||
|
mockInvoke.mockResolvedValue(origins);
|
||||||
|
|
||||||
|
const result = await api.getOrigins('claude-code', '/project');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:getOrigins',
|
||||||
|
'claude-code',
|
||||||
|
'/project'
|
||||||
|
);
|
||||||
|
expect(result).toEqual(origins);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSessionName', () => {
|
||||||
|
it('should invoke agentSessions:setSessionName', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.setSessionName('claude-code', '/project', 'session-123', 'New Name');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:setSessionName',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
'New Name'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null to clear name', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.setSessionName('claude-code', '/project', 'session-123', null);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:setSessionName',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSessionStarred', () => {
|
||||||
|
it('should invoke agentSessions:setSessionStarred', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.setSessionStarred('claude-code', '/project', 'session-123', true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'agentSessions:setSessionStarred',
|
||||||
|
'claude-code',
|
||||||
|
'/project',
|
||||||
|
'session-123',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
200
src/__tests__/main/preload/settings.test.ts
Normal file
200
src/__tests__/main/preload/settings.test.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* Tests for settings preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createSettingsApi: get, set, getAll
|
||||||
|
* - createSessionsApi: getAll, setAll
|
||||||
|
* - createGroupsApi: getAll, setAll
|
||||||
|
* - createAgentErrorApi: clearError, retryAfterError
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
createSettingsApi,
|
||||||
|
createSessionsApi,
|
||||||
|
createGroupsApi,
|
||||||
|
createAgentErrorApi,
|
||||||
|
} from '../../../main/preload/settings';
|
||||||
|
|
||||||
|
describe('Settings Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSettingsApi', () => {
|
||||||
|
let api: ReturnType<typeof createSettingsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createSettingsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('should invoke settings:get with key', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('test-value');
|
||||||
|
|
||||||
|
const result = await api.get('theme');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('settings:get', 'theme');
|
||||||
|
expect(result).toBe('test-value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for non-existent key', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await api.get('non-existent');
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('set', () => {
|
||||||
|
it('should invoke settings:set with key and value and return result', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.set('theme', 'dark');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('settings:set', 'theme', 'dark');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex values', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
const complexValue = { nested: { key: 'value' }, array: [1, 2, 3] };
|
||||||
|
|
||||||
|
const result = await api.set('config', complexValue);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('settings:set', 'config', complexValue);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should invoke settings:getAll', async () => {
|
||||||
|
const allSettings = { theme: 'dark', fontSize: 14 };
|
||||||
|
mockInvoke.mockResolvedValue(allSettings);
|
||||||
|
|
||||||
|
const result = await api.getAll();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('settings:getAll');
|
||||||
|
expect(result).toEqual(allSettings);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSessionsApi', () => {
|
||||||
|
let api: ReturnType<typeof createSessionsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createSessionsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should invoke sessions:getAll', async () => {
|
||||||
|
const sessions = [{ id: '1', name: 'Session 1' }];
|
||||||
|
mockInvoke.mockResolvedValue(sessions);
|
||||||
|
|
||||||
|
const result = await api.getAll();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sessions:getAll');
|
||||||
|
expect(result).toEqual(sessions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAll', () => {
|
||||||
|
it('should invoke sessions:setAll with sessions array and return result', async () => {
|
||||||
|
const sessions = [
|
||||||
|
{ id: '1', name: 'Session 1' },
|
||||||
|
{ id: '2', name: 'Session 2' },
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.setAll(sessions);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sessions:setAll', sessions);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createGroupsApi', () => {
|
||||||
|
let api: ReturnType<typeof createGroupsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createGroupsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should invoke groups:getAll', async () => {
|
||||||
|
const groups = [{ id: '1', name: 'Group 1' }];
|
||||||
|
mockInvoke.mockResolvedValue(groups);
|
||||||
|
|
||||||
|
const result = await api.getAll();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groups:getAll');
|
||||||
|
expect(result).toEqual(groups);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAll', () => {
|
||||||
|
it('should invoke groups:setAll with groups array and return result', async () => {
|
||||||
|
const groups = [{ id: '1', name: 'Group 1' }];
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.setAll(groups);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('groups:setAll', groups);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAgentErrorApi', () => {
|
||||||
|
let api: ReturnType<typeof createAgentErrorApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createAgentErrorApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearError', () => {
|
||||||
|
it('should invoke agent:clearError with sessionId and return result', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.clearError('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agent:clearError', 'session-123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('retryAfterError', () => {
|
||||||
|
it('should invoke agent:retryAfterError with sessionId and undefined options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.retryAfterError('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agent:retryAfterError', 'session-123', undefined);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke agent:retryAfterError with options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
const options = { prompt: 'retry prompt', newSession: true };
|
||||||
|
|
||||||
|
const result = await api.retryAfterError('session-123', options);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('agent:retryAfterError', 'session-123', options);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
211
src/__tests__/main/preload/sshRemote.test.ts
Normal file
211
src/__tests__/main/preload/sshRemote.test.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* Tests for sshRemote preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createSshRemoteApi: saveConfig, deleteConfig, getConfigs, getDefaultId, setDefaultId,
|
||||||
|
* test, getSshConfigHosts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createSshRemoteApi, type SshRemoteConfig } from '../../../main/preload/sshRemote';
|
||||||
|
|
||||||
|
describe('SSH Remote Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createSshRemoteApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createSshRemoteApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveConfig', () => {
|
||||||
|
it('should invoke ssh-remote:saveConfig with config', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'config-123' });
|
||||||
|
const config = {
|
||||||
|
name: 'My Server',
|
||||||
|
host: 'server.example.com',
|
||||||
|
port: 22,
|
||||||
|
username: 'user',
|
||||||
|
privateKeyPath: '/path/to/key',
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.saveConfig(config);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:saveConfig', config);
|
||||||
|
expect(result).toEqual({ id: 'config-123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle updating existing config', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ id: 'existing-id' });
|
||||||
|
const config = {
|
||||||
|
id: 'existing-id',
|
||||||
|
name: 'Updated Server',
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.saveConfig(config);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:saveConfig', config);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteConfig', () => {
|
||||||
|
it('should invoke ssh-remote:deleteConfig with id', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.deleteConfig('config-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:deleteConfig', 'config-123');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigs', () => {
|
||||||
|
it('should invoke ssh-remote:getConfigs', async () => {
|
||||||
|
const configs: SshRemoteConfig[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Server 1',
|
||||||
|
host: 'server1.example.com',
|
||||||
|
port: 22,
|
||||||
|
username: 'user1',
|
||||||
|
privateKeyPath: '/path/to/key1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Server 2',
|
||||||
|
host: 'server2.example.com',
|
||||||
|
port: 2222,
|
||||||
|
username: 'user2',
|
||||||
|
privateKeyPath: '/path/to/key2',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(configs);
|
||||||
|
|
||||||
|
const result = await api.getConfigs();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:getConfigs');
|
||||||
|
expect(result).toEqual(configs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDefaultId', () => {
|
||||||
|
it('should invoke ssh-remote:getDefaultId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('default-config-id');
|
||||||
|
|
||||||
|
const result = await api.getDefaultId();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:getDefaultId');
|
||||||
|
expect(result).toBe('default-config-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no default', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await api.getDefaultId();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDefaultId', () => {
|
||||||
|
it('should invoke ssh-remote:setDefaultId with id', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.setDefaultId('config-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:setDefaultId', 'config-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null to clear default', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await api.setDefaultId(null);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:setDefaultId', null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('test', () => {
|
||||||
|
it('should invoke ssh-remote:test with config id', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.test('config-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:test', 'config-123', undefined);
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke ssh-remote:test with config object', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
const config: SshRemoteConfig = {
|
||||||
|
id: 'test',
|
||||||
|
name: 'Test Server',
|
||||||
|
host: 'test.example.com',
|
||||||
|
port: 22,
|
||||||
|
username: 'testuser',
|
||||||
|
privateKeyPath: '/path/to/key',
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.test(config);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:test', config, undefined);
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke ssh-remote:test with agent command', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, agentVersion: '1.0.0' });
|
||||||
|
|
||||||
|
const result = await api.test('config-123', 'claude');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:test', 'config-123', 'claude');
|
||||||
|
expect(result).toEqual({ success: true, agentVersion: '1.0.0' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSshConfigHosts', () => {
|
||||||
|
it('should invoke ssh-remote:getSshConfigHosts', async () => {
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
hosts: [
|
||||||
|
{ host: 'server1', hostName: 'server1.example.com', user: 'user1' },
|
||||||
|
{ host: 'server2', hostName: 'server2.example.com', port: 2222 },
|
||||||
|
],
|
||||||
|
configPath: '/home/user/.ssh/config',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.getSshConfigHosts();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('ssh-remote:getSshConfigHosts');
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
const response = {
|
||||||
|
success: false,
|
||||||
|
hosts: [],
|
||||||
|
error: 'Config file not found',
|
||||||
|
configPath: '/home/user/.ssh/config',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.getSshConfigHosts();
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Config file not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
368
src/__tests__/main/preload/stats.test.ts
Normal file
368
src/__tests__/main/preload/stats.test.ts
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* Tests for stats preload API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createStatsApi } from '../../../main/preload/stats';
|
||||||
|
|
||||||
|
describe('Stats Preload API', () => {
|
||||||
|
let api: ReturnType<typeof createStatsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api = createStatsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordQuery', () => {
|
||||||
|
it('should invoke stats:record-query', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('query-123');
|
||||||
|
const event = {
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
source: 'user' as const,
|
||||||
|
startTime: Date.now(),
|
||||||
|
duration: 5000,
|
||||||
|
projectPath: '/project',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.recordQuery(event);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:record-query', event);
|
||||||
|
expect(result).toBe('query-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startAutoRun', () => {
|
||||||
|
it('should invoke stats:start-autorun', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('autorun-123');
|
||||||
|
const session = {
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
documentPath: '/project/.maestro/tasks.md',
|
||||||
|
startTime: Date.now(),
|
||||||
|
tasksTotal: 10,
|
||||||
|
projectPath: '/project',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.startAutoRun(session);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:start-autorun', session);
|
||||||
|
expect(result).toBe('autorun-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('endAutoRun', () => {
|
||||||
|
it('should invoke stats:end-autorun', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.endAutoRun('autorun-123', 60000, 8);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:end-autorun', 'autorun-123', 60000, 8);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordAutoTask', () => {
|
||||||
|
it('should invoke stats:record-task', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('task-123');
|
||||||
|
const task = {
|
||||||
|
autoRunSessionId: 'autorun-123',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
taskIndex: 3,
|
||||||
|
taskContent: 'Fix the bug',
|
||||||
|
startTime: Date.now(),
|
||||||
|
duration: 10000,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.recordAutoTask(task);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:record-task', task);
|
||||||
|
expect(result).toBe('task-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStats', () => {
|
||||||
|
it('should invoke stats:get-stats without filters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await api.getStats('day');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-stats', 'day', undefined);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke stats:get-stats with filters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
const filters = {
|
||||||
|
agentType: 'claude-code',
|
||||||
|
source: 'user' as const,
|
||||||
|
projectPath: '/project',
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.getStats('week', filters);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-stats', 'week', filters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all time ranges', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await api.getStats('month');
|
||||||
|
await api.getStats('year');
|
||||||
|
await api.getStats('all');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-stats', 'month', undefined);
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-stats', 'year', undefined);
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-stats', 'all', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAutoRunSessions', () => {
|
||||||
|
it('should invoke stats:get-autorun-sessions', async () => {
|
||||||
|
const sessions = [
|
||||||
|
{
|
||||||
|
id: 'ar-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
startTime: Date.now(),
|
||||||
|
duration: 60000,
|
||||||
|
tasksTotal: 10,
|
||||||
|
tasksCompleted: 8,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(sessions);
|
||||||
|
|
||||||
|
const result = await api.getAutoRunSessions('day');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-autorun-sessions', 'day');
|
||||||
|
expect(result).toEqual(sessions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAutoRunTasks', () => {
|
||||||
|
it('should invoke stats:get-autorun-tasks', async () => {
|
||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
id: 'task-1',
|
||||||
|
autoRunSessionId: 'ar-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
taskIndex: 0,
|
||||||
|
taskContent: 'Task 1',
|
||||||
|
startTime: Date.now(),
|
||||||
|
duration: 5000,
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(tasks);
|
||||||
|
|
||||||
|
const result = await api.getAutoRunTasks('ar-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-autorun-tasks', 'ar-1');
|
||||||
|
expect(result).toEqual(tasks);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAggregation', () => {
|
||||||
|
it('should invoke stats:get-aggregation', async () => {
|
||||||
|
const aggregation = {
|
||||||
|
totalQueries: 100,
|
||||||
|
totalDuration: 500000,
|
||||||
|
avgDuration: 5000,
|
||||||
|
byAgent: {
|
||||||
|
'claude-code': { count: 80, duration: 400000 },
|
||||||
|
opencode: { count: 20, duration: 100000 },
|
||||||
|
},
|
||||||
|
bySource: { user: 70, auto: 30 },
|
||||||
|
byDay: [
|
||||||
|
{ date: '2024-01-01', count: 50, duration: 250000 },
|
||||||
|
{ date: '2024-01-02', count: 50, duration: 250000 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(aggregation);
|
||||||
|
|
||||||
|
const result = await api.getAggregation('week');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-aggregation', 'week');
|
||||||
|
expect(result).toEqual(aggregation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exportCsv', () => {
|
||||||
|
it('should invoke stats:export-csv', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/path/to/export.csv');
|
||||||
|
|
||||||
|
const result = await api.exportCsv('month');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:export-csv', 'month');
|
||||||
|
expect(result).toBe('/path/to/export.csv');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onStatsUpdate', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onStatsUpdate(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('stats:updated', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onStatsUpdate(callback);
|
||||||
|
registeredHandler!();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = api.onStatsUpdate(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith('stats:updated', registeredHandler!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearOldData', () => {
|
||||||
|
it('should invoke stats:clear-old-data', async () => {
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
deletedQueryEvents: 100,
|
||||||
|
deletedAutoRunSessions: 10,
|
||||||
|
deletedAutoRunTasks: 50,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await api.clearOldData(30);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:clear-old-data', 30);
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
deletedQueryEvents: 0,
|
||||||
|
deletedAutoRunSessions: 0,
|
||||||
|
deletedAutoRunTasks: 0,
|
||||||
|
error: 'Database error',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.clearOldData(7);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Database error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDatabaseSize', () => {
|
||||||
|
it('should invoke stats:get-database-size', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(1024000);
|
||||||
|
|
||||||
|
const result = await api.getDatabaseSize();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-database-size');
|
||||||
|
expect(result).toBe(1024000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordSessionCreated', () => {
|
||||||
|
it('should invoke stats:record-session-created', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('lifecycle-123');
|
||||||
|
const event = {
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
projectPath: '/project',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
isRemote: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.recordSessionCreated(event);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:record-session-created', event);
|
||||||
|
expect(result).toBe('lifecycle-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for duplicate sessions', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(null);
|
||||||
|
const event = {
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.recordSessionCreated(event);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordSessionClosed', () => {
|
||||||
|
it('should invoke stats:record-session-closed', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.recordSessionClosed('session-1', Date.now());
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'stats:record-session-closed',
|
||||||
|
'session-1',
|
||||||
|
expect.any(Number)
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSessionLifecycle', () => {
|
||||||
|
it('should invoke stats:get-session-lifecycle', async () => {
|
||||||
|
const lifecycle = [
|
||||||
|
{
|
||||||
|
id: 'lc-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
projectPath: '/project',
|
||||||
|
createdAt: Date.now() - 60000,
|
||||||
|
closedAt: Date.now(),
|
||||||
|
duration: 60000,
|
||||||
|
isRemote: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(lifecycle);
|
||||||
|
|
||||||
|
const result = await api.getSessionLifecycle('day');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('stats:get-session-lifecycle', 'day');
|
||||||
|
expect(result).toEqual(lifecycle);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
553
src/__tests__/main/preload/system.test.ts
Normal file
553
src/__tests__/main/preload/system.test.ts
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
/**
|
||||||
|
* Tests for system preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createDialogApi: selectFolder, saveFile
|
||||||
|
* - createFontsApi: detect
|
||||||
|
* - createShellsApi: detect
|
||||||
|
* - createShellApi: openExternal, trashItem
|
||||||
|
* - createTunnelApi: isCloudflaredInstalled, start, stop, getStatus
|
||||||
|
* - createSyncApi: getDefaultPath, getSettings, getCurrentStoragePath, selectSyncFolder, setCustomPath
|
||||||
|
* - createDevtoolsApi: open, close, toggle
|
||||||
|
* - createPowerApi: setEnabled, isEnabled, getStatus, addReason, removeReason
|
||||||
|
* - createUpdatesApi: check, download, install, getStatus, onStatus, setAllowPrerelease
|
||||||
|
* - createAppApi: onQuitConfirmationRequest, confirmQuit, cancelQuit
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
const mockOn = vi.fn();
|
||||||
|
const mockRemoveListener = vi.fn();
|
||||||
|
const mockSend = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
on: (...args: unknown[]) => mockOn(...args),
|
||||||
|
removeListener: (...args: unknown[]) => mockRemoveListener(...args),
|
||||||
|
send: (...args: unknown[]) => mockSend(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDialogApi,
|
||||||
|
createFontsApi,
|
||||||
|
createShellsApi,
|
||||||
|
createShellApi,
|
||||||
|
createTunnelApi,
|
||||||
|
createSyncApi,
|
||||||
|
createDevtoolsApi,
|
||||||
|
createPowerApi,
|
||||||
|
createUpdatesApi,
|
||||||
|
createAppApi,
|
||||||
|
} from '../../../main/preload/system';
|
||||||
|
|
||||||
|
describe('System Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDialogApi', () => {
|
||||||
|
let api: ReturnType<typeof createDialogApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createDialogApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectFolder', () => {
|
||||||
|
it('should invoke dialog:selectFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/selected/path');
|
||||||
|
|
||||||
|
const result = await api.selectFolder();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('dialog:selectFolder');
|
||||||
|
expect(result).toBe('/selected/path');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveFile', () => {
|
||||||
|
it('should invoke dialog:saveFile with options', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/saved/file.txt');
|
||||||
|
const options = {
|
||||||
|
defaultPath: '/default/path.txt',
|
||||||
|
filters: [{ name: 'Text', extensions: ['txt'] }],
|
||||||
|
title: 'Save File',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.saveFile(options);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('dialog:saveFile', options);
|
||||||
|
expect(result).toBe('/saved/file.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createFontsApi', () => {
|
||||||
|
let api: ReturnType<typeof createFontsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createFontsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
it('should invoke fonts:detect', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['Arial', 'Helvetica', 'Monaco']);
|
||||||
|
|
||||||
|
const result = await api.detect();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('fonts:detect');
|
||||||
|
expect(result).toEqual(['Arial', 'Helvetica', 'Monaco']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createShellsApi', () => {
|
||||||
|
let api: ReturnType<typeof createShellsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createShellsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
it('should invoke shells:detect', async () => {
|
||||||
|
const shells = [
|
||||||
|
{ id: 'bash', name: 'Bash', available: true, path: '/bin/bash' },
|
||||||
|
{ id: 'zsh', name: 'Zsh', available: true, path: '/bin/zsh' },
|
||||||
|
];
|
||||||
|
mockInvoke.mockResolvedValue(shells);
|
||||||
|
|
||||||
|
const result = await api.detect();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('shells:detect');
|
||||||
|
expect(result).toEqual(shells);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createShellApi', () => {
|
||||||
|
let api: ReturnType<typeof createShellApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createShellApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openExternal', () => {
|
||||||
|
it('should invoke shell:openExternal with url', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.openExternal('https://example.com');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('shell:openExternal', 'https://example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trashItem', () => {
|
||||||
|
it('should invoke shell:trashItem with path', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.trashItem('/path/to/file');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('shell:trashItem', '/path/to/file');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTunnelApi', () => {
|
||||||
|
let api: ReturnType<typeof createTunnelApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createTunnelApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isCloudflaredInstalled', () => {
|
||||||
|
it('should invoke tunnel:isCloudflaredInstalled', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.isCloudflaredInstalled();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tunnel:isCloudflaredInstalled');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('start', () => {
|
||||||
|
it('should invoke tunnel:start', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ url: 'https://tunnel.example.com' });
|
||||||
|
|
||||||
|
const result = await api.start();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tunnel:start');
|
||||||
|
expect(result).toEqual({ url: 'https://tunnel.example.com' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stop', () => {
|
||||||
|
it('should invoke tunnel:stop', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.stop();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tunnel:stop');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStatus', () => {
|
||||||
|
it('should invoke tunnel:getStatus', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ running: true, url: 'https://tunnel.example.com' });
|
||||||
|
|
||||||
|
const result = await api.getStatus();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('tunnel:getStatus');
|
||||||
|
expect(result).toEqual({ running: true, url: 'https://tunnel.example.com' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSyncApi', () => {
|
||||||
|
let api: ReturnType<typeof createSyncApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createSyncApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDefaultPath', () => {
|
||||||
|
it('should invoke sync:getDefaultPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/default/sync/path');
|
||||||
|
|
||||||
|
const result = await api.getDefaultPath();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sync:getDefaultPath');
|
||||||
|
expect(result).toBe('/default/sync/path');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSettings', () => {
|
||||||
|
it('should invoke sync:getSettings', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ customSyncPath: '/custom/path' });
|
||||||
|
|
||||||
|
const result = await api.getSettings();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sync:getSettings');
|
||||||
|
expect(result).toEqual({ customSyncPath: '/custom/path' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentStoragePath', () => {
|
||||||
|
it('should invoke sync:getCurrentStoragePath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/current/storage/path');
|
||||||
|
|
||||||
|
const result = await api.getCurrentStoragePath();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sync:getCurrentStoragePath');
|
||||||
|
expect(result).toBe('/current/storage/path');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectSyncFolder', () => {
|
||||||
|
it('should invoke sync:selectSyncFolder', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('/selected/sync/folder');
|
||||||
|
|
||||||
|
const result = await api.selectSyncFolder();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sync:selectSyncFolder');
|
||||||
|
expect(result).toBe('/selected/sync/folder');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCustomPath', () => {
|
||||||
|
it('should invoke sync:setCustomPath', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true, migrated: 5 });
|
||||||
|
|
||||||
|
const result = await api.setCustomPath('/new/custom/path');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sync:setCustomPath', '/new/custom/path');
|
||||||
|
expect(result).toEqual({ success: true, migrated: 5 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null to reset path', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.setCustomPath(null);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('sync:setCustomPath', null);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDevtoolsApi', () => {
|
||||||
|
let api: ReturnType<typeof createDevtoolsApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createDevtoolsApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('open', () => {
|
||||||
|
it('should invoke devtools:open', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.open();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('devtools:open');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('close', () => {
|
||||||
|
it('should invoke devtools:close', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.close();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('devtools:close');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle', () => {
|
||||||
|
it('should invoke devtools:toggle', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.toggle();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('devtools:toggle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPowerApi', () => {
|
||||||
|
let api: ReturnType<typeof createPowerApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createPowerApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setEnabled', () => {
|
||||||
|
it('should invoke power:setEnabled', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.setEnabled(true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('power:setEnabled', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEnabled', () => {
|
||||||
|
it('should invoke power:isEnabled', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await api.isEnabled();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('power:isEnabled');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStatus', () => {
|
||||||
|
it('should invoke power:getStatus', async () => {
|
||||||
|
const status = {
|
||||||
|
enabled: true,
|
||||||
|
blocking: true,
|
||||||
|
reasons: ['Auto Run in progress'],
|
||||||
|
platform: 'darwin' as const,
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(status);
|
||||||
|
|
||||||
|
const result = await api.getStatus();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('power:getStatus');
|
||||||
|
expect(result).toEqual(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addReason', () => {
|
||||||
|
it('should invoke power:addReason', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.addReason('Auto Run in progress');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('power:addReason', 'Auto Run in progress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeReason', () => {
|
||||||
|
it('should invoke power:removeReason', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.removeReason('Auto Run in progress');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('power:removeReason', 'Auto Run in progress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createUpdatesApi', () => {
|
||||||
|
let api: ReturnType<typeof createUpdatesApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createUpdatesApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('check', () => {
|
||||||
|
it('should invoke updates:check', async () => {
|
||||||
|
const updateInfo = {
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
latestVersion: '1.1.0',
|
||||||
|
updateAvailable: true,
|
||||||
|
versionsBehind: 1,
|
||||||
|
releases: [],
|
||||||
|
releasesUrl: 'https://github.com/example/releases',
|
||||||
|
};
|
||||||
|
mockInvoke.mockResolvedValue(updateInfo);
|
||||||
|
|
||||||
|
const result = await api.check();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('updates:check', undefined);
|
||||||
|
expect(result).toEqual(updateInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke updates:check with prerelease flag', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({});
|
||||||
|
|
||||||
|
await api.check(true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('updates:check', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('download', () => {
|
||||||
|
it('should invoke updates:download', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
const result = await api.download();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('updates:download');
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('install', () => {
|
||||||
|
it('should invoke updates:install', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.install();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('updates:install');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStatus', () => {
|
||||||
|
it('should invoke updates:getStatus', async () => {
|
||||||
|
const status = { status: 'idle' as const };
|
||||||
|
mockInvoke.mockResolvedValue(status);
|
||||||
|
|
||||||
|
const result = await api.getStatus();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('updates:getStatus');
|
||||||
|
expect(result).toEqual(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onStatus', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onStatus(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('updates:status', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, status: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, status: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.onStatus(callback);
|
||||||
|
registeredHandler!({}, { status: 'downloading' });
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith({ status: 'downloading' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove listener when cleanup is called', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: (event: unknown, status: unknown) => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation(
|
||||||
|
(_channel: string, handler: (event: unknown, status: unknown) => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanup = api.onStatus(callback);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockRemoveListener).toHaveBeenCalledWith('updates:status', registeredHandler!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAllowPrerelease', () => {
|
||||||
|
it('should invoke updates:setAllowPrerelease', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.setAllowPrerelease(true);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('updates:setAllowPrerelease', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAppApi', () => {
|
||||||
|
let api: ReturnType<typeof createAppApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createAppApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onQuitConfirmationRequest', () => {
|
||||||
|
it('should register event listener and return cleanup function', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const cleanup = api.onQuitConfirmationRequest(callback);
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith('app:requestQuitConfirmation', expect.any(Function));
|
||||||
|
expect(typeof cleanup).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback when event is received', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
let registeredHandler: () => void;
|
||||||
|
|
||||||
|
mockOn.mockImplementation((_channel: string, handler: () => void) => {
|
||||||
|
registeredHandler = handler;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onQuitConfirmationRequest(callback);
|
||||||
|
registeredHandler!();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('confirmQuit', () => {
|
||||||
|
it('should send app:quitConfirmed', () => {
|
||||||
|
api.confirmQuit();
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledWith('app:quitConfirmed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cancelQuit', () => {
|
||||||
|
it('should send app:quitCancelled', () => {
|
||||||
|
api.cancelQuit();
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledWith('app:quitCancelled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
272
src/__tests__/main/preload/web.test.ts
Normal file
272
src/__tests__/main/preload/web.test.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* Tests for web preload API
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - createWebApi: broadcastUserInput, broadcastAutoRunState, broadcastTabsChange, broadcastSessionState
|
||||||
|
* - createWebserverApi: getUrl, getConnectedClients
|
||||||
|
* - createLiveApi: toggle, getStatus, getDashboardUrl, getLiveSessions, broadcastActiveSession,
|
||||||
|
* disableAll, startServer, stopServer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock electron ipcRenderer
|
||||||
|
const mockInvoke = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createWebApi, createWebserverApi, createLiveApi } from '../../../main/preload/web';
|
||||||
|
|
||||||
|
describe('Web Preload API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createWebApi', () => {
|
||||||
|
let api: ReturnType<typeof createWebApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createWebApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('broadcastUserInput', () => {
|
||||||
|
it('should invoke web:broadcastUserInput with correct parameters', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.broadcastUserInput('session-123', 'hello world', 'ai');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'web:broadcastUserInput',
|
||||||
|
'session-123',
|
||||||
|
'hello world',
|
||||||
|
'ai'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle terminal mode', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.broadcastUserInput('session-123', 'ls -la', 'terminal');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'web:broadcastUserInput',
|
||||||
|
'session-123',
|
||||||
|
'ls -la',
|
||||||
|
'terminal'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('broadcastAutoRunState', () => {
|
||||||
|
it('should invoke web:broadcastAutoRunState with state', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
const state = {
|
||||||
|
isRunning: true,
|
||||||
|
totalTasks: 10,
|
||||||
|
completedTasks: 5,
|
||||||
|
currentTaskIndex: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.broadcastAutoRunState('session-123', state);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('web:broadcastAutoRunState', 'session-123', state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null state', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.broadcastAutoRunState('session-123', null);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('web:broadcastAutoRunState', 'session-123', null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('broadcastTabsChange', () => {
|
||||||
|
it('should invoke web:broadcastTabsChange with tabs', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: 'tab-1',
|
||||||
|
agentSessionId: 'agent-1',
|
||||||
|
name: 'Tab 1',
|
||||||
|
starred: false,
|
||||||
|
inputValue: '',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
state: 'idle' as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await api.broadcastTabsChange('session-123', tabs, 'tab-1');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'web:broadcastTabsChange',
|
||||||
|
'session-123',
|
||||||
|
tabs,
|
||||||
|
'tab-1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('broadcastSessionState', () => {
|
||||||
|
it('should invoke web:broadcastSessionState with state', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.broadcastSessionState('session-123', 'busy');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'web:broadcastSessionState',
|
||||||
|
'session-123',
|
||||||
|
'busy',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke with additional data', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
const additionalData = { name: 'My Session', toolType: 'claude-code' };
|
||||||
|
|
||||||
|
await api.broadcastSessionState('session-123', 'idle', additionalData);
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith(
|
||||||
|
'web:broadcastSessionState',
|
||||||
|
'session-123',
|
||||||
|
'idle',
|
||||||
|
additionalData
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createWebserverApi', () => {
|
||||||
|
let api: ReturnType<typeof createWebserverApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createWebserverApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUrl', () => {
|
||||||
|
it('should invoke webserver:getUrl', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('http://localhost:3000');
|
||||||
|
|
||||||
|
const result = await api.getUrl();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('webserver:getUrl');
|
||||||
|
expect(result).toBe('http://localhost:3000');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConnectedClients', () => {
|
||||||
|
it('should invoke webserver:getConnectedClients', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(['client-1', 'client-2']);
|
||||||
|
|
||||||
|
const result = await api.getConnectedClients();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('webserver:getConnectedClients');
|
||||||
|
expect(result).toEqual(['client-1', 'client-2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createLiveApi', () => {
|
||||||
|
let api: ReturnType<typeof createLiveApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = createLiveApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle', () => {
|
||||||
|
it('should invoke live:toggle with sessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ enabled: true });
|
||||||
|
|
||||||
|
await api.toggle('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:toggle', 'session-123', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke live:toggle with agentSessionId', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ enabled: true });
|
||||||
|
|
||||||
|
await api.toggle('session-123', 'agent-session-456');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:toggle', 'session-123', 'agent-session-456');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStatus', () => {
|
||||||
|
it('should invoke live:getStatus', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ enabled: true, url: 'https://live.example.com' });
|
||||||
|
|
||||||
|
const result = await api.getStatus('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:getStatus', 'session-123');
|
||||||
|
expect(result).toEqual({ enabled: true, url: 'https://live.example.com' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDashboardUrl', () => {
|
||||||
|
it('should invoke live:getDashboardUrl', async () => {
|
||||||
|
mockInvoke.mockResolvedValue('https://dashboard.example.com');
|
||||||
|
|
||||||
|
const result = await api.getDashboardUrl();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:getDashboardUrl');
|
||||||
|
expect(result).toBe('https://dashboard.example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLiveSessions', () => {
|
||||||
|
it('should invoke live:getLiveSessions', async () => {
|
||||||
|
mockInvoke.mockResolvedValue([{ sessionId: '123', agentSessionId: '456' }]);
|
||||||
|
|
||||||
|
const result = await api.getLiveSessions();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:getLiveSessions');
|
||||||
|
expect(result).toEqual([{ sessionId: '123', agentSessionId: '456' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('broadcastActiveSession', () => {
|
||||||
|
it('should invoke live:broadcastActiveSession', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.broadcastActiveSession('session-123');
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:broadcastActiveSession', 'session-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('disableAll', () => {
|
||||||
|
it('should invoke live:disableAll', async () => {
|
||||||
|
mockInvoke.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await api.disableAll();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:disableAll');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startServer', () => {
|
||||||
|
it('should invoke live:startServer', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.startServer();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:startServer');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopServer', () => {
|
||||||
|
it('should invoke live:stopServer', async () => {
|
||||||
|
mockInvoke.mockResolvedValue({ success: true });
|
||||||
|
|
||||||
|
await api.stopServer();
|
||||||
|
|
||||||
|
expect(mockInvoke).toHaveBeenCalledWith('live:stopServer');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-updater module for Maestro
|
* Auto-updater module for Maestro
|
||||||
* Uses electron-updater to download and install updates from GitHub releases
|
* Uses electron-updater to download and install updates from GitHub releases
|
||||||
|
*
|
||||||
|
* Note: electron-updater accesses electron.app at module load time, so we use
|
||||||
|
* lazy initialization to avoid "Cannot read properties of undefined" errors
|
||||||
|
* when the module is imported before app.whenReady().
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { autoUpdater, UpdateInfo, ProgressInfo } from 'electron-updater';
|
import type { UpdateInfo, ProgressInfo, AppUpdater } from 'electron-updater';
|
||||||
import { BrowserWindow, ipcMain } from 'electron';
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
import { logger } from './utils/logger';
|
import { logger } from './utils/logger';
|
||||||
|
|
||||||
// Don't auto-download - we want user to initiate
|
|
||||||
autoUpdater.autoDownload = false;
|
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
|
||||||
// Default to stable releases only (will be updated from settings)
|
|
||||||
autoUpdater.allowPrerelease = false;
|
|
||||||
|
|
||||||
export interface UpdateStatus {
|
export interface UpdateStatus {
|
||||||
status:
|
status:
|
||||||
| 'idle'
|
| 'idle'
|
||||||
@@ -31,12 +29,34 @@ let mainWindow: BrowserWindow | null = null;
|
|||||||
let currentStatus: UpdateStatus = { status: 'idle' };
|
let currentStatus: UpdateStatus = { status: 'idle' };
|
||||||
let ipcHandlersRegistered = false;
|
let ipcHandlersRegistered = false;
|
||||||
|
|
||||||
|
// Lazy-loaded autoUpdater instance
|
||||||
|
let _autoUpdater: AppUpdater | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the autoUpdater instance, initializing it lazily
|
||||||
|
* This is necessary because electron-updater accesses electron.app at import time
|
||||||
|
*/
|
||||||
|
function getAutoUpdater(): AppUpdater {
|
||||||
|
if (!_autoUpdater) {
|
||||||
|
// Dynamic require to defer the module load
|
||||||
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
_autoUpdater = autoUpdater;
|
||||||
|
// Configure defaults
|
||||||
|
_autoUpdater!.autoDownload = false;
|
||||||
|
_autoUpdater!.autoInstallOnAppQuit = true;
|
||||||
|
_autoUpdater!.allowPrerelease = false;
|
||||||
|
}
|
||||||
|
return _autoUpdater!;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the auto-updater and set up event handlers
|
* Initialize the auto-updater and set up event handlers
|
||||||
*/
|
*/
|
||||||
export function initAutoUpdater(window: BrowserWindow): void {
|
export function initAutoUpdater(window: BrowserWindow): void {
|
||||||
mainWindow = window;
|
mainWindow = window;
|
||||||
|
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
|
|
||||||
// Update available
|
// Update available
|
||||||
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
||||||
logger.info(`Update available: ${info.version}`, 'AutoUpdater');
|
logger.info(`Update available: ${info.version}`, 'AutoUpdater');
|
||||||
@@ -93,6 +113,8 @@ function setupIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
ipcHandlersRegistered = true;
|
ipcHandlersRegistered = true;
|
||||||
|
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
|
|
||||||
// Check for updates using electron-updater (different from manual GitHub API check)
|
// Check for updates using electron-updater (different from manual GitHub API check)
|
||||||
ipcMain.handle('updates:checkAutoUpdater', async () => {
|
ipcMain.handle('updates:checkAutoUpdater', async () => {
|
||||||
try {
|
try {
|
||||||
@@ -159,6 +181,7 @@ function setupIpcHandlers(): void {
|
|||||||
*/
|
*/
|
||||||
export async function checkForUpdatesManual(): Promise<UpdateInfo | null> {
|
export async function checkForUpdatesManual(): Promise<UpdateInfo | null> {
|
||||||
try {
|
try {
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
const result = await autoUpdater.checkForUpdates();
|
const result = await autoUpdater.checkForUpdates();
|
||||||
return result?.updateInfo || null;
|
return result?.updateInfo || null;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -171,6 +194,7 @@ export async function checkForUpdatesManual(): Promise<UpdateInfo | null> {
|
|||||||
* This should be called when the user setting changes
|
* This should be called when the user setting changes
|
||||||
*/
|
*/
|
||||||
export function setAllowPrerelease(allow: boolean): void {
|
export function setAllowPrerelease(allow: boolean): void {
|
||||||
|
const autoUpdater = getAutoUpdater();
|
||||||
autoUpdater.allowPrerelease = allow;
|
autoUpdater.allowPrerelease = allow;
|
||||||
logger.info(`Auto-updater prerelease mode: ${allow ? 'enabled' : 'disabled'}`, 'AutoUpdater');
|
logger.info(`Auto-updater prerelease mode: ${allow ? 'enabled' : 'disabled'}`, 'AutoUpdater');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { app, BrowserWindow, ipcMain } from 'electron';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fsSync from 'fs';
|
import fsSync from 'fs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import * as Sentry from '@sentry/electron/main';
|
// Sentry is imported dynamically below to avoid module-load-time access to electron.app
|
||||||
import { IPCMode } from '@sentry/electron/main';
|
// which causes "Cannot read properties of undefined (reading 'getAppPath')" errors
|
||||||
import { ProcessManager } from './process-manager';
|
import { ProcessManager } from './process-manager';
|
||||||
import { WebServer } from './web-server';
|
import { WebServer } from './web-server';
|
||||||
import { AgentDetector } from './agent-detector';
|
import { AgentDetector } from './agent-detector';
|
||||||
@@ -153,30 +153,6 @@ if (disableGpuAcceleration) {
|
|||||||
console.log('[STARTUP] GPU hardware acceleration disabled by user preference');
|
console.log('[STARTUP] GPU hardware acceleration disabled by user preference');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Sentry for crash reporting
|
|
||||||
// Only enable in production - skip during development to avoid noise from hot-reload artifacts
|
|
||||||
if (crashReportingEnabled && !isDevelopment) {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://2303c5f787f910863d83ed5d27ce8ed2@o4510554134740992.ingest.us.sentry.io/4510554135789568',
|
|
||||||
// Set release version for better debugging
|
|
||||||
release: app.getVersion(),
|
|
||||||
// Use Classic IPC mode to avoid "sentry-ipc:// URL scheme not supported" errors
|
|
||||||
// See: https://github.com/getsentry/sentry-electron/issues/661
|
|
||||||
ipcMode: IPCMode.Classic,
|
|
||||||
// Only send errors, not performance data
|
|
||||||
tracesSampleRate: 0,
|
|
||||||
// Filter out sensitive data
|
|
||||||
beforeSend(event) {
|
|
||||||
// Remove any potential sensitive data from the event
|
|
||||||
if (event.user) {
|
|
||||||
delete event.user.ip_address;
|
|
||||||
delete event.user.email;
|
|
||||||
}
|
|
||||||
return event;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate installation ID on first run (one-time generation)
|
// Generate installation ID on first run (one-time generation)
|
||||||
// This creates a unique identifier per Maestro installation for telemetry differentiation
|
// This creates a unique identifier per Maestro installation for telemetry differentiation
|
||||||
const store = getSettingsStore();
|
const store = getSettingsStore();
|
||||||
@@ -187,9 +163,38 @@ if (!installationId) {
|
|||||||
logger.info('Generated new installation ID', 'Startup', { installationId });
|
logger.info('Generated new installation ID', 'Startup', { installationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add installation ID to Sentry for error correlation across installations
|
// Initialize Sentry for crash reporting (dynamic import to avoid module-load-time errors)
|
||||||
|
// Only enable in production - skip during development to avoid noise from hot-reload artifacts
|
||||||
|
// The dynamic import is necessary because @sentry/electron accesses electron.app at module load time
|
||||||
|
// which fails if the module is imported before app.whenReady() in some Node/Electron version combinations
|
||||||
if (crashReportingEnabled && !isDevelopment) {
|
if (crashReportingEnabled && !isDevelopment) {
|
||||||
Sentry.setTag('installationId', installationId);
|
import('@sentry/electron/main')
|
||||||
|
.then(({ init, setTag, IPCMode }) => {
|
||||||
|
init({
|
||||||
|
dsn: 'https://2303c5f787f910863d83ed5d27ce8ed2@o4510554134740992.ingest.us.sentry.io/4510554135789568',
|
||||||
|
// Set release version for better debugging
|
||||||
|
release: app.getVersion(),
|
||||||
|
// Use Classic IPC mode to avoid "sentry-ipc:// URL scheme not supported" errors
|
||||||
|
// See: https://github.com/getsentry/sentry-electron/issues/661
|
||||||
|
ipcMode: IPCMode.Classic,
|
||||||
|
// Only send errors, not performance data
|
||||||
|
tracesSampleRate: 0,
|
||||||
|
// Filter out sensitive data
|
||||||
|
beforeSend(event) {
|
||||||
|
// Remove any potential sensitive data from the event
|
||||||
|
if (event.user) {
|
||||||
|
delete event.user.ip_address;
|
||||||
|
delete event.user.email;
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Add installation ID to Sentry for error correlation across installations
|
||||||
|
setTag('installationId', installationId);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn('Failed to initialize Sentry', 'Startup', { error: String(err) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create local references to stores for use throughout this module
|
// Create local references to stores for use throughout this module
|
||||||
|
|||||||
3761
src/main/preload.ts
3761
src/main/preload.ts
File diff suppressed because it is too large
Load Diff
189
src/main/preload/agents.ts
Normal file
189
src/main/preload/agents.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for agent management
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.agents namespace for:
|
||||||
|
* - Detecting available agents (Claude Code, Codex, OpenCode, etc.)
|
||||||
|
* - Managing agent configurations and custom paths
|
||||||
|
* - Getting agent capabilities
|
||||||
|
* - Discovering slash commands and models
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capability flags that determine what features are available for each agent.
|
||||||
|
* This is a simplified version for the renderer - full definition in agent-capabilities.ts
|
||||||
|
*/
|
||||||
|
export interface AgentCapabilities {
|
||||||
|
supportsResume: boolean;
|
||||||
|
supportsReadOnlyMode: boolean;
|
||||||
|
supportsJsonOutput: boolean;
|
||||||
|
supportsSessionId: boolean;
|
||||||
|
supportsImageInput: boolean;
|
||||||
|
supportsImageInputOnResume: boolean;
|
||||||
|
supportsSlashCommands: boolean;
|
||||||
|
supportsSessionStorage: boolean;
|
||||||
|
supportsCostTracking: boolean;
|
||||||
|
supportsUsageStats: boolean;
|
||||||
|
supportsBatchMode: boolean;
|
||||||
|
requiresPromptToStart: boolean;
|
||||||
|
supportsStreaming: boolean;
|
||||||
|
supportsResultMessages: boolean;
|
||||||
|
supportsModelSelection: boolean;
|
||||||
|
supportsStreamJsonInput: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent configuration
|
||||||
|
*/
|
||||||
|
export interface AgentConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
args?: string[];
|
||||||
|
available: boolean;
|
||||||
|
path?: string;
|
||||||
|
capabilities?: AgentCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent refresh result
|
||||||
|
*/
|
||||||
|
export interface AgentRefreshResult {
|
||||||
|
agents: AgentConfig[];
|
||||||
|
debugInfo: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the agents API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createAgentsApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Detect available agents
|
||||||
|
*/
|
||||||
|
detect: (sshRemoteId?: string): Promise<AgentConfig[]> =>
|
||||||
|
ipcRenderer.invoke('agents:detect', sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh agent detection (optionally for a specific agent)
|
||||||
|
*/
|
||||||
|
refresh: (agentId?: string, sshRemoteId?: string): Promise<AgentRefreshResult> =>
|
||||||
|
ipcRenderer.invoke('agents:refresh', agentId, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific agent's configuration
|
||||||
|
*/
|
||||||
|
get: (agentId: string): Promise<AgentConfig | null> =>
|
||||||
|
ipcRenderer.invoke('agents:get', agentId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an agent's capabilities
|
||||||
|
*/
|
||||||
|
getCapabilities: (agentId: string): Promise<AgentCapabilities> =>
|
||||||
|
ipcRenderer.invoke('agents:getCapabilities', agentId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an agent's full configuration
|
||||||
|
*/
|
||||||
|
getConfig: (agentId: string): Promise<Record<string, unknown>> =>
|
||||||
|
ipcRenderer.invoke('agents:getConfig', agentId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an agent's configuration
|
||||||
|
*/
|
||||||
|
setConfig: (agentId: string, config: Record<string, unknown>): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('agents:setConfig', agentId, config),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific configuration value for an agent
|
||||||
|
*/
|
||||||
|
getConfigValue: (agentId: string, key: string): Promise<unknown> =>
|
||||||
|
ipcRenderer.invoke('agents:getConfigValue', agentId, key),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a specific configuration value for an agent
|
||||||
|
*/
|
||||||
|
setConfigValue: (agentId: string, key: string, value: unknown): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('agents:setConfigValue', agentId, key, value),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom path for an agent
|
||||||
|
*/
|
||||||
|
setCustomPath: (agentId: string, customPath: string | null): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('agents:setCustomPath', agentId, customPath),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the custom path for an agent
|
||||||
|
*/
|
||||||
|
getCustomPath: (agentId: string): Promise<string | null> =>
|
||||||
|
ipcRenderer.invoke('agents:getCustomPath', agentId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all custom paths for all agents
|
||||||
|
*/
|
||||||
|
getAllCustomPaths: (): Promise<Record<string, string>> =>
|
||||||
|
ipcRenderer.invoke('agents:getAllCustomPaths'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom CLI arguments that are appended to all agent invocations
|
||||||
|
*/
|
||||||
|
setCustomArgs: (agentId: string, customArgs: string | null): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('agents:setCustomArgs', agentId, customArgs),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom CLI arguments for an agent
|
||||||
|
*/
|
||||||
|
getCustomArgs: (agentId: string): Promise<string | null> =>
|
||||||
|
ipcRenderer.invoke('agents:getCustomArgs', agentId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all custom arguments for all agents
|
||||||
|
*/
|
||||||
|
getAllCustomArgs: (): Promise<Record<string, string>> =>
|
||||||
|
ipcRenderer.invoke('agents:getAllCustomArgs'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom environment variables that are passed to all agent invocations
|
||||||
|
*/
|
||||||
|
setCustomEnvVars: (
|
||||||
|
agentId: string,
|
||||||
|
customEnvVars: Record<string, string> | null
|
||||||
|
): Promise<boolean> => ipcRenderer.invoke('agents:setCustomEnvVars', agentId, customEnvVars),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom environment variables for an agent
|
||||||
|
*/
|
||||||
|
getCustomEnvVars: (agentId: string): Promise<Record<string, string> | null> =>
|
||||||
|
ipcRenderer.invoke('agents:getCustomEnvVars', agentId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all custom environment variables for all agents
|
||||||
|
*/
|
||||||
|
getAllCustomEnvVars: (): Promise<Record<string, Record<string, string>>> =>
|
||||||
|
ipcRenderer.invoke('agents:getAllCustomEnvVars'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover available models for agents that support model selection
|
||||||
|
* (e.g., OpenCode with Ollama)
|
||||||
|
*/
|
||||||
|
getModels: (agentId: string, forceRefresh?: boolean): Promise<string[]> =>
|
||||||
|
ipcRenderer.invoke('agents:getModels', agentId, forceRefresh),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover available slash commands for an agent by spawning it briefly
|
||||||
|
* Returns array of command names (e.g., ['compact', 'help', 'my-custom-command'])
|
||||||
|
*/
|
||||||
|
discoverSlashCommands: (
|
||||||
|
agentId: string,
|
||||||
|
cwd: string,
|
||||||
|
customPath?: string
|
||||||
|
): Promise<string[] | null> =>
|
||||||
|
ipcRenderer.invoke('agents:discoverSlashCommands', agentId, cwd, customPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the agents API
|
||||||
|
*/
|
||||||
|
export type AgentsApi = ReturnType<typeof createAgentsApi>;
|
||||||
96
src/main/preload/attachments.ts
Normal file
96
src/main/preload/attachments.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for attachments
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.attachments namespace for:
|
||||||
|
* - Per-session image storage for scratchpad
|
||||||
|
* - Saving, loading, deleting attachments
|
||||||
|
* - Listing attachments for a session
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from attachment operations
|
||||||
|
*/
|
||||||
|
export interface AttachmentResponse {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from loading an attachment
|
||||||
|
*/
|
||||||
|
export interface AttachmentLoadResponse {
|
||||||
|
success: boolean;
|
||||||
|
data?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from listing attachments
|
||||||
|
*/
|
||||||
|
export interface AttachmentListResponse {
|
||||||
|
success: boolean;
|
||||||
|
files?: string[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from getting attachment path
|
||||||
|
*/
|
||||||
|
export interface AttachmentPathResponse {
|
||||||
|
success: boolean;
|
||||||
|
path?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the attachments API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createAttachmentsApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Save an attachment for a session
|
||||||
|
* @param sessionId - Session ID
|
||||||
|
* @param base64Data - Base64 encoded file data
|
||||||
|
* @param filename - Filename to save as
|
||||||
|
*/
|
||||||
|
save: (sessionId: string, base64Data: string, filename: string): Promise<AttachmentResponse> =>
|
||||||
|
ipcRenderer.invoke('attachments:save', sessionId, base64Data, filename),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load an attachment for a session
|
||||||
|
* @param sessionId - Session ID
|
||||||
|
* @param filename - Filename to load
|
||||||
|
*/
|
||||||
|
load: (sessionId: string, filename: string): Promise<AttachmentLoadResponse> =>
|
||||||
|
ipcRenderer.invoke('attachments:load', sessionId, filename),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an attachment for a session
|
||||||
|
* @param sessionId - Session ID
|
||||||
|
* @param filename - Filename to delete
|
||||||
|
*/
|
||||||
|
delete: (sessionId: string, filename: string): Promise<AttachmentResponse> =>
|
||||||
|
ipcRenderer.invoke('attachments:delete', sessionId, filename),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all attachments for a session
|
||||||
|
* @param sessionId - Session ID
|
||||||
|
*/
|
||||||
|
list: (sessionId: string): Promise<AttachmentListResponse> =>
|
||||||
|
ipcRenderer.invoke('attachments:list', sessionId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filesystem path for session attachments
|
||||||
|
* @param sessionId - Session ID
|
||||||
|
*/
|
||||||
|
getPath: (sessionId: string): Promise<AttachmentPathResponse> =>
|
||||||
|
ipcRenderer.invoke('attachments:getPath', sessionId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the attachments API
|
||||||
|
*/
|
||||||
|
export type AttachmentsApi = ReturnType<typeof createAttachmentsApi>;
|
||||||
174
src/main/preload/autorun.ts
Normal file
174
src/main/preload/autorun.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for Auto Run operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.autorun, playbooks, and marketplace namespaces for:
|
||||||
|
* - Auto Run document management
|
||||||
|
* - Playbook CRUD operations
|
||||||
|
* - Marketplace playbook browsing and importing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playbook document configuration
|
||||||
|
*/
|
||||||
|
export interface PlaybookDocument {
|
||||||
|
filename: string;
|
||||||
|
resetOnCompletion: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worktree settings for playbook
|
||||||
|
*/
|
||||||
|
export interface WorktreeSettings {
|
||||||
|
branchNameTemplate: string;
|
||||||
|
createPROnCompletion: boolean;
|
||||||
|
prTargetBranch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playbook definition
|
||||||
|
*/
|
||||||
|
export interface Playbook {
|
||||||
|
name: string;
|
||||||
|
documents: PlaybookDocument[];
|
||||||
|
loopEnabled: boolean;
|
||||||
|
maxLoops?: number | null;
|
||||||
|
prompt: string;
|
||||||
|
worktreeSettings?: WorktreeSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Auto Run API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createAutorunApi() {
|
||||||
|
return {
|
||||||
|
listDocs: (folderPath: string, sshRemoteId?: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:listDocs', folderPath, sshRemoteId),
|
||||||
|
|
||||||
|
hasDocuments: (folderPath: string): Promise<{ hasDocuments: boolean }> =>
|
||||||
|
ipcRenderer.invoke('autorun:hasDocuments', folderPath),
|
||||||
|
|
||||||
|
readDoc: (folderPath: string, filename: string, sshRemoteId?: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:readDoc', folderPath, filename, sshRemoteId),
|
||||||
|
|
||||||
|
writeDoc: (folderPath: string, filename: string, content: string, sshRemoteId?: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:writeDoc', folderPath, filename, content, sshRemoteId),
|
||||||
|
|
||||||
|
saveImage: (folderPath: string, docName: string, base64Data: string, extension: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:saveImage', folderPath, docName, base64Data, extension),
|
||||||
|
|
||||||
|
deleteImage: (folderPath: string, relativePath: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:deleteImage', folderPath, relativePath),
|
||||||
|
|
||||||
|
listImages: (folderPath: string, docName: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:listImages', folderPath, docName),
|
||||||
|
|
||||||
|
deleteFolder: (projectPath: string) => ipcRenderer.invoke('autorun:deleteFolder', projectPath),
|
||||||
|
|
||||||
|
watchFolder: (
|
||||||
|
folderPath: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{ isRemote?: boolean; message?: string }> =>
|
||||||
|
ipcRenderer.invoke('autorun:watchFolder', folderPath, sshRemoteId),
|
||||||
|
|
||||||
|
unwatchFolder: (folderPath: string) => ipcRenderer.invoke('autorun:unwatchFolder', folderPath),
|
||||||
|
|
||||||
|
onFileChanged: (
|
||||||
|
handler: (data: { folderPath: string; filename: string; eventType: string }) => void
|
||||||
|
) => {
|
||||||
|
const wrappedHandler = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
data: { folderPath: string; filename: string; eventType: string }
|
||||||
|
) => handler(data);
|
||||||
|
ipcRenderer.on('autorun:fileChanged', wrappedHandler);
|
||||||
|
return () => ipcRenderer.removeListener('autorun:fileChanged', wrappedHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
createBackup: (folderPath: string, filename: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:createBackup', folderPath, filename),
|
||||||
|
|
||||||
|
restoreBackup: (folderPath: string, filename: string) =>
|
||||||
|
ipcRenderer.invoke('autorun:restoreBackup', folderPath, filename),
|
||||||
|
|
||||||
|
deleteBackups: (folderPath: string) => ipcRenderer.invoke('autorun:deleteBackups', folderPath),
|
||||||
|
|
||||||
|
createWorkingCopy: (
|
||||||
|
folderPath: string,
|
||||||
|
filename: string,
|
||||||
|
loopNumber: number
|
||||||
|
): Promise<{ workingCopyPath: string; originalPath: string }> =>
|
||||||
|
ipcRenderer.invoke('autorun:createWorkingCopy', folderPath, filename, loopNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Playbooks API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createPlaybooksApi() {
|
||||||
|
return {
|
||||||
|
list: (sessionId: string) => ipcRenderer.invoke('playbooks:list', sessionId),
|
||||||
|
|
||||||
|
create: (sessionId: string, playbook: Playbook) =>
|
||||||
|
ipcRenderer.invoke('playbooks:create', sessionId, playbook),
|
||||||
|
|
||||||
|
update: (
|
||||||
|
sessionId: string,
|
||||||
|
playbookId: string,
|
||||||
|
updates: Partial<Playbook & { updatedAt: number }>
|
||||||
|
) => ipcRenderer.invoke('playbooks:update', sessionId, playbookId, updates),
|
||||||
|
|
||||||
|
delete: (sessionId: string, playbookId: string) =>
|
||||||
|
ipcRenderer.invoke('playbooks:delete', sessionId, playbookId),
|
||||||
|
|
||||||
|
deleteAll: (sessionId: string) => ipcRenderer.invoke('playbooks:deleteAll', sessionId),
|
||||||
|
|
||||||
|
export: (sessionId: string, playbookId: string, autoRunFolderPath: string) =>
|
||||||
|
ipcRenderer.invoke('playbooks:export', sessionId, playbookId, autoRunFolderPath),
|
||||||
|
|
||||||
|
import: (sessionId: string, autoRunFolderPath: string) =>
|
||||||
|
ipcRenderer.invoke('playbooks:import', sessionId, autoRunFolderPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Marketplace API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createMarketplaceApi() {
|
||||||
|
return {
|
||||||
|
getManifest: () => ipcRenderer.invoke('marketplace:getManifest'),
|
||||||
|
|
||||||
|
refreshManifest: () => ipcRenderer.invoke('marketplace:refreshManifest'),
|
||||||
|
|
||||||
|
getDocument: (playbookPath: string, filename: string) =>
|
||||||
|
ipcRenderer.invoke('marketplace:getDocument', playbookPath, filename),
|
||||||
|
|
||||||
|
getReadme: (playbookPath: string) => ipcRenderer.invoke('marketplace:getReadme', playbookPath),
|
||||||
|
|
||||||
|
importPlaybook: (
|
||||||
|
playbookId: string,
|
||||||
|
targetFolderName: string,
|
||||||
|
autoRunFolderPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'marketplace:importPlaybook',
|
||||||
|
playbookId,
|
||||||
|
targetFolderName,
|
||||||
|
autoRunFolderPath,
|
||||||
|
sessionId,
|
||||||
|
sshRemoteId
|
||||||
|
),
|
||||||
|
|
||||||
|
onManifestChanged: (handler: () => void) => {
|
||||||
|
const wrappedHandler = () => handler();
|
||||||
|
ipcRenderer.on('marketplace:manifestChanged', wrappedHandler);
|
||||||
|
return () => ipcRenderer.removeListener('marketplace:manifestChanged', wrappedHandler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AutorunApi = ReturnType<typeof createAutorunApi>;
|
||||||
|
export type PlaybooksApi = ReturnType<typeof createPlaybooksApi>;
|
||||||
|
export type MarketplaceApi = ReturnType<typeof createMarketplaceApi>;
|
||||||
132
src/main/preload/commands.ts
Normal file
132
src/main/preload/commands.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for slash commands
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.speckit and window.maestro.openspec namespaces for:
|
||||||
|
* - Spec-Kit slash commands
|
||||||
|
* - OpenSpec slash commands
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command metadata
|
||||||
|
*/
|
||||||
|
export interface CommandMetadata {
|
||||||
|
lastRefreshed: string;
|
||||||
|
commitSha: string;
|
||||||
|
sourceVersion: string;
|
||||||
|
sourceUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command definition
|
||||||
|
*/
|
||||||
|
export interface CommandDefinition {
|
||||||
|
id: string;
|
||||||
|
command: string;
|
||||||
|
description: string;
|
||||||
|
prompt: string;
|
||||||
|
isCustom: boolean;
|
||||||
|
isModified: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Spec-Kit API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createSpeckitApi() {
|
||||||
|
return {
|
||||||
|
getMetadata: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
metadata?: CommandMetadata;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('speckit:getMetadata'),
|
||||||
|
|
||||||
|
getPrompts: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
commands?: CommandDefinition[];
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('speckit:getPrompts'),
|
||||||
|
|
||||||
|
getCommand: (
|
||||||
|
slashCommand: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
command?: CommandDefinition | null;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('speckit:getCommand', slashCommand),
|
||||||
|
|
||||||
|
savePrompt: (
|
||||||
|
id: string,
|
||||||
|
content: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('speckit:savePrompt', id, content),
|
||||||
|
|
||||||
|
resetPrompt: (
|
||||||
|
id: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
prompt?: string;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('speckit:resetPrompt', id),
|
||||||
|
|
||||||
|
refresh: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
metadata?: CommandMetadata;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('speckit:refresh'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the OpenSpec API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createOpenspecApi() {
|
||||||
|
return {
|
||||||
|
getMetadata: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
metadata?: CommandMetadata;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('openspec:getMetadata'),
|
||||||
|
|
||||||
|
getPrompts: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
commands?: CommandDefinition[];
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('openspec:getPrompts'),
|
||||||
|
|
||||||
|
getCommand: (
|
||||||
|
slashCommand: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
command?: CommandDefinition | null;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('openspec:getCommand', slashCommand),
|
||||||
|
|
||||||
|
savePrompt: (
|
||||||
|
id: string,
|
||||||
|
content: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('openspec:savePrompt', id, content),
|
||||||
|
|
||||||
|
resetPrompt: (
|
||||||
|
id: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
prompt?: string;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('openspec:resetPrompt', id),
|
||||||
|
|
||||||
|
refresh: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
metadata?: CommandMetadata;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('openspec:refresh'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SpeckitApi = ReturnType<typeof createSpeckitApi>;
|
||||||
|
export type OpenspecApi = ReturnType<typeof createOpenspecApi>;
|
||||||
66
src/main/preload/context.ts
Normal file
66
src/main/preload/context.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for context operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.context namespace for:
|
||||||
|
* - Session context transfer and grooming
|
||||||
|
* - Context retrieval from stored sessions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message structure from stored sessions
|
||||||
|
*/
|
||||||
|
export interface StoredMessage {
|
||||||
|
type: string;
|
||||||
|
role?: string;
|
||||||
|
content: string;
|
||||||
|
timestamp: string;
|
||||||
|
uuid: string;
|
||||||
|
toolUse?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from getStoredSession
|
||||||
|
*/
|
||||||
|
export interface StoredSessionResponse {
|
||||||
|
messages: StoredMessage[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the context API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createContextApi() {
|
||||||
|
return {
|
||||||
|
// Get context from a stored agent session
|
||||||
|
getStoredSession: (
|
||||||
|
agentId: string,
|
||||||
|
projectRoot: string,
|
||||||
|
sessionId: string
|
||||||
|
): Promise<StoredSessionResponse | null> =>
|
||||||
|
ipcRenderer.invoke('context:getStoredSession', agentId, projectRoot, sessionId),
|
||||||
|
|
||||||
|
// Single-call grooming (recommended) - spawns batch process and returns response
|
||||||
|
groomContext: (projectRoot: string, agentType: string, prompt: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('context:groomContext', projectRoot, agentType, prompt),
|
||||||
|
|
||||||
|
// Cancel all active grooming sessions
|
||||||
|
cancelGrooming: (): Promise<void> => ipcRenderer.invoke('context:cancelGrooming'),
|
||||||
|
|
||||||
|
// DEPRECATED: Create a temporary session for context grooming
|
||||||
|
createGroomingSession: (projectRoot: string, agentType: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('context:createGroomingSession', projectRoot, agentType),
|
||||||
|
|
||||||
|
// DEPRECATED: Send grooming prompt to a session and get response
|
||||||
|
sendGroomingPrompt: (sessionId: string, prompt: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('context:sendGroomingPrompt', sessionId, prompt),
|
||||||
|
|
||||||
|
// Clean up a temporary grooming session
|
||||||
|
cleanupGroomingSession: (sessionId: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('context:cleanupGroomingSession', sessionId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContextApi = ReturnType<typeof createContextApi>;
|
||||||
66
src/main/preload/debug.ts
Normal file
66
src/main/preload/debug.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for debug and document graph operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.debug and window.maestro.documentGraph namespaces for:
|
||||||
|
* - Debug package generation
|
||||||
|
* - Document graph file watching
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug package options
|
||||||
|
*/
|
||||||
|
export interface DebugPackageOptions {
|
||||||
|
includeLogs?: boolean;
|
||||||
|
includeErrors?: boolean;
|
||||||
|
includeSessions?: boolean;
|
||||||
|
includeGroupChats?: boolean;
|
||||||
|
includeBatchState?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document graph file change event
|
||||||
|
*/
|
||||||
|
export interface DocumentGraphChange {
|
||||||
|
filePath: string;
|
||||||
|
eventType: 'add' | 'change' | 'unlink';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Debug API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createDebugApi() {
|
||||||
|
return {
|
||||||
|
createPackage: (options?: DebugPackageOptions) =>
|
||||||
|
ipcRenderer.invoke('debug:createPackage', options),
|
||||||
|
|
||||||
|
previewPackage: () => ipcRenderer.invoke('debug:previewPackage'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Document Graph API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createDocumentGraphApi() {
|
||||||
|
return {
|
||||||
|
watchFolder: (rootPath: string) => ipcRenderer.invoke('documentGraph:watchFolder', rootPath),
|
||||||
|
|
||||||
|
unwatchFolder: (rootPath: string) =>
|
||||||
|
ipcRenderer.invoke('documentGraph:unwatchFolder', rootPath),
|
||||||
|
|
||||||
|
onFilesChanged: (
|
||||||
|
handler: (data: { rootPath: string; changes: DocumentGraphChange[] }) => void
|
||||||
|
) => {
|
||||||
|
const wrappedHandler = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
data: { rootPath: string; changes: DocumentGraphChange[] }
|
||||||
|
) => handler(data);
|
||||||
|
ipcRenderer.on('documentGraph:filesChanged', wrappedHandler);
|
||||||
|
return () => ipcRenderer.removeListener('documentGraph:filesChanged', wrappedHandler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DebugApi = ReturnType<typeof createDebugApi>;
|
||||||
|
export type DocumentGraphApi = ReturnType<typeof createDocumentGraphApi>;
|
||||||
108
src/main/preload/files.ts
Normal file
108
src/main/preload/files.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for file operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.tempfile, history, and cli namespaces for:
|
||||||
|
* - Temporary file operations
|
||||||
|
* - History persistence
|
||||||
|
* - CLI activity monitoring
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History entry
|
||||||
|
*/
|
||||||
|
export interface HistoryEntry {
|
||||||
|
id: string;
|
||||||
|
type: 'AUTO' | 'USER';
|
||||||
|
timestamp: number;
|
||||||
|
summary: string;
|
||||||
|
fullResponse?: string;
|
||||||
|
agentSessionId?: string;
|
||||||
|
projectPath: string;
|
||||||
|
sessionId?: string;
|
||||||
|
sessionName?: string;
|
||||||
|
contextUsage?: number;
|
||||||
|
usageStats?: {
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheReadInputTokens: number;
|
||||||
|
cacheCreationInputTokens: number;
|
||||||
|
totalCostUsd: number;
|
||||||
|
contextWindow: number;
|
||||||
|
};
|
||||||
|
success?: boolean;
|
||||||
|
elapsedTimeMs?: number;
|
||||||
|
validated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the tempfile API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createTempfileApi() {
|
||||||
|
return {
|
||||||
|
write: (content: string, filename?: string) =>
|
||||||
|
ipcRenderer.invoke('tempfile:write', content, filename),
|
||||||
|
read: (filePath: string) => ipcRenderer.invoke('tempfile:read', filePath),
|
||||||
|
delete: (filePath: string) => ipcRenderer.invoke('tempfile:delete', filePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the history API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createHistoryApi() {
|
||||||
|
return {
|
||||||
|
getAll: (projectPath?: string, sessionId?: string) =>
|
||||||
|
ipcRenderer.invoke('history:getAll', projectPath, sessionId),
|
||||||
|
|
||||||
|
getAllPaginated: (options?: {
|
||||||
|
projectPath?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
pagination?: { limit?: number; offset?: number };
|
||||||
|
}) => ipcRenderer.invoke('history:getAllPaginated', options),
|
||||||
|
|
||||||
|
add: (entry: HistoryEntry) => ipcRenderer.invoke('history:add', entry),
|
||||||
|
|
||||||
|
clear: (projectPath?: string) => ipcRenderer.invoke('history:clear', projectPath),
|
||||||
|
|
||||||
|
delete: (entryId: string, sessionId?: string) =>
|
||||||
|
ipcRenderer.invoke('history:delete', entryId, sessionId),
|
||||||
|
|
||||||
|
update: (entryId: string, updates: { validated?: boolean }, sessionId?: string) =>
|
||||||
|
ipcRenderer.invoke('history:update', entryId, updates, sessionId),
|
||||||
|
|
||||||
|
updateSessionName: (agentSessionId: string, sessionName: string) =>
|
||||||
|
ipcRenderer.invoke('history:updateSessionName', agentSessionId, sessionName),
|
||||||
|
|
||||||
|
getFilePath: (sessionId: string) => ipcRenderer.invoke('history:getFilePath', sessionId),
|
||||||
|
|
||||||
|
listSessions: () => ipcRenderer.invoke('history:listSessions'),
|
||||||
|
|
||||||
|
onExternalChange: (handler: () => void) => {
|
||||||
|
const wrappedHandler = () => handler();
|
||||||
|
ipcRenderer.on('history:externalChange', wrappedHandler);
|
||||||
|
return () => ipcRenderer.removeListener('history:externalChange', wrappedHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
reload: () => ipcRenderer.invoke('history:reload'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the CLI activity API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createCliApi() {
|
||||||
|
return {
|
||||||
|
getActivity: () => ipcRenderer.invoke('cli:getActivity'),
|
||||||
|
onActivityChange: (handler: () => void) => {
|
||||||
|
const wrappedHandler = () => handler();
|
||||||
|
ipcRenderer.on('cli:activityChange', wrappedHandler);
|
||||||
|
return () => ipcRenderer.removeListener('cli:activityChange', wrappedHandler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TempfileApi = ReturnType<typeof createTempfileApi>;
|
||||||
|
export type HistoryApi = ReturnType<typeof createHistoryApi>;
|
||||||
|
export type CliApi = ReturnType<typeof createCliApi>;
|
||||||
125
src/main/preload/fs.ts
Normal file
125
src/main/preload/fs.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for filesystem operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.fs namespace for:
|
||||||
|
* - Reading directories and files
|
||||||
|
* - File stats and sizes
|
||||||
|
* - Writing, renaming, and deleting files
|
||||||
|
* - SSH remote support for all operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory entry information
|
||||||
|
*/
|
||||||
|
export interface DirectoryEntry {
|
||||||
|
name: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File stat information
|
||||||
|
*/
|
||||||
|
export interface FileStat {
|
||||||
|
size: number;
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
isFile: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory size information
|
||||||
|
*/
|
||||||
|
export interface DirectorySizeInfo {
|
||||||
|
totalSize: number;
|
||||||
|
fileCount: number;
|
||||||
|
folderCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item count information
|
||||||
|
*/
|
||||||
|
export interface ItemCountInfo {
|
||||||
|
fileCount: number;
|
||||||
|
folderCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the filesystem API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createFsApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get the user's home directory
|
||||||
|
*/
|
||||||
|
homeDir: (): Promise<string> => ipcRenderer.invoke('fs:homeDir'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read directory contents
|
||||||
|
*/
|
||||||
|
readDir: (dirPath: string, sshRemoteId?: string): Promise<DirectoryEntry[]> =>
|
||||||
|
ipcRenderer.invoke('fs:readDir', dirPath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file contents
|
||||||
|
*/
|
||||||
|
readFile: (filePath: string, sshRemoteId?: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('fs:readFile', filePath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write file contents
|
||||||
|
*/
|
||||||
|
writeFile: (filePath: string, content: string): Promise<{ success: boolean }> =>
|
||||||
|
ipcRenderer.invoke('fs:writeFile', filePath, content),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file/directory stats
|
||||||
|
*/
|
||||||
|
stat: (filePath: string, sshRemoteId?: string): Promise<FileStat> =>
|
||||||
|
ipcRenderer.invoke('fs:stat', filePath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get directory size information
|
||||||
|
*/
|
||||||
|
directorySize: (dirPath: string, sshRemoteId?: string): Promise<DirectorySizeInfo> =>
|
||||||
|
ipcRenderer.invoke('fs:directorySize', dirPath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an image from URL and return as base64
|
||||||
|
*/
|
||||||
|
fetchImageAsBase64: (url: string): Promise<string | null> =>
|
||||||
|
ipcRenderer.invoke('fs:fetchImageAsBase64', url),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a file or directory
|
||||||
|
*/
|
||||||
|
rename: (
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{ success: boolean }> =>
|
||||||
|
ipcRenderer.invoke('fs:rename', oldPath, newPath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file or directory
|
||||||
|
*/
|
||||||
|
delete: (
|
||||||
|
targetPath: string,
|
||||||
|
options?: { recursive?: boolean; sshRemoteId?: string }
|
||||||
|
): Promise<{ success: boolean }> => ipcRenderer.invoke('fs:delete', targetPath, options),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count files and folders in a directory
|
||||||
|
*/
|
||||||
|
countItems: (dirPath: string, sshRemoteId?: string): Promise<ItemCountInfo> =>
|
||||||
|
ipcRenderer.invoke('fs:countItems', dirPath, sshRemoteId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the filesystem API
|
||||||
|
*/
|
||||||
|
export type FsApi = ReturnType<typeof createFsApi>;
|
||||||
359
src/main/preload/git.ts
Normal file
359
src/main/preload/git.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for git operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.git namespace for:
|
||||||
|
* - Git status, diff, branch operations
|
||||||
|
* - Git log and commit viewing
|
||||||
|
* - Git worktree operations for Auto Run parallelization
|
||||||
|
* - GitHub CLI integration (PR creation, gists)
|
||||||
|
* - SSH remote support for all operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git worktree information
|
||||||
|
*/
|
||||||
|
export interface WorktreeInfo {
|
||||||
|
success: boolean;
|
||||||
|
exists?: boolean;
|
||||||
|
isWorktree?: boolean;
|
||||||
|
currentBranch?: string;
|
||||||
|
repoRoot?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git worktree list entry
|
||||||
|
*/
|
||||||
|
export interface WorktreeEntry {
|
||||||
|
path: string;
|
||||||
|
head: string;
|
||||||
|
branch: string | null;
|
||||||
|
isBare: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git subdirectory scan result
|
||||||
|
*/
|
||||||
|
export interface GitSubdirEntry {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
isWorktree: boolean;
|
||||||
|
branch: string | null;
|
||||||
|
repoRoot: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git log entry
|
||||||
|
*/
|
||||||
|
export interface GitLogEntry {
|
||||||
|
hash: string;
|
||||||
|
shortHash: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
refs: string[];
|
||||||
|
subject: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovered worktree event data
|
||||||
|
*/
|
||||||
|
export interface WorktreeDiscoveredData {
|
||||||
|
sessionId: string;
|
||||||
|
worktree: {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
branch: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the git API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createGitApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get git status for a repository
|
||||||
|
*/
|
||||||
|
status: (cwd: string, sshRemoteId?: string, remoteCwd?: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('git:status', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get git diff for a repository or specific file
|
||||||
|
*/
|
||||||
|
diff: (cwd: string, file?: string, sshRemoteId?: string, remoteCwd?: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('git:diff', cwd, file, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a directory is a git repository
|
||||||
|
*/
|
||||||
|
isRepo: (cwd: string, sshRemoteId?: string, remoteCwd?: string): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('git:isRepo', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get git diff numstat
|
||||||
|
*/
|
||||||
|
numstat: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string,
|
||||||
|
remoteCwd?: string
|
||||||
|
): Promise<{ stdout: string; stderr: string }> =>
|
||||||
|
ipcRenderer.invoke('git:numstat', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current branch name
|
||||||
|
*/
|
||||||
|
branch: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string,
|
||||||
|
remoteCwd?: string
|
||||||
|
): Promise<{ stdout: string; stderr: string }> =>
|
||||||
|
ipcRenderer.invoke('git:branch', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of all branches
|
||||||
|
*/
|
||||||
|
branches: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string,
|
||||||
|
remoteCwd?: string
|
||||||
|
): Promise<{ stdout: string; stderr: string }> =>
|
||||||
|
ipcRenderer.invoke('git:branches', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of tags
|
||||||
|
*/
|
||||||
|
tags: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string,
|
||||||
|
remoteCwd?: string
|
||||||
|
): Promise<{ stdout: string; stderr: string }> =>
|
||||||
|
ipcRenderer.invoke('git:tags', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remote URL
|
||||||
|
*/
|
||||||
|
remote: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string,
|
||||||
|
remoteCwd?: string
|
||||||
|
): Promise<{ stdout: string; stderr: string }> =>
|
||||||
|
ipcRenderer.invoke('git:remote', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get comprehensive git info (branch, remote, ahead/behind, changes)
|
||||||
|
*/
|
||||||
|
info: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string,
|
||||||
|
remoteCwd?: string
|
||||||
|
): Promise<{
|
||||||
|
branch: string;
|
||||||
|
remote: string;
|
||||||
|
behind: number;
|
||||||
|
ahead: number;
|
||||||
|
uncommittedChanges: number;
|
||||||
|
}> => ipcRenderer.invoke('git:info', cwd, sshRemoteId, remoteCwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get git log with optional limit and search
|
||||||
|
*/
|
||||||
|
log: (
|
||||||
|
cwd: string,
|
||||||
|
options?: { limit?: number; search?: string }
|
||||||
|
): Promise<{
|
||||||
|
entries: GitLogEntry[];
|
||||||
|
error: string | null;
|
||||||
|
}> => ipcRenderer.invoke('git:log', cwd, options),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit count
|
||||||
|
*/
|
||||||
|
commitCount: (cwd: string): Promise<{ count: number; error: string | null }> =>
|
||||||
|
ipcRenderer.invoke('git:commitCount', cwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a specific commit
|
||||||
|
*/
|
||||||
|
show: (cwd: string, hash: string): Promise<{ stdout: string; stderr: string }> =>
|
||||||
|
ipcRenderer.invoke('git:show', cwd, hash),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show file content at a specific ref
|
||||||
|
*/
|
||||||
|
showFile: (
|
||||||
|
cwd: string,
|
||||||
|
ref: string,
|
||||||
|
filePath: string
|
||||||
|
): Promise<{ content?: string; error?: string }> =>
|
||||||
|
ipcRenderer.invoke('git:showFile', cwd, ref, filePath),
|
||||||
|
|
||||||
|
// Git worktree operations for Auto Run parallelization
|
||||||
|
// All worktree operations support SSH remote execution via optional sshRemoteId parameter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get worktree information
|
||||||
|
*/
|
||||||
|
worktreeInfo: (worktreePath: string, sshRemoteId?: string): Promise<WorktreeInfo> =>
|
||||||
|
ipcRenderer.invoke('git:worktreeInfo', worktreePath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the root of a git repository
|
||||||
|
*/
|
||||||
|
getRepoRoot: (
|
||||||
|
cwd: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{ success: boolean; root?: string; error?: string }> =>
|
||||||
|
ipcRenderer.invoke('git:getRepoRoot', cwd, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a worktree (create if needed)
|
||||||
|
*/
|
||||||
|
worktreeSetup: (
|
||||||
|
mainRepoCwd: string,
|
||||||
|
worktreePath: string,
|
||||||
|
branchName: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
created?: boolean;
|
||||||
|
currentBranch?: string;
|
||||||
|
requestedBranch?: string;
|
||||||
|
branchMismatch?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> =>
|
||||||
|
ipcRenderer.invoke('git:worktreeSetup', mainRepoCwd, worktreePath, branchName, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkout a branch in a worktree
|
||||||
|
*/
|
||||||
|
worktreeCheckout: (
|
||||||
|
worktreePath: string,
|
||||||
|
branchName: string,
|
||||||
|
createIfMissing: boolean,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
hasUncommittedChanges: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'git:worktreeCheckout',
|
||||||
|
worktreePath,
|
||||||
|
branchName,
|
||||||
|
createIfMissing,
|
||||||
|
sshRemoteId
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GitHub PR
|
||||||
|
*/
|
||||||
|
createPR: (
|
||||||
|
worktreePath: string,
|
||||||
|
baseBranch: string,
|
||||||
|
title: string,
|
||||||
|
body: string,
|
||||||
|
ghPath?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
prUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('git:createPR', worktreePath, baseBranch, title, body, ghPath),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default branch of a repository
|
||||||
|
*/
|
||||||
|
getDefaultBranch: (
|
||||||
|
cwd: string
|
||||||
|
): Promise<{ success: boolean; branch?: string; error?: string }> =>
|
||||||
|
ipcRenderer.invoke('git:getDefaultBranch', cwd),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if GitHub CLI is installed and authenticated
|
||||||
|
*/
|
||||||
|
checkGhCli: (ghPath?: string): Promise<{ installed: boolean; authenticated: boolean }> =>
|
||||||
|
ipcRenderer.invoke('git:checkGhCli', ghPath),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GitHub Gist from file content
|
||||||
|
*/
|
||||||
|
createGist: (
|
||||||
|
filename: string,
|
||||||
|
content: string,
|
||||||
|
description: string,
|
||||||
|
isPublic: boolean,
|
||||||
|
ghPath?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
gistUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('git:createGist', filename, content, description, isPublic, ghPath),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all worktrees for a git repository
|
||||||
|
* Supports SSH remote execution via optional sshRemoteId parameter
|
||||||
|
*/
|
||||||
|
listWorktrees: (cwd: string, sshRemoteId?: string): Promise<{ worktrees: WorktreeEntry[] }> =>
|
||||||
|
ipcRenderer.invoke('git:listWorktrees', cwd, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan a directory for subdirectories that are git repositories or worktrees
|
||||||
|
* Supports SSH remote execution via optional sshRemoteId parameter
|
||||||
|
*/
|
||||||
|
scanWorktreeDirectory: (
|
||||||
|
parentPath: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{ gitSubdirs: GitSubdirEntry[] }> =>
|
||||||
|
ipcRenderer.invoke('git:scanWorktreeDirectory', parentPath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch a worktree directory for new worktrees
|
||||||
|
* Note: File watching is not available for SSH remote sessions.
|
||||||
|
* For remote sessions, returns isRemote: true indicating polling should be used instead.
|
||||||
|
*/
|
||||||
|
watchWorktreeDirectory: (
|
||||||
|
sessionId: string,
|
||||||
|
worktreePath: string,
|
||||||
|
sshRemoteId?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
isRemote?: boolean;
|
||||||
|
message?: string;
|
||||||
|
}> => ipcRenderer.invoke('git:watchWorktreeDirectory', sessionId, worktreePath, sshRemoteId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop watching a worktree directory
|
||||||
|
*/
|
||||||
|
unwatchWorktreeDirectory: (sessionId: string): Promise<{ success: boolean }> =>
|
||||||
|
ipcRenderer.invoke('git:unwatchWorktreeDirectory', sessionId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a worktree directory from disk
|
||||||
|
*/
|
||||||
|
removeWorktree: (
|
||||||
|
worktreePath: string,
|
||||||
|
force?: boolean
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
hasUncommittedChanges?: boolean;
|
||||||
|
}> => ipcRenderer.invoke('git:removeWorktree', worktreePath, force),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to discovered worktrees
|
||||||
|
*/
|
||||||
|
onWorktreeDiscovered: (callback: (data: WorktreeDiscoveredData) => void): (() => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, data: WorktreeDiscoveredData) =>
|
||||||
|
callback(data);
|
||||||
|
ipcRenderer.on('worktree:discovered', handler);
|
||||||
|
return () => ipcRenderer.removeListener('worktree:discovered', handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the git API
|
||||||
|
*/
|
||||||
|
export type GitApi = ReturnType<typeof createGitApi>;
|
||||||
213
src/main/preload/groupChat.ts
Normal file
213
src/main/preload/groupChat.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for group chat operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.groupChat namespace for:
|
||||||
|
* - Group chat creation and management
|
||||||
|
* - Moderator and participant control
|
||||||
|
* - Chat history and messages
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moderator configuration
|
||||||
|
*/
|
||||||
|
export interface ModeratorConfig {
|
||||||
|
customPath?: string;
|
||||||
|
customArgs?: string;
|
||||||
|
customEnvVars?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Participant definition
|
||||||
|
*/
|
||||||
|
export interface Participant {
|
||||||
|
name: string;
|
||||||
|
agentId: string;
|
||||||
|
sessionId: string;
|
||||||
|
addedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat message
|
||||||
|
*/
|
||||||
|
export interface ChatMessage {
|
||||||
|
timestamp: string;
|
||||||
|
from: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History entry
|
||||||
|
*/
|
||||||
|
export interface GroupChatHistoryEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
summary: string;
|
||||||
|
participantName: string;
|
||||||
|
participantColor: string;
|
||||||
|
type: 'delegation' | 'response' | 'synthesis' | 'error';
|
||||||
|
elapsedTimeMs?: number;
|
||||||
|
tokenCount?: number;
|
||||||
|
cost?: number;
|
||||||
|
fullResponse?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moderator usage stats
|
||||||
|
*/
|
||||||
|
export interface ModeratorUsage {
|
||||||
|
contextUsage: number;
|
||||||
|
totalCost: number;
|
||||||
|
tokenCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Group Chat API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createGroupChatApi() {
|
||||||
|
return {
|
||||||
|
// Storage
|
||||||
|
create: (name: string, moderatorAgentId: string, moderatorConfig?: ModeratorConfig) =>
|
||||||
|
ipcRenderer.invoke('groupChat:create', name, moderatorAgentId, moderatorConfig),
|
||||||
|
|
||||||
|
list: () => ipcRenderer.invoke('groupChat:list'),
|
||||||
|
|
||||||
|
load: (id: string) => ipcRenderer.invoke('groupChat:load', id),
|
||||||
|
|
||||||
|
delete: (id: string) => ipcRenderer.invoke('groupChat:delete', id),
|
||||||
|
|
||||||
|
rename: (id: string, name: string) => ipcRenderer.invoke('groupChat:rename', id, name),
|
||||||
|
|
||||||
|
update: (
|
||||||
|
id: string,
|
||||||
|
updates: {
|
||||||
|
name?: string;
|
||||||
|
moderatorAgentId?: string;
|
||||||
|
moderatorConfig?: ModeratorConfig;
|
||||||
|
}
|
||||||
|
) => ipcRenderer.invoke('groupChat:update', id, updates),
|
||||||
|
|
||||||
|
// Chat log
|
||||||
|
appendMessage: (id: string, from: string, content: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:appendMessage', id, from, content),
|
||||||
|
|
||||||
|
getMessages: (id: string) => ipcRenderer.invoke('groupChat:getMessages', id),
|
||||||
|
|
||||||
|
saveImage: (id: string, imageData: string, filename: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:saveImage', id, imageData, filename),
|
||||||
|
|
||||||
|
// Moderator
|
||||||
|
startModerator: (id: string) => ipcRenderer.invoke('groupChat:startModerator', id),
|
||||||
|
|
||||||
|
sendToModerator: (id: string, message: string, images?: string[], readOnly?: boolean) =>
|
||||||
|
ipcRenderer.invoke('groupChat:sendToModerator', id, message, images, readOnly),
|
||||||
|
|
||||||
|
stopModerator: (id: string) => ipcRenderer.invoke('groupChat:stopModerator', id),
|
||||||
|
|
||||||
|
getModeratorSessionId: (id: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:getModeratorSessionId', id),
|
||||||
|
|
||||||
|
// Participants
|
||||||
|
addParticipant: (id: string, name: string, agentId: string, cwd?: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:addParticipant', id, name, agentId, cwd),
|
||||||
|
|
||||||
|
sendToParticipant: (id: string, name: string, message: string, images?: string[]) =>
|
||||||
|
ipcRenderer.invoke('groupChat:sendToParticipant', id, name, message, images),
|
||||||
|
|
||||||
|
removeParticipant: (id: string, name: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:removeParticipant', id, name),
|
||||||
|
|
||||||
|
resetParticipantContext: (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
cwd?: string
|
||||||
|
): Promise<{ newAgentSessionId: string }> =>
|
||||||
|
ipcRenderer.invoke('groupChat:resetParticipantContext', id, name, cwd),
|
||||||
|
|
||||||
|
// History
|
||||||
|
getHistory: (id: string) => ipcRenderer.invoke('groupChat:getHistory', id),
|
||||||
|
|
||||||
|
addHistoryEntry: (id: string, entry: Omit<GroupChatHistoryEntry, 'id'>) =>
|
||||||
|
ipcRenderer.invoke('groupChat:addHistoryEntry', id, entry),
|
||||||
|
|
||||||
|
deleteHistoryEntry: (groupChatId: string, entryId: string) =>
|
||||||
|
ipcRenderer.invoke('groupChat:deleteHistoryEntry', groupChatId, entryId),
|
||||||
|
|
||||||
|
clearHistory: (id: string) => ipcRenderer.invoke('groupChat:clearHistory', id),
|
||||||
|
|
||||||
|
getHistoryFilePath: (id: string) => ipcRenderer.invoke('groupChat:getHistoryFilePath', id),
|
||||||
|
|
||||||
|
// Export
|
||||||
|
getImages: (id: string): Promise<Record<string, string>> =>
|
||||||
|
ipcRenderer.invoke('groupChat:getImages', id),
|
||||||
|
|
||||||
|
// Events
|
||||||
|
onMessage: (callback: (groupChatId: string, message: ChatMessage) => void) => {
|
||||||
|
const handler = (_: any, groupChatId: string, message: ChatMessage) =>
|
||||||
|
callback(groupChatId, message);
|
||||||
|
ipcRenderer.on('groupChat:message', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:message', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
onStateChange: (
|
||||||
|
callback: (
|
||||||
|
groupChatId: string,
|
||||||
|
state: 'idle' | 'moderator-thinking' | 'agent-working'
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
const handler = (
|
||||||
|
_: any,
|
||||||
|
groupChatId: string,
|
||||||
|
state: 'idle' | 'moderator-thinking' | 'agent-working'
|
||||||
|
) => callback(groupChatId, state);
|
||||||
|
ipcRenderer.on('groupChat:stateChange', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:stateChange', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
onParticipantsChanged: (
|
||||||
|
callback: (groupChatId: string, participants: Participant[]) => void
|
||||||
|
) => {
|
||||||
|
const handler = (_: any, groupChatId: string, participants: Participant[]) =>
|
||||||
|
callback(groupChatId, participants);
|
||||||
|
ipcRenderer.on('groupChat:participantsChanged', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:participantsChanged', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
onModeratorUsage: (callback: (groupChatId: string, usage: ModeratorUsage) => void) => {
|
||||||
|
const handler = (_: any, groupChatId: string, usage: ModeratorUsage) =>
|
||||||
|
callback(groupChatId, usage);
|
||||||
|
ipcRenderer.on('groupChat:moderatorUsage', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:moderatorUsage', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
onHistoryEntry: (callback: (groupChatId: string, entry: GroupChatHistoryEntry) => void) => {
|
||||||
|
const handler = (_: any, groupChatId: string, entry: GroupChatHistoryEntry) =>
|
||||||
|
callback(groupChatId, entry);
|
||||||
|
ipcRenderer.on('groupChat:historyEntry', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:historyEntry', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
onParticipantState: (
|
||||||
|
callback: (groupChatId: string, participantName: string, state: 'idle' | 'working') => void
|
||||||
|
) => {
|
||||||
|
const handler = (
|
||||||
|
_: any,
|
||||||
|
groupChatId: string,
|
||||||
|
participantName: string,
|
||||||
|
state: 'idle' | 'working'
|
||||||
|
) => callback(groupChatId, participantName, state);
|
||||||
|
ipcRenderer.on('groupChat:participantState', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:participantState', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
onModeratorSessionIdChanged: (callback: (groupChatId: string, sessionId: string) => void) => {
|
||||||
|
const handler = (_: any, groupChatId: string, sessionId: string) =>
|
||||||
|
callback(groupChatId, sessionId);
|
||||||
|
ipcRenderer.on('groupChat:moderatorSessionIdChanged', handler);
|
||||||
|
return () => ipcRenderer.removeListener('groupChat:moderatorSessionIdChanged', handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GroupChatApi = ReturnType<typeof createGroupChatApi>;
|
||||||
407
src/main/preload/index.ts
Normal file
407
src/main/preload/index.ts
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
/**
|
||||||
|
* Electron Preload Script
|
||||||
|
*
|
||||||
|
* This script runs in the renderer process before any web content is loaded.
|
||||||
|
* It exposes a safe subset of Electron and Node.js APIs to the renderer via contextBridge.
|
||||||
|
*
|
||||||
|
* All APIs are organized in modular files within this directory for maintainability.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { contextBridge } from 'electron';
|
||||||
|
|
||||||
|
// Import all factory functions for contextBridge exposure
|
||||||
|
import {
|
||||||
|
createSettingsApi,
|
||||||
|
createSessionsApi,
|
||||||
|
createGroupsApi,
|
||||||
|
createAgentErrorApi,
|
||||||
|
} from './settings';
|
||||||
|
import { createContextApi } from './context';
|
||||||
|
import { createWebApi, createWebserverApi, createLiveApi } from './web';
|
||||||
|
import {
|
||||||
|
createDialogApi,
|
||||||
|
createFontsApi,
|
||||||
|
createShellsApi,
|
||||||
|
createShellApi,
|
||||||
|
createTunnelApi,
|
||||||
|
createSyncApi,
|
||||||
|
createDevtoolsApi,
|
||||||
|
createPowerApi,
|
||||||
|
createUpdatesApi,
|
||||||
|
createAppApi,
|
||||||
|
} from './system';
|
||||||
|
import { createSshRemoteApi } from './sshRemote';
|
||||||
|
import { createLoggerApi } from './logger';
|
||||||
|
import { createClaudeApi, createAgentSessionsApi } from './sessions';
|
||||||
|
import { createTempfileApi, createHistoryApi, createCliApi } from './files';
|
||||||
|
import { createSpeckitApi, createOpenspecApi } from './commands';
|
||||||
|
import { createAutorunApi, createPlaybooksApi, createMarketplaceApi } from './autorun';
|
||||||
|
import { createDebugApi, createDocumentGraphApi } from './debug';
|
||||||
|
import { createGroupChatApi } from './groupChat';
|
||||||
|
import { createStatsApi } from './stats';
|
||||||
|
import { createNotificationApi } from './notifications';
|
||||||
|
import { createLeaderboardApi } from './leaderboard';
|
||||||
|
import { createAttachmentsApi } from './attachments';
|
||||||
|
import { createProcessApi } from './process';
|
||||||
|
import { createGitApi } from './git';
|
||||||
|
import { createFsApi } from './fs';
|
||||||
|
import { createAgentsApi } from './agents';
|
||||||
|
|
||||||
|
// Expose protected methods that allow the renderer process to use
|
||||||
|
// the ipcRenderer without exposing the entire object
|
||||||
|
contextBridge.exposeInMainWorld('maestro', {
|
||||||
|
// Settings API
|
||||||
|
settings: createSettingsApi(),
|
||||||
|
|
||||||
|
// Sessions persistence API
|
||||||
|
sessions: createSessionsApi(),
|
||||||
|
|
||||||
|
// Groups persistence API
|
||||||
|
groups: createGroupsApi(),
|
||||||
|
|
||||||
|
// Process/Session API
|
||||||
|
process: createProcessApi(),
|
||||||
|
|
||||||
|
// Agent Error Handling API
|
||||||
|
agentError: createAgentErrorApi(),
|
||||||
|
|
||||||
|
// Context Merge API
|
||||||
|
context: createContextApi(),
|
||||||
|
|
||||||
|
// Web interface API
|
||||||
|
web: createWebApi(),
|
||||||
|
|
||||||
|
// Git API
|
||||||
|
git: createGitApi(),
|
||||||
|
|
||||||
|
// File System API
|
||||||
|
fs: createFsApi(),
|
||||||
|
|
||||||
|
// Web Server API
|
||||||
|
webserver: createWebserverApi(),
|
||||||
|
|
||||||
|
// Live Session API
|
||||||
|
live: createLiveApi(),
|
||||||
|
|
||||||
|
// Agent API
|
||||||
|
agents: createAgentsApi(),
|
||||||
|
|
||||||
|
// Dialog API
|
||||||
|
dialog: createDialogApi(),
|
||||||
|
|
||||||
|
// Font API
|
||||||
|
fonts: createFontsApi(),
|
||||||
|
|
||||||
|
// Shells API (terminal shells)
|
||||||
|
shells: createShellsApi(),
|
||||||
|
|
||||||
|
// Shell API
|
||||||
|
shell: createShellApi(),
|
||||||
|
|
||||||
|
// Tunnel API (Cloudflare)
|
||||||
|
tunnel: createTunnelApi(),
|
||||||
|
|
||||||
|
// SSH Remote API
|
||||||
|
sshRemote: createSshRemoteApi(),
|
||||||
|
|
||||||
|
// Sync API
|
||||||
|
sync: createSyncApi(),
|
||||||
|
|
||||||
|
// DevTools API
|
||||||
|
devtools: createDevtoolsApi(),
|
||||||
|
|
||||||
|
// Power Management API
|
||||||
|
power: createPowerApi(),
|
||||||
|
|
||||||
|
// Updates API
|
||||||
|
updates: createUpdatesApi(),
|
||||||
|
|
||||||
|
// Logger API
|
||||||
|
logger: createLoggerApi(),
|
||||||
|
|
||||||
|
// Claude Code sessions API (DEPRECATED)
|
||||||
|
claude: createClaudeApi(),
|
||||||
|
|
||||||
|
// Agent Sessions API (preferred)
|
||||||
|
agentSessions: createAgentSessionsApi(),
|
||||||
|
|
||||||
|
// Temp file API
|
||||||
|
tempfile: createTempfileApi(),
|
||||||
|
|
||||||
|
// History API
|
||||||
|
history: createHistoryApi(),
|
||||||
|
|
||||||
|
// CLI activity API
|
||||||
|
cli: createCliApi(),
|
||||||
|
|
||||||
|
// Spec Kit API
|
||||||
|
speckit: createSpeckitApi(),
|
||||||
|
|
||||||
|
// OpenSpec API
|
||||||
|
openspec: createOpenspecApi(),
|
||||||
|
|
||||||
|
// Notification API
|
||||||
|
notification: createNotificationApi(),
|
||||||
|
|
||||||
|
// Attachments API
|
||||||
|
attachments: createAttachmentsApi(),
|
||||||
|
|
||||||
|
// Auto Run API
|
||||||
|
autorun: createAutorunApi(),
|
||||||
|
|
||||||
|
// Playbooks API
|
||||||
|
playbooks: createPlaybooksApi(),
|
||||||
|
|
||||||
|
// Marketplace API
|
||||||
|
marketplace: createMarketplaceApi(),
|
||||||
|
|
||||||
|
// Debug Package API
|
||||||
|
debug: createDebugApi(),
|
||||||
|
|
||||||
|
// Document Graph API
|
||||||
|
documentGraph: createDocumentGraphApi(),
|
||||||
|
|
||||||
|
// Group Chat API
|
||||||
|
groupChat: createGroupChatApi(),
|
||||||
|
|
||||||
|
// App lifecycle API
|
||||||
|
app: createAppApi(),
|
||||||
|
|
||||||
|
// Stats API
|
||||||
|
stats: createStatsApi(),
|
||||||
|
|
||||||
|
// Leaderboard API
|
||||||
|
leaderboard: createLeaderboardApi(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-export factory functions for external consumers (e.g., tests)
|
||||||
|
export {
|
||||||
|
// Settings and persistence
|
||||||
|
createSettingsApi,
|
||||||
|
createSessionsApi,
|
||||||
|
createGroupsApi,
|
||||||
|
createAgentErrorApi,
|
||||||
|
// Context
|
||||||
|
createContextApi,
|
||||||
|
// Web interface
|
||||||
|
createWebApi,
|
||||||
|
createWebserverApi,
|
||||||
|
createLiveApi,
|
||||||
|
// System utilities
|
||||||
|
createDialogApi,
|
||||||
|
createFontsApi,
|
||||||
|
createShellsApi,
|
||||||
|
createShellApi,
|
||||||
|
createTunnelApi,
|
||||||
|
createSyncApi,
|
||||||
|
createDevtoolsApi,
|
||||||
|
createPowerApi,
|
||||||
|
createUpdatesApi,
|
||||||
|
createAppApi,
|
||||||
|
// SSH Remote
|
||||||
|
createSshRemoteApi,
|
||||||
|
// Logger
|
||||||
|
createLoggerApi,
|
||||||
|
// Sessions
|
||||||
|
createClaudeApi,
|
||||||
|
createAgentSessionsApi,
|
||||||
|
// Files
|
||||||
|
createTempfileApi,
|
||||||
|
createHistoryApi,
|
||||||
|
createCliApi,
|
||||||
|
// Commands
|
||||||
|
createSpeckitApi,
|
||||||
|
createOpenspecApi,
|
||||||
|
// Auto Run
|
||||||
|
createAutorunApi,
|
||||||
|
createPlaybooksApi,
|
||||||
|
createMarketplaceApi,
|
||||||
|
// Debug
|
||||||
|
createDebugApi,
|
||||||
|
createDocumentGraphApi,
|
||||||
|
// Group Chat
|
||||||
|
createGroupChatApi,
|
||||||
|
// Stats
|
||||||
|
createStatsApi,
|
||||||
|
// Notifications
|
||||||
|
createNotificationApi,
|
||||||
|
// Leaderboard
|
||||||
|
createLeaderboardApi,
|
||||||
|
// Attachments
|
||||||
|
createAttachmentsApi,
|
||||||
|
// Process
|
||||||
|
createProcessApi,
|
||||||
|
// Git
|
||||||
|
createGitApi,
|
||||||
|
// Filesystem
|
||||||
|
createFsApi,
|
||||||
|
// Agents
|
||||||
|
createAgentsApi,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export types for TypeScript consumers
|
||||||
|
export type {
|
||||||
|
// From settings
|
||||||
|
SettingsApi,
|
||||||
|
SessionsApi,
|
||||||
|
GroupsApi,
|
||||||
|
AgentErrorApi,
|
||||||
|
} from './settings';
|
||||||
|
export type {
|
||||||
|
// From context
|
||||||
|
ContextApi,
|
||||||
|
StoredMessage,
|
||||||
|
StoredSessionResponse,
|
||||||
|
} from './context';
|
||||||
|
export type {
|
||||||
|
// From web
|
||||||
|
WebApi,
|
||||||
|
WebserverApi,
|
||||||
|
LiveApi,
|
||||||
|
AutoRunState,
|
||||||
|
AiTabState,
|
||||||
|
} from './web';
|
||||||
|
export type {
|
||||||
|
// From system
|
||||||
|
DialogApi,
|
||||||
|
FontsApi,
|
||||||
|
ShellsApi,
|
||||||
|
ShellApi,
|
||||||
|
TunnelApi,
|
||||||
|
SyncApi,
|
||||||
|
DevtoolsApi,
|
||||||
|
PowerApi,
|
||||||
|
UpdatesApi,
|
||||||
|
AppApi,
|
||||||
|
ShellInfo,
|
||||||
|
UpdateStatus,
|
||||||
|
} from './system';
|
||||||
|
export type {
|
||||||
|
// From sshRemote
|
||||||
|
SshRemoteApi,
|
||||||
|
SshRemoteConfig,
|
||||||
|
SshConfigHost,
|
||||||
|
} from './sshRemote';
|
||||||
|
export type {
|
||||||
|
// From logger
|
||||||
|
LoggerApi,
|
||||||
|
} from './logger';
|
||||||
|
export type {
|
||||||
|
// From sessions
|
||||||
|
ClaudeApi,
|
||||||
|
AgentSessionsApi,
|
||||||
|
NamedSessionEntry,
|
||||||
|
NamedSessionEntryWithAgent,
|
||||||
|
GlobalStatsUpdate,
|
||||||
|
} from './sessions';
|
||||||
|
export type {
|
||||||
|
// From files
|
||||||
|
TempfileApi,
|
||||||
|
HistoryApi,
|
||||||
|
CliApi,
|
||||||
|
HistoryEntry,
|
||||||
|
} from './files';
|
||||||
|
export type {
|
||||||
|
// From commands
|
||||||
|
SpeckitApi,
|
||||||
|
OpenspecApi,
|
||||||
|
CommandMetadata,
|
||||||
|
CommandDefinition,
|
||||||
|
} from './commands';
|
||||||
|
export type {
|
||||||
|
// From autorun
|
||||||
|
AutorunApi,
|
||||||
|
PlaybooksApi,
|
||||||
|
MarketplaceApi,
|
||||||
|
Playbook,
|
||||||
|
PlaybookDocument,
|
||||||
|
WorktreeSettings,
|
||||||
|
} from './autorun';
|
||||||
|
export type {
|
||||||
|
// From debug
|
||||||
|
DebugApi,
|
||||||
|
DocumentGraphApi,
|
||||||
|
DebugPackageOptions,
|
||||||
|
DocumentGraphChange,
|
||||||
|
} from './debug';
|
||||||
|
export type {
|
||||||
|
// From groupChat
|
||||||
|
GroupChatApi,
|
||||||
|
ModeratorConfig,
|
||||||
|
Participant,
|
||||||
|
ChatMessage,
|
||||||
|
GroupChatHistoryEntry,
|
||||||
|
ModeratorUsage,
|
||||||
|
} from './groupChat';
|
||||||
|
export type {
|
||||||
|
// From stats
|
||||||
|
StatsApi,
|
||||||
|
QueryEvent,
|
||||||
|
AutoRunSession,
|
||||||
|
AutoRunTask,
|
||||||
|
SessionCreatedEvent,
|
||||||
|
StatsAggregation,
|
||||||
|
} from './stats';
|
||||||
|
export type {
|
||||||
|
// From notifications
|
||||||
|
NotificationApi,
|
||||||
|
NotificationShowResponse,
|
||||||
|
TtsResponse,
|
||||||
|
} from './notifications';
|
||||||
|
export type {
|
||||||
|
// From leaderboard
|
||||||
|
LeaderboardApi,
|
||||||
|
LeaderboardSubmitData,
|
||||||
|
LeaderboardSubmitResponse,
|
||||||
|
AuthStatusResponse,
|
||||||
|
ResendConfirmationResponse,
|
||||||
|
LeaderboardEntry,
|
||||||
|
LongestRunEntry,
|
||||||
|
LeaderboardGetResponse,
|
||||||
|
LongestRunsGetResponse,
|
||||||
|
LeaderboardSyncResponse,
|
||||||
|
} from './leaderboard';
|
||||||
|
export type {
|
||||||
|
// From attachments
|
||||||
|
AttachmentsApi,
|
||||||
|
AttachmentResponse,
|
||||||
|
AttachmentLoadResponse,
|
||||||
|
AttachmentListResponse,
|
||||||
|
AttachmentPathResponse,
|
||||||
|
} from './attachments';
|
||||||
|
export type {
|
||||||
|
// From process
|
||||||
|
ProcessApi,
|
||||||
|
ProcessConfig,
|
||||||
|
ProcessSpawnResponse,
|
||||||
|
RunCommandConfig,
|
||||||
|
ActiveProcess,
|
||||||
|
UsageStats,
|
||||||
|
AgentError,
|
||||||
|
ToolExecutionEvent,
|
||||||
|
SshRemoteInfo,
|
||||||
|
} from './process';
|
||||||
|
export type {
|
||||||
|
// From git
|
||||||
|
GitApi,
|
||||||
|
WorktreeInfo,
|
||||||
|
WorktreeEntry,
|
||||||
|
GitSubdirEntry,
|
||||||
|
GitLogEntry,
|
||||||
|
WorktreeDiscoveredData,
|
||||||
|
} from './git';
|
||||||
|
export type {
|
||||||
|
// From fs
|
||||||
|
FsApi,
|
||||||
|
DirectoryEntry,
|
||||||
|
FileStat,
|
||||||
|
DirectorySizeInfo,
|
||||||
|
ItemCountInfo,
|
||||||
|
} from './fs';
|
||||||
|
export type {
|
||||||
|
// From agents
|
||||||
|
AgentsApi,
|
||||||
|
AgentCapabilities,
|
||||||
|
AgentConfig,
|
||||||
|
AgentRefreshResult,
|
||||||
|
} from './agents';
|
||||||
221
src/main/preload/leaderboard.ts
Normal file
221
src/main/preload/leaderboard.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for leaderboard
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.leaderboard namespace for:
|
||||||
|
* - Getting installation ID
|
||||||
|
* - Submitting leaderboard entries
|
||||||
|
* - Polling auth status after email confirmation
|
||||||
|
* - Resending confirmation emails
|
||||||
|
* - Fetching leaderboard data
|
||||||
|
* - Syncing stats from server
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data submitted when creating/updating a leaderboard entry
|
||||||
|
*/
|
||||||
|
export interface LeaderboardSubmitData {
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
githubUsername?: string;
|
||||||
|
twitterHandle?: string;
|
||||||
|
linkedinHandle?: string;
|
||||||
|
discordUsername?: string;
|
||||||
|
blueskyHandle?: string;
|
||||||
|
badgeLevel: number;
|
||||||
|
badgeName: string;
|
||||||
|
cumulativeTimeMs: number;
|
||||||
|
totalRuns: number;
|
||||||
|
longestRunMs?: number;
|
||||||
|
longestRunDate?: string;
|
||||||
|
currentRunMs?: number;
|
||||||
|
theme?: string;
|
||||||
|
clientToken?: string;
|
||||||
|
authToken?: string;
|
||||||
|
deltaMs?: number;
|
||||||
|
deltaRuns?: number;
|
||||||
|
installationId?: string;
|
||||||
|
clientTotalTimeMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from leaderboard submission
|
||||||
|
*/
|
||||||
|
export interface LeaderboardSubmitResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
pendingEmailConfirmation?: boolean;
|
||||||
|
error?: string;
|
||||||
|
authTokenRequired?: boolean;
|
||||||
|
ranking?: {
|
||||||
|
cumulative: {
|
||||||
|
rank: number;
|
||||||
|
total: number;
|
||||||
|
previousRank: number | null;
|
||||||
|
improved: boolean;
|
||||||
|
};
|
||||||
|
longestRun: {
|
||||||
|
rank: number;
|
||||||
|
total: number;
|
||||||
|
previousRank: number | null;
|
||||||
|
improved: boolean;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
serverTotals?: {
|
||||||
|
cumulativeTimeMs: number;
|
||||||
|
totalRuns: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from polling auth status
|
||||||
|
*/
|
||||||
|
export interface AuthStatusResponse {
|
||||||
|
status: 'pending' | 'confirmed' | 'expired' | 'error';
|
||||||
|
authToken?: string;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from resending confirmation
|
||||||
|
*/
|
||||||
|
export interface ResendConfirmationResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaderboard entry for cumulative time rankings
|
||||||
|
*/
|
||||||
|
export interface LeaderboardEntry {
|
||||||
|
rank: number;
|
||||||
|
displayName: string;
|
||||||
|
githubUsername?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
badgeLevel: number;
|
||||||
|
badgeName: string;
|
||||||
|
cumulativeTimeMs: number;
|
||||||
|
totalRuns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaderboard entry for longest run rankings
|
||||||
|
*/
|
||||||
|
export interface LongestRunEntry {
|
||||||
|
rank: number;
|
||||||
|
displayName: string;
|
||||||
|
githubUsername?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
longestRunMs: number;
|
||||||
|
runDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from fetching leaderboard
|
||||||
|
*/
|
||||||
|
export interface LeaderboardGetResponse {
|
||||||
|
success: boolean;
|
||||||
|
entries?: LeaderboardEntry[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from fetching longest runs leaderboard
|
||||||
|
*/
|
||||||
|
export interface LongestRunsGetResponse {
|
||||||
|
success: boolean;
|
||||||
|
entries?: LongestRunEntry[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from syncing stats
|
||||||
|
*/
|
||||||
|
export interface LeaderboardSyncResponse {
|
||||||
|
success: boolean;
|
||||||
|
found: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
errorCode?: 'EMAIL_NOT_CONFIRMED' | 'INVALID_TOKEN' | 'MISSING_FIELDS';
|
||||||
|
data?: {
|
||||||
|
displayName: string;
|
||||||
|
badgeLevel: number;
|
||||||
|
badgeName: string;
|
||||||
|
cumulativeTimeMs: number;
|
||||||
|
totalRuns: number;
|
||||||
|
longestRunMs: number | null;
|
||||||
|
longestRunDate: string | null;
|
||||||
|
keyboardLevel: number | null;
|
||||||
|
coveragePercent: number | null;
|
||||||
|
ranking: {
|
||||||
|
cumulative: { rank: number; total: number };
|
||||||
|
longestRun: { rank: number; total: number } | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the leaderboard API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createLeaderboardApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get the unique installation ID for this Maestro installation
|
||||||
|
*/
|
||||||
|
getInstallationId: (): Promise<string | null> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:getInstallationId'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit leaderboard entry to runmaestro.ai
|
||||||
|
* @param data - Leaderboard submission data
|
||||||
|
*/
|
||||||
|
submit: (data: LeaderboardSubmitData): Promise<LeaderboardSubmitResponse> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:submit', data),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll for auth token after email confirmation
|
||||||
|
* @param clientToken - Client token from initial submission
|
||||||
|
*/
|
||||||
|
pollAuthStatus: (clientToken: string): Promise<AuthStatusResponse> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:pollAuthStatus', clientToken),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend confirmation email (self-service auth token recovery)
|
||||||
|
* @param data - Email and client token
|
||||||
|
*/
|
||||||
|
resendConfirmation: (data: {
|
||||||
|
email: string;
|
||||||
|
clientToken: string;
|
||||||
|
}): Promise<ResendConfirmationResponse> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:resendConfirmation', data),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get leaderboard entries (cumulative time rankings)
|
||||||
|
* @param options - Optional limit (default: 50)
|
||||||
|
*/
|
||||||
|
get: (options?: { limit?: number }): Promise<LeaderboardGetResponse> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:get', options),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get longest runs leaderboard
|
||||||
|
* @param options - Optional limit (default: 50)
|
||||||
|
*/
|
||||||
|
getLongestRuns: (options?: { limit?: number }): Promise<LongestRunsGetResponse> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:getLongestRuns', options),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync user stats from server (for new device installations)
|
||||||
|
* @param data - Email and auth token
|
||||||
|
*/
|
||||||
|
sync: (data: { email: string; authToken: string }): Promise<LeaderboardSyncResponse> =>
|
||||||
|
ipcRenderer.invoke('leaderboard:sync', data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the leaderboard API
|
||||||
|
*/
|
||||||
|
export type LeaderboardApi = ReturnType<typeof createLeaderboardApi>;
|
||||||
58
src/main/preload/logger.ts
Normal file
58
src/main/preload/logger.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for logging operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.logger namespace for:
|
||||||
|
* - Application logging
|
||||||
|
* - Log level management
|
||||||
|
* - Real-time log subscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
import type { MainLogLevel, SystemLogEntry } from '../../shared/logger-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the logger API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createLoggerApi() {
|
||||||
|
return {
|
||||||
|
log: (level: MainLogLevel, message: string, context?: string, data?: unknown) =>
|
||||||
|
ipcRenderer.invoke('logger:log', level, message, context, data),
|
||||||
|
|
||||||
|
getLogs: (filter?: { level?: MainLogLevel; context?: string; limit?: number }) =>
|
||||||
|
ipcRenderer.invoke('logger:getLogs', filter),
|
||||||
|
|
||||||
|
clearLogs: () => ipcRenderer.invoke('logger:clearLogs'),
|
||||||
|
|
||||||
|
setLogLevel: (level: MainLogLevel) => ipcRenderer.invoke('logger:setLogLevel', level),
|
||||||
|
|
||||||
|
getLogLevel: () => ipcRenderer.invoke('logger:getLogLevel'),
|
||||||
|
|
||||||
|
setMaxLogBuffer: (max: number) => ipcRenderer.invoke('logger:setMaxLogBuffer', max),
|
||||||
|
|
||||||
|
getMaxLogBuffer: () => ipcRenderer.invoke('logger:getMaxLogBuffer'),
|
||||||
|
|
||||||
|
// Convenience method for logging toast notifications
|
||||||
|
toast: (title: string, data?: unknown) =>
|
||||||
|
ipcRenderer.invoke('logger:log', 'toast', title, 'Toast', data),
|
||||||
|
|
||||||
|
// Convenience method for Auto Run workflow logging (cannot be turned off)
|
||||||
|
autorun: (message: string, context?: string, data?: unknown) =>
|
||||||
|
ipcRenderer.invoke('logger:log', 'autorun', message, context || 'AutoRun', data),
|
||||||
|
|
||||||
|
// Subscribe to new log entries in real-time
|
||||||
|
onNewLog: (callback: (log: SystemLogEntry) => void) => {
|
||||||
|
const handler = (_: Electron.IpcRendererEvent, log: SystemLogEntry) => callback(log);
|
||||||
|
ipcRenderer.on('logger:newLog', handler);
|
||||||
|
return () => ipcRenderer.removeListener('logger:newLog', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
// File logging (enabled by default on Windows for debugging)
|
||||||
|
getLogFilePath: (): Promise<string> => ipcRenderer.invoke('logger:getLogFilePath'),
|
||||||
|
|
||||||
|
isFileLoggingEnabled: (): Promise<boolean> => ipcRenderer.invoke('logger:isFileLoggingEnabled'),
|
||||||
|
|
||||||
|
enableFileLogging: (): Promise<void> => ipcRenderer.invoke('logger:enableFileLogging'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoggerApi = ReturnType<typeof createLoggerApi>;
|
||||||
73
src/main/preload/notifications.ts
Normal file
73
src/main/preload/notifications.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for notifications
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.notification namespace for:
|
||||||
|
* - Showing OS notifications
|
||||||
|
* - Text-to-speech (TTS) functionality
|
||||||
|
* - TTS completion events
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from showing a notification
|
||||||
|
*/
|
||||||
|
export interface NotificationShowResponse {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from TTS operations
|
||||||
|
*/
|
||||||
|
export interface TtsResponse {
|
||||||
|
success: boolean;
|
||||||
|
ttsId?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the notification API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createNotificationApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Show an OS notification
|
||||||
|
* @param title - Notification title
|
||||||
|
* @param body - Notification body text
|
||||||
|
*/
|
||||||
|
show: (title: string, body: string): Promise<NotificationShowResponse> =>
|
||||||
|
ipcRenderer.invoke('notification:show', title, body),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speak text using system TTS
|
||||||
|
* @param text - Text to speak
|
||||||
|
* @param command - Optional TTS command (default: 'say' on macOS)
|
||||||
|
*/
|
||||||
|
speak: (text: string, command?: string): Promise<TtsResponse> =>
|
||||||
|
ipcRenderer.invoke('notification:speak', text, command),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a running TTS process
|
||||||
|
* @param ttsId - ID of the TTS process to stop
|
||||||
|
*/
|
||||||
|
stopSpeak: (ttsId: number): Promise<TtsResponse> =>
|
||||||
|
ipcRenderer.invoke('notification:stopSpeak', ttsId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to TTS completion events
|
||||||
|
* @param handler - Callback when a TTS process completes
|
||||||
|
* @returns Cleanup function to unsubscribe
|
||||||
|
*/
|
||||||
|
onTtsCompleted: (handler: (ttsId: number) => void): (() => void) => {
|
||||||
|
const wrappedHandler = (_event: Electron.IpcRendererEvent, ttsId: number) => handler(ttsId);
|
||||||
|
ipcRenderer.on('tts:completed', wrappedHandler);
|
||||||
|
return () => ipcRenderer.removeListener('tts:completed', wrappedHandler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the notification API
|
||||||
|
*/
|
||||||
|
export type NotificationApi = ReturnType<typeof createNotificationApi>;
|
||||||
420
src/main/preload/process.ts
Normal file
420
src/main/preload/process.ts
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for process management
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.process namespace for:
|
||||||
|
* - Spawning and managing agent/terminal processes
|
||||||
|
* - Writing to processes
|
||||||
|
* - Handling process events (data, exit, errors)
|
||||||
|
* - Remote command execution from web interface
|
||||||
|
* - SSH remote execution support
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for spawning a process
|
||||||
|
*/
|
||||||
|
export interface ProcessConfig {
|
||||||
|
sessionId: string;
|
||||||
|
toolType: string;
|
||||||
|
cwd: string;
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
prompt?: string;
|
||||||
|
shell?: string;
|
||||||
|
images?: string[]; // Base64 data URLs for images
|
||||||
|
// Agent-specific spawn options (used to build args via agent config)
|
||||||
|
agentSessionId?: string; // For session resume (uses agent's resumeArgs builder)
|
||||||
|
readOnlyMode?: boolean; // For read-only/plan mode (uses agent's readOnlyArgs)
|
||||||
|
modelId?: string; // For model selection (uses agent's modelArgs builder)
|
||||||
|
yoloMode?: boolean; // For YOLO/full-access mode (uses agent's yoloModeArgs)
|
||||||
|
// Stats tracking options
|
||||||
|
querySource?: 'user' | 'auto'; // Whether this query is user-initiated or from Auto Run
|
||||||
|
tabId?: string; // Tab ID for multi-tab tracking
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from spawning a process
|
||||||
|
*/
|
||||||
|
export interface ProcessSpawnResponse {
|
||||||
|
pid: number;
|
||||||
|
success: boolean;
|
||||||
|
sshRemote?: { id: string; name: string; host: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for running a single command
|
||||||
|
*/
|
||||||
|
export interface RunCommandConfig {
|
||||||
|
sessionId: string;
|
||||||
|
command: string;
|
||||||
|
cwd: string;
|
||||||
|
shell?: string;
|
||||||
|
sessionSshRemoteConfig?: {
|
||||||
|
enabled: boolean;
|
||||||
|
remoteId: string | null;
|
||||||
|
workingDirOverride?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active process information
|
||||||
|
*/
|
||||||
|
export interface ActiveProcess {
|
||||||
|
sessionId: string;
|
||||||
|
toolType: string;
|
||||||
|
pid: number;
|
||||||
|
cwd: string;
|
||||||
|
isTerminal: boolean;
|
||||||
|
isBatchMode: boolean;
|
||||||
|
startTime: number;
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage statistics from AI responses
|
||||||
|
*/
|
||||||
|
export interface UsageStats {
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheReadInputTokens: number;
|
||||||
|
cacheCreationInputTokens: number;
|
||||||
|
totalCostUsd: number;
|
||||||
|
contextWindow: number;
|
||||||
|
reasoningTokens?: number; // Separate reasoning tokens (Codex o3/o4-mini)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent error information
|
||||||
|
*/
|
||||||
|
export interface AgentError {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
recoverable: boolean;
|
||||||
|
agentId: string;
|
||||||
|
sessionId?: string;
|
||||||
|
timestamp: number;
|
||||||
|
raw?: {
|
||||||
|
exitCode?: number;
|
||||||
|
stderr?: string;
|
||||||
|
stdout?: string;
|
||||||
|
errorLine?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool execution event
|
||||||
|
*/
|
||||||
|
export interface ToolExecutionEvent {
|
||||||
|
toolName: string;
|
||||||
|
state?: unknown;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSH remote info
|
||||||
|
*/
|
||||||
|
export interface SshRemoteInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the process API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createProcessApi() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Spawn a new process (agent or terminal)
|
||||||
|
*/
|
||||||
|
spawn: (config: ProcessConfig): Promise<ProcessSpawnResponse> =>
|
||||||
|
ipcRenderer.invoke('process:spawn', config),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to a process stdin
|
||||||
|
*/
|
||||||
|
write: (sessionId: string, data: string): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('process:write', sessionId, data),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send interrupt signal (Ctrl+C) to a process
|
||||||
|
*/
|
||||||
|
interrupt: (sessionId: string): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('process:interrupt', sessionId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill a process
|
||||||
|
*/
|
||||||
|
kill: (sessionId: string): Promise<boolean> => ipcRenderer.invoke('process:kill', sessionId),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize process terminal
|
||||||
|
*/
|
||||||
|
resize: (sessionId: string, cols: number, rows: number): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('process:resize', sessionId, cols, rows),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single command and capture only stdout/stderr (no PTY echo/prompts)
|
||||||
|
* Supports SSH remote execution when sessionSshRemoteConfig is provided
|
||||||
|
*/
|
||||||
|
runCommand: (config: RunCommandConfig): Promise<{ exitCode: number }> =>
|
||||||
|
ipcRenderer.invoke('process:runCommand', config),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active processes from ProcessManager
|
||||||
|
*/
|
||||||
|
getActiveProcesses: (): Promise<ActiveProcess[]> =>
|
||||||
|
ipcRenderer.invoke('process:getActiveProcesses'),
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to process data output
|
||||||
|
*/
|
||||||
|
onData: (callback: (sessionId: string, data: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, data: string) => callback(sessionId, data);
|
||||||
|
ipcRenderer.on('process:data', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:data', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to process exit events
|
||||||
|
*/
|
||||||
|
onExit: (callback: (sessionId: string, code: number) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, code: number) => callback(sessionId, code);
|
||||||
|
ipcRenderer.on('process:exit', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:exit', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to agent session ID events
|
||||||
|
*/
|
||||||
|
onSessionId: (callback: (sessionId: string, agentSessionId: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, agentSessionId: string) =>
|
||||||
|
callback(sessionId, agentSessionId);
|
||||||
|
ipcRenderer.on('process:session-id', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:session-id', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to slash commands discovered from agent
|
||||||
|
*/
|
||||||
|
onSlashCommands: (
|
||||||
|
callback: (sessionId: string, slashCommands: string[]) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, slashCommands: string[]) =>
|
||||||
|
callback(sessionId, slashCommands);
|
||||||
|
ipcRenderer.on('process:slash-commands', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:slash-commands', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to thinking/streaming content chunks from AI agents
|
||||||
|
* Emitted when agents produce partial text events (isPartial: true)
|
||||||
|
* Renderer decides whether to display based on tab's showThinking setting
|
||||||
|
*/
|
||||||
|
onThinkingChunk: (callback: (sessionId: string, content: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, content: string) =>
|
||||||
|
callback(sessionId, content);
|
||||||
|
ipcRenderer.on('process:thinking-chunk', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:thinking-chunk', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to tool execution events
|
||||||
|
*/
|
||||||
|
onToolExecution: (
|
||||||
|
callback: (sessionId: string, toolEvent: ToolExecutionEvent) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, toolEvent: ToolExecutionEvent) =>
|
||||||
|
callback(sessionId, toolEvent);
|
||||||
|
ipcRenderer.on('process:tool-execution', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:tool-execution', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to SSH remote execution status
|
||||||
|
* Emitted when a process starts executing via SSH on a remote host
|
||||||
|
*/
|
||||||
|
onSshRemote: (
|
||||||
|
callback: (sessionId: string, sshRemote: SshRemoteInfo | null) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, sshRemote: SshRemoteInfo | null) =>
|
||||||
|
callback(sessionId, sshRemote);
|
||||||
|
ipcRenderer.on('process:ssh-remote', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:ssh-remote', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote command execution from web interface
|
||||||
|
* This allows web commands to go through the same code path as desktop commands
|
||||||
|
* inputMode is optional - if provided, renderer should use it instead of session state
|
||||||
|
*/
|
||||||
|
onRemoteCommand: (
|
||||||
|
callback: (sessionId: string, command: string, inputMode?: 'ai' | 'terminal') => void
|
||||||
|
): (() => void) => {
|
||||||
|
console.log(
|
||||||
|
'[Preload] Registering onRemoteCommand listener, callback type:',
|
||||||
|
typeof callback
|
||||||
|
);
|
||||||
|
const handler = (
|
||||||
|
_: unknown,
|
||||||
|
sessionId: string,
|
||||||
|
command: string,
|
||||||
|
inputMode?: 'ai' | 'terminal'
|
||||||
|
) => {
|
||||||
|
console.log('[Preload] Received remote:executeCommand IPC:', {
|
||||||
|
sessionId,
|
||||||
|
command: command?.substring(0, 50),
|
||||||
|
inputMode,
|
||||||
|
});
|
||||||
|
console.log('[Preload] About to invoke callback, callback type:', typeof callback);
|
||||||
|
try {
|
||||||
|
callback(sessionId, command, inputMode);
|
||||||
|
console.log('[Preload] Callback invoked successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Preload] Error invoking remote command callback:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ipcRenderer.on('remote:executeCommand', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:executeCommand', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote mode switch from web interface
|
||||||
|
* Forwards to desktop's toggleInputMode logic
|
||||||
|
*/
|
||||||
|
onRemoteSwitchMode: (
|
||||||
|
callback: (sessionId: string, mode: 'ai' | 'terminal') => void
|
||||||
|
): (() => void) => {
|
||||||
|
console.log('[Preload] Registering onRemoteSwitchMode listener');
|
||||||
|
const handler = (_: unknown, sessionId: string, mode: 'ai' | 'terminal') => {
|
||||||
|
console.log('[Preload] Received remote:switchMode IPC:', { sessionId, mode });
|
||||||
|
callback(sessionId, mode);
|
||||||
|
};
|
||||||
|
ipcRenderer.on('remote:switchMode', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:switchMode', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote interrupt from web interface
|
||||||
|
* Forwards to desktop's handleInterrupt logic
|
||||||
|
*/
|
||||||
|
onRemoteInterrupt: (callback: (sessionId: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string) => callback(sessionId);
|
||||||
|
ipcRenderer.on('remote:interrupt', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:interrupt', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote session selection from web interface
|
||||||
|
* Forwards to desktop's setActiveSessionId logic
|
||||||
|
* Optional tabId to also switch to a specific tab within the session
|
||||||
|
*/
|
||||||
|
onRemoteSelectSession: (
|
||||||
|
callback: (sessionId: string, tabId?: string) => void
|
||||||
|
): (() => void) => {
|
||||||
|
console.log('[Preload] Registering onRemoteSelectSession listener');
|
||||||
|
const handler = (_: unknown, sessionId: string, tabId?: string) => {
|
||||||
|
console.log('[Preload] Received remote:selectSession IPC:', { sessionId, tabId });
|
||||||
|
callback(sessionId, tabId);
|
||||||
|
};
|
||||||
|
ipcRenderer.on('remote:selectSession', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:selectSession', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote tab selection from web interface
|
||||||
|
*/
|
||||||
|
onRemoteSelectTab: (callback: (sessionId: string, tabId: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, tabId: string) => callback(sessionId, tabId);
|
||||||
|
ipcRenderer.on('remote:selectTab', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:selectTab', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote new tab from web interface
|
||||||
|
*/
|
||||||
|
onRemoteNewTab: (
|
||||||
|
callback: (sessionId: string, responseChannel: string) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, responseChannel: string) =>
|
||||||
|
callback(sessionId, responseChannel);
|
||||||
|
ipcRenderer.on('remote:newTab', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:newTab', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send response for remote new tab
|
||||||
|
*/
|
||||||
|
sendRemoteNewTabResponse: (responseChannel: string, result: { tabId: string } | null): void => {
|
||||||
|
ipcRenderer.send(responseChannel, result);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote close tab from web interface
|
||||||
|
*/
|
||||||
|
onRemoteCloseTab: (callback: (sessionId: string, tabId: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, tabId: string) => callback(sessionId, tabId);
|
||||||
|
ipcRenderer.on('remote:closeTab', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:closeTab', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to remote rename tab from web interface
|
||||||
|
*/
|
||||||
|
onRemoteRenameTab: (
|
||||||
|
callback: (sessionId: string, tabId: string, newName: string) => void
|
||||||
|
): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, tabId: string, newName: string) =>
|
||||||
|
callback(sessionId, tabId, newName);
|
||||||
|
ipcRenderer.on('remote:renameTab', handler);
|
||||||
|
return () => ipcRenderer.removeListener('remote:renameTab', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to stderr from runCommand (separate stream)
|
||||||
|
*/
|
||||||
|
onStderr: (callback: (sessionId: string, data: string) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, data: string) => callback(sessionId, data);
|
||||||
|
ipcRenderer.on('process:stderr', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:stderr', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to command exit from runCommand (separate from PTY exit)
|
||||||
|
*/
|
||||||
|
onCommandExit: (callback: (sessionId: string, code: number) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, code: number) => callback(sessionId, code);
|
||||||
|
ipcRenderer.on('process:command-exit', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:command-exit', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to usage statistics from AI responses
|
||||||
|
*/
|
||||||
|
onUsage: (callback: (sessionId: string, usageStats: UsageStats) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, usageStats: UsageStats) =>
|
||||||
|
callback(sessionId, usageStats);
|
||||||
|
ipcRenderer.on('process:usage', handler);
|
||||||
|
return () => ipcRenderer.removeListener('process:usage', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to agent error events (auth expired, token exhaustion, rate limits, etc.)
|
||||||
|
*/
|
||||||
|
onAgentError: (callback: (sessionId: string, error: AgentError) => void): (() => void) => {
|
||||||
|
const handler = (_: unknown, sessionId: string, error: AgentError) =>
|
||||||
|
callback(sessionId, error);
|
||||||
|
ipcRenderer.on('agent:error', handler);
|
||||||
|
return () => ipcRenderer.removeListener('agent:error', handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript type for the process API
|
||||||
|
*/
|
||||||
|
export type ProcessApi = ReturnType<typeof createProcessApi>;
|
||||||
352
src/main/preload/sessions.ts
Normal file
352
src/main/preload/sessions.ts
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for agent sessions
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.claude and window.maestro.agentSessions namespaces for:
|
||||||
|
* - Claude Code session storage (deprecated)
|
||||||
|
* - Generic multi-agent session storage
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// Helper to log deprecation warnings
|
||||||
|
const logDeprecationWarning = (method: string, replacement?: string) => {
|
||||||
|
const message = replacement
|
||||||
|
? `[Deprecation Warning] window.maestro.claude.${method}() is deprecated. Use window.maestro.agentSessions.${replacement}() instead.`
|
||||||
|
: `[Deprecation Warning] window.maestro.claude.${method}() is deprecated. Use the agentSessions API instead.`;
|
||||||
|
console.warn(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Named session entry
|
||||||
|
*/
|
||||||
|
export interface NamedSessionEntry {
|
||||||
|
agentSessionId: string;
|
||||||
|
projectPath: string;
|
||||||
|
sessionName: string;
|
||||||
|
starred?: boolean;
|
||||||
|
lastActivityAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Named session entry with agent ID
|
||||||
|
*/
|
||||||
|
export interface NamedSessionEntryWithAgent extends NamedSessionEntry {
|
||||||
|
agentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global stats update
|
||||||
|
*/
|
||||||
|
export interface GlobalStatsUpdate {
|
||||||
|
totalSessions: number;
|
||||||
|
totalMessages: number;
|
||||||
|
totalInputTokens: number;
|
||||||
|
totalOutputTokens: number;
|
||||||
|
totalCacheReadTokens: number;
|
||||||
|
totalCacheCreationTokens: number;
|
||||||
|
totalCostUsd: number;
|
||||||
|
hasCostData: boolean;
|
||||||
|
totalSizeBytes: number;
|
||||||
|
isComplete: boolean;
|
||||||
|
byProvider: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
sessions: number;
|
||||||
|
messages: number;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
costUsd: number;
|
||||||
|
hasCostData: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Claude Code sessions API object (DEPRECATED)
|
||||||
|
*/
|
||||||
|
export function createClaudeApi() {
|
||||||
|
return {
|
||||||
|
listSessions: (projectPath: string) => {
|
||||||
|
logDeprecationWarning('listSessions', 'list');
|
||||||
|
return ipcRenderer.invoke('claude:listSessions', projectPath);
|
||||||
|
},
|
||||||
|
listSessionsPaginated: (projectPath: string, options?: { cursor?: string; limit?: number }) => {
|
||||||
|
logDeprecationWarning('listSessionsPaginated', 'listPaginated');
|
||||||
|
return ipcRenderer.invoke('claude:listSessionsPaginated', projectPath, options);
|
||||||
|
},
|
||||||
|
getProjectStats: (projectPath: string) => {
|
||||||
|
logDeprecationWarning('getProjectStats');
|
||||||
|
return ipcRenderer.invoke('claude:getProjectStats', projectPath);
|
||||||
|
},
|
||||||
|
getSessionTimestamps: (projectPath: string): Promise<{ timestamps: string[] }> => {
|
||||||
|
logDeprecationWarning('getSessionTimestamps');
|
||||||
|
return ipcRenderer.invoke('claude:getSessionTimestamps', projectPath);
|
||||||
|
},
|
||||||
|
onProjectStatsUpdate: (
|
||||||
|
callback: (stats: {
|
||||||
|
projectPath: string;
|
||||||
|
totalSessions: number;
|
||||||
|
totalMessages: number;
|
||||||
|
totalCostUsd: number;
|
||||||
|
totalSizeBytes: number;
|
||||||
|
oldestTimestamp: string | null;
|
||||||
|
processedCount: number;
|
||||||
|
isComplete: boolean;
|
||||||
|
}) => void
|
||||||
|
) => {
|
||||||
|
logDeprecationWarning('onProjectStatsUpdate');
|
||||||
|
const handler = (_: any, stats: any) => callback(stats);
|
||||||
|
ipcRenderer.on('claude:projectStatsUpdate', handler);
|
||||||
|
return () => ipcRenderer.removeListener('claude:projectStatsUpdate', handler);
|
||||||
|
},
|
||||||
|
getGlobalStats: () => {
|
||||||
|
logDeprecationWarning('getGlobalStats');
|
||||||
|
return ipcRenderer.invoke('claude:getGlobalStats');
|
||||||
|
},
|
||||||
|
onGlobalStatsUpdate: (
|
||||||
|
callback: (stats: {
|
||||||
|
totalSessions: number;
|
||||||
|
totalMessages: number;
|
||||||
|
totalInputTokens: number;
|
||||||
|
totalOutputTokens: number;
|
||||||
|
totalCacheReadTokens: number;
|
||||||
|
totalCacheCreationTokens: number;
|
||||||
|
totalCostUsd: number;
|
||||||
|
totalSizeBytes: number;
|
||||||
|
isComplete: boolean;
|
||||||
|
}) => void
|
||||||
|
) => {
|
||||||
|
logDeprecationWarning('onGlobalStatsUpdate');
|
||||||
|
const handler = (_: any, stats: any) => callback(stats);
|
||||||
|
ipcRenderer.on('claude:globalStatsUpdate', handler);
|
||||||
|
return () => ipcRenderer.removeListener('claude:globalStatsUpdate', handler);
|
||||||
|
},
|
||||||
|
readSessionMessages: (
|
||||||
|
projectPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
options?: { offset?: number; limit?: number }
|
||||||
|
) => {
|
||||||
|
logDeprecationWarning('readSessionMessages', 'read');
|
||||||
|
return ipcRenderer.invoke('claude:readSessionMessages', projectPath, sessionId, options);
|
||||||
|
},
|
||||||
|
searchSessions: (
|
||||||
|
projectPath: string,
|
||||||
|
query: string,
|
||||||
|
searchMode: 'title' | 'user' | 'assistant' | 'all'
|
||||||
|
) => {
|
||||||
|
logDeprecationWarning('searchSessions', 'search');
|
||||||
|
return ipcRenderer.invoke('claude:searchSessions', projectPath, query, searchMode);
|
||||||
|
},
|
||||||
|
getCommands: (projectPath: string) => {
|
||||||
|
logDeprecationWarning('getCommands');
|
||||||
|
return ipcRenderer.invoke('claude:getCommands', projectPath);
|
||||||
|
},
|
||||||
|
registerSessionOrigin: (
|
||||||
|
projectPath: string,
|
||||||
|
agentSessionId: string,
|
||||||
|
origin: 'user' | 'auto',
|
||||||
|
sessionName?: string
|
||||||
|
) => {
|
||||||
|
logDeprecationWarning('registerSessionOrigin');
|
||||||
|
return ipcRenderer.invoke(
|
||||||
|
'claude:registerSessionOrigin',
|
||||||
|
projectPath,
|
||||||
|
agentSessionId,
|
||||||
|
origin,
|
||||||
|
sessionName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSessionName: (projectPath: string, agentSessionId: string, sessionName: string) => {
|
||||||
|
logDeprecationWarning('updateSessionName');
|
||||||
|
return ipcRenderer.invoke(
|
||||||
|
'claude:updateSessionName',
|
||||||
|
projectPath,
|
||||||
|
agentSessionId,
|
||||||
|
sessionName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSessionStarred: (projectPath: string, agentSessionId: string, starred: boolean) => {
|
||||||
|
logDeprecationWarning('updateSessionStarred');
|
||||||
|
return ipcRenderer.invoke(
|
||||||
|
'claude:updateSessionStarred',
|
||||||
|
projectPath,
|
||||||
|
agentSessionId,
|
||||||
|
starred
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSessionContextUsage: (
|
||||||
|
projectPath: string,
|
||||||
|
agentSessionId: string,
|
||||||
|
contextUsage: number
|
||||||
|
) => {
|
||||||
|
return ipcRenderer.invoke(
|
||||||
|
'claude:updateSessionContextUsage',
|
||||||
|
projectPath,
|
||||||
|
agentSessionId,
|
||||||
|
contextUsage
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getSessionOrigins: (projectPath: string) => {
|
||||||
|
logDeprecationWarning('getSessionOrigins');
|
||||||
|
return ipcRenderer.invoke('claude:getSessionOrigins', projectPath);
|
||||||
|
},
|
||||||
|
getAllNamedSessions: (): Promise<NamedSessionEntry[]> => {
|
||||||
|
logDeprecationWarning('getAllNamedSessions');
|
||||||
|
return ipcRenderer.invoke('claude:getAllNamedSessions');
|
||||||
|
},
|
||||||
|
deleteMessagePair: (
|
||||||
|
projectPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
userMessageUuid: string,
|
||||||
|
fallbackContent?: string
|
||||||
|
) => {
|
||||||
|
logDeprecationWarning('deleteMessagePair', 'deleteMessagePair');
|
||||||
|
return ipcRenderer.invoke(
|
||||||
|
'claude:deleteMessagePair',
|
||||||
|
projectPath,
|
||||||
|
sessionId,
|
||||||
|
userMessageUuid,
|
||||||
|
fallbackContent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the agent sessions API object (preferred API)
|
||||||
|
*/
|
||||||
|
export function createAgentSessionsApi() {
|
||||||
|
return {
|
||||||
|
list: (agentId: string, projectPath: string, sshRemoteId?: string) =>
|
||||||
|
ipcRenderer.invoke('agentSessions:list', agentId, projectPath, sshRemoteId),
|
||||||
|
|
||||||
|
listPaginated: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string,
|
||||||
|
options?: { cursor?: string; limit?: number },
|
||||||
|
sshRemoteId?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke('agentSessions:listPaginated', agentId, projectPath, options, sshRemoteId),
|
||||||
|
|
||||||
|
read: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
options?: { offset?: number; limit?: number },
|
||||||
|
sshRemoteId?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'agentSessions:read',
|
||||||
|
agentId,
|
||||||
|
projectPath,
|
||||||
|
sessionId,
|
||||||
|
options,
|
||||||
|
sshRemoteId
|
||||||
|
),
|
||||||
|
|
||||||
|
search: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string,
|
||||||
|
query: string,
|
||||||
|
searchMode: 'title' | 'user' | 'assistant' | 'all',
|
||||||
|
sshRemoteId?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'agentSessions:search',
|
||||||
|
agentId,
|
||||||
|
projectPath,
|
||||||
|
query,
|
||||||
|
searchMode,
|
||||||
|
sshRemoteId
|
||||||
|
),
|
||||||
|
|
||||||
|
getPath: (agentId: string, projectPath: string, sessionId: string, sshRemoteId?: string) =>
|
||||||
|
ipcRenderer.invoke('agentSessions:getPath', agentId, projectPath, sessionId, sshRemoteId),
|
||||||
|
|
||||||
|
deleteMessagePair: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
userMessageUuid: string,
|
||||||
|
fallbackContent?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'agentSessions:deleteMessagePair',
|
||||||
|
agentId,
|
||||||
|
projectPath,
|
||||||
|
sessionId,
|
||||||
|
userMessageUuid,
|
||||||
|
fallbackContent
|
||||||
|
),
|
||||||
|
|
||||||
|
hasStorage: (agentId: string) => ipcRenderer.invoke('agentSessions:hasStorage', agentId),
|
||||||
|
|
||||||
|
getAvailableStorages: () => ipcRenderer.invoke('agentSessions:getAvailableStorages'),
|
||||||
|
|
||||||
|
getGlobalStats: () => ipcRenderer.invoke('agentSessions:getGlobalStats'),
|
||||||
|
|
||||||
|
getAllNamedSessions: (): Promise<NamedSessionEntryWithAgent[]> =>
|
||||||
|
ipcRenderer.invoke('agentSessions:getAllNamedSessions'),
|
||||||
|
|
||||||
|
onGlobalStatsUpdate: (callback: (stats: GlobalStatsUpdate) => void) => {
|
||||||
|
const handler = (_: unknown, stats: GlobalStatsUpdate) => callback(stats);
|
||||||
|
ipcRenderer.on('agentSessions:globalStatsUpdate', handler);
|
||||||
|
return () => ipcRenderer.removeListener('agentSessions:globalStatsUpdate', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
registerSessionOrigin: (
|
||||||
|
projectPath: string,
|
||||||
|
agentSessionId: string,
|
||||||
|
origin: 'user' | 'auto',
|
||||||
|
sessionName?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'claude:registerSessionOrigin',
|
||||||
|
projectPath,
|
||||||
|
agentSessionId,
|
||||||
|
origin,
|
||||||
|
sessionName
|
||||||
|
),
|
||||||
|
|
||||||
|
updateSessionName: (projectPath: string, agentSessionId: string, sessionName: string) =>
|
||||||
|
ipcRenderer.invoke('claude:updateSessionName', projectPath, agentSessionId, sessionName),
|
||||||
|
|
||||||
|
getOrigins: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string
|
||||||
|
): Promise<
|
||||||
|
Record<string, { origin?: 'user' | 'auto'; sessionName?: string; starred?: boolean }>
|
||||||
|
> => ipcRenderer.invoke('agentSessions:getOrigins', agentId, projectPath),
|
||||||
|
|
||||||
|
setSessionName: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
sessionName: string | null
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'agentSessions:setSessionName',
|
||||||
|
agentId,
|
||||||
|
projectPath,
|
||||||
|
sessionId,
|
||||||
|
sessionName
|
||||||
|
),
|
||||||
|
|
||||||
|
setSessionStarred: (
|
||||||
|
agentId: string,
|
||||||
|
projectPath: string,
|
||||||
|
sessionId: string,
|
||||||
|
starred: boolean
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
'agentSessions:setSessionStarred',
|
||||||
|
agentId,
|
||||||
|
projectPath,
|
||||||
|
sessionId,
|
||||||
|
starred
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClaudeApi = ReturnType<typeof createClaudeApi>;
|
||||||
|
export type AgentSessionsApi = ReturnType<typeof createAgentSessionsApi>;
|
||||||
62
src/main/preload/settings.ts
Normal file
62
src/main/preload/settings.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for settings and persistence
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.settings, sessions, and groups namespaces for:
|
||||||
|
* - Application settings persistence
|
||||||
|
* - Session list persistence
|
||||||
|
* - Group list persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the settings API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createSettingsApi() {
|
||||||
|
return {
|
||||||
|
get: (key: string) => ipcRenderer.invoke('settings:get', key),
|
||||||
|
set: (key: string, value: unknown) => ipcRenderer.invoke('settings:set', key, value),
|
||||||
|
getAll: () => ipcRenderer.invoke('settings:getAll'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the sessions persistence API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createSessionsApi() {
|
||||||
|
return {
|
||||||
|
getAll: () => ipcRenderer.invoke('sessions:getAll'),
|
||||||
|
setAll: (sessions: any[]) => ipcRenderer.invoke('sessions:setAll', sessions),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the groups persistence API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createGroupsApi() {
|
||||||
|
return {
|
||||||
|
getAll: () => ipcRenderer.invoke('groups:getAll'),
|
||||||
|
setAll: (groups: any[]) => ipcRenderer.invoke('groups:setAll', groups),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the agent error handling API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createAgentErrorApi() {
|
||||||
|
return {
|
||||||
|
clearError: (sessionId: string) => ipcRenderer.invoke('agent:clearError', sessionId),
|
||||||
|
retryAfterError: (
|
||||||
|
sessionId: string,
|
||||||
|
options?: {
|
||||||
|
prompt?: string;
|
||||||
|
newSession?: boolean;
|
||||||
|
}
|
||||||
|
) => ipcRenderer.invoke('agent:retryAfterError', sessionId, options),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SettingsApi = ReturnType<typeof createSettingsApi>;
|
||||||
|
export type SessionsApi = ReturnType<typeof createSessionsApi>;
|
||||||
|
export type GroupsApi = ReturnType<typeof createGroupsApi>;
|
||||||
|
export type AgentErrorApi = ReturnType<typeof createAgentErrorApi>;
|
||||||
74
src/main/preload/sshRemote.ts
Normal file
74
src/main/preload/sshRemote.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for SSH remote operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.sshRemote namespace for:
|
||||||
|
* - SSH remote configuration management
|
||||||
|
* - SSH connection testing
|
||||||
|
* - SSH config file parsing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSH remote configuration
|
||||||
|
*/
|
||||||
|
export interface SshRemoteConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
privateKeyPath: string;
|
||||||
|
remoteEnv?: Record<string, string>;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSH config host entry
|
||||||
|
*/
|
||||||
|
export interface SshConfigHost {
|
||||||
|
host: string;
|
||||||
|
hostName?: string;
|
||||||
|
port?: number;
|
||||||
|
user?: string;
|
||||||
|
identityFile?: string;
|
||||||
|
proxyJump?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the SSH remote API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createSshRemoteApi() {
|
||||||
|
return {
|
||||||
|
saveConfig: (config: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
privateKeyPath?: string;
|
||||||
|
remoteEnv?: Record<string, string>;
|
||||||
|
enabled?: boolean;
|
||||||
|
}) => ipcRenderer.invoke('ssh-remote:saveConfig', config),
|
||||||
|
|
||||||
|
deleteConfig: (id: string) => ipcRenderer.invoke('ssh-remote:deleteConfig', id),
|
||||||
|
|
||||||
|
getConfigs: () => ipcRenderer.invoke('ssh-remote:getConfigs'),
|
||||||
|
|
||||||
|
getDefaultId: () => ipcRenderer.invoke('ssh-remote:getDefaultId'),
|
||||||
|
|
||||||
|
setDefaultId: (id: string | null) => ipcRenderer.invoke('ssh-remote:setDefaultId', id),
|
||||||
|
|
||||||
|
test: (configOrId: string | SshRemoteConfig, agentCommand?: string) =>
|
||||||
|
ipcRenderer.invoke('ssh-remote:test', configOrId, agentCommand),
|
||||||
|
|
||||||
|
getSshConfigHosts: (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
hosts: SshConfigHost[];
|
||||||
|
error?: string;
|
||||||
|
configPath: string;
|
||||||
|
}> => ipcRenderer.invoke('ssh-remote:getSshConfigHosts'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SshRemoteApi = ReturnType<typeof createSshRemoteApi>;
|
||||||
207
src/main/preload/stats.ts
Normal file
207
src/main/preload/stats.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for stats operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.stats namespace for:
|
||||||
|
* - Usage tracking and analytics
|
||||||
|
* - Query event recording
|
||||||
|
* - Auto Run session tracking
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query event for recording
|
||||||
|
*/
|
||||||
|
export interface QueryEvent {
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
source: 'user' | 'auto';
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
projectPath?: string;
|
||||||
|
tabId?: string;
|
||||||
|
isRemote?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto Run session for recording
|
||||||
|
*/
|
||||||
|
export interface AutoRunSession {
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
documentPath?: string;
|
||||||
|
startTime: number;
|
||||||
|
tasksTotal?: number;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto Run task for recording
|
||||||
|
*/
|
||||||
|
export interface AutoRunTask {
|
||||||
|
autoRunSessionId: string;
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
taskIndex: number;
|
||||||
|
taskContent?: string;
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session lifecycle event
|
||||||
|
*/
|
||||||
|
export interface SessionCreatedEvent {
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
projectPath?: string;
|
||||||
|
createdAt: number;
|
||||||
|
isRemote?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregation result
|
||||||
|
*/
|
||||||
|
export interface StatsAggregation {
|
||||||
|
totalQueries: number;
|
||||||
|
totalDuration: number;
|
||||||
|
avgDuration: number;
|
||||||
|
byAgent: Record<string, { count: number; duration: number }>;
|
||||||
|
bySource: { user: number; auto: number };
|
||||||
|
byDay: Array<{ date: string; count: number; duration: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Stats API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createStatsApi() {
|
||||||
|
return {
|
||||||
|
// Record a query event (interactive conversation turn)
|
||||||
|
recordQuery: (event: QueryEvent): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('stats:record-query', event),
|
||||||
|
|
||||||
|
// Start an Auto Run session (returns session ID)
|
||||||
|
startAutoRun: (session: AutoRunSession): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('stats:start-autorun', session),
|
||||||
|
|
||||||
|
// End an Auto Run session (update duration and completed count)
|
||||||
|
endAutoRun: (id: string, duration: number, tasksCompleted: number): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('stats:end-autorun', id, duration, tasksCompleted),
|
||||||
|
|
||||||
|
// Record an Auto Run task completion
|
||||||
|
recordAutoTask: (task: AutoRunTask): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('stats:record-task', task),
|
||||||
|
|
||||||
|
// Get query events with time range and optional filters
|
||||||
|
getStats: (
|
||||||
|
range: 'day' | 'week' | 'month' | 'year' | 'all',
|
||||||
|
filters?: {
|
||||||
|
agentType?: string;
|
||||||
|
source?: 'user' | 'auto';
|
||||||
|
projectPath?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
source: 'user' | 'auto';
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
projectPath?: string;
|
||||||
|
tabId?: string;
|
||||||
|
}>
|
||||||
|
> => ipcRenderer.invoke('stats:get-stats', range, filters),
|
||||||
|
|
||||||
|
// Get Auto Run sessions within a time range
|
||||||
|
getAutoRunSessions: (
|
||||||
|
range: 'day' | 'week' | 'month' | 'year' | 'all'
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
documentPath?: string;
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
tasksTotal?: number;
|
||||||
|
tasksCompleted?: number;
|
||||||
|
projectPath?: string;
|
||||||
|
}>
|
||||||
|
> => ipcRenderer.invoke('stats:get-autorun-sessions', range),
|
||||||
|
|
||||||
|
// Get tasks for a specific Auto Run session
|
||||||
|
getAutoRunTasks: (
|
||||||
|
autoRunSessionId: string
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
autoRunSessionId: string;
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
taskIndex: number;
|
||||||
|
taskContent?: string;
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
success: boolean;
|
||||||
|
}>
|
||||||
|
> => ipcRenderer.invoke('stats:get-autorun-tasks', autoRunSessionId),
|
||||||
|
|
||||||
|
// Get aggregated stats for dashboard display
|
||||||
|
getAggregation: (range: 'day' | 'week' | 'month' | 'year' | 'all'): Promise<StatsAggregation> =>
|
||||||
|
ipcRenderer.invoke('stats:get-aggregation', range),
|
||||||
|
|
||||||
|
// Export query events to CSV
|
||||||
|
exportCsv: (range: 'day' | 'week' | 'month' | 'year' | 'all'): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('stats:export-csv', range),
|
||||||
|
|
||||||
|
// Subscribe to stats updates (for real-time dashboard refresh)
|
||||||
|
onStatsUpdate: (callback: () => void) => {
|
||||||
|
const handler = () => callback();
|
||||||
|
ipcRenderer.on('stats:updated', handler);
|
||||||
|
return () => ipcRenderer.removeListener('stats:updated', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear old stats data (older than specified number of days)
|
||||||
|
clearOldData: (
|
||||||
|
olderThanDays: number
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
deletedQueryEvents: number;
|
||||||
|
deletedAutoRunSessions: number;
|
||||||
|
deletedAutoRunTasks: number;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('stats:clear-old-data', olderThanDays),
|
||||||
|
|
||||||
|
// Get database size in bytes
|
||||||
|
getDatabaseSize: (): Promise<number> => ipcRenderer.invoke('stats:get-database-size'),
|
||||||
|
|
||||||
|
// Record session creation (for lifecycle tracking)
|
||||||
|
recordSessionCreated: (event: SessionCreatedEvent): Promise<string | null> =>
|
||||||
|
ipcRenderer.invoke('stats:record-session-created', event),
|
||||||
|
|
||||||
|
// Record session closure (for lifecycle tracking)
|
||||||
|
recordSessionClosed: (sessionId: string, closedAt: number): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('stats:record-session-closed', sessionId, closedAt),
|
||||||
|
|
||||||
|
// Get session lifecycle events within a time range
|
||||||
|
getSessionLifecycle: (
|
||||||
|
range: 'day' | 'week' | 'month' | 'year' | 'all'
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
sessionId: string;
|
||||||
|
agentType: string;
|
||||||
|
projectPath?: string;
|
||||||
|
createdAt: number;
|
||||||
|
closedAt?: number;
|
||||||
|
duration?: number;
|
||||||
|
isRemote?: boolean;
|
||||||
|
}>
|
||||||
|
> => ipcRenderer.invoke('stats:get-session-lifecycle', range),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StatsApi = ReturnType<typeof createStatsApi>;
|
||||||
206
src/main/preload/system.ts
Normal file
206
src/main/preload/system.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for system operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.dialog, fonts, shells, shell, tunnel, sync, devtools, power, updates, app namespaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shell information
|
||||||
|
*/
|
||||||
|
export interface ShellInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
available: boolean;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update status from electron-updater
|
||||||
|
*/
|
||||||
|
export interface UpdateStatus {
|
||||||
|
status:
|
||||||
|
| 'idle'
|
||||||
|
| 'checking'
|
||||||
|
| 'available'
|
||||||
|
| 'not-available'
|
||||||
|
| 'downloading'
|
||||||
|
| 'downloaded'
|
||||||
|
| 'error';
|
||||||
|
info?: { version: string };
|
||||||
|
progress?: { percent: number; bytesPerSecond: number; total: number; transferred: number };
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the dialog API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createDialogApi() {
|
||||||
|
return {
|
||||||
|
selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'),
|
||||||
|
saveFile: (options: {
|
||||||
|
defaultPath?: string;
|
||||||
|
filters?: Array<{ name: string; extensions: string[] }>;
|
||||||
|
title?: string;
|
||||||
|
}) => ipcRenderer.invoke('dialog:saveFile', options),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the fonts API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createFontsApi() {
|
||||||
|
return {
|
||||||
|
detect: () => ipcRenderer.invoke('fonts:detect'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the shells API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createShellsApi() {
|
||||||
|
return {
|
||||||
|
detect: (): Promise<ShellInfo[]> => ipcRenderer.invoke('shells:detect'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the shell API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createShellApi() {
|
||||||
|
return {
|
||||||
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||||
|
trashItem: (itemPath: string) => ipcRenderer.invoke('shell:trashItem', itemPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the tunnel API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createTunnelApi() {
|
||||||
|
return {
|
||||||
|
isCloudflaredInstalled: () => ipcRenderer.invoke('tunnel:isCloudflaredInstalled'),
|
||||||
|
start: () => ipcRenderer.invoke('tunnel:start'),
|
||||||
|
stop: () => ipcRenderer.invoke('tunnel:stop'),
|
||||||
|
getStatus: () => ipcRenderer.invoke('tunnel:getStatus'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the sync API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createSyncApi() {
|
||||||
|
return {
|
||||||
|
getDefaultPath: (): Promise<string> => ipcRenderer.invoke('sync:getDefaultPath'),
|
||||||
|
getSettings: (): Promise<{ customSyncPath?: string }> => ipcRenderer.invoke('sync:getSettings'),
|
||||||
|
getCurrentStoragePath: (): Promise<string> => ipcRenderer.invoke('sync:getCurrentStoragePath'),
|
||||||
|
selectSyncFolder: (): Promise<string | null> => ipcRenderer.invoke('sync:selectSyncFolder'),
|
||||||
|
setCustomPath: (
|
||||||
|
customPath: string | null
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
migrated?: number;
|
||||||
|
errors?: string[];
|
||||||
|
requiresRestart?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('sync:setCustomPath', customPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the devtools API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createDevtoolsApi() {
|
||||||
|
return {
|
||||||
|
open: () => ipcRenderer.invoke('devtools:open'),
|
||||||
|
close: () => ipcRenderer.invoke('devtools:close'),
|
||||||
|
toggle: () => ipcRenderer.invoke('devtools:toggle'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the power API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createPowerApi() {
|
||||||
|
return {
|
||||||
|
setEnabled: (enabled: boolean): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('power:setEnabled', enabled),
|
||||||
|
isEnabled: (): Promise<boolean> => ipcRenderer.invoke('power:isEnabled'),
|
||||||
|
getStatus: (): Promise<{
|
||||||
|
enabled: boolean;
|
||||||
|
blocking: boolean;
|
||||||
|
reasons: string[];
|
||||||
|
platform: 'darwin' | 'win32' | 'linux';
|
||||||
|
}> => ipcRenderer.invoke('power:getStatus'),
|
||||||
|
addReason: (reason: string): Promise<void> => ipcRenderer.invoke('power:addReason', reason),
|
||||||
|
removeReason: (reason: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('power:removeReason', reason),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the updates API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createUpdatesApi() {
|
||||||
|
return {
|
||||||
|
check: (
|
||||||
|
includePrerelease?: boolean
|
||||||
|
): Promise<{
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
updateAvailable: boolean;
|
||||||
|
versionsBehind: number;
|
||||||
|
releases: Array<{
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
body: string;
|
||||||
|
html_url: string;
|
||||||
|
published_at: string;
|
||||||
|
}>;
|
||||||
|
releasesUrl: string;
|
||||||
|
error?: string;
|
||||||
|
}> => ipcRenderer.invoke('updates:check', includePrerelease),
|
||||||
|
download: (): Promise<{ success: boolean; error?: string }> =>
|
||||||
|
ipcRenderer.invoke('updates:download'),
|
||||||
|
install: (): Promise<void> => ipcRenderer.invoke('updates:install'),
|
||||||
|
getStatus: (): Promise<UpdateStatus> => ipcRenderer.invoke('updates:getStatus'),
|
||||||
|
onStatus: (callback: (status: UpdateStatus) => void) => {
|
||||||
|
const handler = (_: any, status: UpdateStatus) => callback(status);
|
||||||
|
ipcRenderer.on('updates:status', handler);
|
||||||
|
return () => ipcRenderer.removeListener('updates:status', handler);
|
||||||
|
},
|
||||||
|
setAllowPrerelease: (allow: boolean): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('updates:setAllowPrerelease', allow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the app lifecycle API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createAppApi() {
|
||||||
|
return {
|
||||||
|
onQuitConfirmationRequest: (callback: () => void) => {
|
||||||
|
const handler = () => callback();
|
||||||
|
ipcRenderer.on('app:requestQuitConfirmation', handler);
|
||||||
|
return () => ipcRenderer.removeListener('app:requestQuitConfirmation', handler);
|
||||||
|
},
|
||||||
|
confirmQuit: () => {
|
||||||
|
ipcRenderer.send('app:quitConfirmed');
|
||||||
|
},
|
||||||
|
cancelQuit: () => {
|
||||||
|
ipcRenderer.send('app:quitCancelled');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DialogApi = ReturnType<typeof createDialogApi>;
|
||||||
|
export type FontsApi = ReturnType<typeof createFontsApi>;
|
||||||
|
export type ShellsApi = ReturnType<typeof createShellsApi>;
|
||||||
|
export type ShellApi = ReturnType<typeof createShellApi>;
|
||||||
|
export type TunnelApi = ReturnType<typeof createTunnelApi>;
|
||||||
|
export type SyncApi = ReturnType<typeof createSyncApi>;
|
||||||
|
export type DevtoolsApi = ReturnType<typeof createDevtoolsApi>;
|
||||||
|
export type PowerApi = ReturnType<typeof createPowerApi>;
|
||||||
|
export type UpdatesApi = ReturnType<typeof createUpdatesApi>;
|
||||||
|
export type AppApi = ReturnType<typeof createAppApi>;
|
||||||
103
src/main/preload/web.ts
Normal file
103
src/main/preload/web.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Preload API for web interface operations
|
||||||
|
*
|
||||||
|
* Provides the window.maestro.web, webserver, and live namespaces for:
|
||||||
|
* - Broadcasting state to web clients
|
||||||
|
* - Web server management
|
||||||
|
* - Live session management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto Run state for broadcasting
|
||||||
|
*/
|
||||||
|
export interface AutoRunState {
|
||||||
|
isRunning: boolean;
|
||||||
|
totalTasks: number;
|
||||||
|
completedTasks: number;
|
||||||
|
currentTaskIndex: number;
|
||||||
|
isStopping?: boolean;
|
||||||
|
totalDocuments?: number;
|
||||||
|
currentDocumentIndex?: number;
|
||||||
|
totalTasksAcrossAllDocs?: number;
|
||||||
|
completedTasksAcrossAllDocs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Tab state for broadcasting
|
||||||
|
*/
|
||||||
|
export interface AiTabState {
|
||||||
|
id: string;
|
||||||
|
agentSessionId: string | null;
|
||||||
|
name: string | null;
|
||||||
|
starred: boolean;
|
||||||
|
inputValue: string;
|
||||||
|
usageStats?: any;
|
||||||
|
createdAt: number;
|
||||||
|
state: 'idle' | 'busy';
|
||||||
|
thinkingStartTime?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the web interface API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createWebApi() {
|
||||||
|
return {
|
||||||
|
// Broadcast user input to web clients (for keeping web interface in sync)
|
||||||
|
broadcastUserInput: (sessionId: string, command: string, inputMode: 'ai' | 'terminal') =>
|
||||||
|
ipcRenderer.invoke('web:broadcastUserInput', sessionId, command, inputMode),
|
||||||
|
|
||||||
|
// Broadcast AutoRun state to web clients (for showing task progress on mobile)
|
||||||
|
broadcastAutoRunState: (sessionId: string, state: AutoRunState | null) =>
|
||||||
|
ipcRenderer.invoke('web:broadcastAutoRunState', sessionId, state),
|
||||||
|
|
||||||
|
// Broadcast tab changes to web clients (for tab sync)
|
||||||
|
broadcastTabsChange: (sessionId: string, aiTabs: AiTabState[], activeTabId: string) =>
|
||||||
|
ipcRenderer.invoke('web:broadcastTabsChange', sessionId, aiTabs, activeTabId),
|
||||||
|
|
||||||
|
// Broadcast session state change to web clients (for real-time busy/idle updates)
|
||||||
|
broadcastSessionState: (
|
||||||
|
sessionId: string,
|
||||||
|
state: string,
|
||||||
|
additionalData?: {
|
||||||
|
name?: string;
|
||||||
|
toolType?: string;
|
||||||
|
inputMode?: string;
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
) => ipcRenderer.invoke('web:broadcastSessionState', sessionId, state, additionalData),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the web server API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createWebserverApi() {
|
||||||
|
return {
|
||||||
|
getUrl: () => ipcRenderer.invoke('webserver:getUrl'),
|
||||||
|
getConnectedClients: () => ipcRenderer.invoke('webserver:getConnectedClients'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the live session API object for preload exposure
|
||||||
|
*/
|
||||||
|
export function createLiveApi() {
|
||||||
|
return {
|
||||||
|
toggle: (sessionId: string, agentSessionId?: string) =>
|
||||||
|
ipcRenderer.invoke('live:toggle', sessionId, agentSessionId),
|
||||||
|
getStatus: (sessionId: string) => ipcRenderer.invoke('live:getStatus', sessionId),
|
||||||
|
getDashboardUrl: () => ipcRenderer.invoke('live:getDashboardUrl'),
|
||||||
|
getLiveSessions: () => ipcRenderer.invoke('live:getLiveSessions'),
|
||||||
|
broadcastActiveSession: (sessionId: string) =>
|
||||||
|
ipcRenderer.invoke('live:broadcastActiveSession', sessionId),
|
||||||
|
disableAll: () => ipcRenderer.invoke('live:disableAll'),
|
||||||
|
startServer: () => ipcRenderer.invoke('live:startServer'),
|
||||||
|
stopServer: () => ipcRenderer.invoke('live:stopServer'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebApi = ReturnType<typeof createWebApi>;
|
||||||
|
export type WebserverApi = ReturnType<typeof createWebserverApi>;
|
||||||
|
export type LiveApi = ReturnType<typeof createLiveApi>;
|
||||||
Reference in New Issue
Block a user