Skip to content

Commit 2b8efc2

Browse files
authored
Merge pull request #5 from Semantic-partners/feat/name-chkr-plugin
feat: add name-chkr plugin
2 parents 9ae597a + 37c512b commit 2b8efc2

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

plugins/spai-name-chkr

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#!/usr/bin/env bb
2+
;; spai plugin: name-chkr
3+
;;
4+
;; Checks name availability across domains, GitHub, Homebrew, npm, PyPI, crates.io.
5+
;; Takes multiple names as args, checks them all in parallel.
6+
;;
7+
;; Usage: spai name-chkr <name1> [name2] [name3] ...
8+
9+
{:spai/args "<name> [name2] [name3] ..."
10+
:spai/returns "EDN map of availability per name across registries"
11+
:spai/example "spai name-chkr spoqe spindle hedl"}
12+
13+
(require '[clojure.string :as str]
14+
'[clojure.pprint :as pp]
15+
'[babashka.process :as p]
16+
'[babashka.http-client :as http]
17+
'[cheshire.core :as json])
18+
19+
(def args *command-line-args*)
20+
21+
(when (or (empty? args) (contains? #{"--help" "-h"} (first args)))
22+
(println "Usage: spai name-chkr <name> [name2] [name3] ...")
23+
(println)
24+
(println "Checks availability across:")
25+
(println " domains — .com .io .dev .org .app .sh")
26+
(println " github — org/user name")
27+
(println " homebrew — formula/cask")
28+
(println " npm — package")
29+
(println " pypi — package")
30+
(println " crates.io — crate")
31+
(println)
32+
(println "Examples:")
33+
(println " spai name-chkr spoqe")
34+
(println " spai name-chkr hedl spindle gearn")
35+
(System/exit 0))
36+
37+
(def domain-tlds ["com" "io" "dev" "org" "app" "sh"])
38+
39+
(defn check-http
40+
"GET url, return status code. Returns :error on exception."
41+
[url & {:keys [timeout] :or {timeout 8000}}]
42+
(try
43+
(let [resp (http/get url {:throw false
44+
:timeout timeout
45+
:headers {"User-Agent" "spai-name-chkr/1.0"}})]
46+
(:status resp))
47+
(catch Exception e
48+
(let [msg (str e)]
49+
(cond
50+
(str/includes? msg "UnknownHost") :no-dns
51+
(str/includes? msg "ConnectTimeout") :timeout
52+
(str/includes? msg "Timeout") :timeout
53+
(str/includes? msg "Connection refused") :refused
54+
:else :error)))))
55+
56+
(defn check-domain
57+
"Check if a domain resolves via DNS (dig)."
58+
[domain]
59+
(let [{:keys [exit out]} (p/shell {:out :string :err :string :continue true}
60+
"dig" "+short" "+time=3" "+tries=1" domain "A")]
61+
(if (and (zero? exit) (not (str/blank? out)))
62+
:taken
63+
;; Also check AAAA
64+
(let [{:keys [exit out]} (p/shell {:out :string :err :string :continue true}
65+
"dig" "+short" "+time=3" "+tries=1" domain "AAAA")]
66+
(if (and (zero? exit) (not (str/blank? out)))
67+
:taken
68+
:available)))))
69+
70+
(defn check-github
71+
"Search GitHub repos matching name, return top 10 by stars."
72+
[name]
73+
(try
74+
(let [resp (http/get "https://api.github.com/search/repositories"
75+
{:query-params {"q" name "sort" "stars" "order" "desc" "per_page" "10"}
76+
:throw false
77+
:timeout 8000
78+
:headers {"User-Agent" "spai-name-chkr/1.0"
79+
"Accept" "application/vnd.github.v3+json"}})
80+
body (when (= 200 (:status resp))
81+
(cheshire.core/parse-string (:body resp) true))]
82+
(if body
83+
{:total (:total_count body)
84+
:hits (mapv (fn [r]
85+
{:repo (:full_name r)
86+
:stars (:stargazers_count r)
87+
:forks (:forks_count r)
88+
:description (some-> (:description r) (subs 0 (min 80 (count (:description r)))))
89+
:url (:html_url r)})
90+
(:items body))}
91+
{:total 0 :hits [] :note (str "status:" (:status resp))}))
92+
(catch Exception e
93+
{:total 0 :hits [] :error (str e)})))
94+
95+
(defn check-npm
96+
"Search npm for packages matching name, return top hits."
97+
[name]
98+
(try
99+
(let [resp (http/get "https://registry.npmjs.org/-/v1/search"
100+
{:query-params {"text" name "size" "10"}
101+
:throw false
102+
:timeout 8000
103+
:headers {"User-Agent" "spai-name-chkr/1.0"}})
104+
body (when (= 200 (:status resp))
105+
(json/parse-string (:body resp) true))]
106+
(if body
107+
(let [objects (:objects body)]
108+
{:total (get-in body [:total])
109+
:exact (some #(= name (get-in % [:package :name])) objects)
110+
:hits (mapv (fn [o]
111+
(let [pkg (:package o)]
112+
{:name (:name pkg)
113+
:version (:version pkg)
114+
:description (some-> (:description pkg) (subs 0 (min 80 (count (:description pkg)))))
115+
:weekly-dl (get-in o [:score :detail :popularity])}))
116+
objects)})
117+
{:total 0 :hits []}))
118+
(catch Exception e
119+
{:total 0 :hits [] :error (str e)})))
120+
121+
(defn check-pypi
122+
"Search PyPI for packages matching name, return top hits."
123+
[name]
124+
(try
125+
(let [;; Check exact match first
126+
exact-resp (http/get (str "https://pypi.org/pypi/" name "/json")
127+
{:throw false :timeout 8000
128+
:headers {"User-Agent" "spai-name-chkr/1.0"}})
129+
exact (when (= 200 (:status exact-resp))
130+
(let [body (json/parse-string (:body exact-resp) true)
131+
info (:info body)]
132+
{:name (:name info)
133+
:version (:version info)
134+
:description (some-> (:summary info) (subs 0 (min 80 (count (:summary info)))))
135+
:author (:author info)}))
136+
;; Search for similar
137+
search-resp (http/get "https://pypi.org/search/"
138+
{:query-params {"q" name}
139+
:throw false :timeout 8000
140+
:headers {"User-Agent" "spai-name-chkr/1.0"
141+
"Accept" "application/json"}})
142+
;; PyPI search doesn't have a JSON API, use simple API list isn't useful
143+
;; Just use xmlrpc or report exact match + note
144+
]
145+
(if exact
146+
{:exact true :hit exact}
147+
{:exact false}))
148+
(catch Exception e
149+
{:exact false :error (str e)})))
150+
151+
(defn check-crates
152+
"Check crates.io crate existence."
153+
[name]
154+
(let [status (check-http (str "https://crates.io/api/v1/crates/" name))]
155+
(case status
156+
200 :taken
157+
404 :available
158+
:unknown)))
159+
160+
(defn check-homebrew
161+
"Check Homebrew formula/cask existence."
162+
[name]
163+
(let [formula-status (check-http (str "https://formulae.brew.sh/api/formula/" name ".json"))
164+
cask-status (when (not= 200 formula-status)
165+
(check-http (str "https://formulae.brew.sh/api/cask/" name ".json")))]
166+
(cond
167+
(= 200 formula-status) :taken-formula
168+
(= 200 cask-status) :taken-cask
169+
:else :available)))
170+
171+
(defn check-name
172+
"Check a single name across all registries. Returns EDN map."
173+
[name]
174+
(binding [*out* *err*]
175+
(println (str " checking: " name "...")))
176+
(let [lower (str/lower-case name)
177+
;; Fire all checks in parallel
178+
domains (into {} (map (fn [tld]
179+
[(keyword (str tld))
180+
(future (check-domain (str lower "." tld)))])
181+
domain-tlds))
182+
gh-f (future (check-github lower))
183+
npm-f (future (check-npm lower))
184+
pypi-f (future (check-pypi lower))
185+
crate-f (future (check-crates lower))
186+
brew-f (future (check-homebrew lower))]
187+
188+
{:domains (into {} (map (fn [[k v]] [k @v]) domains))
189+
:github @gh-f
190+
:npm @npm-f
191+
:pypi @pypi-f
192+
:crates-io @crate-f
193+
:homebrew @brew-f}))
194+
195+
(defn registry-taken? [v]
196+
(cond
197+
(keyword? v) (not (#{:available :unknown :error :timeout} v))
198+
(map? v) (or (:exact v) (pos? (get v :total 0)))
199+
:else false))
200+
201+
(defn summarize
202+
"Add a quick summary line."
203+
[results]
204+
(let [domain-vals (vals (:domains results))
205+
npm-taken (registry-taken? (:npm results))
206+
pypi-taken (registry-taken? (:pypi results))
207+
crate-taken (registry-taken? (:crates-io results))
208+
brew-taken (registry-taken? (:homebrew results))]
209+
(assoc results :summary
210+
{:domains-available (count (filter #{:available} domain-vals))
211+
:domains-taken (count (filter #{:taken} domain-vals))
212+
:npm (if npm-taken :taken :available)
213+
:pypi (if pypi-taken :taken :available)
214+
:crates-io (if crate-taken :taken :available)
215+
:homebrew (if brew-taken :taken :available)
216+
:github-repos (get-in results [:github :total] 0)})))
217+
218+
;; Main
219+
(binding [*out* *err*]
220+
(println (str "name-chkr: checking " (count args) " name(s)...")))
221+
222+
(let [results (into {}
223+
(map (fn [name]
224+
[(keyword name) (summarize (check-name name))])
225+
args))]
226+
(pp/pprint results))

0 commit comments

Comments
 (0)