@runfusion/fusion 0.26.0 → 0.27.0

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 (171) hide show
  1. package/dist/bin.js +11036 -1992
  2. package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
  3. package/dist/client/assets/AgentDetailView-DwLmRXTY.js +18 -0
  4. package/dist/client/assets/{AgentsView-D6Zi5zfP.js → AgentsView-D-N6aA0P.js} +12 -7
  5. package/dist/client/assets/ChatView-DnCdKu8Z.js +1 -0
  6. package/dist/client/assets/{DevServerView--_WBvIDQ.js → DevServerView-BiA1nYtt.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-xedtR-Rd.js → DirectoryPicker-DvBviDG6.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-Bg2oaZks.js → DocumentsView-BWXOxpuq.js} +1 -1
  9. package/dist/client/assets/{EvalsView-B3uOCXfr.js → EvalsView-CJFbtL7i.js} +1 -1
  10. package/dist/client/assets/{ExperimentalAgentOnboardingModal-Bx6yXVS5.js → ExperimentalAgentOnboardingModal-DuGIPd0B.js} +1 -1
  11. package/dist/client/assets/InsightsView-BBpRiolN.js +11 -0
  12. package/dist/client/assets/{MemoryView-xcN_eouf.js → MemoryView-48LuNkKk.js} +2 -2
  13. package/dist/client/assets/NodesView-CGQWSNZM.js +14 -0
  14. package/dist/client/assets/{PiExtensionsManager-Cc8aAZXg.js → PiExtensionsManager-i-7UL2oh.js} +2 -2
  15. package/dist/client/assets/PluginManager-DoSAykD6.js +1 -0
  16. package/dist/client/assets/{ResearchView-CERNf7sJ.js → ResearchView-XZuRtOxE.js} +1 -1
  17. package/dist/client/assets/{SettingsModal-Cis-4Lot.css → SettingsModal-Ci0_sqbU.css} +1 -1
  18. package/dist/client/assets/{SettingsModal-B1r0yASu.js → SettingsModal-CmeF8CN4.js} +1 -1
  19. package/dist/client/assets/SettingsModal-DBcjf9Bu.js +31 -0
  20. package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
  21. package/dist/client/assets/{SetupWizardModal-D1q548_L.js → SetupWizardModal-CgtvpMX9.js} +1 -1
  22. package/dist/client/assets/{SkillsView-ClLM6u6p.js → SkillsView-DErYRumF.js} +1 -1
  23. package/dist/client/assets/{StashRecoveryView-ze0pEZ5U.js → StashRecoveryView-QJrNS4Vg.js} +1 -1
  24. package/dist/client/assets/{TodoView-CTmIfy2M.js → TodoView-BD9NRwq0.js} +2 -2
  25. package/dist/client/assets/{dashboard-view-CyWN-d02.js → dashboard-view-BWGH_fAq.js} +1 -1
  26. package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
  27. package/dist/client/assets/dashboard-view-Ws9_ZnKu.js +21 -0
  28. package/dist/client/assets/{folder-open-BZuKESeq.js → folder-open-CHSlllzf.js} +1 -1
  29. package/dist/client/assets/index-DCovGm5b.css +1 -0
  30. package/dist/client/assets/index-bEwSVl7B.js +692 -0
  31. package/dist/client/assets/{star-D75YKEq-.js → star-BgVwWAPz.js} +1 -1
  32. package/dist/client/assets/{upload-BYYTgWFj.js → upload-CAzycxr9.js} +1 -1
  33. package/dist/client/assets/{users-RS90Aii3.js → users-CZnxCCCJ.js} +1 -1
  34. package/dist/client/index.html +2 -2
  35. package/dist/client/version.json +1 -1
  36. package/dist/droid-cli/package.json +1 -1
  37. package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
  38. package/dist/extension.js +5517 -1193
  39. package/dist/pi-claude-cli/package.json +1 -1
  40. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
  41. package/dist/pi-claude-cli/src/provider.ts +7 -1
  42. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +19 -1
  43. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +20 -2
  44. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
  45. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
  46. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
  47. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
  48. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
  49. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
  50. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
  51. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
  52. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
  53. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
  54. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +21 -5
  55. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
  56. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
  57. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
  58. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
  59. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
  60. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
  61. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
  62. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
  63. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
  64. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
  65. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
  66. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
  67. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
  68. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +46 -2
  69. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
  70. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
  71. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
  72. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
  73. package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
  74. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
  75. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
  76. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
  77. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
  78. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
  79. package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
  80. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
  81. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
  82. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
  83. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
  84. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
  85. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
  86. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
  87. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
  88. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  89. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  90. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  91. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  92. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  93. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  94. package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
  95. package/dist/plugins/fusion-plugin-reports/package.json +18 -2
  96. package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
  97. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
  98. package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
  99. package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
  100. package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
  101. package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
  102. package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
  103. package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
  104. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
  105. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
  106. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
  107. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
  108. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
  109. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
  110. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
  111. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
  112. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
  113. package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
  114. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
  115. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
  116. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
  117. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
  118. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
  119. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
  120. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
  121. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
  122. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
  123. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
  124. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
  125. package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
  126. package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
  127. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
  128. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
  129. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
  130. package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
  131. package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
  132. package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
  133. package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
  134. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
  135. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
  136. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
  137. package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
  138. package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
  139. package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
  140. package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
  141. package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
  142. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
  143. package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
  144. package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
  145. package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
  146. package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
  147. package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
  148. package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
  149. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
  150. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
  151. package/dist/plugins/fusion-plugin-roadmap/bundled.js +1528 -29391
  152. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
  153. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  154. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  155. package/package.json +1 -1
  156. package/skill/fusion/references/engine-tools.md +1 -1
  157. package/skill/fusion/references/extension-tools.md +3 -3
  158. package/skill/fusion/references/fusion-capabilities.md +1 -1
  159. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
  160. package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +0 -18
  161. package/dist/client/assets/ChatView-CAHjY9uO.js +0 -1
  162. package/dist/client/assets/InsightsView-Q1zvtF4F.js +0 -11
  163. package/dist/client/assets/NodesView-RxXg58_Q.js +0 -14
  164. package/dist/client/assets/PluginManager-BEkyBajl.js +0 -1
  165. package/dist/client/assets/SettingsModal-BLsac7CJ.js +0 -31
  166. package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
  167. package/dist/client/assets/dashboard-view-4xAN3yO5.js +0 -21
  168. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  169. package/dist/client/assets/index-Bdw6llW6.js +0 -692
  170. package/dist/client/assets/index-CZGlyJuS.css +0 -1
  171. package/dist/plugins/fusion-plugin-roadmap/bundled.css +0 -1093
@@ -0,0 +1,191 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { Database } from "@fusion/core";
5
+ import { describe, expect, it } from "vitest";
6
+ import { createCliPressStore } from "../../store/cli-press-store.js";
7
+ import { encodeCredentialValue } from "../../store/credentials.js";
8
+ import { buildExecutorRuntimeEnv } from "../executor-runtime-env.js";
9
+
10
+ function createHarness() {
11
+ const rootDir = mkdtempSync(join(tmpdir(), "cli-press-runtime-env-"));
12
+ const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
13
+ db.init();
14
+ const store = createCliPressStore(db);
15
+ const warnings: string[] = [];
16
+
17
+ const ctx = {
18
+ pluginId: "fusion-plugin-cli-printing-press",
19
+ taskStore: undefined,
20
+ settings: {},
21
+ logger: {
22
+ log: () => {},
23
+ info: () => {},
24
+ warn: (msg: string) => warnings.push(msg),
25
+ error: () => {},
26
+ debug: () => {},
27
+ },
28
+ emitEvent: () => {},
29
+ };
30
+
31
+ const cleanup = () => {
32
+ db.close();
33
+ rmSync(rootDir, { recursive: true, force: true });
34
+ };
35
+
36
+ return { rootDir, db, store, warnings, ctx, cleanup };
37
+ }
38
+
39
+ describe("buildExecutorRuntimeEnv", () => {
40
+ it("returns empty env/path when no generated services are present", () => {
41
+ const h = createHarness();
42
+ try {
43
+ const result = buildExecutorRuntimeEnv(h.store, { taskId: "FN-1", worktreePath: h.rootDir, rootDir: h.rootDir }, h.ctx as never);
44
+ expect(result.pathPrepend).toEqual([]);
45
+ expect(result.env).toEqual({});
46
+ } finally {
47
+ h.cleanup();
48
+ }
49
+ });
50
+
51
+ it("adds executable artifact directory and env_var credential", () => {
52
+ const h = createHarness();
53
+ try {
54
+ const service = h.store.createService({
55
+ slug: "alpha",
56
+ displayName: "Alpha",
57
+ baseUrl: "https://example.com",
58
+ sourceKind: "manual",
59
+ });
60
+ const spec = h.store.createSpec({
61
+ serviceId: service.id,
62
+ name: "alpha-cli",
63
+ version: "0.1.0",
64
+ generatorVersion: "1.0.0",
65
+ specJson: "{}",
66
+ status: "generated",
67
+ generatedAt: new Date().toISOString(),
68
+ });
69
+ const relativePath = `plugins/cli-printing-press/artifacts/${service.id}/${spec.id}/alpha`;
70
+ const absolutePath = join(h.rootDir, ".fusion", relativePath);
71
+ mkdirSync(join(absolutePath, ".."), { recursive: true });
72
+ writeFileSync(absolutePath, "#!/bin/sh\necho alpha\n");
73
+
74
+ h.store.createArtifact({ cliSpecId: spec.id, kind: "script", path: relativePath, executable: true });
75
+ h.store.createCredential({
76
+ serviceId: service.id,
77
+ name: "api",
78
+ kind: "env_var",
79
+ placement: { kind: "env_var", envVar: "ALPHA_TOKEN" },
80
+ value: encodeCredentialValue("secret-alpha"),
81
+ });
82
+
83
+ const result = buildExecutorRuntimeEnv(h.store, { taskId: "FN-1", worktreePath: h.rootDir, rootDir: h.rootDir }, h.ctx as never);
84
+ expect(result.pathPrepend).toEqual([join(h.rootDir, ".fusion", "plugins/cli-printing-press/artifacts", service.id, spec.id)]);
85
+ expect(result.env).toEqual({ ALPHA_TOKEN: "secret-alpha" });
86
+ } finally {
87
+ h.cleanup();
88
+ }
89
+ });
90
+
91
+ it("deduplicates path entries across services", () => {
92
+ const h = createHarness();
93
+ try {
94
+ const sharedDir = `plugins/cli-printing-press/artifacts/shared/bin`;
95
+ const sharedExec1 = `${sharedDir}/one`;
96
+ const sharedExec2 = `${sharedDir}/two`;
97
+ mkdirSync(join(h.rootDir, ".fusion", sharedDir), { recursive: true });
98
+ writeFileSync(join(h.rootDir, ".fusion", sharedExec1), "1");
99
+ writeFileSync(join(h.rootDir, ".fusion", sharedExec2), "2");
100
+
101
+ for (const slug of ["one", "two"]) {
102
+ const service = h.store.createService({ slug, displayName: slug, baseUrl: "https://example.com", sourceKind: "manual" });
103
+ const spec = h.store.createSpec({
104
+ serviceId: service.id,
105
+ name: `${slug}-cli`,
106
+ version: "0.1.0",
107
+ generatorVersion: "1.0.0",
108
+ specJson: "{}",
109
+ status: "generated",
110
+ generatedAt: new Date().toISOString(),
111
+ });
112
+ h.store.createArtifact({
113
+ cliSpecId: spec.id,
114
+ kind: "script",
115
+ path: slug === "one" ? sharedExec1 : sharedExec2,
116
+ executable: true,
117
+ });
118
+ }
119
+
120
+ const result = buildExecutorRuntimeEnv(h.store, { taskId: "FN-1", worktreePath: h.rootDir, rootDir: h.rootDir }, h.ctx as never);
121
+ expect(result.pathPrepend).toEqual([join(h.rootDir, ".fusion", sharedDir)]);
122
+ } finally {
123
+ h.cleanup();
124
+ }
125
+ });
126
+
127
+ it("skips missing artifacts and logs warning", () => {
128
+ const h = createHarness();
129
+ try {
130
+ const service = h.store.createService({ slug: "missing", displayName: "Missing", baseUrl: "https://example.com", sourceKind: "manual" });
131
+ const spec = h.store.createSpec({
132
+ serviceId: service.id,
133
+ name: "missing-cli",
134
+ version: "0.1.0",
135
+ generatorVersion: "1.0.0",
136
+ specJson: "{}",
137
+ status: "generated",
138
+ generatedAt: new Date().toISOString(),
139
+ });
140
+ h.store.createArtifact({
141
+ cliSpecId: spec.id,
142
+ kind: "script",
143
+ path: `plugins/cli-printing-press/artifacts/${service.id}/${spec.id}/missing`,
144
+ executable: true,
145
+ });
146
+
147
+ const result = buildExecutorRuntimeEnv(h.store, { taskId: "FN-1", worktreePath: h.rootDir, rootDir: h.rootDir }, h.ctx as never);
148
+ expect(result.pathPrepend).toEqual([]);
149
+ expect(h.warnings.length).toBe(1);
150
+ expect(h.warnings[0]).toContain("Skipping missing artifact");
151
+ } finally {
152
+ h.cleanup();
153
+ }
154
+ });
155
+
156
+ it("rejects oauth credentials defensively", () => {
157
+ const h = createHarness();
158
+ try {
159
+ const service = h.store.createService({ slug: "oauth", displayName: "OAuth", baseUrl: "https://example.com", sourceKind: "manual" });
160
+ const encoded = JSON.stringify(encodeCredentialValue("token"));
161
+ const placement = JSON.stringify({ kind: "oauth", provider: "x" });
162
+ h.db.prepare("INSERT INTO cli_press_credentials (id, serviceId, name, kind, value, placement, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))")
163
+ .run("cred_oauth", service.id, "oauth", "oauth", encoded, placement);
164
+
165
+ expect(() =>
166
+ buildExecutorRuntimeEnv(h.store, { taskId: "FN-1", worktreePath: h.rootDir, rootDir: h.rootDir }, h.ctx as never),
167
+ ).toThrow(/OAuth credentials are not supported/);
168
+ } finally {
169
+ h.cleanup();
170
+ }
171
+ });
172
+
173
+ it("ignores non env_var credentials", () => {
174
+ const h = createHarness();
175
+ try {
176
+ const service = h.store.createService({ slug: "beta", displayName: "Beta", baseUrl: "https://example.com", sourceKind: "manual" });
177
+ h.store.createCredential({
178
+ serviceId: service.id,
179
+ name: "header",
180
+ kind: "header",
181
+ placement: { kind: "header", header: "X-Token" },
182
+ value: encodeCredentialValue("header-token"),
183
+ });
184
+
185
+ const result = buildExecutorRuntimeEnv(h.store, { taskId: "FN-1", worktreePath: h.rootDir, rootDir: h.rootDir }, h.ctx as never);
186
+ expect(result.env).toEqual({});
187
+ } finally {
188
+ h.cleanup();
189
+ }
190
+ });
191
+ });
@@ -0,0 +1,75 @@
1
+ import { dirname, isAbsolute, join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import type { ExecutorRuntimeEnvContribution, ExecutorRuntimeTaskContext, PluginContext } from "@fusion/plugin-sdk";
4
+ import type { createCliPressStore } from "../store/cli-press-store.js";
5
+ import { decodeCredentialValue } from "../store/credentials.js";
6
+
7
+ type CliPressStore = ReturnType<typeof createCliPressStore>;
8
+
9
+ function toEpoch(value?: string): number {
10
+ if (!value) return 0;
11
+ const parsed = Date.parse(value);
12
+ return Number.isFinite(parsed) ? parsed : 0;
13
+ }
14
+
15
+ export function buildExecutorRuntimeEnv(
16
+ store: CliPressStore,
17
+ taskCtx: ExecutorRuntimeTaskContext,
18
+ ctx: PluginContext,
19
+ ): ExecutorRuntimeEnvContribution {
20
+ const pathDirs: string[] = [];
21
+ const env: Record<string, string> = {};
22
+
23
+ for (const service of store.listServices()) {
24
+ const specs = store
25
+ .listSpecs(service.id)
26
+ .filter((spec) => spec.status === "generated")
27
+ .sort((a, b) => toEpoch(b.generatedAt ?? b.updatedAt) - toEpoch(a.generatedAt ?? a.updatedAt));
28
+
29
+ const selectedSpec = specs.find((spec) => {
30
+ const artifacts = store.listArtifacts(spec.id);
31
+ return artifacts.some((artifact) => artifact.executable);
32
+ });
33
+
34
+ if (selectedSpec) {
35
+ const executableArtifacts = store.listArtifacts(selectedSpec.id).filter((artifact) => artifact.executable);
36
+ for (const artifact of executableArtifacts) {
37
+ const absoluteArtifactPath = isAbsolute(artifact.path)
38
+ ? artifact.path
39
+ : join(taskCtx.rootDir, ".fusion", artifact.path);
40
+ if (!existsSync(absoluteArtifactPath)) {
41
+ ctx.logger.warn(
42
+ `[executorRuntimeEnv] Skipping missing artifact for service ${service.slug}: ${absoluteArtifactPath}`,
43
+ );
44
+ continue;
45
+ }
46
+ pathDirs.push(dirname(absoluteArtifactPath));
47
+ }
48
+ }
49
+
50
+ for (const credential of store.listCredentials(service.id)) {
51
+ const credentialKind = (credential as { kind: string }).kind;
52
+ if (credentialKind === "oauth" || credentialKind === "oauth2") {
53
+ throw new Error(`OAuth credentials are not supported for service ${service.slug}`);
54
+ }
55
+
56
+ if (credential.kind !== "env_var") {
57
+ continue;
58
+ }
59
+
60
+ if (credential.placement.kind !== "env_var") {
61
+ throw new Error(
62
+ `Credential placement mismatch for ${credential.name}: expected env_var placement, got ${credential.placement.kind}`,
63
+ );
64
+ }
65
+
66
+ env[credential.placement.envVar] = decodeCredentialValue(credential.value);
67
+ }
68
+ }
69
+
70
+ return {
71
+ pathPrepend: Array.from(new Set(pathDirs)),
72
+ env,
73
+ description: "cli-printing-press generated CLIs",
74
+ };
75
+ }
@@ -0,0 +1,85 @@
1
+ /* Interim storage — replaced by FN-3766's canonical schema. Do not extend without updating that ticket. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import type { ServiceDraft } from "../wizard/types.js";
6
+
7
+ export class NotFoundError extends Error {
8
+ constructor(id: string) {
9
+ super(`Draft not found: ${id}`);
10
+ this.name = "NotFoundError";
11
+ }
12
+ }
13
+
14
+ function mergeDraft(existing: ServiceDraft, patch: Partial<ServiceDraft>): ServiceDraft {
15
+ const mergedCredential = patch.credential && typeof patch.credential === "object"
16
+ ? { ...existing.credential, ...patch.credential }
17
+ : existing.credential;
18
+
19
+ return {
20
+ ...existing,
21
+ ...patch,
22
+ credential: mergedCredential,
23
+ endpoints: patch.endpoints ?? existing.endpoints,
24
+ };
25
+ }
26
+
27
+ export function getArtifactDir(id: string, projectRoot: string): string {
28
+ return join(projectRoot, ".fusion", "plugins", "cli-printing-press", "generated", id);
29
+ }
30
+
31
+ export function createDraftStore({ rootDir }: { rootDir: string }) {
32
+ const draftsDir = join(rootDir, ".fusion", "plugins", "cli-printing-press", "drafts");
33
+
34
+ async function ensureDir() { await mkdir(draftsDir, { recursive: true }); }
35
+
36
+ function nextUpdatedAt(previous?: string): string {
37
+ const now = Date.now();
38
+ const previousTime = previous ? Date.parse(previous) : Number.NaN;
39
+ if (Number.isFinite(previousTime) && now <= previousTime) {
40
+ return new Date(previousTime + 1).toISOString();
41
+ }
42
+ return new Date(now).toISOString();
43
+ }
44
+
45
+ async function writeAtomic(path: string, draft: ServiceDraft): Promise<void> {
46
+ const tempPath = `${path}.tmp-${randomUUID()}`;
47
+ await writeFile(tempPath, JSON.stringify(draft, null, 2), "utf8");
48
+ await rename(tempPath, path);
49
+ }
50
+
51
+ return {
52
+ async create(input: ServiceDraft) {
53
+ await ensureDir();
54
+ const now = nextUpdatedAt();
55
+ const draft: ServiceDraft = { ...input, id: input.id || randomUUID(), createdAt: input.createdAt || now, updatedAt: now };
56
+ await writeAtomic(join(draftsDir, `${draft.id}.json`), draft);
57
+ return draft;
58
+ },
59
+ async list() {
60
+ await ensureDir();
61
+ const files = await readdir(draftsDir);
62
+ const entries = await Promise.all(files.filter((file) => file.endsWith(".json")).map(async (file) => JSON.parse(await readFile(join(draftsDir, file), "utf8")) as ServiceDraft));
63
+ return entries.map(({ id, name, slug, updatedAt }) => ({ id, name, slug, updatedAt }));
64
+ },
65
+ async get(id: string) {
66
+ try { return JSON.parse(await readFile(join(draftsDir, `${id}.json`), "utf8")) as ServiceDraft; } catch { return null; }
67
+ },
68
+ async update(id: string, patch: Partial<ServiceDraft>) {
69
+ await ensureDir();
70
+ const current = await this.get(id);
71
+ if (!current) throw new NotFoundError(id);
72
+ const updated: ServiceDraft = {
73
+ ...mergeDraft(current, patch),
74
+ id: current.id,
75
+ createdAt: current.createdAt,
76
+ updatedAt: nextUpdatedAt(current.updatedAt),
77
+ };
78
+ await writeAtomic(join(draftsDir, `${id}.json`), updated);
79
+ return updated;
80
+ },
81
+ async delete(id: string) {
82
+ await rm(join(draftsDir, `${id}.json`), { force: true });
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,128 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Database } from "@fusion/core";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { createCliPressStore, ensureCliPressSchema } from "../cli-press-store.js";
8
+ import { encodeCredentialValue } from "../credentials.js";
9
+
10
+ describe("cli-press-store", () => {
11
+ let rootDir: string;
12
+ let db: Database;
13
+ let store: ReturnType<typeof createCliPressStore>;
14
+
15
+ beforeEach(() => {
16
+ rootDir = mkdtempSync(join(tmpdir(), "cli-press-store-"));
17
+ db = new Database(join(rootDir, ".fusion"), { inMemory: true });
18
+ db.init();
19
+ ensureCliPressSchema(db);
20
+ ensureCliPressSchema(db);
21
+ store = createCliPressStore(db);
22
+ });
23
+
24
+ afterEach(async () => {
25
+ db.close();
26
+ await rm(rootDir, { recursive: true, force: true });
27
+ });
28
+
29
+ it("creates schema idempotently and runs full CRUD", () => {
30
+ const service = store.createService({
31
+ slug: "demo",
32
+ displayName: "Demo",
33
+ description: "Demo service",
34
+ baseUrl: "https://example.com",
35
+ sourceKind: "manual",
36
+ sourceRef: "wizard",
37
+ });
38
+ expect(service.id).toMatch(/^svc_/);
39
+
40
+ const updatedService = store.updateService(service.id, { displayName: "Demo Updated" });
41
+ expect(updatedService.displayName).toBe("Demo Updated");
42
+
43
+ const spec = store.createSpec({
44
+ serviceId: service.id,
45
+ name: "demo-cli",
46
+ version: "0.1.0",
47
+ generatorVersion: "1.0.0",
48
+ specJson: JSON.stringify({ hello: "world" }),
49
+ status: "draft",
50
+ generatedAt: undefined,
51
+ lastGenerationError: undefined,
52
+ });
53
+ expect(store.getSpec(spec.id)?.specJson).toBe(JSON.stringify({ hello: "world" }));
54
+
55
+ const updatedSpec = store.updateSpec(spec.id, { status: "generated" });
56
+ expect(updatedSpec.status).toBe("generated");
57
+
58
+ const artifact = store.createArtifact({
59
+ cliSpecId: spec.id,
60
+ kind: "script",
61
+ path: "plugins/cli-printing-press/artifacts/demo.sh",
62
+ executable: true,
63
+ checksum: "abc",
64
+ sizeBytes: 42,
65
+ });
66
+ expect(artifact.id).toMatch(/^art_/);
67
+
68
+ const cred = store.createCredential({
69
+ serviceId: service.id,
70
+ name: "api",
71
+ kind: "api_key",
72
+ value: encodeCredentialValue("secret"),
73
+ placement: { kind: "api_key", header: "X-API-Key" },
74
+ });
75
+ expect(cred.id).toMatch(/^cred_/);
76
+
77
+ const setting = store.setSetting({
78
+ serviceId: service.id,
79
+ key: "region",
80
+ value: "us-east-1",
81
+ scope: "runtime",
82
+ });
83
+ expect(setting.id).toMatch(/^set_/);
84
+
85
+ expect(store.listServices()).toHaveLength(1);
86
+ expect(store.listSpecs(service.id)).toHaveLength(1);
87
+ expect(store.listArtifacts(spec.id)).toHaveLength(1);
88
+ expect(store.listCredentials(service.id)).toHaveLength(1);
89
+ expect(store.listSettings(service.id)).toHaveLength(1);
90
+
91
+ store.deleteService(service.id);
92
+ expect(store.listServices()).toHaveLength(0);
93
+ expect(store.listSpecs(service.id)).toHaveLength(0);
94
+ expect(store.listCredentials(service.id)).toHaveLength(0);
95
+ expect(store.listSettings(service.id)).toHaveLength(0);
96
+ });
97
+
98
+ it("rejects oauth and invalid placement", () => {
99
+ const service = store.createService({
100
+ slug: "oauth-demo",
101
+ displayName: "OAuth Demo",
102
+ description: "",
103
+ baseUrl: "https://example.com",
104
+ sourceKind: "manual",
105
+ sourceRef: "wizard",
106
+ });
107
+
108
+ expect(() =>
109
+ store.createCredential({
110
+ serviceId: service.id,
111
+ name: "bad",
112
+ kind: "oauth" as never,
113
+ value: encodeCredentialValue("x"),
114
+ placement: { kind: "header", header: "Authorization" },
115
+ }),
116
+ ).toThrow("not supported");
117
+
118
+ expect(() =>
119
+ store.createCredential({
120
+ serviceId: service.id,
121
+ name: "bad2",
122
+ kind: "api_key",
123
+ value: encodeCredentialValue("x"),
124
+ placement: { kind: "api_key", header: "X", queryParam: "token" },
125
+ }),
126
+ ).toThrow("Invalid credential placement");
127
+ });
128
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Credential } from "../cli-press-types.js";
3
+ import { applyCredentialToRequest, decodeCredentialValue, encodeCredentialValue } from "../credentials.js";
4
+
5
+ function baseCredential(partial: Partial<Credential>): Credential {
6
+ return {
7
+ id: "cred_1",
8
+ serviceId: "svc_1",
9
+ name: "cred",
10
+ kind: "header",
11
+ value: encodeCredentialValue("secret"),
12
+ placement: { kind: "header", header: "X-Custom-Token" },
13
+ createdAt: new Date().toISOString(),
14
+ updatedAt: new Date().toISOString(),
15
+ ...partial,
16
+ } as Credential;
17
+ }
18
+
19
+ describe("credentials", () => {
20
+ it("encodes and decodes", () => {
21
+ const encoded = encodeCredentialValue("alice:s3cret");
22
+ expect(encoded.encoding).toBe("base64");
23
+ expect(decodeCredentialValue(encoded)).toBe("alice:s3cret");
24
+ });
25
+
26
+ it("applies all credential kinds", () => {
27
+ const request = { headers: {}, query: {}, env: {} };
28
+
29
+ applyCredentialToRequest(baseCredential({ kind: "header", placement: { kind: "header", header: "X-Custom-Token" } }), request);
30
+ applyCredentialToRequest(baseCredential({ kind: "query_param", placement: { kind: "query_param", queryParam: "api_token" } }), request);
31
+ applyCredentialToRequest(baseCredential({ kind: "env_var", placement: { kind: "env_var", envVar: "GITHUB_TOKEN" } }), request);
32
+ applyCredentialToRequest(baseCredential({ kind: "bearer_token", placement: { kind: "bearer_token", header: "Authorization" } }), request);
33
+ applyCredentialToRequest(baseCredential({ kind: "api_key", placement: { kind: "api_key", header: "X-API-Key" } }), request);
34
+ applyCredentialToRequest(baseCredential({ kind: "api_key", placement: { kind: "api_key", queryParam: "api_key" } }), request);
35
+ applyCredentialToRequest(
36
+ baseCredential({
37
+ kind: "basic_auth",
38
+ value: encodeCredentialValue("alice:s3cret"),
39
+ placement: { kind: "basic_auth", header: "Authorization" },
40
+ }),
41
+ request,
42
+ );
43
+
44
+ expect(request.headers["X-Custom-Token"]).toBe("secret");
45
+ expect(request.query.api_token).toBe("secret");
46
+ expect(request.env.GITHUB_TOKEN).toBe("secret");
47
+ expect(request.headers.Authorization).toBe("Basic YWxpY2U6czNjcmV0");
48
+ expect(request.headers["X-API-Key"]).toBe("secret");
49
+ expect(request.query.api_key).toBe("secret");
50
+ });
51
+
52
+ it("throws on placement mismatch and oauth", () => {
53
+ const request = { headers: {}, query: {}, env: {} };
54
+ expect(() =>
55
+ applyCredentialToRequest(baseCredential({ kind: "header", placement: { kind: "query_param", queryParam: "x" } as any }), request),
56
+ ).toThrow("Invalid credential placement");
57
+
58
+ expect(() =>
59
+ applyCredentialToRequest(baseCredential({ kind: "oauth" as any, placement: { kind: "oauth", header: "Authorization" } as any }), request),
60
+ ).toThrow("not supported");
61
+ });
62
+ });