;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; CLI for sr.ht ;;; ;;; Copyright (c) 2019-2020, Evan Hanson ;;; ;;; See LICENSE for details. ;;; (declare (module (sourcehut cli))) (import (chicken condition) (chicken file posix) (chicken keyword) (chicken io) (chicken pretty-print) (chicken process) (chicken process signal) (chicken process-context) (chicken string) (openssl) (sourcehut) (sourcehut builds) (sourcehut git) (sourcehut meta) (sourcehut paste) (sourcehut todo) (medea) (optimism getopt-long) (simple-exceptions) (only (srfi 1) any filter-map) (only (srfi 133) vector-append)) (import-syntax (begin-syntax) (matchable)) (import-for-syntax (chicken io) (chicken process)) (define common-options '((--debug) (--access-token . token) ((-o --output) . format) ((-h --help)) ((--version)))) (define paging-options '((-n . lines))) (define (configure-access-token x) (let ((from-file (and (string? x) (substring-index "/" x) (with-input-from-file x read-line)))) (access-token (or from-file x)))) (define output-format (let ((x (get-environment-variable "SOURCEHUT_CLI_OUTPUT"))) (make-parameter (or x "s") string->symbol))) (define maximum-results (let ((x (get-environment-variable "SOURCEHUT_CLI_MAX_RESULTS"))) (make-parameter (or x "50")))) (define debug-mode (make-parameter (get-environment-variable "SOURCEHUT_CLI_DEBUG"))) (define manual (begin-syntax (with-input-from-file "sourcehut.1" read-string))) (define version (begin-syntax (let ((x (with-input-from-pipe "git describe --tags 2>/dev/null" read-line))) (if (string? x) x "(egg)")))) (define (alist-ref* keys alist #!optional (test eqv?) default) (or (any (lambda (key) (alist-ref key alist test)) keys) default)) (define (error-reason e) (and-let* ((body (get-condition-property e 'sourcehut 'body '())) (errors (alist-ref 'errors body)) (reasons (filter-map (lambda (error) (alist-ref 'reason error)) (vector->list errors)))) (string-intersperse reasons ", "))) (define (error-message e) (let ((m (message e)) (a (arguments e))) (if (null? a) m (conc m ": " (string-intersperse (map conc a) ", "))))) (define (print-json . args) (apply write-json args) (print)) (define (string->id x) (let ((id (string->number x))) (if (and (integer? id) (positive? id)) id (error "Invalid ID" x)))) (define (crud? x) (and (pair? x) (pair? (car x)) (keyword? (caar x)))) (define (output x) (let ((printer (case (output-format) ((s) pretty-print) ((j json) print-json)))) (cond ((crud? x) (printer (cdr x))) ((string? x) (print (string-chomp x))) (else (printer x))))) (define (input-file x) (if (equal? x "-") (current-input-port) (open-input-file x))) (define (input x) (if (equal? x "-") (read-string) (with-input-from-file x read-string))) (define (retrieve-paged crud #!optional (limit 50)) (let loop ((crud crud) (count 0)) (and-let* ((response (retrieve crud)) (results (alist-ref 'results response)) (next (alist-ref 'next response)) (count* (+ count (vector-length results)))) (if (or (>= count* limit) (eq? next 'null)) (subvector results 0 (min (vector-length results) (- (min (alist-ref 'total response) limit) count))) (vector-append results (loop (page crud next) count*)))))) (define (get-paged endpoint args) (define opts (parse-command-line args paging-options)) (when (pair? (alist-ref '-- opts)) (usage (current-error-port)) (exit 1)) (retrieve-paged (endpoint) (let* ((n (alist-ref* '(-n --max-results) opts eq? (maximum-results))) (n* (string->number n))) (if (and (integer? n*) (positive? n*)) (inexact->exact n*) (error "Invalid result limit" n))))) (define (get-profile) (retrieve (profile))) (define (get-audit-log args) (get-paged audit-log args)) (define (get-ssh-keys args) (get-paged ssh-keys args)) (define (get-pgp-keys args) (get-paged pgp-keys args)) (define ((key-creator type function) args) (define opts (parse-command-line args '(((-f --filename) . input)))) (define file (alist-ref* '(-f --filename) opts eq? "-")) (match (assq '-- opts) ((_) (create (apply function (list type (input file))))) ((_ . _) (usage (current-error-port)) (exit 1)))) (define ((key-deleter function) id) (delete (function (string->id id)))) (define (create-job args) (define opts (parse-command-line args '(((-f --filename) . input) ((-m --manifest) . input) ((-n --note) . message)))) (define manifest (alist-ref* '(-m --manifest -f --filename) opts eq? "-")) (define note (alist-ref* '(-n --note) opts)) (match (assq '-- opts) ((_) (let ((args* (append (list manifest: (input manifest)) (if (not note) (list) (list note: note))))) (create (apply job args*)))) ((_ . _) (usage (current-error-port)) (exit 1)))) (define (get-jobs args) (get-paged jobs args)) (define (get-manifest id) (retrieve (manifest (string->id id)))) (define (get-job id) (retrieve (job (string->id id)))) (define (get-refs repository args) (get-paged (cut refs repository) args)) (define (create-artifact args) (define opts (parse-command-line args '(((-f --filename) . input) ((-n --name) . message)))) (define file (alist-ref* '(-f --filename) opts eq? "-")) (define name (alist-ref* '(-n --name) opts)) (match (assq '-- opts) ((_ repository reference) (define args* (append (list file: (input-file file)) (cond ((string? name) (list filename: name)) ((equal? file "-") (error "A name must be specified when reading from stdin")) (else (list))))) (create (apply artifact repository reference args*))) ((_ . _) (usage (current-error-port)) (exit 1)))) (define (create-paste args) (define opts (parse-command-line args '(((-c --contents) . input) ((-f --filename) . input) ((-n --name) . name) ((-v --visibility) . level)))) (define contents (alist-ref* '(-c --contents -f --filename) opts eq? "-")) (define filename (alist-ref* '(-n --name) opts eq? 'null)) (define visibility (alist-ref* '(-v --visibility) opts eq? "unlisted")) (match (assq '-- opts) ((_) (let ((args* (list contents: (input contents) filename: filename visibility: visibility))) (create (apply paste args*)))) ((_ . _) (usage (current-error-port)) (exit 1)))) (define (create-paste* args) (let ((paste (create-paste args))) (conc "https://paste." (service-domain) "/blob/" (alist-ref 'blob_id (vector-ref (alist-ref 'files paste) 0))))) (define (get-paste sha) (retrieve (paste sha))) (define (get-pastes args) (get-paged pastes args)) (define (get-blob sha) (and-let* ((blob (retrieve (blob sha)))) (alist-ref 'contents blob))) (define (get-trackers args) (get-paged trackers args)) (define (get-tracker name) (retrieve (tracker name))) (define (get-tickets tracker args) (get-paged (cut tickets tracker) args)) (define (get-ticket tracker id) (retrieve (ticket tracker id))) (define (get-events tracker ticket args) (get-paged (cut events tracker ticket) args)) (define (usage #!optional (port (current-output-port))) (parameterize ((current-output-port port)) (print "Usage: " (program-name) " command [options ...]") (print) (print "Options:") (print " --access-token Set API token") (print " -o, --output Set output format {s|j|json} (s)") (print " -h, --help Show this message") (print " --version Show version information") (print) (print "Commands:") (print) (print " get profile Fetch user details") (print " get audit-log Fetch audit log") (print " get ssh-keys List SSH keys") (print " get pgp-keys List PGP keys") (print " create ssh-key Create SSH key") (print " -f, --filename filename (stdin)") (print " create pgp-key Create PGP key") (print " -f, --filename filename (stdin)") (print " delete ssh-key Delete SSH key") (print " delete pgp-key Delete PGP key") (print) (print " get jobs Fetch job list") (print " get job Fetch job details") (print " get manifest Fetch job manifest") (print " create job Create job") (print " -f, --manifest manifest (stdin)") (print " -n, --note description (null)") (print) (print " get refs Fetch references") (print " create artifact Attach artifact to reference") (print " -f, --filename source (stdin)") (print " -n, --name name (null)") (print) (print " get pastes Fetch paste list") (print " get paste Fetch paste details") (print " get blob Fetch blob contents") (print " create paste Create paste") (print " paste Create paste and print URL") (print " -f, --contents source (stdin)") (print " -n, --name name (null)") (print " -v, --visibility visibility (unlisted)") (print) (print " get trackers Fetch tracker list") (print " get tracker Fetch tracker details") (print " get tickets Fetch ticket list") (print " get ticket Fetch ticket details") (print " get events Fetch ticket events") (print))) (define (handle-global-options! opts) (let ((x (get-environment-variable "SOURCEHUT_CLI_ACCESS_TOKEN"))) (configure-access-token x)) (and-let* ((help (alist-ref* '(-h --help) opts))) (usage) (exit 0)) (and-let* ((help (alist-ref* '(--version) opts))) (print version) (exit 0)) (and-let* ((debug (alist-ref* '(--debug) opts))) (debug-mode #t)) (and-let* ((format (alist-ref* '(-o --output) opts))) (output-format format)) (and-let* ((token (alist-ref '--access-token opts))) (configure-access-token token)) (unless (member (output-format) '(s j json)) (error "Invalid output format" (output-format)))) (define (display-manual) (set-signal-handler! signal/int void) (set-signal-handler! signal/term void) (let*-values (((i o) (create-pipe)) ((pid) (process-fork (lambda () (handle-exceptions _ (exit 1) (file-close o) (duplicate-fileno i 0) (process-execute "man" (list "/dev/stdin"))))))) (file-close i) (file-write o manual) (file-close o) (receive (_ _ status) (process-wait pid) (unless (zero? status) (usage))))) (define (main args) (let ((opts (parse-command-line args common-options))) (handle-global-options! opts) (match (assq '-- opts) ((_ "get" "profile") (output (get-profile))) ((_ "get" (or "audit" "audit-log" "audit-logs") . args*) (output (get-audit-log args*))) ((_ "get" "ssh-keys" . args*) (output (get-ssh-keys args*))) ((_ "create" "ssh-key" . args*) (output ((key-creator ssh-key: ssh-key) args*))) ((_ "delete" "ssh-key" id) (output ((key-deleter ssh-key) id))) ((_ "get" (or "pgp-keys" "gpg-keys") . args*) (output (get-pgp-keys args*))) ((_ "create" (or "pgp-key" "gpg-key") . args*) (output ((key-creator pgp-key: pgp-key) args*))) ((_ "delete" (or "pgp-key" "gpg-key") id) (output ((key-deleter pgp-key) id))) ((_ "get" "jobs" . args*) (output (get-jobs args*))) ((_ "get" "job" id) (output (get-job id))) ((_ "create" "job" . args*) (output (create-job args*))) ((_ "get" "manifest" id) (output (get-manifest id))) ((_ "get" "refs" repository . args*) (output (get-refs repository args*))) ((_ "create" "artifact" . args*) (output (create-artifact args*))) ((_ "get" "pastes" . args*) (output (get-pastes args*))) ((_ "get" "paste" sha) (output (get-paste sha))) ((_ "get" "blob" sha) (output (get-blob sha))) ((_ "create" "paste" . args*) (output (create-paste args*))) ((_ "paste" . args*) (output (create-paste* args*))) ((_ "get" "trackers" . args*) (output (get-trackers args*))) ((_ "get" "tracker" name) (output (get-tracker name))) ((_ "get" "tickets" tracker . args*) (output (get-tickets tracker args*))) ((_ "get" "ticket" name id) (output (get-ticket name id))) ((_ "get" "events" name id . args*) (output (get-events name id args*))) ((_ "help") (display-manual)) ((_ "manual") (display manual)) ((_ "version") (print version)) ((_ . _) (usage (current-error-port)) (exit 1))))) (cond-expand (compiling (handle-exceptions e (begin (when (debug-mode) (signal e)) (parameterize ((current-output-port (current-error-port))) (print "Error: " (or (error-reason e) (error-message e))) (exit 1))) (main (command-line-arguments)))) (else))