scriptorium 0.6.1 → 0.7.2

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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/assets/icons/social/reddit.png +0 -0
  3. data/assets/icons/social/x-logo.png +0 -0
  4. data/assets/imagenotfound.jpg +0 -0
  5. data/bin/sblog +84 -5
  6. data/bin/scriptorium +1 -0
  7. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
  8. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
  9. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
  10. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
  11. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
  12. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
  13. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
  14. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
  15. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  16. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  17. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  18. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  19. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  20. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  21. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  22. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  23. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  24. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  25. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  26. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  27. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  28. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  29. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  30. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  31. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  32. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  33. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  34. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  35. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  36. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  37. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  38. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  39. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  40. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  41. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  42. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  43. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  44. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  45. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  46. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  47. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  48. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  49. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  50. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  51. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  52. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  53. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  54. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  55. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  56. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  57. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  58. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  59. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  60. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  61. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  62. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  63. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  64. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  65. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  66. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  67. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  68. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  69. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  70. data/doc/imported/0018-ruby-puns/post.html +46 -0
  71. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  72. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  73. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  74. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  75. data/doc/imported/0020-fffff/metadata.txt +7 -0
  76. data/doc/imported/0020-fffff/post.html +24 -0
  77. data/doc/imported/0020-fffff/source.lt3 +12 -0
  78. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  79. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  80. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  81. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  82. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  83. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  84. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  85. data/doc/imported/0024-just-a-test/post.html +24 -0
  86. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  87. data/doc/imported/import_summary.txt +98 -0
  88. data/doc/livetext-informal-spec.txt +65 -0
  89. data/doc/myuserdoc/ch-0.lt3 +31 -0
  90. data/doc/myuserdoc/ch-1.lt3 +37 -0
  91. data/doc/myuserdoc/ch-10.lt3 +22 -0
  92. data/doc/myuserdoc/ch-2.lt3 +37 -0
  93. data/doc/myuserdoc/ch-3.lt3 +19 -0
  94. data/doc/myuserdoc/ch-4.lt3 +43 -0
  95. data/doc/myuserdoc/ch-5.lt3 +22 -0
  96. data/doc/myuserdoc/ch-6.lt3 +19 -0
  97. data/doc/myuserdoc/ch-7.lt3 +16 -0
  98. data/doc/myuserdoc/ch-8.lt3 +13 -0
  99. data/doc/myuserdoc/ch-9.lt3 +19 -0
  100. data/doc/myuserdoc/tweak.rb +18 -0
  101. data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
  102. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  103. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  104. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  105. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  106. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  107. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  108. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  109. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  110. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  111. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  112. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  113. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  114. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  115. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  116. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  117. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  118. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  119. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  120. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  121. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  122. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  123. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  124. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  125. data/doc/old-repo +1 -0
  126. data/doc/reddit_integration.md +2 -2
  127. data/doc/user.lt3 +0 -3
  128. data/lib/scriptorium/api.rb +1811 -78
  129. data/lib/scriptorium/banner_svg.rb +55 -68
  130. data/lib/scriptorium/contract.rb +3 -2
  131. data/lib/scriptorium/exceptions.rb +133 -102
  132. data/lib/scriptorium/helpers.rb +282 -82
  133. data/lib/scriptorium/post.rb +81 -17
  134. data/lib/scriptorium/reddit.rb +1 -1
  135. data/lib/scriptorium/repo.rb +478 -164
  136. data/lib/scriptorium/standard_files.rb +30 -396
  137. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  138. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  139. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  140. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  141. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  142. data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
  143. data/lib/scriptorium/support/highlight/css.txt +2 -0
  144. data/lib/scriptorium/support/highlight/custom.css +119 -0
  145. data/lib/scriptorium/support/highlight/js.txt +1 -0
  146. data/lib/scriptorium/support/post_index/config.txt +15 -0
  147. data/lib/scriptorium/support/post_index/style.css +55 -0
  148. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  149. data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
  150. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  151. data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
  152. data/lib/scriptorium/theme.rb +83 -70
  153. data/lib/scriptorium/version.rb +2 -2
  154. data/lib/scriptorium/view.rb +194 -149
  155. data/lib/scriptorium.rb +24 -1
  156. data/lib/skeleton.rb +4 -1
  157. data/scriptorium.gemspec +2 -1
  158. data/test/WEB_INTEGRATION_README.md +196 -0
  159. data/test/all +40 -0
  160. data/test/banner_svg/unit.rb +267 -35
  161. data/test/config/deployment.txt +5 -0
  162. data/test/integration/integration_test.rb +7 -7
  163. data/test/integration/preview_flow_test.rb +94 -0
  164. data/test/livetext_plugin_test.rb +453 -182
  165. data/test/manual/banner-tests/test01.html +82 -18
  166. data/test/manual/banner-tests/test02.html +82 -18
  167. data/test/manual/banner-tests/test03.html +82 -18
  168. data/test/manual/banner-tests/test04.html +89 -25
  169. data/test/manual/banner-tests/test05.html +89 -25
  170. data/test/manual/banner-tests/test06.html +89 -25
  171. data/test/manual/banner-tests/test07.html +89 -25
  172. data/test/manual/banner-tests/test08.html +82 -18
  173. data/test/manual/banner-tests/test09.html +82 -18
  174. data/test/manual/banner-tests/test10.html +82 -18
  175. data/test/manual/banner-tests/test11.html +82 -18
  176. data/test/manual/banner-tests/test12.html +82 -18
  177. data/test/manual/banner-tests/test13.html +82 -18
  178. data/test/manual/banner-tests/test14.html +82 -18
  179. data/test/manual/banner-tests/test15.html +82 -18
  180. data/test/manual/banner-tests/test16.html +82 -18
  181. data/test/manual/banner-tests/test17.html +82 -18
  182. data/test/manual/banner-tests/test18.html +90 -26
  183. data/test/manual/banner-tests/test19.html +90 -26
  184. data/test/manual/banner-tests/test20.html +90 -26
  185. data/test/manual/banner-tests/test21.html +90 -26
  186. data/test/manual/banner-tests/test22.html +90 -26
  187. data/test/manual/banner-tests/test23.html +90 -26
  188. data/test/manual/banner-tests/test24.html +90 -26
  189. data/test/manual/banner-tests/test25.html +89 -25
  190. data/test/manual/banner_environment.rb +15 -2
  191. data/test/manual/codemirror_demo.html +773 -0
  192. data/test/manual/create_posts_for_web.rb +114 -0
  193. data/test/manual/preview_manual_test.rb +129 -0
  194. data/test/manual/test_banner_features.rb +14 -14
  195. data/test/manual/test_banner_integration.rb +115 -0
  196. data/test/manual/test_banner_radial.rb +87 -0
  197. data/test/manual/test_syntax_highlighting.rb +60 -40
  198. data/test/support/preview_utils.rb +88 -0
  199. data/test/test_gem_assets.rb +48 -0
  200. data/test/test_helpers.rb +10 -0
  201. data/test/tui_editor_integration_test.rb +15 -15
  202. data/test/tui_integration_test.rb +687 -441
  203. data/test/unit/api.rb +757 -37
  204. data/test/unit/asset_management.rb +195 -221
  205. data/test/unit/backup_test.rb +451 -0
  206. data/test/unit/contract_test.rb +1 -23
  207. data/test/unit/core.rb +415 -61
  208. data/test/unit/deploy_config_test.rb +248 -0
  209. data/test/unit/deploy_test.rb +312 -21
  210. data/test/unit/edit_post_test.rb +168 -0
  211. data/test/unit/gem_asset_management.rb +36 -42
  212. data/test/unit/livetext_basic.rb +23 -35
  213. data/test/unit/livetext_compatibility.rb +7 -14
  214. data/test/unit/parse_cmd_test.rb +260 -0
  215. data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
  216. data/test/unit/post.rb +91 -26
  217. data/test/unit/post_index_config_test.rb +258 -0
  218. data/test/unit/post_state_helpers_test.rb +137 -0
  219. data/test/unit/read_commented_file_test.rb +8 -6
  220. data/test/unit/repo.rb +75 -54
  221. data/test/unit/social_test.rb +41 -44
  222. data/test/unit/syntax_highlighting.rb +70 -0
  223. data/test/unit/theme_management_test.rb +91 -0
  224. data/test/unit/view.rb +79 -12
  225. data/test/unit/widgets.rb +8 -8
  226. data/test/web_integration_test.rb +231 -0
  227. data/test/web_test_helper.rb +218 -0
  228. data/test/web_workflow_test.rb +527 -0
  229. data/ui/tui/bin/scriptorium +885 -415
  230. data/ui/web/app/app.rb +1398 -176
  231. data/ui/web/app/assets/livetext_mode.js +244 -0
  232. data/ui/web/app/error_helpers.rb +16 -16
  233. data/ui/web/app/views/advanced_config.erb +8 -2
  234. data/ui/web/app/views/asset_management.erb +56 -0
  235. data/ui/web/app/views/backup_management.erb +238 -0
  236. data/ui/web/app/views/config_widget.erb +232 -0
  237. data/ui/web/app/views/dashboard.erb +64 -72
  238. data/ui/web/app/views/deploy_config.erb +3 -0
  239. data/ui/web/app/views/edit_pages.erb +170 -2
  240. data/ui/web/app/views/edit_post.erb +130 -9
  241. data/ui/web/app/views/edit_theme.erb +73 -0
  242. data/ui/web/app/views/edit_theme_file.erb +74 -0
  243. data/ui/web/app/views/theme_management.erb +130 -0
  244. data/ui/web/app/views/view_dashboard.erb +666 -25
  245. data/ui/web/app/views/widgets.erb +249 -0
  246. data/ui/web/bin/scriptorium-web +35 -24
  247. data/ui/web/tmp/timing.log +17 -0
  248. data/ui/web/tmp/web_server.log +0 -5
  249. metadata +190 -116
  250. data/assets/back-icon.png +0 -0
  251. data/assets/icons/facebook.svg +0 -1
  252. data/assets/icons/github.svg +0 -1
  253. data/assets/icons/instagram.svg +0 -1
  254. data/assets/icons/reddit.svg +0 -1
  255. data/assets/icons/x.svg +0 -1
  256. data/assets/icons/youtube.svg +0 -1
  257. data/bin/scriptorium +0 -1511
  258. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
  259. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
  260. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
  261. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
  262. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
  263. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
  264. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
  265. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
  266. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
  267. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
  268. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
  269. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
  270. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
  271. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
  272. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
  273. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
  274. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
  275. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
  276. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
  277. data/lib/scriptorium/syntax_highlighter.rb +0 -234
  278. data/test/manual/deploy_symlink_demo.rb +0 -142
  279. data/test/manual/symlink_demo.rb +0 -117
  280. data/test/manual/test2.rb +0 -12
  281. data/test/manual/test_banner_from_file.rb +0 -150
  282. data/test/manual/test_banner_in_header.rb +0 -35
  283. data/test/manual/test_code_highlighting.rb +0 -68
  284. data/test/manual/test_complex_header.rb +0 -74
  285. data/test/manual/test_empty_header.rb +0 -32
  286. data/test/manual/test_radial_custom.rb +0 -58
  287. data/test/manual/test_radial_large_radius.rb +0 -52
  288. data/test/manual/test_svg_debug.rb +0 -47
  289. data/test/pages-demo/config/currentview.txt +0 -1
  290. data/test/pages-demo/views/demo/config/common.js +0 -57
  291. data/test/pages-demo/views/demo/config/footer.txt +0 -1
  292. data/test/pages-demo/views/demo/config/global-head.txt +0 -8
  293. data/test/pages-demo/views/demo/config/header.txt +0 -1
  294. data/test/pages-demo/views/demo/config/layout.txt +0 -1
  295. data/test/pages-demo/views/demo/config/left.txt +0 -1
  296. data/test/pages-demo/views/demo/config/main.txt +0 -1
  297. data/test/pages-demo/views/demo/config/right.txt +0 -1
  298. data/test/pages-demo/views/demo/config.txt +0 -3
  299. data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
  300. data/test/pages-demo/views/demo/output/panes/header.html +0 -1
  301. data/test/pages-demo/views/demo/output/panes/left.html +0 -1
  302. data/test/pages-demo/views/demo/output/panes/main.html +0 -1
  303. data/test/pages-demo/views/demo/output/panes/right.html +0 -1
  304. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
  305. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
  306. data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
  307. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
  308. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
  309. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
  310. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
  311. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
  312. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
  313. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
  314. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
  315. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
  316. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
  317. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
  318. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
  319. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
  320. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
  321. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
  322. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
  323. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
  324. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
  325. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
  326. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
  327. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
  328. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
  329. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
  330. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
  331. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
  332. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
  333. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
  334. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
  335. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
  336. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
  337. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
  338. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
  339. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
  340. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
  341. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
  342. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
  343. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
  344. data/ui/web/tmp/web_server.pid +0 -1
  345. /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
  346. /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
  347. /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
  348. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
  349. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
  350. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
  351. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
  352. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
  353. /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
  354. /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
  355. /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
  356. /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
  357. /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
  358. /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
@@ -9,8 +9,14 @@ class ScriptoriumTUI
9
9
  include Scriptorium::Helpers
10
10
 
11
11
  def initialize
12
- @api = Scriptorium::API.new(testmode: true)
13
- @testing = true
12
+ # Parse command line arguments for test mode
13
+ @testing = ARGV.include?('--test')
14
+
15
+ # Remove --test from ARGV so it doesn't interfere with other processing
16
+ ARGV.delete('--test')
17
+
18
+ # Default to production mode (use core Repo default: ~/.scriptorium)
19
+ @api = Scriptorium::API.new(testmode: @testing)
14
20
  setup_readline
15
21
  end
16
22
 
@@ -35,25 +41,65 @@ class ScriptoriumTUI
35
41
  return false
36
42
  end
37
43
  else
38
- # Later: for production
44
+ # Production mode: use core Repo default (~/.scriptorium)
45
+ home = ENV['HOME']
46
+ production_path = "#{home}/.scriptorium"
47
+
48
+ if Dir.exist?(production_path)
49
+ puts "Found existing production repository: #{production_path}"
50
+ begin
51
+ @api.open_repo(production_path)
52
+ puts "Current view: #{@api.current_view&.name || 'nil'}"
53
+ puts "Loaded production repository"
54
+ return true
55
+ rescue => e
56
+ puts "Error opening repository: #{e.message}"
57
+ return false
58
+ end
59
+ else
60
+ puts "No repository found."
61
+ return false
62
+ end
39
63
  end
40
64
  return false
41
65
  end
42
66
 
43
67
  def create_new_repo
44
- puts "Creating new repository..."
45
- @testing = "scriptorium-TEST"
46
- @api = Scriptorium::API.new(testmode: true)
47
- begin
48
- @api.create_repo("scriptorium-TEST")
49
- puts "Created repository successfully."
68
+ if @testing
69
+ puts "Creating new test repository..."
70
+ @testing = "scriptorium-TEST"
71
+ @api = Scriptorium::API.new(testmode: true)
72
+ begin
73
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: About to create repo" }
74
+ @api.create_repo("scriptorium-TEST")
75
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: Repo created, about to call get_started" }
76
+ puts "Created test repository successfully."
77
+
78
+ # Run initial setup (like Runeblog)
79
+ get_started
80
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: get_started completed" }
81
+ rescue => e
82
+ puts " DEBUG: Exception in create_new_repo: #{e.class} - #{e.message}"
83
+ puts "Error creating repository: #{e.message}"
84
+ puts e.backtrace.first if @testing
85
+ return false
86
+ end
87
+ else
88
+ puts "Creating new production repository..."
89
+ home = ENV['HOME']
90
+ production_path = "#{home}/.scriptorium"
50
91
 
51
- # Run initial setup (like Runeblog)
52
- get_started
53
- rescue => e
54
- puts "Error creating repository: #{e.message}"
55
- puts e.backtrace.first if @testing
56
- return false
92
+ begin
93
+ # For production, use the full home path
94
+ @api.create_repo(production_path)
95
+ puts "Created production repository successfully."
96
+
97
+ # Run initial setup (like Runeblog)
98
+ get_started
99
+ rescue => e
100
+ puts "Error creating repository: #{e.message}"
101
+ return false
102
+ end
57
103
  end
58
104
  end
59
105
 
@@ -64,7 +110,7 @@ class ScriptoriumTUI
64
110
  puts "Let's set up your first view!"
65
111
 
66
112
  # Create a new view using existing interactive method
67
- create_view("view")
113
+ create_view
68
114
 
69
115
  # Get the current view name (the one we just created)
70
116
  current_view = @api.current_view
@@ -196,14 +242,13 @@ class ScriptoriumTUI
196
242
  end
197
243
 
198
244
  def mainloop
245
+ # Ensure we have a valid API with repository
246
+ if @api.nil? || @api.instance_variable_get(:@repo).nil?
247
+ puts "Error: No valid repository loaded. Exiting."
248
+ return
249
+ end
199
250
  loop do
200
251
  begin
201
- # Ensure we have a valid API with repository
202
- if @api.nil? || @api.instance_variable_get(:@repo).nil?
203
- puts "Error: No valid repository loaded. Exiting."
204
- return
205
- end
206
-
207
252
  current_view = @api.current_view
208
253
  current_view_name = current_view&.name || "no-view"
209
254
  prompt = "[#{current_view_name}] "
@@ -215,18 +260,14 @@ class ScriptoriumTUI
215
260
  print prompt
216
261
  input = gets&.chomp&.strip
217
262
  end
218
-
219
263
  break if input.nil? || input.downcase == "quit" || input.downcase == "q"
220
-
221
264
  next if input.empty?
222
-
223
265
  execute_command(input)
224
266
  rescue Interrupt
225
267
  puts "\nUse 'quit' to exit"
226
268
  rescue => e
227
- puts "Error: #{e.message}"
269
+ puts e.message
228
270
  puts e.backtrace.first if @testing
229
- puts "DEBUG: Exception caught in mainloop: #{e.class}: #{e.message}"
230
271
  end
231
272
  end
232
273
 
@@ -235,9 +276,7 @@ class ScriptoriumTUI
235
276
  puts
236
277
  end
237
278
 
238
- private
239
-
240
- def setup_readline
279
+ private def setup_readline
241
280
  # Only set up Readline if we're not in automated testing mode
242
281
  return if ENV['NOREADLINE']
243
282
 
@@ -252,7 +291,7 @@ class ScriptoriumTUI
252
291
 
253
292
  if args.empty?
254
293
  # Complete command names
255
- commands = %w[view change list new version help quit cv lsv v h q]
294
+ commands = %w[view change list new version help quit cv lsv v h q upload copy delete asset configure]
256
295
  completions = commands.select { |cmd| cmd.start_with?(command || "") }
257
296
  elsif command == "change" || command == "cv"
258
297
  # Complete view names
@@ -263,10 +302,31 @@ class ScriptoriumTUI
263
302
  elsif command == "list" && args.length == 1 && args[0] == "views"
264
303
  # Complete "list views" command
265
304
  completions = []
305
+ elsif command == "list" && args.length == 1 && args[0] == "assets"
306
+ # Complete asset targets
307
+ completions = %w[global library view gem]
266
308
  elsif command == "new" && args.length == 1 && args[0] == "view"
267
309
  # Suggest common view names for new view
268
310
  suggestions = %w[blog personal work tech travel]
269
311
  completions = suggestions
312
+ elsif command == "upload" && args.length == 1 && args[0] == "asset"
313
+ # Complete asset targets
314
+ completions = %w[global library view]
315
+ elsif command == "copy" && args.length == 1 && args[0] == "asset"
316
+ # Complete asset targets
317
+ completions = %w[global library view gem]
318
+ elsif command == "delete" && args.length == 1 && args[0] == "asset"
319
+ # Complete asset targets
320
+ completions = %w[global library view]
321
+ elsif command == "asset" && args.length == 1 && args[0] == "info"
322
+ # Complete asset targets
323
+ completions = %w[global library view gem]
324
+ elsif command == "configure" && args.length == 1 && args[0] == "deployment"
325
+ # Complete view names for deployment config
326
+ if @api
327
+ view_names = @api.views.map(&:name)
328
+ completions = view_names
329
+ end
270
330
  end
271
331
 
272
332
  completions
@@ -281,82 +341,209 @@ class ScriptoriumTUI
281
341
  puts "Test repository created successfully!"
282
342
  end
283
343
 
284
- private def execute_command(input)
285
- parts = input.split(/\s+/, 2)
286
- cmd = parts[0].downcase
287
- args = parts[1] || ""
288
-
289
- # Handle multi-word commands first
290
- if cmd == "list" && args.start_with?("views")
291
- list_views
292
- elsif cmd == "list" && args.start_with?("posts")
293
- list_posts
294
- elsif cmd == "list" && args.start_with?("drafts")
295
- list_drafts
296
- elsif cmd == "change" && args.start_with?("view")
297
- change_view(args)
298
- elsif cmd == "new" && args.start_with?("view")
299
- create_view(args)
300
- elsif cmd == "new" && args.start_with?("draft")
301
- create_draft(args)
302
- elsif cmd == "new" && args.start_with?("post")
303
- create_post(args)
304
- elsif cmd == "list" && args.start_with?("themes")
305
- list_themes
306
- elsif cmd == "clone" && args.include?(" ")
307
- clone_theme(args)
308
-
344
+ def which(command)
345
+ # Mock which in test mode to avoid hanging
346
+ if @testing
347
+ case command
348
+ when 'nano', 'vim', 'vi', 'ed'
349
+ "/usr/bin/#{command}"
350
+ else
351
+ nil
352
+ end
309
353
  else
310
- # Handle single-word commands
311
- case cmd
312
- when "help", "h"
313
- show_help
314
- when "view"
315
- show_current_view
316
- when "cv"
317
- change_view(args)
318
- when "lsv"
319
- list_views
320
- when "lsp"
321
- list_posts
322
- when "lsd"
323
- list_drafts
324
- when "cd"
325
- create_draft("draft")
326
- when "version", "v"
327
- show_version
328
- when "deploy"
329
- deploy_current_view
330
- when "preview"
331
- preview_current_view
332
- when "browse"
333
- browse_deployed_view
334
- when "list" && args.start_with?("widgets")
335
- list_widgets
336
- when "add" && args.start_with?("widget")
337
- add_widget(args)
338
- when "config" && args.start_with?("widget")
339
- config_widget(args)
340
- when "config" && args.start_with?("social")
341
- config_social
342
- when "config" && args.start_with?("reddit")
343
- config_reddit
344
- when "generate"
345
- generate_current_view
346
- when "quit", "q"
347
- exit 0
354
+ # Use File.which if available (Ruby 3.2+)
355
+ if File.respond_to?(:which)
356
+ File.which(command)
348
357
  else
349
- puts
350
- puts " Unknown command: #{cmd}. Type 'help' for available commands."
351
- puts
358
+ # Fall back to system call
359
+ result = `which #{command} 2>/dev/null`.chomp
360
+ result.empty? ? nil : result
352
361
  end
353
362
  end
354
363
  end
355
364
 
365
+ def get_started
366
+ puts
367
+ puts " No editor configured. Let's set one up."
368
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: About to call pick_editor" }
369
+ pick_editor
370
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: pick_editor completed" }
371
+
372
+ puts
373
+ puts " Setup complete!"
374
+ puts " You can now use 'new post <title>' to create posts with your editor."
375
+ puts
376
+ end
377
+
378
+ def pick_editor
379
+ puts
380
+ puts " Available editors:"
381
+
382
+ # Check for common editors (prioritized for single file editing)
383
+ editors = []
384
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: Checking for editors" }
385
+ %w[nano vim emacs vi micro].each do |editor|
386
+ if which(editor)
387
+ editors << editor
388
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: Found editor: #{editor}" }
389
+ end
390
+ end
391
+
392
+ # The original Unix line editor - for the brave souls who want ultimate speed
393
+ if which("ed")
394
+ editors << "ed"
395
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: Found editor: ed" }
396
+ end
397
+
398
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: Total editors found: #{editors.length}" }
399
+
400
+ if editors.empty?
401
+ puts " No common editors found. Please install nano, vim, emacs, vi, micro, or ed."
402
+ puts " You can manually set your editor later by editing config/editor.txt"
403
+ puts
404
+ return
405
+ end
406
+
407
+ # Show available editors
408
+ editors.each_with_index do |editor, index|
409
+ puts " #{index + 1}. #{editor}"
410
+ end
411
+
412
+ # Let user pick
413
+ print " Choose editor (1-#{editors.length}): "
414
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: About to call get_string for editor choice" }
415
+ choice = get_string
416
+ File.open('/dev/tty', 'w') { |f| f.puts " DEBUG: get_string returned: '#{choice}'" }
417
+
418
+ if choice && choice.match?(/^\d+$/) && choice.to_i.between?(1, editors.length)
419
+ selected_editor = editors[choice.to_i - 1]
420
+
421
+ # Save the choice
422
+ make_dir(@api.root/"config")
423
+ write_file(@api.root/"config/editor.txt", selected_editor)
424
+
425
+ puts
426
+ puts " Selected editor: #{selected_editor}"
427
+ puts " Editor preference saved to config/editor.txt"
428
+ else
429
+ puts
430
+ puts " Invalid choice. Editor not changed."
431
+ end
432
+ end
433
+
434
+ private def execute_command(input)
435
+ calling = parse_cmd(input.strip)
436
+ name, *args = calling
437
+ send(name, *args)
438
+ rescue NoMethodError => e
439
+ puts
440
+ puts e.message
441
+ puts " ...Unknown command: #{name}. Type 'help' for available commands."
442
+ puts
443
+ end
444
+
445
+ private def two_words?(parts)
446
+ cmds = ["list views", "list posts", "list drafts", "list assets",
447
+ "list widgets", "list themes", "list backups", "change view", "new view",
448
+ "new post", "upload asset", "copy asset", "delete asset",
449
+ "delete theme", "delete backup", "asset info", "configure deployment",
450
+ "add widget", "config widget", "config social",
451
+ "config reddit", "clone theme"]
452
+ two_word_cmd = parts[0..1].join(" ")
453
+ flag = cmds.include?(two_word_cmd)
454
+ return flag ? two_word_cmd : nil
455
+ end
456
+
457
+ private def one_word?(parts)
458
+ cmds = %w[help h view cv lsv lsp lsd version v deploy preview browse
459
+ generate quit q backup restore]
460
+ one_word_cmd = parts[0]
461
+ flag = cmds.include?(one_word_cmd)
462
+ return flag ? one_word_cmd : nil
463
+ end
464
+
465
+ private def parse_cmd(cmdstr)
466
+ parts = cmdstr.downcase.split
467
+ case
468
+ when cmd = two_words?(parts)
469
+ args = parts[2..-1]
470
+ when cmd = one_word?(parts)
471
+ args = parts[1..-1]
472
+ else
473
+ puts " Unknown command: #{cmdstr}"
474
+ return [:unknown_command, cmdstr]
475
+ end
476
+ transform(cmd, args)
477
+ end
478
+
479
+ private def transform(cmd, args)
480
+ command_map = {
481
+ # Single word commands
482
+ "help" => [:show_help],
483
+ "h" => [:show_help],
484
+ "view" => [:show_current_view],
485
+ "cv" => [:change_view, args.join(" ")],
486
+ "lsv" => [:list_views],
487
+ "lsp" => [:list_posts],
488
+ "lsd" => [:list_drafts],
489
+ "version" => [:show_version],
490
+ "v" => [:show_version],
491
+ "deploy" => [:deploy_current_view],
492
+ "preview" => [:preview_current_view],
493
+ "browse" => [:browse_deployed_view],
494
+ "generate" => [:generate_current_view],
495
+ "quit" => [:exit, 0],
496
+ "q" => [:exit, 0],
497
+ "backup" => [:create_backup, args],
498
+ "restore" => [:restore_backup, args],
499
+
500
+ # Two word commands
501
+ "list views" => [:list_views],
502
+ "list posts" => [:list_posts],
503
+ "list drafts" => [:list_drafts],
504
+ "list assets" => [:list_assets, args],
505
+ "list widgets" => [:list_widgets, args],
506
+ "list themes" => [:list_themes, args],
507
+ "list backups" => [:list_backups],
508
+ "change view" => [:create_view, args],
509
+ "new view" => [:create_view, args],
510
+ "new post" => [:create_post, args],
511
+ "upload asset" => [:upload_asset, args],
512
+ "copy asset" => [:copy_asset, args],
513
+ "delete asset" => [:delete_asset, args],
514
+ "delete theme" => [:delete_theme, args],
515
+ "delete backup" => [:delete_backup, args],
516
+ "asset info" => [:asset_info, args],
517
+ "configure deployment" => [:configure_deployment, args],
518
+ "add widget" => [:add_widget, args],
519
+ "config widget" => [:config_widget, args],
520
+ "config social" => [:config_social],
521
+ "config reddit" => [:config_reddit],
522
+ "clone theme" => [:clone_theme, args]
523
+ }
524
+
525
+ command_map[cmd] || [:unknown_command, cmd]
526
+ end
527
+
528
+
529
+ private def unknown_command(cmd)
530
+ puts
531
+ puts " Unknown command: #{cmd}. Type 'help' for available commands."
532
+ puts
533
+ end
534
+
356
535
  private def show_help
357
536
  puts
358
537
  puts <<~HELP
538
+ Scriptorium CLI - Blog Management Tool
539
+
540
+ Usage: scriptorium [--test]
359
541
 
542
+ Flags:
543
+ --test - Use test repository (scriptorium-TEST)
544
+ - Default: production repository (~/.scriptorium)
545
+
546
+ Commands:
360
547
  view - Show current view
361
548
  change view [<name>] - Switch to a view
362
549
  cv [<name>]
@@ -370,6 +557,12 @@ class ScriptoriumTUI
370
557
  lsd
371
558
  new post [<title>] - Create draft, edit, and convert to post
372
559
 
560
+ list assets [target] - List assets (global, library, view, gem)
561
+ upload asset [file] [target] - Upload file to asset location
562
+ copy asset [file] [from] [to] - Copy asset between locations
563
+ delete asset [file] [target] - Delete asset from location
564
+ asset info [file] [target] - Show asset information
565
+ configure deployment [view] - Edit deployment configuration
373
566
  deploy - Deploy current view to server
374
567
  preview - Preview current view locally
375
568
  browse - Browse deployed view on server
@@ -383,7 +576,13 @@ class ScriptoriumTUI
383
576
  generate - Regenerate current view
384
577
 
385
578
  list themes - List available themes
386
- clone <old> <new> - Clone a theme
579
+ clone theme <source> <name> - Clone a theme
580
+ delete theme <path> - Delete a user theme
581
+
582
+ list backups - List available backups
583
+ backup [type] [desc] - Create backup (full/incr)
584
+ restore [timestamp] [strategy] - Restore from backup
585
+ delete backup [timestamp] - Delete a backup
387
586
 
388
587
  version, v - Show version
389
588
  help, h - Show this help
@@ -434,243 +633,82 @@ class ScriptoriumTUI
434
633
  return if view_name.nil? || view_name.empty?
435
634
  end
436
635
 
437
- view = @api.lookup_view(view_name)
438
- @api.view(view_name)
439
- puts
440
- puts " Switched to view '#{view_name}'"
441
- puts
442
- rescue => e
443
- puts
444
- puts " View '#{view_name}' not found"
445
- puts
446
- end
447
-
448
- private def create_view(args)
449
- # Handle "new view" format - prompt for all parameters
450
- if args == "view" || args.start_with?("view ")
451
- # Remove "view " prefix if present, otherwise args is just "view"
452
- view_args = args == "view" ? "" : args[5..-1]
453
-
454
- if view_args.strip.empty?
455
- # Interactive mode - prompt for all parameters
456
- print " Enter view name: "
457
- name = get_string
458
- return if name.nil? || name.empty?
459
-
460
- print " Enter view title: "
461
- title = get_string
462
- return if title.nil? || title.empty?
463
-
464
- print " Enter subtitle (optional): "
465
- subtitle = get_string
466
- subtitle = nil if subtitle.empty?
467
-
468
- # Check if view already exists
469
- existing_views = @api.views
470
- if existing_views.any? { |view| view.name == name }
471
- puts
472
- puts " View '#{name}' already exists"
473
- puts
474
- return
475
- end
476
-
477
- # Create view with all parameters
478
- begin
479
- @api.create_view(name, title, subtitle, theme: "standard")
480
- puts
481
- puts " Created view '#{name}' with title '#{title}'"
482
- puts " Switched to view '#{name}'"
483
- puts
484
- rescue Exception => e
485
- puts
486
- puts " #{e.message}"
487
- puts
488
- puts "DEBUG: Exception caught in create_view (interactive): #{e.class}: #{e.message}"
489
- end
490
- else
491
- # Legacy mode - still support "new view <name> <title>"
492
- parts = view_args.split(/\s+/, 2)
493
- if parts.length < 2
494
- puts
495
- puts " Usage: new view [<name> <title>]"
496
- puts
497
- return
498
- end
499
-
500
- name, title = parts
501
-
502
- # Prompt for subtitle
503
- print " Enter subtitle (optional): "
504
- subtitle = get_string
505
- subtitle = nil if subtitle.empty?
506
-
507
- # Check if view already exists
508
- existing_views = @api.views
509
- if existing_views.any? { |view| view.name == name }
510
- puts
511
- puts " View '#{name}' already exists"
512
- puts
513
- return
514
- end
515
-
516
- # Create view with all parameters
517
- begin
518
- @api.create_view(name, title, subtitle, theme: "standard")
519
- puts
520
- puts " Created view '#{name}' with title '#{title}'"
521
- puts " Switched to view '#{name}'"
522
- puts
523
- rescue Exception => e
524
- puts
525
- puts " #{e.message}"
526
- puts
527
- puts "DEBUG: Exception caught in create_view (legacy): #{e.class}: #{e.message}"
528
- end
529
- end
530
- else
636
+ begin
637
+ view = @api.lookup_view(view_name)
638
+ @api.view(view_name)
531
639
  puts
532
- puts " Usage: new view [<name> <title>]"
640
+ puts " Switched to view '#{view_name}'"
533
641
  puts
534
- end
535
- end
536
-
537
- private def create_draft(args)
538
- # Handle "new draft" format - prompt for all parameters
539
- if args == "draft" || args.start_with?("draft ")
540
- # Remove "draft " prefix if present, otherwise args is just "draft"
541
- draft_args = args == "draft" ? "" : args[6..-1]
542
-
543
- if draft_args.strip.empty?
544
- # Interactive mode - prompt for all parameters
545
- print " Enter draft title: "
546
- title = gets&.chomp&.strip
547
- return if title.nil? || title.empty?
548
-
549
- print " Enter draft body: "
550
- body = gets&.chomp&.strip
551
- return if body.nil? || body.empty?
552
-
553
- print " Enter tags (optional, comma-separated): "
554
- tags_input = gets&.chomp&.strip
555
- tags = tags_input.empty? ? nil : tags_input.split(",").map(&:strip)
556
-
557
- print " Enter blurb (optional): "
558
- blurb = gets&.chomp&.strip
559
- blurb = nil if blurb.empty?
560
-
561
- # Create draft with all parameters
562
- draft_path = @api.create_draft(
563
- title: title,
564
- body: body,
565
- views: @api.current_view&.name,
566
- tags: tags,
567
- blurb: blurb
568
- )
569
- puts
570
- puts " Created draft: #{draft_path}"
571
- puts
572
- else
573
- # Legacy mode - still support "new draft <title>"
574
- title = draft_args.strip
575
-
576
- print " Enter draft body: "
577
- body = gets&.chomp&.strip
578
- return if body.nil? || body.empty?
579
-
580
- print " Enter tags (optional, comma-separated): "
581
- tags_input = gets&.chomp&.strip
582
- tags = tags_input.empty? ? nil : tags_input.split(",").map(&:strip)
583
-
584
- print " Enter blurb (optional): "
585
- blurb = gets&.chomp&.strip
586
- blurb = nil if blurb.empty?
587
-
588
- # Create draft with all parameters
589
- draft_path = @api.create_draft(
590
- title: title,
591
- body: body,
592
- views: @api.current_view&.name,
593
- tags: tags,
594
- blurb: blurb
595
- )
596
- puts
597
- puts " Created draft: #{draft_path}"
598
- puts
599
- end
600
- else
642
+ rescue Exception => e
601
643
  puts
602
- puts " Usage: new draft [<title>]"
644
+ puts " View '#{view_name}' not found"
603
645
  puts
604
646
  end
605
647
  end
606
648
 
607
- def show_version
608
- puts
609
- puts " Scriptorium #{Scriptorium::VERSION}"
610
- puts
611
- end
612
-
613
- def get_started
614
- puts
615
- puts " No editor configured. Let's set one up."
616
- pick_editor
617
-
618
- puts
619
- puts " Setup complete!"
620
- puts " You can now use 'new post <title>' to create posts with your editor."
621
- puts
622
- end
623
-
624
- def pick_editor
625
- puts
626
- puts " Available editors:"
627
-
628
- # Check for common editors (prioritized for single file editing)
629
- editors = []
630
- %w[nano vim emacs vi micro].each do |editor|
631
- if which(editor)
632
- editors << editor
633
- end
634
- end
635
-
636
- # The original Unix line editor - for the brave souls who want ultimate speed
637
- if which("ed")
638
- editors << "ed"
649
+ private def create_view(args = nil)
650
+ # Handle array arguments from parse_cmd
651
+ if args.nil? || args.empty?
652
+ # No arguments provided - interactive mode
653
+ print " Enter view name: "
654
+ name = get_string
655
+ return if name.nil? || name.empty?
656
+
657
+ print " Enter view title: "
658
+ title = get_string
659
+ return if title.nil? || title.empty?
660
+
661
+ print " Enter subtitle (optional): "
662
+ subtitle = get_string
663
+ subtitle = "" if subtitle.nil? || subtitle.empty?
664
+ elsif args.length == 1
665
+ # One argument provided - use as name, prompt for title and subtitle
666
+ name = args[0]
667
+ print " Enter view title: "
668
+ title = get_string
669
+ return if title.nil? || title.empty?
670
+
671
+ print " Enter subtitle (optional): "
672
+ subtitle = get_string
673
+ subtitle = "" if subtitle.nil? || subtitle.empty?
674
+ elsif args.length >= 2
675
+ # Two or more arguments provided - use first two as name and title, prompt for subtitle
676
+ name = args[0]
677
+ title = args[1]
678
+ print " Enter subtitle (optional): "
679
+ subtitle = get_string
680
+ subtitle = "" if subtitle.nil? || subtitle.empty?
639
681
  end
640
682
 
641
-
642
- if editors.empty?
643
- puts " No common editors found. Please install nano, vim, emacs, vi, micro, or ed."
644
- puts " You can manually set your editor later by editing config/editor.txt"
683
+ # Check if view already exists
684
+ existing_views = @api.views
685
+ if existing_views.any? { |view| view.name == name }
686
+ puts
687
+ puts " View '#{name}' already exists"
645
688
  puts
646
689
  return
647
690
  end
648
691
 
649
- # Show available editors
650
- editors.each_with_index do |editor, index|
651
- puts " #{index + 1}. #{editor}"
652
- end
653
-
654
- # Let user pick
655
- print " Choose editor (1-#{editors.length}): "
656
- choice = get_string
657
-
658
- if choice && choice.match?(/^\d+$/) && choice.to_i.between?(1, editors.length)
659
- selected_editor = editors[choice.to_i - 1]
660
-
661
- # Save the choice
662
- make_dir(@api.root/"config")
663
- write_file(@api.root/"config/editor.txt", selected_editor)
664
-
692
+ # Create view with all parameters
693
+ begin
694
+ @api.create_view(name, title, subtitle, theme: "standard")
665
695
  puts
666
- puts " Selected editor: #{selected_editor}"
667
- puts " Editor preference saved to config/editor.txt"
668
- else
696
+ puts " Created view '#{name}' with title '#{title}'"
697
+ puts " Switched to view '#{name}'"
698
+ puts
699
+ rescue Exception => e
700
+ puts
701
+ puts " #{e.message}"
669
702
  puts
670
- puts " Invalid choice. Editor not changed."
671
703
  end
672
704
  end
673
705
 
706
+ def show_version
707
+ puts
708
+ puts " Scriptorium #{Scriptorium::VERSION}"
709
+ puts
710
+ end
711
+
674
712
  def list_views
675
713
  puts
676
714
  views = @api.views
@@ -709,21 +747,17 @@ class ScriptoriumTUI
709
747
  end
710
748
  end
711
749
 
712
- private def create_post(args)
713
- # Handle "new post <title>" format
714
- if args == "post" || args.start_with?("post ")
715
- # Remove "post " prefix if present, otherwise args is just "post"
716
- post_args = args == "post" ? "" : args[5..-1]
717
-
718
- if post_args.strip.empty?
719
- # Interactive mode - prompt for title
720
- print " Enter post title: "
721
- title = gets&.chomp&.strip
722
- return if title.nil? || title.empty?
723
- else
724
- # Use provided title
725
- title = post_args.strip
726
- end
750
+ def create_post(args)
751
+ # Handle array arguments from parse_cmd
752
+ if args.nil? || args.empty?
753
+ # No arguments provided - interactive mode
754
+ print " Enter post title: "
755
+ title = gets&.chomp&.strip
756
+ return if title.nil? || title.empty?
757
+ else
758
+ # Use first argument as title
759
+ title = args[0]
760
+ end
727
761
 
728
762
  # Check if editor is configured
729
763
  editor_file = @api.root/"config/editor.txt"
@@ -778,14 +812,9 @@ class ScriptoriumTUI
778
812
  puts " Error creating post: #{e.message}"
779
813
  puts
780
814
  end
781
- else
782
- puts
783
- puts " Usage: new post [<title>]"
784
- puts
785
- end
786
815
  end
787
816
 
788
- private def list_posts
817
+ def list_posts
789
818
  current_view = @api.current_view
790
819
  if current_view.nil?
791
820
  puts
@@ -794,7 +823,7 @@ class ScriptoriumTUI
794
823
  return
795
824
  end
796
825
 
797
- posts = @api.all_posts(current_view)
826
+ posts = @api.posts(current_view)
798
827
 
799
828
  puts
800
829
  if posts.empty?
@@ -808,7 +837,7 @@ class ScriptoriumTUI
808
837
  puts
809
838
  end
810
839
 
811
- private def list_drafts
840
+ def list_drafts
812
841
  drafts_dir = @api.root/:drafts
813
842
  return unless Dir.exist?(drafts_dir)
814
843
 
@@ -826,7 +855,15 @@ class ScriptoriumTUI
826
855
  puts
827
856
  end
828
857
 
829
- private def deploy_current_view
858
+ private def log_tty(str)
859
+ log_entry = "DEBUG: #{s}"
860
+ File.open('debug.log', 'a') { |f| f.puts log_entry }
861
+ tty = File.open('/dev/tty', 'w')
862
+ tty.puts log_entry
863
+ tty.close
864
+ end
865
+
866
+ def deploy_current_view
830
867
  current_view = @api.current_view
831
868
  if current_view.nil?
832
869
  puts
@@ -835,58 +872,17 @@ class ScriptoriumTUI
835
872
  return
836
873
  end
837
874
 
838
- # Check if deploy config exists
839
- deploy_config_file = current_view.dir/:config/"deploy.txt"
840
- unless File.exist?(deploy_config_file)
841
- puts
842
- puts " No deployment configuration found."
843
- puts " Create #{deploy_config_file} with format:"
844
- puts " user@server:path"
845
- puts
846
- return
847
- end
848
-
849
- # Read deployment configuration
850
- deploy_config = read_file(deploy_config_file).strip
851
- if deploy_config.empty?
852
- puts
853
- puts " Deployment configuration is empty."
854
- puts
855
- return
856
- end
857
-
858
- # Check if output directory exists
859
- output_dir = current_view.dir/:output
860
- unless Dir.exist?(output_dir)
861
- puts
862
- puts " Output directory does not exist: #{output_dir}"
863
- puts " Generate content first with 'new post' or similar."
864
- puts
865
- return
866
- end
867
-
868
- # Create deployment marker file
869
- marker_content = "Deployed: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
870
- marker_file = output_dir/"last-deployed.txt"
871
- write_file(marker_file, marker_content)
872
-
873
- # Execute rsync command
874
- puts
875
- puts " Deploying view '#{current_view.name}' to #{deploy_config}..."
876
-
877
- cmd = "rsync -r -z -l #{output_dir}/ #{deploy_config}/"
878
- puts " Executing: #{cmd}"
879
-
880
- result = system(cmd)
875
+ # Check deployment readiness first
876
+ unless @api.can_deploy?(current_view.name)
877
+ puts " Deployment error: View '#{current_view.name}' is not ready for deployment. Check status and configuration."
878
+ puts
879
+ return
880
+ end
881
881
 
882
+ # Use the API's deploy method
883
+ result = @api.deploy(current_view.name)
882
884
  if result
883
- puts " Deployment successful!"
884
-
885
- # Extract domain and verify deployment
886
- domain = extract_domain_from_deploy_config(deploy_config)
887
- if domain
888
- verify_deployment(domain)
889
- end
885
+ puts " Deployment completed!"
890
886
  else
891
887
  puts " Deployment failed!"
892
888
  end
@@ -1011,7 +1007,7 @@ class ScriptoriumTUI
1011
1007
  end
1012
1008
  end
1013
1009
 
1014
- private def list_widgets
1010
+ def list_widgets
1015
1011
  current_view = @api.current_view
1016
1012
  if current_view.nil?
1017
1013
  puts
@@ -1340,64 +1336,538 @@ class ScriptoriumTUI
1340
1336
  private def list_themes
1341
1337
  puts
1342
1338
  themes = @api.themes_available
1339
+
1343
1340
  if themes.empty?
1344
1341
  puts " No themes found"
1345
1342
  else
1346
- puts " Available themes:"
1347
- themes.each do |theme|
1348
- puts " #{theme}"
1343
+ # Group by type
1344
+ system_themes = themes.select { |t| t[:type] == 'system' }
1345
+ user_themes = themes.select { |t| t[:type] == 'user' }
1346
+ shared_themes = themes.select { |t| t[:type] == 'shared' }
1347
+
1348
+ puts " System Themes (Read-only):"
1349
+ if system_themes.empty?
1350
+ puts " none"
1351
+ else
1352
+ system_themes.each { |t| puts " #{t[:name]} (#{t[:path]})" }
1353
+ end
1354
+
1355
+ puts " User Themes (Editable):"
1356
+ if user_themes.empty?
1357
+ puts " none"
1358
+ else
1359
+ user_themes.each { |t| puts " #{t[:name]} (#{t[:path]})" }
1360
+ end
1361
+
1362
+ puts " Shared Themes (Community):"
1363
+ if shared_themes.empty?
1364
+ puts " none"
1365
+ else
1366
+ shared_themes.each { |t| puts " #{t[:name]} (#{t[:path]})" }
1349
1367
  end
1350
1368
  end
1351
1369
  puts
1352
1370
  end
1353
1371
 
1354
1372
  private def clone_theme(args)
1355
- parts = args.split(/\s+/)
1373
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1356
1374
  if parts.length != 2
1357
1375
  puts
1358
- puts " Usage: clone <oldtheme> <newtheme>"
1359
- puts " Example: clone standard mytheme"
1376
+ puts " Usage: clone theme <source> <newname>"
1377
+ puts " Example: clone theme standard my-custom"
1378
+ puts " Note: Cloned themes become user themes"
1360
1379
  puts
1361
1380
  return
1362
1381
  end
1363
1382
 
1364
- old_theme, new_theme = parts[0], parts[1]
1383
+ source, newname = parts[0], parts[1]
1365
1384
 
1366
1385
  begin
1367
- # Check if old theme exists
1368
- old_theme_path = @api.root/:themes/old_theme
1369
- unless Dir.exist?(old_theme_path)
1386
+ # Use the new API method for cloning
1387
+ result = @api.clone_theme(source, newname)
1388
+
1389
+ if result
1370
1390
  puts
1371
- puts " Theme '#{old_theme}' not found"
1391
+ puts " Theme cloned successfully: #{source} #{result}"
1392
+ puts " Use 'config theme #{result}' to customize your theme"
1393
+ puts
1394
+ else
1395
+ puts
1396
+ puts " ❌ Failed to clone theme"
1397
+ puts
1398
+ end
1399
+ rescue => e
1400
+ puts
1401
+ puts " ❌ Failed to clone theme: #{e.message}"
1402
+ puts
1403
+ puts
1404
+ end
1405
+ end
1406
+
1407
+ private def delete_theme(args)
1408
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1409
+ if parts.length != 2
1410
+ puts
1411
+ puts " Usage: delete theme <theme_path>"
1412
+ puts " Example: delete theme user/my-custom"
1413
+ puts " Note: Can only delete user themes"
1414
+ puts
1415
+ return
1416
+ end
1417
+
1418
+ theme_path = parts[1]
1419
+
1420
+ begin
1421
+ # Use the new API method for deleting
1422
+ result = @api.delete_theme(theme_path)
1423
+
1424
+ if result
1425
+ puts
1426
+ puts " ✅ Theme deleted successfully: #{theme_path}"
1427
+ puts
1428
+ else
1429
+ puts
1430
+ puts " ❌ Failed to delete theme"
1431
+ puts
1432
+ end
1433
+ rescue => e
1434
+ puts
1435
+ puts " ❌ Failed to delete theme: #{e.message}"
1436
+ puts
1437
+ end
1438
+ end
1439
+
1440
+ # Asset Management Commands
1441
+
1442
+ def list_assets(args)
1443
+ target = args[0] || 'global'
1444
+
1445
+ unless %w[global library view gem].include?(target)
1446
+ puts
1447
+ puts " Usage: list assets [target]"
1448
+ puts " Targets: global, library, view, gem"
1449
+ puts " Example: list assets view"
1450
+ puts
1451
+ return
1452
+ end
1453
+
1454
+ begin
1455
+ assets = @api.list_assets(target: target, view: @api.current_view&.name)
1456
+ if assets.empty?
1457
+ puts
1458
+ puts " No assets found in #{target}"
1459
+ puts
1460
+ else
1461
+ puts
1462
+ puts " Assets in #{target}:"
1463
+ assets.each do |asset|
1464
+ puts " #{asset[:filename]} (#{asset[:size]} bytes, #{asset[:type]})"
1465
+ puts " Path: #{asset[:path]}"
1466
+ puts " Dimensions: #{asset[:dimensions] || 'N/A'}"
1467
+ end
1372
1468
  puts
1373
- return
1374
1469
  end
1470
+ rescue => e
1471
+ puts
1472
+ puts " ❌ Failed to list assets: #{e.message}"
1473
+ puts
1474
+ end
1475
+ end
1476
+
1477
+ private def upload_asset(args)
1478
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1479
+ if parts.length < 2
1480
+ puts
1481
+ puts " Usage: upload asset <filepath> [target]"
1482
+ puts " Targets: global, library, view"
1483
+ puts " Example: upload asset /path/to/image.jpg global"
1484
+ puts
1485
+ return
1486
+ end
1487
+
1488
+ filepath = parts[1]
1489
+ target = parts[2] || 'global'
1490
+
1491
+ unless %w[global library view].include?(target)
1492
+ puts
1493
+ puts " Invalid target: #{target}"
1494
+ puts " Valid targets: global, library, view"
1495
+ puts
1496
+ return
1497
+ end
1498
+
1499
+ unless File.exist?(filepath)
1500
+ puts
1501
+ puts " ❌ File not found: #{filepath}"
1502
+ puts
1503
+ return
1504
+ end
1505
+
1506
+ begin
1507
+ result = @api.upload_asset(filepath, target: target, view: @api.current_view&.name)
1508
+ puts
1509
+ puts " ✅ Asset uploaded successfully to #{target}"
1510
+ puts " Filename: #{result[:filename]}"
1511
+ puts " Size: #{result[:size]} bytes"
1512
+ puts
1513
+ rescue => e
1514
+ puts
1515
+ puts " ❌ Failed to upload asset: #{e.message}"
1516
+ puts
1517
+ end
1518
+ end
1519
+
1520
+ private def copy_asset(args)
1521
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1522
+ if parts.length < 3
1523
+ puts
1524
+ puts " Usage: copy asset <filename> <from> <to>"
1525
+ puts " From/To: global, library, view, gem"
1526
+ puts " Example: copy asset logo.png gem global"
1527
+ puts
1528
+ return
1529
+ end
1530
+
1531
+ filename = parts[1]
1532
+ from = parts[2]
1533
+ to = parts[3]
1534
+
1535
+ unless %w[global library view gem].include?(from) && %w[global library view].include?(to)
1536
+ puts
1537
+ puts " Invalid source or target"
1538
+ puts " Valid sources: global, library, view, gem"
1539
+ puts " Valid targets: global, library, view"
1540
+ puts
1541
+ return
1542
+ end
1543
+
1544
+ begin
1545
+ result = @api.copy_asset(filename, from: from, to: to, view: @api.current_view&.name)
1546
+ puts
1547
+ puts " ✅ Asset copied successfully"
1548
+ puts " From: #{from} (#{result[:from_path]})"
1549
+ puts " To: #{to} (#{result[:to_path]})"
1550
+ puts
1551
+ rescue => e
1552
+ puts
1553
+ puts " ❌ Failed to copy asset: #{e.message}"
1554
+ puts
1555
+ end
1556
+ end
1557
+
1558
+ private def delete_asset(args)
1559
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1560
+ if parts.length < 2
1561
+ puts
1562
+ puts " Usage: delete asset <filename> [target]"
1563
+ puts " Targets: global, library, view"
1564
+ puts
1565
+ return
1566
+ end
1567
+
1568
+ filename = parts[1]
1569
+ target = parts[2] || 'global'
1570
+
1571
+ unless %w[global library view].include?(target)
1572
+ puts
1573
+ puts " Invalid target: #{target}"
1574
+ puts " Valid targets: global, library, view"
1575
+ puts
1576
+ return
1577
+ end
1375
1578
 
1376
- # Check if new theme already exists
1377
- new_theme_path = @api.root/:themes/new_theme
1378
- if Dir.exist?(new_theme_path)
1579
+ begin
1580
+ @api.delete_asset(filename, target: target, view: @api.current_view&.name)
1581
+ puts
1582
+ puts " ✅ Asset deleted successfully from #{target}"
1583
+ puts
1584
+ rescue => e
1585
+ puts
1586
+ puts " ❌ Failed to delete asset: #{e.message}"
1587
+ puts
1588
+ end
1589
+ end
1590
+
1591
+ private def asset_info(args)
1592
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1593
+ if parts.length < 1
1594
+ puts
1595
+ puts " Usage: asset info <filename> [target]"
1596
+ puts " Targets: global, library, view, gem"
1597
+ puts " Example: asset info logo.png global"
1598
+ puts
1599
+ return
1600
+ end
1601
+
1602
+ filename = parts[0]
1603
+ target = parts[1] || 'global'
1604
+
1605
+ unless %w[global library view gem].include?(target)
1606
+ puts
1607
+ puts " Invalid target: #{target}"
1608
+ puts " Valid targets: global, library, view, gem"
1609
+ puts
1610
+ return
1611
+ end
1612
+
1613
+ begin
1614
+ info = @api.get_asset_info(filename, target: target, view: @api.current_view&.name)
1615
+ if info.nil?
1379
1616
  puts
1380
- puts " Theme '#{new_theme}' already exists"
1617
+ puts " Asset not found: #{filename}"
1381
1618
  puts
1382
1619
  return
1383
1620
  end
1621
+
1622
+ puts
1623
+ puts " Asset Information:"
1624
+ puts " Filename: #{info[:filename]}"
1625
+ puts " Size: #{info[:size]} bytes"
1626
+ puts " Type: #{info[:type]}"
1627
+ puts " Path: #{info[:path]}"
1628
+ puts " Dimensions: #{info[:dimensions] || 'N/A'}"
1629
+ puts
1630
+ rescue => e
1631
+ puts
1632
+ puts " ❌ Failed to get asset info: #{e.message}"
1633
+ puts
1634
+ end
1635
+ end
1636
+
1637
+ private def configure_deployment(args)
1638
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1639
+ view_name = parts[1] || @api.current_view&.name
1640
+
1641
+ unless view_name
1642
+ puts
1643
+ puts " ❌ No view specified and no current view"
1644
+ puts
1645
+ return
1646
+ end
1384
1647
 
1385
- # Clone the theme
1386
- require 'fileutils'
1387
- FileUtils.cp_r(old_theme_path, new_theme_path)
1648
+ begin
1649
+ deploy_file = @api.root/"views"/view_name/"config"/"deploy.txt"
1650
+ puts " DEBUG: About to edit file: #{deploy_file}"
1651
+ puts " DEBUG: File exists: #{File.exist?(deploy_file)}"
1652
+
1653
+ # Use TUI's editor configuration instead of API's edit_file
1654
+ editor_file = @api.root/"config/editor.txt"
1655
+ if File.exist?(editor_file)
1656
+ editor = read_file(editor_file).strip
1657
+ puts " DEBUG: Using TUI editor: #{editor}"
1658
+ system("#{editor} #{deploy_file}")
1659
+ else
1660
+ puts " DEBUG: No TUI editor config, using API edit_file"
1661
+ @api.edit_file(deploy_file)
1662
+ end
1388
1663
 
1664
+ puts " DEBUG: edit_file returned"
1389
1665
  puts
1390
- puts " ✅ Theme '#{old_theme}' cloned to '#{new_theme}'"
1391
- puts " Edit #{new_theme_path} to customize your theme"
1666
+ puts " ✅ Deployment configuration edited for view: #{view_name}"
1392
1667
  puts
1393
1668
  rescue => e
1669
+ puts " DEBUG: Exception in configure_deployment: #{e.class} - #{e.message}"
1394
1670
  puts
1395
- puts " ❌ Failed to clone theme: #{e.message}"
1671
+ puts " ❌ Failed to edit deployment configuration: #{e.message}"
1396
1672
  puts
1397
1673
  end
1398
1674
  end
1399
1675
 
1400
- end
1676
+ end
1677
+
1678
+ def list_backups
1679
+ puts
1680
+ begin
1681
+ backups = @api.list_backups
1682
+ if backups.empty?
1683
+ puts " No backups available"
1684
+ else
1685
+ puts " Available backups:"
1686
+ backups.each do |backup|
1687
+ timestamp = backup[:timestamp]
1688
+ type = backup[:type]
1689
+ description = backup[:description]
1690
+ desc_text = description ? " - #{description}" : ""
1691
+ puts " #{timestamp} (#{type})#{desc_text}"
1692
+ end
1693
+ end
1694
+ rescue => e
1695
+ puts " ❌ Failed to list backups: #{e.message}"
1696
+ end
1697
+ puts
1698
+ end
1699
+
1700
+ def create_backup(args)
1701
+ puts
1702
+ begin
1703
+ # Parse arguments
1704
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1705
+ type = parts[0]&.downcase
1706
+ description = parts[1..-1]&.join(" ")
1707
+
1708
+ # Prompt for missing type
1709
+ unless %w[full incr].include?(type)
1710
+ print " Backup type (full/incr): "
1711
+ type = gets.chomp.downcase
1712
+ unless %w[full incr].include?(type)
1713
+ puts " ❌ Invalid backup type. Must be 'full' or 'incr'"
1714
+ puts
1715
+ return
1716
+ end
1717
+ end
1718
+
1719
+ # Prompt for description if not provided
1720
+ if description.nil? || description.empty?
1721
+ print " Description (optional): "
1722
+ description = gets.chomp
1723
+ description = nil if description.empty?
1724
+ end
1725
+
1726
+ # Create backup
1727
+ backup_type = type.to_sym
1728
+ timestamp = @api.create_backup(type: backup_type, label: description)
1729
+
1730
+ puts " ✅ Backup created: #{timestamp}"
1731
+ if description
1732
+ puts " Description: #{description}"
1733
+ end
1734
+ rescue => e
1735
+ puts " ❌ Failed to create backup: #{e.message}"
1736
+ end
1737
+ puts
1738
+ end
1739
+
1740
+ def restore_backup(args)
1741
+ puts
1742
+ begin
1743
+ # Parse arguments
1744
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1745
+ timestamp = parts[0]
1746
+ strategy = parts[1]&.downcase
1747
+
1748
+ # Prompt for missing timestamp
1749
+ if timestamp.nil? || timestamp.empty?
1750
+ backups = @api.list_backups
1751
+ if backups.empty?
1752
+ puts " No backups available to restore"
1753
+ puts
1754
+ return
1755
+ end
1756
+
1757
+ puts " Available backups:"
1758
+ backups.each_with_index do |backup, index|
1759
+ desc_text = backup[:description] ? " - #{backup[:description]}" : ""
1760
+ puts " #{index + 1}. #{backup[:timestamp]} (#{backup[:type]})#{desc_text}"
1761
+ end
1762
+
1763
+ print " Select backup (number or timestamp): "
1764
+ selection = gets.chomp
1765
+
1766
+ # Check if it's a number
1767
+ if selection.match?(/^\d+$/)
1768
+ index = selection.to_i - 1
1769
+ if index >= 0 && index < backups.length
1770
+ timestamp = backups[index][:timestamp]
1771
+ else
1772
+ puts " ❌ Invalid selection"
1773
+ puts
1774
+ return
1775
+ end
1776
+ else
1777
+ timestamp = selection
1778
+ end
1779
+ end
1780
+
1781
+ # Prompt for strategy if not provided
1782
+ unless %w[safe merge destroy].include?(strategy)
1783
+ print " Strategy (Enter=safe, or type: merge, destroy): "
1784
+ strategy_input = gets.chomp.downcase
1785
+ strategy = strategy_input.empty? ? "safe" : strategy_input
1786
+
1787
+ unless %w[safe merge destroy].include?(strategy)
1788
+ puts " ❌ Invalid strategy. Must be 'safe', 'merge', or 'destroy'"
1789
+ puts
1790
+ return
1791
+ end
1792
+ end
1793
+
1794
+ # Confirm restore
1795
+ puts " About to restore backup: #{timestamp}"
1796
+ puts " Strategy: #{strategy}"
1797
+ unless yesno("Continue?")
1798
+ puts " Restore cancelled"
1799
+ puts
1800
+ return
1801
+ end
1802
+
1803
+ # Restore backup
1804
+ strategy_sym = strategy.to_sym
1805
+ @api.restore_backup(timestamp, strategy: strategy_sym)
1806
+
1807
+ puts " ✅ Backup restored successfully"
1808
+ rescue => e
1809
+ puts " ❌ Failed to restore backup: #{e.message}"
1810
+ end
1811
+ puts
1812
+ end
1813
+
1814
+ def delete_backup(args)
1815
+ puts
1816
+ begin
1817
+ # Parse arguments
1818
+ parts = args.is_a?(String) ? args.split(/\s+/) : args
1819
+ timestamp = parts[0]
1820
+
1821
+ # Prompt for missing timestamp
1822
+ if timestamp.nil? || timestamp.empty?
1823
+ backups = @api.list_backups
1824
+ if backups.empty?
1825
+ puts " No backups available to delete"
1826
+ puts
1827
+ return
1828
+ end
1829
+
1830
+ puts " Available backups:"
1831
+ backups.each_with_index do |backup, index|
1832
+ desc_text = backup[:description] ? " - #{backup[:description]}" : ""
1833
+ puts " #{index + 1}. #{backup[:timestamp]} (#{backup[:type]})#{desc_text}"
1834
+ end
1835
+
1836
+ print " Select backup to delete (number or timestamp): "
1837
+ selection = gets.chomp
1838
+
1839
+ # Check if it's a number
1840
+ if selection.match?(/^\d+$/)
1841
+ index = selection.to_i - 1
1842
+ if index >= 0 && index < backups.length
1843
+ timestamp = backups[index][:timestamp]
1844
+ else
1845
+ puts " ❌ Invalid selection"
1846
+ puts
1847
+ return
1848
+ end
1849
+ else
1850
+ timestamp = selection
1851
+ end
1852
+ end
1853
+
1854
+ # Confirm deletion
1855
+ puts " About to delete backup: #{timestamp}"
1856
+ unless yesno("Are you sure?")
1857
+ puts " Deletion cancelled"
1858
+ puts
1859
+ return
1860
+ end
1861
+
1862
+ # Delete backup
1863
+ @api.delete_backup(timestamp)
1864
+
1865
+ puts " ✅ Backup deleted successfully"
1866
+ rescue => e
1867
+ puts " ❌ Failed to delete backup: #{e.message}"
1868
+ end
1869
+ puts
1870
+ end
1401
1871
 
1402
1872
  ###### Main ######
1403
1873