willow_camp_cli 0.1.12
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 +7 -0
- data/.github/workflows/gem-push.yml +45 -0
- data/.gitignore +61 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +61 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +53 -0
- data/README.md +131 -0
- data/Rakefile +10 -0
- data/bin/console +13 -0
- data/bin/setup +26 -0
- data/exe/willow-camp +12 -0
- data/lib/willow_camp_cli/cli.rb +489 -0
- data/lib/willow_camp_cli/version.rb +3 -0
- data/lib/willow_camp_cli.rb +6 -0
- data/mise.toml +2 -0
- data/willow_camp_cli.gemspec +35 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c468d463e934298a4d1b7978fba95864b0706d1ec8c9f9c663f4471c0f656750
|
4
|
+
data.tar.gz: '03921da40af47edb550b0649f9208f10f6537220b5eb5599f9ce1332e7887c79'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff22ca55177e194ca8e0a1c31de62cce902873baa21b0927905d27ca3bc53da6909434e2666c6ef69d87f614200fc48b4bd29dbc69ff01a953d743d631c1bb6f
|
7
|
+
data.tar.gz: 1c8b21285051eae025cdb2a96788a90bc709cf39f62da2bd0bf1ae1000fd5e444ef69dd8a02af517f8effab3533193cc4010b270696cd5157b2563c82c44d226
|
@@ -0,0 +1,45 @@
|
|
1
|
+
name: Ruby Gem
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: ["main"]
|
6
|
+
pull_request:
|
7
|
+
branches: ["main"]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
build:
|
11
|
+
name: Build + Publish
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
permissions:
|
15
|
+
contents: write
|
16
|
+
packages: write
|
17
|
+
id-token: write
|
18
|
+
|
19
|
+
steps:
|
20
|
+
- uses: actions/checkout@v4
|
21
|
+
- name: Set up Ruby
|
22
|
+
uses: ruby/setup-ruby@v1
|
23
|
+
- name: Install dependencies
|
24
|
+
run: bundle install --jobs 4 --retry 3
|
25
|
+
- name: Run tests
|
26
|
+
run: rake test
|
27
|
+
- name: Build gem
|
28
|
+
run: gem build *.gemspec
|
29
|
+
- name: Publish to GPR
|
30
|
+
run: |
|
31
|
+
mkdir -p $HOME/.gem
|
32
|
+
touch $HOME/.gem/credentials
|
33
|
+
chmod 0600 $HOME/.gem/credentials
|
34
|
+
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
35
|
+
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
36
|
+
env:
|
37
|
+
GEM_HOST_API_KEY: "Bearer ${{secrets.GH_GEM_HOST_TOKEN}}"
|
38
|
+
OWNER: ${{ github.repository_owner }}
|
39
|
+
- name: Debug Git State
|
40
|
+
run: |
|
41
|
+
git status
|
42
|
+
git diff
|
43
|
+
git ls-files --others --exclude-standard
|
44
|
+
- name: Publish to RubyGems
|
45
|
+
uses: rubygems/release-gem@v1
|
data/.gitignore
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Created by https://www.toptal.com/developers/gitignore/api/ruby
|
2
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=ruby
|
3
|
+
|
4
|
+
### Ruby ###
|
5
|
+
*.gem
|
6
|
+
*.rbc
|
7
|
+
/.config
|
8
|
+
/coverage/
|
9
|
+
/InstalledFiles
|
10
|
+
/pkg/
|
11
|
+
/spec/reports/
|
12
|
+
/spec/examples.txt
|
13
|
+
/test/tmp/
|
14
|
+
/test/version_tmp/
|
15
|
+
/tmp/
|
16
|
+
|
17
|
+
# Used by dotenv library to load environment variables.
|
18
|
+
# .env
|
19
|
+
|
20
|
+
# Ignore Byebug command history file.
|
21
|
+
.byebug_history
|
22
|
+
|
23
|
+
## Specific to RubyMotion:
|
24
|
+
.dat*
|
25
|
+
.repl_history
|
26
|
+
build/
|
27
|
+
*.bridgesupport
|
28
|
+
build-iPhoneOS/
|
29
|
+
build-iPhoneSimulator/
|
30
|
+
|
31
|
+
## Specific to RubyMotion (use of CocoaPods):
|
32
|
+
#
|
33
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
34
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
35
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
36
|
+
# vendor/Pods/
|
37
|
+
|
38
|
+
## Documentation cache and generated files:
|
39
|
+
/.yardoc/
|
40
|
+
/_yardoc/
|
41
|
+
/doc/
|
42
|
+
/rdoc/
|
43
|
+
|
44
|
+
## Environment normalization:
|
45
|
+
/.bundle/
|
46
|
+
/vendor/bundle
|
47
|
+
/lib/bundler/man/
|
48
|
+
|
49
|
+
# for a library or gem, you might want to ignore these files since the code is
|
50
|
+
# intended to run in multiple environments; otherwise, check them in:
|
51
|
+
# Gemfile.lock
|
52
|
+
# .ruby-version
|
53
|
+
# .ruby-gemset
|
54
|
+
|
55
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
56
|
+
.rvmrc
|
57
|
+
|
58
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
59
|
+
# .rubocop-https?--*
|
60
|
+
|
61
|
+
# End of https://www.toptal.com/developers/gitignore/api/ruby
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.4.4
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [0.1.12] - 2025-05-26
|
4
|
+
|
5
|
+
- debug gem publishing
|
6
|
+
|
7
|
+
## [0.1.11] - 2025-05-26
|
8
|
+
|
9
|
+
- debug gem publishing
|
10
|
+
|
11
|
+
## [0.1.10] - 2025-05-26
|
12
|
+
|
13
|
+
- debug gem publishing
|
14
|
+
|
15
|
+
## [0.1.9] - 2025-05-26
|
16
|
+
|
17
|
+
- debug gem publishing
|
18
|
+
|
19
|
+
## [0.1.8] - 2025-05-26
|
20
|
+
|
21
|
+
- update gem publishing
|
22
|
+
|
23
|
+
## [0.1.7] - 2025-05-26
|
24
|
+
|
25
|
+
- update gem publishing
|
26
|
+
|
27
|
+
## [0.1.6] - 2025-05-26
|
28
|
+
|
29
|
+
- update gem publishing
|
30
|
+
|
31
|
+
## [0.1.5] - 2025-05-26
|
32
|
+
|
33
|
+
- Bumping version for CI
|
34
|
+
|
35
|
+
## [0.1.4] - 2025-05-26
|
36
|
+
|
37
|
+
- Convert ghost html to markdown when importing
|
38
|
+
|
39
|
+
## [0.1.3] - 2025-05-25
|
40
|
+
|
41
|
+
- Bumping version for CI
|
42
|
+
|
43
|
+
|
44
|
+
## [0.1.2] - 2025-05-25
|
45
|
+
|
46
|
+
- Bumping version for CI
|
47
|
+
|
48
|
+
## [0.1.1] - 2025-05-25
|
49
|
+
|
50
|
+
- Added ghost-import command to convert Ghost blog exports to willow.camp markdown format
|
51
|
+
- Support for converting Ghost exports with proper frontmatter
|
52
|
+
- Automatically detect and use the best content format available (markdown, HTML or plaintext)
|
53
|
+
|
54
|
+
## [0.1.0] - 2025-05-25
|
55
|
+
|
56
|
+
- Initial release
|
57
|
+
- Support for listing, showing, creating, updating, and deleting posts
|
58
|
+
- Support for bulk uploading posts from a directory
|
59
|
+
- Support for downloading posts to a file
|
60
|
+
- Support for dry-run mode
|
61
|
+
- Support for verbose output
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
willow_camp_cli (0.1.12)
|
5
|
+
colorize (~> 0.8.1)
|
6
|
+
fileutils (~> 1.0)
|
7
|
+
json (~> 2.0)
|
8
|
+
reverse_markdown (~> 2.1)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
addressable (2.8.7)
|
14
|
+
public_suffix (>= 2.0.2, < 7.0)
|
15
|
+
bigdecimal (3.1.9)
|
16
|
+
colorize (0.8.1)
|
17
|
+
crack (1.0.0)
|
18
|
+
bigdecimal
|
19
|
+
rexml
|
20
|
+
fileutils (1.7.3)
|
21
|
+
hashdiff (1.2.0)
|
22
|
+
json (2.12.2)
|
23
|
+
mini_portile2 (2.8.9)
|
24
|
+
minitest (5.25.5)
|
25
|
+
nokogiri (1.18.8)
|
26
|
+
mini_portile2 (~> 2.8.2)
|
27
|
+
racc (~> 1.4)
|
28
|
+
nokogiri (1.18.8-arm64-darwin)
|
29
|
+
racc (~> 1.4)
|
30
|
+
public_suffix (6.0.2)
|
31
|
+
racc (1.8.1)
|
32
|
+
rake (13.2.1)
|
33
|
+
reverse_markdown (2.1.1)
|
34
|
+
nokogiri
|
35
|
+
rexml (3.4.1)
|
36
|
+
webmock (3.25.1)
|
37
|
+
addressable (>= 2.8.0)
|
38
|
+
crack (>= 0.3.2)
|
39
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
40
|
+
|
41
|
+
PLATFORMS
|
42
|
+
arm64-darwin-24
|
43
|
+
ruby
|
44
|
+
|
45
|
+
DEPENDENCIES
|
46
|
+
bundler (~> 2.0)
|
47
|
+
minitest (~> 5.0)
|
48
|
+
rake (~> 13.0)
|
49
|
+
webmock (~> 3.18)
|
50
|
+
willow_camp_cli!
|
51
|
+
|
52
|
+
BUNDLED WITH
|
53
|
+
2.6.7
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Willow Camp CLI
|
2
|
+
|
3
|
+
A command-line interface for managing blog posts on a willow.camp.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'willow_camp_cli'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install willow_camp_cli
|
23
|
+
```
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
```
|
27
|
+
Usage: willow-camp COMMAND [options]
|
28
|
+
|
29
|
+
Commands:
|
30
|
+
list List all posts
|
31
|
+
show Show a single post by slug
|
32
|
+
create Create a new post from a Markdown file
|
33
|
+
update Update an existing post by slug
|
34
|
+
delete Delete a post by slug
|
35
|
+
upload Bulk upload posts from a directory
|
36
|
+
download Download a post to a Markdown file
|
37
|
+
ghost-import Import posts from a Ghost export file
|
38
|
+
help Show this help message
|
39
|
+
|
40
|
+
Options:
|
41
|
+
-u, --url URL API URL (e.g., https://yourblog.example.com)
|
42
|
+
-t, --token TOKEN API Bearer Token
|
43
|
+
-d, --directory DIRECTORY Directory containing Markdown files (for upload)
|
44
|
+
-f, --file FILE Single Markdown file (for create/update)
|
45
|
+
-s, --slug SLUG Post slug (for show/update/delete/download)
|
46
|
+
-o, --output FILE Output file (for download)
|
47
|
+
-g, --ghost-export FILE Ghost export JSON file
|
48
|
+
--output-dir DIRECTORY Output directory for Ghost import (default: 'markdown')
|
49
|
+
--dry-run Show what would be done without making actual changes
|
50
|
+
-v, --verbose Show detailed output
|
51
|
+
-h, --help Show this help message
|
52
|
+
```
|
53
|
+
|
54
|
+
## Examples
|
55
|
+
|
56
|
+
```sh
|
57
|
+
export WILLOW_CAMP_API_TOKEN=<your token>
|
58
|
+
```
|
59
|
+
|
60
|
+
### List all posts
|
61
|
+
|
62
|
+
```bash
|
63
|
+
willow-camp list
|
64
|
+
```
|
65
|
+
|
66
|
+
### Show a single post
|
67
|
+
|
68
|
+
```bash
|
69
|
+
willow-camp show -s my-post-slug
|
70
|
+
```
|
71
|
+
|
72
|
+
### Create a new post from a Markdown file
|
73
|
+
|
74
|
+
```bash
|
75
|
+
willow-camp create -f path/to/post.md
|
76
|
+
```
|
77
|
+
|
78
|
+
### Update an existing post
|
79
|
+
|
80
|
+
```bash
|
81
|
+
willow-camp update -s my-post-slug -f path/to/updated-post.md
|
82
|
+
```
|
83
|
+
|
84
|
+
### Delete a post
|
85
|
+
|
86
|
+
```bash
|
87
|
+
willow-camp delete -s my-post-slug
|
88
|
+
```
|
89
|
+
|
90
|
+
### Bulk upload posts from a directory
|
91
|
+
|
92
|
+
```bash
|
93
|
+
willow-camp upload -d path/to/markdown/files
|
94
|
+
```
|
95
|
+
|
96
|
+
### Download a post to a file
|
97
|
+
|
98
|
+
```bash
|
99
|
+
willow-camp download -s my-post-slug -o path/to/save.md
|
100
|
+
```
|
101
|
+
|
102
|
+
### Import posts from a Ghost export file
|
103
|
+
|
104
|
+
```bash
|
105
|
+
# Just convert to Markdown files
|
106
|
+
willow-camp ghost-import --ghost-export path/to/ghost-export.json --output-dir path/to/output
|
107
|
+
|
108
|
+
# Convert and upload in one step
|
109
|
+
willow-camp ghost-import -g path/to/ghost-export.json --output-dir path/to/output
|
110
|
+
```
|
111
|
+
|
112
|
+
## Environment Variables
|
113
|
+
|
114
|
+
You can set default API URL and token using environment variables:
|
115
|
+
|
116
|
+
```bash
|
117
|
+
export WILLOW_API_URL="https://yourblog.example.com"
|
118
|
+
export WILLOW_API_TOKEN="your-api-token"
|
119
|
+
```
|
120
|
+
|
121
|
+
If these environment variables are set, you don't need to provide the `-u` and `-t` options.
|
122
|
+
|
123
|
+
## Development
|
124
|
+
|
125
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
126
|
+
|
127
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
128
|
+
|
129
|
+
## Contributing
|
130
|
+
|
131
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/willow_camp.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "bundler/setup"
|
3
|
+
require "willow_camp_cli"
|
4
|
+
|
5
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
6
|
+
# with your gem easier. You can also use a different console, if you like.
|
7
|
+
|
8
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
9
|
+
# require "pry"
|
10
|
+
# Pry.start
|
11
|
+
|
12
|
+
require "irb"
|
13
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
# path to your application root.
|
5
|
+
APP_ROOT = File.expand_path("..", __dir__)
|
6
|
+
|
7
|
+
def system!(*args)
|
8
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
9
|
+
end
|
10
|
+
|
11
|
+
FileUtils.chdir APP_ROOT do
|
12
|
+
# This script is a way to set up or update your development environment automatically.
|
13
|
+
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
|
14
|
+
# Add necessary setup steps to this file.
|
15
|
+
|
16
|
+
puts "== Installing dependencies =="
|
17
|
+
system! "gem install bundler --conservative"
|
18
|
+
system("bundle check") || system!("bundle install")
|
19
|
+
|
20
|
+
puts "\n== Removing old logs and tempfiles =="
|
21
|
+
system! "rm -f log/*"
|
22
|
+
system! "rm -rf tmp/cache"
|
23
|
+
|
24
|
+
puts "\n== Restarting application server =="
|
25
|
+
system! "bin/rails restart"
|
26
|
+
end
|
data/exe/willow-camp
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "willow_camp_cli"
|
5
|
+
rescue LoadError
|
6
|
+
# Try to load from the local path if the gem is not found
|
7
|
+
lib_path = File.expand_path("../../lib", __FILE__)
|
8
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
9
|
+
require "willow_camp_cli"
|
10
|
+
end
|
11
|
+
|
12
|
+
WillowCampCLI::CLI.run(ARGV)
|
@@ -0,0 +1,489 @@
|
|
1
|
+
require "json"
|
2
|
+
require "uri"
|
3
|
+
require "net/http"
|
4
|
+
require "optparse"
|
5
|
+
require "pathname"
|
6
|
+
require "colorize"
|
7
|
+
require "fileutils"
|
8
|
+
require "reverse_markdown"
|
9
|
+
|
10
|
+
module WillowCampCLI
|
11
|
+
class CLI
|
12
|
+
API_URL = "https://willow.camp/"
|
13
|
+
attr_reader :token, :verbose
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@token = options[:token]
|
17
|
+
@directory = options[:directory]
|
18
|
+
@dry_run = options[:dry_run]
|
19
|
+
@verbose = options[:verbose]
|
20
|
+
@slug = options[:slug]
|
21
|
+
end
|
22
|
+
|
23
|
+
# List all posts
|
24
|
+
def list_posts
|
25
|
+
puts "📋 Listing all posts from #{API_URL}...".blue
|
26
|
+
|
27
|
+
response = api_request(:get, "/api/posts")
|
28
|
+
if response
|
29
|
+
posts = JSON.parse(response.body)["posts"]
|
30
|
+
if posts.empty?
|
31
|
+
puts "No posts found".yellow
|
32
|
+
else
|
33
|
+
puts "\nFound #{posts.size} post(s):".green
|
34
|
+
posts.each do |post|
|
35
|
+
puts "- [#{post["id"]}] #{post["slug"]}".cyan
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Show a single post by slug
|
42
|
+
def show_post
|
43
|
+
return puts "Error: Slug is required".red unless @slug
|
44
|
+
|
45
|
+
puts "🔍 Fetching post with slug: #{@slug}...".blue
|
46
|
+
|
47
|
+
response = api_request(:get, "/api/posts/#{@slug}")
|
48
|
+
if response
|
49
|
+
post = JSON.parse(response.body)["post"]
|
50
|
+
puts "\nPost details:".green
|
51
|
+
puts "ID: #{post["id"]}".cyan
|
52
|
+
puts "Slug: #{post["slug"]}".cyan
|
53
|
+
puts "Title: #{post.dig("title")}".cyan
|
54
|
+
puts "Published: #{post["published"] || false}".cyan
|
55
|
+
puts "Published at: #{post["published_at"] || "Not published"}".cyan
|
56
|
+
puts "Tags: #{(post["tag_list"] || []).join(", ")}".cyan
|
57
|
+
|
58
|
+
if @verbose
|
59
|
+
puts "\nContent:".cyan
|
60
|
+
puts "-" * 50
|
61
|
+
puts post["markdown"]
|
62
|
+
puts "-" * 50
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Update a post by slug
|
68
|
+
def update_post(content)
|
69
|
+
return puts "Error: Slug and content are required".red unless @slug && content
|
70
|
+
|
71
|
+
puts "🔄 Updating post with slug: #{@slug}...".blue
|
72
|
+
|
73
|
+
if @dry_run
|
74
|
+
puts " DRY RUN: Would update post #{@slug}".yellow
|
75
|
+
puts " Content preview: #{content[0..100]}...".yellow if @verbose
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
response = api_request(:patch, "/api/posts/#{@slug}", {post: {markdown: content}})
|
80
|
+
if response
|
81
|
+
post = JSON.parse(response.body)["post"]
|
82
|
+
puts "✅ Successfully updated post: #{post["title"]} (#{post["slug"]})".green
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Delete a post by slug
|
87
|
+
def delete_post
|
88
|
+
return puts "Error: Slug is required".red unless @slug
|
89
|
+
|
90
|
+
puts "🗑️ Deleting post with slug: #{@slug}...".blue
|
91
|
+
|
92
|
+
if @dry_run
|
93
|
+
puts " DRY RUN: Would delete post #{@slug}".yellow
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
97
|
+
response = api_request(:delete, "/api/posts/#{@slug}")
|
98
|
+
if response && response.code.to_i == 204
|
99
|
+
puts "✅ Successfully deleted post: #{@slug}".green
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Upload a single Markdown file
|
104
|
+
def upload_file(file_path)
|
105
|
+
puts "📤 Uploading #{file_path}...".blue
|
106
|
+
content = File.read(file_path)
|
107
|
+
|
108
|
+
if @dry_run
|
109
|
+
puts " DRY RUN: Would upload #{file_path}".yellow
|
110
|
+
puts " Content preview: #{content[0..100]}...".yellow if @verbose
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
response = api_request(:post, "/api/posts", {post: {markdown: content}})
|
115
|
+
if response
|
116
|
+
post = JSON.parse(response.body)["post"]
|
117
|
+
puts "✅ Successfully uploaded: #{file_path}".green
|
118
|
+
puts "📌 Created post '#{post["title"]}' with slug: #{post["slug"]}".green
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Upload all Markdown files from a directory
|
123
|
+
def upload_all
|
124
|
+
puts "🔍 Looking for Markdown files in #{@directory}...".blue
|
125
|
+
|
126
|
+
files = find_markdown_files
|
127
|
+
if files.empty?
|
128
|
+
puts "❌ No Markdown files found in #{@directory}".red
|
129
|
+
return
|
130
|
+
end
|
131
|
+
|
132
|
+
puts "📝 Found #{files.size} Markdown file(s)".blue
|
133
|
+
|
134
|
+
files.each_with_index do |file, index|
|
135
|
+
puts "\n[#{index + 1}/#{files.size}] Processing #{file}".cyan
|
136
|
+
upload_file(file)
|
137
|
+
end
|
138
|
+
|
139
|
+
puts "\n✅ Operation complete!".green
|
140
|
+
end
|
141
|
+
|
142
|
+
# Download a post to a file
|
143
|
+
def download_post(output_path)
|
144
|
+
return puts "Error: Slug is required".red unless @slug
|
145
|
+
|
146
|
+
puts "📥 Downloading post with slug: #{@slug}...".blue
|
147
|
+
|
148
|
+
response = api_request(:get, "/api/posts/#{@slug}")
|
149
|
+
if response
|
150
|
+
post = JSON.parse(response.body)["post"]
|
151
|
+
|
152
|
+
# Use provided output path or generate one based on slug
|
153
|
+
output_path ||= "#{@slug}.md"
|
154
|
+
|
155
|
+
File.write(output_path, post["markdown"])
|
156
|
+
puts "✅ Successfully downloaded post to #{output_path}".green
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Import posts from a Ghost export file
|
161
|
+
def ghost_import(ghost_export_file, output_dir = "markdown")
|
162
|
+
return puts "Error: Ghost export file is required".red unless ghost_export_file
|
163
|
+
return puts "Error: Ghost export file not found: #{ghost_export_file}".red unless File.exist?(ghost_export_file)
|
164
|
+
|
165
|
+
puts "🔍 Processing Ghost export file: #{ghost_export_file}...".blue
|
166
|
+
|
167
|
+
begin
|
168
|
+
# Create output directory if it doesn't exist
|
169
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
170
|
+
|
171
|
+
# Parse JSON export file
|
172
|
+
ghost_data = JSON.parse(File.read(ghost_export_file))
|
173
|
+
|
174
|
+
posts = ghost_data["db"][0]["data"]["posts"].select { |post| post["status"] == "published" }
|
175
|
+
|
176
|
+
if posts.empty?
|
177
|
+
puts "❌ No published posts found in the Ghost export".red
|
178
|
+
return
|
179
|
+
end
|
180
|
+
|
181
|
+
puts "Found #{posts.size} published posts".green
|
182
|
+
|
183
|
+
# Process each post
|
184
|
+
processed_count = 0
|
185
|
+
posts.each do |post|
|
186
|
+
title = post["title"]
|
187
|
+
slug = post["slug"]
|
188
|
+
published = post["status"] == "published" ? !post["published_at"].nil? : nil
|
189
|
+
published_at = post["published_at"]&.split("T")&.first
|
190
|
+
|
191
|
+
puts "\n[#{processed_count + 1}/#{posts.size}] Processing '#{title}' (#{slug})".cyan
|
192
|
+
|
193
|
+
# Get content from the most appropriate source
|
194
|
+
# First try html, then markdown (for test compatibility), then lexical, then plaintext
|
195
|
+
content = nil
|
196
|
+
|
197
|
+
if post["html"] && !post["html"].empty?
|
198
|
+
# Convert HTML to Markdown
|
199
|
+
html_content = post["html"]
|
200
|
+
content = ReverseMarkdown.convert(html_content)
|
201
|
+
source = "html converted to markdown"
|
202
|
+
puts " Note: Converting HTML content to markdown".yellow if @verbose
|
203
|
+
elsif post["plaintext"] && !post["plaintext"].empty?
|
204
|
+
content = post["plaintext"]
|
205
|
+
source = "plaintext"
|
206
|
+
puts " Note: Using plaintext content (HTML/lexical not available)".yellow if @verbose
|
207
|
+
else
|
208
|
+
puts " Warning: No content found for post '#{title}'".yellow
|
209
|
+
next
|
210
|
+
end
|
211
|
+
|
212
|
+
# Replace Ghost URL placeholders if present
|
213
|
+
content = content.gsub(/__GHOST_URL__/, "")
|
214
|
+
|
215
|
+
# Get tags for this post
|
216
|
+
tags = []
|
217
|
+
if ghost_data["db"][0]["data"]["posts_tags"]
|
218
|
+
post_tags = ghost_data["db"][0]["data"]["posts_tags"].select { |pt| pt["post_id"] == post["id"] }
|
219
|
+
|
220
|
+
post_tags.each do |pt|
|
221
|
+
tag = ghost_data["db"][0]["data"]["tags"].find { |t| t["id"] == pt["tag_id"] }
|
222
|
+
tags << tag["name"] if tag
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Get feature image
|
227
|
+
feature_image = post["feature_image"]
|
228
|
+
feature_image&.gsub!(/__GHOST_URL__/, "")
|
229
|
+
|
230
|
+
# Create markdown file with proper frontmatter
|
231
|
+
filename = File.join(output_dir, "#{slug}.md")
|
232
|
+
|
233
|
+
File.open(filename, "w") do |file|
|
234
|
+
file.puts "---"
|
235
|
+
file.puts "title: \"#{title}\""
|
236
|
+
file.puts "published_at: #{published_at}" if published_at
|
237
|
+
file.puts "slug: #{slug}"
|
238
|
+
file.puts "published: #{published}" if published
|
239
|
+
|
240
|
+
# Add meta description if available
|
241
|
+
if post["custom_excerpt"] && !post["custom_excerpt"].empty?
|
242
|
+
file.puts "meta_description: \"#{post['custom_excerpt']}\""
|
243
|
+
end
|
244
|
+
|
245
|
+
# Add tags if available
|
246
|
+
unless tags.empty?
|
247
|
+
file.puts "tags:"
|
248
|
+
tags.each do |tag|
|
249
|
+
file.puts " - #{tag}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
file.puts "---"
|
254
|
+
file.puts
|
255
|
+
file.puts content
|
256
|
+
end
|
257
|
+
|
258
|
+
puts " ✅ Created: #{filename} (from #{source})".green
|
259
|
+
processed_count += 1
|
260
|
+
|
261
|
+
# Upload the post if requested
|
262
|
+
if @token && !@dry_run
|
263
|
+
upload_file(filename)
|
264
|
+
elsif @dry_run
|
265
|
+
puts " DRY RUN: Would upload #{filename}".yellow
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
puts "\n✅ Conversion complete! #{processed_count} markdown files created in #{output_dir}/".green
|
270
|
+
|
271
|
+
rescue JSON::ParserError => e
|
272
|
+
puts "❌ Error parsing Ghost export JSON: #{e.message}".red
|
273
|
+
rescue => e
|
274
|
+
puts "❌ Error processing Ghost export: #{e.message}".red
|
275
|
+
puts e.backtrace.join("\n") if @verbose
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def self.run(args, testing = false)
|
280
|
+
command = args.shift
|
281
|
+
commands = %w[list show create update delete upload download ghost-import help]
|
282
|
+
|
283
|
+
unless commands.include?(command)
|
284
|
+
puts "Unknown command: #{command}".red
|
285
|
+
puts "Available commands: #{commands.join(", ")}"
|
286
|
+
return false if testing
|
287
|
+
exit(1)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Parse command-line options
|
291
|
+
options = {
|
292
|
+
token: ENV["WILLOW_CAMP_API_TOKEN"],
|
293
|
+
directory: ".",
|
294
|
+
file: nil,
|
295
|
+
slug: nil,
|
296
|
+
output: nil,
|
297
|
+
ghost_export: nil,
|
298
|
+
output_dir: "markdown",
|
299
|
+
dry_run: false,
|
300
|
+
verbose: false
|
301
|
+
}
|
302
|
+
|
303
|
+
opt_parser = OptionParser.new do |opts|
|
304
|
+
opts.banner = "Usage: willow-camp COMMAND [options]"
|
305
|
+
opts.separator ""
|
306
|
+
opts.separator "Commands:"
|
307
|
+
opts.separator " list List all posts"
|
308
|
+
opts.separator " show Show a single post by slug"
|
309
|
+
opts.separator " create Create a new post from a Markdown file"
|
310
|
+
opts.separator " update Update an existing post by slug"
|
311
|
+
opts.separator " delete Delete a post by slug"
|
312
|
+
opts.separator " upload Bulk upload posts from a directory"
|
313
|
+
opts.separator " download Download a post to a Markdown file"
|
314
|
+
opts.separator " ghost-import Import posts from a Ghost export file"
|
315
|
+
opts.separator " help Show this help message"
|
316
|
+
opts.separator ""
|
317
|
+
opts.separator "Options:"
|
318
|
+
|
319
|
+
|
320
|
+
|
321
|
+
opts.on("-t", "--token TOKEN", "API Bearer Token") do |token|
|
322
|
+
options[:token] = token
|
323
|
+
end
|
324
|
+
|
325
|
+
opts.on("-d", "--directory DIRECTORY", "Directory containing Markdown files (for upload)") do |dir|
|
326
|
+
options[:directory] = dir
|
327
|
+
end
|
328
|
+
|
329
|
+
opts.on("-f", "--file FILE", "Single Markdown file (for create/update)") do |file|
|
330
|
+
options[:file] = file
|
331
|
+
end
|
332
|
+
|
333
|
+
opts.on("-s", "--slug SLUG", "Post slug (for show/update/delete/download)") do |slug|
|
334
|
+
options[:slug] = slug
|
335
|
+
end
|
336
|
+
|
337
|
+
opts.on("-o", "--output FILE", "Output file (for download)") do |file|
|
338
|
+
options[:output] = file
|
339
|
+
end
|
340
|
+
|
341
|
+
opts.on("-g", "--ghost-export FILE", "Ghost export JSON file") do |file|
|
342
|
+
options[:ghost_export] = file
|
343
|
+
end
|
344
|
+
|
345
|
+
opts.on("--output-dir DIRECTORY", "Output directory for Ghost import (default: 'markdown')") do |dir|
|
346
|
+
options[:output_dir] = dir
|
347
|
+
end
|
348
|
+
|
349
|
+
opts.on("--dry-run", "Show what would be done without making actual changes") do
|
350
|
+
options[:dry_run] = true
|
351
|
+
end
|
352
|
+
|
353
|
+
opts.on("-v", "--verbose", "Show detailed output") do
|
354
|
+
options[:verbose] = true
|
355
|
+
end
|
356
|
+
|
357
|
+
opts.on("-h", "--help", "Show this help message") do
|
358
|
+
puts opts
|
359
|
+
exit
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Special case for help command
|
364
|
+
if command == "help"
|
365
|
+
puts opt_parser
|
366
|
+
exit
|
367
|
+
end
|
368
|
+
|
369
|
+
# Parse the command-line arguments
|
370
|
+
opt_parser.parse!(args)
|
371
|
+
|
372
|
+
# Validate required options for each command
|
373
|
+
case command
|
374
|
+
when "list"
|
375
|
+
# No specific validation needed
|
376
|
+
when "show", "delete", "download"
|
377
|
+
if !options[:slug]
|
378
|
+
puts "Error: Slug is required for #{command} command (use --slug)".red
|
379
|
+
exit 1
|
380
|
+
end
|
381
|
+
when "create"
|
382
|
+
if !options[:file]
|
383
|
+
puts "Error: File path is required for create command (use --file)".red
|
384
|
+
exit 1
|
385
|
+
end
|
386
|
+
when "update"
|
387
|
+
if !options[:slug] || !options[:file]
|
388
|
+
puts "Error: Both slug and file are required for update command (use --slug and --file)".red
|
389
|
+
exit 1
|
390
|
+
end
|
391
|
+
when "upload"
|
392
|
+
# No specific validation needed beyond the common ones
|
393
|
+
when "ghost-import"
|
394
|
+
if !options[:ghost_export]
|
395
|
+
puts "Error: Ghost export file is required for ghost-import command (use --ghost-export)".red
|
396
|
+
exit 1
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Common validation for token (except for dry runs and ghost-import when not uploading)
|
401
|
+
unless options[:token] || options[:dry_run] || (command == "ghost-import" && !options[:token])
|
402
|
+
puts "Error: API token is required (unless using --dry-run)".red
|
403
|
+
puts "Try 'willow-camp help' for more information"
|
404
|
+
exit 1
|
405
|
+
end
|
406
|
+
|
407
|
+
# Create client and execute command
|
408
|
+
begin
|
409
|
+
client = new(options)
|
410
|
+
|
411
|
+
case command
|
412
|
+
when "list"
|
413
|
+
client.list_posts
|
414
|
+
when "show"
|
415
|
+
client.show_post
|
416
|
+
when "create"
|
417
|
+
content = File.read(options[:file])
|
418
|
+
client.upload_file(options[:file])
|
419
|
+
when "update"
|
420
|
+
content = File.read(options[:file])
|
421
|
+
client.update_post(content)
|
422
|
+
when "delete"
|
423
|
+
client.delete_post
|
424
|
+
when "upload"
|
425
|
+
client.upload_all
|
426
|
+
when "download"
|
427
|
+
client.download_post(options[:output])
|
428
|
+
when "ghost-import"
|
429
|
+
client.ghost_import(options[:ghost_export], options[:output_dir])
|
430
|
+
end
|
431
|
+
rescue => e
|
432
|
+
puts "Error: #{e.message}".red
|
433
|
+
exit 1
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
private
|
438
|
+
|
439
|
+
def find_markdown_files
|
440
|
+
Dir.glob(File.join(@directory, "**", "*.md"))
|
441
|
+
end
|
442
|
+
|
443
|
+
def api_request(method, endpoint, data = nil)
|
444
|
+
uri = URI("#{API_URL}#{endpoint}")
|
445
|
+
|
446
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
447
|
+
http.use_ssl = uri.scheme == "https"
|
448
|
+
|
449
|
+
case method
|
450
|
+
when :get
|
451
|
+
request = Net::HTTP::Get.new(uri)
|
452
|
+
when :post
|
453
|
+
request = Net::HTTP::Post.new(uri)
|
454
|
+
when :patch
|
455
|
+
request = Net::HTTP::Patch.new(uri)
|
456
|
+
when :delete
|
457
|
+
request = Net::HTTP::Delete.new(uri)
|
458
|
+
else
|
459
|
+
puts "❌ Unsupported HTTP method: #{method}".red
|
460
|
+
return nil
|
461
|
+
end
|
462
|
+
|
463
|
+
request["Content-Type"] = "application/json"
|
464
|
+
request["Authorization"] = "Bearer #{@token}" if @token
|
465
|
+
request.body = data.to_json if data
|
466
|
+
|
467
|
+
if @verbose
|
468
|
+
puts "🔗 API Endpoint: #{uri} (#{method.to_s.upcase})".blue
|
469
|
+
puts "📄 Request body: #{request.body}" if request.body && @verbose
|
470
|
+
end
|
471
|
+
|
472
|
+
begin
|
473
|
+
response = http.request(request)
|
474
|
+
|
475
|
+
case response.code.to_i
|
476
|
+
when 200..299
|
477
|
+
response
|
478
|
+
else
|
479
|
+
puts "❌ API request failed: HTTP #{response.code}".red
|
480
|
+
puts "Error: #{response.body}".red
|
481
|
+
nil
|
482
|
+
end
|
483
|
+
rescue => e
|
484
|
+
puts "❌ Error making API request: #{e.message}".red
|
485
|
+
nil
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
data/mise.toml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative "lib/willow_camp_cli/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "willow_camp_cli"
|
5
|
+
spec.version = WillowCampCLI::VERSION
|
6
|
+
spec.authors = ["Cassia Scheffer"]
|
7
|
+
spec.email = ["cassia@willow.camp"]
|
8
|
+
|
9
|
+
spec.summary = "Command-line interface for managing blog posts on a Willow Camp "
|
10
|
+
spec.description = "A command-line interface for managing blog posts on a Willow Camp, supporting operations like listing, creating, updating, and deleting posts."
|
11
|
+
spec.homepage = "https://github.com/cassiascheffer/willow_camp_cli"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
14
|
+
|
15
|
+
spec.metadata["source_code_uri"] = "https://github.com/cassiascheffer/willow_camp_cli"
|
16
|
+
spec.metadata["changelog_uri"] = "https://github.com/cassiascheffer/willow_camp_cli/blob/main/CHANGELOG.md"
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = ["willow-camp"]
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_dependency "colorize", "~> 0.8.1"
|
28
|
+
spec.add_dependency "json", "~> 2.0"
|
29
|
+
spec.add_dependency "fileutils", "~> 1.0"
|
30
|
+
spec.add_dependency "reverse_markdown", "~> 2.1"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
34
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: willow_camp_cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.12
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cassia Scheffer
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: colorize
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 0.8.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 0.8.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: json
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: fileutils
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: reverse_markdown
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.1'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.1'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: bundler
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '2.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '2.0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rake
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '13.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '13.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: minitest
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '5.0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '5.0'
|
110
|
+
description: A command-line interface for managing blog posts on a Willow Camp, supporting
|
111
|
+
operations like listing, creating, updating, and deleting posts.
|
112
|
+
email:
|
113
|
+
- cassia@willow.camp
|
114
|
+
executables:
|
115
|
+
- willow-camp
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".github/workflows/gem-push.yml"
|
120
|
+
- ".gitignore"
|
121
|
+
- ".ruby-version"
|
122
|
+
- CHANGELOG.md
|
123
|
+
- Gemfile
|
124
|
+
- Gemfile.lock
|
125
|
+
- README.md
|
126
|
+
- Rakefile
|
127
|
+
- bin/console
|
128
|
+
- bin/setup
|
129
|
+
- exe/willow-camp
|
130
|
+
- lib/willow_camp_cli.rb
|
131
|
+
- lib/willow_camp_cli/cli.rb
|
132
|
+
- lib/willow_camp_cli/version.rb
|
133
|
+
- mise.toml
|
134
|
+
- willow_camp_cli.gemspec
|
135
|
+
homepage: https://github.com/cassiascheffer/willow_camp_cli
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
metadata:
|
139
|
+
source_code_uri: https://github.com/cassiascheffer/willow_camp_cli
|
140
|
+
changelog_uri: https://github.com/cassiascheffer/willow_camp_cli/blob/main/CHANGELOG.md
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: 2.7.0
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubygems_version: 3.6.7
|
156
|
+
specification_version: 4
|
157
|
+
summary: Command-line interface for managing blog posts on a Willow Camp
|
158
|
+
test_files: []
|