diff --git a/backend/package-lock.json b/backend/package-lock.json index db1e6f6..fb101a5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.65.0", "@google/generative-ai": "^0.24.1", - "@prompd/cli": "^0.5.0-beta.7", + "@prompd/cli": "^0.5.0-beta.9", "adm-zip": "^0.5.10", "archiver": "^6.0.1", "axios": "^1.6.2", @@ -2947,9 +2947,9 @@ } }, "node_modules/@prompd/cli": { - "version": "0.5.0-beta.7", - "resolved": "https://registry.npmjs.org/@prompd/cli/-/cli-0.5.0-beta.7.tgz", - "integrity": "sha512-BEyRSjP8H7x3lmAHl4onON9lUecocQnd8VLksUs5mzYV4dj7FI0JztrtA8samy6a8REB4/8CdstrgPo5UhlK3A==", + "version": "0.5.0-beta.9", + "resolved": "https://registry.npmjs.org/@prompd/cli/-/cli-0.5.0-beta.9.tgz", + "integrity": "sha512-YEoYmilLKY8SFB10559vKPXOlKdD8pvSabi5dDiD36F+enBSOB+mPFLY1BNnS83dBBuuHrdRJ00+WOPOWmnA+A==", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", "@types/nunjucks": "^3.2.6", diff --git a/backend/package.json b/backend/package.json index 53b6f55..4307244 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "api.prompd.app", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "description": "Node.js backend for Prompd Editor with MongoDB and WebSocket support", "main": "src/server.js", "license": "Elastic-2.0", @@ -14,7 +14,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.65.0", "@google/generative-ai": "^0.24.1", - "@prompd/cli": "^0.5.0-beta.7", + "@prompd/cli": "0.5.0-beta.10", "adm-zip": "^0.5.10", "archiver": "^6.0.1", "axios": "^1.6.2", diff --git a/backend/src/server.js b/backend/src/server.js index 7b25209..d0efe43 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -2,6 +2,19 @@ import dotenv from 'dotenv' // Load environment variables FIRST dotenv.config() +// Node 24+ c-ares DNS resolver can't resolve SRV records through +// ISP DNS-over-HTTPS proxies (e.g., Cox doh). Use public DNS as fallback. +import dns from 'dns' +try { + const resolver = new dns.Resolver() + resolver.setServers(['8.8.8.8', '1.1.1.1']) + resolver.resolveSrv('_mongodb._tcp.test.mongodb.net', () => {}) + // If the default resolver fails SRV lookups, this ensures MongoDB SRV works + dns.setServers(['8.8.8.8', '1.1.1.1', ...dns.getServers()]) +} catch { + // Ignore — default DNS is fine +} + import express from 'express' import { createServer } from 'http' import { Server } from 'socket.io' diff --git a/frontend/electron/ipc/TestIpcRegistration.js b/frontend/electron/ipc/TestIpcRegistration.js new file mode 100644 index 0000000..5451db3 --- /dev/null +++ b/frontend/electron/ipc/TestIpcRegistration.js @@ -0,0 +1,173 @@ +/** + * TestIpcRegistration — IPC handlers for test:* channels + * + * Bridges the renderer process to @prompd/test for prompt test discovery, + * execution, and live progress streaming. + * + * IMPORTANT: Passes the main process's @prompd/cli module to TestRunner + * so they share the same ConfigManager singleton (API keys, provider config). + * + * Handlers: test:discover, test:run, test:runAll, test:stop + * Events: test:progress (main → renderer) + */ + +const { BaseIpcRegistration } = require('./IpcRegistration') +const crypto = require('crypto') + +/** @type {import('@prompd/test') | null} */ +let testModule = null + +/** @type {any} */ +let cliModule = null + +/** + * Lazy-load @prompd/test (CommonJS package) + */ +function getTestModule() { + if (!testModule) { + try { + testModule = require('@prompd/test') + } catch (err) { + console.error('[TestIpc] Failed to load @prompd/test:', err.message) + throw err + } + } + return testModule +} + +/** + * Lazy-load @prompd/cli — same instance used by main.js + */ +async function getCliModule() { + if (!cliModule) { + try { + cliModule = await import('@prompd/cli') + // Use the singleton ConfigManager — same instance the PrompdExecutor uses internally + const cm = cliModule.ConfigManager.getInstance + ? cliModule.ConfigManager.getInstance() + : new cliModule.ConfigManager() + await cm.loadConfig() + console.log('[TestIpc] @prompd/cli loaded and config initialized via singleton') + } catch (err) { + console.error('[TestIpc] Failed to load @prompd/cli:', err.message) + throw err + } + } + return cliModule +} + +class TestIpcRegistration extends BaseIpcRegistration { + constructor() { + super('TestIpc') + /** @type {Map} */ + this.activeRuns = new Map() + } + + register(ipcMain) { + // Discover .test.prmd files in a directory + ipcMain.handle('test:discover', async (_event, { directory }) => { + try { + const { TestDiscovery } = getTestModule() + const discovery = new TestDiscovery() + const result = await discovery.discover(directory) + + return { + success: true, + suites: result.suites.map(s => ({ + name: s.name, + description: s.description, + testFilePath: s.testFilePath, + targetPath: s.target, + testCount: s.tests.length, + testNames: s.tests.map(t => t.name), + })), + errors: result.errors, + } + } catch (err) { + return { + success: false, + suites: [], + errors: [{ filePath: directory, message: err.message }], + } + } + }) + + // Run tests for a specific target + ipcMain.handle('test:run', async (event, { target, options = {} }) => { + return this._runTests(event, target, options) + }) + + // Run all tests in a directory + ipcMain.handle('test:runAll', async (event, { directory, options = {} }) => { + return this._runTests(event, directory, options) + }) + + // Stop a running test + ipcMain.handle('test:stop', async (_event, { runId }) => { + const run = this.activeRuns.get(runId) + if (run) { + run.abort.abort() + this.activeRuns.delete(runId) + return { success: true } + } + return { success: false, error: 'No active run with that ID' } + }) + + console.log('[TestIpc] Registered test:discover, test:run, test:runAll, test:stop') + } + + /** + * Internal: run tests with progress streaming + */ + async _runTests(event, target, options) { + const runId = crypto.randomUUID() + const abort = new AbortController() + this.activeRuns.set(runId, { abort }) + + const sender = event.sender + + try { + const { TestRunner } = getTestModule() + // Pass the CLI module so TestRunner uses the same singleton (shared config/API keys) + const cli = await getCliModule() + const runner = new TestRunner(cli) + + // Progress callback sends events to renderer + const onProgress = (progressEvent) => { + if (abort.signal.aborted) return + try { + sender.send('test:progress', { runId, event: progressEvent }) + } catch { + // Sender may be destroyed if window closed + } + } + + const result = await runner.run(target, { ...options, signal: abort.signal }, onProgress) + + this.activeRuns.delete(runId) + + return { + success: true, + runId, + result, + } + } catch (err) { + this.activeRuns.delete(runId) + return { + success: false, + runId, + error: err.message, + } + } + } + + async cleanup() { + // Abort any running tests on app quit + for (const [runId, run] of this.activeRuns) { + run.abort.abort() + this.activeRuns.delete(runId) + } + } +} + +module.exports = { TestIpcRegistration } diff --git a/frontend/electron/main.js b/frontend/electron/main.js index 6dfb7ca..3b366a2 100644 --- a/frontend/electron/main.js +++ b/frontend/electron/main.js @@ -31,6 +31,7 @@ const { TemplateIpcRegistration } = require('./ipc/TemplateIpcRegistration') const { ResourceIpcRegistration } = require('./ipc/ResourceIpcRegistration') const { SkillIpcRegistration } = require('./ipc/SkillIpcRegistration') const { CacheIpcRegistration } = require('./ipc/CacheIpcRegistration') +const { TestIpcRegistration } = require('./ipc/TestIpcRegistration') const mcpService = require('./services/mcpService') const { mcpServerService } = require('./services/mcpServerService') const ipcModules = [ @@ -40,6 +41,7 @@ const ipcModules = [ new ResourceIpcRegistration(), new SkillIpcRegistration(), new CacheIpcRegistration(), + new TestIpcRegistration(), ] // Tray and trigger services for background workflow execution diff --git a/frontend/electron/preload.js b/frontend/electron/preload.js index 3992593..ed47553 100644 --- a/frontend/electron/preload.js +++ b/frontend/electron/preload.js @@ -507,6 +507,23 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('skill:list', workspacePath), }, + // Test runner - discover, run, and stream .test.prmd evaluation results + test: { + discover: (directory) => + ipcRenderer.invoke('test:discover', { directory }), + run: (target, options) => + ipcRenderer.invoke('test:run', { target, options }), + runAll: (directory, options) => + ipcRenderer.invoke('test:runAll', { directory, options }), + stop: (runId) => + ipcRenderer.invoke('test:stop', { runId }), + onProgress: (callback) => { + const handler = (_, data) => callback(data) + ipcRenderer.on('test:progress', handler) + return () => ipcRenderer.removeListener('test:progress', handler) + }, + }, + // Trigger service - background workflow execution management // Handles scheduled (cron), webhook, and file-watch triggers trigger: { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4125e20..0fb946d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,33 +1,36 @@ { "name": "@prompd/app", - "version": "0.5.0-beta.8", + "version": "0.5.0-beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@prompd/app", - "version": "0.5.0-beta.8", + "version": "0.5.0-beta.10", "hasInstallScript": true, "license": "Elastic-2.0", "dependencies": { "@clerk/clerk-react": "^5.58.1", "@modelcontextprotocol/sdk": "^1.26.0", "@monaco-editor/react": "^4.6.0", - "@prompd/cli": "file:../../prompd-cli/typescript", + "@prompd/cli": "0.5.0-beta.10", "@prompd/react": "file:../packages/react", "@prompd/scheduler": "file:../packages/scheduler", - "@tiptap/core": "^3.19.0", - "@tiptap/extension-code-block-lowlight": "^3.19.0", - "@tiptap/extension-image": "^3.19.0", - "@tiptap/extension-link": "^3.19.0", - "@tiptap/extension-placeholder": "^3.19.0", - "@tiptap/extension-table": "^3.19.0", - "@tiptap/extension-table-cell": "^3.19.0", - "@tiptap/extension-table-header": "^3.19.0", - "@tiptap/extension-table-row": "^3.19.0", - "@tiptap/pm": "^3.19.0", - "@tiptap/react": "^3.19.0", - "@tiptap/starter-kit": "^3.19.0", + "@prompd/test": "^0.5.0-beta.9", + "@tiptap/core": "^3.20.4", + "@tiptap/extension-code-block": "^3.20.4", + "@tiptap/extension-code-block-lowlight": "^3.20.4", + "@tiptap/extension-image": "^3.20.4", + "@tiptap/extension-link": "^3.20.4", + "@tiptap/extension-placeholder": "^3.20.4", + "@tiptap/extension-table": "^3.20.4", + "@tiptap/extension-table-cell": "^3.20.4", + "@tiptap/extension-table-header": "^3.20.4", + "@tiptap/extension-table-row": "^3.20.4", + "@tiptap/extensions": "^3.20.4", + "@tiptap/pm": "^3.20.4", + "@tiptap/react": "^3.20.4", + "@tiptap/starter-kit": "^3.20.4", "@xyflow/react": "^12.10.0", "adm-zip": "^0.5.10", "better-sqlite3": "^12.6.2", @@ -85,7 +88,7 @@ }, "../../prompd-cli/typescript": { "name": "@prompd/cli", - "version": "0.5.0-beta.7", + "version": "0.5.0-beta.10", "license": "Elastic-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", @@ -142,7 +145,7 @@ }, "../packages/react": { "name": "@prompd/react", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "license": "Elastic-2.0", "dependencies": { "clsx": "^2.1.0", @@ -176,10 +179,10 @@ }, "../packages/scheduler": { "name": "@prompd/scheduler", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "license": "Elastic-2.0", "dependencies": { - "@prompd/cli": "^0.5.0-beta.7", + "@prompd/cli": "^0.5.0-beta.9", "adm-zip": "^0.5.10", "better-sqlite3": "^12.6.2", "chokidar": "^3.6.0", @@ -1428,7 +1431,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1445,7 +1447,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "engines": { "node": ">=12" }, @@ -1457,7 +1458,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "engines": { "node": ">=12" }, @@ -1468,14 +1468,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1492,7 +1490,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "dependencies": { "ansi-regex": "^6.2.2" }, @@ -1507,7 +1504,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1747,7 +1743,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -1765,6 +1760,70 @@ "resolved": "../packages/scheduler", "link": true }, + "node_modules/@prompd/test": { + "version": "0.5.0-beta.9", + "resolved": "https://registry.npmjs.org/@prompd/test/-/test-0.5.0-beta.9.tgz", + "integrity": "sha512-hyVJ0v46EPswnyPTr4sE9DmdPDkOc+9i4LS4Qawcs2rsf0EN7Vi3Z+Rxssbjdb70WU58q3BljtDjmveJHsEvow==", + "license": "Elastic-2.0", + "dependencies": { + "glob": "^10.3.10", + "yaml": "^2.7.1" + }, + "peerDependencies": { + "@prompd/cli": ">=0.5.0-beta.9" + } + }, + "node_modules/@prompd/test/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/@prompd/test/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@prompd/test/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@prompd/test/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -2137,45 +2196,45 @@ } }, "node_modules/@tiptap/core": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz", - "integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz", + "integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.19.0" + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.3.tgz", - "integrity": "sha512-CXFRsNvInPAfclHAg6CIV/rf+Pvtlj1gV/V1F74xLn7M0qn+BWVM9PpyUbhBFdJ6dFicvCBugzyu87UMz/4jbg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.4.tgz", + "integrity": "sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-bold": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.3.tgz", - "integrity": "sha512-LF91cLmup5KXOYoM59Jv4ek9ggIkTiByU1BUybmwAio84FddW/yDQij0XPf5JhPi9x1oCg/kaGO51Y1/4k/T2g==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.4.tgz", + "integrity": "sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.3.tgz", - "integrity": "sha512-21sVeo9ixzK44W6abCI3tbX3aSa9zwounqTkPArGCmk/imI9DQyo8JaZ+36KnnpWFJiKbiikMLhqrEdvV3Wj6w==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.4.tgz", + "integrity": "sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==", "optional": true, "dependencies": { "@floating-ui/dom": "^1.0.0" @@ -2185,91 +2244,91 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.3.tgz", - "integrity": "sha512-UW9rtLCe4qJaSsQl4Mp6rPvqLn5erA2CAmvbAVg5IrrOs45j4Pd1HRaYDpbqVCxOdhRNVWdikcfu5KM5yAE5lw==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.4.tgz", + "integrity": "sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.3" + "@tiptap/extension-list": "^3.20.4" } }, "node_modules/@tiptap/extension-code": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.3.tgz", - "integrity": "sha512-5ik0UjaSsn9VrfmlM+gI8+bZmGIwfJvzb0SopgE7NkP4duB8mk0aANpzPCpr8NSuCxY+kqP+SbSUMJg25eK2bA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.4.tgz", + "integrity": "sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.3.tgz", - "integrity": "sha512-jn/w0csZVVmM9KFIvhsKpKhXF66Q9MNTx97gFqTLZSvJUxofu6Zx3+Sf+R8f1fMjgaOfHV1YPeVOfcczNEGldg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.4.tgz", + "integrity": "sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-code-block-lowlight": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.19.0.tgz", - "integrity": "sha512-P8O8i1J+XozEVA7bF/Ijwf/r1rVqrh1DBQ7dXxBcrUvLpIGyVjtxX228jBF/kD4kf2xOlphvjDhV2fLa8XOVsg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.20.4.tgz", + "integrity": "sha512-YE+OxuvQx3oXGzSkhRyQzCGXOWrVntdTUQgRfOu5Ky+ZtScPWCVsTwUP8SBGBQjqp3sbbBehZznipbUIpWWJDA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.19.0", - "@tiptap/extension-code-block": "^3.19.0", - "@tiptap/pm": "^3.19.0", + "@tiptap/core": "^3.20.4", + "@tiptap/extension-code-block": "^3.20.4", + "@tiptap/pm": "^3.20.4", "highlight.js": "^11", "lowlight": "^2 || ^3" } }, "node_modules/@tiptap/extension-document": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.3.tgz", - "integrity": "sha512-nJZP1dixbWwzq2Q5kf3EBsorlkJ5lYKCBlkmwcAxjuekHAxmyQGFpj6V/OIsPmcbI5hHUbjYPsI2wH12LL7rLQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.4.tgz", + "integrity": "sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.3.tgz", - "integrity": "sha512-KupPvqQpZe+ezjJ0h6spN6D6mfYr5+r36B8DCS9+OmgjIWnwvXWCdvkLpdeJ/N1nCfBewBbsdnZLJ7aitpaYWQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.4.tgz", + "integrity": "sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.20.3" + "@tiptap/extensions": "^3.20.4" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.3.tgz", - "integrity": "sha512-vojKVspzxlnC3DjVKhfbYkijNDDGzxHTA13Y6/J0cOJMGmx+M/QO05gjYKZMyw0JpmkhT9Rbcsg1bElwuI/SMw==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.4.tgz", + "integrity": "sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==", "optional": true, "funding": { "type": "github", @@ -2277,87 +2336,87 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.3.tgz", - "integrity": "sha512-irrvYXBXWOR6KydC5b+SNpEu7w1mzgqKRCIAQkdo7/IQbsJlGiaxJEJXBKjRInzJmZCBnicVOTNGjt5rjZK1MQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.4.tgz", + "integrity": "sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.20.3" + "@tiptap/extensions": "^3.20.4" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.3.tgz", - "integrity": "sha512-r2E5AcVePCsCYm+6OtXCzyn2/U8Kr4H2fT7allqnDB2XQwyhpBbPKmV87FmwUpcWzM872hvRST1xggNUxD5+iA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.4.tgz", + "integrity": "sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-heading": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.3.tgz", - "integrity": "sha512-OGsRJCn7jDmPILa4/NL7skiZg7YBg/Kh3YDI71NBf8WTuEvqC3gP3igQizVfJQZphHlAqy8DuxwAvaYTeqIy+Q==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.4.tgz", + "integrity": "sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.3.tgz", - "integrity": "sha512-iV8SPyo+S4SvJsTt3/1wjCpaYyh0AOOy6xZr9Cf4/dkC0mYzPDvpOIqnFoFpgsyOLDFzSiYju4T8Ho+IyFL8oQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.4.tgz", + "integrity": "sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-image": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.19.0.tgz", - "integrity": "sha512-/rGl8nBziBPVJJ/9639eQWFDKcI3RQsDM3s+cqYQMFQfMqc7sQB9h4o4sHCBpmKxk3Y0FV/0NjnjLbBVm8OKdQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.20.4.tgz", + "integrity": "sha512-57w2TevHQljTh6Xiry9duIm7NNOQAUSTwtwRn4GGLoKwHR8qXTxzp513ASrFOgR2kgs2TP471Au6RHf947P+jg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.19.0" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-italic": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.3.tgz", - "integrity": "sha512-BTRicY4NnPw2fWjUZJJr6gUEjgoVRxu35K0gQTI7So3pHWWlBXle2MZea/biiIm5iugDYX3eSDXUkF4M8ga3bQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.4.tgz", + "integrity": "sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-link": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.3.tgz", - "integrity": "sha512-t707kGcdXpwagCIqQ1kQ9fcf7v0TKN0DUwaesGCwxLk93uPBvR0P9PDtM/64Y1B/DoVM7JzyHQNc+s5ZFtKToQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.4.tgz", + "integrity": "sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==", "dependencies": { "linkifyjs": "^4.3.2" }, @@ -2366,185 +2425,185 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-list": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.3.tgz", - "integrity": "sha512-g8DrDr6XyW4BWpQvophHKeUlUPKm4Vi9CCielX06CrLC5W5J5v4E6h3hXPmKAjr7yhteLJ+bP28lWjrcuYxInQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz", + "integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-list-item": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.3.tgz", - "integrity": "sha512-Vvyui+fkz2KhRkLqOWRI1WO8rO2NQFsPriBvFPu/A3yZJE62IaEIcsrLfPu0wwb/ryKYkkWQTOS/rcDFi2PlzA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.4.tgz", + "integrity": "sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.3" + "@tiptap/extension-list": "^3.20.4" } }, "node_modules/@tiptap/extension-list-keymap": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.3.tgz", - "integrity": "sha512-BH81ljfrzW1T4+G8IRGYLejNgkwOCn7mPuY0bSLebrmIr8Nx/Ehw0z4sLDJLs9BtaFVSYm8Cgmg7BmdTRSZXhQ==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.4.tgz", + "integrity": "sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.3" + "@tiptap/extension-list": "^3.20.4" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.3.tgz", - "integrity": "sha512-Uybf+oyzUm3upxw4AMFP5e6btzwdQfqVDsyF3xQ0y92SCszEx6QU0dxSsWR7UMb91EhAXPzRuR0LLkUnTxEkzg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.4.tgz", + "integrity": "sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.3" + "@tiptap/extension-list": "^3.20.4" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.3.tgz", - "integrity": "sha512-8+ICwCRCHaraJUeGl3sGG4n5LnNCA0F/fF7rDatP1EmYW4mpkD7FXTFKqNYpVksQVdnM7Pcz1PGaPOSPHUvTkg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.4.tgz", + "integrity": "sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-placeholder": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.19.0.tgz", - "integrity": "sha512-i15OfgyI4IDCYAcYSKUMnuZkYuUInfanjf9zquH8J2BETiomf/jZldVCp/QycMJ8DOXZ38fXDc99wOygnSNySg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.4.tgz", + "integrity": "sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.19.0" + "@tiptap/extensions": "^3.20.4" } }, "node_modules/@tiptap/extension-strike": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.3.tgz", - "integrity": "sha512-7uxLbM3I2vr+jWiJkS4xpTAoDxLpMriBKOzptOqbUfCAWb+T4+PKA6KrFVgCb1+4PWl9xGRGuSzxdyAVziHL5w==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.4.tgz", + "integrity": "sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-table": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.19.0.tgz", - "integrity": "sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.20.4.tgz", + "integrity": "sha512-vEHXRL9k9G02pp3P+DyUnN4YRaRAHGfTBC6gck0s9TpsCM9NIchL0qI1fb/u46Bu6UaoMMk58DGr7xaJ29g7KQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.19.0", - "@tiptap/pm": "^3.19.0" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/extension-table-cell": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.19.0.tgz", - "integrity": "sha512-T67EDWmiRdOGctolaUpMPXffDkEFL+NUppSV61cPU3jQtDwGg01meauy5u67u6OUM8ICiZ+Sc7Xd2DIt+7Nv5g==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.20.4.tgz", + "integrity": "sha512-R+AKOPKCq2NwETa4/nsidXnYe7UP8CxdbvJI1r6DMYipdj4ksQI24ij91hkwDLkeAhJyNGKub4soEMRdLgcQyQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-table": "^3.19.0" + "@tiptap/extension-table": "^3.20.4" } }, "node_modules/@tiptap/extension-table-header": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.19.0.tgz", - "integrity": "sha512-fVHJUCfZp/JOE96hJ3paElRynpIVdukOiAjgZbaY9WuCKoXd3xYqdsbUSdf+XFmdPqwvQKhnkaY/qXW3FIxO3w==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.20.4.tgz", + "integrity": "sha512-f8xlcNfmh2MD+fzPYXhy1zvao5DlWf+Sdd7eQ4G3baui/PdgBpJEroVCPYZIOazVRHL43MFxtvPeSJ5RN6J+bw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-table": "^3.19.0" + "@tiptap/extension-table": "^3.20.4" } }, "node_modules/@tiptap/extension-table-row": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.19.0.tgz", - "integrity": "sha512-L3DeIXQARCs+m4rsQWiUPJso1z6xZ6awa/Kc+PCoq7rbPLtwhWUIld3sm9gdLac61Mf/TVwb18EdfygHcU7jCA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.20.4.tgz", + "integrity": "sha512-bW9pXTMpNmDoZrLVPYylkfhiSwPxTYpubFduW3cjsiSJK046kSw5zOlvlv4+9vZI1TSlA+BFykPeW34YqLbr6Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-table": "^3.19.0" + "@tiptap/extension-table": "^3.20.4" } }, "node_modules/@tiptap/extension-text": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.3.tgz", - "integrity": "sha512-7LZhGH6jWUH+wZJWzjCmyCvkTc4yf6iH1NHN2GPDWVQHOUU35EBxs1ptkE1EU4GJHplaLNXjKHUlxC7nlkdi9w==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.4.tgz", + "integrity": "sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extension-underline": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.3.tgz", - "integrity": "sha512-0tBU1sm8ulfuJGXq1pOB3KzAYIFTZjCqmk6BplE9uNmpt76bpNOnNhe/1CIjhSU4sqgkwAglDVUJ1h/SuMjmQw==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.4.tgz", + "integrity": "sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3" + "@tiptap/core": "^3.20.4" } }, "node_modules/@tiptap/extensions": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.3.tgz", - "integrity": "sha512-SqKzXnTrKK/MPyBdzEiC/UazXMCilIQXCl6fuaGkXFOfbYIs9ly9bD2ucgghhBq+khIRY6joNQndqbGi0U0OCA==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz", + "integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.3", - "@tiptap/pm": "^3.20.3" + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4" } }, "node_modules/@tiptap/pm": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz", - "integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz", + "integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==", "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -2571,9 +2630,9 @@ } }, "node_modules/@tiptap/react": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.19.0.tgz", - "integrity": "sha512-GQQMUUXMpNd8tRjc1jDK3tDRXFugJO7C928EqmeBcBzTKDrFIJ3QUoZKEPxUNb6HWhZ2WL7q00fiMzsv4DNSmg==", + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.4.tgz", + "integrity": "sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==", "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", @@ -2584,12 +2643,12 @@ "url": "https://github.com/sponsors/ueberdosis" }, "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.19.0", - "@tiptap/extension-floating-menu": "^3.19.0" + "@tiptap/extension-bubble-menu": "^3.20.4", + "@tiptap/extension-floating-menu": "^3.20.4" }, "peerDependencies": { - "@tiptap/core": "^3.19.0", - "@tiptap/pm": "^3.19.0", + "@tiptap/core": "^3.20.4", + "@tiptap/pm": "^3.20.4", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -2597,34 +2656,34 @@ } }, "node_modules/@tiptap/starter-kit": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.19.0.tgz", - "integrity": "sha512-dTCkHEz+Y8ADxX7h+xvl6caAj+3nII/wMB1rTQchSuNKqJTOrzyUsCWm094+IoZmLT738wANE0fRIgziNHs/ug==", - "dependencies": { - "@tiptap/core": "^3.19.0", - "@tiptap/extension-blockquote": "^3.19.0", - "@tiptap/extension-bold": "^3.19.0", - "@tiptap/extension-bullet-list": "^3.19.0", - "@tiptap/extension-code": "^3.19.0", - "@tiptap/extension-code-block": "^3.19.0", - "@tiptap/extension-document": "^3.19.0", - "@tiptap/extension-dropcursor": "^3.19.0", - "@tiptap/extension-gapcursor": "^3.19.0", - "@tiptap/extension-hard-break": "^3.19.0", - "@tiptap/extension-heading": "^3.19.0", - "@tiptap/extension-horizontal-rule": "^3.19.0", - "@tiptap/extension-italic": "^3.19.0", - "@tiptap/extension-link": "^3.19.0", - "@tiptap/extension-list": "^3.19.0", - "@tiptap/extension-list-item": "^3.19.0", - "@tiptap/extension-list-keymap": "^3.19.0", - "@tiptap/extension-ordered-list": "^3.19.0", - "@tiptap/extension-paragraph": "^3.19.0", - "@tiptap/extension-strike": "^3.19.0", - "@tiptap/extension-text": "^3.19.0", - "@tiptap/extension-underline": "^3.19.0", - "@tiptap/extensions": "^3.19.0", - "@tiptap/pm": "^3.19.0" + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.4.tgz", + "integrity": "sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==", + "dependencies": { + "@tiptap/core": "^3.20.4", + "@tiptap/extension-blockquote": "^3.20.4", + "@tiptap/extension-bold": "^3.20.4", + "@tiptap/extension-bullet-list": "^3.20.4", + "@tiptap/extension-code": "^3.20.4", + "@tiptap/extension-code-block": "^3.20.4", + "@tiptap/extension-document": "^3.20.4", + "@tiptap/extension-dropcursor": "^3.20.4", + "@tiptap/extension-gapcursor": "^3.20.4", + "@tiptap/extension-hard-break": "^3.20.4", + "@tiptap/extension-heading": "^3.20.4", + "@tiptap/extension-horizontal-rule": "^3.20.4", + "@tiptap/extension-italic": "^3.20.4", + "@tiptap/extension-link": "^3.20.4", + "@tiptap/extension-list": "^3.20.4", + "@tiptap/extension-list-item": "^3.20.4", + "@tiptap/extension-list-keymap": "^3.20.4", + "@tiptap/extension-ordered-list": "^3.20.4", + "@tiptap/extension-paragraph": "^3.20.4", + "@tiptap/extension-strike": "^3.20.4", + "@tiptap/extension-text": "^3.20.4", + "@tiptap/extension-underline": "^3.20.4", + "@tiptap/extensions": "^3.20.4", + "@tiptap/pm": "^3.20.4" }, "funding": { "type": "github", @@ -3246,7 +3305,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3255,7 +3313,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4360,7 +4417,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4371,8 +4427,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-string": { "version": "1.9.1", @@ -5081,8 +5136,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ee-first": { "version": "1.1.1", @@ -5227,8 +5281,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "2.0.0", @@ -5773,7 +5826,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -5789,7 +5841,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -6696,7 +6747,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -6803,7 +6853,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -8189,7 +8238,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -8779,8 +8827,7 @@ "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, "node_modules/pako": { "version": "1.0.11", @@ -8850,7 +8897,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -8865,8 +8911,7 @@ "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/path-to-regexp": { "version": "8.3.0", @@ -10538,7 +10583,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10553,7 +10597,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10580,7 +10623,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10593,7 +10635,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11670,7 +11711,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", diff --git a/frontend/package.json b/frontend/package.json index 883ee79..adf787c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@prompd/app", - "version": "0.5.0-beta.8", + "version": "0.5.0-beta.10", "productName": "Prompd", "description": "AI prompt editor with package-based inheritance", "author": "Prompd LLC", @@ -43,6 +43,7 @@ "!**/node_modules/**/*.ts", "!**/node_modules/**/*.tsx", "!**/node_modules/**/test/**", + "node_modules/@prompd/test/**", "!**/node_modules/**/tests/**", "!**/node_modules/**/__tests__/**", "!**/node_modules/**/docs/**", @@ -161,21 +162,24 @@ "@clerk/clerk-react": "^5.58.1", "@modelcontextprotocol/sdk": "^1.26.0", "@monaco-editor/react": "^4.6.0", - "@prompd/cli": "file:../../prompd-cli/typescript", + "@prompd/cli": "0.5.0-beta.10", "@prompd/react": "file:../packages/react", "@prompd/scheduler": "file:../packages/scheduler", - "@tiptap/core": "^3.19.0", - "@tiptap/extension-code-block-lowlight": "^3.19.0", - "@tiptap/extension-image": "^3.19.0", - "@tiptap/extension-link": "^3.19.0", - "@tiptap/extension-placeholder": "^3.19.0", - "@tiptap/extension-table": "^3.19.0", - "@tiptap/extension-table-cell": "^3.19.0", - "@tiptap/extension-table-header": "^3.19.0", - "@tiptap/extension-table-row": "^3.19.0", - "@tiptap/pm": "^3.19.0", - "@tiptap/react": "^3.19.0", - "@tiptap/starter-kit": "^3.19.0", + "@prompd/test": "^0.5.0-beta.9", + "@tiptap/core": "^3.20.4", + "@tiptap/extension-code-block": "^3.20.4", + "@tiptap/extension-code-block-lowlight": "^3.20.4", + "@tiptap/extension-image": "^3.20.4", + "@tiptap/extension-link": "^3.20.4", + "@tiptap/extension-placeholder": "^3.20.4", + "@tiptap/extension-table": "^3.20.4", + "@tiptap/extension-table-cell": "^3.20.4", + "@tiptap/extension-table-header": "^3.20.4", + "@tiptap/extension-table-row": "^3.20.4", + "@tiptap/extensions": "^3.20.4", + "@tiptap/pm": "^3.20.4", + "@tiptap/react": "^3.20.4", + "@tiptap/starter-kit": "^3.20.4", "@xyflow/react": "^12.10.0", "adm-zip": "^0.5.10", "better-sqlite3": "^12.6.2", diff --git a/frontend/public/icons/prmd-test.svg b/frontend/public/icons/prmd-test.svg new file mode 100644 index 0000000..220eceb --- /dev/null +++ b/frontend/public/icons/prmd-test.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/frontend/public/licenses.json b/frontend/public/licenses.json index a60e39e..fba9ba7 100644 --- a/frontend/public/licenses.json +++ b/frontend/public/licenses.json @@ -1,33 +1,33 @@ { - "@clerk/clerk-react@5.59.3": { + "@clerk/clerk-react@5.61.3": { "licenses": "MIT", "repository": "https://github.com/clerk/javascript", "publisher": "Clerk", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@clerk\\clerk-react", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@clerk\\clerk-react\\LICENSE" }, - "@clerk/shared@3.42.0": { + "@clerk/shared@3.47.2": { "licenses": "MIT", "repository": "https://github.com/clerk/javascript", "publisher": "Clerk", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@clerk\\shared", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@clerk\\shared\\LICENSE" }, - "@floating-ui/core@1.7.4": { + "@floating-ui/core@1.7.5": { "licenses": "MIT", "repository": "https://github.com/floating-ui/floating-ui", "publisher": "atomiks", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@floating-ui\\core", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@floating-ui\\core\\LICENSE" }, - "@floating-ui/dom@1.7.5": { + "@floating-ui/dom@1.7.6": { "licenses": "MIT", "repository": "https://github.com/floating-ui/floating-ui", "publisher": "atomiks", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@floating-ui\\dom", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@floating-ui\\dom\\LICENSE" }, - "@floating-ui/utils@0.2.10": { + "@floating-ui/utils@0.2.11": { "licenses": "MIT", "repository": "https://github.com/floating-ui/floating-ui", "publisher": "atomiks", @@ -35,22 +35,13 @@ "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@floating-ui\\utils\\LICENSE" }, "@hono/node-server@1.19.11": { - "licenses": "MIT", - "repository": "https://github.com/honojs/node-server", - "publisher": "Yusuke Wada", - "email": "yusuke@kamawada.com", - "url": "https://github.com/yusukebe", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@hono\\node-server", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@hono\\node-server\\LICENSE" - }, - "@hono/node-server@1.19.9": { "licenses": "MIT", "repository": "https://github.com/honojs/node-server", "publisher": "Yusuke Wada", "email": "yusuke@kamawada.com", "url": "https://github.com/yusukebe", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@hono\\node-server", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@hono\\node-server\\README.md" + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@hono\\node-server\\LICENSE" }, "@img/colour@1.0.0": { "licenses": "MIT", @@ -82,7 +73,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\@inquirer\\figures", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\@inquirer\\figures\\LICENSE" }, - "@ioredis/commands@1.5.0": { + "@ioredis/commands@1.5.1": { "licenses": "MIT", "repository": "https://github.com/ioredis/commands", "publisher": "Zihua Li", @@ -156,35 +147,40 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@pkgjs\\parseargs", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@pkgjs\\parseargs\\LICENSE" }, - "@prompd/app@0.5.0-beta.8": { + "@prompd/app@0.5.0-beta.10": { "licenses": "UNLICENSED", "private": true, "publisher": "Prompd LLC", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend" }, - "@prompd/cli@0.5.0-beta.6": { + "@prompd/cli@0.5.0-beta.10": { "licenses": "Elastic-2.0", "publisher": "Prompd LLC", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\@prompd\\cli", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\@prompd\\cli\\README.md" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\README.md" }, - "@prompd/cli@0.5.0-beta.7": { + "@prompd/cli@0.5.0-beta.9": { "licenses": "Elastic-2.0", "publisher": "Prompd LLC", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\README.md" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\@prompd\\cli", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\@prompd\\cli\\README.md" }, - "@prompd/react@0.5.0-beta.1": { + "@prompd/react@0.5.0-beta.10": { "licenses": "Elastic-2.0", "publisher": "Stephen Baker", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\README.md" }, - "@prompd/scheduler@0.5.0-beta.1": { + "@prompd/scheduler@0.5.0-beta.10": { "licenses": "Elastic-2.0", "publisher": "Prompd Team", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler" }, + "@prompd/test@0.5.0-beta.9": { + "licenses": "Elastic-2.0", + "publisher": "Prompd Team", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\test" + }, "@remirror/core-constants@3.0.0": { "licenses": "MIT", "repository": "https://github.com/remirror/remirror", @@ -197,211 +193,211 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@socket.io\\component-emitter", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@socket.io\\component-emitter\\LICENSE" }, - "@tiptap/core@3.19.0": { + "@tiptap/core@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\core", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\core\\LICENSE.md" }, - "@tiptap/extension-blockquote@3.19.0": { + "@tiptap/extension-blockquote@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-blockquote", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-blockquote\\LICENSE.md" }, - "@tiptap/extension-bold@3.19.0": { + "@tiptap/extension-bold@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-bold", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-bold\\LICENSE.md" }, - "@tiptap/extension-bubble-menu@3.19.0": { + "@tiptap/extension-bubble-menu@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-bubble-menu", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-bubble-menu\\LICENSE.md" }, - "@tiptap/extension-bullet-list@3.19.0": { + "@tiptap/extension-bullet-list@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-bullet-list", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-bullet-list\\LICENSE.md" }, - "@tiptap/extension-code-block-lowlight@3.19.0": { + "@tiptap/extension-code-block-lowlight@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-code-block-lowlight", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-code-block-lowlight\\LICENSE.md" }, - "@tiptap/extension-code-block@3.19.0": { + "@tiptap/extension-code-block@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-code-block", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-code-block\\LICENSE.md" }, - "@tiptap/extension-code@3.19.0": { + "@tiptap/extension-code@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-code", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-code\\LICENSE.md" }, - "@tiptap/extension-document@3.19.0": { + "@tiptap/extension-document@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-document", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-document\\LICENSE.md" }, - "@tiptap/extension-dropcursor@3.19.0": { + "@tiptap/extension-dropcursor@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-dropcursor", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-dropcursor\\LICENSE.md" }, - "@tiptap/extension-floating-menu@3.19.0": { + "@tiptap/extension-floating-menu@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-floating-menu", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-floating-menu\\LICENSE.md" }, - "@tiptap/extension-gapcursor@3.19.0": { + "@tiptap/extension-gapcursor@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-gapcursor", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-gapcursor\\LICENSE.md" }, - "@tiptap/extension-hard-break@3.19.0": { + "@tiptap/extension-hard-break@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-hard-break", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-hard-break\\LICENSE.md" }, - "@tiptap/extension-heading@3.19.0": { + "@tiptap/extension-heading@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-heading", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-heading\\LICENSE.md" }, - "@tiptap/extension-horizontal-rule@3.19.0": { + "@tiptap/extension-horizontal-rule@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-horizontal-rule", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-horizontal-rule\\LICENSE.md" }, - "@tiptap/extension-image@3.19.0": { + "@tiptap/extension-image@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-image", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-image\\LICENSE.md" }, - "@tiptap/extension-italic@3.19.0": { + "@tiptap/extension-italic@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-italic", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-italic\\LICENSE.md" }, - "@tiptap/extension-link@3.19.0": { + "@tiptap/extension-link@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-link", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-link\\LICENSE.md" }, - "@tiptap/extension-list-item@3.19.0": { + "@tiptap/extension-list-item@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-list-item", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-list-item\\LICENSE.md" }, - "@tiptap/extension-list-keymap@3.19.0": { + "@tiptap/extension-list-keymap@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-list-keymap", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-list-keymap\\LICENSE.md" }, - "@tiptap/extension-list@3.19.0": { + "@tiptap/extension-list@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-list", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-list\\LICENSE.md" }, - "@tiptap/extension-ordered-list@3.19.0": { + "@tiptap/extension-ordered-list@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-ordered-list", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-ordered-list\\LICENSE.md" }, - "@tiptap/extension-paragraph@3.19.0": { + "@tiptap/extension-paragraph@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-paragraph", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-paragraph\\LICENSE.md" }, - "@tiptap/extension-placeholder@3.19.0": { + "@tiptap/extension-placeholder@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-placeholder", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-placeholder\\LICENSE.md" }, - "@tiptap/extension-strike@3.19.0": { + "@tiptap/extension-strike@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-strike", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-strike\\LICENSE.md" }, - "@tiptap/extension-table-cell@3.19.0": { + "@tiptap/extension-table-cell@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table-cell", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table-cell\\LICENSE.md" }, - "@tiptap/extension-table-header@3.19.0": { + "@tiptap/extension-table-header@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table-header", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table-header\\LICENSE.md" }, - "@tiptap/extension-table-row@3.19.0": { + "@tiptap/extension-table-row@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table-row", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table-row\\LICENSE.md" }, - "@tiptap/extension-table@3.19.0": { + "@tiptap/extension-table@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-table\\LICENSE.md" }, - "@tiptap/extension-text@3.19.0": { + "@tiptap/extension-text@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-text", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-text\\LICENSE.md" }, - "@tiptap/extension-underline@3.19.0": { + "@tiptap/extension-underline@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-underline", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extension-underline\\LICENSE.md" }, - "@tiptap/extensions@3.19.0": { + "@tiptap/extensions@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extensions", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\extensions\\LICENSE.md" }, - "@tiptap/pm@3.19.0": { + "@tiptap/pm@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\pm", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\pm\\LICENSE.md" }, - "@tiptap/react@3.19.0": { + "@tiptap/react@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\react", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\react\\LICENSE.md" }, - "@tiptap/starter-kit@3.19.0": { + "@tiptap/starter-kit@3.20.4": { "licenses": "MIT", "repository": "https://github.com/ueberdosis/tiptap", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@tiptap\\starter-kit", @@ -527,6 +523,12 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@types\\node", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@types\\node\\LICENSE" }, + "@types/node@24.12.0": { + "licenses": "MIT", + "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@types\\node", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@types\\node\\LICENSE" + }, "@types/nunjucks@3.2.6": { "licenses": "MIT", "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped", @@ -551,6 +553,12 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\@types\\react", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\@types\\react\\LICENSE" }, + "@types/react@18.3.28": { + "licenses": "MIT", + "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@types\\react", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@types\\react\\LICENSE" + }, "@types/trusted-types@2.0.7": { "licenses": "MIT", "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped", @@ -600,13 +608,13 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@xmldom\\xmldom", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\@xmldom\\xmldom\\LICENSE" }, - "@xyflow/react@12.10.0": { + "@xyflow/react@12.10.1": { "licenses": "MIT", "repository": "https://github.com/xyflow/xyflow", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@xyflow\\react", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@xyflow\\react\\LICENSE" }, - "@xyflow/system@0.0.74": { + "@xyflow/system@0.0.75": { "licenses": "MIT", "repository": "https://github.com/xyflow/xyflow", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@xyflow\\system", @@ -655,19 +663,12 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ajv-formats", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ajv-formats\\LICENSE" }, - "ajv@8.17.1": { - "licenses": "MIT", - "repository": "https://github.com/ajv-validator/ajv", - "publisher": "Evgeny Poberezkin", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@modelcontextprotocol\\sdk\\node_modules\\ajv", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@modelcontextprotocol\\sdk\\node_modules\\ajv\\LICENSE" - }, "ajv@8.18.0": { "licenses": "MIT", "repository": "https://github.com/ajv-validator/ajv", "publisher": "Evgeny Poberezkin", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\ajv", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\ajv\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ajv", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ajv\\LICENSE" }, "ansi-escapes@4.3.2": { "licenses": "MIT", @@ -863,6 +864,14 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\better-sqlite3", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\better-sqlite3\\LICENSE" }, + "better-sqlite3@12.8.0": { + "licenses": "MIT", + "repository": "https://github.com/WiseLibs/better-sqlite3", + "publisher": "Joshua Wise", + "email": "joshuathomaswise@gmail.com", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\better-sqlite3", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\better-sqlite3\\LICENSE" + }, "binary-extensions@2.3.0": { "licenses": "MIT", "repository": "https://github.com/sindresorhus/binary-extensions", @@ -1339,12 +1348,12 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\cron-parser", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\cron-parser\\LICENSE" }, - "cronstrue@2.59.0": { + "cronstrue@3.13.0": { "licenses": "MIT", "repository": "https://github.com/bradymholt/cronstrue", "publisher": "Brady Holt", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\react-cron-generator\\node_modules\\cronstrue", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\react-cron-generator\\node_modules\\cronstrue\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\cronstrue", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\cronstrue\\LICENSE" }, "cross-spawn@7.0.6": { "licenses": "MIT", @@ -1458,15 +1467,6 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\debug", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\debug\\LICENSE" }, - "decode-named-character-reference@1.2.0": { - "licenses": "MIT", - "repository": "https://github.com/wooorm/decode-named-character-reference", - "publisher": "Titus Wormer", - "email": "tituswormer@gmail.com", - "url": "https://wooorm.com", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\decode-named-character-reference", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\decode-named-character-reference\\license" - }, "decode-named-character-reference@1.3.0": { "licenses": "MIT", "repository": "https://github.com/wooorm/decode-named-character-reference", @@ -1579,7 +1579,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\dompurify", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\dompurify\\LICENSE" }, - "dotenv@17.2.3": { + "dotenv@17.3.1": { "licenses": "BSD-2-Clause", "repository": "https://github.com/motdotla/dotenv", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\dotenv", @@ -1624,14 +1624,14 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ee-first", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ee-first\\LICENSE" }, - "electron-updater@6.7.3": { + "electron-updater@6.8.3": { "licenses": "MIT", "repository": "https://github.com/electron-userland/electron-builder", "publisher": "Vladimir Krivosheev", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\electron-updater", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\electron-updater\\LICENSE" }, - "elkjs@0.11.0": { + "elkjs@0.11.1": { "licenses": "EPL-2.0", "repository": "https://github.com/kieler/elkjs", "publisher": "Ulf Rüegg", @@ -1813,7 +1813,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\express-rate-limit", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\express-rate-limit\\license.md" }, - "express-rate-limit@8.2.1": { + "express-rate-limit@8.3.1": { "licenses": "MIT", "repository": "https://github.com/express-rate-limit/express-rate-limit", "publisher": "Nathan Friedly", @@ -2239,15 +2239,6 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\highlight.js", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\highlight.js\\LICENSE" }, - "hono@4.11.9": { - "licenses": "MIT", - "repository": "https://github.com/honojs/hono", - "publisher": "Yusuke Wada", - "email": "yusuke@kamawada.com", - "url": "https://github.com/yusukebe", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\hono", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\hono\\LICENSE" - }, "hono@4.12.6": { "licenses": "MIT", "repository": "https://github.com/honojs/hono", @@ -2266,6 +2257,15 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\hono", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\hono\\LICENSE" }, + "hono@4.12.8": { + "licenses": "MIT", + "repository": "https://github.com/honojs/hono", + "publisher": "Yusuke Wada", + "email": "yusuke@kamawada.com", + "url": "https://github.com/yusukebe", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\hono", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\hono\\LICENSE" + }, "html-url-attributes@3.0.1": { "licenses": "MIT", "repository": "https://github.com/rehypejs/rehype-minify/tree/main/packages/html-url-attributes", @@ -2386,7 +2386,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\inquirer", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\inquirer\\LICENSE" }, - "ioredis@5.9.3": { + "ioredis@5.10.0": { "licenses": "MIT", "repository": "https://github.com/luin/ioredis", "publisher": "Zihua Li", @@ -2395,23 +2395,14 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ioredis", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ioredis\\LICENSE" }, - "ip-address@10.0.1": { - "licenses": "MIT", - "repository": "https://github.com/beaugunderson/ip-address", - "publisher": "Beau Gunderson", - "email": "beau@beaugunderson.com", - "url": "https://beaugunderson.com/", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\express-rate-limit\\node_modules\\ip-address", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\express-rate-limit\\node_modules\\ip-address\\LICENSE" - }, "ip-address@10.1.0": { "licenses": "MIT", "repository": "https://github.com/beaugunderson/ip-address", "publisher": "Beau Gunderson", "email": "beau@beaugunderson.com", "url": "https://beaugunderson.com/", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\ip-address", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\ip-address\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ip-address", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ip-address\\LICENSE" }, "ipaddr.js@1.9.1": { "licenses": "MIT", @@ -2566,7 +2557,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\jackspeak", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\jackspeak\\LICENSE.md" }, - "jose@6.1.3": { + "jose@6.2.1": { "licenses": "MIT", "repository": "https://github.com/panva/jose", "publisher": "Filip Skokan", @@ -2574,14 +2565,6 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\jose", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\jose\\LICENSE.md" }, - "jose@6.2.1": { - "licenses": "MIT", - "repository": "https://github.com/panva/jose", - "publisher": "Filip Skokan", - "email": "panva.ip@gmail.com", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\jose", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\jose\\LICENSE.md" - }, "js-cookie@3.0.5": { "licenses": "MIT", "repository": "https://github.com/js-cookie/js-cookie", @@ -2608,8 +2591,8 @@ "licenses": "MIT", "repository": "https://github.com/epoberezkin/json-schema-traverse", "publisher": "Evgeny Poberezkin", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@modelcontextprotocol\\sdk\\node_modules\\json-schema-traverse", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@modelcontextprotocol\\sdk\\node_modules\\json-schema-traverse\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\json-schema-traverse", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\json-schema-traverse\\LICENSE" }, "json-schema-typed@8.0.2": { "licenses": "BSD-2-Clause", @@ -2715,7 +2698,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\linkifyjs", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\linkifyjs\\LICENSE" }, - "lodash-es@4.17.22": { + "lodash-es@4.17.23": { "licenses": "MIT", "repository": "https://github.com/lodash/lodash", "publisher": "John-David Dalton", @@ -2980,6 +2963,15 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\mdast-util-from-markdown", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\mdast-util-from-markdown\\license" }, + "mdast-util-from-markdown@2.0.3": { + "licenses": "MIT", + "repository": "https://github.com/syntax-tree/mdast-util-from-markdown", + "publisher": "Titus Wormer", + "email": "tituswormer@gmail.com", + "url": "https://wooorm.com", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\mdast-util-from-markdown", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\mdast-util-from-markdown\\license" + }, "mdast-util-gfm-autolink-literal@2.0.1": { "licenses": "MIT", "repository": "https://github.com/syntax-tree/mdast-util-gfm-autolink-literal", @@ -3409,8 +3401,8 @@ "mime-db@1.54.0": { "licenses": "MIT", "repository": "https://github.com/jshttp/mime-db", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\accepts\\node_modules\\mime-db", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\accepts\\node_modules\\mime-db\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\mime-db", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\mime-db\\LICENSE" }, "mime-types@2.1.35": { "licenses": "MIT", @@ -3421,8 +3413,8 @@ "mime-types@3.0.2": { "licenses": "MIT", "repository": "https://github.com/jshttp/mime-types", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\accepts\\node_modules\\mime-types", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\accepts\\node_modules\\mime-types\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\mime-types", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\mime-types\\LICENSE" }, "mime@1.6.0": { "licenses": "MIT", @@ -3562,7 +3554,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\mute-stream", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\mute-stream\\LICENSE" }, - "mysql2@3.17.0": { + "mysql2@3.20.0": { "licenses": "MIT", "repository": "https://github.com/sidorares/node-mysql2", "publisher": "Andrey Sidorov", @@ -3805,7 +3797,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-cloudflare", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-cloudflare\\LICENSE" }, - "pg-connection-string@2.11.0": { + "pg-connection-string@2.12.0": { "licenses": "MIT", "repository": "https://github.com/brianc/node-postgres", "publisher": "Blaine Bublitz", @@ -3820,14 +3812,14 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-int8", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-int8\\LICENSE" }, - "pg-pool@3.11.0": { + "pg-pool@3.13.0": { "licenses": "MIT", "repository": "https://github.com/brianc/node-postgres", "publisher": "Brian M. Carlson", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-pool", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-pool\\LICENSE" }, - "pg-protocol@1.11.0": { + "pg-protocol@1.13.0": { "licenses": "MIT", "repository": "https://github.com/brianc/node-postgres", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-protocol", @@ -3840,7 +3832,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-types", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pg-types\\README.md" }, - "pg@8.18.0": { + "pg@8.20.0": { "licenses": "MIT", "repository": "https://github.com/brianc/node-postgres", "publisher": "Brian Carlson", @@ -3930,7 +3922,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\property-information", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\property-information\\license" }, - "prosemirror-changeset@2.3.1": { + "prosemirror-changeset@2.4.0": { "licenses": "MIT", "repository": "https://github.com/prosemirror/prosemirror-changeset", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-changeset", @@ -3954,7 +3946,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-dropcursor", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-dropcursor\\LICENSE" }, - "prosemirror-gapcursor@1.4.0": { + "prosemirror-gapcursor@1.4.1": { "licenses": "MIT", "repository": "https://github.com/prosemirror/prosemirror-gapcursor", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-gapcursor", @@ -3984,7 +3976,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-markdown", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-markdown\\LICENSE" }, - "prosemirror-menu@1.2.5": { + "prosemirror-menu@1.3.0": { "licenses": "MIT", "repository": "https://github.com/prosemirror/prosemirror-menu", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\prosemirror-menu", @@ -4063,6 +4055,14 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\pump", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\pump\\LICENSE" }, + "pump@3.0.4": { + "licenses": "MIT", + "repository": "https://github.com/mafintosh/pump", + "publisher": "Mathias Buus Madsen", + "email": "mathiasbuus@gmail.com", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pump", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\pump\\LICENSE" + }, "punycode.js@2.3.1": { "licenses": "MIT", "repository": "https://github.com/mathiasbynens/punycode.js", @@ -4080,6 +4080,12 @@ "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\punycode\\LICENSE-MIT.txt" }, "qs@6.14.2": { + "licenses": "BSD-3-Clause", + "repository": "https://github.com/ljharb/qs", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\qs", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\qs\\LICENSE.md" + }, + "qs@6.15.0": { "licenses": "BSD-3-Clause", "repository": "https://github.com/ljharb/qs", "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\qs", @@ -4121,7 +4127,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\rc", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\scheduler\\node_modules\\rc\\LICENSE.APACHE2" }, - "react-cron-generator@2.1.4": { + "react-cron-generator@2.3.0": { "licenses": "ISC", "repository": "https://github.com/sojinantony01/react-cron-generator", "publisher": "Sojin Antony", @@ -4330,7 +4336,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\safer-buffer", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\safer-buffer\\LICENSE" }, - "sax@1.4.4": { + "sax@1.6.0": { "licenses": "BlueOak-1.0.0", "repository": "https://github.com/isaacs/sax-js", "publisher": "Isaac Z. Schlueter", @@ -4352,13 +4358,6 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\semver", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\semver\\LICENSE" }, - "semver@7.7.3": { - "licenses": "ISC", - "repository": "https://github.com/npm/node-semver", - "publisher": "GitHub Inc.", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\electron-updater\\node_modules\\semver", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\electron-updater\\node_modules\\semver\\LICENSE" - }, "semver@7.7.4": { "licenses": "ISC", "repository": "https://github.com/npm/node-semver", @@ -4390,14 +4389,6 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\send", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\send\\LICENSE" }, - "seq-queue@0.0.5": { - "licenses": "MIT*", - "repository": "https://github.com/changchang/seq-queue", - "publisher": "changchang", - "email": "changchang005@gmail.com", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\seq-queue", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\seq-queue\\LICENSE" - }, "serve-static@1.16.2": { "licenses": "MIT", "repository": "https://github.com/expressjs/serve-static", @@ -4596,7 +4587,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\sprintf-js", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\sprintf-js\\LICENSE" }, - "sql-escaper@1.3.1": { + "sql-escaper@1.3.3": { "licenses": "MIT", "repository": "https://github.com/mysqljs/sql-escaper", "publisher": "https://github.com/mysqljs", @@ -4816,7 +4807,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\tiny-typed-emitter", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\tiny-typed-emitter\\LICENSE" }, - "tiptap-markdown@0.9.0": { + "tiptap-markdown@0.8.10": { "licenses": "MIT", "repository": "https://github.com/aguingand/tiptap-markdown", "publisher": "Antoine Guingand", @@ -4928,6 +4919,12 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\undici-types", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\cli\\node_modules\\undici-types\\LICENSE" }, + "undici-types@7.16.0": { + "licenses": "MIT", + "repository": "https://github.com/nodejs/undici", + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\undici-types", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\undici-types\\LICENSE" + }, "unified@11.0.5": { "licenses": "MIT", "repository": "https://github.com/unifiedjs/unified", @@ -4982,15 +4979,6 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\unist-util-visit-parents", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\unist-util-visit-parents\\license" }, - "unist-util-visit@5.0.0": { - "licenses": "MIT", - "repository": "https://github.com/syntax-tree/unist-util-visit", - "publisher": "Titus Wormer", - "email": "tituswormer@gmail.com", - "url": "https://wooorm.com", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\unist-util-visit", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\unist-util-visit\\license" - }, "unist-util-visit@5.1.0": { "licenses": "MIT", "repository": "https://github.com/syntax-tree/unist-util-visit", @@ -5187,8 +5175,8 @@ "publisher": "Einar Otto Stangvik", "email": "einaros@gmail.com", "url": "http://2x.io", - "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ws", - "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\ws\\LICENSE" + "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\engine.io-client\\node_modules\\ws", + "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\engine.io-client\\node_modules\\ws\\LICENSE" }, "xlsx@0.18.5": { "licenses": "Apache-2.0", @@ -5292,7 +5280,7 @@ "path": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\zustand", "licenseFile": "C:\\git\\github\\Prompd\\prompd-app\\frontend\\node_modules\\@prompd\\react\\node_modules\\zustand\\LICENSE" }, - "zustand@5.0.9": { + "zustand@5.0.12": { "licenses": "MIT", "repository": "https://github.com/pmndrs/zustand", "publisher": "Paul Henschel", diff --git a/frontend/src/electron.d.ts b/frontend/src/electron.d.ts index f9e1d38..fc06738 100644 --- a/frontend/src/electron.d.ts +++ b/frontend/src/electron.d.ts @@ -510,6 +510,143 @@ export interface ElectronAPI { }> } + // Test runner - discover, run, and stream .test.prmd evaluation results + test?: { + discover: (directory: string) => Promise<{ + success: boolean + suites: Array<{ + name: string + description?: string + testFilePath: string + targetPath: string + testCount: number + testNames: string[] + }> + errors: Array<{ filePath: string; message: string }> + }> + run: (target: string, options?: { + evaluators?: Array<'nlp' | 'script' | 'prmd'> + noLlm?: boolean + reporter?: 'console' | 'json' | 'junit' + failFast?: boolean + runAll?: boolean + verbose?: boolean + workspaceRoot?: string + registryUrl?: string + }) => Promise<{ + success: boolean + runId: string + result?: { + suites: Array<{ + suite: string + testFilePath: string + results: Array<{ + suite: string + testName: string + status: 'pass' | 'fail' | 'error' | 'skip' + duration: number + assertions: Array<{ + evaluator: 'nlp' | 'script' | 'prmd' + check?: string + status: 'pass' | 'fail' | 'error' | 'skip' + reason?: string + duration: number + }> + output?: string + compiledInput?: string + error?: string + }> + }> + summary: { + total: number + passed: number + failed: number + errors: number + skipped: number + duration: number + } + } + error?: string + }> + runAll: (directory: string, options?: { + evaluators?: Array<'nlp' | 'script' | 'prmd'> + noLlm?: boolean + reporter?: 'console' | 'json' | 'junit' + failFast?: boolean + runAll?: boolean + verbose?: boolean + workspaceRoot?: string + registryUrl?: string + }) => Promise<{ + success: boolean + runId: string + result?: { + suites: Array<{ + suite: string + testFilePath: string + results: Array<{ + suite: string + testName: string + status: 'pass' | 'fail' | 'error' | 'skip' + duration: number + assertions: Array<{ + evaluator: 'nlp' | 'script' | 'prmd' + check?: string + status: 'pass' | 'fail' | 'error' | 'skip' + reason?: string + duration: number + }> + output?: string + compiledInput?: string + error?: string + }> + }> + summary: { + total: number + passed: number + failed: number + errors: number + skipped: number + duration: number + } + } + error?: string + }> + stop: (runId: string) => Promise<{ success: boolean; error?: string }> + onProgress: (callback: (data: { + runId: string + event: { + type: 'suite_start' | 'test_start' | 'test_complete' | 'suite_complete' | 'assertion_complete' + suite: string + testName?: string + testCount?: number + result?: { + suite: string + testName: string + status: 'pass' | 'fail' | 'error' | 'skip' + duration: number + assertions: Array<{ + evaluator: 'nlp' | 'script' | 'prmd' + check?: string + status: 'pass' | 'fail' | 'error' | 'skip' + reason?: string + duration: number + }> + output?: string + error?: string + } + results?: Array + assertion?: { + evaluator: 'nlp' | 'script' | 'prmd' + check?: string + status: 'pass' | 'fail' | 'error' | 'skip' + reason?: string + duration: number + } + } + }) => void) => () => void + } + // Workflow trigger management (schedule, webhook, file-watch) trigger?: { register: (config: TriggerConfig) => Promise<{ success: boolean; error?: string }> diff --git a/frontend/src/modules/App.tsx b/frontend/src/modules/App.tsx index d544560..27cb84b 100644 --- a/frontend/src/modules/App.tsx +++ b/frontend/src/modules/App.tsx @@ -19,6 +19,8 @@ import { ExecutionHistoryPanel } from './components/ExecutionHistoryPanel' import { ResourcePanel } from './components/ResourcePanel' import InstalledResourcesPanel from './editor/InstalledResourcesPanel' import PackageExplorerPanel from './editor/PackageExplorerPanel' +import { TestExplorerPanel } from './components/testing/TestExplorerPanel' +import { useTestStore } from '../stores/testStore' import type { PackageManifest } from './services/packageService' import { LocalStorageModal } from './components/LocalStorageModal' import { PublishModal } from './components/PublishModal' @@ -524,6 +526,15 @@ export default function App() { } }, []) + // Listen for test progress events from IPC + useEffect(() => { + if (!window.electronAPI?.test?.onProgress) return + const cleanup = window.electronAPI.test.onProgress((data) => { + useTestStore.getState().handleProgressEvent(data) + }) + return cleanup + }, []) + // Analytics opt-in sync moved to uiStore onRehydrateStorage (runs after localStorage hydration) // Auto-restore workspace on startup (Electron only) @@ -4205,9 +4216,15 @@ version: 1.0.0 if (!tab) return true // No tab open, allow switching // .prmd, .pdflow, and prompd.json files support design/code view toggle const name = tab.name.toLowerCase() + if (name.endsWith('.test.prmd')) return false // Design view strips HTML comments in .test.prmd return name.endsWith('.prmd') || name.endsWith('.pdflow') || name === 'prompd.json' || name.endsWith('/prompd.json') || name.endsWith('\\prompd.json') })()} - onExecutePrompd={getActiveTab()?.type === 'brainstorm' ? undefined : handleExecutePrompd} + onExecutePrompd={(() => { + const tab = getActiveTab() + if (!tab || tab.type === 'brainstorm') return undefined + if (tab.name.toLowerCase().endsWith('.test.prmd')) return undefined + return handleExecutePrompd + })()} onExecuteWorkflow={() => { // Dispatch event for WorkflowCanvas to handle window.dispatchEvent(new CustomEvent('execute-workflow')) @@ -4241,6 +4258,16 @@ version: 1.0.0 } } }} + onRunTests={(() => { + const tab = getActiveTab() + if (!tab) return undefined + const name = tab.name.toLowerCase() + if (!name.endsWith('.test.prmd')) return undefined + return () => { + const filePath = tab.filePath || tab.name + useTestStore.getState().runTests(filePath) + } + })()} /> +
+ +
diff --git a/frontend/src/modules/components/BottomPanelTabs.tsx b/frontend/src/modules/components/BottomPanelTabs.tsx index 08e821d..d793abd 100644 --- a/frontend/src/modules/components/BottomPanelTabs.tsx +++ b/frontend/src/modules/components/BottomPanelTabs.tsx @@ -5,12 +5,14 @@ */ import { useState, useRef, useCallback, useEffect } from 'react' -import { X, AlertCircle, FileText, Zap, Package, Pin, PinOff, ChevronUp, ChevronDown } from 'lucide-react' +import { X, AlertCircle, FileText, Zap, Package, Pin, PinOff, ChevronUp, ChevronDown, FlaskConical } from 'lucide-react' import { useUIStore } from '../../stores/uiStore' +import { useTestStore } from '../../stores/testStore' import BuildOutputPanel from './BuildOutputPanel' import { PackageBuildHistory } from './PackageBuildHistory' import { WorkflowExecutionPanel } from './workflow/WorkflowExecutionPanel' import { PrompdSessionHistory, type PrompdExecutionRecord } from './PrompdSessionHistory' +import { TestResultsPanel } from './testing/TestResultsPanel' import type { WorkflowResult } from '@prompd/cli' import type { CheckpointEvent, ExecutionTrace } from '@prompd/cli' @@ -214,6 +216,14 @@ export function BottomPanelTabs({ Packages + + { + setActiveBottomTab('tests') + if (bottomPanelMinimized) setBottomPanelMinimized(false) + }} + />
@@ -277,10 +287,43 @@ export function BottomPanelTabs({
)} + + {activeBottomTab === 'tests' && ( +
+ +
+ )} )} ) } +/** + * Tests tab button with pass/fail badge from testStore + */ +function TestsTabButton({ active, onClick }: { active: boolean; onClick: () => void }) { + const summary = useTestStore(state => state.summary) + const isRunning = useTestStore(state => state.isRunning) + + return ( + + ) +} + export default BottomPanelTabs diff --git a/frontend/src/modules/components/testing/ProviderModelSelector.tsx b/frontend/src/modules/components/testing/ProviderModelSelector.tsx new file mode 100644 index 0000000..74b292b --- /dev/null +++ b/frontend/src/modules/components/testing/ProviderModelSelector.tsx @@ -0,0 +1,185 @@ +/** + * ProviderModelSelector - Two-step provider/model selector for test execution. + * Step 1: Select providers (checkboxes) + * Step 2: Select models within those providers (checkboxes, grouped) + * + * Uses configured providers from uiStore (only shows providers with API keys). + */ + +import { useMemo } from 'react' +import { CheckSquare, Square, ChevronDown, ChevronRight } from 'lucide-react' +import { useUIStore, type ProviderWithPricing, type ModelWithPricing } from '@/stores/uiStore' + +export interface SelectedModel { + providerId: string + providerName: string + modelId: string + modelName: string +} + +interface ProviderModelSelectorProps { + selectedModels: SelectedModel[] + onSelectionChange: (models: SelectedModel[]) => void + maxSelection?: number +} + +export function ProviderModelSelector({ + selectedModels, + onSelectionChange, + maxSelection = 10, +}: ProviderModelSelectorProps) { + const providersWithPricing = useUIStore(state => state.llmProvider.providersWithPricing) + + // Only show providers that have API keys configured + const configuredProviders = useMemo(() => { + if (!providersWithPricing) return [] + return providersWithPricing.filter(p => p.hasKey) + }, [providersWithPricing]) + + // Which provider IDs have at least one model selected + const activeProviderIds = useMemo(() => { + const ids = new Set() + for (const m of selectedModels) { + ids.add(m.providerId) + } + return ids + }, [selectedModels]) + + const isModelSelected = (providerId: string, modelId: string) => { + return selectedModels.some(m => m.providerId === providerId && m.modelId === modelId) + } + + const toggleModel = (provider: ProviderWithPricing, model: ModelWithPricing) => { + const existing = selectedModels.find( + m => m.providerId === provider.providerId && m.modelId === model.model + ) + if (existing) { + onSelectionChange(selectedModels.filter(m => m !== existing)) + } else { + if (selectedModels.length >= maxSelection) return + onSelectionChange([...selectedModels, { + providerId: provider.providerId, + providerName: provider.displayName, + modelId: model.model, + modelName: model.displayName, + }]) + } + } + + const toggleAllModels = (provider: ProviderWithPricing) => { + const providerModels = selectedModels.filter(m => m.providerId === provider.providerId) + if (providerModels.length === provider.models.length) { + // Deselect all models for this provider + onSelectionChange(selectedModels.filter(m => m.providerId !== provider.providerId)) + } else { + // Select all models for this provider (up to max) + const others = selectedModels.filter(m => m.providerId !== provider.providerId) + const allForProvider = provider.models.map(model => ({ + providerId: provider.providerId, + providerName: provider.displayName, + modelId: model.model, + modelName: model.displayName, + })) + const combined = [...others, ...allForProvider].slice(0, maxSelection) + onSelectionChange(combined) + } + } + + if (configuredProviders.length === 0) { + return ( +
+ No providers configured. Add API keys in Settings. +
+ ) + } + + return ( +
+ {/* Selected count */} + {selectedModels.length > 0 && ( +
+ {selectedModels.length} model{selectedModels.length !== 1 ? 's' : ''} selected + +
+ )} + + {/* Provider list with nested models */} +
+ {configuredProviders.map(provider => { + const isActive = activeProviderIds.has(provider.providerId) + const selectedCount = selectedModels.filter(m => m.providerId === provider.providerId).length + const allSelected = selectedCount === provider.models.length + + return ( +
+ {/* Provider row */} +
toggleAllModels(provider)} + > + + {allSelected ? ( + + ) : selectedCount > 0 ? ( + + ) : ( + + )} + + {provider.displayName} + {selectedCount > 0 && ( + {selectedCount}/{provider.models.length} + )} + + {isActive || selectedCount > 0 ? : } + +
+ + {/* Model rows (show when provider has selections or is expanded) */} + {(isActive || selectedCount > 0 || true) && ( +
+ {provider.models.map(model => { + const selected = isModelSelected(provider.providerId, model.model) + const atLimit = !selected && selectedModels.length >= maxSelection + + return ( +
!atLimit && toggleModel(provider, model)} + > + + {selected ? ( + + ) : ( + + )} + + {model.displayName} + {model.inputPrice !== null && model.inputPrice !== undefined && ( + + ${model.inputPrice.toFixed(2)}/M + + )} +
+ ) + })} +
+ )} +
+ ) + })} +
+
+ ) +} diff --git a/frontend/src/modules/components/testing/TestExplorerPanel.tsx b/frontend/src/modules/components/testing/TestExplorerPanel.tsx new file mode 100644 index 0000000..94d299b --- /dev/null +++ b/frontend/src/modules/components/testing/TestExplorerPanel.tsx @@ -0,0 +1,271 @@ +/** + * TestExplorerPanel - Sidebar panel for test discovery and management. + * Shows .prmd files with colocated .test.prmd sidecars and their status. + */ + +import { useEffect, useCallback, useMemo } from 'react' +import { Play, RefreshCw, Ban, Square } from 'lucide-react' +import { useTestStore, type TestSuiteInfo } from '@/stores/testStore' +import { useEditorStore } from '@/stores/editorStore' +import { useUIStore } from '@/stores/uiStore' +import { SidebarPanelHeader } from '../SidebarPanelHeader' +import { TestFileItem } from './TestTreeItem' +import { ProviderModelSelector } from '../ProviderModelSelector' + +/** + * Group suites by relative directory, sorted alphabetically. + */ +function groupByDirectory( + suites: TestSuiteInfo[], + workspacePath: string +): Array<{ dir: string; suites: TestSuiteInfo[] }> { + const groups = new Map() + + for (const suite of suites) { + let rel = suite.targetPath.replace(/\\/g, '/') + if (workspacePath) { + const wp = workspacePath.replace(/\\/g, '/') + if (rel.startsWith(wp)) { + rel = rel.slice(wp.length).replace(/^\//, '') + } + } + const parts = rel.split('/') + const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : '' + + if (!groups.has(dir)) groups.set(dir, []) + groups.get(dir)!.push(suite) + } + + // Sort groups by directory name, sort suites within each group + return Array.from(groups.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([dir, dirSuites]) => ({ + dir, + suites: dirSuites.sort((a, b) => a.name.localeCompare(b.name)), + })) +} + +export function TestExplorerPanel() { + const discoveredSuites = useTestStore(state => state.discoveredSuites) + const isDiscovering = useTestStore(state => state.isDiscovering) + const isRunning = useTestStore(state => state.isRunning) + const expandedFiles = useTestStore(state => state.expandedFiles) + const discover = useTestStore(state => state.discover) + const clearDiscovered = useTestStore(state => state.clearDiscovered) + const runTests = useTestStore(state => state.runTests) + const runAll = useTestStore(state => state.runAll) + const stopTests = useTestStore(state => state.stopTests) + const toggleExpanded = useTestStore(state => state.toggleExpanded) + + const workspacePath = useEditorStore(state => state.explorerDirPath) + + // Auto-discover on mount and workspace change, clear on workspace close + useEffect(() => { + if (workspacePath) { + discover(workspacePath) + } else { + clearDiscovered() + } + }, [workspacePath, discover, clearDiscovered]) + + const groups = useMemo( + () => groupByDirectory(discoveredSuites, workspacePath || ''), + [discoveredSuites, workspacePath] + ) + + const handleRunAll = useCallback(() => { + if (workspacePath && !isRunning) { + runAll(workspacePath) + } + }, [workspacePath, isRunning, runAll]) + + const handleRunAllNoLlm = useCallback(() => { + if (workspacePath && !isRunning) { + runAll(workspacePath, { noLlm: true }) + } + }, [workspacePath, isRunning, runAll]) + + const handleRunFile = useCallback((targetPath: string) => { + if (!isRunning) { + runTests(targetPath) + } + }, [isRunning, runTests]) + + const handleRefresh = useCallback(() => { + if (workspacePath) { + discover(workspacePath) + } + }, [workspacePath, discover]) + + const providersWithPricing = useUIStore(state => state.llmProvider.providersWithPricing) + const testProvider = useTestStore(state => state.testProvider) + const testModel = useTestStore(state => state.testModel) + const setTestProvider = useTestStore(state => state.setTestProvider) + const setTestModel = useTestStore(state => state.setTestModel) + + // Only providers with API keys configured + const configuredProviders = useMemo(() => { + if (!providersWithPricing) return [] + return providersWithPricing.filter(p => p.hasKey) + }, [providersWithPricing]) + + // Auto-select first configured provider if none selected + useEffect(() => { + if (!testProvider && configuredProviders.length > 0) { + setTestProvider(configuredProviders[0].providerId) + if (configuredProviders[0].models.length > 0) { + setTestModel(configuredProviders[0].models[0].model) + } + } + }, [testProvider, configuredProviders, setTestProvider, setTestModel]) + + // Summary counts + const totalSuites = discoveredSuites.length + const passedCount = discoveredSuites.filter(s => s.lastStatus === 'pass').length + const failedCount = discoveredSuites.filter(s => s.lastStatus === 'fail' || s.lastStatus === 'error').length + + return ( +
+ + + + + {/* Action bar */} +
+ + + {isRunning && ( + + )} +
+ + {/* Provider/Model selector for test execution */} + {configuredProviders.length > 0 && ( +
+ { + setTestProvider(p) + // Auto-select first model for new provider + const prov = configuredProviders.find(pr => pr.providerId === p) + if (prov && prov.models.length > 0) { + setTestModel(prov.models[0].model) + } + }} + onModelChange={setTestModel} + layout="compact" + showPricing={true} + forceDropdown={true} + /> +
+ )} + + {/* Summary bar (only after tests have run) */} + {(passedCount > 0 || failedCount > 0) && ( +
+ {passedCount > 0 && ( + {passedCount} passed + )} + {failedCount > 0 && ( + {failedCount} failed + )} + {totalSuites} total +
+ )} + + {/* Tree */} +
+ {totalSuites === 0 && !isDiscovering && ( +
+ {workspacePath + ? 'No .test.prmd files found.' + : 'Open a workspace to discover tests.'} +
+ )} + + {groups.map(({ dir, suites }) => ( +
+ {dir && ( +
{dir}/
+ )} + {suites.map((suite) => ( + toggleExpanded(suite.testFilePath)} + onRun={handleRunFile} + /> + ))} +
+ ))} +
+
+ ) +} diff --git a/frontend/src/modules/components/testing/TestResultsPanel.tsx b/frontend/src/modules/components/testing/TestResultsPanel.tsx new file mode 100644 index 0000000..32cfd40 --- /dev/null +++ b/frontend/src/modules/components/testing/TestResultsPanel.tsx @@ -0,0 +1,290 @@ +/** + * TestResultsPanel - Test execution results with collapsible file groups + * and expandable test cards. Displayed in the bottom panel "Tests" tab. + */ + +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' +import { Trash2, Download, Square, ChevronRight, ChevronDown, CheckCircle2, XCircle, AlertCircle, MinusCircle } from 'lucide-react' +import { useTestStore, type TestLogEntry, type TestRunSummary } from '@/stores/testStore' + +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + return `${(ms / 60000).toFixed(1)}m` +} + +interface TestGroup { + id: number + entry: TestLogEntry + details: TestLogEntry[] +} + +interface SuiteGroup { + id: number + heading: TestLogEntry + tests: TestGroup[] +} + +/** Group flat log entries into suites → test cards → details */ +function groupLogEntries(log: TestLogEntry[]): Array { + const result: Array = [] + let i = 0 + + while (i < log.length) { + const entry = log[i] + + if (entry.type === 'heading') { + // Start a new suite — collect all test entries until next heading + const suite: SuiteGroup = { id: entry.id, heading: entry, tests: [] } + i++ + + while (i < log.length && log[i].type !== 'heading') { + const testEntry = log[i] + + if (testEntry.type === 'pass' || testEntry.type === 'fail' || testEntry.type === 'error' || testEntry.type === 'skip') { + const details: TestLogEntry[] = [] + let j = i + 1 + while (j < log.length && log[j].type === 'detail') { + details.push(log[j]) + j++ + } + suite.tests.push({ id: testEntry.id, entry: testEntry, details }) + i = j + } else { + i++ + } + } + + result.push(suite) + } else { + result.push(entry) + i++ + } + } + + return result +} + +function statusIcon(type: TestLogEntry['type']) { + switch (type) { + case 'pass': return + case 'fail': return + case 'error': return + case 'skip': return + default: return null + } +} + +function TestCard({ group }: { group: TestGroup }) { + const hasDetails = group.details.length > 0 + const [expanded, setExpanded] = useState(group.entry.type !== 'pass') + + return ( +
+
hasDetails && setExpanded(!expanded)} + style={{ cursor: hasDetails ? 'pointer' : 'default' }} + > + {hasDetails ? ( + expanded + ? + : + ) : ( + + )} + {statusIcon(group.entry.type)} + {group.entry.message} + {group.entry.duration !== undefined && ( + {formatDuration(group.entry.duration)} + )} +
+ {expanded && hasDetails && ( +
+ {group.details.map((detail) => ( +
+ {detail.message} + {detail.duration !== undefined && detail.duration > 0 && ( + {formatDuration(detail.duration)} + )} +
+ ))} +
+ )} +
+ ) +} + +function SuiteSection({ suite }: { suite: SuiteGroup }) { + const [expanded, setExpanded] = useState(true) + + const counts = useMemo(() => { + let passed = 0, failed = 0, errors = 0, skipped = 0 + for (const t of suite.tests) { + switch (t.entry.type) { + case 'pass': passed++; break + case 'fail': failed++; break + case 'error': errors++; break + case 'skip': skipped++; break + } + } + return { passed, failed, errors, skipped, total: suite.tests.length } + }, [suite.tests]) + + const allPassed = counts.failed === 0 && counts.errors === 0 + + return ( +
+
setExpanded(!expanded)} + > + {expanded + ? + : + } + {suite.heading.message} + {!expanded && counts.total > 0 && ( + + + {counts.passed > 0 && {counts.passed} passed} + {counts.failed > 0 && {counts.passed > 0 ? ', ' : ''}{counts.failed} failed} + {counts.errors > 0 && {(counts.passed > 0 || counts.failed > 0) ? ', ' : ''}{counts.errors} errors} + {counts.skipped > 0 && {(counts.passed > 0 || counts.failed > 0 || counts.errors > 0) ? ', ' : ''}{counts.skipped} skipped} + + + )} +
+ {expanded && ( +
+ {suite.tests.map((group) => ( + + ))} +
+ )} +
+ ) +} + +function SummaryLine({ summary }: { summary: TestRunSummary }) { + const allPassed = summary.failed === 0 && summary.errors === 0 + return ( +
+
+ Tests: {summary.passed} passed, {summary.failed} failed + {summary.errors > 0 && `, ${summary.errors} errors`} + {summary.skipped > 0 && `, ${summary.skipped} skipped`} + {' '} | {summary.total} total | Time: {formatDuration(summary.duration)} +
+ {(summary.totalTokens || summary.models) && ( +
+ {summary.models && summary.models.length > 0 && ( + Models: {summary.models.join(', ')} + )} + {summary.totalTokens && ( + {summary.models ? ' | ' : ''}Tokens: {summary.totalTokens.toLocaleString()} + )} +
+ )} +
+ ) +} + +function exportTestResults(log: TestLogEntry[], summary: TestRunSummary | null) { + const data = JSON.stringify({ log, summary }, null, 2) + const blob = new Blob([data], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json` + a.click() + URL.revokeObjectURL(url) +} + +export function TestResultsPanel() { + const log = useTestStore(state => state.log) + const summary = useTestStore(state => state.summary) + const isRunning = useTestStore(state => state.isRunning) + const clearLog = useTestStore(state => state.clearLog) + const stopTests = useTestStore(state => state.stopTests) + + const scrollRef = useRef(null) + const shouldAutoScroll = useRef(true) + + const grouped = useMemo(() => groupLogEntries(log), [log]) + + useEffect(() => { + if (shouldAutoScroll.current && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [log.length]) + + const handleScroll = useCallback(() => { + if (!scrollRef.current) return + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current + shouldAutoScroll.current = scrollHeight - scrollTop - clientHeight < 50 + }, []) + + return ( +
+
+ {isRunning && ( + + )} + + +
+ +
+ {log.length === 0 && !isRunning && ( +
+ No test results yet. Run tests from the Test Explorer or editor toolbar. +
+ )} + + {grouped.map((item) => { + if ('heading' in item) { + return + } + const entry = item as TestLogEntry + return ( +
+ {entry.message} +
+ ) + })} + + {isRunning && ( +
+ + Running... +
+ )} + + {summary && } +
+
+ ) +} diff --git a/frontend/src/modules/components/testing/TestTreeItem.tsx b/frontend/src/modules/components/testing/TestTreeItem.tsx new file mode 100644 index 0000000..0beb134 --- /dev/null +++ b/frontend/src/modules/components/testing/TestTreeItem.tsx @@ -0,0 +1,142 @@ +/** + * TestTreeItem - Recursive tree node for test explorer. + * Renders .prmd files with their test status and expandable test cases. + */ + +import { ChevronRight, ChevronDown, Play, CheckCircle2, XCircle, AlertCircle, MinusCircle, Circle } from 'lucide-react' +import type { TestSuiteInfo, TestCaseResult } from '@/stores/testStore' + +// --- Status rendering --- + +function StatusIcon({ status, size = 14 }: { status?: string; size?: number }) { + switch (status) { + case 'pass': + return + case 'fail': + return + case 'error': + return + case 'skip': + return + default: + return + } +} + +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + return `${(ms / 60000).toFixed(1)}m` +} + +// --- Props --- + +interface TestFileItemProps { + suite: TestSuiteInfo + expanded: boolean + onToggle: () => void + onRun: (targetPath: string) => void +} + +interface TestCaseItemProps { + result?: TestCaseResult + name: string + depth: number +} + +// --- File-level item --- + +export function TestFileItem({ suite, expanded, onToggle, onRun }: TestFileItemProps) { + const fileName = suite.targetPath.replace(/\\/g, '/').split('/').pop() || suite.name + const hasResults = !!suite.lastResults + const hasTests = suite.testCount > 0 + + return ( +
+
+ + {hasTests ? ( + expanded ? : + ) : ( + + )} + + + + + {fileName} + + {hasResults && ( + + {suite.lastResults?.filter(r => r.status === 'pass').length}/{suite.testCount} + + )} + + {!hasResults && hasTests && ( + + {suite.testCount} tests + + )} + + +
+ + {expanded && hasTests && ( +
+ {suite.lastResults ? ( + suite.lastResults.map((result, i) => ( + + )) + ) : ( + suite.testNames.map((name, i) => ( + + )) + )} +
+ )} +
+ ) +} + +// --- Test case item --- + +function TestCaseItem({ result, name, depth }: TestCaseItemProps) { + return ( +
+ + {name} + {result?.duration !== undefined && ( + {formatDuration(result.duration)} + )} + {result?.error && ( + + {result.error.length > 40 ? result.error.substring(0, 40) + '...' : result.error} + + )} +
+ ) +} diff --git a/frontend/src/modules/editor/ActivityBar.tsx b/frontend/src/modules/editor/ActivityBar.tsx index 250abcf..f024ddb 100644 --- a/frontend/src/modules/editor/ActivityBar.tsx +++ b/frontend/src/modules/editor/ActivityBar.tsx @@ -1,7 +1,7 @@ -import { Files, Package, GitBranch, History, FolderOpen, CircleHelp, Library } from 'lucide-react' +import { Files, Package, GitBranch, History, FolderOpen, CircleHelp, Library, FlaskConical } from 'lucide-react' import { PrompdIcon } from '../components/PrompdIcon' -type SideKey = 'explorer' | 'packages' | 'ai' | 'git' | 'history' | 'resources' | 'library' +type SideKey = 'explorer' | 'packages' | 'ai' | 'git' | 'history' | 'resources' | 'library' | 'tests' type Props = { showSidebar: boolean @@ -106,6 +106,16 @@ export default function ActivityBar({ showSidebar, active, onSelect, onToggleSid color={active === 'library' && showSidebar ? iconColor : inactiveIconColor} /> + {helpEnabled && ( <> diff --git a/frontend/src/modules/editor/EditorHeader.tsx b/frontend/src/modules/editor/EditorHeader.tsx index f97b2d9..fb25eea 100644 --- a/frontend/src/modules/editor/EditorHeader.tsx +++ b/frontend/src/modules/editor/EditorHeader.tsx @@ -1,4 +1,4 @@ -import { Code2, Palette, Settings, Play, Square, LogOut, User, Moon, Sun, HelpCircle, KeyRound } from 'lucide-react' +import { Code2, Palette, Settings, Play, Square, LogOut, User, Moon, Sun, HelpCircle, KeyRound, FlaskConical } from 'lucide-react' import { useState, useEffect, useRef } from 'react' import { useShallow } from 'zustand/react/shallow' import { PreviewToggle, ChatToggle } from './SplitViewToggles' @@ -298,6 +298,7 @@ type Props = { onTogglePreview?: () => void // Toggle split preview showChat?: boolean // Whether chat pane is shown in split view onToggleChat?: () => void // Toggle chat pane + onRunTests?: () => void // Run tests for current .prmd file } export default function EditorHeader({ @@ -315,7 +316,8 @@ export default function EditorHeader({ showPreview = false, onTogglePreview, showChat = false, - onToggleChat + onToggleChat, + onRunTests }: Props) { const { isAuthenticated, isLoaded, getToken, email } = useAuthenticatedUser() const headerRef = useRef(null) @@ -559,6 +561,36 @@ export default function EditorHeader({ )} + {/* Run Tests button - show only for .test.prmd files */} + {isPrompdFile && onRunTests && ( + + )} {/* Show for .pdflow workflow files — play/continue + stop */} {isWorkflowFile && onExecuteWorkflow && (
diff --git a/frontend/src/modules/editor/FileExplorer.tsx b/frontend/src/modules/editor/FileExplorer.tsx index 79bb1db..c3117ca 100644 --- a/frontend/src/modules/editor/FileExplorer.tsx +++ b/frontend/src/modules/editor/FileExplorer.tsx @@ -23,7 +23,8 @@ import { Download, RefreshCw, Sparkles, - Lightbulb + Lightbulb, + FlaskConical } from 'lucide-react' import { SidebarPanelHeader } from '../components/SidebarPanelHeader' import { useConfirmDialog } from '../components/ConfirmDialog' @@ -1399,6 +1400,144 @@ export default function FileExplorer({ currentFileName, onOpenFile, onCreateNewP )} + {/* Create Test File - only for .prmd files (not .test.prmd) */} + {contextMenu.entry.name.endsWith('.prmd') && !contextMenu.entry.name.endsWith('.test.prmd') && ( + <> +
+
{ + try { + const entry = entries.find(e => e.path === contextMenu.entry.path) + const electronPath = (dirHandle as FileSystemDirectoryHandle & { _electronPath?: string })?._electronPath + if (!electronPath) { + setContextMenu(null) + return + } + + const sourceName = contextMenu.entry.name + const testName = sourceName.replace(/\.prmd$/, '.test.prmd') + const sourceDir = contextMenu.entry.path.includes('/') + ? contextMenu.entry.path.split('/').slice(0, -1).join('/') + : '' + const testPath = sourceDir + ? `${electronPath}/${sourceDir}/${testName}`.replace(/\\/g, '/') + : `${electronPath}/${testName}`.replace(/\\/g, '/') + + // Check if test file already exists + if (window.electronAPI?.isElectron) { + const existing = await window.electronAPI.readFile(testPath) + if (existing?.success) { + // Already exists — just open it + openPath(sourceDir ? `${sourceDir}/${testName}` : testName) + setContextMenu(null) + return + } + } + + // Read the source .prmd to extract id, name, and parameters + let promptName = sourceName.replace(/\.prmd$/, '') + let promptId = sourceName.replace(/\.prmd$/, '') + let paramsBlock = '' + const fullSourcePath = sourceDir + ? `${electronPath}/${sourceDir}/${sourceName}`.replace(/\\/g, '/') + : `${electronPath}/${sourceName}`.replace(/\\/g, '/') + + if (window.electronAPI?.isElectron) { + const sourceResult = await window.electronAPI.readFile(fullSourcePath) + if (sourceResult?.success && sourceResult.content) { + const content = sourceResult.content.replace(/\r\n/g, '\n') + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/) + if (fmMatch) { + const idMatch = fmMatch[1].match(/^id:\s*["']?(.+?)["']?\s*$/m) + if (idMatch) promptId = idMatch[1] + const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m) + if (nameMatch) promptName = nameMatch[1] + // Extract parameter names for scaffold + const paramMatches = [...fmMatch[1].matchAll(/- name:\s*["']?(.+?)["']?\s*$/gm)] + if (paramMatches.length > 0) { + const paramLines = paramMatches.map(m => ` ${m[1]}: ""`) + paramsBlock = ` params:\n${paramLines.join('\n')}` + } + } + } + } + + if (!paramsBlock) { + paramsBlock = ' params: {}' + } + + // Generate scaffold content + const scaffold = [ + '---', + `id: ${promptId}-test`, + `name: ${promptName}.test`, + 'version: 0.0.1', + `description: "Tests for ${promptName}"`, + `target: ./${sourceName}`, + 'tests:', + ' - name: "basic output"', + paramsBlock, + ' assert:', + ' - evaluator: nlp', + ' evaluate: response # response | prompt | both - required for nlp', + ' check: min_tokens', + ' value: 1', + ' # - evaluator: nlp', + ' # evaluate: prompt', + ' # check: contains', + ' # value: "expected text"', + ' # - evaluator: script', + ' # script: "./test/eval-script.js"', + ' # evaluate: both # defaults for prmd', + ' # - evaluator: prmd', + ' # evaluate: both # defaults for prmd', + ' # prompt: "@prompd/eval-coherence@^1.0.0"', + '', + ' # - name: "expected error"', + ' # params: {}', + ' # expect_error: true', + '---', + '', + '# Evaluator', + '', + '', + '', + ].join('\n') + + if (window.electronAPI?.isElectron) { + const result = await window.electronAPI.writeFile(testPath, scaffold) + if (result?.success) { + await refresh() + openPath(sourceDir ? `${sourceDir}/${testName}` : testName) + } + } + } catch (err) { + console.error('Failed to create test file:', err) + } + setContextMenu(null) + }}> + + Create Test File +
+ + )} + {/* Install Dependencies - only for prompd.json files */} {onInstallDependencies && contextMenu.entry.name === 'prompd.json' && ( <> @@ -1820,6 +1959,20 @@ function iconFor(name: string, isIgnored = false) { const ignoredColor = '#6b7280' // Grey color for ignored files // Prompd-specific files with custom SVG icons + if (lower.endsWith('.test.prmd')) { + return ( + test.prmd + ) + } if (lower.endsWith('.prmd')) { return ( state.summary) + const isTestRunning = useTestStore(state => state.isRunning) + const setActiveBottomTab = useUIStore(state => state.setActiveBottomTab) + const setShowBottomPanel = useUIStore(state => state.setShowBottomPanel) const handleIssuesClick = () => { if (issuesCount > 0 && onIssuesClick) { onIssuesClick() @@ -52,6 +58,38 @@ export default function StatusBar({ fileName, dirty, line, column, issuesCount, )} + {(testSummary || isTestRunning) && ( + + )} {language &&
{language.charAt(0).toUpperCase() + language.slice(1)}
}
= { + 'prompt': { + title: 'Compiled Prompt', + desc: 'The compiled prompt that was sent to the LLM. Contains the fully resolved .prmd output with all parameters substituted.', + usage: '{{ prompt }}' + }, + 'response': { + title: 'LLM Response', + desc: 'The response returned by the LLM being evaluated. This is the text your evaluator should assess.', + usage: '{{ response }}' + }, + 'params': { + title: 'Test Parameters', + desc: 'The parameters object from the test case. Access individual values with dot notation.', + usage: '{{ params }} or {{ params.name }}' + } + } + + if (testEvalBuiltins[paramName]) { + const bi = testEvalBuiltins[paramName] + const contents: monacoEditor.IMarkdownString[] = [] + contents.push({ value: `**Test Evaluator: \`${paramName}\`**` }) + contents.push({ value: bi.desc }) + contents.push({ value: '---' }) + contents.push({ value: `**Usage:** \`${bi.usage}\`` }) + contents.push({ value: 'Available in the content block of `.test.prmd` files when used as an evaluator prompt.' }) + return { range, contents } + } + if (parameters.includes(paramName)) { // Try to find parameter definition in frontmatter (handle CRLF) const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) diff --git a/frontend/src/modules/lib/intellisense/validation.ts b/frontend/src/modules/lib/intellisense/validation.ts index 6b87969..3be934c 100644 --- a/frontend/src/modules/lib/intellisense/validation.ts +++ b/frontend/src/modules/lib/intellisense/validation.ts @@ -55,7 +55,10 @@ export function setModelFilePath(modelUri: string, filePath: string | null) { * Falls back to the singleton currentFilePath if no mapping exists. */ export function getModelFilePath(modelUri: string): string | null { - return modelFilePathMap.get(modelUri) ?? currentFilePath + // Use per-model mapping only — do NOT fall back to currentFilePath + // Falling back causes cross-tab contamination where validation for one model + // uses the file path of the last-activated tab (off-by-one / stale state) + return modelFilePathMap.get(modelUri) ?? null } /** @@ -1405,12 +1408,17 @@ export async function validateModel( } // Check if id matches the filename (without extension) - // Derive filename from model-specific file path map, falling back to singleton + // Skip for .test.prmd files — they use a different frontmatter schema const modelFilePath = getModelFilePath(model.uri.toString()) if (modelFilePath) { const fileName = modelFilePath.replace(/\\/g, '/').split('/').pop() || '' + if (fileName.endsWith('.test.prmd')) { + // .test.prmd files are test definitions, not prompts — skip id validation + } else { const fileBaseName = fileName.replace(/\.prmd$/i, '') - if (fileBaseName && id !== fileBaseName) { + // Normalize dots and hyphens for comparison (hello-world == hello.world) + const normalizeId = (s: string) => s.toLowerCase().replace(/[.\-]/g, '') + if (fileBaseName && normalizeId(id) !== normalizeId(fileBaseName)) { markers.push({ severity: monaco.MarkerSeverity.Warning, startLineNumber: lineNumber, @@ -1421,6 +1429,7 @@ export async function validateModel( code: 'id-filename-mismatch' }) } + } } } @@ -1611,6 +1620,17 @@ export async function validateModel( definedParams.add('previous_step') // Alias for previous_output definedParams.add('input') // Alias for previous_output in code/transform nodes + // Test evaluator variables (injected by @prompd/test when running .test.prmd evaluator prompts) + // Check both the model file path mapping AND the model URI (which contains the filename) + const testFilePath = getModelFilePath(model.uri.toString()) + const modelUriStr = model.uri.toString() + const isTestFile = testFilePath?.endsWith('.test.prmd') || modelUriStr.endsWith('.test.prmd') + if (isTestFile) { + definedParams.add('prompt') // The compiled prompt that was sent to the LLM + definedParams.add('response') // The LLM's response + definedParams.add('params') // Test case parameters ({{ params.key }}) + } + // Add inherited parameters so they're recognized as defined for (const inheritedParam of resolvedInheritedParams) { definedParams.add(inheritedParam.name) @@ -1745,8 +1765,11 @@ export async function validateModel( } // Cross-reference analysis for parameter usage (unused/undefined parameters) - // Reuses the inherited parameters already resolved above for the single-brace validator - if (isPrompdFile) { + // Skip for .test.prmd files — they use a different frontmatter schema (tests: not params in body) + const uriString = model.uri.toString() + const skipCrossRef = uriString.endsWith('.test.prmd') || getModelFilePath(uriString)?.endsWith('.test.prmd') || content.includes('\ntests:') || content.includes('\ntarget:') + if (skipCrossRef) console.log('[intellisense] Skipping cross-ref and compiler diagnostics for .test.prmd') + if (isPrompdFile && !skipCrossRef) { try { // Reuse inherited params resolved earlier in the single-brace validation block const inheritedDefs = resolvedInheritedParams.length > 0 ? resolvedInheritedParams : undefined @@ -1758,7 +1781,8 @@ export async function validateModel( } // Fetch compiler diagnostics (inheritance errors, dependency resolution, etc.) - if (isPrompdFile) { + // Skip for .test.prmd files — they have a different schema, the CLI compiler doesn't understand them + if (isPrompdFile && !skipCrossRef) { try { const compilerDiagnostics = await fetchCompilerDiagnostics(content) diff --git a/frontend/src/stores/testStore.ts b/frontend/src/stores/testStore.ts new file mode 100644 index 0000000..ee3f7f3 --- /dev/null +++ b/frontend/src/stores/testStore.ts @@ -0,0 +1,409 @@ +/** + * Test Store + * Manages test discovery, execution, and results state. + * Not persisted — test results are transient (like workflow execution state). + */ + +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' +import { useUIStore } from './uiStore' + +// --- Types --- + +export interface TestSuiteInfo { + name: string + description?: string + testFilePath: string + targetPath: string + testCount: number + testNames: string[] + lastStatus?: 'pass' | 'fail' | 'error' | 'pending' + lastResults?: TestCaseResult[] + lastRunTime?: number +} + +export interface TestCaseResult { + testName: string + status: 'pass' | 'fail' | 'error' | 'skip' + duration: number + assertions: AssertionResultInfo[] + output?: string + error?: string +} + +export interface AssertionResultInfo { + evaluator: 'nlp' | 'script' | 'prmd' + check?: string + status: 'pass' | 'fail' | 'error' | 'skip' + reason?: string + duration: number +} + +export interface TestLogEntry { + id: number + timestamp: number + type: 'info' | 'pass' | 'fail' | 'error' | 'skip' | 'heading' | 'detail' | 'summary' + message: string + detail?: string + duration?: number +} + +export interface TestRunSummary { + total: number + passed: number + failed: number + errors: number + skipped: number + duration: number + totalTokens?: number + providers?: string[] + models?: string[] +} + +// --- Store interface --- + +interface TestStoreState { + // Discovery + discoveredSuites: TestSuiteInfo[] + isDiscovering: boolean + + // Execution + isRunning: boolean + currentRunId: string | null + activeTarget: string | null + + // Test provider/model (independent from editor's provider/model) + testProvider: string + testModel: string + + // Results + log: TestLogEntry[] + summary: TestRunSummary | null + + // Tree state + expandedFiles: string[] + + // Actions + setTestProvider: (provider: string) => void + setTestModel: (model: string) => void + discover: (directory: string) => Promise + runTests: (target: string, options?: Record) => Promise + runAll: (directory: string, options?: Record) => Promise + stopTests: () => void + handleProgressEvent: (data: { runId: string; event: ProgressEventData }) => void + toggleExpanded: (filePath: string) => void + clearLog: () => void + clearDiscovered: () => void +} + +interface ProgressEventData { + type: 'suite_start' | 'test_start' | 'test_complete' | 'suite_complete' | 'assertion_complete' + suite: string + testName?: string + testCount?: number + result?: { + suite: string + testName: string + status: 'pass' | 'fail' | 'error' | 'skip' + duration: number + assertions: AssertionResultInfo[] + output?: string + error?: string + } + results?: unknown[] + assertion?: AssertionResultInfo +} + +let logIdCounter = 0 + +function nextLogId(): number { + return ++logIdCounter +} + +export const useTestStore = create()( + immer((set, get) => ({ + // Initial state + discoveredSuites: [], + isDiscovering: false, + isRunning: false, + currentRunId: null, + activeTarget: null, + testProvider: '', + testModel: '', + log: [], + summary: null, + expandedFiles: [], + + setTestProvider: (provider) => set((s) => { s.testProvider = provider }), + setTestModel: (model) => set((s) => { s.testModel = model }), + + discover: async (directory) => { + if (!window.electronAPI?.test) return + + set((state) => { + state.isDiscovering = true + }) + + try { + const result = await window.electronAPI.test.discover(directory) + + set((state) => { + state.isDiscovering = false + if (result.success) { + // Merge with existing status data + const existing = new Map(state.discoveredSuites.map(s => [s.testFilePath, s])) + state.discoveredSuites = result.suites.map(s => { + const prev = existing.get(s.testFilePath) + return { + ...s, + lastStatus: prev?.lastStatus, + lastResults: prev?.lastResults, + lastRunTime: prev?.lastRunTime, + } + }) + } + }) + } catch { + set((state) => { + state.isDiscovering = false + }) + } + }, + + runTests: async (target, options = {}) => { + if (!window.electronAPI?.test) return + const state = get() + if (state.isRunning) return + + // Inject test provider/model into options (overridable by .prmd frontmatter) + const runOptions = { ...options } + if (state.testProvider) runOptions.provider = state.testProvider + if (state.testModel) runOptions.model = state.testModel + + set((s) => { + s.isRunning = true + s.activeTarget = target + s.log = [] + s.summary = null + }) + + // Open bottom panel to tests tab + useUIStore.getState().setActiveBottomTab('tests') + useUIStore.getState().setBottomPanelMinimized(false) + + try { + const result = await window.electronAPI.test.run(target, runOptions) + + set((s) => { + s.isRunning = false + s.currentRunId = null + + if (result.success && result.result) { + s.summary = result.result.summary + + // Update suite status from results + for (const suiteResult of result.result.suites) { + const suite = s.discoveredSuites.find( + ds => ds.name === suiteResult.suite + ) + if (suite) { + const hasFail = suiteResult.results.some(r => r.status === 'fail') + const hasError = suiteResult.results.some(r => r.status === 'error') + suite.lastStatus = hasError ? 'error' : hasFail ? 'fail' : 'pass' + suite.lastResults = suiteResult.results.map(r => ({ + testName: r.testName, + status: r.status, + duration: r.duration, + assertions: r.assertions, + output: r.output, + error: r.error, + })) + suite.lastRunTime = Date.now() + } + } + } + }) + } catch { + set((s) => { + s.isRunning = false + s.currentRunId = null + }) + } + }, + + runAll: async (directory, options = {}) => { + if (!window.electronAPI?.test) return + const state = get() + if (state.isRunning) return + + const runOptions = { ...options } + if (state.testProvider) runOptions.provider = state.testProvider + if (state.testModel) runOptions.model = state.testModel + + set((s) => { + s.isRunning = true + s.activeTarget = directory + s.log = [] + s.summary = null + }) + + useUIStore.getState().setActiveBottomTab('tests') + useUIStore.getState().setBottomPanelMinimized(false) + + try { + const result = await window.electronAPI.test.runAll(directory, runOptions) + + set((s) => { + s.isRunning = false + s.currentRunId = null + + if (result.success && result.result) { + s.summary = result.result.summary + + for (const suiteResult of result.result.suites) { + const suite = s.discoveredSuites.find( + ds => ds.name === suiteResult.suite + ) + if (suite) { + const hasFail = suiteResult.results.some(r => r.status === 'fail') + const hasError = suiteResult.results.some(r => r.status === 'error') + suite.lastStatus = hasError ? 'error' : hasFail ? 'fail' : 'pass' + suite.lastResults = suiteResult.results.map(r => ({ + testName: r.testName, + status: r.status, + duration: r.duration, + assertions: r.assertions, + output: r.output, + error: r.error, + })) + suite.lastRunTime = Date.now() + } + } + } + }) + } catch { + set((s) => { + s.isRunning = false + s.currentRunId = null + }) + } + }, + + stopTests: () => { + const state = get() + if (state.currentRunId && window.electronAPI?.test) { + window.electronAPI.test.stop(state.currentRunId) + } + set((s) => { + s.isRunning = false + s.currentRunId = null + }) + }, + + handleProgressEvent: (data) => { + const { event } = data + + set((s) => { + if (!s.currentRunId) { + s.currentRunId = data.runId + } + + const now = Date.now() + + switch (event.type) { + case 'suite_start': + s.log.push({ + id: nextLogId(), + timestamp: now, + type: 'heading', + message: `Running ${event.suite} (${event.testCount} tests)...`, + }) + break + + case 'test_start': + // No log entry for start — wait for completion + break + + case 'test_complete': { + const r = event.result + if (!r) break + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const exec = (r as any).execution as { provider?: string; model?: string; usage?: { totalTokens?: number } } | undefined + const metaParts: string[] = [] + if (exec?.provider && exec.provider !== 'none') metaParts.push(`${exec.provider}/${exec.model || '?'}`) + if (exec?.usage?.totalTokens) metaParts.push(`${exec.usage.totalTokens} tokens`) + const metaSuffix = metaParts.length > 0 ? ` [${metaParts.join(', ')}]` : '' + + s.log.push({ + id: nextLogId(), + timestamp: now, + type: r.status === 'pass' ? 'pass' : r.status === 'fail' ? 'fail' : r.status === 'skip' ? 'skip' : 'error', + message: `${r.testName}${metaSuffix}`, + duration: r.duration, + detail: r.error, + }) + + // Add error message if present + if (r.error) { + s.log.push({ + id: nextLogId(), + timestamp: now, + type: 'detail', + message: ` ${r.error}`, + }) + } + + // Add assertion details for failures/errors + if (r.status !== 'pass') { + for (const a of r.assertions) { + if (a.status !== 'pass') { + s.log.push({ + id: nextLogId(), + timestamp: now, + type: 'detail', + message: ` ${a.evaluator}${a.check ? `(${a.check})` : ''}: ${a.reason || a.status}`, + duration: a.duration, + }) + } + } + } + break + } + + case 'suite_complete': + // Summary is handled when the full run completes + break + + case 'assertion_complete': + // Logged inline with test_complete for cleaner output + break + } + }) + }, + + toggleExpanded: (filePath) => { + set((s) => { + const idx = s.expandedFiles.indexOf(filePath) + if (idx >= 0) { + s.expandedFiles.splice(idx, 1) + } else { + s.expandedFiles.push(filePath) + } + }) + }, + + clearLog: () => { + set((s) => { + s.log = [] + s.summary = null + }) + }, + + clearDiscovered: () => { + set((s) => { + s.discoveredSuites = [] + s.expandedFiles = [] + }) + }, + })) +) diff --git a/frontend/src/stores/types.ts b/frontend/src/stores/types.ts index 241601b..abb36b8 100644 --- a/frontend/src/stores/types.ts +++ b/frontend/src/stores/types.ts @@ -70,7 +70,7 @@ export interface FileSystemEntry { /** * UI State for sidebar */ -export type SidebarPanel = 'explorer' | 'packages' | 'ai' | 'git' | 'history' | 'resources' | 'library' +export type SidebarPanel = 'explorer' | 'packages' | 'ai' | 'git' | 'history' | 'resources' | 'library' | 'tests' /** * Modal types diff --git a/frontend/src/stores/uiStore.ts b/frontend/src/stores/uiStore.ts index 6fb8542..26eabe5 100644 --- a/frontend/src/stores/uiStore.ts +++ b/frontend/src/stores/uiStore.ts @@ -160,7 +160,7 @@ interface UIState { // Bottom panel (unified tab interface) showBottomPanel: boolean - activeBottomTab: 'errors' | 'prompds' | 'workflows' | 'packages' | 'output' + activeBottomTab: 'errors' | 'prompds' | 'workflows' | 'packages' | 'output' | 'tests' bottomPanelHeight: number bottomPanelPinned: boolean bottomPanelMinimized: boolean @@ -258,7 +258,7 @@ interface UIActions { // Bottom panel (unified) setShowBottomPanel: (show: boolean) => void - setActiveBottomTab: (tab: 'errors' | 'prompds' | 'workflows' | 'packages' | 'output') => void + setActiveBottomTab: (tab: 'errors' | 'prompds' | 'workflows' | 'packages' | 'output' | 'tests') => void setBottomPanelHeight: (height: number) => void setBottomPanelPinned: (pinned: boolean) => void setBottomPanelMinimized: (minimized: boolean) => void diff --git a/frontend/src/styles/styles.css b/frontend/src/styles/styles.css index 7a5d722..1d242e7 100644 --- a/frontend/src/styles/styles.css +++ b/frontend/src/styles/styles.css @@ -2093,6 +2093,589 @@ select.input { overflow: auto; } +.bottom-panel-tab-badge.success { + background: #10b981; +} + +/* ============================================================ + Test Explorer (Sidebar) + Test Results (Bottom Panel) + ============================================================ */ + +/* --- Test Explorer Action Bar --- */ + +.te-action-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + background: transparent; + border: 1px solid var(--border); + color: var(--muted); + font-size: 11px; + cursor: pointer; + border-radius: 6px; + transition: all 0.15s; +} + +.te-action-btn:hover { + background: var(--hover); + color: var(--foreground); + border-color: var(--accent); +} + +.te-action-btn:disabled { + opacity: 0.35; + cursor: default; +} + +.te-action-btn:disabled:hover { + background: transparent; + color: var(--muted); + border-color: var(--border); +} + +.te-action-stop { + color: #ef4444; + border-color: rgba(239, 68, 68, 0.4); +} + +.te-action-stop:hover { + background: rgba(239, 68, 68, 0.08); + color: #ef4444; + border-color: #ef4444; +} + +/* --- Test Explorer Directory Labels --- */ + +.te-dir-label { + padding: 6px 12px 2px; + font-size: 11px; + font-weight: 500; + color: var(--muted); + text-transform: none; + letter-spacing: 0.2px; + user-select: none; +} + +/* --- File-level tree items --- */ + +.te-file-group { + margin-bottom: 1px; +} + +.te-file-item { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 8px; + cursor: pointer; + font-size: 13px; + line-height: 22px; + white-space: nowrap; + overflow: hidden; + border-radius: 4px; + margin: 0 4px; + transition: background 0.12s; + user-select: none; + border: 1px solid transparent; +} + +.te-file-item:hover { + background: var(--hover); + border-color: var(--accent); +} + +.te-file-item:active { + transform: scale(0.99); +} + +.te-chevron { + display: flex; + align-items: center; + flex-shrink: 0; + color: var(--muted); + width: 14px; +} + +.te-file-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + color: var(--foreground); + font-weight: 450; +} + +.te-file-count { + flex-shrink: 0; + font-size: 11px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +.te-file-count-pending { + opacity: 0.5; +} + +.te-run-btn { + display: none; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: transparent; + border: none; + color: var(--muted); + cursor: pointer; + border-radius: 4px; + flex-shrink: 0; + transition: all 0.12s; +} + +.te-file-item:hover .te-run-btn { + display: flex; +} + +.te-run-btn:hover { + background: rgba(16, 185, 129, 0.1); + color: #10b981; +} + +/* --- Test case items (children) --- */ + +.te-cases { + padding: 2px 0 4px; +} + +.te-case-item { + display: flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + font-size: 12px; + line-height: 20px; + color: var(--muted); + white-space: nowrap; + overflow: hidden; + user-select: none; + border-radius: 3px; + margin: 0 4px; + transition: background 0.1s; +} + +.te-case-item:hover { + background: var(--hover); +} + +.te-case-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +.te-case-duration { + flex-shrink: 0; + font-size: 10px; + color: var(--muted); + opacity: 0.7; + font-variant-numeric: tabular-nums; +} + +.te-case-error { + flex-shrink: 1; + font-size: 10px; + color: #ef4444; + overflow: hidden; + text-overflow: ellipsis; + max-width: 120px; + opacity: 0.8; +} + +/* --- Provider/Model Selector (Test Explorer) --- */ + +.pms-container { + font-size: 12px; +} + +.pms-summary { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 4px 6px; + font-size: 11px; + color: var(--muted); +} + +.pms-clear { + background: none; + border: none; + color: var(--accent); + font-size: 11px; + cursor: pointer; + padding: 0 4px; +} + +.pms-clear:hover { + text-decoration: underline; +} + +.pms-providers { + display: flex; + flex-direction: column; + gap: 2px; +} + +.pms-provider-group { + border-radius: 6px; + overflow: hidden; +} + +.pms-provider-row { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 6px; + cursor: pointer; + border-radius: 4px; + transition: background 0.12s; + user-select: none; +} + +.pms-provider-row:hover { + background: var(--hover); +} + +.pms-check { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.pms-provider-name { + flex: 1; + font-weight: 500; + color: var(--foreground); +} + +.pms-provider-count { + font-size: 10px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +.pms-chevron { + display: flex; + align-items: center; + color: var(--muted); + flex-shrink: 0; +} + +.pms-models { + padding: 0 0 2px 20px; +} + +.pms-model-row { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 6px; + cursor: pointer; + border-radius: 3px; + transition: background 0.1s; + user-select: none; +} + +.pms-model-row:hover { + background: var(--hover); +} + +.pms-model-selected { + background: rgba(99, 102, 241, 0.06); +} + +.pms-model-disabled { + opacity: 0.35; + cursor: default; +} + +.pms-model-disabled:hover { + background: transparent; +} + +.pms-model-name { + flex: 1; + color: var(--foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pms-model-price { + flex-shrink: 0; + font-size: 10px; + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +/* --- Test Results Panel (Bottom Panel Tab) --- */ + +.test-results-panel { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + font-family: var(--font-mono, 'SF Mono', 'Fira Code', monospace); + font-size: 12px; +} + +.test-results-toolbar { + display: flex; + gap: 2px; + padding: 4px 8px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.test-toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-radius: 3px; + transition: all 0.15s; +} + +.test-toolbar-btn:hover { + background: var(--panel-3); + color: var(--text); +} + +.test-results-log { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 4px 0; +} + +.test-results-empty { + padding: 16px; + color: var(--text-secondary); + text-align: center; + font-family: inherit; +} + +/* --- Log Entries (standalone: headings, info) --- */ + +.test-log-entry { + display: flex; + align-items: baseline; + gap: 8px; + padding: 1px 12px; + line-height: 20px; + white-space: pre-wrap; + word-break: break-word; +} + +.test-log-message { + flex: 1; + min-width: 0; +} + +.test-status-heading { + padding-top: 8px; + padding-bottom: 4px; +} + +.test-status-heading .test-log-message { + color: var(--text); + font-weight: 600; +} + +.test-status-info .test-log-message { + color: var(--text-secondary); +} + +/* --- Suite Groups (collapsible by file) --- */ + +.test-suite { + margin: 2px 0; +} + +.test-suite-header { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + cursor: pointer; + user-select: none; +} + +.test-suite-header:hover { + background: var(--panel-2); +} + +.test-suite-chevron { + flex-shrink: 0; + color: var(--text-secondary); +} + +.test-suite-name { + font-weight: 600; + color: var(--text); + flex: 1; + min-width: 0; +} + +.test-suite-counts { + flex-shrink: 0; + font-size: 11px; + font-weight: 400; + padding-left: 8px; +} + +.test-count-pass { color: #10b981; } +.test-count-fail { color: #ef4444; } +.test-count-error { color: #f59e0b; } +.test-count-skip { color: var(--text-secondary); } + +.test-suite-body { + padding-left: 8px; +} + +/* --- Test Cards (expandable) --- */ + +.test-card { + margin: 1px 8px; + border-radius: 4px; + overflow: hidden; + border-left: 3px solid transparent; +} + +.test-card-pass { border-left-color: #10b981; } +.test-card-fail { border-left-color: #ef4444; } +.test-card-error { border-left-color: #f59e0b; } +.test-card-skip { border-left-color: var(--text-secondary); } + +.test-card-header { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + line-height: 20px; + user-select: none; +} + +.test-card-header:hover { + background: var(--panel-2); +} + +.test-card-chevron { + flex-shrink: 0; + color: var(--text-secondary); + opacity: 0.6; +} + +.test-card-chevron-spacer { + display: inline-block; + width: 12px; + flex-shrink: 0; +} + +.test-icon-pass { color: #10b981; flex-shrink: 0; } +.test-icon-fail { color: #ef4444; flex-shrink: 0; } +.test-icon-error { color: #f59e0b; flex-shrink: 0; } +.test-icon-skip { color: var(--text-secondary); flex-shrink: 0; } + +.test-card-name { + flex: 1; + min-width: 0; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.test-card-duration { + flex-shrink: 0; + color: var(--text-secondary); + font-size: 11px; + padding-left: 8px; +} + +.test-card-details { + padding: 2px 0 4px 0; + border-top: 1px solid var(--border); + background: var(--panel-1); +} + +.test-card-detail-row { + display: flex; + align-items: baseline; + gap: 8px; + padding: 2px 8px 2px 38px; + line-height: 18px; +} + +.test-card-detail-message { + flex: 1; + min-width: 0; + color: var(--text-secondary); + font-size: 11px; + white-space: pre-wrap; + word-break: break-word; +} + +/* Running indicator animation */ + +.test-log-running-indicator { + display: inline-block; + width: 6px; + height: 6px; + background: var(--accent); + border-radius: 50%; + animation: testPulse 1.2s ease-in-out infinite; + flex-shrink: 0; + margin-top: 7px; +} + +@keyframes testPulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +/* --- Summary Line --- */ + +.test-summary-line { + padding: 8px 12px; + margin-top: 4px; + border-top: 1px solid var(--border); + font-weight: 600; + font-size: 12px; +} + +.test-summary-pass { + color: #10b981; +} + +.test-summary-fail { + color: #ef4444; +} + +/* --- Spinner for refresh --- */ + +.spin { + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + /* Help Chat Popover */ .help-chat-backdrop { position: fixed; diff --git a/package.json b/package.json index ebfcc7c..d0ba7ab 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "prompd-app", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "private": true, "license": "Elastic-2.0", "scripts": { "dev": "cd frontend && npm run dev", "dev:backend": "cd backend && npm run dev", - "build": "cd packages/scheduler && npm run build && cd ../react && npm run build && cd ../../frontend && npm run build", + "build": "cd packages/scheduler && npm run build && cd ../react && npm run build && cd ../test && npm run build && cd ../../frontend && npm run build", "build:scheduler": "cd packages/scheduler && npm run build", "build:react": "cd packages/react && npm run build", + "build:test": "cd packages/test && npm run build", "electron:dev": "cd frontend && npm run electron:dev", "electron:build:win": "cd frontend && npm run electron:build:win" } diff --git a/packages/react/package.json b/packages/react/package.json index e77c1c9..a51553a 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@prompd/react", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "description": "React component library for building Prompd-powered AI interfaces with intelligent intent classification", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/scheduler/package-lock.json b/packages/scheduler/package-lock.json index daf07f2..578afaa 100644 --- a/packages/scheduler/package-lock.json +++ b/packages/scheduler/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.0-beta.1", "license": "Elastic-2.0", "dependencies": { - "@prompd/cli": "^0.5.0-beta.7", + "@prompd/cli": "^0.5.0-beta.9", "adm-zip": "^0.5.10", "better-sqlite3": "^12.6.2", "chokidar": "^3.6.0", @@ -1122,9 +1122,9 @@ } }, "node_modules/@prompd/cli": { - "version": "0.5.0-beta.7", - "resolved": "https://registry.npmjs.org/@prompd/cli/-/cli-0.5.0-beta.7.tgz", - "integrity": "sha512-BEyRSjP8H7x3lmAHl4onON9lUecocQnd8VLksUs5mzYV4dj7FI0JztrtA8samy6a8REB4/8CdstrgPo5UhlK3A==", + "version": "0.5.0-beta.9", + "resolved": "https://registry.npmjs.org/@prompd/cli/-/cli-0.5.0-beta.9.tgz", + "integrity": "sha512-YEoYmilLKY8SFB10559vKPXOlKdD8pvSabi5dDiD36F+enBSOB+mPFLY1BNnS83dBBuuHrdRJ00+WOPOWmnA+A==", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", "@types/nunjucks": "^3.2.6", diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json index 5e1c0c1..cdd4478 100644 --- a/packages/scheduler/package.json +++ b/packages/scheduler/package.json @@ -1,6 +1,6 @@ { "name": "@prompd/scheduler", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "description": "Workflow deployment and trigger management for Prompd", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -20,7 +20,7 @@ "author": "Prompd Team", "license": "Elastic-2.0", "dependencies": { - "@prompd/cli": "^0.5.0-beta.7", + "@prompd/cli": "^0.5.0-beta.9", "adm-zip": "^0.5.10", "better-sqlite3": "^12.6.2", "chokidar": "^3.6.0", diff --git a/packages/test/package-lock.json b/packages/test/package-lock.json new file mode 100644 index 0000000..54d39a4 --- /dev/null +++ b/packages/test/package-lock.json @@ -0,0 +1,529 @@ +{ + "name": "@prompd/test", + "version": "0.5.0-beta.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@prompd/test", + "version": "0.5.0-beta.1", + "license": "Elastic-2.0", + "dependencies": { + "glob": "^10.3.10", + "yaml": "^2.7.1" + }, + "devDependencies": { + "@prompd/cli": "^0.5.0-beta.9", + "@types/node": "^18.19.17", + "typescript": "^5.7.3" + }, + "peerDependencies": { + "@prompd/cli": ">=0.5.0-beta.9" + } + }, + "../../../prompd-cli/typescript": { + "name": "@prompd/cli", + "version": "0.5.0-beta.9", + "dev": true, + "license": "Elastic-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "@types/nunjucks": "^3.2.6", + "adm-zip": "^0.5.16", + "archiver": "^6.0.1", + "axios": "^1.6.2", + "chalk": "^4.1.2", + "commander": "^11.1.0", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "helmet": "^7.1.0", + "inquirer": "^9.2.12", + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "mammoth": "^1.11.0", + "nunjucks": "^3.2.4", + "pdf-parse": "^2.4.5", + "semver": "^7.5.4", + "sharp": "^0.34.4", + "tar": "^7.0.1", + "xlsx": "^0.18.5", + "yaml": "^2.3.4" + }, + "bin": { + "prompd": "bin/prompd.js" + }, + "devDependencies": { + "@types/adm-zip": "^0.5.7", + "@types/archiver": "^6.0.2", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/fs-extra": "^11.0.4", + "@types/jest": "^29.5.8", + "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.4", + "@types/pdf-parse": "^1.1.5", + "@types/semver": "^7.5.6", + "@types/tar": "^6.1.11", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prompd/cli": { + "resolved": "../../../prompd-cli/typescript", + "link": true + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/packages/test/package.json b/packages/test/package.json new file mode 100644 index 0000000..a38db18 --- /dev/null +++ b/packages/test/package.json @@ -0,0 +1,34 @@ +{ + "name": "@prompd/test", + "version": "0.5.0-beta.10", + "description": "Prompt testing and evaluation framework for Prompd", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "keywords": [ + "test", + "eval", + "evaluator", + "prompt", + "prompd" + ], + "author": "Prompd Team", + "license": "Elastic-2.0", + "dependencies": { + "glob": "^10.3.10", + "yaml": "^2.7.1" + }, + "peerDependencies": { + "@prompd/cli": "0.5.0-beta.10" + }, + "devDependencies": { + "@prompd/cli": "0.5.0-beta.10", + "@types/node": "^18.19.17", + "typescript": "^5.7.3" + } +} diff --git a/packages/test/src/EvaluatorEngine.ts b/packages/test/src/EvaluatorEngine.ts new file mode 100644 index 0000000..05394a4 --- /dev/null +++ b/packages/test/src/EvaluatorEngine.ts @@ -0,0 +1,130 @@ +/** + * Routes assertions to the correct evaluator and manages execution order. + * + * Execution order: nlp -> script -> prmd (cheap to expensive). + * Fail-fast by default — stops on first failure unless runAll is set. + */ + +import type { AssertionDef, AssertionResult, EvaluatorType } from './types'; +import type { Evaluator, EvaluatorContext } from './evaluators/types'; +import type { CompilerModule } from './cli-types'; +import { NlpEvaluator } from './evaluators/NlpEvaluator'; +import { ScriptEvaluator } from './evaluators/ScriptEvaluator'; +import { PrmdEvaluator, type PrmdEvaluatorOptions } from './evaluators/PrmdEvaluator'; + +/** Execution priority — lower number runs first */ +const EVALUATOR_PRIORITY: Record = { + nlp: 0, + script: 1, + prmd: 2, +}; + +export interface EvaluatorEngineOptions { + testFileDir: string; + evaluatorPrompt?: string; + workspaceRoot?: string; + registryUrl?: string; + allowedEvaluators?: EvaluatorType[]; + failFast?: boolean; + cliModule?: CompilerModule; + provider?: string; + model?: string; +} + +export class EvaluatorEngine { + private evaluators: Map; + private allowedEvaluators: Set; + private failFast: boolean; + + constructor(options: EvaluatorEngineOptions) { + this.failFast = options.failFast !== false; + this.allowedEvaluators = new Set(options.allowedEvaluators || ['nlp', 'script', 'prmd']); + + const prmdOptions: PrmdEvaluatorOptions = { + testFileDir: options.testFileDir, + evaluatorPrompt: options.evaluatorPrompt, + workspaceRoot: options.workspaceRoot, + registryUrl: options.registryUrl, + cliModule: options.cliModule, + provider: options.provider, + model: options.model, + }; + + this.evaluators = new Map([ + ['nlp', new NlpEvaluator()], + ['script', new ScriptEvaluator(options.testFileDir)], + ['prmd', new PrmdEvaluator(prmdOptions)], + ]); + } + + /** + * Evaluate all assertions in cost-priority order. + * Returns results for each assertion. + */ + async evaluate( + assertions: AssertionDef[], + context: EvaluatorContext, + onResult?: (result: AssertionResult) => void + ): Promise { + const results: AssertionResult[] = []; + + // Sort by evaluator priority (nlp first, prmd last) + const sorted = [...assertions].sort( + (a, b) => EVALUATOR_PRIORITY[a.evaluator] - EVALUATOR_PRIORITY[b.evaluator] + ); + + for (const assertion of sorted) { + // Skip evaluators that aren't allowed + if (!this.allowedEvaluators.has(assertion.evaluator)) { + const skipped: AssertionResult = { + evaluator: assertion.evaluator, + check: assertion.check, + status: 'skip', + reason: `Evaluator type "${assertion.evaluator}" skipped by filter`, + duration: 0, + }; + results.push(skipped); + onResult?.(skipped); + continue; + } + + const evaluator = this.evaluators.get(assertion.evaluator); + if (!evaluator) { + const errorResult: AssertionResult = { + evaluator: assertion.evaluator, + check: assertion.check, + status: 'error', + reason: `No evaluator registered for type "${assertion.evaluator}"`, + duration: 0, + }; + results.push(errorResult); + onResult?.(errorResult); + continue; + } + + const result = await evaluator.evaluate(assertion, context); + results.push(result); + onResult?.(result); + + // Fail-fast: stop on first failure + if (this.failFast && result.status !== 'pass') { + // Mark remaining assertions as skipped + const remaining = sorted.slice(sorted.indexOf(assertion) + 1); + for (const rem of remaining) { + const skipped: AssertionResult = { + evaluator: rem.evaluator, + check: rem.check, + status: 'skip', + reason: 'Skipped due to prior failure (fail-fast)', + duration: 0, + }; + results.push(skipped); + onResult?.(skipped); + } + break; + } + } + + return results; + } +} diff --git a/packages/test/src/TestDiscovery.ts b/packages/test/src/TestDiscovery.ts new file mode 100644 index 0000000..276720d --- /dev/null +++ b/packages/test/src/TestDiscovery.ts @@ -0,0 +1,133 @@ +/** + * Discovers .test.prmd files and pairs them with their source .prmd files. + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { glob } from 'glob'; +import { TestParser } from './TestParser'; +import type { TestSuite } from './types'; + +export interface DiscoveryResult { + suites: TestSuite[]; + errors: DiscoveryError[]; +} + +export interface DiscoveryError { + filePath: string; + message: string; +} + +export class TestDiscovery { + private parser: TestParser; + + constructor() { + this.parser = new TestParser(); + } + + /** + * Discover test suites from a target path. + * + * - If targetPath is a .test.prmd file, parse it directly. + * - If targetPath is a .prmd file, look for a colocated .test.prmd sidecar. + * - If targetPath is a directory, glob for all .test.prmd files recursively. + */ + async discover(targetPath: string): Promise { + const resolved = path.resolve(targetPath); + const suites: TestSuite[] = []; + const errors: DiscoveryError[] = []; + + if (!fs.existsSync(resolved)) { + errors.push({ filePath: resolved, message: 'Path does not exist' }); + return { suites, errors }; + } + + const stat = fs.statSync(resolved); + + if (stat.isDirectory()) { + return this.discoverDirectory(resolved); + } + + if (resolved.endsWith('.test.prmd')) { + return this.discoverTestFile(resolved); + } + + if (resolved.endsWith('.prmd')) { + return this.discoverFromSource(resolved); + } + + errors.push({ + filePath: resolved, + message: 'Target must be a .prmd file, .test.prmd file, or directory', + }); + return { suites, errors }; + } + + private async discoverDirectory(dirPath: string): Promise { + const suites: TestSuite[] = []; + const errors: DiscoveryError[] = []; + + const pattern = '**/*.test.prmd'; + const testFiles = await glob(pattern, { + cwd: dirPath, + absolute: true, + nodir: true, + windowsPathsNoEscape: true, + }); + + for (const testFile of testFiles) { + const normalized = testFile.replace(/\\/g, '/'); + const result = await this.discoverTestFile(normalized); + suites.push(...result.suites); + errors.push(...result.errors); + } + + return { suites, errors }; + } + + private async discoverTestFile(testFilePath: string): Promise { + const suites: TestSuite[] = []; + const errors: DiscoveryError[] = []; + + try { + const content = fs.readFileSync(testFilePath, 'utf-8'); + const suite = this.parser.parse(content, testFilePath); + + // Validate that the target .prmd file exists + if (!fs.existsSync(suite.target)) { + errors.push({ + filePath: testFilePath, + message: `Target prompt file not found: ${suite.target}`, + }); + return { suites, errors }; + } + + suites.push(suite); + } catch (err) { + errors.push({ + filePath: testFilePath, + message: err instanceof Error ? err.message : String(err), + }); + } + + return { suites, errors }; + } + + private async discoverFromSource(sourcePath: string): Promise { + const dir = path.dirname(sourcePath); + const base = path.basename(sourcePath, '.prmd'); + const testFilePath = path.join(dir, `${base}.test.prmd`); + + if (!fs.existsSync(testFilePath)) { + return { + suites: [], + errors: [{ + filePath: sourcePath, + message: `No colocated test file found: ${testFilePath}`, + }], + }; + } + + return this.discoverTestFile(testFilePath); + } +} diff --git a/packages/test/src/TestParser.ts b/packages/test/src/TestParser.ts new file mode 100644 index 0000000..3a593b5 --- /dev/null +++ b/packages/test/src/TestParser.ts @@ -0,0 +1,240 @@ +/** + * Parses .test.prmd files into TestSuite structures. + * + * A .test.prmd file has YAML frontmatter (test definitions) and + * an optional content block (evaluator prompt for prmd evaluators). + */ + +import * as path from 'path'; +import * as YAML from 'yaml'; +import type { TestSuite, TestCase, AssertionDef, EvaluatorType, NlpCheck } from './types'; + +const VALID_EVALUATOR_TYPES: EvaluatorType[] = ['nlp', 'script', 'prmd']; +const VALID_NLP_CHECKS: NlpCheck[] = [ + 'contains', 'not_contains', 'matches', + 'max_tokens', 'min_tokens', 'max_words', 'min_words', + 'starts_with', 'ends_with' +]; + +interface ParsedFrontmatter { + name?: string; + description?: string; + target?: string; + tests?: RawTestCase[]; +} + +interface RawTestCase { + name?: string; + params?: Record; + assert?: RawAssertionDef[]; + expect_error?: boolean; +} + +interface RawAssertionDef { + evaluator?: string; + check?: string; + value?: unknown; + evaluate?: string; + run?: string; + prompt?: string; + provider?: string; + model?: string; +} + +export class TestParser { + /** + * Parse a .test.prmd file's raw content into a TestSuite. + */ + parse(content: string, testFilePath: string): TestSuite { + const normalized = content.replace(/\r\n/g, '\n'); + const { frontmatter, body } = this.splitFrontmatter(normalized); + + if (!frontmatter) { + throw new TestParseError('Missing YAML frontmatter in .test.prmd file', testFilePath); + } + + let parsed: ParsedFrontmatter; + try { + parsed = YAML.parse(frontmatter) as ParsedFrontmatter; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new TestParseError(`Invalid YAML frontmatter: ${message}`, testFilePath); + } + + if (!parsed || typeof parsed !== 'object') { + throw new TestParseError('Frontmatter must be a YAML object', testFilePath); + } + + const name = parsed.name || path.basename(testFilePath, '.test.prmd'); + const target = this.resolveTarget(parsed.target, testFilePath); + const tests = this.parseTests(parsed.tests, testFilePath); + const evaluatorPrompt = body.trim() || undefined; + + return { + name, + description: parsed.description, + target, + testFilePath, + tests, + evaluatorPrompt, + }; + } + + private splitFrontmatter(content: string): { frontmatter: string | null; body: string } { + const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); + if (!match) { + return { frontmatter: null, body: content }; + } + return { + frontmatter: match[1], + body: match[2], + }; + } + + private resolveTarget(target: string | undefined, testFilePath: string): string { + if (target) { + const dir = path.dirname(testFilePath); + return path.resolve(dir, target); + } + + // Auto-discover: summarize.test.prmd -> summarize.prmd + const dir = path.dirname(testFilePath); + const base = path.basename(testFilePath); + const sourceBase = base.replace(/\.test\.prmd$/, '.prmd'); + return path.resolve(dir, sourceBase); + } + + private parseTests(rawTests: RawTestCase[] | undefined, filePath: string): TestCase[] { + if (!rawTests || !Array.isArray(rawTests)) { + throw new TestParseError('Frontmatter must contain a "tests" array', filePath); + } + + if (rawTests.length === 0) { + throw new TestParseError('"tests" array must not be empty', filePath); + } + + return rawTests.map((raw, index) => { + const name = raw.name || `test_${index + 1}`; + const params = raw.params && typeof raw.params === 'object' ? raw.params : {}; + + if (raw.expect_error) { + return { + name, + params, + assert: [], + expect_error: true, + }; + } + + const assertions = this.parseAssertions(raw.assert, name, filePath); + return { name, params, assert: assertions }; + }); + } + + private parseAssertions( + rawAssertions: RawAssertionDef[] | undefined, + testName: string, + filePath: string + ): AssertionDef[] { + if (!rawAssertions || !Array.isArray(rawAssertions)) { + return []; + } + + return rawAssertions.map((raw, index) => { + if (!raw.evaluator || !VALID_EVALUATOR_TYPES.includes(raw.evaluator as EvaluatorType)) { + throw new TestParseError( + `Test "${testName}", assertion ${index + 1}: invalid evaluator "${raw.evaluator}". ` + + `Must be one of: ${VALID_EVALUATOR_TYPES.join(', ')}`, + filePath + ); + } + + const evaluator = raw.evaluator as EvaluatorType; + + if (evaluator === 'nlp') { + return this.validateNlpAssertion(raw, testName, index, filePath); + } + + if (evaluator === 'script') { + return this.validateScriptAssertion(raw, testName, index, filePath); + } + + return this.validatePrmdAssertion(raw, testName, index, filePath); + }); + } + + private validateNlpAssertion( + raw: RawAssertionDef, + testName: string, + index: number, + filePath: string + ): AssertionDef { + if (!raw.check || !VALID_NLP_CHECKS.includes(raw.check as NlpCheck)) { + throw new TestParseError( + `Test "${testName}", assertion ${index + 1}: NLP evaluator requires a valid "check". ` + + `Must be one of: ${VALID_NLP_CHECKS.join(', ')}`, + filePath + ); + } + + if (raw.value === undefined || raw.value === null) { + throw new TestParseError( + `Test "${testName}", assertion ${index + 1}: NLP evaluator requires a "value"`, + filePath + ); + } + + return { + evaluator: 'nlp', + check: raw.check as NlpCheck, + value: raw.value as string | string[] | number, + evaluate: (raw.evaluate as AssertionDef['evaluate']) || undefined, + }; + } + + private validateScriptAssertion( + raw: RawAssertionDef, + testName: string, + index: number, + filePath: string + ): AssertionDef { + if (!raw.run || typeof raw.run !== 'string') { + throw new TestParseError( + `Test "${testName}", assertion ${index + 1}: script evaluator requires a "run" path`, + filePath + ); + } + + return { + evaluator: 'script', + run: raw.run, + evaluate: (raw.evaluate as AssertionDef['evaluate']) || undefined, + }; + } + + private validatePrmdAssertion( + raw: RawAssertionDef, + _testName: string, + _index: number, + _filePath: string + ): AssertionDef { + // prompt: is optional — if omitted, uses the content block of the .test.prmd + return { + evaluator: 'prmd', + prompt: raw.prompt || undefined, + provider: raw.provider || undefined, + model: raw.model || undefined, + evaluate: (raw.evaluate as AssertionDef['evaluate']) || undefined, + }; + } +} + +export class TestParseError extends Error { + public readonly filePath: string; + + constructor(message: string, filePath: string) { + super(`${message} (${filePath})`); + this.name = 'TestParseError'; + this.filePath = filePath; + } +} diff --git a/packages/test/src/TestRunner.ts b/packages/test/src/TestRunner.ts new file mode 100644 index 0000000..f8621ae --- /dev/null +++ b/packages/test/src/TestRunner.ts @@ -0,0 +1,520 @@ +/** + * TestRunner - orchestrates the full test lifecycle: + * discovery -> compile -> execute -> evaluate -> report + * + * Consumes @prompd/cli for compilation and execution. + * This is the primary public API for @prompd/test. + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { TestDiscovery } from './TestDiscovery'; +import { EvaluatorEngine } from './EvaluatorEngine'; +import { ConsoleReporter } from './reporters/ConsoleReporter'; +import { JsonReporter } from './reporters/JsonReporter'; +import { JunitReporter } from './reporters/JunitReporter'; +import type { Reporter } from './reporters/types'; +import type { EvaluatorContext } from './evaluators/types'; +import type { CompilerModule } from './cli-types'; +import type { TestHarness } from '@prompd/cli'; +import type { + TestSuite, + TestCase, + TestResult, + TestRunResult, + TestSuiteResult, + TestRunSummary, + TestRunOptions, + TestProgressCallback, + EvaluatorType, +} from './types'; + +export class TestRunner implements TestHarness { + private discovery: TestDiscovery; + private cliModule: CompilerModule | null = null; + private configLoaded = false; + + /** + * @param cli - Optional pre-loaded @prompd/cli module. If provided, skips dynamic import. + * This is the recommended approach when running inside Electron where the CLI + * is already loaded by the main process. + */ + constructor(cli?: CompilerModule) { + this.discovery = new TestDiscovery(); + if (cli) { + this.cliModule = cli; + } + } + + /** + * Ensure CLI config is loaded (API keys, provider settings). + * Called once before any execution. + */ + private async ensureConfig(): Promise { + if (this.configLoaded) return; + const cli = await this.getCli(); + try { + const configManager = new cli.ConfigManager(); + console.log('[TestRunner] Loading config...'); + // loadConfig() is async — must await it + if (configManager.loadConfig) { + await configManager.loadConfig(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cfg = (configManager as any).config; + console.log('[TestRunner] Config loaded:', cfg ? Object.keys(cfg) : 'null'); + console.log('[TestRunner] API keys:', cfg?.apiKeys ? Object.keys(cfg.apiKeys).filter((k: string) => cfg.apiKeys[k]) : 'none'); + console.log('[TestRunner] Default provider:', cfg?.defaultProvider); + } else if (configManager.load) { + await configManager.load(); + console.log('[TestRunner] Config loaded via load()'); + } + this.configLoaded = true; + } catch (err) { + console.error('[TestRunner] Config load failed:', err); + // Config may not exist — that's OK for --no-llm runs + } + } + + /** + * Run tests for a target path (file or directory). + * Returns structured results and an exit code (0 = all pass, 1 = failures). + */ + async run( + targetPath: string, + options: TestRunOptions = {}, + onProgress?: TestProgressCallback + ): Promise { + const startTime = Date.now(); + + // 1. Discovery + const { suites, errors: discoveryErrors } = await this.discovery.discover(targetPath); + + if (discoveryErrors.length > 0 && suites.length === 0) { + return this.buildErrorResult(discoveryErrors.map(e => e.message), startTime); + } + + // 2. Run each suite + const suiteResults: TestSuiteResult[] = []; + + for (const suite of suites) { + if (options.signal?.aborted) break; + + onProgress?.({ type: 'suite_start', suite: suite.name, testCount: suite.tests.length }); + + const results = await this.runSuite(suite, options, onProgress); + suiteResults.push({ + suite: suite.name, + testFilePath: suite.testFilePath, + results, + }); + + onProgress?.({ type: 'suite_complete', suite: suite.name, results }); + } + + // 3. Build summary + const summary = this.buildSummary(suiteResults, startTime); + + return { suites: suiteResults, summary }; + } + + /** + * Run tests and return formatted output string. + */ + async runAndReport( + targetPath: string, + options: TestRunOptions = {}, + onProgress?: TestProgressCallback + ): Promise<{ output: string; exitCode: number }> { + const result = await this.run(targetPath, options, onProgress); + const reporter = this.getReporter(options); + const output = reporter.report(result); + const exitCode = (result.summary.failed > 0 || result.summary.errors > 0) ? 1 : 0; + return { output, exitCode }; + } + + private async runSuite( + suite: TestSuite, + options: TestRunOptions, + onProgress?: TestProgressCallback + ): Promise { + const results: TestResult[] = []; + const allowedEvaluators = this.resolveAllowedEvaluators(options); + + for (const testCase of suite.tests) { + if (options.signal?.aborted) break; + + onProgress?.({ type: 'test_start', suite: suite.name, testName: testCase.name }); + + const result = await this.runTestCase(suite, testCase, allowedEvaluators, options, onProgress); + results.push(result); + + onProgress?.({ type: 'test_complete', suite: suite.name, testName: testCase.name, result }); + } + + return results; + } + + private async runTestCase( + suite: TestSuite, + testCase: TestCase, + allowedEvaluators: EvaluatorType[], + options: TestRunOptions, + onProgress?: TestProgressCallback + ): Promise { + const start = Date.now(); + + // Step 1: Compile the target .prmd with test params + let compiledOutput: string; + let promptMetadata: Record = {}; + try { + const compileResult = await this.compileTarget(suite.target, testCase.params, options); + compiledOutput = compileResult.compiled; + promptMetadata = compileResult.metadata; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + + // If expect_error is set, compilation failure is a PASS + if (testCase.expect_error) { + return { + suite: suite.name, + testName: testCase.name, + status: 'pass', + duration: Date.now() - start, + assertions: [], + error: `Expected error occurred: ${errorMessage}`, + }; + } + + return { + suite: suite.name, + testName: testCase.name, + status: 'error', + duration: Date.now() - start, + assertions: [], + error: `Compilation failed: ${errorMessage}`, + }; + } + + // If expect_error was set but compilation succeeded, that's a failure + if (testCase.expect_error) { + return { + suite: suite.name, + testName: testCase.name, + status: 'fail', + duration: Date.now() - start, + assertions: [], + compiledInput: compiledOutput, + error: 'Expected compilation to fail, but it succeeded', + }; + } + + // Step 2: Execute against LLM (unless --no-llm) + let llmOutput = ''; + let provider = 'none'; + let model = 'none'; + let execDuration = 0; + let usage: { promptTokens?: number; completionTokens?: number; totalTokens?: number } | undefined; + + if (!options.noLlm) { + try { + const execResult = await this.executePrompt(compiledOutput, promptMetadata, options); + llmOutput = execResult.response; + provider = execResult.provider; + model = execResult.model; + execDuration = execResult.duration; + usage = execResult.usage; + } catch (err) { + return { + suite: suite.name, + testName: testCase.name, + status: 'error', + duration: Date.now() - start, + assertions: [], + compiledInput: compiledOutput, + error: `Execution failed: ${err instanceof Error ? err.message : String(err)}`, + }; + } + } else { + // In --no-llm mode, use the compiled output as the "output" for NLP checks + // This enables structural assertions against the compiled prompt itself + llmOutput = compiledOutput; + } + + const execution = !options.noLlm ? { provider, model, duration: execDuration, usage } : undefined; + + // Step 3: Run evaluations + if (testCase.assert.length === 0) { + return { + suite: suite.name, + testName: testCase.name, + status: 'pass', + duration: Date.now() - start, + assertions: [], + output: llmOutput, + compiledInput: compiledOutput, + execution, + }; + } + + const engine = new EvaluatorEngine({ + testFileDir: path.dirname(suite.testFilePath), + evaluatorPrompt: suite.evaluatorPrompt, + workspaceRoot: options.workspaceRoot, + registryUrl: options.registryUrl, + allowedEvaluators, + failFast: options.runAll ? false : (options.failFast !== false), + cliModule: this.cliModule || undefined, + provider: options.provider, + model: options.model, + }); + + const context: EvaluatorContext = { + prompt: compiledOutput, + response: llmOutput, + params: testCase.params, + metadata: { provider, model, duration: execDuration }, + }; + + const assertions = await engine.evaluate( + testCase.assert, + context, + (assertion) => { + onProgress?.({ + type: 'assertion_complete', + suite: suite.name, + testName: testCase.name, + assertion, + }); + } + ); + + // Determine overall test status from assertions + const hasFailure = assertions.some(a => a.status === 'fail'); + const hasError = assertions.some(a => a.status === 'error'); + const status = hasError ? 'error' : hasFailure ? 'fail' : 'pass'; + + return { + suite: suite.name, + testName: testCase.name, + status, + duration: Date.now() - start, + assertions, + output: llmOutput, + compiledInput: compiledOutput, + execution, + }; + } + + /** + * Compile a .prmd file and return both the compiled text and metadata + * (provider, model, temperature, max_tokens from frontmatter). + */ + private async compileTarget( + targetPath: string, + params: Record, + options: TestRunOptions + ): Promise<{ compiled: string; metadata: Record }> { + const cli = await this.getCli(); + const compiler = new cli.PrompdCompiler(); + + if (!fs.existsSync(targetPath)) { + throw new Error(`Target prompt file not found: ${targetPath}`); + } + + // Use compileWithContext to get both output and frontmatter metadata + const context = await compiler.compileWithContext(targetPath, { + outputFormat: 'markdown', + parameters: params, + filePath: targetPath, + workspaceRoot: options.workspaceRoot, + registryUrl: options.registryUrl, + fileSystem: new cli.NodeFileSystem(), + }); + + // compileWithContext may return { compiledResult, metadata } or a string + let compiled: string; + let metadata: Record = {}; + + if (typeof context === 'string') { + compiled = context; + } else if (context && typeof context === 'object') { + compiled = (context as { compiledResult?: string }).compiledResult || ''; + metadata = (context as { metadata?: Record }).metadata || {}; + } else { + throw new Error('Compilation produced no output'); + } + + if (!compiled) { + throw new Error('Compilation produced no output'); + } + + console.log(`[TestRunner] Compiled ${targetPath}`); + console.log(`[TestRunner] params: ${JSON.stringify(params)}`); + console.log(`[TestRunner] metadata: ${JSON.stringify(metadata)}`); + console.log(`[TestRunner] output (${compiled.length} chars): ${compiled.substring(0, 200)}`); + + return { compiled, metadata }; + } + + /** + * Execute compiled prompt text against an LLM using the executor's callLLM directly. + * This avoids re-compilation through executeRawText which loses metadata. + */ + private async executePrompt( + compiled: string, + metadata: Record, + runOptions: TestRunOptions + ): Promise<{ response: string; provider: string; model: string; duration: number; usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }> { + await this.ensureConfig(); + const cli = await this.getCli(); + const executor = new cli.PrompdExecutor(); + const start = Date.now(); + + // Resolve provider/model from frontmatter metadata + config defaults + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const configManager = (cli as any).ConfigManager?.getInstance + ? (cli as any).ConfigManager.getInstance() + : null; + const config = configManager?.config || {}; + + // Priority: .prmd frontmatter > test run options (UI selector) > config defaults + const provider = String(metadata.provider || runOptions.provider || config.defaultProvider || 'openai'); + const rawModel = metadata.model || runOptions.model || config.default_model || config.defaultModel || ''; + // Fall back to a sensible default model if none specified + const model = String(rawModel) || this.getDefaultModel(provider); + const temperature = Number(metadata.temperature ?? 0.7); + const maxTokens = Number(metadata.max_tokens ?? 4096); + + // Get API key from config + const apiKey = configManager?.getApiKey?.(provider, config) || ''; + + console.log(`[TestRunner] Executing: provider=${provider}, model=${model || '(default)'}, tokens=${compiled.length}`); + + if (!apiKey && provider !== 'ollama') { + throw new Error(`No API key configured for provider "${provider}". Check ~/.prompd/config.yaml`); + } + + try { + const result = await executor.callLLM(provider, model, compiled, apiKey, temperature, maxTokens); + + if (!result.success) { + throw new Error(result.error || 'LLM execution failed'); + } + + return { + response: result.response || result.content || '', + provider, + model, + duration: Date.now() - start, + usage: result.usage, + }; + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + console.error(`[TestRunner] callLLM failed: ${errMsg}`); + throw new Error(errMsg); + } + } + + private resolveAllowedEvaluators(options: TestRunOptions): EvaluatorType[] { + if (options.noLlm) { + // In --no-llm mode, skip prmd evaluators (they require LLM calls) + const base = options.evaluators || ['nlp', 'script']; + return base.filter(e => e !== 'prmd'); + } + + return options.evaluators || ['nlp', 'script', 'prmd']; + } + + private getReporter(options: TestRunOptions): Reporter { + switch (options.reporter) { + case 'json': + return new JsonReporter(options.verbose); + case 'junit': + return new JunitReporter(); + case 'console': + default: + return new ConsoleReporter(options.verbose); + } + } + + private buildSummary(suiteResults: TestSuiteResult[], startTime: number): TestRunSummary { + let total = 0; + let passed = 0; + let failed = 0; + let errors = 0; + let skipped = 0; + let totalTokens = 0; + const providerSet = new Set(); + const modelSet = new Set(); + + for (const suite of suiteResults) { + for (const result of suite.results) { + total++; + switch (result.status) { + case 'pass': passed++; break; + case 'fail': failed++; break; + case 'error': errors++; break; + case 'skip': skipped++; break; + } + if (result.execution) { + if (result.execution.provider && result.execution.provider !== 'none') { + providerSet.add(result.execution.provider); + } + if (result.execution.model && result.execution.model !== 'none') { + modelSet.add(result.execution.model); + } + if (result.execution.usage?.totalTokens) { + totalTokens += result.execution.usage.totalTokens; + } + } + } + } + + return { + total, + passed, + failed, + errors, + skipped, + duration: Date.now() - startTime, + totalTokens: totalTokens || undefined, + providers: providerSet.size > 0 ? Array.from(providerSet) : undefined, + models: modelSet.size > 0 ? Array.from(modelSet) : undefined, + }; + } + + private buildErrorResult(errorMessages: string[], startTime: number): TestRunResult { + return { + suites: [], + summary: { + total: 0, + passed: 0, + failed: 0, + errors: errorMessages.length, + skipped: 0, + duration: Date.now() - startTime, + }, + }; + } + + private getDefaultModel(provider: string): string { + const defaults: Record = { + openai: 'gpt-4o', + anthropic: 'claude-sonnet-4-20250514', + groq: 'llama-3.1-70b-versatile', + google: 'gemini-2.0-flash', + mistral: 'mistral-large-latest', + deepseek: 'deepseek-chat', + }; + return defaults[provider.toLowerCase()] || 'gpt-4o'; + } + + private async getCli(): Promise { + if (!this.cliModule) { + throw new Error( + '@prompd/cli module not provided. Pass it to the TestRunner constructor: new TestRunner(cliModule)' + ); + } + return this.cliModule; + } +} diff --git a/packages/test/src/cli-types.ts b/packages/test/src/cli-types.ts new file mode 100644 index 0000000..e833311 --- /dev/null +++ b/packages/test/src/cli-types.ts @@ -0,0 +1,92 @@ +/** + * Shared type interfaces for dynamically imported @prompd/cli module. + * These mirror the CLI's public API without requiring a compile-time dependency. + */ + +export interface CompilerModule { + PrompdCompiler: new (config?: Record) => Compiler; + PrompdExecutor: new () => Executor; + ConfigManager: new () => ConfigManagerInstance; + MemoryFileSystem: new (files?: Record) => MemoryFileSystemInstance; + NodeFileSystem: new () => NodeFileSystemInstance; +} + +export interface Compiler { + compile( + sourcePath: string, + options: Record + ): Promise; + }>; + + compileWithContext( + sourcePath: string, + options: Record + ): Promise; + errors?: unknown[]; + warnings?: unknown[]; + }>; +} + +export interface Executor { + execute( + filePath: string, + options: Record + ): Promise; + + executeRawText( + compiledText: string, + options: Record + ): Promise; + + callLLM( + provider: string, + model: string, + content: string, + apiKey: string, + temperature?: number, + maxTokens?: number + ): Promise<{ + success: boolean; + response?: string; + content?: string; + error?: string; + usage?: { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + }; + }>; +} + +export interface ExecutorResult { + response?: string; + error?: string; + usage?: { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + }; + metadata?: { + provider?: string; + model?: string; + }; +} + +export interface MemoryFileSystemInstance { + // In-memory file system for compilation without disk access +} + +export interface NodeFileSystemInstance { + // Disk-backed file system for compilation with file access +} + +export interface ConfigManagerInstance { + loadConfig?(): void; + load?(): void; + getConfig?(): Record | null; +} diff --git a/packages/test/src/evaluators/NlpEvaluator.ts b/packages/test/src/evaluators/NlpEvaluator.ts new file mode 100644 index 0000000..c320a7f --- /dev/null +++ b/packages/test/src/evaluators/NlpEvaluator.ts @@ -0,0 +1,240 @@ +/** + * NLP Evaluator - local, fast, free, deterministic assertions. + * + * Checks: contains, not_contains, matches, max_tokens, min_tokens, starts_with, ends_with + */ + +import type { Evaluator, EvaluatorContext } from './types'; +import type { AssertionDef, AssertionResult, NlpCheck, EvaluateTarget } from '../types'; + +export class NlpEvaluator implements Evaluator { + readonly type = 'nlp'; + + async evaluate(assertion: AssertionDef, context: EvaluatorContext): Promise { + const start = Date.now(); + const check = assertion.check as NlpCheck; + const target: EvaluateTarget = assertion.evaluate || 'response'; + + try { + const text = this.resolveTarget(target, context); + const targetLabel = target === 'both' ? 'Prompt+Response' : target === 'prompt' ? 'Prompt' : 'Output'; + const result = this.runCheck(check, assertion.value, text, targetLabel); + return { + evaluator: 'nlp', + check, + status: result.pass ? 'pass' : 'fail', + reason: result.reason, + duration: Date.now() - start, + }; + } catch (err) { + return { + evaluator: 'nlp', + check, + status: 'error', + reason: err instanceof Error ? err.message : String(err), + duration: Date.now() - start, + }; + } + } + + private resolveTarget(target: EvaluateTarget, context: EvaluatorContext): string { + switch (target) { + case 'prompt': return context.prompt; + case 'both': return `${context.prompt}\n\n${context.response}`; + case 'response': + default: return context.response; + } + } + + private runCheck( + check: NlpCheck, + value: string | string[] | number | undefined, + output: string, + label: string = 'Output' + ): { pass: boolean; reason: string } { + switch (check) { + case 'contains': + return this.checkContains(value, output, label); + case 'not_contains': + return this.checkNotContains(value, output, label); + case 'matches': + return this.checkMatches(value, output, label); + case 'max_tokens': + return this.checkMaxTokens(value, output); + case 'min_tokens': + return this.checkMinTokens(value, output); + case 'max_words': + return this.checkMaxWords(value, output); + case 'min_words': + return this.checkMinWords(value, output); + case 'starts_with': + return this.checkStartsWith(value, output, label); + case 'ends_with': + return this.checkEndsWith(value, output, label); + default: + return { pass: false, reason: `Unknown NLP check: ${check}` }; + } + } + + private checkContains( + value: string | string[] | number | undefined, + output: string, + label: string + ): { pass: boolean; reason: string } { + const values = this.toStringArray(value); + const lower = output.toLowerCase(); + const missing = values.filter(v => !lower.includes(v.toLowerCase())); + + if (missing.length === 0) { + return { pass: true, reason: `${label} contains all expected values` }; + } + return { + pass: false, + reason: `${label} missing: ${missing.map(v => `"${v}"`).join(', ')}`, + }; + } + + private checkNotContains( + value: string | string[] | number | undefined, + output: string, + label: string + ): { pass: boolean; reason: string } { + const values = this.toStringArray(value); + const lower = output.toLowerCase(); + const found = values.filter(v => lower.includes(v.toLowerCase())); + + if (found.length === 0) { + return { pass: true, reason: `${label} does not contain any excluded values` }; + } + return { + pass: false, + reason: `${label} contains excluded values: ${found.map(v => `"${v}"`).join(', ')}`, + }; + } + + private checkMatches( + value: string | string[] | number | undefined, + output: string, + label: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'string') { + return { pass: false, reason: '"matches" check requires a string regex pattern' }; + } + + const regex = new RegExp(value); + if (regex.test(output)) { + return { pass: true, reason: `${label} matches pattern /${value}/` }; + } + return { pass: false, reason: `${label} does not match pattern /${value}/` }; + } + + private checkMaxTokens( + value: string | string[] | number | undefined, + output: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'number') { + return { pass: false, reason: '"max_tokens" check requires a numeric value' }; + } + + const tokenCount = this.estimateTokens(output); + if (tokenCount <= value) { + return { pass: true, reason: `Token count ${tokenCount} <= ${value}` }; + } + return { pass: false, reason: `Token count ${tokenCount} exceeds max ${value}` }; + } + + private checkMinTokens( + value: string | string[] | number | undefined, + output: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'number') { + return { pass: false, reason: '"min_tokens" check requires a numeric value' }; + } + + const tokenCount = this.estimateTokens(output); + if (tokenCount >= value) { + return { pass: true, reason: `Token count ${tokenCount} >= ${value}` }; + } + return { pass: false, reason: `Token count ${tokenCount} below min ${value}` }; + } + + private checkStartsWith( + value: string | string[] | number | undefined, + output: string, + label: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'string') { + return { pass: false, reason: '"starts_with" check requires a string value' }; + } + + const trimmed = output.trimStart(); + if (trimmed.toLowerCase().startsWith(value.toLowerCase())) { + return { pass: true, reason: `${label} starts with "${value}"` }; + } + return { pass: false, reason: `${label} does not start with "${value}"` }; + } + + private checkEndsWith( + value: string | string[] | number | undefined, + output: string, + label: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'string') { + return { pass: false, reason: '"ends_with" check requires a string value' }; + } + + const trimmed = output.trimEnd(); + if (trimmed.toLowerCase().endsWith(value.toLowerCase())) { + return { pass: true, reason: `${label} ends with "${value}"` }; + } + return { pass: false, reason: `${label} does not end with "${value}"` }; + } + + private checkMaxWords( + value: string | string[] | number | undefined, + output: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'number') { + return { pass: false, reason: '"max_words" check requires a numeric value' }; + } + + const wordCount = this.countWords(output); + if (wordCount <= value) { + return { pass: true, reason: `Word count ${wordCount} <= ${value}` }; + } + return { pass: false, reason: `Word count ${wordCount} exceeds max ${value}` }; + } + + private checkMinWords( + value: string | string[] | number | undefined, + output: string + ): { pass: boolean; reason: string } { + if (typeof value !== 'number') { + return { pass: false, reason: '"min_words" check requires a numeric value' }; + } + + const wordCount = this.countWords(output); + if (wordCount >= value) { + return { pass: true, reason: `Word count ${wordCount} >= ${value}` }; + } + return { pass: false, reason: `Word count ${wordCount} below min ${value}` }; + } + + private countWords(text: string): number { + return text.trim().split(/\s+/).filter(w => w.length > 0).length; + } + + /** + * Rough token estimation: ~4 characters per token (GPT-family average). + * This is intentionally approximate — for precise counting, use a tokenizer. + */ + private estimateTokens(text: string): number { + return Math.ceil(text.length / 4); + } + + private toStringArray(value: string | string[] | number | undefined): string[] { + if (value === undefined || value === null) return []; + if (Array.isArray(value)) return value.map(String); + return [String(value)]; + } +} diff --git a/packages/test/src/evaluators/PrmdEvaluator.ts b/packages/test/src/evaluators/PrmdEvaluator.ts new file mode 100644 index 0000000..17a265e --- /dev/null +++ b/packages/test/src/evaluators/PrmdEvaluator.ts @@ -0,0 +1,284 @@ +/** + * Prmd Evaluator - LLM-based evaluation via @prompd/cli. + * + * Modes: + * - prompt: "@scope/pkg@version" -> uses a registry package as the evaluator + * - prompt: "./path" -> uses a local .prmd file as the evaluator + * - (no prompt field) -> uses the content block of the .test.prmd + * + * The evaluator prompt receives {{input}}, {{output}}, and {{params}} variables. + * Response must start with PASS or FAIL. + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import type { Evaluator, EvaluatorContext } from './types'; +import type { AssertionDef, AssertionResult } from '../types'; +import type { CompilerModule } from '../cli-types'; + +const PASS_FAIL_REGEX = /^(PASS|FAIL)[:\s]*(.*)/i; + +export interface PrmdEvaluatorOptions { + testFileDir: string; + evaluatorPrompt?: string; + workspaceRoot?: string; + registryUrl?: string; + cliModule?: CompilerModule; + provider?: string; + model?: string; +} + +export class PrmdEvaluator implements Evaluator { + readonly type = 'prmd'; + private options: PrmdEvaluatorOptions; + private cliModule: CompilerModule | null = null; + + constructor(options: PrmdEvaluatorOptions) { + this.options = options; + if (options.cliModule) { + this.cliModule = options.cliModule; + } + } + + async evaluate(assertion: AssertionDef, context: EvaluatorContext): Promise { + const start = Date.now(); + + try { + const evaluatorContent = await this.resolveEvaluatorContent(assertion); + console.log(`[PrmdEvaluator] Resolved evaluator content (${evaluatorContent?.length || 0} chars)`); + if (evaluatorContent) { + console.log(`[PrmdEvaluator] source: ${assertion.prompt || 'content block'}`); + console.log(`[PrmdEvaluator] preview: ${evaluatorContent.substring(0, 150)}`); + } + + if (!evaluatorContent) { + return { + evaluator: 'prmd', + status: 'error', + reason: 'Could not resolve evaluator prompt content', + duration: Date.now() - start, + }; + } + + // Compile the evaluator prompt with context as parameters + const cli = await this.getCli(); + const compiled = await this.compileEvaluator(cli, evaluatorContent, context); + + console.log(`[PrmdEvaluator] Compiled evaluator (${compiled?.length || 0} chars): ${compiled?.substring(0, 150) || 'null'}`); + + if (!compiled) { + return { + evaluator: 'prmd', + status: 'error', + reason: 'Evaluator prompt compilation failed', + duration: Date.now() - start, + }; + } + + // Execute against LLM using callLLM directly (avoids executeRawText re-compilation) + const executor = new cli.PrompdExecutor(); + + // Resolve provider/model/apiKey — same logic as TestRunner + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const configManager = (cli as any).ConfigManager?.getInstance + ? (cli as any).ConfigManager.getInstance() + : null; + const config = configManager?.config || {}; + + // Priority: assertion-level > run options (UI selector) > config defaults + const provider = assertion.provider || this.options.provider || config.defaultProvider || 'openai'; + const rawModel = assertion.model || this.options.model || config.default_model || config.defaultModel || ''; + const model = rawModel || this.getDefaultModel(provider); + const apiKey = configManager?.getApiKey?.(provider, config) || ''; + + console.log(`[PrmdEvaluator] Executing: provider=${provider}, model=${model}`); + + if (!apiKey && provider !== 'ollama') { + return { + evaluator: 'prmd', + status: 'error', + reason: `No API key configured for provider "${provider}"`, + duration: Date.now() - start, + }; + } + + const execResult = await executor.callLLM(provider, model, compiled, apiKey); + + if (!execResult.success) { + return { + evaluator: 'prmd', + status: 'error', + reason: execResult.error || 'Evaluator LLM execution failed', + duration: Date.now() - start, + }; + } + + const response = execResult.response || execResult.content || ''; + if (!response) { + return { + evaluator: 'prmd', + status: 'error', + reason: 'No response from evaluator', + duration: Date.now() - start, + }; + } + + // Parse PASS/FAIL from response + return this.parseEvaluatorResponse(response, Date.now() - start); + } catch (err) { + return { + evaluator: 'prmd', + status: 'error', + reason: err instanceof Error ? err.message : String(err), + duration: Date.now() - start, + }; + } + } + + private async resolveEvaluatorContent(assertion: AssertionDef): Promise { + // If prompt: is specified, resolve it (registry ref, local file) + if (assertion.prompt) { + return this.resolvePromptTarget(assertion.prompt); + } + + // No prompt: field — use the content block of the .test.prmd + return this.options.evaluatorPrompt || null; + } + + private async resolvePromptTarget(prompt: string): Promise { + // Registry reference: @scope/package@version + if (prompt.startsWith('@')) { + return this.wrapAsInherits(prompt); + } + + // Local file path + const resolved = path.resolve(this.options.testFileDir, prompt); + if (!fs.existsSync(resolved)) { + throw new Error(`Evaluator prompt file not found: ${resolved}`); + } + + return fs.readFileSync(resolved, 'utf-8'); + } + + /** + * Wrap a registry reference as a minimal .prmd that inherits from the evaluator package. + * The compiler handles resolution, download, and caching. + */ + private wrapAsInherits(registryRef: string): string { + return [ + '---', + `inherits: "${registryRef}"`, + 'parameters:', + ' - name: prompt', + ' type: string', + ' - name: response', + ' type: string', + ' - name: params', + ' type: string', + '---', + '', + ].join('\n'); + } + + private async compileEvaluator( + cli: CompilerModule, + content: string, + context: EvaluatorContext + ): Promise { + // If content doesn't start with frontmatter, wrap it with minimal frontmatter + // so the compiler can process it. Content blocks from .test.prmd are raw markdown. + let prmdContent = content; + if (!content.trimStart().startsWith('---')) { + prmdContent = [ + '---', + 'id: evaluator', + 'name: "Test Evaluator"', + 'version: 0.0.1', + 'parameters:', + ' - name: prompt', + ' type: string', + ' - name: response', + ' type: string', + ' - name: params', + ' type: object', + '---', + '', + content, + ].join('\n'); + } + + const memFs = new cli.MemoryFileSystem({ '/evaluator.prmd': prmdContent }); + const compiler = new cli.PrompdCompiler(); + + // Inject evaluation context as template variables + const parameters: Record = { + prompt: context.prompt, + response: context.response, + params: JSON.stringify(context.params, null, 2), + }; + + // Also expose individual params via dot notation + for (const [key, value] of Object.entries(context.params)) { + parameters[`params.${key}`] = String(value); + } + + const result = await compiler.compile('/evaluator.prmd', { + outputFormat: 'markdown', + parameters, + fileSystem: memFs, + workspaceRoot: this.options.workspaceRoot, + registryUrl: this.options.registryUrl, + }); + + // CLI compile() may return a string directly or an object + if (typeof result === 'string') { + return result || null; + } + return result.output || null; + } + + private parseEvaluatorResponse(response: string, duration: number): AssertionResult { + const firstLine = response.trim().split('\n')[0]; + const match = firstLine.match(PASS_FAIL_REGEX); + + if (!match) { + return { + evaluator: 'prmd', + status: 'error', + reason: `Evaluator response did not start with PASS or FAIL. Got: "${firstLine.substring(0, 100)}"`, + duration, + }; + } + + const verdict = match[1].toUpperCase(); + const reason = match[2]?.trim() || undefined; + + return { + evaluator: 'prmd', + status: verdict === 'PASS' ? 'pass' : 'fail', + reason: reason || `Evaluator returned ${verdict}`, + duration, + }; + } + + private getDefaultModel(provider: string): string { + const defaults: Record = { + openai: 'gpt-4o', + anthropic: 'claude-sonnet-4-20250514', + groq: 'llama-3.1-70b-versatile', + google: 'gemini-2.0-flash', + mistral: 'mistral-large-latest', + deepseek: 'deepseek-chat', + }; + return defaults[provider.toLowerCase()] || 'gpt-4o'; + } + + private async getCli(): Promise { + if (!this.cliModule) { + throw new Error( + '@prompd/cli module not provided. Pass it via PrmdEvaluatorOptions.cliModule' + ); + } + return this.cliModule; + } +} diff --git a/packages/test/src/evaluators/ScriptEvaluator.ts b/packages/test/src/evaluators/ScriptEvaluator.ts new file mode 100644 index 0000000..e10f7ee --- /dev/null +++ b/packages/test/src/evaluators/ScriptEvaluator.ts @@ -0,0 +1,157 @@ +/** + * Script Evaluator - runs external scripts with stdin/stdout contract. + * + * Contract: + * - Receives JSON on stdin: { input, output, params, metadata } + * - Exit code 0 = PASS, 1 = FAIL, other = ERROR + * - Stdout = reason (optional) + */ + +import { spawn } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; +import type { Evaluator, EvaluatorContext } from './types'; +import type { AssertionDef, AssertionResult, EvaluateTarget } from '../types'; + +const SCRIPT_TIMEOUT_MS = 30_000; + +export class ScriptEvaluator implements Evaluator { + readonly type = 'script'; + private testFileDir: string; + + constructor(testFileDir: string) { + this.testFileDir = testFileDir; + } + + async evaluate(assertion: AssertionDef, context: EvaluatorContext): Promise { + const start = Date.now(); + const scriptPath = assertion.run; + + if (!scriptPath) { + return { + evaluator: 'script', + status: 'error', + reason: 'No "run" path specified for script evaluator', + duration: Date.now() - start, + }; + } + + const resolvedPath = path.resolve(this.testFileDir, scriptPath); + + if (!fs.existsSync(resolvedPath)) { + return { + evaluator: 'script', + status: 'error', + reason: `Script not found: ${resolvedPath}`, + duration: Date.now() - start, + }; + } + + // Validate script stays within the test file's directory tree + const normalizedScript = path.normalize(resolvedPath); + const normalizedBase = path.normalize(this.testFileDir); + if (!normalizedScript.startsWith(normalizedBase)) { + return { + evaluator: 'script', + status: 'error', + reason: `Script path escapes test directory: ${scriptPath}`, + duration: Date.now() - start, + }; + } + + try { + const result = await this.runScript(resolvedPath, context, assertion); + // Exit 0 = pass, exit 1 = assertion failure, anything else = execution error + const status = result.exitCode === 0 ? 'pass' : result.exitCode === 1 ? 'fail' : 'error'; + const defaultReason = result.exitCode === 0 ? 'Script passed' + : result.exitCode === 1 ? 'Script failed' + : `Script exited with code ${result.exitCode}`; + return { + evaluator: 'script', + status, + reason: result.stdout.trim() || result.stderr.trim() || defaultReason, + duration: Date.now() - start, + }; + } catch (err) { + return { + evaluator: 'script', + status: 'error', + reason: err instanceof Error ? err.message : String(err), + duration: Date.now() - start, + }; + } + } + + private runScript( + scriptPath: string, + context: EvaluatorContext, + assertion: AssertionDef + ): Promise<{ exitCode: number; stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + const { command, args } = this.getRunner(scriptPath); + const child = spawn(command, args, { + cwd: this.testFileDir, + timeout: SCRIPT_TIMEOUT_MS, + stdio: ['pipe', 'pipe', 'pipe'], + shell: process.platform === 'win32', + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data: Buffer) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data: Buffer) => { + stderr += data.toString(); + }); + + child.on('error', (err) => { + reject(new Error(`Failed to spawn script: ${err.message}`)); + }); + + child.on('close', (code) => { + if (code === null) { + reject(new Error('Script process was killed (timeout or signal)')); + return; + } + resolve({ exitCode: code, stdout, stderr }); + }); + + // Send context as JSON on stdin, include target so script knows what to evaluate + const target: EvaluateTarget = assertion.evaluate || 'response'; + const payload = JSON.stringify({ + target, + prompt: context.prompt, + response: context.response, + params: context.params, + metadata: context.metadata, + }); + + child.stdin.write(payload); + child.stdin.end(); + }); + } + + private getRunner(scriptPath: string): { command: string; args: string[] } { + const ext = path.extname(scriptPath).toLowerCase(); + + switch (ext) { + case '.ts': + return { command: 'npx', args: ['tsx', scriptPath] }; + case '.js': + case '.mjs': + return { command: 'node', args: [scriptPath] }; + case '.py': + return { command: 'python', args: [scriptPath] }; + case '.sh': + return { command: 'bash', args: [scriptPath] }; + case '.ps1': + return { command: 'powershell', args: ['-File', scriptPath] }; + default: + // For unknown extensions, try running directly (relies on shebang or OS association) + return { command: scriptPath, args: [] }; + } + } +} diff --git a/packages/test/src/evaluators/types.ts b/packages/test/src/evaluators/types.ts new file mode 100644 index 0000000..c934c11 --- /dev/null +++ b/packages/test/src/evaluators/types.ts @@ -0,0 +1,24 @@ +/** + * Evaluator interfaces for @prompd/test + */ + +import type { AssertionDef, AssertionResult } from '../types'; + +export interface EvaluatorContext { + prompt: string; + response: string; + params: Record; + metadata: { + provider: string; + model: string; + duration: number; + }; +} + +export interface Evaluator { + readonly type: string; + evaluate( + assertion: AssertionDef, + context: EvaluatorContext + ): Promise; +} diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts new file mode 100644 index 0000000..0f0a66c --- /dev/null +++ b/packages/test/src/index.ts @@ -0,0 +1,76 @@ +/** + * @prompd/test - Prompt testing and evaluation framework + * + * Provides test discovery, assertion evaluation, and reporting for .prmd files. + * Consumes @prompd/cli for compilation and execution. + */ + +// Core classes +export { TestRunner } from './TestRunner'; +export { TestParser, TestParseError } from './TestParser'; +export { TestDiscovery } from './TestDiscovery'; +export { EvaluatorEngine } from './EvaluatorEngine'; + +// Evaluators +export { NlpEvaluator } from './evaluators/NlpEvaluator'; +export { ScriptEvaluator } from './evaluators/ScriptEvaluator'; +export { PrmdEvaluator } from './evaluators/PrmdEvaluator'; + +// Reporters +export { ConsoleReporter } from './reporters/ConsoleReporter'; +export { JsonReporter } from './reporters/JsonReporter'; +export { JunitReporter } from './reporters/JunitReporter'; + +// Types +export type { + TestSuite, + TestCase, + AssertionDef, + TestResult, + TestRunResult, + TestSuiteResult, + TestRunSummary, + TestRunOptions, + TestProgressEvent, + TestProgressCallback, + TestStatus, + AssertionStatus, + AssertionResult, + EvaluatorType, + NlpCheck, +} from './types'; + +export type { + Evaluator, + EvaluatorContext, +} from './evaluators/types'; + +export type { + Reporter, +} from './reporters/types'; + +export type { + DiscoveryResult, + DiscoveryError, +} from './TestDiscovery'; + +export type { + EvaluatorEngineOptions, +} from './EvaluatorEngine'; + +// Re-export TestHarness interface from @prompd/cli for convenience +export type { + TestHarness, + TestHarnessResult, + TestHarnessOptions, + TestHarnessProgressEvent, + TestHarnessProgressCallback, +} from '@prompd/cli'; + +export type { + PrmdEvaluatorOptions, +} from './evaluators/PrmdEvaluator'; + +export type { + CompilerModule, +} from './cli-types'; diff --git a/packages/test/src/reporters/ConsoleReporter.ts b/packages/test/src/reporters/ConsoleReporter.ts new file mode 100644 index 0000000..3443427 --- /dev/null +++ b/packages/test/src/reporters/ConsoleReporter.ts @@ -0,0 +1,100 @@ +/** + * Console Reporter - terminal output with pass/fail formatting. + * + * Does NOT use emojis (breaks things per project rules). + * Uses simple text markers: [PASS], [FAIL], [ERROR], [SKIP]. + */ + +import type { Reporter } from './types'; +import type { TestRunResult, TestResult, AssertionResult } from '../types'; + +export class ConsoleReporter implements Reporter { + private verbose: boolean; + + constructor(verbose = false) { + this.verbose = verbose; + } + + report(result: TestRunResult): string { + const lines: string[] = []; + + lines.push(''); + lines.push('=== Prompd Test Results ==='); + lines.push(''); + + for (const suite of result.suites) { + lines.push(` ${suite.suite}`); + + for (const test of suite.results) { + const marker = this.statusMarker(test.status); + const duration = this.formatDuration(test.duration); + const meta = test.execution + ? ` [${test.execution.provider}/${test.execution.model}${test.execution.usage?.totalTokens ? ` ${test.execution.usage.totalTokens}tok` : ''}]` + : ''; + lines.push(` ${marker} ${test.testName} (${duration})${meta}`); + + if (test.status === 'error' && test.error) { + lines.push(` Error: ${test.error}`); + } + + if (this.verbose || test.status === 'fail' || test.status === 'error') { + for (const assertion of test.assertions) { + this.appendAssertionDetail(lines, assertion); + } + } + } + + lines.push(''); + } + + // Summary + const s = result.summary; + lines.push('---'); + lines.push( + `Tests: ${s.passed} passed, ${s.failed} failed, ${s.errors} errors, ${s.skipped} skipped, ${s.total} total` + ); + lines.push(`Time: ${this.formatDuration(s.duration)}`); + if (s.totalTokens) { + lines.push(`Tokens: ${s.totalTokens.toLocaleString()}`); + } + if (s.models && s.models.length > 0) { + lines.push(`Models: ${s.models.join(', ')}`); + } + + if (s.failed > 0 || s.errors > 0) { + lines.push('Result: FAIL'); + } else { + lines.push('Result: PASS'); + } + + lines.push(''); + return lines.join('\n'); + } + + private appendAssertionDetail(lines: string[], assertion: AssertionResult): void { + const marker = this.statusMarker(assertion.status); + const check = assertion.check ? ` (${assertion.check})` : ''; + const duration = this.formatDuration(assertion.duration); + lines.push(` ${marker} ${assertion.evaluator}${check} [${duration}]`); + + if (assertion.reason && (assertion.status !== 'pass' || this.verbose)) { + lines.push(` ${assertion.reason}`); + } + } + + private statusMarker(status: string): string { + switch (status) { + case 'pass': return '[PASS]'; + case 'fail': return '[FAIL]'; + case 'error': return '[ERR ]'; + case 'skip': return '[SKIP]'; + default: return '[????]'; + } + } + + private formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${(ms / 60000).toFixed(1)}m`; + } +} diff --git a/packages/test/src/reporters/JsonReporter.ts b/packages/test/src/reporters/JsonReporter.ts new file mode 100644 index 0000000..6e0ad64 --- /dev/null +++ b/packages/test/src/reporters/JsonReporter.ts @@ -0,0 +1,21 @@ +/** + * JSON Reporter - structured output for programmatic consumption and CI. + */ + +import type { Reporter } from './types'; +import type { TestRunResult } from '../types'; + +export class JsonReporter implements Reporter { + private pretty: boolean; + + constructor(pretty = true) { + this.pretty = pretty; + } + + report(result: TestRunResult): string { + if (this.pretty) { + return JSON.stringify(result, null, 2); + } + return JSON.stringify(result); + } +} diff --git a/packages/test/src/reporters/JunitReporter.ts b/packages/test/src/reporters/JunitReporter.ts new file mode 100644 index 0000000..b404b52 --- /dev/null +++ b/packages/test/src/reporters/JunitReporter.ts @@ -0,0 +1,113 @@ +/** + * JUnit XML Reporter - generates JUnit-compatible XML for CI systems. + * + * Output format follows the JUnit XML schema used by Jenkins, GitHub Actions, + * Azure DevOps, and most CI platforms. + */ + +import type { Reporter } from './types'; +import type { TestRunResult, TestResult } from '../types'; + +export class JunitReporter implements Reporter { + report(result: TestRunResult): string { + const lines: string[] = []; + + lines.push(''); + lines.push( + `` + ); + + for (const suite of result.suites) { + const suiteTests = suite.results.length; + const suiteFailures = suite.results.filter(r => r.status === 'fail').length; + const suiteErrors = suite.results.filter(r => r.status === 'error').length; + const suiteSkipped = suite.results.filter(r => r.status === 'skip').length; + const suiteDuration = suite.results.reduce((sum, r) => sum + r.duration, 0); + + lines.push( + ` ` + ); + + for (const test of suite.results) { + this.appendTestCase(lines, suite.suite, test); + } + + lines.push(' '); + } + + lines.push(''); + return lines.join('\n'); + } + + private appendTestCase(lines: string[], suiteName: string, test: TestResult): void { + const time = (test.duration / 1000).toFixed(3); + + lines.push( + ` ` + ); + + if (test.status === 'fail') { + const failedAssertions = test.assertions.filter(a => a.status === 'fail'); + const message = failedAssertions + .map(a => `${a.evaluator}${a.check ? `(${a.check})` : ''}: ${a.reason || 'failed'}`) + .join('; '); + + lines.push(` `); + lines.push(this.escapeXml(this.buildFailureDetail(test))); + lines.push(' '); + } + + if (test.status === 'error') { + const errorMessage = test.error || 'Unknown error'; + lines.push(` `); + lines.push(this.escapeXml(errorMessage)); + lines.push(' '); + } + + if (test.status === 'skip') { + lines.push(' '); + } + + // Include output as system-out if available + if (test.output) { + lines.push(' '); + lines.push(this.escapeXml(test.output.substring(0, 10000))); + lines.push(' '); + } + + lines.push(' '); + } + + private buildFailureDetail(test: TestResult): string { + const details: string[] = []; + + for (const assertion of test.assertions) { + const prefix = assertion.status === 'pass' ? '[PASS]' : '[FAIL]'; + const check = assertion.check ? ` (${assertion.check})` : ''; + details.push(`${prefix} ${assertion.evaluator}${check}: ${assertion.reason || ''}`); + } + + return details.join('\n'); + } + + private escapeXml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } +} diff --git a/packages/test/src/reporters/types.ts b/packages/test/src/reporters/types.ts new file mode 100644 index 0000000..66e535a --- /dev/null +++ b/packages/test/src/reporters/types.ts @@ -0,0 +1,9 @@ +/** + * Reporter interface for @prompd/test + */ + +import type { TestRunResult } from '../types'; + +export interface Reporter { + report(result: TestRunResult): string; +} diff --git a/packages/test/src/types.ts b/packages/test/src/types.ts new file mode 100644 index 0000000..5c3efdb --- /dev/null +++ b/packages/test/src/types.ts @@ -0,0 +1,142 @@ +/** + * Core type definitions for @prompd/test + */ + +// --- Evaluator taxonomy --- + +export type EvaluatorType = 'nlp' | 'script' | 'prmd'; + +export type NlpCheck = + | 'contains' + | 'not_contains' + | 'matches' + | 'max_tokens' + | 'min_tokens' + | 'max_words' + | 'min_words' + | 'starts_with' + | 'ends_with'; + +// --- Test definition types (parsed from .test.prmd frontmatter) --- + +/** What the evaluator checks: the compiled prompt, the LLM response, or both */ +export type EvaluateTarget = 'prompt' | 'response' | 'both'; + +export interface AssertionDef { + evaluator: EvaluatorType; + /** What to evaluate: 'prompt' (compiled input), 'response' (LLM output), or 'both'. Defaults to 'response'. */ + evaluate?: EvaluateTarget; + // NLP fields + check?: NlpCheck; + value?: string | string[] | number; + // Script fields + run?: string; + // Prmd fields — prompt: registry ref, local file, or omit to use content block + prompt?: string; + provider?: string; + model?: string; +} + +export interface TestCase { + name: string; + params: Record; + assert: AssertionDef[]; + expect_error?: boolean; +} + +export interface TestSuite { + name: string; + description?: string; + target: string; + testFilePath: string; + tests: TestCase[]; + evaluatorPrompt?: string; +} + +// --- Test result types --- + +export type TestStatus = 'pass' | 'fail' | 'error' | 'skip'; +export type AssertionStatus = 'pass' | 'fail' | 'error' | 'skip'; + +export interface AssertionResult { + evaluator: EvaluatorType; + check?: string; + status: AssertionStatus; + reason?: string; + duration: number; +} + +export interface TestExecutionMetadata { + provider: string; + model: string; + duration: number; + usage?: { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + }; +} + +export interface TestResult { + suite: string; + testName: string; + status: TestStatus; + duration: number; + assertions: AssertionResult[]; + output?: string; + compiledInput?: string; + error?: string; + execution?: TestExecutionMetadata; +} + +export interface TestRunSummary { + total: number; + passed: number; + failed: number; + errors: number; + skipped: number; + duration: number; + totalTokens?: number; + providers?: string[]; + models?: string[]; +} + +export interface TestRunResult { + suites: TestSuiteResult[]; + summary: TestRunSummary; +} + +export interface TestSuiteResult { + suite: string; + testFilePath: string; + results: TestResult[]; +} + +// --- Options --- + +export interface TestRunOptions { + evaluators?: EvaluatorType[]; + noLlm?: boolean; + reporter?: 'console' | 'json' | 'junit'; + failFast?: boolean; + runAll?: boolean; + verbose?: boolean; + workspaceRoot?: string; + registryUrl?: string; + // Default provider/model for test execution (overridden by .prmd frontmatter) + provider?: string; + model?: string; + /** AbortSignal for cancelling a running test */ + signal?: AbortSignal; +} + +// --- Progress callback --- + +export type TestProgressEvent = + | { type: 'suite_start'; suite: string; testCount: number } + | { type: 'test_start'; suite: string; testName: string } + | { type: 'test_complete'; suite: string; testName: string; result: TestResult } + | { type: 'suite_complete'; suite: string; results: TestResult[] } + | { type: 'assertion_complete'; suite: string; testName: string; assertion: AssertionResult }; + +export type TestProgressCallback = (event: TestProgressEvent) => void; diff --git a/packages/test/tsconfig.json b/packages/test/tsconfig.json new file mode 100644 index 0000000..a556ad0 --- /dev/null +++ b/packages/test/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/prompd-service/package.json b/prompd-service/package.json index 53a01bc..47e392a 100644 --- a/prompd-service/package.json +++ b/prompd-service/package.json @@ -1,6 +1,6 @@ { "name": "@prompd/service", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.10", "description": "Standalone workflow scheduler service for Prompd - runs 24/7 independently", "main": "src/server.js", "type": "module",