@pi-harness/pi-harness 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/apps/web/dist/assets/index-BcLXLKqO.js +24 -0
- package/apps/web/dist/assets/index-xF647pQJ.css +2 -0
- package/apps/web/dist/index.html +2 -2
- package/apps/web/package.json +6 -6
- package/apps/web/profile/cordis.yml +1 -0
- package/apps/web/server-dist/bin.js +27 -2
- package/apps/web/server-dist/bin.js.map +1 -1
- package/apps/web/src/server/bin.ts +27 -2
- package/apps/web/test/bin.test.ts +228 -5
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +113 -42
- package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/package.json +1 -1
- package/node_modules/@pi-harness/cli/dist/main.d.ts +1 -0
- package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/main.js +11 -0
- package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.js +2 -4
- package/node_modules/@pi-harness/cli/dist/relaunch.js.map +1 -1
- package/node_modules/@pi-harness/cli/package.json +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts +1 -0
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.js +39 -5
- package/node_modules/@pi-harness/host-webserver/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/host-webserver/package.json +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.js +12 -3
- package/node_modules/@pi-harness/web-app/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/web-app/package.json +1 -1
- package/package.json +7 -7
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +113 -42
- package/packages/api-gateway/dist/index.js.map +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/package.json +1 -1
- package/packages/api-gateway/src/index.ts +104 -40
- package/packages/api-gateway/src/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/at-file.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-fetch.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/openpets.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/test/index.test.ts +438 -4
- package/packages/api-gateway/test/packed-package.test.ts +70 -0
- package/packages/api-gateway/test/reference-docs.test.ts +50 -0
- package/packages/api-gateway/test/release-package.test.ts +32 -1
- package/packages/api-gateway/test/release-version.test.ts +40 -0
- package/packages/bundle-web-app/dist/index.d.ts.map +1 -1
- package/packages/bundle-web-app/dist/index.js +12 -3
- package/packages/bundle-web-app/dist/index.js.map +1 -1
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/bundle-web-app/src/index.ts +13 -3
- package/packages/bundle-web-app/test/index.test.ts +83 -2
- package/packages/cli/dist/main.d.ts +1 -0
- package/packages/cli/dist/main.d.ts.map +1 -1
- package/packages/cli/dist/main.js +11 -0
- package/packages/cli/dist/main.js.map +1 -1
- package/packages/cli/dist/relaunch.d.ts.map +1 -1
- package/packages/cli/dist/relaunch.js +2 -4
- package/packages/cli/dist/relaunch.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/main.ts +11 -0
- package/packages/cli/src/relaunch.ts +2 -3
- package/packages/cli/test/cli.test.ts +3 -1
- package/packages/cli/test/main.test.ts +134 -0
- package/packages/cli/test/relaunch.test.ts +86 -11
- package/packages/host-webserver/dist/index.d.ts +1 -0
- package/packages/host-webserver/dist/index.d.ts.map +1 -1
- package/packages/host-webserver/dist/index.js +39 -5
- package/packages/host-webserver/dist/index.js.map +1 -1
- package/packages/host-webserver/package.json +1 -1
- package/packages/host-webserver/src/index.ts +44 -6
- package/packages/host-webserver/test/index.test.ts +36 -5
- package/apps/web/dist/assets/index-BVI_IS-R.css +0 -2
- package/apps/web/dist/assets/index-Bo9CNI_U.js +0 -22
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { execFile as execFileCallback } from "node:child_process";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { join, sep } from "node:path";
|
|
7
7
|
import { DatabaseSync } from "node:sqlite";
|
|
8
8
|
import { Context } from "@deepseek-ai/cordis";
|
|
9
9
|
import timerPlugin from "@deepseek-ai/cordis-plugin-timer";
|
|
10
10
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
11
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
12
12
|
import webServerPlugin from "@pi-harness/host-webserver";
|
|
13
13
|
import { PiPluginUiRegistry, PiToolRegistry } from "@pi-harness/core";
|
|
14
|
+
import type { MarketplacePlugin } from "../src/marketplace.js";
|
|
14
15
|
import apiPlugin from "../src/index.js";
|
|
15
16
|
|
|
17
|
+
// Every shipped catalog entry that npm has to install names a bare package, so `packageName` and its npm package name coincide and the install route cannot show whether it narrows the specifier. This entry is the case where the two differ: a deep import path whose npm package is only the first two segments. It is hoisted because the module mock below reads it while the test module body is still in its temporal dead zone.
|
|
18
|
+
const { subpathPlugin } = vi.hoisted(() => ({
|
|
19
|
+
subpathPlugin: {
|
|
20
|
+
id: "subpath-toolkit",
|
|
21
|
+
packageName: "@example-scope/toolkit/plugins/subpath",
|
|
22
|
+
version: "1.2.3",
|
|
23
|
+
name: "Subpath toolkit",
|
|
24
|
+
description: "Catalog entry whose profile name is a deep import of its npm package",
|
|
25
|
+
author: "example",
|
|
26
|
+
repository: "https://example.invalid/toolkit",
|
|
27
|
+
license: "MIT",
|
|
28
|
+
source: "community",
|
|
29
|
+
status: "experimental",
|
|
30
|
+
category: { id: "tools", label: "工具" },
|
|
31
|
+
capabilities: ["subpath-install"],
|
|
32
|
+
hooks: ["apply"],
|
|
33
|
+
profile: { name: "@example-scope/toolkit/plugins/subpath", config: {} },
|
|
34
|
+
} satisfies MarketplacePlugin,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Only `MARKETPLACE_PLUGINS` is widened: the search and pagination helpers close over the module's own catalog, so the listing endpoints keep serving exactly the shipped entries.
|
|
38
|
+
vi.mock("../src/marketplace.js", async (importOriginal) => {
|
|
39
|
+
const actual = await importOriginal<{ MARKETPLACE_PLUGINS: readonly MarketplacePlugin[] }>();
|
|
40
|
+
return { ...actual, MARKETPLACE_PLUGINS: [...actual.MARKETPLACE_PLUGINS, subpathPlugin] };
|
|
41
|
+
});
|
|
42
|
+
|
|
16
43
|
const contexts: Context[] = [];
|
|
17
44
|
const temporaryDirectories: string[] = [];
|
|
18
45
|
const execFile = promisify(execFileCallback);
|
|
@@ -2554,7 +2581,7 @@ describe("API gateway plugin", () => {
|
|
|
2554
2581
|
expect(failed.status).toBe(500);
|
|
2555
2582
|
await expect(failed.json()).resolves.toEqual({ error: "fatal: index file corrupt" });
|
|
2556
2583
|
|
|
2557
|
-
await writeFile(shim, "#!/bin/sh\
|
|
2584
|
+
await writeFile(shim, "#!/bin/sh\n{ printf ' M tracked.txt\\n'; seq 1 40000 | sed 's/^/?? untracked-/;s/$/.txt/'; } | tr '\\n' '\\0'\n", { mode: 0o755 });
|
|
2558
2585
|
const truncated = await fetch(context.webServer.url + "/api/files");
|
|
2559
2586
|
expect(truncated.status).toBe(200);
|
|
2560
2587
|
const payload = (await truncated.json()) as { items: { path: string }[]; truncated?: boolean };
|
|
@@ -2781,4 +2808,411 @@ describe("API gateway plugin", () => {
|
|
|
2781
2808
|
await expect(readFile(join(targetDir, targetFiles[0] ?? ""), "utf8")).resolves.toContain("x".repeat(100 * 1024));
|
|
2782
2809
|
await expect(stat(join(directory, "escape.jsonl"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
2783
2810
|
});
|
|
2811
|
+
|
|
2812
|
+
test("decodes a request body whose multi-byte characters straddle chunk boundaries", async () => {
|
|
2813
|
+
const context = new Context();
|
|
2814
|
+
contexts.push(context);
|
|
2815
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2816
|
+
const session = { sessionId: "utf8-body-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2817
|
+
const prompts: string[] = [];
|
|
2818
|
+
context.provide("piRuntime", {
|
|
2819
|
+
session,
|
|
2820
|
+
prompt: (prompt: string) => {
|
|
2821
|
+
prompts.push(prompt);
|
|
2822
|
+
return Promise.resolve();
|
|
2823
|
+
},
|
|
2824
|
+
} as never);
|
|
2825
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2826
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2827
|
+
await context.plugin(apiPlugin);
|
|
2828
|
+
|
|
2829
|
+
const prompt = "汉字".repeat(64);
|
|
2830
|
+
const encoded = new TextEncoder().encode(JSON.stringify({ prompt }));
|
|
2831
|
+
// Cuts the first 汉 in half, so a decoder that works chunk by chunk emits U+FFFD instead of the character.
|
|
2832
|
+
const split = 12;
|
|
2833
|
+
const body = new ReadableStream<Uint8Array>({
|
|
2834
|
+
async start(controller) {
|
|
2835
|
+
controller.enqueue(encoded.slice(0, split));
|
|
2836
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
|
|
2837
|
+
controller.enqueue(encoded.slice(split));
|
|
2838
|
+
controller.close();
|
|
2839
|
+
},
|
|
2840
|
+
});
|
|
2841
|
+
const response = await fetch(context.webServer.url + "/api/prompt", {
|
|
2842
|
+
method: "POST",
|
|
2843
|
+
headers: { "content-type": "application/json" },
|
|
2844
|
+
body,
|
|
2845
|
+
duplex: "half",
|
|
2846
|
+
} as RequestInit & { duplex: "half" });
|
|
2847
|
+
|
|
2848
|
+
expect(response.status).toBe(200);
|
|
2849
|
+
expect(prompts).toEqual([prompt]);
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
test("keeps a message that a later trajectory event repeats instead of reporting it as circular", async () => {
|
|
2853
|
+
const context = new Context();
|
|
2854
|
+
contexts.push(context);
|
|
2855
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2856
|
+
type Listener = (event: { type: string; [key: string]: unknown }) => void;
|
|
2857
|
+
const listeners = new Set<Listener>();
|
|
2858
|
+
const message = { role: "assistant", content: [{ type: "text", text: "done" }], timestamp: 2 };
|
|
2859
|
+
const cyclic: Record<string, unknown> = { type: "turn_end" };
|
|
2860
|
+
cyclic.self = cyclic;
|
|
2861
|
+
const session = {
|
|
2862
|
+
sessionId: "shared-message-session",
|
|
2863
|
+
sessionFile: undefined,
|
|
2864
|
+
messages: [] as Array<Record<string, unknown>>,
|
|
2865
|
+
isStreaming: false,
|
|
2866
|
+
subscribe(listener: Listener) {
|
|
2867
|
+
listeners.add(listener);
|
|
2868
|
+
return () => listeners.delete(listener);
|
|
2869
|
+
},
|
|
2870
|
+
};
|
|
2871
|
+
context.provide("piRuntime", {
|
|
2872
|
+
session,
|
|
2873
|
+
prompt: () => {
|
|
2874
|
+
// Pi pushes the very object carried by the event into the message list, so both fields of the response reference one instance.
|
|
2875
|
+
session.messages.push(message);
|
|
2876
|
+
listeners.forEach((listener) => listener({ type: "message_end", message }));
|
|
2877
|
+
listeners.forEach((listener) => listener(cyclic as { type: string }));
|
|
2878
|
+
return Promise.resolve();
|
|
2879
|
+
},
|
|
2880
|
+
} as never);
|
|
2881
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2882
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2883
|
+
await context.plugin(apiPlugin);
|
|
2884
|
+
|
|
2885
|
+
await fetch(context.webServer.url + "/api/prompt", {
|
|
2886
|
+
method: "POST",
|
|
2887
|
+
headers: { "content-type": "application/json" },
|
|
2888
|
+
body: JSON.stringify({ prompt: "hello" }),
|
|
2889
|
+
});
|
|
2890
|
+
const response = await fetch(context.webServer.url + "/api/session");
|
|
2891
|
+
expect(response.status).toBe(200);
|
|
2892
|
+
const payload = (await response.json()) as { messages: unknown[]; events: Array<Record<string, unknown>> };
|
|
2893
|
+
expect(payload.messages).toEqual([message]);
|
|
2894
|
+
expect(payload.events[0]).toEqual({ type: "message_end", message });
|
|
2895
|
+
expect(payload.events[1]).toEqual({ type: "turn_end", self: "[Circular]" });
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
test("returns git status paths verbatim when they hold spaces, non-ASCII characters or a rename", async () => {
|
|
2899
|
+
const context = new Context();
|
|
2900
|
+
contexts.push(context);
|
|
2901
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-quoted-paths-"));
|
|
2902
|
+
temporaryDirectories.push(directory);
|
|
2903
|
+
await execFile("git", ["init", "-q"], { cwd: directory });
|
|
2904
|
+
await execFile("git", ["config", "user.email", "pi-harness@test.invalid"], { cwd: directory });
|
|
2905
|
+
await execFile("git", ["config", "user.name", "Pi Harness Test"], { cwd: directory });
|
|
2906
|
+
await writeFile(join(directory, "notes draft.md"), "before\n", "utf8");
|
|
2907
|
+
await writeFile(join(directory, "old name.md"), "keep\n", "utf8");
|
|
2908
|
+
await execFile("git", ["add", "-A"], { cwd: directory });
|
|
2909
|
+
await execFile("git", ["commit", "-qm", "initial"], { cwd: directory });
|
|
2910
|
+
await writeFile(join(directory, "notes draft.md"), "after\n", "utf8");
|
|
2911
|
+
await execFile("git", ["mv", "old name.md", "renamed name.md"], { cwd: directory });
|
|
2912
|
+
await writeFile(join(directory, "汉字.txt"), "新建\n", "utf8");
|
|
2913
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2914
|
+
const session = { sessionId: "quoted-paths-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2915
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2916
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2917
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2918
|
+
await context.plugin(apiPlugin);
|
|
2919
|
+
|
|
2920
|
+
const response = await fetch(context.webServer.url + "/api/files");
|
|
2921
|
+
expect(response.status).toBe(200);
|
|
2922
|
+
const payload = (await response.json()) as { items: { path: string; status: string; label: string }[] };
|
|
2923
|
+
expect(payload.items).toHaveLength(3);
|
|
2924
|
+
expect(payload.items).toEqual(
|
|
2925
|
+
expect.arrayContaining([
|
|
2926
|
+
{ path: "notes draft.md", status: "M", label: "modified" },
|
|
2927
|
+
{ path: "renamed name.md", status: "R", label: "modified" },
|
|
2928
|
+
{ path: "汉字.txt", status: "??", label: "untracked" },
|
|
2929
|
+
]),
|
|
2930
|
+
);
|
|
2931
|
+
|
|
2932
|
+
const modified = payload.items.find((item) => item.status === "M");
|
|
2933
|
+
const diff = await fetch(context.webServer.url + "/api/files/diff?path=" + encodeURIComponent(modified?.path ?? ""));
|
|
2934
|
+
expect(diff.status).toBe(200);
|
|
2935
|
+
const diffPayload = (await diff.json()) as { path: string; diff: string };
|
|
2936
|
+
expect(diffPayload.path).toBe("notes draft.md");
|
|
2937
|
+
expect(diffPayload.diff).toContain("after");
|
|
2938
|
+
|
|
2939
|
+
const commit = await fetch(context.webServer.url + "/api/files/commit", {
|
|
2940
|
+
method: "POST",
|
|
2941
|
+
headers: { "content-type": "application/json" },
|
|
2942
|
+
body: JSON.stringify({ paths: ["notes draft.md", "汉字.txt"], message: "Commit quoted paths" }),
|
|
2943
|
+
});
|
|
2944
|
+
expect(commit.status).toBe(200);
|
|
2945
|
+
const after = await execFile("git", ["status", "--short", "-z"], { cwd: directory });
|
|
2946
|
+
expect(after.stdout).not.toContain("notes draft.md");
|
|
2947
|
+
expect(after.stdout).not.toContain("汉字.txt");
|
|
2948
|
+
});
|
|
2949
|
+
|
|
2950
|
+
test("refuses to delete a session outside the session directory or without confirmation", async () => {
|
|
2951
|
+
const context = new Context();
|
|
2952
|
+
contexts.push(context);
|
|
2953
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-session-delete-"));
|
|
2954
|
+
temporaryDirectories.push(directory);
|
|
2955
|
+
const sessionDir = join(directory, "sessions");
|
|
2956
|
+
await mkdir(sessionDir);
|
|
2957
|
+
const victim = join(directory, "secrets.jsonl");
|
|
2958
|
+
await writeFile(victim, "keep me\n", "utf8");
|
|
2959
|
+
const note = join(sessionDir, "notes.txt");
|
|
2960
|
+
await writeFile(note, "keep me\n", "utf8");
|
|
2961
|
+
const target = join(sessionDir, "2026-08-30T00-00-00-000Z_target.jsonl");
|
|
2962
|
+
await writeFile(target, "{}\n", "utf8");
|
|
2963
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2964
|
+
const session = {
|
|
2965
|
+
sessionId: "session-delete-session",
|
|
2966
|
+
sessionFile: undefined,
|
|
2967
|
+
messages: [],
|
|
2968
|
+
isStreaming: false,
|
|
2969
|
+
sessionManager: { getSessionDir: () => sessionDir, isPersisted: () => true, getEntries: () => [] },
|
|
2970
|
+
subscribe: () => () => {},
|
|
2971
|
+
};
|
|
2972
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2973
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2974
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2975
|
+
await context.plugin(apiPlugin);
|
|
2976
|
+
const post = (body: unknown) =>
|
|
2977
|
+
fetch(context.webServer.url + "/api/session/delete", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
2978
|
+
|
|
2979
|
+
const pinned = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
2980
|
+
method: "POST",
|
|
2981
|
+
headers: { "content-type": "application/json" },
|
|
2982
|
+
body: JSON.stringify({ path: target, pinned: true }),
|
|
2983
|
+
});
|
|
2984
|
+
expect(pinned.status).toBe(200);
|
|
2985
|
+
|
|
2986
|
+
const outside = await post({ path: victim, confirm: true });
|
|
2987
|
+
expect(outside.status).toBe(400);
|
|
2988
|
+
await expect(outside.json()).resolves.toEqual({ error: "Invalid session path" });
|
|
2989
|
+
// The literal `..` segment has to survive onto the wire: path.join would collapse it into the plain `outside` path and the request would no longer escape anything.
|
|
2990
|
+
const traversalPath = `${sessionDir}${sep}..${sep}secrets.jsonl`;
|
|
2991
|
+
expect(traversalPath).not.toBe(victim);
|
|
2992
|
+
const traversal = await post({ path: traversalPath, confirm: true });
|
|
2993
|
+
expect(traversal.status).toBe(400);
|
|
2994
|
+
await expect(traversal.json()).resolves.toEqual({ error: "Invalid session path" });
|
|
2995
|
+
const suffix = await post({ path: note, confirm: true });
|
|
2996
|
+
expect(suffix.status).toBe(400);
|
|
2997
|
+
const unconfirmed = await post({ path: target });
|
|
2998
|
+
expect(unconfirmed.status).toBe(400);
|
|
2999
|
+
await expect(unconfirmed.json()).resolves.toEqual({ error: "confirm must be true to delete a session" });
|
|
3000
|
+
await expect(readFile(victim, "utf8")).resolves.toBe("keep me\n");
|
|
3001
|
+
await expect(readFile(note, "utf8")).resolves.toBe("keep me\n");
|
|
3002
|
+
await expect(readFile(target, "utf8")).resolves.toBe("{}\n");
|
|
3003
|
+
|
|
3004
|
+
const deleted = await post({ path: target, confirm: true });
|
|
3005
|
+
expect(deleted.status).toBe(200);
|
|
3006
|
+
await expect(deleted.json()).resolves.toMatchObject({ deleted: true, path: target });
|
|
3007
|
+
await expect(stat(target)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3008
|
+
expect(JSON.parse(await readFile(join(sessionDir, ".pi-harness-session-meta.json"), "utf8"))).toEqual({});
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
test("keeps batch deletion going past a failing session and persists the metadata it did remove", async () => {
|
|
3012
|
+
const context = new Context();
|
|
3013
|
+
contexts.push(context);
|
|
3014
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-batch-delete-"));
|
|
3015
|
+
temporaryDirectories.push(directory);
|
|
3016
|
+
const first = join(directory, "2026-08-30T00-00-00-000Z_first.jsonl");
|
|
3017
|
+
const missing = join(directory, "2026-08-30T00-00-01-000Z_missing.jsonl");
|
|
3018
|
+
const stuck = join(directory, "2026-08-30T00-00-02-000Z_stuck.jsonl");
|
|
3019
|
+
const last = join(directory, "2026-08-30T00-00-03-000Z_last.jsonl");
|
|
3020
|
+
await writeFile(first, "{}\n", "utf8");
|
|
3021
|
+
await writeFile(last, "{}\n", "utf8");
|
|
3022
|
+
// A directory cannot be unlinked, which is the closest reproducible stand-in for a session the harness is not allowed to remove.
|
|
3023
|
+
await mkdir(stuck);
|
|
3024
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3025
|
+
const session = {
|
|
3026
|
+
sessionId: "batch-delete-session",
|
|
3027
|
+
sessionFile: undefined,
|
|
3028
|
+
messages: [],
|
|
3029
|
+
isStreaming: false,
|
|
3030
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3031
|
+
subscribe: () => () => {},
|
|
3032
|
+
};
|
|
3033
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3034
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3035
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3036
|
+
await context.plugin(apiPlugin);
|
|
3037
|
+
|
|
3038
|
+
for (const path of [first, missing, stuck, last]) {
|
|
3039
|
+
const pinned = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
3040
|
+
method: "POST",
|
|
3041
|
+
headers: { "content-type": "application/json" },
|
|
3042
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
3043
|
+
});
|
|
3044
|
+
expect(pinned.status).toBe(200);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
const response = await fetch(context.webServer.url + "/api/sessions/batch", {
|
|
3048
|
+
method: "POST",
|
|
3049
|
+
headers: { "content-type": "application/json" },
|
|
3050
|
+
body: JSON.stringify({ action: "delete", paths: [first, missing, stuck, last], confirm: true }),
|
|
3051
|
+
});
|
|
3052
|
+
expect(response.status).toBe(409);
|
|
3053
|
+
await expect(response.json()).resolves.toMatchObject({ action: "delete", count: 3, failed: [{ path: stuck }] });
|
|
3054
|
+
await expect(stat(first)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3055
|
+
await expect(stat(last)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3056
|
+
expect((await stat(stuck)).isDirectory()).toBe(true);
|
|
3057
|
+
expect(JSON.parse(await readFile(join(directory, ".pi-harness-session-meta.json"), "utf8"))).toEqual({ [stuck]: { pinned: true } });
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
test("installs a marketplace package into the package that owns the profile rather than the launch directory", async () => {
|
|
3061
|
+
const context = new Context();
|
|
3062
|
+
contexts.push(context);
|
|
3063
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-install-"));
|
|
3064
|
+
temporaryDirectories.push(directory);
|
|
3065
|
+
const harnessDir = join(directory, "harness");
|
|
3066
|
+
const profileDir = join(harnessDir, "profile");
|
|
3067
|
+
const workspace = join(directory, "workspace");
|
|
3068
|
+
const shimDirectory = join(directory, "bin");
|
|
3069
|
+
await mkdir(profileDir, { recursive: true });
|
|
3070
|
+
await mkdir(workspace);
|
|
3071
|
+
await mkdir(shimDirectory);
|
|
3072
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3073
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3074
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
3075
|
+
const npmLog = join(directory, "npm.log");
|
|
3076
|
+
await writeFile(join(shimDirectory, "npm"), `#!/bin/sh\nprintf '%s|%s\\n' "$(pwd)" "$*" >> ${JSON.stringify(npmLog)}\n`, { mode: 0o755 });
|
|
3077
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3078
|
+
const session = { sessionId: "npm-install-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3079
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3080
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3081
|
+
context.provide("piHarnessLaunch", { cwd: workspace, agentDir: workspace, configPath, args: [], requestExit() {} });
|
|
3082
|
+
const created: Array<{ name?: unknown }> = [];
|
|
3083
|
+
context.reflect.provide("loader", {
|
|
3084
|
+
entries: () => [],
|
|
3085
|
+
create: (options: { name?: unknown }) => {
|
|
3086
|
+
created.push(options);
|
|
3087
|
+
return Promise.resolve("marketplace-entry");
|
|
3088
|
+
},
|
|
3089
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
3090
|
+
remove: () => Promise.resolve(),
|
|
3091
|
+
});
|
|
3092
|
+
await context.plugin(apiPlugin);
|
|
3093
|
+
|
|
3094
|
+
const originalPath = process.env.PATH ?? "";
|
|
3095
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3096
|
+
let response: Response;
|
|
3097
|
+
try {
|
|
3098
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3099
|
+
method: "POST",
|
|
3100
|
+
headers: { "content-type": "application/json" },
|
|
3101
|
+
body: JSON.stringify({ id: "cordis-timer" }),
|
|
3102
|
+
});
|
|
3103
|
+
} finally {
|
|
3104
|
+
process.env.PATH = originalPath;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
expect(response.status).toBe(200);
|
|
3108
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: "cordis-timer" } });
|
|
3109
|
+
const invocations = (await readFile(npmLog, "utf8")).trim().split("\n");
|
|
3110
|
+
expect(invocations).toHaveLength(1);
|
|
3111
|
+
const [invocationCwd, invocationArgs] = (invocations[0] ?? "").split("|");
|
|
3112
|
+
expect(await realpath(invocationCwd ?? "")).toBe(await realpath(harnessDir));
|
|
3113
|
+
expect(invocationArgs).toMatch(/^install --save-exact --package-lock=false @deepseek-ai\/cordis-plugin-timer@\d/);
|
|
3114
|
+
expect(created).toHaveLength(1);
|
|
3115
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@deepseek-ai/cordis-plugin-timer"');
|
|
3116
|
+
await expect(stat(join(workspace, "package.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
test("hands npm the package name rather than the deep import path the profile entry uses", async () => {
|
|
3120
|
+
const context = new Context();
|
|
3121
|
+
contexts.push(context);
|
|
3122
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-subpath-"));
|
|
3123
|
+
temporaryDirectories.push(directory);
|
|
3124
|
+
const harnessDir = join(directory, "harness");
|
|
3125
|
+
const profileDir = join(harnessDir, "profile");
|
|
3126
|
+
const shimDirectory = join(directory, "bin");
|
|
3127
|
+
await mkdir(profileDir, { recursive: true });
|
|
3128
|
+
await mkdir(shimDirectory);
|
|
3129
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3130
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3131
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
3132
|
+
const npmLog = join(directory, "npm.log");
|
|
3133
|
+
await writeFile(join(shimDirectory, "npm"), `#!/bin/sh\nprintf '%s\\n' "$*" >> ${JSON.stringify(npmLog)}\n`, { mode: 0o755 });
|
|
3134
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3135
|
+
const session = { sessionId: "npm-subpath-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3136
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3137
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3138
|
+
context.provide("piHarnessLaunch", { cwd: harnessDir, agentDir: harnessDir, configPath, args: [], requestExit() {} });
|
|
3139
|
+
const created: Array<{ name?: unknown }> = [];
|
|
3140
|
+
context.reflect.provide("loader", {
|
|
3141
|
+
entries: () => [],
|
|
3142
|
+
create: (options: { name?: unknown }) => {
|
|
3143
|
+
created.push(options);
|
|
3144
|
+
return Promise.resolve("marketplace-entry");
|
|
3145
|
+
},
|
|
3146
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
3147
|
+
remove: () => Promise.resolve(),
|
|
3148
|
+
});
|
|
3149
|
+
await context.plugin(apiPlugin);
|
|
3150
|
+
|
|
3151
|
+
const originalPath = process.env.PATH ?? "";
|
|
3152
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3153
|
+
let response: Response;
|
|
3154
|
+
try {
|
|
3155
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3156
|
+
method: "POST",
|
|
3157
|
+
headers: { "content-type": "application/json" },
|
|
3158
|
+
body: JSON.stringify({ id: subpathPlugin.id }),
|
|
3159
|
+
});
|
|
3160
|
+
} finally {
|
|
3161
|
+
process.env.PATH = originalPath;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
expect(response.status).toBe(200);
|
|
3165
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: subpathPlugin.id } });
|
|
3166
|
+
// `npm install @example-scope/toolkit/plugins/subpath@1.2.3` is not a specifier npm can resolve, so the deep import path must be narrowed to its package before it reaches the process.
|
|
3167
|
+
expect((await readFile(npmLog, "utf8")).trim()).toBe("install --save-exact --package-lock=false @example-scope/toolkit@1.2.3");
|
|
3168
|
+
expect(created).toEqual([expect.objectContaining({ name: "@example-scope/toolkit/plugins/subpath" })]);
|
|
3169
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@example-scope/toolkit/plugins/subpath"');
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
test("removes the files npm created when the installed marketplace plugin fails to load", async () => {
|
|
3173
|
+
const context = new Context();
|
|
3174
|
+
contexts.push(context);
|
|
3175
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-rollback-"));
|
|
3176
|
+
temporaryDirectories.push(directory);
|
|
3177
|
+
const harnessDir = join(directory, "harness");
|
|
3178
|
+
const profileDir = join(harnessDir, "profile");
|
|
3179
|
+
const shimDirectory = join(directory, "bin");
|
|
3180
|
+
await mkdir(profileDir, { recursive: true });
|
|
3181
|
+
await mkdir(shimDirectory);
|
|
3182
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3183
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3184
|
+
const profileBefore = '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n';
|
|
3185
|
+
await writeFile(configPath, profileBefore, "utf8");
|
|
3186
|
+
await writeFile(join(shimDirectory, "npm"), '#!/bin/sh\nprintf \'{ "lockfileVersion": 3 }\\n\' > "$(pwd)/package-lock.json"\n', { mode: 0o755 });
|
|
3187
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3188
|
+
const session = { sessionId: "npm-rollback-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3189
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3190
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3191
|
+
context.provide("piHarnessLaunch", { cwd: harnessDir, agentDir: harnessDir, configPath, args: [], requestExit() {} });
|
|
3192
|
+
context.reflect.provide("loader", {
|
|
3193
|
+
entries: () => [],
|
|
3194
|
+
create: () => Promise.reject(new Error("plugin entry failed to load")),
|
|
3195
|
+
remove: () => Promise.resolve(),
|
|
3196
|
+
});
|
|
3197
|
+
await context.plugin(apiPlugin);
|
|
3198
|
+
|
|
3199
|
+
const originalPath = process.env.PATH ?? "";
|
|
3200
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3201
|
+
let response: Response;
|
|
3202
|
+
try {
|
|
3203
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3204
|
+
method: "POST",
|
|
3205
|
+
headers: { "content-type": "application/json" },
|
|
3206
|
+
body: JSON.stringify({ id: "cordis-timer" }),
|
|
3207
|
+
});
|
|
3208
|
+
} finally {
|
|
3209
|
+
process.env.PATH = originalPath;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
expect(response.status).toBe(502);
|
|
3213
|
+
await expect(response.json()).resolves.toEqual({ error: "plugin entry failed to load" });
|
|
3214
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
|
|
3215
|
+
await expect(readFile(join(harnessDir, "package.json"), "utf8")).resolves.toBe('{ "name": "harness" }\n');
|
|
3216
|
+
await expect(stat(join(harnessDir, "package-lock.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3217
|
+
});
|
|
2784
3218
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const fixtures: string[] = [];
|
|
10
|
+
const repositoryRoot = resolve(import.meta.dirname, "../../..");
|
|
11
|
+
|
|
12
|
+
// Stands in for npm so the smoke test can be exercised without packing and globally installing the real workspace: `pack` reports tarball names and `install` lays out
|
|
13
|
+
// the tree the script inspects, optionally without the files the root bin map points at.
|
|
14
|
+
const fakeNpmSource = `import { mkdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import process from "node:process";
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
if (args[0] === "pack") {
|
|
20
|
+
process.stdout.write(args.includes("--workspace") ? "core.tgz\\n" : "harness.tgz\\n");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if (args[0] !== "install") process.exit(1);
|
|
24
|
+
const prefix = args[args.indexOf("--prefix") + 1];
|
|
25
|
+
const harnessRoot = join(prefix, "lib", "node_modules", "@pi-harness", "pi-harness");
|
|
26
|
+
const agentRoot = join(harnessRoot, "node_modules", "@earendil-works", "pi-coding-agent");
|
|
27
|
+
const write = (path, content) => {
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
writeFileSync(path, content);
|
|
30
|
+
};
|
|
31
|
+
const bin = { "pi-harness": "./apps/web/server-dist/bin.js", pih: "./packages/cli/dist/bin.js" };
|
|
32
|
+
write(join(harnessRoot, "package.json"), JSON.stringify({ name: "@pi-harness/pi-harness", version: "9.9.9", bin, dependencies: { "@earendil-works/pi-coding-agent": "0.84.4" } }));
|
|
33
|
+
write(join(agentRoot, "package.json"), JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: "0.84.4", dependencies: {} }));
|
|
34
|
+
write(join(agentRoot, "dist", "cli", "args.js"), "");
|
|
35
|
+
if (process.env.PACKED_PACKAGE_FIXTURE_BINS === "1") for (const target of Object.values(bin)) write(join(harnessRoot, target), "");
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const createFakeNpm = async (): Promise<string> => {
|
|
39
|
+
const fixture = await mkdtemp(resolve(tmpdir(), "pi-harness-packed-package-fixture-"));
|
|
40
|
+
fixtures.push(fixture);
|
|
41
|
+
const fakeNpm = resolve(fixture, "npm.mjs");
|
|
42
|
+
await writeFile(fakeNpm, fakeNpmSource);
|
|
43
|
+
return fakeNpm;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const runSmokeTest = async (fakeNpm: string, bins: boolean): ReturnType<typeof execFileAsync> =>
|
|
47
|
+
execFileAsync(process.execPath, [resolve(repositoryRoot, "scripts/test-packed-package.mjs")], {
|
|
48
|
+
env: { ...process.env, npm_execpath: fakeNpm, PACKED_PACKAGE_FIXTURE_BINS: bins ? "1" : "0" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await Promise.all(fixtures.splice(0).map((fixture) => rm(fixture, { recursive: true, force: true })));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("packed package smoke test", () => {
|
|
56
|
+
it("accepts a tarball whose bin entrypoints are present", async () => {
|
|
57
|
+
const fakeNpm = await createFakeNpm();
|
|
58
|
+
|
|
59
|
+
const { stdout } = await runSmokeTest(fakeNpm, true);
|
|
60
|
+
|
|
61
|
+
expect(stdout).toContain("Packed package smoke test passed");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("rejects a tarball that ships no runnable bin entrypoint", async () => {
|
|
65
|
+
// `npm pack --ignore-scripts` never runs prepack, so an unbuilt workspace produces a tarball with a bin map pointing at files the tarball does not contain.
|
|
66
|
+
const fakeNpm = await createFakeNpm();
|
|
67
|
+
|
|
68
|
+
await expect(runSmokeTest(fakeNpm, false)).rejects.toThrow(/missing the pi-harness entrypoint \.\/apps\/web\/server-dist\/bin\.js/u);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
const repositoryRoot = resolve(import.meta.dirname, "../../..");
|
|
6
|
+
|
|
7
|
+
const readText = async (path: string): Promise<string> => readFile(resolve(repositoryRoot, path), "utf8");
|
|
8
|
+
|
|
9
|
+
// The number of built-in plugins that `docs/README.reference.md` still has no `###` section for. It may only ever go down.
|
|
10
|
+
const undocumentedBudget = 53;
|
|
11
|
+
|
|
12
|
+
describe("reference documentation", () => {
|
|
13
|
+
it("documents every registered API route", async () => {
|
|
14
|
+
// The reference is the inventory a reader audits the unauthenticated local port with, so an undocumented route is an invisible attack surface.
|
|
15
|
+
const gateway = await readText("packages/api-gateway/src/index.ts");
|
|
16
|
+
const reference = await readText("docs/README.reference.md");
|
|
17
|
+
const routes = [...gateway.matchAll(/path: "(\/api[^"]*)"/gu)].map((match) => match[1] ?? "");
|
|
18
|
+
|
|
19
|
+
expect(routes.length).toBeGreaterThan(0);
|
|
20
|
+
for (const route of routes) expect(reference, `${route} is not documented`).toContain(`\`${route}\``);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("claims documentation coverage only for the plugins it actually describes", async () => {
|
|
24
|
+
const readme = await readText("README.md");
|
|
25
|
+
const reference = await readText("docs/README.reference.md");
|
|
26
|
+
const sections = new Set([...reference.matchAll(/^### (.+)$/gmu)].map((match) => (match[1] ?? "").toLowerCase().replaceAll(/[^a-z]/gu, "")));
|
|
27
|
+
const plugins = (await readdir(resolve(repositoryRoot, "packages/core/src/plugins"), { withFileTypes: true }))
|
|
28
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
|
|
29
|
+
.map((entry) => entry.name.replace(/\.ts$/u, "").replaceAll("-", ""));
|
|
30
|
+
const undocumented = plugins.filter((plugin) => !sections.has(plugin));
|
|
31
|
+
|
|
32
|
+
// A ratchet, not a floor: documenting more plugins only lowers the count, while a plugin that arrives without a reference section pushes it over the budget. Lower the budget whenever it drops.
|
|
33
|
+
expect(undocumented.length, `undocumented built-in plugins: ${undocumented.join(", ")}`).toBeLessThanOrEqual(undocumentedBudget);
|
|
34
|
+
expect(readme).not.toMatch(/documents every built-in plugin/u);
|
|
35
|
+
expect(reference).not.toMatch(/canonical reference for the complete plugin catalog/u);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("documents the model the shipped web profile actually selects", async () => {
|
|
39
|
+
const profile = await readText("apps/web/profile/cordis.yml");
|
|
40
|
+
const reference = await readText("docs/README.reference.md");
|
|
41
|
+
const readme = await readText("README.md");
|
|
42
|
+
const provider = /provider: .*\?\? '([^']+)'/u.exec(profile)?.[1];
|
|
43
|
+
const model = /model: .*\?\? '([^']+)'/u.exec(profile)?.[1];
|
|
44
|
+
|
|
45
|
+
expect(provider).toBeDefined();
|
|
46
|
+
expect(model).toBeDefined();
|
|
47
|
+
expect(reference).toContain(`\`${provider ?? ""}/${model ?? ""}\``);
|
|
48
|
+
for (const variable of ["PI_HARNESS_PROVIDER", "PI_HARNESS_MODEL"]) expect(readme, `${variable} is undocumented`).toContain(variable);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
|
|
@@ -35,4 +35,35 @@ describe("release package", () => {
|
|
|
35
35
|
expect(workflow).toContain("Verify package availability");
|
|
36
36
|
expect(workflow).not.toContain("RELEASE_TAG_EXISTS");
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
it("cleans every compiled workspace output", async () => {
|
|
40
|
+
// `tsc` never removes stale emit, so an output directory that no clean script touches keeps publishing files whose sources are gone.
|
|
41
|
+
const rootManifest = await readJson("package.json");
|
|
42
|
+
const rootClean = (rootManifest.scripts as Record<string, string>).clean ?? "";
|
|
43
|
+
const outputs: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (const root of ["packages", "apps", "examples"]) {
|
|
46
|
+
for (const entry of await readdir(resolve(repositoryRoot, root), { withFileTypes: true })) {
|
|
47
|
+
if (!entry.isDirectory()) continue;
|
|
48
|
+
const workspace = `${root}/${entry.name}`;
|
|
49
|
+
const buildConfigPath = resolve(repositoryRoot, workspace, "tsconfig.build.json");
|
|
50
|
+
const hasBuildConfig = await stat(buildConfigPath).then(
|
|
51
|
+
() => true,
|
|
52
|
+
() => false,
|
|
53
|
+
);
|
|
54
|
+
if (!hasBuildConfig) continue;
|
|
55
|
+
const buildConfig = JSON.parse(await readFile(buildConfigPath, "utf8")) as { compilerOptions?: { outDir?: string } };
|
|
56
|
+
const outDir = buildConfig.compilerOptions?.outDir;
|
|
57
|
+
expect(outDir, `${workspace}/tsconfig.build.json declares no outDir`).toBeDefined();
|
|
58
|
+
const workspaceManifest = await readJson(`${workspace}/package.json`);
|
|
59
|
+
const workspaceClean = (workspaceManifest.scripts as Record<string, string> | undefined)?.clean;
|
|
60
|
+
outputs.push(`${workspace}/${outDir ?? ""}`);
|
|
61
|
+
expect(workspaceClean !== undefined || rootClean.includes(`${workspace}/${outDir ?? ""}`), `${workspace}/${outDir ?? ""} survives npm run clean`).toBe(
|
|
62
|
+
true,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(outputs.length).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
38
69
|
});
|