exploremyprofile 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fbb95d414e36bede8a58108431a36c0a439a4bb56e92afbf6a62f866021a5a74
4
+ data.tar.gz: 3e5a41439c7d2e4cb7e640d632aa178731b4dd5ac9a19e872b1aec44982b6cad
5
+ SHA512:
6
+ metadata.gz: 7a46d662af6e8926a4d591ed975a748b9c34bc81c3b8567f1a77b274b085d79bb8b4f7555d7ccfa5dc58449884dd6a8b402a90f5aee5b16a546335af506102cc
7
+ data.tar.gz: e552159bd69ebc39cb17a99c83b2be30fd231ac82fd1cc96f953d3adc742928d158d469da83d328bb4f30699a4365ce2081f08b9ad8bfea6e70a03ffc46d3ca4
data/exe/explore ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "explore"
7
+
8
+ exit Explore::CLI::Runner.new(
9
+ argv: ARGV,
10
+ env: ENV,
11
+ stdout: $stdout,
12
+ stderr: $stderr
13
+ ).call
@@ -0,0 +1,349 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "optparse"
4
+ require "uri"
5
+
6
+ module Explore
7
+ module Cli
8
+ class Error < StandardError
9
+ attr_reader :exit_code
10
+
11
+ def initialize(message, exit_code: 1)
12
+ super(message)
13
+ @exit_code = exit_code
14
+ end
15
+ end
16
+
17
+ class Client
18
+ OWNER_ONLY_PATH_PREFIXES = [
19
+ "/api/agent/v1/drafts",
20
+ "/api/agent/v1/onboarding/status",
21
+ "/api/agent/v1/manifest/next_actions",
22
+ "/api/agent/v1/notion/sync_status",
23
+ "/api/agent/v1/publish/preview",
24
+ "/api/agent/v1/content/create_draft",
25
+ "/api/agent/v1/content/propose_update"
26
+ ].freeze
27
+
28
+ def initialize(base_url:, api_key: nil)
29
+ @base_url = base_url
30
+ @api_key = api_key
31
+ end
32
+
33
+ def get(path, params: {})
34
+ request(Net::HTTP::Get, path, params:)
35
+ end
36
+
37
+ def post(path, params: {}, json: nil)
38
+ request(Net::HTTP::Post, path, params:, json:)
39
+ end
40
+
41
+ def patch(path, params: {}, json: nil)
42
+ request(Net::HTTP::Patch, path, params:, json:)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :base_url, :api_key
48
+
49
+ def request(klass, path, params:, json: nil)
50
+ uri = URI.join(base_url, path)
51
+ uri.query = URI.encode_www_form(params.compact) if params.compact.any?
52
+
53
+ http = Net::HTTP.new(uri.host, uri.port)
54
+ http.use_ssl = uri.scheme == "https"
55
+
56
+ request = klass.new(uri)
57
+ request["Accept"] = "application/json"
58
+ request["Content-Type"] = "application/json" if json
59
+ request["X-API-Key"] = api_key if api_key.to_s != ""
60
+ request.body = JSON.generate(json) if json
61
+
62
+ response = http.request(request)
63
+ parsed = response.body.to_s.empty? ? {} : JSON.parse(response.body)
64
+ return parsed if response.code.to_i.between?(200, 299)
65
+
66
+ message = error_message_for(response:, parsed:, path:)
67
+ raise Error.new(message, exit_code: 1)
68
+ end
69
+
70
+ def error_message_for(response:, parsed:, path:)
71
+ return owner_only_unauthorized_message if response.code.to_i == 401 && owner_only_path?(path)
72
+
73
+ parsed.dig("error", "message") || "Request failed"
74
+ end
75
+
76
+ def owner_only_path?(path)
77
+ OWNER_ONLY_PATH_PREFIXES.any? { |prefix| path.start_with?(prefix) }
78
+ end
79
+
80
+ def owner_only_unauthorized_message
81
+ "Unauthorized: this is an owner-only command. Use a signed-in session or pass EXPLORE_API_KEY/--api-key. " \
82
+ "The key is app-level, not account-specific, and lives in Rails credentials at api.v1_key."
83
+ end
84
+ end
85
+
86
+ class Runner
87
+ DEFAULT_BASE_URL = "http://127.0.0.1:3000".freeze
88
+
89
+ def initialize(argv:, env:, stdout:, stderr:, client_class: Client)
90
+ @argv = argv.dup
91
+ @env = env
92
+ @stdout = stdout
93
+ @stderr = stderr
94
+ @client_class = client_class
95
+ end
96
+
97
+ def call
98
+ command = argv.shift
99
+ return emit_version if version_flag?(command)
100
+ raise Error.new(usage, exit_code: 2) if blank_value?(command)
101
+
102
+ case command
103
+ when "version"
104
+ emit_version
105
+ when "whoami"
106
+ options = parse_options(argv, require_input: false)
107
+ response = client(options).get("/api/agent/v1/profile", params: scope_params(options))
108
+ emit(output_for_whoami(response, options))
109
+ when "profile"
110
+ execute_read(argv.shift, "/api/agent/v1/profile")
111
+ when "content"
112
+ subcommand = argv.shift
113
+ return execute_read(subcommand, "/api/agent/v1/content") if subcommand == "list"
114
+ return execute_create_draft if subcommand == "create-draft"
115
+ return execute_propose_update if subcommand == "propose-update"
116
+ raise Error.new(usage, exit_code: 2)
117
+ when "drafts"
118
+ subcommand = argv.shift
119
+ return execute_read(subcommand, "/api/agent/v1/drafts") if subcommand == "list"
120
+ return execute_draft_inspect if subcommand == "inspect"
121
+ return execute_draft_apply if subcommand == "apply"
122
+ return execute_draft_apply_preview if subcommand == "apply-preview"
123
+ return execute_draft_update if subcommand == "update"
124
+ return execute_draft_archive if subcommand == "archive"
125
+ raise Error.new(usage, exit_code: 2)
126
+ when "onboarding"
127
+ execute_read(argv.shift, "/api/agent/v1/onboarding/status")
128
+ when "manifest"
129
+ execute_read(argv.shift, "/api/agent/v1/manifest/next_actions")
130
+ when "notion"
131
+ execute_read(argv.shift, "/api/agent/v1/notion/sync_status")
132
+ when "publish"
133
+ execute_read(argv.shift, "/api/agent/v1/publish/preview")
134
+ else
135
+ raise Error.new(usage, exit_code: 2)
136
+ end
137
+
138
+ 0
139
+ rescue Error => e
140
+ stderr.puts(e.message)
141
+ e.exit_code
142
+ end
143
+
144
+ private
145
+
146
+ attr_reader :argv, :env, :stdout, :stderr, :client_class
147
+
148
+ def execute_read(subcommand, path)
149
+ expected = {
150
+ "/api/agent/v1/profile" => "inspect",
151
+ "/api/agent/v1/onboarding/status" => "status",
152
+ "/api/agent/v1/manifest/next_actions" => "next-actions",
153
+ "/api/agent/v1/notion/sync_status" => "sync-status",
154
+ "/api/agent/v1/publish/preview" => "preview",
155
+ "/api/agent/v1/content" => "list",
156
+ "/api/agent/v1/drafts" => "list"
157
+ }.fetch(path)
158
+ raise Error.new(usage, exit_code: 2) unless subcommand == expected
159
+
160
+ options = parse_options(argv, require_input: false)
161
+ response = client(options).get(path, params: scope_params(options))
162
+ emit(format_output(response, options))
163
+ 0
164
+ end
165
+
166
+ def execute_propose_update
167
+ options = parse_options(argv, require_input: true)
168
+ input_path = options.fetch(:input)
169
+ payload = JSON.parse(File.read(input_path))
170
+ response = client(options).post(
171
+ "/api/agent/v1/content/propose_update",
172
+ params: scope_params(options),
173
+ json: payload
174
+ )
175
+ emit(format_output(response, options))
176
+ 0
177
+ end
178
+
179
+ def execute_create_draft
180
+ options = parse_options(argv, require_input: true)
181
+ input_path = options.fetch(:input)
182
+ payload = JSON.parse(File.read(input_path))
183
+ response = client(options).post(
184
+ "/api/agent/v1/content/create_draft",
185
+ params: scope_params(options),
186
+ json: payload
187
+ )
188
+ emit(format_output(response, options))
189
+ 0
190
+ end
191
+
192
+ def execute_draft_inspect
193
+ options = parse_options(argv, require_input: false, require_draft_id: true)
194
+ response = client(options).get(
195
+ "/api/agent/v1/drafts/#{options.fetch(:draft_id)}",
196
+ params: scope_params(options)
197
+ )
198
+ emit(format_output(response, options))
199
+ 0
200
+ end
201
+
202
+ def execute_draft_archive
203
+ options = parse_options(argv, require_input: false, require_draft_id: true)
204
+ response = client(options).post(
205
+ "/api/agent/v1/drafts/#{options.fetch(:draft_id)}/archive",
206
+ params: scope_params(options)
207
+ )
208
+ emit(format_output(response, options))
209
+ 0
210
+ end
211
+
212
+ def execute_draft_apply
213
+ options = parse_options(argv, require_input: false, require_draft_id: true)
214
+ response = client(options).post(
215
+ "/api/agent/v1/drafts/#{options.fetch(:draft_id)}/apply",
216
+ params: scope_params(options)
217
+ )
218
+ emit(format_output(response, options))
219
+ 0
220
+ end
221
+
222
+ def execute_draft_apply_preview
223
+ options = parse_options(argv, require_input: false, require_draft_id: true)
224
+ response = client(options).get(
225
+ "/api/agent/v1/drafts/#{options.fetch(:draft_id)}/apply_preview",
226
+ params: scope_params(options)
227
+ )
228
+ emit(format_output(response, options))
229
+ 0
230
+ end
231
+
232
+ def execute_draft_update
233
+ options = parse_options(argv, require_input: true, require_draft_id: true)
234
+ payload = JSON.parse(File.read(options.fetch(:input)))
235
+ response = client(options).patch(
236
+ "/api/agent/v1/drafts/#{options.fetch(:draft_id)}",
237
+ params: scope_params(options),
238
+ json: payload
239
+ )
240
+ emit(format_output(response, options))
241
+ 0
242
+ end
243
+
244
+ def parse_options(arguments, require_input:, require_draft_id: false)
245
+ options = {
246
+ base_url: env.fetch("EXPLORE_BASE_URL", DEFAULT_BASE_URL),
247
+ api_key: env["EXPLORE_API_KEY"],
248
+ account: env["EXPLORE_ACCOUNT"],
249
+ slug: env["EXPLORE_SLUG"],
250
+ json: false
251
+ }
252
+
253
+ parser = OptionParser.new do |opts|
254
+ opts.on("--base-url URL") { |value| options[:base_url] = value }
255
+ opts.on("--api-key TOKEN") { |value| options[:api_key] = value }
256
+ opts.on("--account ACCOUNT") { |value| options[:account] = value }
257
+ opts.on("--slug SLUG") { |value| options[:slug] = value }
258
+ opts.on("--draft ID") { |value| options[:draft_id] = value }
259
+ opts.on("--input PATH") { |value| options[:input] = value }
260
+ opts.on("--json") { options[:json] = true }
261
+ end
262
+
263
+ parser.parse!(arguments)
264
+ raise Error.new("Missing --input", exit_code: 2) if require_input && blank_value?(options[:input])
265
+ raise Error.new("Missing --draft", exit_code: 2) if require_draft_id && blank_value?(options[:draft_id])
266
+
267
+ options
268
+ end
269
+
270
+ def client(options)
271
+ client_class.new(base_url: options.fetch(:base_url), api_key: options[:api_key])
272
+ end
273
+
274
+ def scope_params(options)
275
+ if present_value?(options[:slug])
276
+ { slug: options[:slug] }
277
+ elsif options[:account].to_s.match?(/\A\d+\z/)
278
+ { account_id: options[:account] }
279
+ elsif present_value?(options[:account])
280
+ { account: options[:account] }
281
+ else
282
+ {}
283
+ end
284
+ end
285
+
286
+ def format_output(response, options)
287
+ return JSON.pretty_generate(response) if options[:json]
288
+
289
+ JSON.pretty_generate(response)
290
+ end
291
+
292
+ def output_for_whoami(response, options)
293
+ return JSON.pretty_generate(response) if options[:json]
294
+
295
+ [
296
+ "Account: #{response.dig("profile", "name")}",
297
+ "Slug: #{response["account_slug"]}",
298
+ "Context: #{response.dig("context", "mode")}",
299
+ "Profile: #{response.dig("profile", "public_url")}"
300
+ ].join("\n")
301
+ end
302
+
303
+ def emit(message)
304
+ stdout.puts(message)
305
+ end
306
+
307
+ def emit_version
308
+ stdout.puts(Explore::VERSION)
309
+ 0
310
+ end
311
+
312
+ def blank_value?(value)
313
+ value.nil? || value.respond_to?(:empty?) && value.empty?
314
+ end
315
+
316
+ def present_value?(value)
317
+ !blank_value?(value)
318
+ end
319
+
320
+ def version_flag?(value)
321
+ %w[--version -v].include?(value)
322
+ end
323
+
324
+ def usage
325
+ <<~TEXT
326
+ Usage:
327
+ explore version
328
+ explore whoami [--account <id-or-slug>] [--slug <slug>] [--json]
329
+ explore profile inspect [--account <id-or-slug>] [--slug <slug>] [--json]
330
+ explore content list [--account <id-or-slug>] [--slug <slug>] [--json]
331
+ explore content create-draft --account <id-or-slug> --input <file.json> [--json]
332
+ explore drafts list --account <id-or-slug> [--json]
333
+ explore drafts inspect --account <id-or-slug> --draft <id> [--json]
334
+ explore drafts apply --account <id-or-slug> --draft <id> [--json]
335
+ explore drafts apply-preview --account <id-or-slug> --draft <id> [--json]
336
+ explore drafts update --account <id-or-slug> --draft <id> --input <file.json> [--json]
337
+ explore drafts archive --account <id-or-slug> --draft <id> [--json]
338
+ explore onboarding status [--account <id-or-slug>] [--json]
339
+ explore manifest next-actions [--account <id-or-slug>] [--json]
340
+ explore notion sync-status [--account <id-or-slug>] [--json]
341
+ explore publish preview [--account <id-or-slug>] [--json]
342
+ explore content propose-update --account <id-or-slug> --input <file.json> [--json]
343
+ TEXT
344
+ end
345
+ end
346
+ end
347
+
348
+ CLI = Cli
349
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explore
4
+ module Version
5
+ STRING = "0.1.0"
6
+ end
7
+
8
+ VERSION = Version::STRING
9
+ end
data/lib/explore.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "explore/version"
4
+ require_relative "explore/cli"
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exploremyprofile
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Johnny Butler
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Explore provides a terminal-friendly CLI for public-safe profile inspection
13
+ and owner draft workflows.
14
+ email:
15
+ - johnnybutler@example.com
16
+ executables:
17
+ - explore
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - exe/explore
22
+ - lib/explore.rb
23
+ - lib/explore/cli.rb
24
+ - lib/explore/version.rb
25
+ homepage: https://exploremyprofile.com/agent-setup
26
+ licenses:
27
+ - MIT
28
+ metadata:
29
+ homepage_uri: https://exploremyprofile.com/agent-setup
30
+ documentation_uri: https://exploremyprofile.com/agents
31
+ source_code_uri: https://github.com/johnnybutler7/johnnybutler-com
32
+ rubygems_mfa_required: 'true'
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.2.0
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 4.0.6
48
+ specification_version: 4
49
+ summary: Terminal CLI for the Explore agent API
50
+ test_files: []