@runfusion/fusion 0.26.0 → 0.27.1

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.
Files changed (174) hide show
  1. package/dist/bin.js +12847 -2514
  2. package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
  3. package/dist/client/assets/AgentDetailView-shgiiUb4.js +18 -0
  4. package/dist/client/assets/{AgentsView-CV3vm7Qk.css → AgentsView-B3ADnF0D.css} +1 -1
  5. package/dist/client/assets/{AgentsView-D6Zi5zfP.js → AgentsView-CpwqOVDz.js} +12 -7
  6. package/dist/client/assets/ChatView-DyRBOIKL.js +1 -0
  7. package/dist/client/assets/{DevServerView--_WBvIDQ.js → DevServerView-Cdelj9-m.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-xedtR-Rd.js → DirectoryPicker-C0kmRv0u.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-Bg2oaZks.js → DocumentsView-B94U9ijs.js} +1 -1
  10. package/dist/client/assets/{EvalsView-B3uOCXfr.js → EvalsView-O_4YWy--.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-Bx6yXVS5.js → ExperimentalAgentOnboardingModal-CkEiF85-.js} +1 -1
  12. package/dist/client/assets/InsightsView-D-Qe0tRr.js +11 -0
  13. package/dist/client/assets/{MemoryView-xcN_eouf.js → MemoryView-CoRUmRvb.js} +2 -2
  14. package/dist/client/assets/NodesView-DQzXjcLc.js +14 -0
  15. package/dist/client/assets/{PiExtensionsManager-Cc8aAZXg.js → PiExtensionsManager-Dn1LmFbq.js} +2 -2
  16. package/dist/client/assets/PluginManager-Y0fs-6No.js +1 -0
  17. package/dist/client/assets/{ResearchView-CERNf7sJ.js → ResearchView-CjOxKhdS.js} +1 -1
  18. package/dist/client/assets/{SettingsModal-B1r0yASu.js → SettingsModal-Bg1-3JO_.js} +1 -1
  19. package/dist/client/assets/{SettingsModal-Cis-4Lot.css → SettingsModal-Ci0_sqbU.css} +1 -1
  20. package/dist/client/assets/SettingsModal-DL7tjJQa.js +31 -0
  21. package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
  22. package/dist/client/assets/{SetupWizardModal-D1q548_L.js → SetupWizardModal-DuzYPbuJ.js} +1 -1
  23. package/dist/client/assets/{SkillsView-ClLM6u6p.js → SkillsView-BIFoVNUf.js} +1 -1
  24. package/dist/client/assets/{StashRecoveryView-ze0pEZ5U.js → StashRecoveryView-C52KsV7f.js} +1 -1
  25. package/dist/client/assets/{TodoView-CTmIfy2M.js → TodoView-sS_mT0Y7.js} +2 -2
  26. package/dist/client/assets/{dashboard-view-CyWN-d02.js → dashboard-view-BWGH_fAq.js} +1 -1
  27. package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
  28. package/dist/client/assets/dashboard-view-MB-86hAu.js +21 -0
  29. package/dist/client/assets/{folder-open-BZuKESeq.js → folder-open-B9cwJ-OX.js} +1 -1
  30. package/dist/client/assets/index-BOjPRqEk.js +692 -0
  31. package/dist/client/assets/index-BmSEq8Rb.css +1 -0
  32. package/dist/client/assets/{star-D75YKEq-.js → star-BDn04UYV.js} +1 -1
  33. package/dist/client/assets/{upload-BYYTgWFj.js → upload-zdPPycKQ.js} +1 -1
  34. package/dist/client/assets/{users-RS90Aii3.js → users-CPYZjK2g.js} +1 -1
  35. package/dist/client/index.html +2 -2
  36. package/dist/client/version.json +1 -1
  37. package/dist/droid-cli/package.json +1 -1
  38. package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
  39. package/dist/extension.js +7433 -1920
  40. package/dist/pi-claude-cli/package.json +1 -1
  41. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
  42. package/dist/pi-claude-cli/src/provider.ts +7 -1
  43. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +19 -1
  44. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +20 -2
  45. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
  46. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
  47. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
  48. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
  49. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
  50. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
  51. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
  52. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
  53. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
  54. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
  55. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +21 -5
  56. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
  57. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
  58. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
  59. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
  60. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
  61. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
  62. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
  63. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
  64. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
  65. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
  66. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
  67. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
  68. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
  69. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +46 -2
  70. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
  71. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
  72. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
  73. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
  74. package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
  75. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
  76. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
  77. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
  78. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
  79. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
  80. package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
  81. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
  82. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
  83. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
  84. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
  85. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
  86. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
  87. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
  88. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
  89. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  90. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  91. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  92. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  93. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  94. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  95. package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
  96. package/dist/plugins/fusion-plugin-reports/package.json +18 -2
  97. package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
  98. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
  99. package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
  100. package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
  101. package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
  102. package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
  103. package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
  104. package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
  105. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
  106. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
  107. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
  108. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
  109. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
  110. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
  111. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
  112. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
  113. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
  114. package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
  115. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
  116. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
  117. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
  118. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
  119. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
  120. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
  121. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
  122. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
  123. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
  124. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
  125. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
  126. package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
  127. package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
  128. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
  129. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
  130. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
  131. package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
  132. package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
  133. package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
  134. package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
  135. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
  136. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
  137. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
  138. package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
  139. package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
  140. package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
  141. package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
  142. package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
  143. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
  144. package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
  145. package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
  146. package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
  147. package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
  148. package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
  149. package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
  150. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
  151. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
  152. package/dist/plugins/fusion-plugin-roadmap/bundled.js +1528 -29391
  153. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
  154. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  155. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  156. package/package.json +1 -1
  157. package/skill/fusion/SKILL.md +1 -1
  158. package/skill/fusion/references/engine-tools.md +2 -2
  159. package/skill/fusion/references/extension-tools.md +4 -3
  160. package/skill/fusion/references/fusion-capabilities.md +1 -1
  161. package/skill/fusion/workflows/task-management.md +3 -1
  162. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
  163. package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +0 -18
  164. package/dist/client/assets/ChatView-CAHjY9uO.js +0 -1
  165. package/dist/client/assets/InsightsView-Q1zvtF4F.js +0 -11
  166. package/dist/client/assets/NodesView-RxXg58_Q.js +0 -14
  167. package/dist/client/assets/PluginManager-BEkyBajl.js +0 -1
  168. package/dist/client/assets/SettingsModal-BLsac7CJ.js +0 -31
  169. package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
  170. package/dist/client/assets/dashboard-view-4xAN3yO5.js +0 -21
  171. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  172. package/dist/client/assets/index-Bdw6llW6.js +0 -692
  173. package/dist/client/assets/index-CZGlyJuS.css +0 -1
  174. package/dist/plugins/fusion-plugin-roadmap/bundled.css +0 -1093
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion/pi-claude-cli",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
5
5
  "license": "MIT",
6
6
  "private": true,
@@ -1188,28 +1188,42 @@ describe("streamViaCli", { timeout: 90_000 }, () => {
1188
1188
  expect(doneEvent.message.content).toBeDefined();
1189
1189
  });
1190
1190
 
1191
- it("logs stderr at warn level on close even with exit code 0", async () => {
1192
- const model = mockModels[0] as any;
1193
- const context = {
1194
- messages: [{ role: "user", content: "Hello" }],
1195
- };
1196
-
1197
- const warnSpy = vi.spyOn(console, "warn");
1198
-
1199
- streamViaCli(model, context);
1200
- await vi.advanceTimersByTimeAsync(0);
1201
-
1202
- const proc = (spawn as any).mock.results[0].value;
1203
-
1204
- proc.stderr.emit("data", Buffer.from("minor warning from cli"));
1205
- proc.emit("close", 0, null);
1206
- proc.stdout.end();
1207
- await vi.advanceTimersByTimeAsync(100);
1208
-
1209
- expect(warnSpy).toHaveBeenCalledWith(
1210
- expect.stringContaining("minor warning from cli"),
1211
- );
1212
- });
1191
+ it.each([
1192
+ { code: 0, shouldWarn: false },
1193
+ { code: 42, shouldWarn: true },
1194
+ ])(
1195
+ "FN-3815: routes close stderr logging by exit code=$code",
1196
+ async ({ code, shouldWarn }) => {
1197
+ const model = mockModels[0] as any;
1198
+ const context = {
1199
+ messages: [{ role: "user", content: "Hello" }],
1200
+ };
1201
+
1202
+ const warnSpy = vi.spyOn(console, "warn");
1203
+
1204
+ streamViaCli(model, context);
1205
+ await vi.advanceTimersByTimeAsync(0);
1206
+
1207
+ const proc = (spawn as any).mock.results[0].value;
1208
+
1209
+ proc.stderr.emit("data", Buffer.from("minor warning from cli"));
1210
+ proc.emit("close", code, null);
1211
+ proc.stdout.end();
1212
+ await vi.advanceTimersByTimeAsync(100);
1213
+
1214
+ const stderrWarnCalls = warnSpy.mock.calls.filter(([message]) =>
1215
+ typeof message === "string" &&
1216
+ message.includes("[pi-claude-cli] Claude CLI stderr on close:"),
1217
+ );
1218
+
1219
+ if (shouldWarn) {
1220
+ expect(stderrWarnCalls).toHaveLength(1);
1221
+ expect(stderrWarnCalls[0]?.[0]).toContain("minor warning from cli");
1222
+ } else {
1223
+ expect(stderrWarnCalls).toHaveLength(0);
1224
+ }
1225
+ },
1226
+ );
1213
1227
 
1214
1228
  it("warns when subprocess closes successfully with no content events", async () => {
1215
1229
  const model = mockModels[0] as any;
@@ -252,7 +252,13 @@ export function streamViaCli(
252
252
  if (broken) return; // Break-early kill, expected
253
253
  const stderr = getStderr().trim();
254
254
  if (stderr) {
255
- console.warn(`[pi-claude-cli] Claude CLI stderr on close: ${stderr}`);
255
+ if (code === 0 || code === null) {
256
+ // FN-3815: Claude CLI writes benign MCP bring-up diagnostics to stderr
257
+ // on clean/abort shutdown; keep these debug-only to avoid false TUI warnings.
258
+ debugLog(`Claude CLI stderr on close (clean exit): ${stderr}`);
259
+ } else {
260
+ console.warn(`[pi-claude-cli] Claude CLI stderr on close: ${stderr}`);
261
+ }
256
262
  }
257
263
  if (code !== 0 && code !== null) {
258
264
  const message = stderr
@@ -2,5 +2,23 @@
2
2
  "id": "fusion-plugin-cli-printing-press",
3
3
  "name": "CLI Printing Press",
4
4
  "version": "0.1.0",
5
- "description": "Generate and manage CLIs for external services using cli-printing-press"
5
+ "description": "Guided wizard for drafting external service CLI definitions",
6
+ "dashboardViews": [
7
+ {
8
+ "viewId": "wizard",
9
+ "label": "Create Service CLI",
10
+ "componentPath": "./dashboard-view",
11
+ "icon": "Wand2",
12
+ "placement": "primary",
13
+ "order": 60
14
+ },
15
+ {
16
+ "viewId": "manage",
17
+ "label": "Manage Service CLIs",
18
+ "componentPath": "./manage-view",
19
+ "icon": "List",
20
+ "placement": "primary",
21
+ "order": 61
22
+ }
23
+ ]
6
24
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/cli-printing-press",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "CLI Printing Press plugin package for Fusion",
6
6
  "private": true,
@@ -8,6 +8,14 @@
8
8
  ".": {
9
9
  "types": "./src/index.ts",
10
10
  "import": "./src/index.ts"
11
+ },
12
+ "./dashboard-view": {
13
+ "types": "./src/dashboard-view.tsx",
14
+ "import": "./src/dashboard-view.tsx"
15
+ },
16
+ "./manage-view": {
17
+ "types": "./src/manage-view.tsx",
18
+ "import": "./src/manage-view.tsx"
11
19
  }
12
20
  },
13
21
  "scripts": {
@@ -16,10 +24,20 @@
16
24
  },
17
25
  "dependencies": {
18
26
  "@fusion/core": "workspace:*",
19
- "@fusion/plugin-sdk": "workspace:*"
27
+ "@fusion/dashboard": "workspace:*",
28
+ "@fusion/plugin-sdk": "workspace:*",
29
+ "express": "^5.1.0",
30
+ "lucide-react": "^0.542.0",
31
+ "react": "^19.0.0",
32
+ "react-dom": "^19.2.4"
20
33
  },
21
34
  "devDependencies": {
35
+ "@testing-library/jest-dom": "^6.6.3",
36
+ "@testing-library/react": "^16.3.2",
37
+ "@testing-library/user-event": "^14.6.1",
38
+ "@types/express": "^5.0.5",
22
39
  "@types/node": "^25.5.2",
40
+ "@types/react": "^19.0.0",
23
41
  "typescript": "^5.7.0",
24
42
  "vitest": "^3.2.4"
25
43
  }
@@ -0,0 +1,99 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { CliPrintingPressTestRunner } from "../run/TestRunnerPanel";
6
+ import type { ServiceDraft } from "../wizard/types";
7
+
8
+ vi.mock("lucide-react", () => ({
9
+ Play: () => null,
10
+ RefreshCw: () => null,
11
+ CheckCircle2: () => null,
12
+ AlertTriangle: () => null,
13
+ }));
14
+
15
+ function makeDraft(): ServiceDraft {
16
+ const now = new Date().toISOString();
17
+ return {
18
+ id: "draft-1",
19
+ name: "Demo",
20
+ slug: "demo",
21
+ description: "",
22
+ baseUrl: "https://example.com",
23
+ transport: "http",
24
+ endpoints: [{ id: "e1", name: "Ping", method: "GET", path: "/ping", params: "q" }],
25
+ credential: { kind: "none" },
26
+ createdAt: now,
27
+ updatedAt: now,
28
+ };
29
+ }
30
+
31
+ describe("CliPrintingPressTestRunner", () => {
32
+ afterEach(() => {
33
+ cleanup();
34
+ vi.restoreAllMocks();
35
+ });
36
+
37
+ it("submits run request and renders output states", async () => {
38
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
39
+ const url = String(input);
40
+ if (url.endsWith("/run")) {
41
+ return {
42
+ ok: true,
43
+ json: async () => ({ stdout: "ok", stderr: "", exitCode: 0, durationMs: 12, timedOut: false, argv: ["demo.mjs", "--endpoint", "e1", "--q", "***"] }),
44
+ };
45
+ }
46
+ if (url.endsWith("/regenerate")) {
47
+ return { ok: true, json: async () => ({ draft: makeDraft(), artifact: { draftId: "draft-1", slug: "demo", binPath: "/tmp/demo.mjs", entrypoint: "node", generatedAt: new Date().toISOString() } }) };
48
+ }
49
+ return { ok: false, status: 404, json: async () => ({ error: "missing" }) };
50
+ });
51
+
52
+ vi.stubGlobal("fetch", fetchMock);
53
+
54
+ const user = userEvent.setup();
55
+ render(<CliPrintingPressTestRunner draftId="draft-1" draft={makeDraft()} />);
56
+
57
+ expect(screen.getByRole("combobox")).toBeTruthy();
58
+ await user.type(screen.getByLabelText("q"), "hello");
59
+ await user.type(screen.getByPlaceholderText("api_key"), "secret-value");
60
+ await user.click(screen.getByRole("button", { name: /^Run$/i }));
61
+
62
+ await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining("/run"), expect.objectContaining({ method: "POST" })));
63
+ const runCall = fetchMock.mock.calls.find((call) => String(call[0]).endsWith("/run"));
64
+ expect(runCall).toBeTruthy();
65
+ expect(JSON.parse(String((runCall?.[1] as RequestInit).body))).toMatchObject({ endpointId: "e1", params: { q: "hello" } });
66
+ expect(await screen.findByText("ok")).toBeTruthy();
67
+ expect(screen.queryByText("secret-value")).toBeNull();
68
+ });
69
+
70
+ it("renders error and timeout output states", async () => {
71
+ let calls = 0;
72
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
73
+ const url = String(input);
74
+ if (url.endsWith("/run")) {
75
+ calls += 1;
76
+ if (calls === 1) {
77
+ return {
78
+ ok: true,
79
+ json: async () => ({ stdout: "", stderr: "boom", exitCode: 1, durationMs: 22, timedOut: false, argv: ["demo.mjs"] }),
80
+ };
81
+ }
82
+ return {
83
+ ok: true,
84
+ json: async () => ({ stdout: "", stderr: "", exitCode: null, durationMs: 33, timedOut: true, argv: ["demo.mjs"] }),
85
+ };
86
+ }
87
+ return { ok: false, status: 404, json: async () => ({ error: "missing" }) };
88
+ });
89
+
90
+ vi.stubGlobal("fetch", fetchMock);
91
+ const user = userEvent.setup();
92
+ render(<CliPrintingPressTestRunner draftId="draft-1" draft={makeDraft()} />);
93
+
94
+ await user.click(screen.getByRole("button", { name: /^Run$/i }));
95
+ expect(await screen.findByText("boom")).toBeTruthy();
96
+ await user.click(screen.getByRole("button", { name: /^Run$/i }));
97
+ expect(await screen.findByText("Failed")).toBeTruthy();
98
+ });
99
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { makeFakeRegistry } from "./fixtures/registry.js";
3
+ import { InvalidCredentialPlacementError, OAuthNotSupportedError } from "../store/cli-press-types.js";
4
+
5
+ describe("cli press store config flow", () => {
6
+ it("round-trips service/spec/settings CRUD", () => {
7
+ const h = makeFakeRegistry();
8
+ try {
9
+ const created = h.store.createService({
10
+ slug: "gamma",
11
+ displayName: "Gamma Service",
12
+ description: "gamma",
13
+ baseUrl: "https://gamma.example.com",
14
+ sourceKind: "manual",
15
+ });
16
+ expect(h.store.getService(created.id)?.slug).toBe("gamma");
17
+
18
+ const spec = h.store.createSpec({
19
+ serviceId: created.id,
20
+ name: "gamma-cli",
21
+ version: "1.0.0",
22
+ generatorVersion: "cli-printing-press",
23
+ specJson: JSON.stringify({ id: created.id, regeneratedAt: "before" }),
24
+ status: "draft",
25
+ });
26
+
27
+ const updated = h.store.updateSpec(spec.id, {
28
+ status: "generated",
29
+ generatedAt: new Date().toISOString(),
30
+ specJson: JSON.stringify({ id: created.id, regeneratedAt: "after", artifactPath: "/tmp/gamma" }),
31
+ });
32
+ expect(updated.status).toBe("generated");
33
+ expect(JSON.parse(updated.specJson)).toMatchObject({ regeneratedAt: "after" });
34
+
35
+ const setting = h.store.setSetting({
36
+ serviceId: created.id,
37
+ key: "runner.timeoutMs",
38
+ value: "120000",
39
+ scope: "wizard",
40
+ });
41
+ expect(h.store.listSettings(created.id).find((entry) => entry.id === setting.id)?.value).toBe("120000");
42
+
43
+ h.store.deleteSpec(spec.id);
44
+ expect(h.store.getSpec(spec.id)).toBeUndefined();
45
+ h.store.deleteService(created.id);
46
+ expect(h.store.getService(created.id)).toBeUndefined();
47
+ } finally {
48
+ h.cleanup();
49
+ }
50
+ });
51
+
52
+ it("stores credential values encoded and does not expose raw secrets", () => {
53
+ const h = makeFakeRegistry();
54
+ try {
55
+ const acme = h.services.acme;
56
+ const credential = h.store.listCredentials(acme.id)[0];
57
+ expect(credential.value).toMatchObject({ encoding: "base64" });
58
+ expect(JSON.stringify(credential.value)).not.toContain("acme-secret");
59
+ } finally {
60
+ h.cleanup();
61
+ }
62
+ });
63
+
64
+ it("rejects oauth credentials and mismatched placement", () => {
65
+ const h = makeFakeRegistry();
66
+ try {
67
+ const serviceId = h.services.acme.id;
68
+ expect(() =>
69
+ h.store.createCredential({
70
+ serviceId,
71
+ name: "oauth",
72
+ kind: "oauth",
73
+ placement: { kind: "oauth", provider: "acme" } as never,
74
+ value: { encoding: "base64", value: "abc" },
75
+ } as never),
76
+ ).toThrow(OAuthNotSupportedError);
77
+
78
+ expect(() =>
79
+ h.store.createCredential({
80
+ serviceId,
81
+ name: "bad-placement",
82
+ kind: "header",
83
+ placement: { kind: "query_param", queryParam: "token" } as never,
84
+ value: { encoding: "base64", value: "abc" },
85
+ } as never),
86
+ ).toThrow(InvalidCredentialPlacementError);
87
+ } finally {
88
+ h.cleanup();
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,40 @@
1
+ // @vitest-environment jsdom
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { CliPrintingPressWizardView } from "../dashboard-view";
6
+
7
+ vi.mock("lucide-react", () => ({ Wand2: () => null }));
8
+
9
+ describe("CliPrintingPressWizardView", () => {
10
+ beforeEach(() => {
11
+ vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({ id: "draft-1" }) })));
12
+ });
13
+
14
+ it("walks steps, gates validation, and posts draft", async () => {
15
+ const user = userEvent.setup();
16
+ render(<CliPrintingPressWizardView />);
17
+
18
+ const next = screen.getByRole("button", { name: "Next" });
19
+ expect((next as HTMLButtonElement).disabled).toBe(true);
20
+
21
+ await user.type(screen.getByLabelText("Name"), "GitHub");
22
+ await user.type(screen.getByLabelText("Slug"), "github");
23
+ await user.type(screen.getByLabelText("Base URL"), "https://api.github.com");
24
+ expect((next as HTMLButtonElement).disabled).toBe(false);
25
+
26
+ await user.click(next);
27
+ await user.click(screen.getByRole("button", { name: "Next" }));
28
+
29
+ await user.type(screen.getByPlaceholderText("Name"), "List Repos");
30
+ await user.type(screen.getByPlaceholderText("/path"), "/user/repos");
31
+ await user.click(screen.getByRole("button", { name: "Next" }));
32
+ await user.click(screen.getByRole("button", { name: "Next" }));
33
+
34
+ await user.click(screen.getByRole("button", { name: "Save draft" }));
35
+ expect(fetch).toHaveBeenCalledTimes(1);
36
+ const [, options] = (fetch as any).mock.calls[0];
37
+ expect(JSON.parse(options.body)).toMatchObject({ name: "GitHub", slug: "github", baseUrl: "https://api.github.com", transport: "http" });
38
+ expect(await screen.findByText(/Saved — draft id draft-1/)).toBeTruthy();
39
+ });
40
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createCliPrintingPressRoutes } from "../routes/wizard-routes.js";
3
+ import { makeFakeRegistry } from "./fixtures/registry.js";
4
+
5
+ function route(method: string, path: string) {
6
+ const found = createCliPrintingPressRoutes().find((entry) => entry.method === method && entry.path === path);
7
+ if (!found) throw new Error(`missing route ${method} ${path}`);
8
+ return found;
9
+ }
10
+
11
+ describe("dashboard API route contracts", () => {
12
+ it("defines expected wizard/list/detail/run endpoints", () => {
13
+ const routes = createCliPrintingPressRoutes();
14
+ expect(routes.map((entry) => `${entry.method} ${entry.path}`)).toEqual(
15
+ expect.arrayContaining([
16
+ "POST /drafts",
17
+ "GET /drafts",
18
+ "GET /drafts/:id",
19
+ "PUT /drafts/:id",
20
+ "POST /drafts/:id/regenerate",
21
+ "POST /drafts/:id/run",
22
+ ]),
23
+ );
24
+ });
25
+
26
+ it("supports happy and error API paths", async () => {
27
+ const h = makeFakeRegistry();
28
+ try {
29
+ const ctx = { taskStore: { getRootDir: () => h.rootDir, getDatabase: () => h.db } } as any;
30
+ const listRes = await route("GET", "/drafts").handler({ params: {} }, ctx);
31
+ expect(listRes.status).toBe(200);
32
+
33
+ const missRes = await route("GET", "/drafts/:id").handler({ params: { id: "missing" } }, ctx);
34
+ expect(missRes.status).toBe(404);
35
+
36
+ const badRun = await route("POST", "/drafts/:id/run").handler({ params: { id: h.services.acme.id }, body: { endpointId: "", params: {} } }, ctx);
37
+ expect(badRun.status).toBe(400);
38
+ } finally {
39
+ h.cleanup();
40
+ }
41
+ });
42
+
43
+ it("documents plugin-prefixed mount contract", () => {
44
+ expect("/api/plugins/cli-printing-press/drafts").toContain("/api/plugins/cli-printing-press/");
45
+ });
46
+ });
@@ -0,0 +1,50 @@
1
+ import { mkdtemp, readdir } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { createDraftStore, NotFoundError } from "../storage/draft-store";
6
+ import type { ServiceDraft } from "../wizard/types";
7
+
8
+ function makeDraft(): ServiceDraft {
9
+ const now = new Date().toISOString();
10
+ return { id: "", name: "Demo", slug: "demo", description: "", baseUrl: "https://example.com", transport: "http", endpoints: [{ id: "e1", name: "Ping", method: "GET", path: "/ping" }], credential: { kind: "none" }, createdAt: now, updatedAt: now };
11
+ }
12
+
13
+ describe("draft store", () => {
14
+ it("creates, lists, gets, and deletes drafts", async () => {
15
+ const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
16
+ const store = createDraftStore({ rootDir });
17
+ const created = await store.create(makeDraft());
18
+ expect(created.id).toBeTruthy();
19
+ const list = await store.list();
20
+ expect(list).toHaveLength(1);
21
+ expect(await store.get(created.id)).toMatchObject({ id: created.id, slug: "demo" });
22
+ await store.delete(created.id);
23
+ expect(await store.get(created.id)).toBeNull();
24
+ });
25
+
26
+ it("updates an existing draft and replaces endpoints", async () => {
27
+ const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
28
+ const store = createDraftStore({ rootDir });
29
+ const created = await store.create(makeDraft());
30
+
31
+ const updated = await store.update(created.id, {
32
+ name: "Renamed",
33
+ endpoints: [{ id: "e2", name: "Health", method: "GET", path: "/health" }],
34
+ });
35
+
36
+ expect(updated.name).toBe("Renamed");
37
+ expect(updated.endpoints).toHaveLength(1);
38
+ expect(updated.endpoints[0]?.id).toBe("e2");
39
+ expect(updated.updatedAt).not.toBe(created.updatedAt);
40
+
41
+ const draftFiles = await readdir(join(rootDir, ".fusion", "plugins", "cli-printing-press", "drafts"));
42
+ expect(draftFiles.some((entry) => entry.includes(".tmp-"))).toBe(false);
43
+ });
44
+
45
+ it("throws NotFoundError on update for unknown ids", async () => {
46
+ const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
47
+ const store = createDraftStore({ rootDir });
48
+ await expect(store.update("missing", { name: "Nope" })).rejects.toBeInstanceOf(NotFoundError);
49
+ });
50
+ });
@@ -0,0 +1,80 @@
1
+ import { execSync } from "node:child_process";
2
+ import { expect, vi } from "vitest";
3
+
4
+ type ExecResult = {
5
+ stdout?: string;
6
+ stderr?: string;
7
+ code?: number;
8
+ timeoutAfterMs?: number;
9
+ };
10
+
11
+ type ExecCall = {
12
+ command: string;
13
+ options: Record<string, unknown>;
14
+ };
15
+
16
+ const calls: ExecCall[] = [];
17
+ const queue: ExecResult[] = [];
18
+
19
+ vi.mock("node:child_process", () => {
20
+ const execMock = vi.fn((command: string, options: Record<string, unknown>, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
21
+ calls.push({ command, options });
22
+ const next = queue.shift() ?? { stdout: "", stderr: "", code: 0 };
23
+ if (next.timeoutAfterMs) {
24
+ const err = new Error(`Command timed out after ${next.timeoutAfterMs}ms`) as Error & { killed?: boolean; signal?: string };
25
+ err.killed = true;
26
+ err.signal = "SIGTERM";
27
+ callback(err, next.stdout ?? "", next.stderr ?? "");
28
+ return { kill: () => true };
29
+ }
30
+ if ((next.code ?? 0) !== 0) {
31
+ const err = new Error(next.stderr || `Command failed with code ${next.code}`) as Error & {
32
+ code?: number;
33
+ stdout?: string;
34
+ stderr?: string;
35
+ };
36
+ err.code = next.code;
37
+ err.stdout = next.stdout ?? "";
38
+ err.stderr = next.stderr ?? "";
39
+ callback(err, next.stdout ?? "", next.stderr ?? "");
40
+ return { kill: () => true };
41
+ }
42
+ callback(null, next.stdout ?? "", next.stderr ?? "");
43
+ return { kill: () => true };
44
+ });
45
+
46
+ execMock[Symbol.for("nodejs.util.promisify.custom")] = (command: string, options: Record<string, unknown>) =>
47
+ new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
48
+ execMock(command, options, (error: Error | null, stdout: string, stderr: string) => {
49
+ if (error) {
50
+ reject(error);
51
+ return;
52
+ }
53
+ resolve({ stdout, stderr });
54
+ });
55
+ });
56
+
57
+ return {
58
+ exec: execMock,
59
+ execSync: vi.fn(() => {
60
+ throw new Error("execSync should never be called");
61
+ }),
62
+ };
63
+ });
64
+
65
+ export function installExecMock() {
66
+ calls.length = 0;
67
+ queue.length = 0;
68
+
69
+ return {
70
+ setNextResult(result: ExecResult) {
71
+ queue.push(result);
72
+ },
73
+ getCalls() {
74
+ return [...calls];
75
+ },
76
+ assertExecSyncUnused() {
77
+ expect(execSync).not.toHaveBeenCalled();
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,40 @@
1
+ import { promisify } from "node:util";
2
+ import { describe, expect, it } from "vitest";
3
+ import { makeFakeRegistry } from "./registry.js";
4
+ import { installExecMock } from "./exec-mock.js";
5
+
6
+ describe("test fixtures", () => {
7
+ it("creates seeded fake registry", () => {
8
+ const registry = makeFakeRegistry();
9
+ try {
10
+ expect(registry.store.listServices().map((s) => s.slug).sort()).toEqual(["acme", "beta"]);
11
+ expect(registry.store.listArtifacts(registry.specs.acme.id)).toHaveLength(1);
12
+ expect(registry.store.listArtifacts(registry.specs.beta.id)).toHaveLength(0);
13
+ } finally {
14
+ registry.cleanup();
15
+ }
16
+ });
17
+
18
+ it("records mocked exec calls", async () => {
19
+ const execMock = installExecMock();
20
+ execMock.setNextResult({ stdout: "ok" });
21
+
22
+ const { exec } = await import("node:child_process");
23
+ const execAsync = promisify(exec);
24
+ const result = await execAsync("node --version", { cwd: "/tmp" });
25
+
26
+ expect(result.stdout).toBe("ok");
27
+ expect(execMock.getCalls()).toEqual([{ command: "node --version", options: { cwd: "/tmp" } }]);
28
+ execMock.assertExecSyncUnused();
29
+ });
30
+
31
+ it("simulates timeout and blocks execSync", async () => {
32
+ const execMock = installExecMock();
33
+ execMock.setNextResult({ timeoutAfterMs: 25, stderr: "timed out" });
34
+ const { exec, execSync } = await import("node:child_process");
35
+ const execAsync = promisify(exec);
36
+
37
+ await expect(execAsync("echo slow", { timeout: 25 })).rejects.toThrow(/timed out/i);
38
+ expect(() => execSync("echo no")).toThrow("execSync should never be called");
39
+ });
40
+ });