@kortix/sandbox 0.4.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 (246) hide show
  1. package/config/customize.sh +143 -0
  2. package/config/kortix-env-setup.sh +25 -0
  3. package/kortix-master/package.json +22 -0
  4. package/kortix-master/src/config.ts +22 -0
  5. package/kortix-master/src/index.ts +44 -0
  6. package/kortix-master/src/routes/env.ts +65 -0
  7. package/kortix-master/src/routes/proxy.ts +108 -0
  8. package/kortix-master/src/routes/update.ts +185 -0
  9. package/kortix-master/src/services/proxy.ts +43 -0
  10. package/kortix-master/src/services/secret-store.ts +156 -0
  11. package/kortix-master/tsconfig.json +14 -0
  12. package/opencode/agents/kortix-browser.md +142 -0
  13. package/opencode/agents/kortix-build.md +62 -0
  14. package/opencode/agents/kortix-explore.md +66 -0
  15. package/opencode/agents/kortix-image-gen.md +33 -0
  16. package/opencode/agents/kortix-main.md +450 -0
  17. package/opencode/agents/kortix-plan.md +100 -0
  18. package/opencode/agents/kortix-research.md +84 -0
  19. package/opencode/agents/kortix-sheets.md +61 -0
  20. package/opencode/agents/kortix-slides.md +64 -0
  21. package/opencode/agents/kortix-web-dev.md +572 -0
  22. package/opencode/commands/email.md +36 -0
  23. package/opencode/commands/init.md +43 -0
  24. package/opencode/commands/journal.md +44 -0
  25. package/opencode/commands/memory-init.md +81 -0
  26. package/opencode/commands/memory-search.md +50 -0
  27. package/opencode/commands/memory-status.md +56 -0
  28. package/opencode/commands/research.md +36 -0
  29. package/opencode/commands/search.md +38 -0
  30. package/opencode/commands/slides.md +32 -0
  31. package/opencode/commands/spreadsheet.md +30 -0
  32. package/opencode/memory.json +37 -0
  33. package/opencode/ocx.jsonc +10 -0
  34. package/opencode/opencode.jsonc +103 -0
  35. package/opencode/package.json +25 -0
  36. package/opencode/patches/apply.sh +19 -0
  37. package/opencode/patches/opencode-pty-spawn.txt +49 -0
  38. package/opencode/plugin/background-agents.ts.disabled +483 -0
  39. package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
  40. package/opencode/plugin/kdco-primitives/index.ts +26 -0
  41. package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
  42. package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
  43. package/opencode/plugin/kdco-primitives/shell.ts +138 -0
  44. package/opencode/plugin/kdco-primitives/temp.ts +36 -0
  45. package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
  46. package/opencode/plugin/kdco-primitives/types.ts +13 -0
  47. package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
  48. package/opencode/plugin/memory.ts +306 -0
  49. package/opencode/plugin/worktree/state.ts +412 -0
  50. package/opencode/plugin/worktree/terminal.ts +1002 -0
  51. package/opencode/plugin/worktree.ts +861 -0
  52. package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
  53. package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
  54. package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
  55. package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
  56. package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
  57. package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
  58. package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
  59. package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
  60. package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
  61. package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
  62. package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
  63. package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
  64. package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
  65. package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
  66. package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
  67. package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
  68. package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
  69. package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
  70. package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
  71. package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
  72. package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
  73. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
  74. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
  75. package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
  76. package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
  77. package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
  78. package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
  79. package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
  80. package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
  81. package/opencode/skills/KORTIX-email/SKILL.md +145 -0
  82. package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
  83. package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
  84. package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
  85. package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
  86. package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
  87. package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
  88. package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
  89. package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
  90. package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
  91. package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
  92. package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
  93. package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
  94. package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
  95. package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
  96. package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
  97. package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
  98. package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
  99. package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
  100. package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
  101. package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
  102. package/opencode/skills/KORTIX-pdf/forms.md +36 -0
  103. package/opencode/skills/KORTIX-pdf/reference.md +105 -0
  104. package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
  105. package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
  106. package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
  107. package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
  108. package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
  109. package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
  110. package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
  111. package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  112. package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
  113. package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
  114. package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
  115. package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
  116. package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
  117. package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
  118. package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
  119. package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
  120. package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
  121. package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
  122. package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
  123. package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
  124. package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
  125. package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
  126. package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
  127. package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
  128. package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
  129. package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
  130. package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
  131. package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
  132. package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
  133. package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
  134. package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
  135. package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
  136. package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
  137. package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
  138. package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
  139. package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
  140. package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
  141. package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
  142. package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
  143. package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
  144. package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
  145. package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
  146. package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
  147. package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
  148. package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
  149. package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
  150. package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
  151. package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
  152. package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
  153. package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
  154. package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
  155. package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
  156. package/opencode/skills/KORTIX-session-search/Untitled +1 -0
  157. package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
  158. package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
  159. package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
  160. package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
  161. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
  162. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
  163. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  164. package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
  165. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  166. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  167. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  168. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  169. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  170. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  171. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  172. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  173. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  174. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  175. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  176. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  177. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  178. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  179. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  180. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  181. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  182. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  183. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  184. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  185. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  186. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  187. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  188. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  189. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  190. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  191. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  192. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  193. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  194. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  195. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  196. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  197. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  198. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  199. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  200. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  201. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  202. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  203. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  204. package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
  205. package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
  206. package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
  207. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
  208. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
  209. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
  210. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
  211. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
  212. package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
  213. package/opencode/tools/image-gen.ts +342 -0
  214. package/opencode/tools/image-search.ts +190 -0
  215. package/opencode/tools/memory-get.ts +168 -0
  216. package/opencode/tools/memory-search.ts +247 -0
  217. package/opencode/tools/presentation-gen.ts +723 -0
  218. package/opencode/tools/scrape-webpage.ts +115 -0
  219. package/opencode/tools/scripts/.python-version +1 -0
  220. package/opencode/tools/scripts/convert_pdf.py +184 -0
  221. package/opencode/tools/scripts/convert_pptx.py +562 -0
  222. package/opencode/tools/scripts/pyproject.toml +11 -0
  223. package/opencode/tools/scripts/uv.lock +287 -0
  224. package/opencode/tools/scripts/validate_slide.py +74 -0
  225. package/opencode/tools/show-user.ts +217 -0
  226. package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
  227. package/opencode/tools/tests/image-gen.test.ts +215 -0
  228. package/opencode/tools/tests/image-search.test.ts +125 -0
  229. package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
  230. package/opencode/tools/tests/presentation-gen.test.ts +389 -0
  231. package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
  232. package/opencode/tools/tests/show-user.test.ts +241 -0
  233. package/opencode/tools/tests/video-gen.test.ts +110 -0
  234. package/opencode/tools/tests/web-search.test.ts +106 -0
  235. package/opencode/tools/video-gen.ts +200 -0
  236. package/opencode/tools/web-search.ts +153 -0
  237. package/opencode/tsconfig.json +29 -0
  238. package/package.json +36 -0
  239. package/patch-agent-browser.js +100 -0
  240. package/postinstall.sh +88 -0
  241. package/services/KORTIX-presentation-viewer/run +37 -0
  242. package/services/agent-browser-viewer/run +48 -0
  243. package/services/kortix-master/run +16 -0
  244. package/services/lss-sync/run +22 -0
  245. package/services/opencode-serve/run +25 -0
  246. package/services/opencode-web/run +21 -0
@@ -0,0 +1,389 @@
1
+ import { readFileSync, existsSync, rmSync } from "fs";
2
+ import { resolve, join } from "path";
3
+
4
+ const envPath = resolve(import.meta.dir, "../../.env");
5
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
6
+ const trimmed = line.trim();
7
+ if (!trimmed || trimmed.startsWith("#")) continue;
8
+ const eq = trimmed.indexOf("=");
9
+ if (eq > 0) process.env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
10
+ }
11
+
12
+ const OUTPUT_DIR = resolve(import.meta.dir, "test-output-presentations");
13
+
14
+ async function test(name: string, fn: () => Promise<void>) {
15
+ process.stdout.write(`\n=== ${name} ===\n`);
16
+ try {
17
+ await fn();
18
+ process.stdout.write(`PASS\n`);
19
+ } catch (e) {
20
+ process.stdout.write(`FAIL: ${e}\n`);
21
+ process.exit(1);
22
+ }
23
+ }
24
+
25
+ function assert(cond: boolean, msg: string) {
26
+ if (!cond) throw new Error(msg);
27
+ }
28
+
29
+ const presGen = (await import("../presentation-gen.ts")).default;
30
+
31
+ const ctx = { directory: process.cwd(), worktree: process.cwd() } as Parameters<
32
+ typeof presGen.execute
33
+ >[1];
34
+
35
+ // ── Worktree "/" Fallback Test ──
36
+
37
+ await test("presentation_gen: worktree '/' falls back to directory", async () => {
38
+ const rootCtx = { directory: OUTPUT_DIR, worktree: "/" } as Parameters<
39
+ typeof presGen.execute
40
+ >[1];
41
+ const raw = await presGen.execute(
42
+ {
43
+ action: "create_slide",
44
+ presentation_name: "worktree_test",
45
+ slide_number: 1,
46
+ slide_title: "Test",
47
+ content: "<div>hello</div>",
48
+ presentation_title: "Worktree Test",
49
+ },
50
+ rootCtx,
51
+ );
52
+ const result = JSON.parse(raw as string);
53
+ assert(result.success === true, `should succeed with worktree '/', got: ${raw}`);
54
+ assert(
55
+ result.presentation_path === "presentations/worktree_test",
56
+ "path should be relative",
57
+ );
58
+ // Verify file was created under OUTPUT_DIR, not under /
59
+ const slidePath = join(OUTPUT_DIR, result.slide_file);
60
+ assert(existsSync(slidePath), `slide should exist at ${slidePath}, not at /${result.slide_file}`);
61
+ // Cleanup
62
+ rmSync(join(OUTPUT_DIR, "presentations"), { recursive: true, force: true });
63
+ });
64
+
65
+ // ── Validation Tests ──
66
+
67
+ await test("presentation_gen: invalid action", async () => {
68
+ const raw = await presGen.execute({ action: "dance" } as any, ctx);
69
+ assert(
70
+ (raw as string).includes("Invalid action"),
71
+ "should reject invalid action",
72
+ );
73
+ });
74
+
75
+ await test("presentation_gen: create_slide missing presentation_name", async () => {
76
+ const raw = await presGen.execute(
77
+ {
78
+ action: "create_slide",
79
+ slide_number: 1,
80
+ slide_title: "Test",
81
+ content: "<div>hi</div>",
82
+ },
83
+ ctx,
84
+ );
85
+ const result = JSON.parse(raw as string);
86
+ assert(result.success === false, "should fail without presentation_name");
87
+ assert(
88
+ result.error.includes("presentation_name"),
89
+ "should mention presentation_name",
90
+ );
91
+ });
92
+
93
+ await test("presentation_gen: create_slide missing content", async () => {
94
+ const raw = await presGen.execute(
95
+ {
96
+ action: "create_slide",
97
+ presentation_name: "test",
98
+ slide_number: 1,
99
+ slide_title: "Test",
100
+ },
101
+ ctx,
102
+ );
103
+ const result = JSON.parse(raw as string);
104
+ assert(result.success === false, "should fail without content");
105
+ assert(result.error.includes("content"), "should mention content");
106
+ });
107
+
108
+ await test("presentation_gen: create_slide missing slide_title", async () => {
109
+ const raw = await presGen.execute(
110
+ {
111
+ action: "create_slide",
112
+ presentation_name: "test",
113
+ slide_number: 1,
114
+ content: "<div>hi</div>",
115
+ },
116
+ ctx,
117
+ );
118
+ const result = JSON.parse(raw as string);
119
+ assert(result.success === false, "should fail without slide_title");
120
+ });
121
+
122
+ await test("presentation_gen: delete_slide invalid number", async () => {
123
+ const raw = await presGen.execute(
124
+ {
125
+ action: "delete_slide",
126
+ presentation_name: "nonexistent",
127
+ slide_number: 1,
128
+ },
129
+ ctx,
130
+ );
131
+ const result = JSON.parse(raw as string);
132
+ assert(result.success === false, "should fail for nonexistent presentation");
133
+ });
134
+
135
+ // ── Functional Tests ──
136
+
137
+ const PRES_NAME = "test_presentation";
138
+
139
+ await test("presentation_gen: list_presentations (empty)", async () => {
140
+ const raw = await presGen.execute(
141
+ { action: "list_presentations", output_dir: OUTPUT_DIR },
142
+ ctx,
143
+ );
144
+ const result = JSON.parse(raw as string);
145
+ assert(result.success === true, "should succeed");
146
+ assert(result.total_count === 0, "should have 0 presentations initially");
147
+ });
148
+
149
+ await test("presentation_gen: create_slide 1", async () => {
150
+ const raw = await presGen.execute(
151
+ {
152
+ action: "create_slide",
153
+ presentation_name: PRES_NAME,
154
+ slide_number: 1,
155
+ slide_title: "Introduction",
156
+ content: `<div style="display:flex;align-items:center;justify-content:center;height:1080px;width:1920px;background:#1a1a2e;">
157
+ <h1 style="color:#e94560;font-size:72px;font-weight:900;">Test Presentation</h1>
158
+ </div>`,
159
+ presentation_title: "Test Presentation",
160
+ output_dir: OUTPUT_DIR,
161
+ },
162
+ ctx,
163
+ );
164
+ const result = JSON.parse(raw as string);
165
+ assert(result.success === true, `should succeed: ${raw}`);
166
+ assert(result.slide_number === 1, "slide_number should be 1");
167
+ assert(result.total_slides === 1, "total_slides should be 1");
168
+
169
+ const slidePath = join(OUTPUT_DIR, result.slide_file);
170
+ assert(existsSync(slidePath), `slide file should exist: ${slidePath}`);
171
+
172
+ const html = readFileSync(slidePath, "utf-8");
173
+ assert(html.includes("1920"), "should have 1920 viewport");
174
+ assert(html.includes("Inter"), "should have Inter font");
175
+ assert(
176
+ html.includes("Test Presentation"),
177
+ "should contain presentation title",
178
+ );
179
+ assert(html.includes("#e94560"), "should contain slide content");
180
+ process.stdout.write(` created: ${slidePath}\n`);
181
+ });
182
+
183
+ await test("presentation_gen: create_slide 2", async () => {
184
+ const raw = await presGen.execute(
185
+ {
186
+ action: "create_slide",
187
+ presentation_name: PRES_NAME,
188
+ slide_number: 2,
189
+ slide_title: "Details",
190
+ content: `<div style="padding:40px;box-sizing:border-box;height:1080px;width:1920px;background:#16213e;">
191
+ <h2 style="color:#e94560;font-size:48px;">Key Points</h2>
192
+ <ul style="color:#fff;font-size:24px;line-height:1.8;">
193
+ <li>Point one</li>
194
+ <li>Point two</li>
195
+ <li>Point three</li>
196
+ </ul>
197
+ </div>`,
198
+ presentation_title: "Test Presentation",
199
+ output_dir: OUTPUT_DIR,
200
+ },
201
+ ctx,
202
+ );
203
+ const result = JSON.parse(raw as string);
204
+ assert(result.success === true, `should succeed: ${raw}`);
205
+ assert(result.slide_number === 2, "slide_number should be 2");
206
+ assert(result.total_slides === 2, "total_slides should be 2");
207
+ });
208
+
209
+ await test("presentation_gen: create_slide 3", async () => {
210
+ const raw = await presGen.execute(
211
+ {
212
+ action: "create_slide",
213
+ presentation_name: PRES_NAME,
214
+ slide_number: 3,
215
+ slide_title: "Conclusion",
216
+ content: `<div style="display:flex;align-items:center;justify-content:center;height:1080px;width:1920px;background:#0f3460;">
217
+ <h1 style="color:#e94560;font-size:64px;">Thank You</h1>
218
+ </div>`,
219
+ presentation_title: "Test Presentation",
220
+ output_dir: OUTPUT_DIR,
221
+ },
222
+ ctx,
223
+ );
224
+ const result = JSON.parse(raw as string);
225
+ assert(result.success === true, `should succeed: ${raw}`);
226
+ assert(result.total_slides === 3, "total_slides should be 3");
227
+ });
228
+
229
+ await test("presentation_gen: list_slides", async () => {
230
+ const raw = await presGen.execute(
231
+ {
232
+ action: "list_slides",
233
+ presentation_name: PRES_NAME,
234
+ output_dir: OUTPUT_DIR,
235
+ },
236
+ ctx,
237
+ );
238
+ const result = JSON.parse(raw as string);
239
+ assert(result.success === true, "should succeed");
240
+ assert(
241
+ result.total_slides === 3,
242
+ `should have 3 slides, got ${result.total_slides}`,
243
+ );
244
+ assert(result.slides[0].slide_number === 1, "first slide should be 1");
245
+ assert(result.slides[1].slide_number === 2, "second slide should be 2");
246
+ assert(result.slides[2].slide_number === 3, "third slide should be 3");
247
+ assert(result.slides[0].title === "Introduction", "first slide title");
248
+ process.stdout.write(` ${result.total_slides} slides listed\n`);
249
+ });
250
+
251
+ await test("presentation_gen: list_presentations", async () => {
252
+ const raw = await presGen.execute(
253
+ { action: "list_presentations", output_dir: OUTPUT_DIR },
254
+ ctx,
255
+ );
256
+ const result = JSON.parse(raw as string);
257
+ assert(result.success === true, "should succeed");
258
+ assert(
259
+ result.total_count === 1,
260
+ `should have 1 presentation, got ${result.total_count}`,
261
+ );
262
+ assert(result.presentations[0].total_slides === 3, "should show 3 slides");
263
+ assert(
264
+ result.presentations[0].title === "Test Presentation",
265
+ "title should match",
266
+ );
267
+ process.stdout.write(
268
+ ` found: ${result.presentations[0].title} (${result.presentations[0].total_slides} slides)\n`,
269
+ );
270
+ });
271
+
272
+ await test("presentation_gen: update existing slide", async () => {
273
+ const raw = await presGen.execute(
274
+ {
275
+ action: "create_slide",
276
+ presentation_name: PRES_NAME,
277
+ slide_number: 2,
278
+ slide_title: "Updated Details",
279
+ content: `<div style="padding:40px;box-sizing:border-box;height:1080px;width:1920px;background:#16213e;">
280
+ <h2 style="color:#e94560;font-size:48px;">Updated Content</h2>
281
+ </div>`,
282
+ presentation_title: "Test Presentation",
283
+ output_dir: OUTPUT_DIR,
284
+ },
285
+ ctx,
286
+ );
287
+ const result = JSON.parse(raw as string);
288
+ assert(result.success === true, "should succeed");
289
+ assert(result.total_slides === 3, "total should still be 3 after update");
290
+
291
+ const listRaw = await presGen.execute(
292
+ {
293
+ action: "list_slides",
294
+ presentation_name: PRES_NAME,
295
+ output_dir: OUTPUT_DIR,
296
+ },
297
+ ctx,
298
+ );
299
+ const listResult = JSON.parse(listRaw as string);
300
+ assert(
301
+ listResult.slides[1].title === "Updated Details",
302
+ `slide 2 title should be updated, got: ${listResult.slides[1].title}`,
303
+ );
304
+ });
305
+
306
+ await test("presentation_gen: delete_slide", async () => {
307
+ const raw = await presGen.execute(
308
+ {
309
+ action: "delete_slide",
310
+ presentation_name: PRES_NAME,
311
+ slide_number: 3,
312
+ output_dir: OUTPUT_DIR,
313
+ },
314
+ ctx,
315
+ );
316
+ const result = JSON.parse(raw as string);
317
+ assert(result.success === true, "should succeed");
318
+ assert(result.deleted_slide === 3, "should have deleted slide 3");
319
+ assert(
320
+ result.remaining_slides === 2,
321
+ `should have 2 remaining, got ${result.remaining_slides}`,
322
+ );
323
+ });
324
+
325
+ await test("presentation_gen: metadata.json structure", async () => {
326
+ const metaPath = join(
327
+ OUTPUT_DIR,
328
+ "presentations",
329
+ "test_presentation",
330
+ "metadata.json",
331
+ );
332
+ assert(existsSync(metaPath), "metadata.json should exist");
333
+ const metadata = JSON.parse(readFileSync(metaPath, "utf-8"));
334
+ assert(metadata.presentation_name === PRES_NAME, "name should match");
335
+ assert(metadata.title === "Test Presentation", "title should match");
336
+ assert(
337
+ Object.keys(metadata.slides).length === 2,
338
+ "should have 2 slides after deletion",
339
+ );
340
+ assert(metadata.slides["1"] !== undefined, "slide 1 should exist");
341
+ assert(metadata.slides["2"] !== undefined, "slide 2 should exist");
342
+ assert(metadata.slides["3"] === undefined, "slide 3 should not exist");
343
+ assert(typeof metadata.created_at === "string", "should have created_at");
344
+ assert(typeof metadata.updated_at === "string", "should have updated_at");
345
+ process.stdout.write(
346
+ ` metadata valid: ${Object.keys(metadata.slides).length} slides\n`,
347
+ );
348
+ });
349
+
350
+ await test("presentation_gen: images dir created", async () => {
351
+ const imagesDir = join(OUTPUT_DIR, "presentations", "images");
352
+ assert(
353
+ existsSync(imagesDir),
354
+ "presentations/images/ should be created automatically",
355
+ );
356
+ });
357
+
358
+ await test("presentation_gen: delete_presentation", async () => {
359
+ const raw = await presGen.execute(
360
+ {
361
+ action: "delete_presentation",
362
+ presentation_name: PRES_NAME,
363
+ output_dir: OUTPUT_DIR,
364
+ },
365
+ ctx,
366
+ );
367
+ const result = JSON.parse(raw as string);
368
+ assert(result.success === true, "should succeed");
369
+
370
+ const listRaw = await presGen.execute(
371
+ { action: "list_presentations", output_dir: OUTPUT_DIR },
372
+ ctx,
373
+ );
374
+ const listResult = JSON.parse(listRaw as string);
375
+ assert(
376
+ listResult.total_count === 0,
377
+ "should have 0 presentations after deletion",
378
+ );
379
+ });
380
+
381
+ // ── Cleanup ──
382
+
383
+ if (!process.argv.includes("--keep-output")) {
384
+ try {
385
+ rmSync(OUTPUT_DIR, { recursive: true, force: true });
386
+ } catch {}
387
+ }
388
+
389
+ process.stdout.write("\n\nAll presentation-gen tests passed!\n");
@@ -0,0 +1,74 @@
1
+ import { readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+
4
+ const envPath = resolve(import.meta.dir, "../../.env");
5
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
6
+ const trimmed = line.trim();
7
+ if (!trimmed || trimmed.startsWith("#")) continue;
8
+ const eq = trimmed.indexOf("=");
9
+ if (eq > 0) process.env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
10
+ }
11
+
12
+ async function test(name: string, fn: () => Promise<void>) {
13
+ process.stdout.write(`\n=== ${name} ===\n`);
14
+ try {
15
+ await fn();
16
+ process.stdout.write(`PASS\n`);
17
+ } catch (e) {
18
+ process.stdout.write(`FAIL: ${e}\n`);
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ function assert(cond: boolean, msg: string) {
24
+ if (!cond) throw new Error(msg);
25
+ }
26
+
27
+ const scrapeWebpage = (await import("../scrape-webpage.ts")).default;
28
+
29
+ const ctx = { directory: process.cwd(), worktree: process.cwd() } as Parameters<
30
+ typeof scrapeWebpage.execute
31
+ >[1];
32
+
33
+ await test("scrape_webpage: single URL", async () => {
34
+ const raw = await scrapeWebpage.execute({ urls: "https://example.com" }, ctx);
35
+ const result = JSON.parse(raw as string);
36
+ assert(
37
+ result.success === true,
38
+ `expected success, got: ${JSON.stringify(result).slice(0, 200)}`,
39
+ );
40
+ assert(typeof result.content === "string", "should have content");
41
+ assert(
42
+ result.content_length > 0,
43
+ `content should not be empty, got length=${result.content_length}`,
44
+ );
45
+ process.stdout.write(
46
+ ` title: "${result.title}", ${result.content_length} chars\n`,
47
+ );
48
+ });
49
+
50
+ await test("scrape_webpage: multiple URLs", async () => {
51
+ const raw = await scrapeWebpage.execute(
52
+ {
53
+ urls: "https://example.com,https://httpbin.org/html",
54
+ },
55
+ ctx,
56
+ );
57
+ const result = JSON.parse(raw as string);
58
+ assert(result.total === 2, `expected 2 results, got ${result.total}`);
59
+ assert(result.successful >= 1, "at least one should succeed");
60
+ process.stdout.write(` ${result.successful}/${result.total} succeeded\n`);
61
+ });
62
+
63
+ await test("scrape_webpage: missing API key", async () => {
64
+ const saved = process.env.FIRECRAWL_API_KEY;
65
+ delete process.env.FIRECRAWL_API_KEY;
66
+ const raw = await scrapeWebpage.execute({ urls: "https://example.com" }, ctx);
67
+ process.env.FIRECRAWL_API_KEY = saved;
68
+ assert(
69
+ (raw as string).includes("FIRECRAWL_API_KEY"),
70
+ "should mention missing key",
71
+ );
72
+ });
73
+
74
+ process.stdout.write("\n\nAll scrape-webpage tests passed!\n");
@@ -0,0 +1,241 @@
1
+ import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+
4
+ // Use a temp directory for the queue instead of /workspace/.show-user (which is container-only)
5
+ const TEST_SHOW_DIR = resolve(import.meta.dir, "test-show-user");
6
+ const TEST_QUEUE_FILE = `${TEST_SHOW_DIR}/queue.jsonl`;
7
+
8
+ async function test(name: string, fn: () => Promise<void>) {
9
+ process.stdout.write(`\n=== ${name} ===\n`);
10
+ try {
11
+ await fn();
12
+ process.stdout.write(`PASS\n`);
13
+ } catch (e) {
14
+ process.stdout.write(`FAIL: ${e}\n`);
15
+ process.exit(1);
16
+ }
17
+ }
18
+
19
+ function assert(cond: boolean, msg: string) {
20
+ if (!cond) throw new Error(msg);
21
+ }
22
+
23
+ const showUser = (await import("../show-user.ts")).default;
24
+
25
+ const ctx = { directory: process.cwd(), worktree: process.cwd() } as Parameters<
26
+ typeof showUser.execute
27
+ >[1];
28
+
29
+ // ── Validation Tests ──
30
+
31
+ await test("show-user: invalid action", async () => {
32
+ const raw = await showUser.execute({ action: "dance" }, ctx);
33
+ assert(
34
+ (raw as string).includes("Invalid action"),
35
+ "should reject invalid action",
36
+ );
37
+ });
38
+
39
+ await test("show-user: show without type", async () => {
40
+ const raw = await showUser.execute({ action: "show" }, ctx);
41
+ assert(
42
+ (raw as string).includes("'type' is required"),
43
+ "should require type for show",
44
+ );
45
+ });
46
+
47
+ await test("show-user: show file without path", async () => {
48
+ const raw = await showUser.execute(
49
+ { action: "show", type: "file" },
50
+ ctx,
51
+ );
52
+ assert(
53
+ (raw as string).includes("'path' is required"),
54
+ "should require path for file type",
55
+ );
56
+ });
57
+
58
+ await test("show-user: show image without path", async () => {
59
+ const raw = await showUser.execute(
60
+ { action: "show", type: "image" },
61
+ ctx,
62
+ );
63
+ assert(
64
+ (raw as string).includes("'path' is required"),
65
+ "should require path for image type",
66
+ );
67
+ });
68
+
69
+ await test("show-user: show url without url", async () => {
70
+ const raw = await showUser.execute(
71
+ { action: "show", type: "url" },
72
+ ctx,
73
+ );
74
+ assert(
75
+ (raw as string).includes("'url' is required"),
76
+ "should require url for url type",
77
+ );
78
+ });
79
+
80
+ await test("show-user: show text without content", async () => {
81
+ const raw = await showUser.execute(
82
+ { action: "show", type: "text" },
83
+ ctx,
84
+ );
85
+ assert(
86
+ (raw as string).includes("'content' is required"),
87
+ "should require content for text type",
88
+ );
89
+ });
90
+
91
+ await test("show-user: show error without content", async () => {
92
+ const raw = await showUser.execute(
93
+ { action: "show", type: "error" },
94
+ ctx,
95
+ );
96
+ assert(
97
+ (raw as string).includes("'content' is required"),
98
+ "should require content for error type",
99
+ );
100
+ });
101
+
102
+ await test("show-user: show file that doesn't exist", async () => {
103
+ const raw = await showUser.execute(
104
+ {
105
+ action: "show",
106
+ type: "file",
107
+ path: "/tmp/does-not-exist-show-user-test-12345.txt",
108
+ },
109
+ ctx,
110
+ );
111
+ assert(
112
+ (raw as string).includes("File not found"),
113
+ "should reject nonexistent file",
114
+ );
115
+ });
116
+
117
+ await test("show-user: invalid metadata JSON", async () => {
118
+ // Create a temp file so the file check passes
119
+ const tmpFile = resolve(TEST_SHOW_DIR, "temp.txt");
120
+ mkdirSync(TEST_SHOW_DIR, { recursive: true });
121
+ writeFileSync(tmpFile, "test");
122
+
123
+ const raw = await showUser.execute(
124
+ {
125
+ action: "show",
126
+ type: "file",
127
+ path: tmpFile,
128
+ metadata: "not valid json{{{",
129
+ },
130
+ ctx,
131
+ );
132
+ assert(
133
+ (raw as string).includes("Invalid JSON"),
134
+ "should reject invalid metadata JSON",
135
+ );
136
+ });
137
+
138
+ // ── Functional Tests (show, list, clear) ──
139
+
140
+ await test("show-user: list empty queue", async () => {
141
+ // Ensure clean state
142
+ if (existsSync(TEST_QUEUE_FILE)) rmSync(TEST_QUEUE_FILE);
143
+
144
+ const raw = await showUser.execute({ action: "list" }, ctx);
145
+ const result = JSON.parse(raw as string);
146
+ assert(result.success === true, "list should succeed");
147
+ assert(result.count === 0, "empty queue should have count 0");
148
+ });
149
+
150
+ await test("show-user: show a URL item", async () => {
151
+ const raw = await showUser.execute(
152
+ {
153
+ action: "show",
154
+ type: "url",
155
+ title: "Test Report",
156
+ description: "A test URL for unit testing",
157
+ url: "https://example.com/report",
158
+ },
159
+ ctx,
160
+ );
161
+ const result = JSON.parse(raw as string);
162
+ assert(result.success === true, `expected success: ${raw}`);
163
+ assert(result.entry.type === "url", "entry type should be url");
164
+ assert(result.entry.url === "https://example.com/report", "url should match");
165
+ assert(result.entry.title === "Test Report", "title should match");
166
+ assert(typeof result.entry.id === "string", "should have an id");
167
+ assert(typeof result.entry.timestamp === "string", "should have a timestamp");
168
+ });
169
+
170
+ await test("show-user: show a text item", async () => {
171
+ const raw = await showUser.execute(
172
+ {
173
+ action: "show",
174
+ type: "text",
175
+ title: "Summary",
176
+ content: "# Hello World\n\nThis is a **test** summary.",
177
+ },
178
+ ctx,
179
+ );
180
+ const result = JSON.parse(raw as string);
181
+ assert(result.success === true, `expected success: ${raw}`);
182
+ assert(result.entry.type === "text", "entry type should be text");
183
+ assert(
184
+ result.entry.content.includes("Hello World"),
185
+ "content should be preserved",
186
+ );
187
+ });
188
+
189
+ await test("show-user: show a file item", async () => {
190
+ const tmpFile = resolve(TEST_SHOW_DIR, "output.txt");
191
+ mkdirSync(TEST_SHOW_DIR, { recursive: true });
192
+ writeFileSync(tmpFile, "file content here");
193
+
194
+ const raw = await showUser.execute(
195
+ {
196
+ action: "show",
197
+ type: "file",
198
+ title: "Output File",
199
+ path: tmpFile,
200
+ metadata: '{"format":"txt","size":17}',
201
+ },
202
+ ctx,
203
+ );
204
+ const result = JSON.parse(raw as string);
205
+ assert(result.success === true, `expected success: ${raw}`);
206
+ assert(result.entry.type === "file", "entry type should be file");
207
+ assert(result.entry.path === tmpFile, "path should be resolved");
208
+ assert(result.entry.metadata?.format === "txt", "metadata should be parsed");
209
+ });
210
+
211
+ await test("show-user: show an error item", async () => {
212
+ const raw = await showUser.execute(
213
+ {
214
+ action: "show",
215
+ type: "error",
216
+ title: "Generation Failed",
217
+ content: "The image generation API returned a 500 error.",
218
+ },
219
+ ctx,
220
+ );
221
+ const result = JSON.parse(raw as string);
222
+ assert(result.success === true, `expected success: ${raw}`);
223
+ assert(result.entry.type === "error", "entry type should be error");
224
+ });
225
+
226
+ await test("show-user: clear queue", async () => {
227
+ const raw = await showUser.execute({ action: "clear" }, ctx);
228
+ const result = JSON.parse(raw as string);
229
+ assert(result.success === true, "clear should succeed");
230
+ assert(result.message === "Queue cleared.", "should confirm cleared");
231
+ });
232
+
233
+ // ── Cleanup ──
234
+
235
+ if (!process.argv.includes("--keep-output")) {
236
+ try {
237
+ rmSync(TEST_SHOW_DIR, { recursive: true, force: true });
238
+ } catch {}
239
+ }
240
+
241
+ process.stdout.write("\n\nAll show-user tests passed!\n");