scriptorium 0.0.3 → 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/README.lt3 +324 -0
- data/README.md +3155 -1
- data/assets/.DS_Store +0 -0
- data/assets/README.md +44 -0
- data/assets/icons/social/reddit.png +0 -0
- data/assets/icons/social/x-logo.png +0 -0
- data/assets/icons/ui/.DS_Store +0 -0
- data/assets/icons/ui/back.png +0 -0
- data/assets/icons/ui/copy.png +0 -0
- data/assets/icons/ui/down.png +0 -0
- data/assets/icons/ui/end.png +0 -0
- data/assets/icons/ui/exit.png +0 -0
- data/assets/icons/ui/foo +10 -0
- data/assets/icons/ui/home.png +0 -0
- data/assets/icons/ui/left.png +0 -0
- data/assets/icons/ui/next.png +0 -0
- data/assets/icons/ui/right.png +0 -0
- data/assets/icons/ui/start.png +0 -0
- data/assets/icons/ui/up.png +0 -0
- data/assets/imagenotfound.jpg +0 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/sblog +84 -5
- data/bin/scriptorium +1 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +94 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +2 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +45 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +35 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +27 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- 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/banner_svg_config.md +114 -0
- data/doc/contrib.lt3 +8 -0
- data/doc/dependencies.md +281 -0
- data/doc/hacker.lt3 +5 -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/myuserdoc/userdoc-toc.txt +88 -0
- 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_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +35 -0
- data/doc/user_guide_section_1.md +137 -0
- data/doc/user_guide_section_10.md +515 -0
- data/doc/user_guide_section_11.md +708 -0
- data/doc/user_guide_section_2.md +233 -0
- data/doc/user_guide_section_3.md +5 -0
- data/doc/user_guide_section_4.md +221 -0
- data/doc/user_guide_section_5.md +243 -0
- data/doc/user_guide_section_6.md +147 -0
- data/doc/user_guide_section_7.md +311 -0
- data/doc/user_guide_section_8.md +224 -0
- data/doc/user_guide_section_9.md +375 -0
- data/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +2373 -0
- data/lib/scriptorium/banner_svg.rb +729 -0
- data/lib/scriptorium/contract.rb +34 -0
- data/lib/scriptorium/exceptions.rb +201 -1
- data/lib/scriptorium/helpers.rb +675 -0
- data/lib/scriptorium/post.rb +259 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +938 -0
- data/lib/scriptorium/standard_files.rb +149 -0
- data/lib/scriptorium/support/bootstrap/css.txt +5 -0
- data/lib/scriptorium/support/bootstrap/js.txt +4 -0
- 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.txt +10 -0
- data/lib/scriptorium/support/config/reddit_template.txt +17 -0
- data/lib/scriptorium/support/config/social.txt +8 -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/lib/scriptorium/support/templates/initial_post.lt3 +12 -0
- data/lib/scriptorium/support/templates/layout.txt +5 -0
- data/lib/scriptorium/support/templates/post.lt3 +104 -0
- data/lib/scriptorium/support/theme/footer.lt3 +2 -0
- data/lib/scriptorium/support/theme/header.lt3 +4 -0
- data/lib/scriptorium/support/theme/left.lt3 +3 -0
- data/lib/scriptorium/support/theme/main.lt3 +5 -0
- data/lib/scriptorium/support/theme/right.lt3 +3 -0
- data/lib/scriptorium/theme.rb +192 -0
- data/lib/scriptorium/version.rb +1 -1
- data/lib/scriptorium/view.rb +1021 -0
- data/lib/scriptorium/widgets/featured_posts.rb +149 -0
- data/lib/scriptorium/widgets/links.rb +112 -0
- data/lib/scriptorium/widgets/pages.rb +133 -0
- data/lib/scriptorium/widgets/widget.rb +133 -0
- data/lib/scriptorium.rb +38 -34
- data/lib/skeleton.rb +10 -1
- data/scriptorium.gemspec +17 -5
- data/test/README.md +69 -0
- data/test/WEB_INTEGRATION_README.md +196 -0
- data/test/all +83 -0
- data/test/api_demo.rb +99 -0
- data/test/assets/imagenotfound.jpg +0 -0
- data/test/assets/images/.DS_Store +0 -0
- data/test/assets/images/README.md +27 -0
- data/test/assets/images/odd_aspect.png +0 -0
- data/test/assets/images/perfect.png +0 -0
- data/test/assets/images/small.png +0 -0
- data/test/assets/images/tall.png +0 -0
- data/test/assets/images/very_tall.png +0 -0
- data/test/assets/images/very_wide.png +0 -0
- data/test/assets/images/wide.png +0 -0
- data/test/assets/testbanner.jpg +0 -0
- data/test/banner_svg/simple_helpers.rb +13 -0
- data/test/banner_svg/unit.rb +1000 -0
- data/test/config/deployment.txt +5 -0
- data/test/ed_test.rb +204 -0
- data/test/integration/cursor_banner_combinations.rb +193 -0
- data/test/integration/cursor_banner_features.rb +374 -0
- data/test/integration/integration_test.rb +326 -0
- data/test/integration/preview_flow_test.rb +94 -0
- data/test/livetext_plugin_test.rb +500 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/svg.txt +3 -0
- data/test/manual/banner-tests/test01.html +122 -0
- data/test/manual/banner-tests/test02.html +122 -0
- data/test/manual/banner-tests/test03.html +122 -0
- data/test/manual/banner-tests/test04.html +129 -0
- data/test/manual/banner-tests/test05.html +129 -0
- data/test/manual/banner-tests/test06.html +129 -0
- data/test/manual/banner-tests/test07.html +129 -0
- data/test/manual/banner-tests/test08.html +123 -0
- data/test/manual/banner-tests/test09.html +123 -0
- data/test/manual/banner-tests/test10.html +123 -0
- data/test/manual/banner-tests/test11.html +123 -0
- data/test/manual/banner-tests/test12.html +123 -0
- data/test/manual/banner-tests/test13.html +123 -0
- data/test/manual/banner-tests/test14.html +123 -0
- data/test/manual/banner-tests/test15.html +122 -0
- data/test/manual/banner-tests/test16.html +122 -0
- data/test/manual/banner-tests/test17.html +122 -0
- data/test/manual/banner-tests/test18.html +132 -0
- data/test/manual/banner-tests/test19.html +132 -0
- data/test/manual/banner-tests/test20.html +132 -0
- data/test/manual/banner-tests/test21.html +132 -0
- data/test/manual/banner-tests/test22.html +132 -0
- data/test/manual/banner-tests/test23.html +132 -0
- data/test/manual/banner-tests/test24.html +132 -0
- data/test/manual/banner-tests/test25.html +131 -0
- data/test/manual/banner_environment.rb +205 -0
- data/test/manual/codemirror_demo.html +773 -0
- data/test/manual/create_posts_for_web.rb +114 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/preview_manual_test.rb +129 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/test_advanced_widgets.rb +73 -0
- data/test/manual/test_banner_combinations.rb +120 -0
- data/test/manual/test_banner_features.rb +306 -0
- data/test/manual/test_banner_integration.rb +115 -0
- data/test/manual/test_banner_radial.rb +87 -0
- data/test/manual/test_basic_posts.rb +47 -0
- data/test/manual/test_layout_widgets.rb +40 -0
- data/test/manual/test_pagination.rb +24 -0
- data/test/manual/test_random_posts.rb +38 -0
- data/test/manual/test_syntax_highlighting.rb +167 -0
- data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
- data/test/rubytext/rubytext_demo_test.rb +42 -0
- data/test/rubytext/rubytext_testing_guide.md +277 -0
- data/test/run_automated_tests.rb +45 -0
- data/test/staging/.DS_Store +0 -0
- data/test/support/preview_utils.rb +88 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_gem_assets.rb +48 -0
- data/test/test_helpers.rb +240 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +883 -0
- data/test/unit/api.rb +1776 -0
- data/test/unit/asset_management.rb +219 -0
- data/test/unit/backup_test.rb +451 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +69 -0
- data/test/unit/core.rb +1211 -0
- data/test/unit/deploy_config_test.rb +248 -0
- data/test/unit/deploy_test.rb +478 -0
- data/test/unit/edit_post_test.rb +168 -0
- data/test/unit/gem_asset_management.rb +183 -0
- data/test/unit/livetext_basic.rb +57 -0
- data/test/unit/livetext_compatibility.rb +82 -0
- data/test/unit/parse_cmd_test.rb +260 -0
- data/test/unit/permalink_copy_test.rb +211 -0
- data/test/unit/post.rb +309 -0
- 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 +278 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +569 -0
- data/test/unit/social_test.rb +366 -0
- data/test/unit/syntax_highlighting.rb +70 -0
- data/test/unit/theme_management_test.rb +91 -0
- data/test/unit/view.rb +498 -0
- data/test/unit/widgets.rb +669 -0
- 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/test/wizard_test.rb +123 -0
- data/ui/README.md +67 -0
- data/ui/common/lib/ui_common.rb +8 -0
- data/ui/rubytext/README.md +191 -0
- data/ui/rubytext/bin/scriptorium-rubytext +402 -0
- data/ui/rubytext/lib/rubytext_ui.rb +300 -0
- data/ui/tui/bin/scriptorium +1890 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +2600 -0
- data/ui/web/app/assets/livetext_mode.js +244 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +196 -0
- data/ui/web/app/views/asset_management.erb +645 -0
- data/ui/web/app/views/backup_management.erb +238 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/config_widget.erb +232 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +154 -0
- data/ui/web/app/views/deploy_config.erb +149 -0
- data/ui/web/app/views/edit_pages.erb +363 -0
- data/ui/web/app/views/edit_post.erb +175 -0
- 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/error_page.erb +29 -0
- data/ui/web/app/views/header_config.erb +155 -0
- data/ui/web/app/views/layout_config.erb +147 -0
- data/ui/web/app/views/navbar_config.erb +411 -0
- data/ui/web/app/views/theme_management.erb +130 -0
- data/ui/web/app/views/view_dashboard.erb +779 -0
- data/ui/web/app/views/widgets.erb +249 -0
- data/ui/web/bin/scriptorium-web +164 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/timing.log +17 -0
- data/ui/web/tmp/web_server.log +0 -0
- metadata +434 -8
- data/lib/scriptorium/engine.rb +0 -22
- data/test/engine/unit.rb +0 -44
@@ -0,0 +1,1890 @@
|
|
1
|
+
#!/Users/Hal/.rbenv/versions/3.2.3/bin/ruby
|
2
|
+
|
3
|
+
require_relative "../../../lib/scriptorium"
|
4
|
+
require 'readline' unless ENV['NOREADLINE']
|
5
|
+
|
6
|
+
# Main entry point for Scriptorium TUI
|
7
|
+
class ScriptoriumTUI
|
8
|
+
include Scriptorium::Exceptions
|
9
|
+
include Scriptorium::Helpers
|
10
|
+
|
11
|
+
def initialize
|
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)
|
20
|
+
setup_readline
|
21
|
+
end
|
22
|
+
|
23
|
+
def discover_repo
|
24
|
+
if @testing
|
25
|
+
if Dir.exist?("scriptorium-TEST")
|
26
|
+
puts "Found existing test repository: scriptorium-TEST"
|
27
|
+
@testing = "scriptorium-TEST"
|
28
|
+
@api = Scriptorium::API.new(testmode: true)
|
29
|
+
begin
|
30
|
+
@api.open_repo("scriptorium-TEST")
|
31
|
+
puts "Current view: #{@api.current_view&.name || 'nil'}"
|
32
|
+
puts "Loaded test repository"
|
33
|
+
return true
|
34
|
+
rescue => e
|
35
|
+
puts "Error opening repository: #{e.message}"
|
36
|
+
puts e.backtrace.first if @testing
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
else
|
40
|
+
puts "No repository found."
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
else
|
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
|
63
|
+
end
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_new_repo
|
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"
|
91
|
+
|
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
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def wizard_first_view
|
107
|
+
# Check if this is the first view (only sample view exists)
|
108
|
+
views = @api.views
|
109
|
+
if views.length == 1 && views[0].name == "sample"
|
110
|
+
puts "Let's set up your first view!"
|
111
|
+
|
112
|
+
# Create a new view using existing interactive method
|
113
|
+
create_view
|
114
|
+
|
115
|
+
# Get the current view name (the one we just created)
|
116
|
+
current_view = @api.current_view
|
117
|
+
return unless current_view
|
118
|
+
name = current_view.name
|
119
|
+
|
120
|
+
# Ask about layout
|
121
|
+
puts
|
122
|
+
if yesno("Would you like to edit the layout?")
|
123
|
+
@api.edit_file("#{@api.root}/views/#{name}/config/layout.txt")
|
124
|
+
end
|
125
|
+
|
126
|
+
# Read the layout to see what containers we have
|
127
|
+
layout_file = "#{@api.root}/views/#{name}/config/layout.txt"
|
128
|
+
layout_content = read_file(layout_file)
|
129
|
+
file_containers = layout_content.lines.map { |line| line.split(/\s+/).first }.compact
|
130
|
+
|
131
|
+
# Define logical order for containers
|
132
|
+
logical_order = ['header', 'main', 'left', 'right', 'footer']
|
133
|
+
|
134
|
+
# Use logical order, but only include containers that exist in the file
|
135
|
+
containers = logical_order.select { |container| file_containers.include?(container) }
|
136
|
+
|
137
|
+
# Configure each container
|
138
|
+
containers.each do |container|
|
139
|
+
puts
|
140
|
+
if yesno("Would you like to configure #{container}?")
|
141
|
+
case container
|
142
|
+
when 'header'
|
143
|
+
# This is complex and will be expanded later
|
144
|
+
@api.edit_file("#{@api.root}/views/#{name}/config/header.txt")
|
145
|
+
when 'main'
|
146
|
+
puts "Main container is just a stub for now"
|
147
|
+
when 'left', 'right'
|
148
|
+
configure_sidebar_widgets(name, container)
|
149
|
+
when 'footer'
|
150
|
+
puts "Footer has no real config for now"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
puts
|
156
|
+
puts "View setup complete!"
|
157
|
+
else
|
158
|
+
puts "Wizard is only available for the first view setup"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def configure_sidebar_widgets(view_name, container)
|
163
|
+
puts "Add widgets to #{container}? (y/n)"
|
164
|
+
return unless yesno("Add widgets to #{container}?")
|
165
|
+
|
166
|
+
# Show available widgets
|
167
|
+
available_widgets = @api.widgets_available
|
168
|
+
puts "Available widgets: #{available_widgets.join(', ')}"
|
169
|
+
|
170
|
+
selected_widgets = []
|
171
|
+
available_widgets.each do |widget|
|
172
|
+
if yesno("Add #{widget} widget?")
|
173
|
+
selected_widgets << widget
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Configure each selected widget
|
178
|
+
selected_widgets.each do |widget|
|
179
|
+
if yesno("Configure #{widget} widget?")
|
180
|
+
case widget
|
181
|
+
when 'links'
|
182
|
+
@api.edit_file("#{@api.root}/views/#{view_name}/widgets/links/list.txt")
|
183
|
+
when 'pages'
|
184
|
+
configure_pages_widget(view_name)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def configure_pages_widget(view_name)
|
191
|
+
list_file = "#{@api.root}/views/#{view_name}/widgets/pages/list.txt"
|
192
|
+
@api.edit_file(list_file)
|
193
|
+
|
194
|
+
# Check for missing pages
|
195
|
+
pages_list = read_file(list_file, lines: true, chomp: true)
|
196
|
+
missing_pages = []
|
197
|
+
|
198
|
+
pages_list.each do |page|
|
199
|
+
page_file = "#{@api.root}/views/#{view_name}/pages/#{page}.html"
|
200
|
+
unless File.exist?(page_file)
|
201
|
+
missing_pages << page
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
if missing_pages.any?
|
206
|
+
puts
|
207
|
+
puts "Found #{missing_pages.length} missing pages: #{missing_pages.join(', ')}"
|
208
|
+
if yesno("Do you want to edit the missing pages?")
|
209
|
+
missing_pages.each do |page|
|
210
|
+
if yesno("Edit #{page}?")
|
211
|
+
@api.edit_file("#{@api.root}/views/#{view_name}/pages/#{page}.html")
|
212
|
+
else
|
213
|
+
# Create empty .lt3 file
|
214
|
+
write_file("#{@api.root}/views/#{view_name}/pages/#{page}.lt3", "")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
else
|
218
|
+
# Create empty .lt3 files for all missing pages
|
219
|
+
missing_pages.each do |page|
|
220
|
+
write_file("#{@api.root}/views/#{view_name}/pages/#{page}.lt3", "")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
else
|
224
|
+
puts "[WIZARD] No missing pages found"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def yesno(question)
|
229
|
+
print "#{question} (y/n): "
|
230
|
+
response = get_string&.downcase
|
231
|
+
response == "y" || response == "yes"
|
232
|
+
end
|
233
|
+
|
234
|
+
def get_string
|
235
|
+
if STDIN.tty? && !ENV['NOREADLINE']
|
236
|
+
result = Readline.readline
|
237
|
+
result
|
238
|
+
else
|
239
|
+
result = gets&.chomp&.strip
|
240
|
+
result
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
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
|
250
|
+
loop do
|
251
|
+
begin
|
252
|
+
current_view = @api.current_view
|
253
|
+
current_view_name = current_view&.name || "no-view"
|
254
|
+
prompt = "[#{current_view_name}] "
|
255
|
+
|
256
|
+
# Use regular gets for automated tests, Readline for interactive
|
257
|
+
if STDIN.tty? && !ENV['NOREADLINE']
|
258
|
+
input = Readline.readline(prompt, true)
|
259
|
+
else
|
260
|
+
print prompt
|
261
|
+
input = gets&.chomp&.strip
|
262
|
+
end
|
263
|
+
break if input.nil? || input.downcase == "quit" || input.downcase == "q"
|
264
|
+
next if input.empty?
|
265
|
+
execute_command(input)
|
266
|
+
rescue Interrupt
|
267
|
+
puts "\nUse 'quit' to exit"
|
268
|
+
rescue => e
|
269
|
+
puts e.message
|
270
|
+
puts e.backtrace.first if @testing
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
puts
|
275
|
+
puts " Goodbye!"
|
276
|
+
puts
|
277
|
+
end
|
278
|
+
|
279
|
+
private def setup_readline
|
280
|
+
# Only set up Readline if we're not in automated testing mode
|
281
|
+
return if ENV['NOREADLINE']
|
282
|
+
|
283
|
+
# Set up tab completion
|
284
|
+
Readline.completion_proc = proc do |input|
|
285
|
+
completions = []
|
286
|
+
|
287
|
+
# Split input to get command and arguments
|
288
|
+
parts = input.split(/\s+/)
|
289
|
+
command = parts[0]&.downcase
|
290
|
+
args = parts[1..-1] || []
|
291
|
+
|
292
|
+
if args.empty?
|
293
|
+
# Complete command names
|
294
|
+
commands = %w[view change list new version help quit cv lsv v h q upload copy delete asset configure]
|
295
|
+
completions = commands.select { |cmd| cmd.start_with?(command || "") }
|
296
|
+
elsif command == "change" || command == "cv"
|
297
|
+
# Complete view names
|
298
|
+
if @api
|
299
|
+
view_names = @api.views.map(&:name)
|
300
|
+
completions = view_names.select { |name| name.start_with?(args.last || "") }
|
301
|
+
end
|
302
|
+
elsif command == "list" && args.length == 1 && args[0] == "views"
|
303
|
+
# Complete "list views" command
|
304
|
+
completions = []
|
305
|
+
elsif command == "list" && args.length == 1 && args[0] == "assets"
|
306
|
+
# Complete asset targets
|
307
|
+
completions = %w[global library view gem]
|
308
|
+
elsif command == "new" && args.length == 1 && args[0] == "view"
|
309
|
+
# Suggest common view names for new view
|
310
|
+
suggestions = %w[blog personal work tech travel]
|
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
|
330
|
+
end
|
331
|
+
|
332
|
+
completions
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def create_test_repo
|
337
|
+
puts "Creating test repository..."
|
338
|
+
@testing = true
|
339
|
+
@api = Scriptorium::API.new(testmode: true)
|
340
|
+
@api.create_repo("scriptorium-TEST")
|
341
|
+
puts "Test repository created successfully!"
|
342
|
+
end
|
343
|
+
|
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
|
353
|
+
else
|
354
|
+
# Use File.which if available (Ruby 3.2+)
|
355
|
+
if File.respond_to?(:which)
|
356
|
+
File.which(command)
|
357
|
+
else
|
358
|
+
# Fall back to system call
|
359
|
+
result = `which #{command} 2>/dev/null`.chomp
|
360
|
+
result.empty? ? nil : result
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
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
|
+
|
535
|
+
private def show_help
|
536
|
+
puts
|
537
|
+
puts <<~HELP
|
538
|
+
Scriptorium CLI - Blog Management Tool
|
539
|
+
|
540
|
+
Usage: scriptorium [--test]
|
541
|
+
|
542
|
+
Flags:
|
543
|
+
--test - Use test repository (scriptorium-TEST)
|
544
|
+
- Default: production repository (~/.scriptorium)
|
545
|
+
|
546
|
+
Commands:
|
547
|
+
view - Show current view
|
548
|
+
change view [<name>] - Switch to a view
|
549
|
+
cv [<name>]
|
550
|
+
list views - List all views
|
551
|
+
lsv
|
552
|
+
new view [<name> <title>] - Create a new view
|
553
|
+
|
554
|
+
list posts - List posts in current view
|
555
|
+
lsp
|
556
|
+
list drafts - List all drafts
|
557
|
+
lsd
|
558
|
+
new post [<title>] - Create draft, edit, and convert to post
|
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
|
566
|
+
deploy - Deploy current view to server
|
567
|
+
preview - Preview current view locally
|
568
|
+
browse - Browse deployed view on server
|
569
|
+
|
570
|
+
list widgets - List available and configured widgets
|
571
|
+
add widget <name> - Add widget to current view
|
572
|
+
config widget <name> - Configure widget data
|
573
|
+
|
574
|
+
config social - Configure social media sharing
|
575
|
+
config reddit - Configure Reddit sharing buttons
|
576
|
+
generate - Regenerate current view
|
577
|
+
|
578
|
+
list themes - List available themes
|
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
|
586
|
+
|
587
|
+
version, v - Show version
|
588
|
+
help, h - Show this help
|
589
|
+
quit, q, ^D - Exit
|
590
|
+
HELP
|
591
|
+
puts
|
592
|
+
end
|
593
|
+
|
594
|
+
private def show_current_view
|
595
|
+
current_view = @api.current_view
|
596
|
+
current_view_name = current_view&.name || "none"
|
597
|
+
puts
|
598
|
+
puts " Current view: #{current_view_name}"
|
599
|
+
puts
|
600
|
+
end
|
601
|
+
|
602
|
+
private def change_view(args)
|
603
|
+
# Handle "change view <name>" format
|
604
|
+
if args == "view" || args.start_with?("view ")
|
605
|
+
# Remove "view " prefix if present, otherwise args is just "view"
|
606
|
+
view_name = args == "view" ? "" : args[5..-1].strip
|
607
|
+
else
|
608
|
+
view_name = args.strip
|
609
|
+
end
|
610
|
+
|
611
|
+
if view_name.empty?
|
612
|
+
# Interactive mode - prompt for view name
|
613
|
+
puts
|
614
|
+
puts " Available views:"
|
615
|
+
views = @api.views
|
616
|
+
if views.empty?
|
617
|
+
puts " No views found"
|
618
|
+
puts
|
619
|
+
return
|
620
|
+
else
|
621
|
+
current_view = @api.current_view
|
622
|
+
current_view_name = current_view&.name
|
623
|
+
|
624
|
+
views.each do |view|
|
625
|
+
current = view.name == current_view_name ? "*" : " "
|
626
|
+
puts " #{current} #{view.name} - #{view.title}"
|
627
|
+
end
|
628
|
+
puts
|
629
|
+
end
|
630
|
+
|
631
|
+
print " Enter view name: "
|
632
|
+
view_name = gets&.chomp&.strip
|
633
|
+
return if view_name.nil? || view_name.empty?
|
634
|
+
end
|
635
|
+
|
636
|
+
begin
|
637
|
+
view = @api.lookup_view(view_name)
|
638
|
+
@api.view(view_name)
|
639
|
+
puts
|
640
|
+
puts " Switched to view '#{view_name}'"
|
641
|
+
puts
|
642
|
+
rescue Exception => e
|
643
|
+
puts
|
644
|
+
puts " View '#{view_name}' not found"
|
645
|
+
puts
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
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?
|
681
|
+
end
|
682
|
+
|
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"
|
688
|
+
puts
|
689
|
+
return
|
690
|
+
end
|
691
|
+
|
692
|
+
# Create view with all parameters
|
693
|
+
begin
|
694
|
+
@api.create_view(name, title, subtitle, theme: "standard")
|
695
|
+
puts
|
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}"
|
702
|
+
puts
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
def show_version
|
707
|
+
puts
|
708
|
+
puts " Scriptorium #{Scriptorium::VERSION}"
|
709
|
+
puts
|
710
|
+
end
|
711
|
+
|
712
|
+
def list_views
|
713
|
+
puts
|
714
|
+
views = @api.views
|
715
|
+
if views.empty?
|
716
|
+
puts " No views found"
|
717
|
+
else
|
718
|
+
current_view = @api.current_view
|
719
|
+
current_view_name = current_view&.name
|
720
|
+
|
721
|
+
views.each do |view|
|
722
|
+
current = view.name == current_view_name ? "*" : " "
|
723
|
+
puts " #{current} #{view.name} #{view.title}"
|
724
|
+
end
|
725
|
+
end
|
726
|
+
puts
|
727
|
+
end
|
728
|
+
|
729
|
+
def which(command)
|
730
|
+
# Mock which in test mode to avoid hanging
|
731
|
+
if @testing
|
732
|
+
case command
|
733
|
+
when 'nano', 'vim', 'vi', 'ed'
|
734
|
+
"/usr/bin/#{command}"
|
735
|
+
else
|
736
|
+
nil
|
737
|
+
end
|
738
|
+
else
|
739
|
+
# Use File.which if available (Ruby 3.2+)
|
740
|
+
if File.respond_to?(:which)
|
741
|
+
File.which(command)
|
742
|
+
else
|
743
|
+
# Fall back to system call
|
744
|
+
result = `which #{command} 2>/dev/null`.chomp
|
745
|
+
result.empty? ? nil : result
|
746
|
+
end
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
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
|
761
|
+
|
762
|
+
# Check if editor is configured
|
763
|
+
editor_file = @api.root/"config/editor.txt"
|
764
|
+
unless File.exist?(editor_file)
|
765
|
+
puts
|
766
|
+
puts " No editor configured. Please configure an editor in config/editor.txt"
|
767
|
+
puts
|
768
|
+
return
|
769
|
+
end
|
770
|
+
|
771
|
+
editor = read_file(editor_file).strip
|
772
|
+
|
773
|
+
# Create draft
|
774
|
+
begin
|
775
|
+
draft_path = @api.create_draft(
|
776
|
+
title: title,
|
777
|
+
body: "", # Empty body to start
|
778
|
+
views: @api.current_view&.name,
|
779
|
+
tags: nil,
|
780
|
+
blurb: nil
|
781
|
+
)
|
782
|
+
|
783
|
+
puts
|
784
|
+
puts " Created draft: #{File.basename(draft_path)}"
|
785
|
+
puts " Opening in #{editor}..."
|
786
|
+
puts
|
787
|
+
|
788
|
+
# Open in editor
|
789
|
+
system("#{editor} #{draft_path}")
|
790
|
+
|
791
|
+
puts
|
792
|
+
puts " Converting draft to post..."
|
793
|
+
|
794
|
+
# Convert draft to post (like Runeblog)
|
795
|
+
begin
|
796
|
+
post_num = @api.finish_draft(draft_path)
|
797
|
+
post = @api.post(post_num)
|
798
|
+
if post && post.title
|
799
|
+
puts " Post created: ##{post_num} - #{post.title}"
|
800
|
+
else
|
801
|
+
puts " Post created: ##{post_num}"
|
802
|
+
end
|
803
|
+
puts " Use 'deploy' to publish to server when ready."
|
804
|
+
rescue => e
|
805
|
+
puts " Error converting to post: #{e.message}"
|
806
|
+
end
|
807
|
+
|
808
|
+
puts
|
809
|
+
|
810
|
+
rescue => e
|
811
|
+
puts
|
812
|
+
puts " Error creating post: #{e.message}"
|
813
|
+
puts
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
def list_posts
|
818
|
+
current_view = @api.current_view
|
819
|
+
if current_view.nil?
|
820
|
+
puts
|
821
|
+
puts " No current view selected"
|
822
|
+
puts
|
823
|
+
return
|
824
|
+
end
|
825
|
+
|
826
|
+
posts = @api.posts(current_view)
|
827
|
+
|
828
|
+
puts
|
829
|
+
if posts.empty?
|
830
|
+
puts " No posts found in view '#{current_view.name}'"
|
831
|
+
else
|
832
|
+
puts " Posts in view '#{current_view.name}':"
|
833
|
+
posts.each do |post|
|
834
|
+
puts " #{post.title}"
|
835
|
+
end
|
836
|
+
end
|
837
|
+
puts
|
838
|
+
end
|
839
|
+
|
840
|
+
def list_drafts
|
841
|
+
drafts_dir = @api.root/:drafts
|
842
|
+
return unless Dir.exist?(drafts_dir)
|
843
|
+
|
844
|
+
draft_files = Dir.glob("#{drafts_dir}/*-draft.lt3")
|
845
|
+
|
846
|
+
puts
|
847
|
+
if draft_files.empty?
|
848
|
+
puts " No drafts found"
|
849
|
+
else
|
850
|
+
draft_files.each do |file|
|
851
|
+
filename = File.basename(file)
|
852
|
+
puts " #{filename}"
|
853
|
+
end
|
854
|
+
end
|
855
|
+
puts
|
856
|
+
end
|
857
|
+
|
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
|
867
|
+
current_view = @api.current_view
|
868
|
+
if current_view.nil?
|
869
|
+
puts
|
870
|
+
puts " No current view selected"
|
871
|
+
puts
|
872
|
+
return
|
873
|
+
end
|
874
|
+
|
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
|
+
|
882
|
+
# Use the API's deploy method
|
883
|
+
result = @api.deploy(current_view.name)
|
884
|
+
if result
|
885
|
+
puts " Deployment completed!"
|
886
|
+
else
|
887
|
+
puts " Deployment failed!"
|
888
|
+
end
|
889
|
+
puts
|
890
|
+
end
|
891
|
+
|
892
|
+
private def extract_domain_from_deploy_config(config)
|
893
|
+
# user@example.com:/path/ -> example.com
|
894
|
+
if config =~ /@([^:]+):/
|
895
|
+
$1
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
private def verify_deployment(domain)
|
900
|
+
url = "https://#{domain}/last-deployed.txt"
|
901
|
+
puts " Verifying deployment..."
|
902
|
+
|
903
|
+
require 'net/http'
|
904
|
+
begin
|
905
|
+
response = Net::HTTP.get_response(URI(url))
|
906
|
+
if response.code == "200"
|
907
|
+
puts " ✅ Deployment verified!"
|
908
|
+
else
|
909
|
+
puts " ⚠️ Deployment verification failed (HTTP #{response.code})"
|
910
|
+
end
|
911
|
+
rescue => e
|
912
|
+
puts " ⚠️ Deployment verification failed: #{e.message}"
|
913
|
+
end
|
914
|
+
end
|
915
|
+
|
916
|
+
private def preview_current_view
|
917
|
+
current_view = @api.current_view
|
918
|
+
if current_view.nil?
|
919
|
+
puts
|
920
|
+
puts " No current view selected"
|
921
|
+
puts
|
922
|
+
return
|
923
|
+
end
|
924
|
+
|
925
|
+
# Check if output directory exists
|
926
|
+
output_dir = current_view.dir/:output
|
927
|
+
unless Dir.exist?(output_dir)
|
928
|
+
puts
|
929
|
+
puts " Output directory does not exist: #{output_dir}"
|
930
|
+
puts " Generate content first with 'new post' or similar."
|
931
|
+
puts
|
932
|
+
return
|
933
|
+
end
|
934
|
+
|
935
|
+
# Find the main index file
|
936
|
+
index_file = output_dir/"index.html"
|
937
|
+
unless File.exist?(index_file)
|
938
|
+
puts
|
939
|
+
puts " No index.html found in output directory"
|
940
|
+
puts " Generate content first with 'new post' or similar."
|
941
|
+
puts
|
942
|
+
return
|
943
|
+
end
|
944
|
+
|
945
|
+
# Load OS-specific helper and open the file
|
946
|
+
load_os_helpers
|
947
|
+
puts
|
948
|
+
puts " Opening preview of view '#{current_view.name}'..."
|
949
|
+
open_file(index_file)
|
950
|
+
puts
|
951
|
+
end
|
952
|
+
|
953
|
+
private def browse_deployed_view
|
954
|
+
current_view = @api.current_view
|
955
|
+
if current_view.nil?
|
956
|
+
puts
|
957
|
+
puts " No current view selected"
|
958
|
+
puts
|
959
|
+
return
|
960
|
+
end
|
961
|
+
|
962
|
+
# Check if deploy config exists
|
963
|
+
deploy_config_file = current_view.dir/:config/"deploy.txt"
|
964
|
+
unless File.exist?(deploy_config_file)
|
965
|
+
puts
|
966
|
+
puts " No deployment configuration found."
|
967
|
+
puts " Create #{deploy_config_file} with format:"
|
968
|
+
puts " user@server:path"
|
969
|
+
puts
|
970
|
+
return
|
971
|
+
end
|
972
|
+
|
973
|
+
# Read deployment configuration and extract domain
|
974
|
+
deploy_config = read_file(deploy_config_file).strip
|
975
|
+
if deploy_config.empty?
|
976
|
+
puts
|
977
|
+
puts " Deployment configuration is empty."
|
978
|
+
puts
|
979
|
+
return
|
980
|
+
end
|
981
|
+
|
982
|
+
# Extract domain for browsing
|
983
|
+
domain = extract_domain_from_deploy_config(deploy_config)
|
984
|
+
unless domain
|
985
|
+
puts
|
986
|
+
puts " Could not extract domain from deployment configuration."
|
987
|
+
puts
|
988
|
+
return
|
989
|
+
end
|
990
|
+
|
991
|
+
# Load OS-specific helper and open the URL
|
992
|
+
load_os_helpers
|
993
|
+
url = "https://#{domain}/"
|
994
|
+
puts
|
995
|
+
puts " Opening deployed view at: #{url}"
|
996
|
+
open_file(url)
|
997
|
+
puts
|
998
|
+
end
|
999
|
+
|
1000
|
+
private def load_os_helpers
|
1001
|
+
# Load the OS-specific helper functions
|
1002
|
+
os_helpers_file = @api.root/:config/"os_helpers.rb"
|
1003
|
+
if File.exist?(os_helpers_file)
|
1004
|
+
load os_helpers_file
|
1005
|
+
else
|
1006
|
+
puts " Warning: OS helpers not found. Preview/browse may not work."
|
1007
|
+
end
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
def list_widgets
|
1011
|
+
current_view = @api.current_view
|
1012
|
+
if current_view.nil?
|
1013
|
+
puts
|
1014
|
+
puts " No current view selected"
|
1015
|
+
puts
|
1016
|
+
return
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# Get available widgets
|
1020
|
+
available_widgets = @api.widgets_available
|
1021
|
+
puts
|
1022
|
+
puts " Available widgets: #{available_widgets.join(', ')}"
|
1023
|
+
|
1024
|
+
# Check which widgets are configured
|
1025
|
+
configured_widgets = []
|
1026
|
+
available_widgets.each do |widget|
|
1027
|
+
widget_dir = current_view.dir/:widgets/widget
|
1028
|
+
if Dir.exist?(widget_dir)
|
1029
|
+
configured_widgets << widget
|
1030
|
+
end
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
puts " Configured widgets: #{configured_widgets.empty? ? 'none' : configured_widgets.join(', ')}"
|
1034
|
+
puts
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
private def add_widget(args)
|
1038
|
+
current_view = @api.current_view
|
1039
|
+
if current_view.nil?
|
1040
|
+
puts
|
1041
|
+
puts " No current view selected"
|
1042
|
+
puts
|
1043
|
+
return
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
# Parse widget name from args
|
1047
|
+
widget_name = args.sub(/^widget\s+/, '').strip
|
1048
|
+
if widget_name.empty?
|
1049
|
+
puts
|
1050
|
+
puts " Usage: add widget <name>"
|
1051
|
+
puts " Example: add widget links"
|
1052
|
+
puts
|
1053
|
+
return
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
# Check if widget is available
|
1057
|
+
available_widgets = @api.widgets_available
|
1058
|
+
unless available_widgets.include?(widget_name)
|
1059
|
+
puts
|
1060
|
+
puts " Widget '#{widget_name}' is not available."
|
1061
|
+
puts " Available widgets: #{available_widgets.join(', ')}"
|
1062
|
+
puts
|
1063
|
+
return
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
# Check if widget is already configured
|
1067
|
+
widget_dir = current_view.dir/:widgets/widget_name
|
1068
|
+
if Dir.exist?(widget_dir)
|
1069
|
+
puts
|
1070
|
+
puts " Widget '#{widget_name}' is already configured."
|
1071
|
+
puts
|
1072
|
+
return
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
# Determine container (left/right)
|
1076
|
+
container = determine_widget_container(current_view)
|
1077
|
+
unless container
|
1078
|
+
puts
|
1079
|
+
puts " Error: No left or right container found in layout."
|
1080
|
+
puts " Add a left or right container to your layout first."
|
1081
|
+
puts
|
1082
|
+
return
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
# Create widget directory and list.txt
|
1086
|
+
make_dir(widget_dir)
|
1087
|
+
list_file = widget_dir/"list.txt"
|
1088
|
+
write_file(list_file, "# Add #{widget_name} items here\n")
|
1089
|
+
|
1090
|
+
puts
|
1091
|
+
puts " Added widget '#{widget_name}' to #{container} container."
|
1092
|
+
puts " Use 'config widget #{widget_name}' to configure it."
|
1093
|
+
puts
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
private def config_widget(args)
|
1097
|
+
current_view = @api.current_view
|
1098
|
+
if current_view.nil?
|
1099
|
+
puts
|
1100
|
+
puts " No current view selected"
|
1101
|
+
puts
|
1102
|
+
return
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
# Parse widget name from args
|
1106
|
+
widget_name = args.sub(/^widget\s+/, '').strip
|
1107
|
+
if widget_name.empty?
|
1108
|
+
puts
|
1109
|
+
puts " Usage: config widget <name>"
|
1110
|
+
puts " Example: config widget links"
|
1111
|
+
puts
|
1112
|
+
return
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
# Check if widget is configured
|
1116
|
+
widget_dir = current_view.dir/:widgets/widget_name
|
1117
|
+
unless Dir.exist?(widget_dir)
|
1118
|
+
puts
|
1119
|
+
puts " Widget '#{widget_name}' is not configured."
|
1120
|
+
puts " Use 'add widget #{widget_name}' to add it first."
|
1121
|
+
puts
|
1122
|
+
return
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
list_file = widget_dir/"list.txt"
|
1126
|
+
unless File.exist?(list_file)
|
1127
|
+
puts
|
1128
|
+
puts " Error: Widget list file not found: #{list_file}"
|
1129
|
+
puts
|
1130
|
+
return
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
# Show widget-specific instructions
|
1134
|
+
show_widget_instructions(widget_name)
|
1135
|
+
|
1136
|
+
puts " Press Enter to edit the widget data file..."
|
1137
|
+
gets
|
1138
|
+
|
1139
|
+
@api.edit_file(list_file)
|
1140
|
+
|
1141
|
+
# Regenerate the widget after editing
|
1142
|
+
puts " Regenerating widget..."
|
1143
|
+
begin
|
1144
|
+
@api.generate_widget(widget_name)
|
1145
|
+
puts " ✅ Widget regenerated successfully!"
|
1146
|
+
rescue => e
|
1147
|
+
puts " ⚠️ Widget regeneration failed: #{e.message}"
|
1148
|
+
end
|
1149
|
+
puts
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
private def config_social
|
1153
|
+
current_view = @api.current_view
|
1154
|
+
if current_view.nil?
|
1155
|
+
puts
|
1156
|
+
puts " No current view selected"
|
1157
|
+
puts
|
1158
|
+
return
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
social_config_file = current_view.dir/:config/"social.txt"
|
1162
|
+
unless File.exist?(social_config_file)
|
1163
|
+
puts
|
1164
|
+
puts " Social configuration file not found: #{social_config_file}"
|
1165
|
+
puts
|
1166
|
+
return
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
puts
|
1170
|
+
puts " Social Media Sharing Configuration"
|
1171
|
+
puts " ================================="
|
1172
|
+
puts
|
1173
|
+
puts " This feature adds social media meta tags to your posts for better sharing."
|
1174
|
+
puts " When enabled, posts will have proper Open Graph and Twitter Card meta tags."
|
1175
|
+
puts
|
1176
|
+
puts " Configuration:"
|
1177
|
+
puts " - List one platform per line to enable (facebook, twitter, linkedin, reddit)"
|
1178
|
+
puts " - If no platforms listed, social meta tags are disabled"
|
1179
|
+
puts " - For Reddit buttons, also configure reddit.txt file"
|
1180
|
+
puts
|
1181
|
+
puts " No Facebook App ID or Twitter username required for basic meta tags."
|
1182
|
+
puts " These are only needed if you want to add social sharing buttons later."
|
1183
|
+
puts
|
1184
|
+
puts " Press Enter to edit the configuration file..."
|
1185
|
+
gets
|
1186
|
+
|
1187
|
+
@api.edit_file(social_config_file)
|
1188
|
+
|
1189
|
+
puts
|
1190
|
+
puts " Social configuration updated."
|
1191
|
+
puts " Regenerate your view to apply changes:"
|
1192
|
+
puts " generate"
|
1193
|
+
puts
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
private def config_reddit
|
1197
|
+
current_view = @api.current_view
|
1198
|
+
if current_view.nil?
|
1199
|
+
puts
|
1200
|
+
puts " No current view selected"
|
1201
|
+
puts
|
1202
|
+
return
|
1203
|
+
end
|
1204
|
+
|
1205
|
+
reddit_config_file = current_view.dir/:config/"reddit.txt"
|
1206
|
+
unless File.exist?(reddit_config_file)
|
1207
|
+
puts
|
1208
|
+
puts " Reddit configuration file not found: #{reddit_config_file}"
|
1209
|
+
puts " Creating new Reddit configuration file..."
|
1210
|
+
puts
|
1211
|
+
# Create the file with default content
|
1212
|
+
write_file(reddit_config_file, @api.repo.predef.reddit_config)
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
puts
|
1216
|
+
puts " Reddit Sharing Button Configuration"
|
1217
|
+
puts " =================================="
|
1218
|
+
puts
|
1219
|
+
puts " This feature adds Reddit share buttons to your posts."
|
1220
|
+
puts " When enabled, readers can easily share your posts to Reddit."
|
1221
|
+
puts
|
1222
|
+
puts " Configuration options:"
|
1223
|
+
puts " - button: true/false - Enable or disable Reddit share button"
|
1224
|
+
puts " - subreddit: <name> - Specify a subreddit for direct posting (optional)"
|
1225
|
+
puts " - hover_text: <text> - Custom hover text (optional)"
|
1226
|
+
puts
|
1227
|
+
puts " Examples:"
|
1228
|
+
puts " button true"
|
1229
|
+
puts " subreddit RubyElixirEtc"
|
1230
|
+
puts " hover_text \"Share on RubyElixirEtc\""
|
1231
|
+
puts
|
1232
|
+
puts " Note: Reddit must also be enabled in social.txt for buttons to appear."
|
1233
|
+
puts
|
1234
|
+
puts " Press Enter to edit the configuration file..."
|
1235
|
+
gets
|
1236
|
+
|
1237
|
+
@api.edit_file(reddit_config_file)
|
1238
|
+
|
1239
|
+
puts
|
1240
|
+
puts " Reddit configuration updated."
|
1241
|
+
puts " Regenerate your view to apply changes:"
|
1242
|
+
puts " generate"
|
1243
|
+
puts
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
private def generate_current_view
|
1247
|
+
current_view = @api.current_view
|
1248
|
+
if current_view.nil?
|
1249
|
+
puts
|
1250
|
+
puts " No current view selected"
|
1251
|
+
puts
|
1252
|
+
return
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
puts
|
1256
|
+
puts " Regenerating view '#{current_view.name}'..."
|
1257
|
+
begin
|
1258
|
+
@api.generate_view(current_view.name)
|
1259
|
+
puts " ✅ View regenerated successfully!"
|
1260
|
+
rescue => e
|
1261
|
+
puts " ⚠️ View regeneration failed: #{e.message}"
|
1262
|
+
end
|
1263
|
+
puts
|
1264
|
+
end
|
1265
|
+
|
1266
|
+
private def determine_widget_container(view)
|
1267
|
+
# Check which containers exist in the layout
|
1268
|
+
layout_file = view.dir/:config/"layout.txt"
|
1269
|
+
return nil unless File.exist?(layout_file)
|
1270
|
+
|
1271
|
+
layout_content = read_file(layout_file)
|
1272
|
+
has_left = layout_content.include?('left')
|
1273
|
+
has_right = layout_content.include?('right')
|
1274
|
+
|
1275
|
+
if has_left && has_right
|
1276
|
+
# Both exist, prompt user
|
1277
|
+
puts
|
1278
|
+
puts " Both left and right containers found."
|
1279
|
+
puts " Which container should the widget go in?"
|
1280
|
+
puts " (l) left (r) right"
|
1281
|
+
print " Choice: "
|
1282
|
+
choice = gets&.chomp&.downcase
|
1283
|
+
|
1284
|
+
case choice
|
1285
|
+
when 'l', 'left'
|
1286
|
+
'left'
|
1287
|
+
when 'r', 'right'
|
1288
|
+
'right'
|
1289
|
+
else
|
1290
|
+
puts " Invalid choice. Widget not added."
|
1291
|
+
nil
|
1292
|
+
end
|
1293
|
+
elsif has_left
|
1294
|
+
'left'
|
1295
|
+
elsif has_right
|
1296
|
+
'right'
|
1297
|
+
else
|
1298
|
+
nil
|
1299
|
+
end
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
private def show_widget_instructions(widget_name)
|
1303
|
+
case widget_name
|
1304
|
+
when 'links'
|
1305
|
+
puts
|
1306
|
+
puts " Links Widget Configuration:"
|
1307
|
+
puts " Format: <url> <title>"
|
1308
|
+
puts " Example:"
|
1309
|
+
puts " https://example.com My Website"
|
1310
|
+
puts " https://github.com GitHub"
|
1311
|
+
puts
|
1312
|
+
when 'pages'
|
1313
|
+
puts
|
1314
|
+
puts " Pages Widget Configuration:"
|
1315
|
+
puts " Format: <filename> <title>"
|
1316
|
+
puts " Example:"
|
1317
|
+
puts " about.html About Us"
|
1318
|
+
puts " contact.html Contact"
|
1319
|
+
puts
|
1320
|
+
when 'featuredposts'
|
1321
|
+
puts
|
1322
|
+
puts " Featured Posts Widget Configuration:"
|
1323
|
+
puts " Format: <post_id> <optional_title>"
|
1324
|
+
puts " Example:"
|
1325
|
+
puts " 0001 My First Post"
|
1326
|
+
puts " 0002"
|
1327
|
+
puts
|
1328
|
+
else
|
1329
|
+
puts
|
1330
|
+
puts " Widget Configuration:"
|
1331
|
+
puts " Edit the list.txt file to configure widget data."
|
1332
|
+
puts
|
1333
|
+
end
|
1334
|
+
end
|
1335
|
+
|
1336
|
+
private def list_themes
|
1337
|
+
puts
|
1338
|
+
themes = @api.themes_available
|
1339
|
+
|
1340
|
+
if themes.empty?
|
1341
|
+
puts " No themes found"
|
1342
|
+
else
|
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]})" }
|
1367
|
+
end
|
1368
|
+
end
|
1369
|
+
puts
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
private def clone_theme(args)
|
1373
|
+
parts = args.is_a?(String) ? args.split(/\s+/) : args
|
1374
|
+
if parts.length != 2
|
1375
|
+
puts
|
1376
|
+
puts " Usage: clone theme <source> <newname>"
|
1377
|
+
puts " Example: clone theme standard my-custom"
|
1378
|
+
puts " Note: Cloned themes become user themes"
|
1379
|
+
puts
|
1380
|
+
return
|
1381
|
+
end
|
1382
|
+
|
1383
|
+
source, newname = parts[0], parts[1]
|
1384
|
+
|
1385
|
+
begin
|
1386
|
+
# Use the new API method for cloning
|
1387
|
+
result = @api.clone_theme(source, newname)
|
1388
|
+
|
1389
|
+
if result
|
1390
|
+
puts
|
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
|
1468
|
+
puts
|
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
|
1578
|
+
|
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?
|
1616
|
+
puts
|
1617
|
+
puts " ❌ Asset not found: #{filename}"
|
1618
|
+
puts
|
1619
|
+
return
|
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
|
1647
|
+
|
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
|
1663
|
+
|
1664
|
+
puts " DEBUG: edit_file returned"
|
1665
|
+
puts
|
1666
|
+
puts " ✅ Deployment configuration edited for view: #{view_name}"
|
1667
|
+
puts
|
1668
|
+
rescue => e
|
1669
|
+
puts " DEBUG: Exception in configure_deployment: #{e.class} - #{e.message}"
|
1670
|
+
puts
|
1671
|
+
puts " ❌ Failed to edit deployment configuration: #{e.message}"
|
1672
|
+
puts
|
1673
|
+
end
|
1674
|
+
end
|
1675
|
+
|
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
|
1871
|
+
|
1872
|
+
###### Main ######
|
1873
|
+
|
1874
|
+
s = ScriptoriumTUI.new
|
1875
|
+
|
1876
|
+
# Auto-discovery: check for existing repo
|
1877
|
+
got_repo = s.discover_repo
|
1878
|
+
|
1879
|
+
unless got_repo
|
1880
|
+
if s.yesno("Create new repository?")
|
1881
|
+
s.create_new_repo
|
1882
|
+
ques = "Do you want assistance in creating your first view?"
|
1883
|
+
if s.yesno(ques)
|
1884
|
+
s.wizard_first_view
|
1885
|
+
end
|
1886
|
+
end
|
1887
|
+
end
|
1888
|
+
|
1889
|
+
# Main REPL loop
|
1890
|
+
s.mainloop
|