diff --git a/clients/clojure/sync/src/un.clj b/clients/clojure/sync/src/un.clj index 20ad8aa..a733b60 100644 --- a/clients/clojure/sync/src/un.clj +++ b/clients/clojure/sync/src/un.clj @@ -118,17 +118,51 @@ (or (second (re-find pattern-str json-str)) (second (re-find pattern-num json-str))))) +(def ^:dynamic *account-index* nil) + +(defn load-accounts-csv [path index] + (when (.exists (io/file path)) + (try + (let [lines (str/split-lines (slurp path)) + rows (->> lines + (filter #(and (> (count %) 0) + (not (str/starts-with? % "#")))) + (map #(str/split % #"," 2)) + (filter #(>= (count %) 2)))] + (when (< index (count rows)) + (let [row (nth rows index)] + [(str/trim (first row)) (str/trim (second row))]))) + (catch Exception _ nil)))) + (defn get-api-keys [] (let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY") secret-key (System/getenv "UNSANDBOX_SECRET_KEY") - api-key (System/getenv "UNSANDBOX_API_KEY")] + api-key (System/getenv "UNSANDBOX_API_KEY") + home (System/getenv "HOME") + account-idx (or *account-index* 0)] (cond + ;; --account N: load row N from accounts.csv, bypasses env vars + (some? *account-index*) + (or (load-accounts-csv (str home "/.unsandbox/accounts.csv") account-idx) + (load-accounts-csv "./accounts.csv" account-idx) + (do + (binding [*out* *err*] + (println (str "Error: account " account-idx " not found in accounts.csv"))) + (System/exit 1))) + ;; env vars (and public-key secret-key) [public-key secret-key] api-key [api-key nil] - :else (do + ;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT) + :else + (let [row-idx (if-let [acc (System/getenv "UNSANDBOX_ACCOUNT")] + (try (Integer/parseInt acc) (catch Exception _ 0)) + 0)] + (or (load-accounts-csv (str home "/.unsandbox/accounts.csv") row-idx) + (load-accounts-csv "./accounts.csv" row-idx) + (do (binding [*out* *err*] (println "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")) - (System/exit 1))))) + (System/exit 1))))))) (defn get-api-key [] (first (get-api-keys))) @@ -1216,4 +1250,14 @@ (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file service-unfreeze-on-demand key-extend image-action image-id image-source-type image-visibility image-name image-ports mode)))) -(parse-args *command-line-args*) +(let [raw-args *command-line-args* + account-val (second (drop-while #(not= % "--account") raw-args)) + account-idx (when account-val + (try (Integer/parseInt account-val) (catch Exception _ nil))) + filtered-args (loop [in raw-args out []] + (cond + (empty? in) out + (= (first in) "--account") (recur (drop 2 in) out) + :else (recur (rest in) (conj out (first in)))))] + (binding [*account-index* account-idx] + (parse-args filtered-args))) diff --git a/clients/lisp/sync/src/un.lisp b/clients/lisp/sync/src/un.lisp index 67e2829..52f340e 100644 --- a/clients/lisp/sync/src/un.lisp +++ b/clients/lisp/sync/src/un.lisp @@ -343,16 +343,63 @@ (curl-delete api-key (format nil "/services/~a/env" service-id)) (format t "~aVault deleted: ~a~a~%" *green* service-id *reset*)) +(defparameter *account-index* nil) + +(defun load-accounts-csv (path index) + "Load row INDEX from a CSV file of public_key,secret_key pairs. + Skips blank lines and lines starting with #. Returns (list pk sk) or nil." + (when (probe-file path) + (handler-case + (with-open-file (stream path) + (let ((rows nil)) + (loop for line = (read-line stream nil nil) + while line do + (let ((trimmed (string-trim '(#\Space #\Tab #\Return) line))) + (when (and (> (length trimmed) 0) + (not (char= (char trimmed 0) #\#))) + (let ((comma-pos (position #\, trimmed))) + (when comma-pos + (push (list (string-trim '(#\Space #\Tab) (subseq trimmed 0 comma-pos)) + (string-trim '(#\Space #\Tab) (subseq trimmed (1+ comma-pos)))) + rows)))))) + (let ((rows (nreverse rows))) + (when (< index (length rows)) + (nth index rows))))) + (error () nil)))) + (defun get-api-keys () (let ((public-key (uiop:getenv "UNSANDBOX_PUBLIC_KEY")) (secret-key (uiop:getenv "UNSANDBOX_SECRET_KEY")) - (api-key (uiop:getenv "UNSANDBOX_API_KEY"))) + (api-key (uiop:getenv "UNSANDBOX_API_KEY")) + (home (uiop:getenv "HOME"))) (cond + ;; --account N: load row N from accounts.csv, bypasses env vars + ((not (null *account-index*)) + (let ((result (or (load-accounts-csv + (format nil "~a/.unsandbox/accounts.csv" home) + *account-index*) + (load-accounts-csv "./accounts.csv" *account-index*)))) + (or result + (progn + (format *error-output* "Error: account ~a not found in accounts.csv~%" *account-index*) + (uiop:quit 1))))) + ;; env vars ((and public-key secret-key) (list public-key secret-key)) (api-key (list api-key nil)) - (t (progn - (format t "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%") - (uiop:quit 1)))))) + ;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var) + (t + (let* ((acc-env (uiop:getenv "UNSANDBOX_ACCOUNT")) + (row-idx (if acc-env + (handler-case (parse-integer acc-env) (error () 0)) + 0)) + (result (or (load-accounts-csv + (format nil "~a/.unsandbox/accounts.csv" home) + row-idx) + (load-accounts-csv "./accounts.csv" row-idx)))) + (or result + (progn + (format *error-output* "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%") + (uiop:quit 1)))))))) (defun get-api-key () (first (get-api-keys))) @@ -912,8 +959,26 @@ do (format t "~a~%" (subseq array-content (1+ start) end)))) (format t ""))))) +(defun strip-account-flag (args) + "Remove --account N from args, set *account-index* as side-effect. Returns filtered args." + (let ((result nil) + (i 0) + (vec (coerce args 'vector))) + (loop while (< i (length vec)) do + (cond + ((string= (aref vec i) "--account") + (when (< (+ i 1) (length vec)) + (setf *account-index* + (handler-case (parse-integer (aref vec (+ i 1))) (error () nil)))) + (incf i 2)) + (t + (push (aref vec i) result) + (incf i)))) + (nreverse result))) + (defun main () - (let ((args (uiop:command-line-arguments))) + (let* ((raw-args (uiop:command-line-arguments)) + (args (strip-account-flag raw-args))) (if (null args) (progn (format t "Usage: un.lisp [options] ~%") diff --git a/clients/powershell/sync/src/un.ps1 b/clients/powershell/sync/src/un.ps1 index 4c54cd6..ad0ad95 100644 --- a/clients/powershell/sync/src/un.ps1 +++ b/clients/powershell/sync/src/un.ps1 @@ -61,7 +61,43 @@ $EXT_MAP = @{ ".raku" = "raku"; ".m" = "objc"; ".awk" = "awk" } +$script:AccountIndex = $null + +function Load-AccountsCSV { + param($Path, $Index) + if (-not (Test-Path $Path)) { return $null } + try { + $rows = @() + Get-Content $Path | Where-Object { + $_.Trim() -ne "" -and -not $_.TrimStart().StartsWith("#") + } | ForEach-Object { + $parts = $_ -split ",", 2 + if ($parts.Count -ge 2) { + $rows += ,@($parts[0].Trim(), $parts[1].Trim()) + } + } + if ($Index -lt $rows.Count) { + return $rows[$Index] + } + } catch {} + return $null +} + function Get-ApiKeys { + $home = $env:HOME + if (-not $home) { $home = $env:USERPROFILE } + + # --account N: load row N from accounts.csv, bypasses env vars + if ($null -ne $script:AccountIndex) { + $result = Load-AccountsCSV -Path "$home/.unsandbox/accounts.csv" -Index $script:AccountIndex + if (-not $result) { + $result = Load-AccountsCSV -Path "./accounts.csv" -Index $script:AccountIndex + } + if ($result) { return $result } + Write-Error "Error: account $($script:AccountIndex) not found in accounts.csv" + exit 1 + } + $publicKey = $env:UNSANDBOX_PUBLIC_KEY $secretKey = $env:UNSANDBOX_SECRET_KEY @@ -71,11 +107,27 @@ function Get-ApiKeys { $secretKey = "" } - if (-not $publicKey) { - Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" - exit 1 + if ($publicKey -and $secretKey) { + return @($publicKey, $secretKey) } - return @($publicKey, $secretKey) + + if ($publicKey) { + return @($publicKey, "") + } + + # accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var) + $rowIdx = 0 + if ($env:UNSANDBOX_ACCOUNT) { + try { $rowIdx = [int]$env:UNSANDBOX_ACCOUNT } catch {} + } + $result = Load-AccountsCSV -Path "$home/.unsandbox/accounts.csv" -Index $rowIdx + if (-not $result) { + $result = Load-AccountsCSV -Path "./accounts.csv" -Index $rowIdx + } + if ($result) { return $result } + + Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" + exit 1 } function Invoke-Api { @@ -1179,38 +1231,49 @@ Key options: exit 0 } -if ($args[0] -eq "session") { - Invoke-Session -Args $args[1..($args.Count-1)] -} elseif ($args[0] -eq "service") { - Invoke-Service -Args $args[1..($args.Count-1)] -} elseif ($args[0] -eq "snapshot") { - Invoke-Snapshot -Args $args[1..($args.Count-1)] -} elseif ($args[0] -eq "image") { - Invoke-Image -Args $args[1..($args.Count-1)] -} elseif ($args[0] -eq "languages") { - Invoke-Languages -Args $args[1..($args.Count-1)] -} elseif ($args[0] -eq "key") { - Invoke-Key -Args $args[1..($args.Count-1)] +# Pre-parse --account N from args, strip from effective arg list +$effectiveArgs = @() +for ($i = 0; $i -lt $args.Count; $i++) { + if ($args[$i] -eq "--account" -and ($i + 1) -lt $args.Count) { + try { $script:AccountIndex = [int]$args[$i + 1] } catch {} + $i++ + } else { + $effectiveArgs += $args[$i] + } +} + +if ($effectiveArgs[0] -eq "session") { + Invoke-Session -Args $effectiveArgs[1..($effectiveArgs.Count-1)] +} elseif ($effectiveArgs[0] -eq "service") { + Invoke-Service -Args $effectiveArgs[1..($effectiveArgs.Count-1)] +} elseif ($effectiveArgs[0] -eq "snapshot") { + Invoke-Snapshot -Args $effectiveArgs[1..($effectiveArgs.Count-1)] +} elseif ($effectiveArgs[0] -eq "image") { + Invoke-Image -Args $effectiveArgs[1..($effectiveArgs.Count-1)] +} elseif ($effectiveArgs[0] -eq "languages") { + Invoke-Languages -Args $effectiveArgs[1..($effectiveArgs.Count-1)] +} elseif ($effectiveArgs[0] -eq "key") { + Invoke-Key -Args $effectiveArgs[1..($effectiveArgs.Count-1)] } else { # Parse execute args $sourceFile = $null $envVars = @{} $network = $null - for ($i = 0; $i -lt $args.Count; $i++) { - switch ($args[$i]) { + for ($i = 0; $i -lt $effectiveArgs.Count; $i++) { + switch ($effectiveArgs[$i]) { "-e" { - $kv = $args[$i+1] -split "=", 2 + $kv = $effectiveArgs[$i+1] -split "=", 2 $envVars[$kv[0]] = $kv[1] $i++ } - "-n" { $network = $args[$i+1]; $i++ } + "-n" { $network = $effectiveArgs[$i+1]; $i++ } default { - if ($args[$i].StartsWith("-")) { - Write-Error "${RED}Unknown option: $($args[$i])${RESET}" + if ($effectiveArgs[$i].StartsWith("-")) { + Write-Error "${RED}Unknown option: $($effectiveArgs[$i])${RESET}" exit 1 } else { - $sourceFile = $args[$i] + $sourceFile = $effectiveArgs[$i] } } } diff --git a/clients/scheme/sync/src/un.scm b/clients/scheme/sync/src/un.scm index c2e2d35..bdd572d 100644 --- a/clients/scheme/sync/src/un.scm +++ b/clients/scheme/sync/src/un.scm @@ -445,16 +445,72 @@ (curl-delete api-key (format #f "/services/~a/env" service-id)) (format #t "~aVault deleted for: ~a~a\n" green service-id reset)) +(define *account-index* #f) + +(define (load-accounts-csv path index) + "Load row INDEX from a CSV file of public_key,secret_key pairs. + Skips blank lines and lines starting with #. Returns (list pk sk) or #f." + (if (not (access? path R_OK)) + #f + (catch #t + (lambda () + (let ((port (open-input-file path)) + (rows '())) + (let loop () + (let ((line (read-line port))) + (when (not (eof-object? line)) + (let ((trimmed (string-trim-both line))) + (when (and (> (string-length trimmed) 0) + (not (char=? (string-ref trimmed 0) #\#))) + (let ((comma-pos (string-index trimmed #\,))) + (when comma-pos + (set! rows (append rows + (list (list + (string-trim-both (substring trimmed 0 comma-pos)) + (string-trim-both (substring trimmed (1+ comma-pos))))))))))) + (loop)))) + (close-input-port port) + (if (< index (length rows)) + (list-ref rows index) + #f))) + (lambda (key . args) #f)))) + (define (get-api-keys) (let ((public-key (getenv "UNSANDBOX_PUBLIC_KEY")) (secret-key (getenv "UNSANDBOX_SECRET_KEY")) - (api-key (getenv "UNSANDBOX_API_KEY"))) + (api-key (getenv "UNSANDBOX_API_KEY")) + (home (getenv "HOME"))) (cond + ;; --account N: load row N from accounts.csv, bypasses env vars + ((not (eq? *account-index* #f)) + (let ((result (or (load-accounts-csv + (string-append home "/.unsandbox/accounts.csv") + *account-index*) + (load-accounts-csv "./accounts.csv" *account-index*)))) + (or result + (begin + (format (current-error-port) "Error: account ~a not found in accounts.csv\n" *account-index*) + (exit 1))))) + ;; env vars ((and public-key secret-key) (list public-key secret-key)) (api-key (list api-key #f)) - (else (begin - (display "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n" (current-error-port)) - (exit 1)))))) + ;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var) + (else + (let* ((acc-env (getenv "UNSANDBOX_ACCOUNT")) + (row-idx (if acc-env + (catch #t + (lambda () (string->number acc-env)) + (lambda (key . args) 0)) + 0)) + (row-idx (if (number? row-idx) (inexact->exact row-idx) 0)) + (result (or (load-accounts-csv + (string-append home "/.unsandbox/accounts.csv") + row-idx) + (load-accounts-csv "./accounts.csv" row-idx)))) + (or result + (begin + (display "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n" (current-error-port)) + (exit 1))))))) (define (get-api-key) (car (get-api-keys))) @@ -1286,4 +1342,15 @@ (else (execute-cmd (car args)))))) -(main (cdr (command-line))) +(define (strip-account-flag args) + "Remove --account N from args list, setting *account-index* as side-effect." + (let loop ((in args) (out '())) + (cond + ((null? in) (reverse out)) + ((and (equal? (car in) "--account") (pair? (cdr in))) + (let ((n (string->number (cadr in)))) + (set! *account-index* (if (number? n) (inexact->exact n) #f)) + (loop (cddr in) out))) + (else (loop (cdr in) (cons (car in) out)))))) + +(main (strip-account-flag (cdr (command-line))))