automatic 14.12.2 → 26.09

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +669 -83
  3. data/VERSION +1 -1
  4. data/assets/siteinfo/items_all.json +60300 -52138
  5. data/automatic.gemspec +110 -248
  6. data/bin/automatic +20 -139
  7. data/config/feed2console.yml +10 -4
  8. data/config/feed2markdown.yml +41 -0
  9. data/doc/AI_TUTORIAL.md +542 -0
  10. data/doc/BASIC_DESIGN.md +547 -0
  11. data/doc/COPYING.LESSER +165 -0
  12. data/doc/DEPLOYMENT.md +830 -0
  13. data/doc/LICENSE.md +14 -0
  14. data/doc/PLUGINS.md +2015 -0
  15. data/doc/PLUGIN_DEVELOPMENT.md +86 -0
  16. data/doc/POLICY.md +962 -0
  17. data/doc/QUICKSTART.md +260 -0
  18. data/doc/RELEASING.md +394 -0
  19. data/doc/REQUIREMENTS.md +556 -0
  20. data/doc/VERSIONS +266 -0
  21. data/lib/automatic/cli.rb +270 -0
  22. data/lib/automatic/environment.rb +31 -5
  23. data/lib/automatic/feed_maker.rb +65 -25
  24. data/lib/automatic/feed_parser.rb +51 -35
  25. data/lib/automatic/http.rb +107 -0
  26. data/lib/automatic/log.rb +49 -18
  27. data/lib/automatic/opml.rb +3 -1
  28. data/lib/automatic/pipeline.rb +73 -32
  29. data/lib/automatic/recipe.rb +100 -17
  30. data/lib/automatic/version.rb +14 -1
  31. data/lib/automatic.rb +93 -20
  32. data/plugins/custom_feed/svn_log.rb +73 -32
  33. data/plugins/custom_feed/web.rb +348 -0
  34. data/plugins/filter/absolute_uri.rb +43 -27
  35. data/plugins/filter/accept.rb +38 -45
  36. data/plugins/filter/batch.rb +97 -0
  37. data/plugins/filter/claude.rb +217 -0
  38. data/plugins/filter/clear.rb +12 -8
  39. data/plugins/filter/description_link.rb +66 -51
  40. data/plugins/filter/full_feed.rb +170 -52
  41. data/plugins/filter/gemini.rb +216 -0
  42. data/plugins/filter/github_feed.rb +38 -26
  43. data/plugins/filter/ignore.rb +33 -44
  44. data/plugins/filter/image.rb +36 -25
  45. data/plugins/filter/image_source.rb +75 -52
  46. data/plugins/filter/join.rb +105 -0
  47. data/plugins/filter/kimi.rb +216 -0
  48. data/plugins/filter/limit.rb +57 -0
  49. data/plugins/filter/one.rb +19 -26
  50. data/plugins/filter/open_ai.rb +198 -0
  51. data/plugins/filter/present.rb +75 -0
  52. data/plugins/filter/rand.rb +16 -17
  53. data/plugins/filter/sakura_ai.rb +205 -0
  54. data/plugins/filter/sanitize.rb +29 -34
  55. data/plugins/filter/sort.rb +20 -27
  56. data/plugins/filter/tumblr_resize.rb +31 -23
  57. data/plugins/notify/ikachan.rb +86 -48
  58. data/plugins/provide/fluentd.rb +43 -24
  59. data/plugins/publish/amazon_s3.rb +79 -40
  60. data/plugins/publish/console.rb +19 -16
  61. data/plugins/publish/console_link.rb +20 -17
  62. data/plugins/publish/eject.rb +48 -26
  63. data/plugins/publish/fluentd.rb +50 -30
  64. data/plugins/publish/hatena_bookmark.rb +88 -71
  65. data/plugins/publish/instapaper.rb +69 -59
  66. data/plugins/publish/markdown.rb +278 -0
  67. data/plugins/publish/memcached.rb +35 -29
  68. data/plugins/store/database.rb +50 -48
  69. data/plugins/store/digest.rb +212 -0
  70. data/plugins/store/file.rb +107 -68
  71. data/plugins/store/full_text.rb +31 -28
  72. data/plugins/store/permalink.rb +18 -22
  73. data/plugins/subscription/feed.rb +34 -21
  74. data/plugins/subscription/link.rb +31 -32
  75. data/plugins/subscription/text.rb +40 -45
  76. data/plugins/subscription/tumblr.rb +55 -44
  77. data/plugins/subscription/xml.rb +40 -36
  78. metadata +112 -321
  79. data/Gemfile +0 -38
  80. data/Rakefile +0 -59
  81. data/doc/ChangeLog +0 -303
  82. data/doc/PLUGINS +0 -750
  83. data/doc/PLUGINS.ja +0 -753
  84. data/doc/README +0 -511
  85. data/doc/README.ja +0 -519
  86. data/plugins/filter/google_news.rb +0 -50
  87. data/plugins/publish/google_calendar.rb +0 -84
  88. data/plugins/publish/hipchat.rb +0 -46
  89. data/plugins/publish/pocket.rb +0 -45
  90. data/plugins/publish/twitter.rb +0 -58
  91. data/plugins/subscription/chan_toru.rb +0 -57
  92. data/plugins/subscription/g_guide.rb +0 -57
  93. data/plugins/subscription/pocket.rb +0 -51
  94. data/plugins/subscription/twitter.rb +0 -69
  95. data/plugins/subscription/twitter_search.rb +0 -50
  96. data/plugins/subscription/weather.rb +0 -33
  97. data/script/build +0 -84
  98. data/spec/fixtures/sampleFeeds.tsv +0 -1
  99. data/spec/fixtures/sampleFeeds2.tsv +0 -2
  100. data/spec/fixtures/sampleRecipe.yml +0 -24
  101. data/spec/lib/automatic/log_spec.rb +0 -32
  102. data/spec/lib/automatic/pipeline_spec.rb +0 -68
  103. data/spec/lib/automatic/recipe_spec.rb +0 -40
  104. data/spec/lib/automatic_spec.rb +0 -99
  105. data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
  106. data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
  107. data/spec/plugins/filter/accept_spec.rb +0 -331
  108. data/spec/plugins/filter/clear_spec.rb +0 -49
  109. data/spec/plugins/filter/description_link_spec.rb +0 -138
  110. data/spec/plugins/filter/full_feed_spec.rb +0 -129
  111. data/spec/plugins/filter/github_feed_spec.rb +0 -55
  112. data/spec/plugins/filter/google_news_spec.rb +0 -69
  113. data/spec/plugins/filter/ignore_spec.rb +0 -328
  114. data/spec/plugins/filter/image_source_spec.rb +0 -89
  115. data/spec/plugins/filter/image_spec.rb +0 -65
  116. data/spec/plugins/filter/one_spec.rb +0 -71
  117. data/spec/plugins/filter/rand_spec.rb +0 -52
  118. data/spec/plugins/filter/sanitize_spec.rb +0 -153
  119. data/spec/plugins/filter/sort_spec.rb +0 -189
  120. data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
  121. data/spec/plugins/notify/ikachan_spec.rb +0 -58
  122. data/spec/plugins/provide/fluentd_spec.rb +0 -49
  123. data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
  124. data/spec/plugins/publish/console_spec.rb +0 -30
  125. data/spec/plugins/publish/eject_spec.rb +0 -40
  126. data/spec/plugins/publish/fluentd_spec.rb +0 -40
  127. data/spec/plugins/publish/google_calendar_spec.rb +0 -83
  128. data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
  129. data/spec/plugins/publish/hipchat_spec.rb +0 -69
  130. data/spec/plugins/publish/instapaper_spec.rb +0 -82
  131. data/spec/plugins/publish/memcached_spec.rb +0 -63
  132. data/spec/plugins/publish/pocket_spec.rb +0 -51
  133. data/spec/plugins/publish/twitter_spec.rb +0 -73
  134. data/spec/plugins/store/file_spec.rb +0 -58
  135. data/spec/plugins/store/full_text_spec.rb +0 -152
  136. data/spec/plugins/store/permalink_spec.rb +0 -206
  137. data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
  138. data/spec/plugins/subscription/feed_spec.rb +0 -71
  139. data/spec/plugins/subscription/g_guide_spec.rb +0 -82
  140. data/spec/plugins/subscription/link_spec.rb +0 -72
  141. data/spec/plugins/subscription/pocket_spec.rb +0 -57
  142. data/spec/plugins/subscription/text_spec.rb +0 -84
  143. data/spec/plugins/subscription/tumblr_spec.rb +0 -74
  144. data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
  145. data/spec/plugins/subscription/twitter_spec.rb +0 -73
  146. data/spec/plugins/subscription/weather_spec.rb +0 -44
  147. data/spec/plugins/subscription/xml_spec.rb +0 -84
  148. data/spec/spec_helper.rb +0 -106
  149. data/spec/user_dir/plugins/store/mock.rb +0 -16
  150. data/test/fixtures/sampleOPML.xml +0 -11
  151. data/test/integration/test_absoluteurl.yml +0 -25
  152. data/test/integration/test_activerecord.yml +0 -24
  153. data/test/integration/test_add_pocket.yml +0 -26
  154. data/test/integration/test_chan_toru.yml +0 -21
  155. data/test/integration/test_descriptionlink.yml +0 -21
  156. data/test/integration/test_fluentd.yml +0 -22
  157. data/test/integration/test_fulltext.yml +0 -30
  158. data/test/integration/test_google_news.yml +0 -21
  159. data/test/integration/test_googlealert.yml +0 -21
  160. data/test/integration/test_hatenabookmark.yml +0 -30
  161. data/test/integration/test_ignore.yml +0 -25
  162. data/test/integration/test_ignore2.yml +0 -22
  163. data/test/integration/test_image2local.yml +0 -33
  164. data/test/integration/test_instapaper.yml +0 -26
  165. data/test/integration/test_link2local.yml +0 -34
  166. data/test/integration/test_one.yml +0 -23
  167. data/test/integration/test_pocket.yml +0 -22
  168. data/test/integration/test_rand.yml +0 -21
  169. data/test/integration/test_sanitize.yml +0 -23
  170. data/test/integration/test_sort.yml +0 -36
  171. data/test/integration/test_svnlog.yml +0 -15
  172. data/test/integration/test_text2feed.yml +0 -36
  173. data/test/integration/test_tumblr2local.yml +0 -43
  174. data/test/integration/test_twitter_search.yml +0 -22
  175. data/test/integration/test_weather.yml +0 -19
  176. data/test/integration/test_xml2fluentd.yml +0 -21
  177. data/vendor/.gitkeep +0 -0
data/doc/VERSIONS ADDED
@@ -0,0 +1,266 @@
1
+ automaticruby Repository Version History
2
+ ========================================
3
+
4
+ v26.09 (2026-09-23)
5
+ -------------------
6
+ - Add FilterKimi for the Kimi API, using its OpenAI-compatible Chat Completions
7
+ interface.
8
+ - Add FilterLimit, FilterBatch, and FilterPresent for pipeline-wide limiting,
9
+ batching, and required-field filtering.
10
+ - Add per-fetch interval handling to FilterFullFeed, FilterImageSource, and
11
+ FilterDescriptionLink.
12
+ - Refresh the bundled LDRFullFeed siteinfo database from a newer upstream
13
+ snapshot.
14
+ - Make doc/PLUGINS.md section 6 the plugin catalogue's source of truth.
15
+ - Harden the framework's execution boundary: fix CLI contract drift, distinguish
16
+ dependency from load failures, and preflight-validate Recipe plugins.
17
+ - Preserve standard item metadata when FeedMaker rebuilds pipelines so filters
18
+ keep source, enclosure, and full content.
19
+ - Preserve nil-link pipeline items and remove SubscriptionText placeholder
20
+ title and link values.
21
+ - Preserve SubscriptionText TSV column positions and ignore empty input rows.
22
+ - Preserve local file paths when StoreFile hands file URIs to PublishAmazonS3.
23
+ - Stop StoreFullText from passing items downstream when database writes fail.
24
+
25
+ v26.08 (2026-08-22)
26
+ -------------------
27
+ - Support Ruby 3.3 through 4.0 and modernize the codebase for current Ruby and
28
+ maintained library APIs, validating representative versions in CI.
29
+ - Harden Recipe loading with safe YAML parsing, structural validation and
30
+ framework-specific errors.
31
+ - Restructure the CLI with help and version options, predictable error reporting
32
+ and documented exit statuses.
33
+ - Report a clear CLI error when feed inspection discovers no feed instead of
34
+ raising an internal exception.
35
+ - Reject HTTP and HTTPS URLs without a host before attempting a network request.
36
+ - Verify TLS certificates when publishing to Instapaper instead of accepting an
37
+ unverified connection.
38
+ - Modernize gem packaging: separate core from optional plugin dependencies so an
39
+ install can be minimal or extended one plugin at a time.
40
+ - Classify every shipped plugin by its current support status rather than
41
+ simulating obsolete services in tests.
42
+ - Modernize the bundled plugins for current Ruby, libraries and services, and
43
+ remove the integrations that are no longer viable.
44
+ - Remove the plugins for services that no longer exist: Twitter, Pocket, HipChat,
45
+ Google Calendar, livedoor Weather, So-net G-Guide and Chan-Toru.
46
+ - Add the plugin CustomFeedWeb, building one feed per HTML index page from its
47
+ article links, selected by CSS selector and filtered by host, pattern and count.
48
+ - Add the plugin StoreDigest, which drops items by content rather than by URL, on
49
+ a SHA-256 digest of the item fields a Recipe names.
50
+ - Add the plugin FilterJoin, which joins the whole pipeline into one item carrying
51
+ each item's title, link and body, and invents no permalink for it.
52
+ - Add one AI filter plugin per service (FilterOpenAI, FilterClaude, FilterGemini,
53
+ FilterSakuraAI), replacing an item's description with that service's answer.
54
+ - Rebuild the test and CI strategy for current RSpec and Ruby, with deterministic
55
+ isolation from user data and external services.
56
+ - Add Markdown as the primary service-independent publication format, with a
57
+ documented and tested first-run workflow.
58
+ - Rebuild the maintained documentation around current usage, architecture, policy,
59
+ plugins and deployment, and remove superseded historical documents.
60
+ - Offer the project under either GPLv3 or LGPLv3, retaining third-party
61
+ attribution and shipping both licence texts.
62
+
63
+ v14.12.2 (2015-01-04)
64
+ ---------------------
65
+ - Pin every gem in the Gemfile to an exact minor version.
66
+
67
+ v14.12.1 (2015-01-03)
68
+ ---------------------
69
+ - Depend on activerecord 4.1.
70
+
71
+ v14.12.0 (2014-12-27)
72
+ ---------------------
73
+ - Rewrite obsolete methods.
74
+ - Update Gemfile.
75
+
76
+ v14.10.1 (2014-10-23)
77
+ ---------------------
78
+ - Version-only release. No change beyond the version number is recorded for it.
79
+
80
+ v14.10.0 (2014-10-23)
81
+ ---------------------
82
+ - Use Logger for log output.
83
+ - Store::FullText ignores an entry whose title is already stored.
84
+ - Add the plugins Filter::Clear, Filter::GoogleNews and Filter::DescriptionLink.
85
+
86
+ v14.5.0 (2014-05-31)
87
+ --------------------
88
+ - Update to activesupport and activerecord 4.1.
89
+
90
+ v14.3.0 (2014-03-23)
91
+ --------------------
92
+ - Downgrade the nokogiri version for OS X.
93
+
94
+ v14.2.0 (2014-02-26)
95
+ --------------------
96
+ - Add the plugin Publish::AmazonS3.
97
+ - Rename Store::TargetLink to Store::File, and have it rewrite the link to a file
98
+ URI scheme.
99
+ - Enable the file URI scheme in an RSS 2.0 feed, and let Store::File fetch a file
100
+ from Amazon S3.
101
+ - Read text from TSV formatted files in Subscription::Text, and add the
102
+ description, author and comments fields to the RSS 2.0 feed.
103
+ - Give the log messages more detail.
104
+ - Use 'double' instead of 'mock' and 'stub' for rspec-mocks.
105
+ - Improve CI with a network connection.
106
+ - Divide Feed::Parser into Feed::Maker.
107
+
108
+ v14.1.0 (2014-01-24)
109
+ --------------------
110
+ - Fix a dependency conflict.
111
+ - Refactor the error retry handling.
112
+
113
+ v13.12.0 (2013-12-17)
114
+ ---------------------
115
+ - Fix the twitter gem version.
116
+
117
+ v13.7.0 (2013-07-28)
118
+ --------------------
119
+ - Add the plugins Subscription::Xml and Provide::Fluentd.
120
+ - Fix some problems.
121
+
122
+ v13.6.0 (2013-06-28)
123
+ --------------------
124
+ - Add eleven plugins spanning Subscription, Filter and Publish, including Pocket,
125
+ GGuide, ChanToru, Accept, Sanitize, ConsoleLink, Hipchat, Memcached and Fluentd.
126
+ - Correspond to activesupport and activerecord 4.0, temporarily removing the mail
127
+ plugins for actionmailer 4.0.
128
+ - Ruby 1.8 support was outdated.
129
+ - And other improvements and bug fixes.
130
+
131
+ v13.5.0 (2013-05-18)
132
+ --------------------
133
+ - Add the plugins Subscription::Text, Subscription::Weather, Filter::One,
134
+ Filter::Rand, Publish::Twitter and Publish::Pocket.
135
+ - Move the repository from id774/automaticruby to automaticruby/automaticruby.
136
+
137
+ v13.4.1 (2013-04-10)
138
+ --------------------
139
+ - Merge the command automatic-config into automatic.
140
+ - Improve the retry functions.
141
+ - Run the tests on Ruby 2.0.
142
+
143
+ v13.2.0 (2013-02-13)
144
+ --------------------
145
+ - Implement the retry function in all subscription and download plugins.
146
+ - Plugin Filter::Ignore has become an item-based filter.
147
+ - Rename the plugin Filter::Reverse to Filter::Sort.
148
+ - Add the plugins Subscription::GoogleReaderStar, Filter::FullFeed and
149
+ Publish::Instapaper.
150
+ - Improve the tests.
151
+ - And other minor bug fixes.
152
+
153
+ v13.1.0 (2013-01-08)
154
+ --------------------
155
+ - Fix the interval bugs.
156
+ - Improve the timing of logging.
157
+ - Return only a new feed.
158
+ - Refactoring.
159
+
160
+ v12.10.0 (2012-10-16)
161
+ ---------------------
162
+ - Add the plugin Subscription::Tumblr.
163
+
164
+ v12.9.1 (2012-09-18)
165
+ --------------------
166
+ - Consider the case of a feed link including nil.
167
+
168
+ v12.9.0 (2012-09-18)
169
+ --------------------
170
+ - Unify the format of the pipeline to a feed object, an array of feeds; some
171
+ plugins lost backward compatibility.
172
+ - Add the plugins Subscription::Link, Subscription::Twitter, Filter::Image and
173
+ Filter::AbsoluteURI.
174
+ - Remove the obsolete plugins Subscription::URI, Extract::Link, Filter::ImageLink,
175
+ Store::Link and Store::Target.
176
+
177
+ v12.6.0 (2012-06-18)
178
+ --------------------
179
+ - Add the plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link
180
+ and Store::Target.
181
+ - Make scaffold create the config and db directories, and add the unscaffold
182
+ subcommand to automatic-config.
183
+ - Search the user directory's config when given -c with a simple filename, and use
184
+ the user's db directory when it exists.
185
+ - Show the recipe and db name in the log.
186
+ - Refactor the plug-in names and the header fields.
187
+ - Add tests, taking RSpec code coverage over 99%, and add the all option to
188
+ script/build.
189
+
190
+ v12.4.0 (2012-04-30)
191
+ --------------------
192
+ - Add the inspect subcommand to automatic-config.
193
+ - Add the plugins Filter::Reverse, Publish::SMTP and Publish::Mail.
194
+ - Run all integration tests with CI.
195
+
196
+ v12.3.1 (2012-03-16)
197
+ --------------------
198
+ - Write the English documents doc/README and doc/PLUGINS.
199
+ - Let a user who installed the gem package create their own plugins, under
200
+ ~/.automatic/plugins.
201
+ - Merge the utilities into the automatic-config command, adding the subcommands
202
+ scaffold, autodiscovery, opmlparser, feedparser and log.
203
+ - Add the integration test script script/build for CI.
204
+
205
+ v12.03 (2012-03-07)
206
+ -------------------
207
+ - Distribute as a gem library.
208
+ - Add the plugins CustomFeed::SVNLog, Notify::Ikachan, Filter::TumblrResize,
209
+ Filter::Image and Store::TargetLink.
210
+ - Add the CI and build script script/bootstrap.
211
+ - Significant refactoring.
212
+
213
+ v12.02.1 (2012-02-25)
214
+ ---------------------
215
+ - Point release: a large refactoring of the core, a reorganization of the
216
+ namespace, and a few plugins added.
217
+
218
+ v12.02 (2012-02-18)
219
+ -------------------
220
+ - Initial release.
221
+
222
+
223
+ Notes on this record
224
+ ====================
225
+ - Entries from v12.02 to v14.12.0 were moved here from doc/ChangeLog, unchanged in
226
+ date or content, one bullet per change.
227
+ - v14.10.1, v14.12.1 and v14.12.2 were released and recorded in the commit log but
228
+ never in doc/ChangeLog; their entries above are reconstructed from it.
229
+ - No Git tag exists for any release before v26.08; the version numbers above are
230
+ those recorded in the VERSION file at the time.
231
+ - Between v14.12.2 and v26.08 the only committed change was adding
232
+ test/integration/test_pdf2local.yml (April 2015), folded into v26.08.
233
+
234
+
235
+ Version History Guidelines
236
+ ==========================
237
+ - Record externally meaningful or architecturally significant outcomes, not a
238
+ diary of implementation work.
239
+ - Omit development-time corrections and intermediate states that were reverted
240
+ or superseded before release; Git history retains that detail.
241
+ - Summarize related low-level changes under their substantive outcome.
242
+ - Write one coherent change per bullet, at most two physical lines.
243
+ - A single line at or under 80 columns is preferred whenever practical.
244
+ Necessary file names, module names, plugin names, option names, and
245
+ configuration names may pass 80 columns on the bullet's one or two lines
246
+ when they cannot be shortened without losing meaning.
247
+ - When a version history already exists, balance a new entry against the length
248
+ of the existing lines, so that the whole file stays consistent.
249
+ - When an entry becomes substantially longer, omit or abstract implementation
250
+ details, examples, reasons, and secondary effects where possible.
251
+ - Preserve the changed target, externally observable behavior, compatibility
252
+ or security impact, and important option or configuration names.
253
+ - Combine related changes within the same version, especially changes to the
254
+ same file or feature.
255
+ - Place closely related entries near each other.
256
+ - Append an independent new change to the end of the current version entry.
257
+ - Never create more than one repository version for the same calendar date.
258
+ - Independent changes may use separate bullets, but they stay in that day's
259
+ single version entry.
260
+ - Past entries and their dates are a record. Do not rewrite, renumber or merge
261
+ them. Past releases that share a date stay recorded as they were made; they
262
+ do not conform to the rule above and are not precedent for new releases.
263
+ - This file records the history of the repository. A change that is not
264
+ observable from outside does not need an entry here.
265
+ - The first entry, at the lowest version this file reaches, reads only
266
+ "Initial release." and nothing else.
@@ -0,0 +1,270 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Name:: Automatic::CLI
3
+ # Author: id774 (More info: https://id774.net)
4
+ # Source Code:: https://github.com/id774/automaticruby
5
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
+ # Contact:: idnanashi@gmail.com
7
+ # Created:: Aug 14, 2026
8
+ # Updated:: Sep 9, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # Everything that belongs to being a command: option parsing, the subcommands,
12
+ # and turning a failure into a message and an exit status.
13
+ #
14
+ # CLI.run returns an Integer and never calls exit. Deciding the process's fate
15
+ # is bin/automatic's job, and returning the status is what makes the command
16
+ # line testable without spawning a process. See doc/BASIC_DESIGN.md section 4.2.
17
+
18
+ require 'automatic'
19
+ require 'fileutils'
20
+ require 'optparse'
21
+
22
+ module Automatic
23
+ class CLI
24
+ EXIT_SUCCESS = 0
25
+ EXIT_FAILURE = 1
26
+ EXIT_USAGE = 2
27
+
28
+ # The installation root: the directory holding plugins/, config/ and db/.
29
+ DEFAULT_ROOT_DIR = File.expand_path('../..', __dir__)
30
+
31
+ def self.run(argv, **options)
32
+ new(**options).run(argv)
33
+ end
34
+
35
+ def initialize(root_dir: DEFAULT_ROOT_DIR, stdout: $stdout, stderr: $stderr)
36
+ @root_dir = root_dir
37
+ @stdout = stdout
38
+ @stderr = stderr
39
+ end
40
+
41
+ # Returns the process exit status. --help and --version, and a subcommand
42
+ # invoked without its argument, finish early by throwing :automatic_exit.
43
+ def run(argv)
44
+ catch(:automatic_exit) do
45
+ argv = argv.dup
46
+ recipe_path = nil
47
+
48
+ parser = build_parser { |path| recipe_path = path }
49
+
50
+ begin
51
+ parser.order!(argv)
52
+ rescue OptionParser::ParseError => e
53
+ @stderr.puts e.message
54
+ @stderr.puts parser.help
55
+ throw :automatic_exit, EXIT_USAGE
56
+ end
57
+
58
+ next run_recipe(recipe_path) unless recipe_path.to_s.empty?
59
+ next usage(parser) if argv.empty?
60
+
61
+ run_subcommand(argv)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ SUBCOMMAND_USAGE = {
68
+ 'scaffold' => '',
69
+ 'unscaffold' => '',
70
+ 'autodiscovery' => '<url>',
71
+ 'feedparser' => '<url>',
72
+ 'inspect' => '<url>',
73
+ 'opmlparser' => '<opml path>',
74
+ 'log' => '<level> <message>'
75
+ }.freeze
76
+
77
+ def build_parser
78
+ OptionParser.new do |parser|
79
+ parser.version = Automatic::VERSION
80
+ parser.banner = "Automatic Ruby #{Automatic::VERSION}\n" \
81
+ " Usage: automatic [options] [subcommand]"
82
+ parser.separator 'SubCommands:'
83
+ SUBCOMMAND_USAGE.each do |name, arguments|
84
+ parser.separator " #{name} #{arguments}".rstrip
85
+ end
86
+ parser.separator 'Options:'
87
+ parser.on('-c', '--config FILE', String, 'recipe YAML file') do |path|
88
+ yield path
89
+ end
90
+ parser.on('-h', '--help', 'show this message') do
91
+ @stdout.puts parser.help
92
+ throw :automatic_exit, EXIT_SUCCESS
93
+ end
94
+ parser.on('-v', '--version', 'show the version') do
95
+ @stdout.puts Automatic::VERSION
96
+ throw :automatic_exit, EXIT_SUCCESS
97
+ end
98
+ end
99
+ end
100
+
101
+ def usage(parser)
102
+ @stdout.puts parser.help
103
+ EXIT_FAILURE
104
+ end
105
+
106
+ # Automatic::OptionalDependencyError is one of the failures reported as a
107
+ # message here: a Recipe naming a plugin whose optional gem is not
108
+ # installed is an operator's mistake with an answer, and the answer is in
109
+ # the message rather than in a backtrace. A LoadError raised from inside a
110
+ # plugin's own load -- for a different, unrelated missing file -- is not
111
+ # this, and is left alone along with everything else the plugin itself
112
+ # raises; see doc/PLUGINS.md section 2.7.
113
+ def run_recipe(path)
114
+ unless File.exist?(resolve_recipe(path))
115
+ @stderr.puts "automatic: no such recipe: #{path}"
116
+ return EXIT_FAILURE
117
+ end
118
+
119
+ Automatic.run(recipe: Automatic::Recipe.new(path), root_dir: @root_dir)
120
+ EXIT_SUCCESS
121
+ rescue Automatic::Error, Psych::Exception, SystemCallError, IOError,
122
+ Automatic::OptionalDependencyError => e
123
+ @stderr.puts "automatic: #{e.message}"
124
+ EXIT_FAILURE
125
+ end
126
+
127
+ # Mirrors Automatic::Recipe's own resolution, so that a missing file is
128
+ # reported as a message rather than as a backtrace.
129
+ def resolve_recipe(path)
130
+ in_user_dir = File.join(Automatic.user_config_dir, path.to_s)
131
+ File.exist?(in_user_dir) ? in_user_dir : path.to_s
132
+ end
133
+
134
+ def run_subcommand(argv)
135
+ name = argv.shift
136
+ handler = subcommands[name]
137
+
138
+ unless handler
139
+ @stderr.puts "automatic: no such subcommand: #{name}"
140
+ return EXIT_FAILURE
141
+ end
142
+
143
+ handler.call(argv)
144
+ EXIT_SUCCESS
145
+ rescue Automatic::Error, SystemCallError, IOError, Automatic::OptionalDependencyError => e
146
+ @stderr.puts "automatic: #{e.message}"
147
+ EXIT_FAILURE
148
+ end
149
+
150
+ def missing_argument(name)
151
+ @stderr.puts "Usage: automatic #{name} #{SUBCOMMAND_USAGE[name]}"
152
+ throw :automatic_exit, EXIT_FAILURE
153
+ end
154
+
155
+ # Each subcommand requires what it needs, so that `automatic --version`
156
+ # loads neither a feed parser nor an OPML parser.
157
+ #
158
+ # `feedbag` is an optional dependency: two subcommands out of seven use it,
159
+ # and neither running a Recipe nor any other subcommand does. It is
160
+ # required through Automatic.require_optional, so that not having it is a
161
+ # sentence rather than a backtrace. See doc/POLICY.md section 9.1.
162
+ def subcommands
163
+ {
164
+ 'scaffold' => method(:scaffold),
165
+ 'unscaffold' => method(:unscaffold),
166
+ 'autodiscovery' => method(:autodiscovery),
167
+ 'feedparser' => method(:feedparser),
168
+ 'inspect' => method(:inspect_url),
169
+ 'opmlparser' => method(:opmlparser),
170
+ 'log' => method(:log)
171
+ }
172
+ end
173
+
174
+ def scaffold(_argv)
175
+ Dir.children(File.join(@root_dir, 'plugins')).sort.each do |category|
176
+ next unless File.directory?(File.join(@root_dir, 'plugins', category))
177
+
178
+ create_dir(File.join(Automatic.user_dir, 'plugins', category))
179
+ end
180
+
181
+ create_dir(File.join(Automatic.user_dir, 'db'))
182
+
183
+ assets = File.join(Automatic.user_dir, 'assets')
184
+ create_dir(assets)
185
+ copy_bundled(File.join(@root_dir, 'assets', 'siteinfo'), File.join(assets, 'siteinfo'))
186
+
187
+ config = Automatic.user_config_dir
188
+ create_dir(config)
189
+ copy_bundled(File.join(@root_dir, 'config'), File.join(config, 'example'))
190
+ end
191
+
192
+ def unscaffold(_argv)
193
+ dir = Automatic.user_dir
194
+ return unless File.directory?(dir)
195
+
196
+ @stdout.puts "Removing #{dir}"
197
+ FileUtils.rm_r(dir)
198
+ end
199
+
200
+ def autodiscovery(argv)
201
+ Automatic.require_optional('feedbag', needed_by: 'the autodiscovery subcommand')
202
+ require 'pp'
203
+ url = argv.shift || missing_argument('autodiscovery')
204
+ @stdout.puts Feedbag.find(url).pretty_inspect
205
+ end
206
+
207
+ def feedparser(argv)
208
+ require 'automatic/feed_parser'
209
+ require 'pp'
210
+ url = argv.shift || missing_argument('feedparser')
211
+ validate_fetch_url(url)
212
+ @stdout.puts Automatic::FeedParser.get_url(url).pretty_inspect
213
+ end
214
+
215
+ # Converts only the URL validation failure `Automatic::Http.uri` already
216
+ # applies into the CLI's ordinary failure path; an unexpected error from
217
+ # inside FeedParser itself is left to propagate.
218
+ def validate_fetch_url(url)
219
+ Automatic::Http.uri(url)
220
+ rescue ArgumentError, URI::InvalidURIError => e
221
+ raise Automatic::Error, e.message
222
+ end
223
+
224
+ def inspect_url(argv)
225
+ require 'automatic/feed_parser'
226
+ Automatic.require_optional('feedbag', needed_by: 'the inspect subcommand')
227
+ require 'pp'
228
+ url = argv.shift || missing_argument('inspect')
229
+ feeds = Feedbag.find(url)
230
+ raise Automatic::Error, "no feed found at #{url}" if feeds.empty?
231
+
232
+ @stdout.puts feeds.pretty_inspect
233
+ @stdout.puts Automatic::FeedParser.get_url(feeds.first).pretty_inspect
234
+ end
235
+
236
+ def opmlparser(argv)
237
+ require 'automatic/opml'
238
+ path = argv.shift || missing_argument('opmlparser')
239
+ parser = Automatic::OPML::Parser.new(File.read(path))
240
+ parser.each_outline { |_opml, outline| @stdout.puts outline.xmlUrl }
241
+ end
242
+
243
+ def log(argv)
244
+ level = argv.shift || missing_argument('log')
245
+ message = argv.shift || missing_argument('log')
246
+ Automatic::Log.puts(level, message)
247
+ end
248
+
249
+ def create_dir(path)
250
+ return false if File.exist?(path)
251
+
252
+ FileUtils.mkdir_p(path)
253
+ @stdout.puts "Creating #{path}"
254
+ true
255
+ end
256
+
257
+ # Bundled initial data (siteinfo, example config) is copied whenever its
258
+ # own destination is missing, independent of whether the parent user
259
+ # directory already existed. That is what lets a re-run of scaffold heal a
260
+ # partially-scaffolded user directory instead of only ever populating a
261
+ # brand new one. See doc/DEPLOYMENT.md and doc/POLICY.md on scaffold only
262
+ # adding what is missing.
263
+ def copy_bundled(src, dest)
264
+ return if File.exist?(dest)
265
+
266
+ FileUtils.cp_r(src, dest)
267
+ @stdout.puts "Creating #{dest}"
268
+ end
269
+ end
270
+ end
@@ -1,8 +1,34 @@
1
1
  # -*- coding: utf-8 -*-
2
- require 'rubygems'
2
+ # Name:: Automatic::Environment
3
+ # Author: id774 (More info: https://id774.net)
4
+ # Source Code:: https://github.com/id774/automaticruby
5
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
+ # Contact:: idnanashi@gmail.com
7
+ # Created:: Feb 18, 2012
8
+ # Updated:: Aug 14, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # Bundler setup for a source checkout, and nothing more.
12
+ #
13
+ # When this repository is used from a checkout, bin/automatic should resolve
14
+ # the gems the checkout's Gemfile locks. When the gem is installed normally
15
+ # there is no Gemfile beside lib/, this file does nothing, and that is correct:
16
+ # an installed library must not impose a bundle on the program requiring it.
17
+ #
18
+ # This deliberately does not call Bundler.require. Loading every gem in the
19
+ # bundle is how a plugin's dependency used to become everyone's; see
20
+ # doc/POLICY.md section 2.5.
3
21
 
4
- # Set up gems listed in the Gemfile.
5
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
22
+ gemfile = File.expand_path('../../Gemfile', __dir__)
6
23
 
7
- require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
8
- Bundler.require :default if defined?(Bundler)
24
+ if File.exist?(gemfile)
25
+ ENV['BUNDLE_GEMFILE'] ||= gemfile
26
+ begin
27
+ require 'bundler/setup'
28
+ rescue LoadError, StandardError
29
+ # Bundler is absent, or the bundle is not installed. Fall back to whatever
30
+ # RubyGems can activate; a genuinely missing dependency will surface as a
31
+ # LoadError naming it, which is the more useful message anyway.
32
+ nil
33
+ end
34
+ end