ric 0.14.6 → 0.15.0
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/CHANGELOG.md +5 -0
- data/VERSION +1 -1
- data/bin/riclib-test +1 -1
- data/bin/septober +632 -366
- data/bin/septober-cli +650 -0
- data/bin/septober-cli-26 +650 -0
- data/blueprint.yaml +1 -1
- data/lib/ric/conf.rb +1 -1
- data/lib/ric/files.rb +3 -3
- data/lib/ric.rb +12 -9
- metadata +19 -70
- data/Gemfile +0 -23
- data/Makefile +0 -59
- data/Manifest +0 -32
- data/Rakefile +0 -56
- data/TODO +0 -3
- data/ric.gemspec +0 -27
- data/test/sample_conf.yml +0 -17
- data/test/strings_helper.rb +0 -17
- data/test/test_helper.rb +0 -42
- data/var/www/index.html +0 -5
data/bin/septober
CHANGED
|
@@ -1,384 +1,650 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
$
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
#################################################################################################################################################
|
|
111
|
-
class RemoteTodo < ActiveResource::Base
|
|
112
|
-
# With a singleton u can define the accessors of the class (attr accessor for self!)
|
|
113
|
-
#class << self
|
|
114
|
-
# attr_accessor :description # = '-'
|
|
115
|
-
#end
|
|
116
|
-
self.site = "http://septober.example.com/" # uninitialized
|
|
117
|
-
self.element_name = 'todo'
|
|
118
|
-
self.timeout = $opts[:timeout] rescue 6
|
|
119
|
-
#self.priority = 3 # default
|
|
120
|
-
|
|
121
|
-
@@sample_yml_config = <<-SAMPLE_YML_CONFIG
|
|
122
|
-
local: &DEVEL_LOCAL
|
|
123
|
-
# YAML file with septober configuration. For more info: http://septober.heroku.com/
|
|
124
|
-
site: "http://localhost:3000/api/" # or http://septober.heroku.com/api/
|
|
125
|
-
user: guest
|
|
126
|
-
password: guest
|
|
127
|
-
description: My Rails 3 development (guest//guest shuld work for real This is NOT a random thing)
|
|
128
|
-
remote:
|
|
129
|
-
site: http://your_server.example.com/api/
|
|
130
|
-
user: USERNAME
|
|
131
|
-
pass: V3ry_D1ff1cul1
|
|
132
|
-
description: you can use your own server. this is open source. Just remember to say Riccardo you think hes a genius. I dont want money, just fame.
|
|
133
|
-
#########################################################################################################
|
|
134
|
-
|
|
135
|
-
#########################################################################################################
|
|
136
|
-
# YAML file with septober configuration. For more info: http://septober.heroku.com/
|
|
137
|
-
# You just change here:
|
|
138
|
-
#remote:
|
|
139
|
-
# <<: *RICCARDO_HEROKU
|
|
140
|
-
#local:
|
|
141
|
-
# <<: *DEVEL_LOCAL
|
|
142
|
-
# IMPORTANT! It looks like psych gem doesnt expand these magic &HEROKU/*HEROKU so better remove them and simplify with dereferencing :/
|
|
143
|
-
SAMPLE_YML_CONFIG
|
|
144
|
-
|
|
145
|
-
# self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file))
|
|
146
|
-
# :key => OpenSSL::PKey::RSA.new(File.open(pem_file)),
|
|
147
|
-
# :ca_path => "/path/to/OpenSSL/formatted/CA_Certs",
|
|
148
|
-
# :verify_mode => OpenSSL::SSL::VERIFY_PEER}
|
|
149
|
-
|
|
150
|
-
def self.import_config(file=nil)
|
|
151
|
-
file ||= $opts[:config_file]
|
|
152
|
-
real_file = File.expand_path file
|
|
153
|
-
deb "Importing config from #{file}.."
|
|
154
|
-
if File.exists?(real_file)
|
|
155
|
-
#puts "TODO import from #{file} (local=#{$opts[:local]})"
|
|
156
|
-
whole_hash = YAML.load_file(real_file) # rescue { :error => $! }
|
|
157
|
-
|
|
158
|
-
sub_conf = $opts[:subconf].nil? ?
|
|
159
|
-
($opts[:local] ? 'local' : 'remote' ) :
|
|
160
|
-
$opts[:subconf]
|
|
161
|
-
if not whole_hash.keys.include?(sub_conf)
|
|
162
|
-
puts "SubConf non existing: #{red(sub_conf.to_s)}. Available sub_confs: #{yellow whole_hash.keys.join(', ')}. Note that 'remote' is the default, 'local' comes from '-l 'and you can set anything with '-s whatevs' :)"
|
|
163
|
-
raise "SubConf not existing: #{(sub_conf)}"
|
|
164
|
-
end
|
|
165
|
-
puts "SubConf: #{yellow(sub_conf.to_s)}. Available sub_confs (you can enforce them with -s new as of 2022!): #{yellow whole_hash.keys.join(', ')}"
|
|
166
|
-
conf_hash = whole_hash[sub_conf] rescue nil
|
|
167
|
-
self.site = conf_hash['site']
|
|
168
|
-
self.user = conf_hash['user']
|
|
169
|
-
self.password = conf_hash['password']
|
|
170
|
-
self.description = conf_hash['description'] rescue '-'
|
|
171
|
-
deb "Conf imported successuflly: #{blue self.user}@#{self.site}"
|
|
172
|
-
else
|
|
173
|
-
pred "Missing file: #{file} !!! Try to do the following:"
|
|
174
|
-
puts "cat > #{file}\n#{@@sample_yml_config rescue "Err#{$!}" }"
|
|
175
|
-
exit 79
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# If Ruby < 3.4 is first in PATH (e.g. system Ruby 3.3 on Linux), re-exec into rbenv Ruby 3.4.5+ if available
|
|
5
|
+
if RUBY_VERSION < "3.4.0"
|
|
6
|
+
rbenv_ruby = File.expand_path("~/.rbenv/versions/3.4.5/bin/ruby")
|
|
7
|
+
exec(rbenv_ruby, $PROGRAM_NAME, *ARGV) if File.executable?(rbenv_ruby)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require "optparse"
|
|
11
|
+
require "net/http"
|
|
12
|
+
require "uri"
|
|
13
|
+
require "json"
|
|
14
|
+
require "yaml"
|
|
15
|
+
require "socket"
|
|
16
|
+
require "base64"
|
|
17
|
+
require "date"
|
|
18
|
+
require "time"
|
|
19
|
+
|
|
20
|
+
PROG_VER = "2.6.1"
|
|
21
|
+
|
|
22
|
+
# Optional ric gem support; built-in ANSI fallback so CLI is 100% zero-dependency on Ruby 3.4.5+
|
|
23
|
+
$colors_active = $stdout.tty?
|
|
24
|
+
def colorize(code, str)
|
|
25
|
+
return str.to_s unless $colors_active
|
|
26
|
+
"\e[#{code}m#{str}\e[0m"
|
|
27
|
+
end
|
|
28
|
+
def red(s) = colorize(31, s)
|
|
29
|
+
def green(s) = colorize(32, s)
|
|
30
|
+
def yellow(s) = colorize(33, s)
|
|
31
|
+
def blue(s) = colorize(34, s)
|
|
32
|
+
def purple(s) = colorize(35, s)
|
|
33
|
+
def cyan(s) = colorize(36, s)
|
|
34
|
+
def gray(s) = colorize(90, s)
|
|
35
|
+
def bold(s) = colorize(1, s)
|
|
36
|
+
|
|
37
|
+
CONFIG_PATH = File.expand_path("~/.septober.yml")
|
|
38
|
+
LOCAL_CONFIG_PATH = File.expand_path("~/.septober.local.yml")
|
|
39
|
+
|
|
40
|
+
AGENT_ICONS = {
|
|
41
|
+
"ermete" => "🚛",
|
|
42
|
+
"lobby" => "🦞",
|
|
43
|
+
"pux" => "🐾",
|
|
44
|
+
"freevola" => "🧚♀️",
|
|
45
|
+
"derek" => "🖥️"
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
AGENT_ENV_FILES = {
|
|
49
|
+
"lobby" => "~/.openclaw/.env",
|
|
50
|
+
"ermete" => "~/.hermes/.env",
|
|
51
|
+
"pux" => "~/.openclaw/.env",
|
|
52
|
+
"freevola" => "~/.hermes-freevola/.env"
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
def parse_dotenv_file(path)
|
|
56
|
+
res = {}
|
|
57
|
+
expanded = File.expand_path(path.to_s)
|
|
58
|
+
return res unless File.file?(expanded)
|
|
59
|
+
|
|
60
|
+
File.readlines(expanded, chomp: true).each do |line|
|
|
61
|
+
stripped = line.strip
|
|
62
|
+
next if stripped.empty? || stripped.start_with?("#")
|
|
63
|
+
next unless stripped.include?("=")
|
|
64
|
+
|
|
65
|
+
k, v = stripped.split("=", 2)
|
|
66
|
+
res[k.strip] = v.strip.gsub(/\A['"]|['"]\z/, "")
|
|
67
|
+
end
|
|
68
|
+
res
|
|
69
|
+
rescue StandardError
|
|
70
|
+
res
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def load_config(conf_path: nil, target_env: nil, target_agent: nil, cli_user: nil, cli_password: nil)
|
|
74
|
+
resolved_path = conf_path
|
|
75
|
+
if resolved_path.nil? || resolved_path.empty?
|
|
76
|
+
candidates = [
|
|
77
|
+
ENV["SEPTOBER_CONFIG"],
|
|
78
|
+
LOCAL_CONFIG_PATH,
|
|
79
|
+
CONFIG_PATH,
|
|
80
|
+
File.expand_path("~/git/gic/.septober.yml"),
|
|
81
|
+
File.expand_path("~/git/gic25/.septober.yml"),
|
|
82
|
+
File.expand_path("../.septober.yml", __dir__)
|
|
83
|
+
].compact
|
|
84
|
+
resolved_path = candidates.find { |p| File.file?(p) }
|
|
85
|
+
else
|
|
86
|
+
resolved_path = File.expand_path(resolved_path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless resolved_path && File.file?(resolved_path)
|
|
90
|
+
warn red("Error: Config file not found (checked #{LOCAL_CONFIG_PATH}, #{CONFIG_PATH})")
|
|
91
|
+
exit 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
begin
|
|
95
|
+
raw_yaml = File.read(resolved_path)
|
|
96
|
+
data = YAML.safe_load(raw_yaml, aliases: true, permitted_classes: [Symbol, Date, Time]) || {}
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
warn red("Error reading YAML config #{resolved_path}: #{e.message}")
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
env_name = target_env || ENV["SEPTOBER_ENV"]
|
|
103
|
+
if env_name.nil? || env_name.empty?
|
|
104
|
+
%w[septober production remote local septober_local].each do |cand|
|
|
105
|
+
if data[cand].is_a?(Hash)
|
|
106
|
+
env_name = cand
|
|
107
|
+
break
|
|
176
108
|
end
|
|
177
109
|
end
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if (env_name.nil? || !data[env_name].is_a?(Hash)) && target_env == "local" && data["septober_local"].is_a?(Hash)
|
|
113
|
+
env_name = "septober_local"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if env_name.nil? || !data[env_name].is_a?(Hash)
|
|
117
|
+
pair = data.find { |_k, v| v.is_a?(Hash) && (v["site"] || v[:site]) }
|
|
118
|
+
env_name = pair[0] if pair
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
unless env_name && data[env_name].is_a?(Hash)
|
|
122
|
+
warn red("Error: Could not find valid environment '#{target_env || 'septober'}' in #{resolved_path}")
|
|
123
|
+
warn "Available environments: #{data.keys.join(', ')}"
|
|
124
|
+
exit 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
cfg = data[env_name]
|
|
128
|
+
site = ENV["SEPTOBER_SITE"] || cfg["site"] || cfg[:site] || "http://localhost:8080/api/"
|
|
129
|
+
site = "#{site.to_s.sub(%r{/+\z}, '')}/"
|
|
130
|
+
user = cli_user || ENV["SEPTOBER_USER"] || cfg["user"] || cfg[:user] || "guest"
|
|
131
|
+
password = cli_password || ENV["SEPTOBER_PASSWORD"] || (cfg["password"] || cfg[:password] || "guest").to_s
|
|
132
|
+
agent_icon = nil
|
|
133
|
+
|
|
134
|
+
# Resolve agent profile
|
|
135
|
+
agent_name = target_agent || ENV["SEPTOBER_AGENT"]
|
|
136
|
+
if (agent_name.nil? || agent_name.empty?) && ENV["SEPTOBER_USER"].nil? && cli_user.nil?
|
|
137
|
+
if ENV["HARNESS_NAME"] == "Hermes" || ENV["HARNESS_NICKNAME"] == "Ermete Bottazzi"
|
|
138
|
+
agent_name = "ermete"
|
|
139
|
+
elsif ENV["HARNESS_NAME"] == "Lobby" || ENV["HARNESS_NICKNAME"] == "Lobby"
|
|
140
|
+
agent_name = "lobby"
|
|
141
|
+
elsif ENV["OPENCLAW_AGENT"] && !ENV["OPENCLAW_AGENT"].empty?
|
|
142
|
+
agent_name = ENV["OPENCLAW_AGENT"]
|
|
143
|
+
elsif Dir.pwd.include?(".openclaw")
|
|
144
|
+
agent_name = "lobby"
|
|
145
|
+
elsif Dir.pwd.include?(".hermes")
|
|
146
|
+
agent_name = "ermete"
|
|
185
147
|
end
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if agent_name && !agent_name.empty?
|
|
151
|
+
agent_icon = AGENT_ICONS[agent_name]
|
|
152
|
+
yaml_agents = data["agents"].is_a?(Hash) ? data["agents"][agent_name] : nil
|
|
153
|
+
env_file = AGENT_ENV_FILES[agent_name] ? File.expand_path(AGENT_ENV_FILES[agent_name]) : nil
|
|
154
|
+
|
|
155
|
+
agent_user = nil
|
|
156
|
+
agent_pass = nil
|
|
157
|
+
|
|
158
|
+
if ENV["SEPTOBER_AGENT"] == agent_name && ENV["SEPTOBER_USER"] && ENV["SEPTOBER_PASSWORD"]
|
|
159
|
+
agent_user = ENV["SEPTOBER_USER"]
|
|
160
|
+
agent_pass = ENV["SEPTOBER_PASSWORD"]
|
|
161
|
+
elsif env_file && File.file?(env_file)
|
|
162
|
+
dotenv = parse_dotenv_file(env_file)
|
|
163
|
+
agent_user = dotenv["SEPTOBER_USER"]
|
|
164
|
+
agent_pass = dotenv["SEPTOBER_PASSWORD"]
|
|
165
|
+
elsif yaml_agents.is_a?(Hash)
|
|
166
|
+
agent_user = yaml_agents["user"] || yaml_agents[:user]
|
|
167
|
+
agent_pass = (yaml_agents["password"] || yaml_agents[:password]).to_s
|
|
168
|
+
agent_icon ||= yaml_agents["icon"] || yaml_agents[:icon]
|
|
189
169
|
end
|
|
190
170
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
171
|
+
if agent_user && agent_pass && !agent_user.empty? && !agent_pass.empty?
|
|
172
|
+
user = agent_user
|
|
173
|
+
password = agent_pass
|
|
174
|
+
else
|
|
175
|
+
hint_file = env_file || "~/.#{agent_name}/.env"
|
|
176
|
+
warn "\n❌ Error: ENV credentials missing for agent '#{agent_name}'!"
|
|
177
|
+
warn " Configure SEPTOBER_USER, SEPTOBER_PASSWORD, SEPTOBER_AGENT in #{hint_file} or environment."
|
|
178
|
+
exit 1
|
|
195
179
|
end
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
"env" => env_name,
|
|
184
|
+
"site" => site,
|
|
185
|
+
"user" => user,
|
|
186
|
+
"password" => password,
|
|
187
|
+
"agent" => agent_name,
|
|
188
|
+
"agent_icon" => agent_icon,
|
|
189
|
+
"description" => cfg["description"] || "",
|
|
190
|
+
"config_path" => resolved_path
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def make_request(cfg, endpoint, method: "GET", body: nil, timeout: 25)
|
|
195
|
+
base_uri = URI.join(cfg["site"], endpoint.to_s.sub(%r{\A/+}, ""))
|
|
196
|
+
http = Net::HTTP.new(base_uri.host, base_uri.port)
|
|
197
|
+
http.use_ssl = (base_uri.scheme == "https")
|
|
198
|
+
http.open_timeout = timeout
|
|
199
|
+
http.read_timeout = timeout
|
|
200
|
+
|
|
201
|
+
req_class = case method.to_s.upcase
|
|
202
|
+
when "GET" then Net::HTTP::Get
|
|
203
|
+
when "POST" then Net::HTTP::Post
|
|
204
|
+
when "PUT" then Net::HTTP::Put
|
|
205
|
+
when "DELETE" then Net::HTTP::Delete
|
|
206
|
+
else raise "Unsupported HTTP method: #{method}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
req = req_class.new(base_uri.request_uri)
|
|
210
|
+
req.basic_auth(cfg["user"].to_s, cfg["password"].to_s)
|
|
211
|
+
req["Accept"] = "application/json"
|
|
212
|
+
req["User-Agent"] = "septober-cli/#{PROG_VER} (Ruby/#{RUBY_VERSION})"
|
|
213
|
+
|
|
214
|
+
if body
|
|
215
|
+
req["Content-Type"] = "application/json"
|
|
216
|
+
req.body = JSON.generate(body)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
resp = http.request(req)
|
|
220
|
+
code = resp.code.to_i
|
|
221
|
+
raw_body = resp.body.to_s
|
|
222
|
+
|
|
223
|
+
if code >= 200 && code < 300
|
|
224
|
+
return { "status" => code, "data" => nil } if raw_body.strip.empty?
|
|
225
|
+
parsed = begin
|
|
226
|
+
JSON.parse(raw_body)
|
|
227
|
+
rescue StandardError
|
|
228
|
+
raw_body
|
|
207
229
|
end
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
when '4'; return '!'
|
|
215
|
-
when '5'; return '!!'
|
|
216
|
-
end
|
|
217
|
-
return "'?' #{priority}"
|
|
230
|
+
{ "status" => code, "data" => parsed }
|
|
231
|
+
else
|
|
232
|
+
parsed_err = begin
|
|
233
|
+
JSON.parse(raw_body)
|
|
234
|
+
rescue StandardError
|
|
235
|
+
raw_body
|
|
218
236
|
end
|
|
237
|
+
{ "status" => code, "error" => parsed_err }
|
|
238
|
+
end
|
|
239
|
+
rescue StandardError => e
|
|
240
|
+
{ "status" => 0, "error" => e.message }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def format_priority(prio)
|
|
244
|
+
case prio.to_i
|
|
245
|
+
when 5 then "🔴 P0/Urgent"
|
|
246
|
+
when 4 then "🟠 P1/High"
|
|
247
|
+
when 3 then "🟡 P2/Med"
|
|
248
|
+
when 2 then "🟢 P3/Low"
|
|
249
|
+
when 1 then "⚪️ P4/Backlog"
|
|
250
|
+
else "P#{prio}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def extract_todo_id(raw_arg)
|
|
255
|
+
str = raw_arg.to_s.strip
|
|
256
|
+
if str =~ %r{/todos/(\d+)}
|
|
257
|
+
Regexp.last_match(1).to_i
|
|
258
|
+
elsif str =~ /\A#?(\d+)\z/
|
|
259
|
+
Regexp.last_match(1).to_i
|
|
260
|
+
else
|
|
261
|
+
warn red("Error: Invalid Todo ID or URL '#{raw_arg}'. Expected an integer ID (e.g. 235) or URL (.../todos/235).")
|
|
262
|
+
exit 1
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def local_hostname
|
|
267
|
+
Socket.gethostname.split(".").first
|
|
268
|
+
rescue StandardError
|
|
269
|
+
"unknown-host"
|
|
270
|
+
end
|
|
219
271
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
272
|
+
def harness_co_author(agent_name = nil)
|
|
273
|
+
return ENV["CARLESSIAN_HARNESS_CO_AUTHOR"] if ENV["CARLESSIAN_HARNESS_CO_AUTHOR"]
|
|
274
|
+
|
|
275
|
+
env_file = AGENT_ENV_FILES[agent_name]
|
|
276
|
+
if env_file
|
|
277
|
+
dotenv = parse_dotenv_file(env_file)
|
|
278
|
+
return dotenv["CARLESSIAN_HARNESS_CO_AUTHOR"] if dotenv["CARLESSIAN_HARNESS_CO_AUTHOR"]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
case agent_name
|
|
282
|
+
when "ermete" then "Ermete Bottazzi <ermete.bottazzi@romagnatrucking.it>"
|
|
283
|
+
when "lobby" then "Lobby <lobby@openclaw.local>"
|
|
284
|
+
when "pux" then "Pux <pux@openclaw.local>"
|
|
285
|
+
when "freevola" then "Freevola <freevola@pupurabbux.local>"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def harness_nickname(agent_name = nil)
|
|
290
|
+
return ENV["HARNESS_NICKNAME"] if ENV["HARNESS_NICKNAME"]
|
|
291
|
+
return ENV["HARNESS_NAME"] if ENV["HARNESS_NAME"]
|
|
292
|
+
|
|
293
|
+
env_file = AGENT_ENV_FILES[agent_name]
|
|
294
|
+
if env_file
|
|
295
|
+
dotenv = parse_dotenv_file(env_file)
|
|
296
|
+
return dotenv["HARNESS_NICKNAME"] || dotenv["HARNESS_NAME"] if dotenv["HARNESS_NICKNAME"] || dotenv["HARNESS_NAME"]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
case agent_name
|
|
300
|
+
when "ermete" then "Ermete Bottazzi"
|
|
301
|
+
when "lobby" then "Lobby"
|
|
302
|
+
when "pux" then "Pux"
|
|
303
|
+
when "freevola" then "Freevola"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def format_agent_badge(todo)
|
|
308
|
+
u = todo["user"]
|
|
309
|
+
if u.is_a?(Hash) && u["is_agent"]
|
|
310
|
+
icon = u["agent_icon"]
|
|
311
|
+
uname = u["username"].to_s
|
|
312
|
+
agent_key = uname.include?(".") ? uname.split(".").last.downcase : uname.downcase
|
|
313
|
+
icon = AGENT_ICONS.fetch(agent_key, "🤖") if icon.nil? || icon.empty? || icon == "?"
|
|
314
|
+
short_name = agent_key.capitalize
|
|
315
|
+
return " [#{icon} #{short_name}]"
|
|
316
|
+
elsif todo["source"].to_s.include?("cli-26/")
|
|
317
|
+
s = todo["source"].to_s
|
|
318
|
+
agent_part = s.split("cli-26/", 2)[1].to_s.split("@").first.to_s
|
|
319
|
+
icon = agent_part.include?("Ermete") ? "🚛" : "🤖"
|
|
320
|
+
return " [#{icon} #{agent_part}]" unless agent_part.empty?
|
|
321
|
+
end
|
|
322
|
+
""
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def cmd_list(opts, cfg)
|
|
326
|
+
res = make_request(cfg, "todos.json", timeout: opts[:timeout])
|
|
327
|
+
if res["error"]
|
|
328
|
+
warn red("Error (#{res['status']}): #{res['error']}")
|
|
329
|
+
exit 1
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
todos = res["data"] || []
|
|
333
|
+
if opts[:json]
|
|
334
|
+
puts JSON.pretty_generate(todos)
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
puts "\n#{bold("🚛 Septober [#{cfg['env']}]")} (#{cyan(cfg['site'])}) - User: #{yellow(cfg['user'])} #{gray("[Ruby #{RUBY_VERSION}]")}"
|
|
339
|
+
puts "=" * 75
|
|
340
|
+
|
|
341
|
+
shown = 0
|
|
342
|
+
todos.each do |raw_t|
|
|
343
|
+
t = raw_t.is_a?(Hash) && raw_t["todo"].is_a?(Hash) ? raw_t["todo"] : raw_t
|
|
344
|
+
next unless t.is_a?(Hash)
|
|
345
|
+
|
|
346
|
+
active = t.key?("active") ? t["active"] : true
|
|
347
|
+
next if !opts[:all] && !active
|
|
348
|
+
|
|
349
|
+
tags = Array(t["tag_list"])
|
|
350
|
+
if opts[:tag]
|
|
351
|
+
wanted = opts[:tag].to_s.sub(/\A[@#]/, "").downcase
|
|
352
|
+
next unless tags.map { |x| x.to_s.downcase }.include?(wanted)
|
|
228
353
|
end
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
354
|
+
|
|
355
|
+
badge = format_agent_badge(t)
|
|
356
|
+
if opts[:filter_agent]
|
|
357
|
+
fa = opts[:filter_agent].to_s.downcase
|
|
358
|
+
uname = t.dig("user", "username").to_s.downcase
|
|
359
|
+
src = t["source"].to_s.downcase
|
|
360
|
+
next if !uname.include?(fa) && !src.include?(fa) && !badge.downcase.include?(fa)
|
|
232
361
|
end
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
362
|
+
|
|
363
|
+
tid = t["id"]
|
|
364
|
+
name = t["name"] || "Untitled"
|
|
365
|
+
prio = format_priority(t["priority"] || 3)
|
|
366
|
+
due = t["due"] || "No date"
|
|
367
|
+
where = t["where"]
|
|
368
|
+
url = t["url"]
|
|
369
|
+
desc = t["description"].to_s.strip
|
|
370
|
+
state_icon = active ? "⏳" : "✅"
|
|
371
|
+
|
|
372
|
+
puts "#{state_icon} [#{bold("##{tid}")}] #{prio} | Due: #{due}#{badge}"
|
|
373
|
+
puts " #{bold('Title:')} #{name}"
|
|
374
|
+
puts " #{bold('Where:')} #{where}" if where && !where.to_s.empty?
|
|
375
|
+
puts " #{bold('URL:')} #{cyan(url)}" if url && !url.to_s.empty?
|
|
376
|
+
puts " #{bold('Tags:')} #{tags.map { |tg| "##{tg}" }.join(' ')}" unless tags.empty?
|
|
377
|
+
if !desc.empty?
|
|
378
|
+
first_line = desc.lines.first.to_s.strip
|
|
379
|
+
snippet = first_line.length > 90 ? "#{first_line[0..87]}..." : first_line
|
|
380
|
+
puts " #{bold('Ctx:')} #{gray(snippet)}"
|
|
236
381
|
end
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
382
|
+
puts "-" * 75
|
|
383
|
+
|
|
384
|
+
shown += 1
|
|
385
|
+
break if opts[:limit] && shown >= opts[:limit]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
puts "No matching todos found." if shown == 0
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def cmd_show(id_arg, opts, cfg)
|
|
392
|
+
todo_id = extract_todo_id(id_arg)
|
|
393
|
+
res = make_request(cfg, "todos/#{todo_id}.json", timeout: opts[:timeout])
|
|
394
|
+
if res["error"]
|
|
395
|
+
warn red("Error fetching todo ##{todo_id} (#{res['status']}): #{res['error']}")
|
|
396
|
+
exit 1
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
raw_t = res["data"]
|
|
400
|
+
t = raw_t.is_a?(Hash) && raw_t["todo"].is_a?(Hash) ? raw_t["todo"] : raw_t
|
|
401
|
+
if t.nil? || !t.is_a?(Hash) || t["id"].nil?
|
|
402
|
+
warn red("❌ Error: Todo ##{todo_id} not found (or not visible to user '#{cfg['user']}' on #{cfg['site']}).")
|
|
403
|
+
exit 1
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
if opts[:json]
|
|
407
|
+
puts JSON.pretty_generate(t)
|
|
408
|
+
return
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
u = t["user"]
|
|
412
|
+
agent_str = ""
|
|
413
|
+
if u.is_a?(Hash) && u["is_agent"]
|
|
414
|
+
icon = u["agent_icon"]
|
|
415
|
+
uname = u["username"].to_s
|
|
416
|
+
agent_key = uname.include?(".") ? uname.split(".").last.downcase : uname.downcase
|
|
417
|
+
icon = AGENT_ICONS.fetch(agent_key, "🤖") if icon.nil? || icon.empty? || icon == "?"
|
|
418
|
+
agent_str = "\nAgent: #{icon} #{uname} (host: #{u['agent_host'] || 'unknown'})"
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
web_base = cfg["site"].to_s.sub(%r{/api/?\z}, "")
|
|
422
|
+
web_link = "#{web_base}/todos/#{t['id']}"
|
|
423
|
+
|
|
424
|
+
puts "\n📋 #{bold("Todo ##{t['id']} - #{t['name']}")}\n" + ("=" * 60)
|
|
425
|
+
puts "Status: #{t['active'] ? green('Active') : gray('Done')}"
|
|
426
|
+
puts "Priority: #{format_priority(t['priority'] || 3)}"
|
|
427
|
+
puts "Project: #{t.dig('project', 'name') || t['project_id']}" if t["project"] || t["project_id"]
|
|
428
|
+
puts "Due: #{t['due'] || 'None'}"
|
|
429
|
+
puts "Where: #{t['where'] || 'None'}"
|
|
430
|
+
puts "URL: #{t['url'] || 'None'}"
|
|
431
|
+
puts "Web Link: #{cyan(web_link)}"
|
|
432
|
+
puts "Tags: #{Array(t['tag_list']).inspect}#{agent_str}"
|
|
433
|
+
puts "\n#{bold('Context / Description:')}\n#{t['description'] || 'None'}\n"
|
|
434
|
+
puts "-" * 60
|
|
435
|
+
if t["active"]
|
|
436
|
+
puts "💡 #{bold('Mark done:')} #{green("septober-cli done #{t['id']}")}"
|
|
437
|
+
else
|
|
438
|
+
puts "💡 #{bold('Reopen todo:')} #{yellow("septober-cli undone #{t['id']}")}"
|
|
439
|
+
end
|
|
440
|
+
puts "💡 #{bold('Toggle state:')} #{cyan("septober-cli toggle #{t['id']}")}"
|
|
441
|
+
puts "💡 #{bold('Delete todo:')} #{red("septober-cli delete #{t['id']}")}\n\n"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def cmd_add(positional_args, opts, cfg)
|
|
445
|
+
title = opts[:title] || positional_args.join(" ").strip
|
|
446
|
+
if title.nil? || title.empty?
|
|
447
|
+
warn red("Error: Todo title is required (pass --title '...' or positional text).")
|
|
448
|
+
exit 1
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
hostname = local_hostname
|
|
452
|
+
agent_id = cfg["agent"]
|
|
453
|
+
co_author = harness_co_author(agent_id)
|
|
454
|
+
harness_nick = harness_nickname(agent_id)
|
|
455
|
+
|
|
456
|
+
where_val = opts[:where] || hostname
|
|
457
|
+
source_val = harness_nick ? "cli-26/#{harness_nick}@#{hostname}" : "cli-26@#{hostname}"
|
|
458
|
+
|
|
459
|
+
desc = opts[:description]
|
|
460
|
+
if co_author && (desc.nil? || !desc.include?("Co-authored-by"))
|
|
461
|
+
co_note = "Collected via CLI on #{hostname} (Co-authored-by: #{co_author})"
|
|
462
|
+
desc = desc && !desc.empty? ? "#{desc}\n\n#{co_note}" : co_note
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
proj = opts[:project] || "septober"
|
|
466
|
+
todo_payload = {
|
|
467
|
+
"name" => title,
|
|
468
|
+
"priority" => opts[:priority] || 3,
|
|
469
|
+
"source" => source_val,
|
|
470
|
+
"project" => proj,
|
|
471
|
+
"where" => where_val
|
|
472
|
+
}
|
|
473
|
+
todo_payload["description"] = desc if desc && !desc.empty?
|
|
474
|
+
todo_payload["url"] = opts[:url] if opts[:url]
|
|
475
|
+
todo_payload["due"] = opts[:due] if opts[:due]
|
|
476
|
+
|
|
477
|
+
if opts[:tags] && !opts[:tags].empty?
|
|
478
|
+
tag_items = opts[:tags].split(",").map { |x| x.strip.sub(/\A[#@]+/, "") }.reject(&:empty?)
|
|
479
|
+
unless tag_items.empty?
|
|
480
|
+
todo_payload["tags"] = tag_items
|
|
481
|
+
todo_payload["name"] = "#{title} #{tag_items.map { |t| "@#{t}" }.join(' ')}"
|
|
251
482
|
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
res = make_request(cfg, "todos.json", method: "POST", body: todo_payload, timeout: opts[:timeout])
|
|
486
|
+
if res["error"]
|
|
487
|
+
warn red("Error creating todo (#{res['status']}): #{res['error']}")
|
|
488
|
+
exit 1
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
raw_data = res["data"]
|
|
492
|
+
new_id = if raw_data.is_a?(Hash)
|
|
493
|
+
(raw_data["todo"] || raw_data)["id"] || "?"
|
|
494
|
+
elsif raw_data.to_s =~ /\A\d+\z/
|
|
495
|
+
raw_data.to_s
|
|
496
|
+
else
|
|
497
|
+
"new"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
web_base = cfg["site"].to_s.sub(%r{/api/?\z}, "")
|
|
501
|
+
puts "✅ #{green("Todo ##{new_id} created successfully!")}"
|
|
502
|
+
puts " Title: #{title}"
|
|
503
|
+
puts " Priority: #{format_priority(opts[:priority] || 3)}"
|
|
504
|
+
puts " Where: 📍 #{where_val}" if where_val
|
|
505
|
+
puts " URL: #{opts[:url]}" if opts[:url]
|
|
506
|
+
puts " Web Link: #{cyan("#{web_base}/todos/#{new_id}")}" if new_id.to_s =~ /\A\d+\z/
|
|
507
|
+
puts " Context: #{desc}" if desc
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def cmd_action(action_name, id_arg, opts, cfg)
|
|
511
|
+
todo_id = extract_todo_id(id_arg)
|
|
512
|
+
endpoint = "todos/#{todo_id}/#{action_name}.json"
|
|
513
|
+
res = make_request(cfg, endpoint, method: "PUT", timeout: opts[:timeout])
|
|
514
|
+
if res["error"]
|
|
515
|
+
warn red("Error running '#{action_name}' on todo ##{todo_id} (#{res['status']}): #{res['error']}")
|
|
516
|
+
exit 1
|
|
517
|
+
end
|
|
518
|
+
puts "✅ #{green("Todo ##{todo_id}: #{action_name.upcase} completed!")}"
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def main(argv = ARGV)
|
|
522
|
+
opts = {
|
|
523
|
+
conf: nil,
|
|
524
|
+
env: nil,
|
|
525
|
+
agent: nil,
|
|
526
|
+
local: false,
|
|
527
|
+
timeout: 25,
|
|
528
|
+
all: false,
|
|
529
|
+
limit: 20,
|
|
530
|
+
tag: nil,
|
|
531
|
+
json: false,
|
|
532
|
+
title: nil,
|
|
533
|
+
description: nil,
|
|
534
|
+
url: nil,
|
|
535
|
+
where: nil,
|
|
536
|
+
priority: 3,
|
|
537
|
+
project: "septober",
|
|
538
|
+
due: nil,
|
|
539
|
+
tags: nil,
|
|
540
|
+
user: nil,
|
|
541
|
+
password: nil,
|
|
542
|
+
verbose: false,
|
|
543
|
+
debug: false
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
parser = OptionParser.new do |o|
|
|
547
|
+
o.banner = <<~BANNER
|
|
548
|
+
🚛 Septober CLI v#{PROG_VER} (Ruby #{RUBY_VERSION})
|
|
549
|
+
Usage:
|
|
550
|
+
#{File.basename($PROGRAM_NAME)} [options] list [--all] [--limit N] [--tag TAG] [--json]
|
|
551
|
+
#{File.basename($PROGRAM_NAME)} [options] show <ID_OR_URL> [--json]
|
|
552
|
+
#{File.basename($PROGRAM_NAME)} [options] add <TITLE> [--description "..."] [--priority 1-5] [--where "..."] [--url "..."] [--tags t1,t2]
|
|
553
|
+
#{File.basename($PROGRAM_NAME)} [options] done <ID_OR_URL>
|
|
554
|
+
#{File.basename($PROGRAM_NAME)} [options] toggle <ID_OR_URL>
|
|
555
|
+
#{File.basename($PROGRAM_NAME)} [options] delete <ID_OR_URL>
|
|
556
|
+
|
|
557
|
+
Options:
|
|
558
|
+
BANNER
|
|
559
|
+
|
|
560
|
+
o.on("-c", "--conf FILE", "--config FILE", "Path to .septober.yml") { |v| opts[:conf] = v }
|
|
561
|
+
o.on("-e", "-s", "--env ENV", "--subconf ENV", "Target environment (e.g. septober, local)") { |v| opts[:env] = v }
|
|
562
|
+
o.on("--agent AGENT", "Agent profile (e.g. ermete, lobby, pux)") { |v| opts[:agent] = v; opts[:filter_agent] = v }
|
|
563
|
+
o.on("-l", "--local", "Force local environment") { opts[:local] = true }
|
|
564
|
+
o.on("-u", "--user USER", "Override Septober username/email") { |v| opts[:user] = v }
|
|
565
|
+
o.on("-p", "--password PASS", "Override Septober password") { |v| opts[:password] = v }
|
|
566
|
+
o.on("-t", "--timeout SEC", Integer, "HTTP timeout in seconds (default: 25)") { |v| opts[:timeout] = v }
|
|
567
|
+
o.on("-n", "--no-colors", "Disable ANSI colors") { $colors_active = false }
|
|
568
|
+
o.on("-a", "--all", "Include completed todos in list") { opts[:all] = true }
|
|
569
|
+
o.on("--limit N", Integer, "Max todos to show in list (default: 20)") { |v| opts[:limit] = v }
|
|
570
|
+
o.on("--tag TAG", "Filter list by tag") { |v| opts[:tag] = v }
|
|
571
|
+
o.on("--json", "Output raw JSON") { opts[:json] = true }
|
|
572
|
+
o.on("--title TITLE", "Todo title for add") { |v| opts[:title] = v }
|
|
573
|
+
o.on("--description DESC", "--desc DESC", "Rich narrative context for add") { |v| opts[:description] = v }
|
|
574
|
+
o.on("--url URL", "Primary reference URL for add") { |v| opts[:url] = v }
|
|
575
|
+
o.on("--where WHERE", "Location/event anchor for add") { |v| opts[:where] = v }
|
|
576
|
+
o.on("--priority PRIO", Integer, "Priority 1 (low) to 5 (urgent)") { |v| opts[:priority] = v }
|
|
577
|
+
o.on("--project PROJ", "Project name (default: septober)") { |v| opts[:project] = v }
|
|
578
|
+
o.on("--due DUE", "Due date (YYYY-MM-DD or today/tomorrow)") { |v| opts[:due] = v }
|
|
579
|
+
o.on("--tags TAGS", "Comma-separated tags (e.g. lavoro,google,tech)") { |v| opts[:tags] = v }
|
|
580
|
+
o.on("-d", "--debug", "Enable debug output") { opts[:debug] = true }
|
|
581
|
+
o.on("-v", "--verbose", "Enable verbose output") { opts[:verbose] = true }
|
|
582
|
+
o.on("-V", "--version", "Show version") do
|
|
583
|
+
puts "septober-cli v#{PROG_VER} (ruby #{RUBY_VERSION})"
|
|
584
|
+
exit 0
|
|
303
585
|
end
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def todo_show(id)
|
|
309
|
-
todo = RemoteTodo.find(id)
|
|
310
|
-
puts bannerino("Show(#{id})")
|
|
311
|
-
puts( todo.show_full_entry )
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def todo_done(id,done_or_undone=true)
|
|
315
|
-
todo_generic_put(id,:done)
|
|
316
|
-
end
|
|
317
|
-
def todo_toggle(id,done_or_undone=true)
|
|
318
|
-
todo_generic_put(id,:toggle)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def todo_generic_put(id,action,opts={})
|
|
322
|
-
puts bannerino("GenericAction(#{action},#{id})")
|
|
323
|
-
begin
|
|
324
|
-
todo = RemoteTodo.find(id)
|
|
325
|
-
puts "Todo.#{id} ok #{green action}: #{yellow( todo.to_s )}"
|
|
326
|
-
put_options = {
|
|
327
|
-
:my_action => action,
|
|
328
|
-
:from_cli_in_test_ric => Socket.gethostname
|
|
329
|
-
}.merge( opts.fetch( :additional_args, {:nis => :bah } ) ) # if there are ,m good. Otherwise, "nisba" :)
|
|
330
|
-
deb "todo_generic_put(id=#{id},#{purple action}, #{gray opts.inspect})"
|
|
331
|
-
todo.put(action, put_options ) # test stuff
|
|
332
|
-
#todo.put(action, :my_action => action, :from_cli_in_test_ric => Socket.gethostname , additional_args) # test stuff
|
|
333
|
-
rescue ActiveResource::ResourceNotFound
|
|
334
|
-
puts "Sorry ResourceNotFound: #{red $!}"
|
|
335
|
-
exit 11
|
|
336
|
-
rescue ActiveResource::ServerError
|
|
337
|
-
puts "Sorry ServerError: #{red $!}"
|
|
338
|
-
exit 12
|
|
586
|
+
o.on("-h", "--help", "Show this help") do
|
|
587
|
+
puts o
|
|
588
|
+
exit 0
|
|
339
589
|
end
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
when 'help' ; usage 'Explicitly called help..'
|
|
376
|
-
when '' ; todo_list()
|
|
377
|
-
else
|
|
378
|
-
usage "Unrecognized command: '#{ARGV[0]}'"
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
remaining = parser.parse(argv.dup)
|
|
593
|
+
subcommand = remaining.shift
|
|
594
|
+
|
|
595
|
+
if subcommand.nil?
|
|
596
|
+
puts parser
|
|
597
|
+
exit 0
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Support passing a bare ID or Septober URL directly (e.g. `septober-cli 235` or `septober-cli https://.../todos/235`)
|
|
601
|
+
if subcommand =~ /\A#?\d+\z/ || subcommand =~ %r{/todos/\d+}
|
|
602
|
+
remaining.unshift(subcommand)
|
|
603
|
+
subcommand = "show"
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
target_env = opts[:local] ? "local" : opts[:env]
|
|
607
|
+
# For `list`, `--agent` filters by agent unless SEPTOBER_AGENT is explicitly set; for write/show commands, `--agent` selects credentials
|
|
608
|
+
cred_agent = (subcommand == "list") ? ENV["SEPTOBER_AGENT"] : opts[:agent]
|
|
609
|
+
cfg = load_config(
|
|
610
|
+
conf_path: opts[:conf],
|
|
611
|
+
target_env: target_env,
|
|
612
|
+
target_agent: cred_agent,
|
|
613
|
+
cli_user: opts[:user],
|
|
614
|
+
cli_password: opts[:password]
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
case subcommand.downcase
|
|
618
|
+
when "list", "ls"
|
|
619
|
+
cmd_list(opts, cfg)
|
|
620
|
+
when "show", "get", "view"
|
|
621
|
+
id_arg = remaining.first
|
|
622
|
+
if id_arg.nil?
|
|
623
|
+
warn red("Error: 'show' requires a Todo ID or URL.")
|
|
624
|
+
exit 1
|
|
379
625
|
end
|
|
626
|
+
cmd_show(id_arg, opts, cfg)
|
|
627
|
+
when "add", "create", "new"
|
|
628
|
+
cmd_add(remaining, opts, cfg)
|
|
629
|
+
when "done", "complete"
|
|
630
|
+
id_arg = remaining.first || abort(red("Error: 'done' requires a Todo ID."))
|
|
631
|
+
cmd_action("done", id_arg, opts, cfg)
|
|
632
|
+
when "undone"
|
|
633
|
+
id_arg = remaining.first || abort(red("Error: 'undone' requires a Todo ID."))
|
|
634
|
+
cmd_action("undone", id_arg, opts, cfg)
|
|
635
|
+
when "toggle"
|
|
636
|
+
id_arg = remaining.first || abort(red("Error: 'toggle' requires a Todo ID."))
|
|
637
|
+
cmd_action("toggle", id_arg, opts, cfg)
|
|
638
|
+
when "delete", "del", "rm"
|
|
639
|
+
id_arg = remaining.first || abort(red("Error: 'delete' requires a Todo ID."))
|
|
640
|
+
cmd_action("delete", id_arg, opts, cfg)
|
|
641
|
+
when "version"
|
|
642
|
+
puts "septober-cli v#{PROG_VER} (ruby #{RUBY_VERSION})"
|
|
643
|
+
else
|
|
644
|
+
warn red("Unknown subcommand: '#{subcommand}'")
|
|
645
|
+
puts parser
|
|
646
|
+
exit 1
|
|
380
647
|
end
|
|
648
|
+
end
|
|
381
649
|
|
|
382
|
-
|
|
383
|
-
# real program...
|
|
384
|
-
main()
|
|
650
|
+
main(ARGV) unless ENV["SEPTOBER_CLI_NO_AUTORUN"] == "1"
|