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.
- checksums.yaml +4 -4
- data/assets/icons/social/reddit.png +0 -0
- data/assets/icons/social/x-logo.png +0 -0
- data/assets/imagenotfound.jpg +0 -0
- data/bin/sblog +84 -5
- data/bin/scriptorium +1 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
- data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
- data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
- data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
- data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
- data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
- data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
- data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
- data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
- data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
- data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
- data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
- data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
- data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
- data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
- data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
- data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
- data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
- data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
- data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
- data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
- data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
- data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
- data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
- data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
- data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
- data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
- data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
- data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
- data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
- data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
- data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
- data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
- data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
- data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
- data/doc/imported/0018-ruby-puns/post.html +46 -0
- data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
- data/doc/imported/0020-fffff/metadata.txt +7 -0
- data/doc/imported/0020-fffff/post.html +24 -0
- data/doc/imported/0020-fffff/source.lt3 +12 -0
- data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
- data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
- data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
- data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
- data/doc/imported/0023-doctor-sleep/post.html +63 -0
- data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
- data/doc/imported/0024-just-a-test/metadata.txt +7 -0
- data/doc/imported/0024-just-a-test/post.html +24 -0
- data/doc/imported/0024-just-a-test/source.lt3 +12 -0
- data/doc/imported/import_summary.txt +98 -0
- data/doc/livetext-informal-spec.txt +65 -0
- data/doc/myuserdoc/ch-0.lt3 +31 -0
- data/doc/myuserdoc/ch-1.lt3 +37 -0
- data/doc/myuserdoc/ch-10.lt3 +22 -0
- data/doc/myuserdoc/ch-2.lt3 +37 -0
- data/doc/myuserdoc/ch-3.lt3 +19 -0
- data/doc/myuserdoc/ch-4.lt3 +43 -0
- data/doc/myuserdoc/ch-5.lt3 +22 -0
- data/doc/myuserdoc/ch-6.lt3 +19 -0
- data/doc/myuserdoc/ch-7.lt3 +16 -0
- data/doc/myuserdoc/ch-8.lt3 +13 -0
- data/doc/myuserdoc/ch-9.lt3 +19 -0
- data/doc/myuserdoc/tweak.rb +18 -0
- data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
- data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
- data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
- data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
- data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
- data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
- data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
- data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
- data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
- data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
- data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
- data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
- data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
- data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
- data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
- data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
- data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
- data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
- data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
- data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
- data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
- data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
- data/doc/old-posts/0024-just-a-test.lt3 +12 -0
- data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
- data/doc/old-repo +1 -0
- data/doc/reddit_integration.md +2 -2
- data/doc/user.lt3 +0 -3
- data/lib/scriptorium/api.rb +1811 -78
- data/lib/scriptorium/banner_svg.rb +55 -68
- data/lib/scriptorium/contract.rb +3 -2
- data/lib/scriptorium/exceptions.rb +133 -102
- data/lib/scriptorium/helpers.rb +282 -82
- data/lib/scriptorium/post.rb +81 -17
- data/lib/scriptorium/reddit.rb +1 -1
- data/lib/scriptorium/repo.rb +478 -164
- data/lib/scriptorium/standard_files.rb +30 -396
- data/lib/scriptorium/support/common_js/clipboard.js +35 -0
- data/lib/scriptorium/support/common_js/content-loader.js +187 -0
- data/lib/scriptorium/support/common_js/navigation.js +52 -0
- data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
- data/lib/scriptorium/support/config/reddit_template.txt +17 -0
- data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
- data/lib/scriptorium/support/highlight/css.txt +2 -0
- data/lib/scriptorium/support/highlight/custom.css +119 -0
- data/lib/scriptorium/support/highlight/js.txt +1 -0
- data/lib/scriptorium/support/post_index/config.txt +15 -0
- data/lib/scriptorium/support/post_index/style.css +55 -0
- data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
- data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
- data/lib/scriptorium/support/templates/post.lt3 +104 -0
- data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
- data/lib/scriptorium/theme.rb +83 -70
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +194 -149
- data/lib/scriptorium.rb +24 -1
- data/lib/skeleton.rb +4 -1
- data/scriptorium.gemspec +2 -1
- data/test/WEB_INTEGRATION_README.md +196 -0
- data/test/all +40 -0
- data/test/banner_svg/unit.rb +267 -35
- data/test/config/deployment.txt +5 -0
- data/test/integration/integration_test.rb +7 -7
- data/test/integration/preview_flow_test.rb +94 -0
- data/test/livetext_plugin_test.rb +453 -182
- data/test/manual/banner-tests/test01.html +82 -18
- data/test/manual/banner-tests/test02.html +82 -18
- data/test/manual/banner-tests/test03.html +82 -18
- data/test/manual/banner-tests/test04.html +89 -25
- data/test/manual/banner-tests/test05.html +89 -25
- data/test/manual/banner-tests/test06.html +89 -25
- data/test/manual/banner-tests/test07.html +89 -25
- data/test/manual/banner-tests/test08.html +82 -18
- data/test/manual/banner-tests/test09.html +82 -18
- data/test/manual/banner-tests/test10.html +82 -18
- data/test/manual/banner-tests/test11.html +82 -18
- data/test/manual/banner-tests/test12.html +82 -18
- data/test/manual/banner-tests/test13.html +82 -18
- data/test/manual/banner-tests/test14.html +82 -18
- data/test/manual/banner-tests/test15.html +82 -18
- data/test/manual/banner-tests/test16.html +82 -18
- data/test/manual/banner-tests/test17.html +82 -18
- data/test/manual/banner-tests/test18.html +90 -26
- data/test/manual/banner-tests/test19.html +90 -26
- data/test/manual/banner-tests/test20.html +90 -26
- data/test/manual/banner-tests/test21.html +90 -26
- data/test/manual/banner-tests/test22.html +90 -26
- data/test/manual/banner-tests/test23.html +90 -26
- data/test/manual/banner-tests/test24.html +90 -26
- data/test/manual/banner-tests/test25.html +89 -25
- data/test/manual/banner_environment.rb +15 -2
- data/test/manual/codemirror_demo.html +773 -0
- data/test/manual/create_posts_for_web.rb +114 -0
- data/test/manual/preview_manual_test.rb +129 -0
- data/test/manual/test_banner_features.rb +14 -14
- data/test/manual/test_banner_integration.rb +115 -0
- data/test/manual/test_banner_radial.rb +87 -0
- data/test/manual/test_syntax_highlighting.rb +60 -40
- data/test/support/preview_utils.rb +88 -0
- data/test/test_gem_assets.rb +48 -0
- data/test/test_helpers.rb +10 -0
- data/test/tui_editor_integration_test.rb +15 -15
- data/test/tui_integration_test.rb +687 -441
- data/test/unit/api.rb +757 -37
- data/test/unit/asset_management.rb +195 -221
- data/test/unit/backup_test.rb +451 -0
- data/test/unit/contract_test.rb +1 -23
- data/test/unit/core.rb +415 -61
- data/test/unit/deploy_config_test.rb +248 -0
- data/test/unit/deploy_test.rb +312 -21
- data/test/unit/edit_post_test.rb +168 -0
- data/test/unit/gem_asset_management.rb +36 -42
- data/test/unit/livetext_basic.rb +23 -35
- data/test/unit/livetext_compatibility.rb +7 -14
- data/test/unit/parse_cmd_test.rb +260 -0
- data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
- data/test/unit/post.rb +91 -26
- data/test/unit/post_index_config_test.rb +258 -0
- data/test/unit/post_state_helpers_test.rb +137 -0
- data/test/unit/read_commented_file_test.rb +8 -6
- data/test/unit/repo.rb +75 -54
- data/test/unit/social_test.rb +41 -44
- data/test/unit/syntax_highlighting.rb +70 -0
- data/test/unit/theme_management_test.rb +91 -0
- data/test/unit/view.rb +79 -12
- data/test/unit/widgets.rb +8 -8
- data/test/web_integration_test.rb +231 -0
- data/test/web_test_helper.rb +218 -0
- data/test/web_workflow_test.rb +527 -0
- data/ui/tui/bin/scriptorium +885 -415
- data/ui/web/app/app.rb +1398 -176
- data/ui/web/app/assets/livetext_mode.js +244 -0
- data/ui/web/app/error_helpers.rb +16 -16
- data/ui/web/app/views/advanced_config.erb +8 -2
- data/ui/web/app/views/asset_management.erb +56 -0
- data/ui/web/app/views/backup_management.erb +238 -0
- data/ui/web/app/views/config_widget.erb +232 -0
- data/ui/web/app/views/dashboard.erb +64 -72
- data/ui/web/app/views/deploy_config.erb +3 -0
- data/ui/web/app/views/edit_pages.erb +170 -2
- data/ui/web/app/views/edit_post.erb +130 -9
- data/ui/web/app/views/edit_theme.erb +73 -0
- data/ui/web/app/views/edit_theme_file.erb +74 -0
- data/ui/web/app/views/theme_management.erb +130 -0
- data/ui/web/app/views/view_dashboard.erb +666 -25
- data/ui/web/app/views/widgets.erb +249 -0
- data/ui/web/bin/scriptorium-web +35 -24
- data/ui/web/tmp/timing.log +17 -0
- data/ui/web/tmp/web_server.log +0 -5
- metadata +190 -116
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +0 -1
- data/assets/icons/github.svg +0 -1
- data/assets/icons/instagram.svg +0 -1
- data/assets/icons/reddit.svg +0 -1
- data/assets/icons/x.svg +0 -1
- data/assets/icons/youtube.svg +0 -1
- data/bin/scriptorium +0 -1511
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
- data/lib/scriptorium/syntax_highlighter.rb +0 -234
- data/test/manual/deploy_symlink_demo.rb +0 -142
- data/test/manual/symlink_demo.rb +0 -117
- data/test/manual/test2.rb +0 -12
- data/test/manual/test_banner_from_file.rb +0 -150
- data/test/manual/test_banner_in_header.rb +0 -35
- data/test/manual/test_code_highlighting.rb +0 -68
- data/test/manual/test_complex_header.rb +0 -74
- data/test/manual/test_empty_header.rb +0 -32
- data/test/manual/test_radial_custom.rb +0 -58
- data/test/manual/test_radial_large_radius.rb +0 -52
- data/test/manual/test_svg_debug.rb +0 -47
- data/test/pages-demo/config/currentview.txt +0 -1
- data/test/pages-demo/views/demo/config/common.js +0 -57
- data/test/pages-demo/views/demo/config/footer.txt +0 -1
- data/test/pages-demo/views/demo/config/global-head.txt +0 -8
- data/test/pages-demo/views/demo/config/header.txt +0 -1
- data/test/pages-demo/views/demo/config/layout.txt +0 -1
- data/test/pages-demo/views/demo/config/left.txt +0 -1
- data/test/pages-demo/views/demo/config/main.txt +0 -1
- data/test/pages-demo/views/demo/config/right.txt +0 -1
- data/test/pages-demo/views/demo/config.txt +0 -3
- data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
- data/test/pages-demo/views/demo/output/panes/header.html +0 -1
- data/test/pages-demo/views/demo/output/panes/left.html +0 -1
- data/test/pages-demo/views/demo/output/panes/main.html +0 -1
- data/test/pages-demo/views/demo/output/panes/right.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
- data/ui/web/tmp/web_server.pid +0 -1
- /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
- /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
- /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
- /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
- /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
- /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
- /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
- /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
data/ui/tui/bin/scriptorium
CHANGED
@@ -9,8 +9,14 @@ class ScriptoriumTUI
|
|
9
9
|
include Scriptorium::Helpers
|
10
10
|
|
11
11
|
def initialize
|
12
|
-
|
13
|
-
@testing =
|
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
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
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
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
#
|
311
|
-
|
312
|
-
|
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
|
-
|
350
|
-
|
351
|
-
|
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 <
|
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
|
-
|
438
|
-
|
439
|
-
|
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 "
|
640
|
+
puts " Switched to view '#{view_name}'"
|
533
641
|
puts
|
534
|
-
|
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 "
|
644
|
+
puts " View '#{view_name}' not found"
|
603
645
|
puts
|
604
646
|
end
|
605
647
|
end
|
606
648
|
|
607
|
-
def
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
if
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
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
|
-
|
643
|
-
|
644
|
-
puts
|
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
|
-
#
|
650
|
-
|
651
|
-
|
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 "
|
667
|
-
puts "
|
668
|
-
|
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
|
-
|
713
|
-
# Handle
|
714
|
-
if args
|
715
|
-
#
|
716
|
-
|
717
|
-
|
718
|
-
if
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
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
|
839
|
-
|
840
|
-
|
841
|
-
puts
|
842
|
-
|
843
|
-
|
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
|
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
|
-
|
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
|
-
|
1347
|
-
themes.
|
1348
|
-
|
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 <
|
1359
|
-
puts " Example: clone standard
|
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
|
-
|
1383
|
+
source, newname = parts[0], parts[1]
|
1365
1384
|
|
1366
1385
|
begin
|
1367
|
-
#
|
1368
|
-
|
1369
|
-
|
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
|
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
|
-
|
1377
|
-
|
1378
|
-
|
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 "
|
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
|
-
|
1386
|
-
|
1387
|
-
|
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 " ✅
|
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
|
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
|
|