modified: clients/awk/sync/src/un.awk
modified: clients/c/src/un.c modified: clients/clojure/sync/src/un.clj modified: clients/cobol/sync/src/un.cob modified: clients/cpp/sync/src/un.cpp modified: clients/crystal/sync/src/un.cr modified: clients/d/sync/src/un.d modified: clients/dart/sync/src/un.dart modified: clients/elixir/sync/src/un.ex modified: clients/erlang/sync/src/un.erl modified: clients/forth/sync/src/un.forth modified: clients/fortran/sync/src/un.f90 modified: clients/fsharp/sync/src/un.fs modified: clients/haskell/sync/src/un.hs modified: clients/julia/sync/src/un.jl modified: clients/kotlin/sync/src/un.kt modified: clients/lisp/sync/src/un.lisp modified: clients/nim/sync/src/un.nim modified: clients/ocaml/sync/src/un.ml modified: clients/powershell/sync/src/un.ps1 modified: clients/prolog/sync/src/un.pro modified: clients/r/sync/src/un.r modified: clients/scheme/sync/src/un.scm modified: clients/tcl/sync/src/un.tcl modified: clients/typescript/sync/src/un.ts modified: clients/v/sync/src/un.v modified: clients/zig/sync/src/un.zig
This commit is contained in:
parent
cc7674f7d7
commit
1077543b7c
27 changed files with 1952 additions and 192 deletions
|
|
@ -45,6 +45,8 @@
|
|||
BEGIN {
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
LANGUAGES_CACHE_TTL = 3600 # 1 hour cache TTL
|
||||
LANGUAGES_CACHE_FILE = ENVIRON["HOME"] "/.unsandbox/languages.json"
|
||||
|
||||
# Extension to language map
|
||||
split("py:python js:javascript ts:typescript rb:ruby php:php pl:perl lua:lua sh:bash go:go rs:rust c:c cpp:cpp java:java kt:kotlin cs:csharp fs:fsharp hs:haskell ml:ocaml clj:clojure scm:scheme lisp:commonlisp erl:erlang ex:elixir jl:julia r:r cr:crystal d:d nim:nim zig:zig v:v dart:dart groovy:groovy f90:fortran cob:cobol pro:prolog forth:forth tcl:tcl raku:raku m:objc awk:awk ps1:powershell", pairs, " ")
|
||||
|
|
@ -637,7 +639,66 @@ function cmd_key(do_extend) {
|
|||
validate_key(do_extend)
|
||||
}
|
||||
|
||||
function languages_list(json_output , timestamp, sig_headers, signature, sig_input, sig_cmd, line, response, i, lang) {
|
||||
function read_languages_cache( cmd, line, cache_content, cache_timestamp, current_time) {
|
||||
# Check if cache file exists and is valid
|
||||
cmd = "cat '" LANGUAGES_CACHE_FILE "' 2>/dev/null"
|
||||
cache_content = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
cache_content = cache_content line
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
if (cache_content == "") return ""
|
||||
|
||||
# Extract timestamp from cache
|
||||
if (match(cache_content, /"timestamp":([0-9]+)/, arr)) {
|
||||
cache_timestamp = arr[1]
|
||||
current_time = systime()
|
||||
# Check if cache is still valid (within TTL)
|
||||
if ((current_time - cache_timestamp) < LANGUAGES_CACHE_TTL) {
|
||||
return cache_content
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function write_languages_cache(languages_array , cmd, cache_json, current_time) {
|
||||
current_time = systime()
|
||||
# Build cache JSON: {"languages": [...], "timestamp": unix_seconds}
|
||||
cache_json = "{\"languages\":" languages_array ",\"timestamp\":" current_time "}"
|
||||
|
||||
# Ensure ~/.unsandbox directory exists
|
||||
system("mkdir -p \"" ENVIRON["HOME"] "/.unsandbox\"")
|
||||
|
||||
# Write cache file
|
||||
cmd = "cat > '" LANGUAGES_CACHE_FILE "'"
|
||||
print cache_json | cmd
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function languages_list(json_output , timestamp, sig_headers, signature, sig_input, sig_cmd, line, response, i, lang, cache_content, languages_array, content, first, m) {
|
||||
# Try to read from cache first
|
||||
cache_content = read_languages_cache()
|
||||
if (cache_content != "") {
|
||||
# Extract languages array from cache
|
||||
if (match(cache_content, /"languages":(\[[^\]]*\])/, arr)) {
|
||||
languages_array = arr[1]
|
||||
if (json_output) {
|
||||
print languages_array
|
||||
} else {
|
||||
# Parse and print one per line
|
||||
content = languages_array
|
||||
gsub(/[\[\]"]/, "", content)
|
||||
n = split(content, langs, ",")
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (langs[i] != "") print langs[i]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# No valid cache, fetch from API
|
||||
get_api_keys()
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
|
|
@ -655,32 +716,41 @@ function languages_list(json_output , timestamp, sig_headers, signature, sig_
|
|||
}
|
||||
close(cmd)
|
||||
|
||||
# Build languages array for caching
|
||||
languages_array = ""
|
||||
if (match(response, /"languages":\[([^\]]*)\]/, arr)) {
|
||||
# Parse out language names from the array
|
||||
content = arr[1]
|
||||
languages_array = "["
|
||||
first = 1
|
||||
while (match(content, /"name":"([^"]*)"/, m)) {
|
||||
if (!first) languages_array = languages_array ","
|
||||
languages_array = languages_array "\"" m[1] "\""
|
||||
first = 0
|
||||
content = substr(content, RSTART + RLENGTH)
|
||||
}
|
||||
languages_array = languages_array "]"
|
||||
|
||||
# Save to cache
|
||||
write_languages_cache(languages_array)
|
||||
}
|
||||
|
||||
if (json_output) {
|
||||
# Output raw JSON array of language names
|
||||
# Extract languages array and convert to simple array
|
||||
if (match(response, /"languages":\[([^\]]*)\]/, arr)) {
|
||||
# Parse out language names from the array
|
||||
content = arr[1]
|
||||
printf "["
|
||||
first = 1
|
||||
while (match(content, /"name":"([^"]*)"/, m)) {
|
||||
if (!first) printf ","
|
||||
printf "\"%s\"", m[1]
|
||||
first = 0
|
||||
content = substr(content, RSTART + RLENGTH)
|
||||
}
|
||||
printf "]\n"
|
||||
if (languages_array != "") {
|
||||
print languages_array
|
||||
} else {
|
||||
# Fallback: just print raw response
|
||||
print response
|
||||
}
|
||||
} else {
|
||||
# Output one language per line
|
||||
if (match(response, /"languages":\[([^\]]*)\]/, arr)) {
|
||||
content = arr[1]
|
||||
while (match(content, /"name":"([^"]*)"/, m)) {
|
||||
print m[1]
|
||||
content = substr(content, RSTART + RLENGTH)
|
||||
if (languages_array != "") {
|
||||
content = languages_array
|
||||
gsub(/[\[\]"]/, "", content)
|
||||
n = split(content, langs, ",")
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (langs[i] != "") print langs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
#define LARGE_UPLOAD_WARN_SIZE (1024L * 1024 * 1024) // Warn if total > 1GB
|
||||
#define MAX_ENV_VARS 256 // LXC limit is typically higher
|
||||
#define MAX_ENV_CONTENT_SIZE (64 * 1024) // 64KB max env vault size
|
||||
#define LANGUAGES_CACHE_TTL 3600 // 1 hour cache for languages list
|
||||
|
||||
// ============================================================================
|
||||
// SHA-256 Implementation (for HMAC-SHA256)
|
||||
|
|
@ -1834,8 +1835,155 @@ static int list_sessions(const UnsandboxCredentials *creds) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Get path to languages cache file (~/.unsandbox/languages.json)
|
||||
static char* get_languages_cache_path(void) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd *pw = getpwuid(getuid());
|
||||
if (pw) home = pw->pw_dir;
|
||||
}
|
||||
if (!home) return NULL;
|
||||
|
||||
char *path = malloc(strlen(home) + 32);
|
||||
if (!path) return NULL;
|
||||
sprintf(path, "%s/.unsandbox/languages.json", home);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Load languages from cache if valid (< 1 hour old)
|
||||
// Returns JSON string with languages array, or NULL if cache invalid/missing
|
||||
static char* load_languages_cache(void) {
|
||||
char *cache_path = get_languages_cache_path();
|
||||
if (!cache_path) return NULL;
|
||||
|
||||
struct stat st;
|
||||
if (stat(cache_path, &st) != 0) {
|
||||
free(cache_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if cache is fresh (< 1 hour old)
|
||||
time_t now = time(NULL);
|
||||
if (now - st.st_mtime >= LANGUAGES_CACHE_TTL) {
|
||||
free(cache_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FILE *f = fopen(cache_path, "r");
|
||||
free(cache_path);
|
||||
if (!f) return NULL;
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (size <= 0 || size > 1024 * 1024) { // Sanity check: max 1MB
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *data = malloc(size + 1);
|
||||
if (!data) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t read = fread(data, 1, size, f);
|
||||
fclose(f);
|
||||
data[read] = '\0';
|
||||
|
||||
// Verify it contains languages array
|
||||
if (!strstr(data, "\"languages\"")) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Save languages to cache
|
||||
static void save_languages_cache(const char *json_response) {
|
||||
if (!json_response) return;
|
||||
|
||||
char *cache_path = get_languages_cache_path();
|
||||
if (!cache_path) return;
|
||||
|
||||
// Ensure ~/.unsandbox directory exists
|
||||
char *dir = strdup(cache_path);
|
||||
if (dir) {
|
||||
char *last_slash = strrchr(dir, '/');
|
||||
if (last_slash) {
|
||||
*last_slash = '\0';
|
||||
mkdir(dir, 0700);
|
||||
}
|
||||
free(dir);
|
||||
}
|
||||
|
||||
FILE *f = fopen(cache_path, "w");
|
||||
free(cache_path);
|
||||
if (!f) return;
|
||||
|
||||
// Extract just the languages array if present, wrap in standard format
|
||||
const char *langs_start = strstr(json_response, "\"languages\":[");
|
||||
if (langs_start) {
|
||||
const char *arr_start = strchr(langs_start, '[');
|
||||
const char *arr_end = strchr(arr_start, ']');
|
||||
if (arr_start && arr_end) {
|
||||
fprintf(f, "{\"languages\":%.*s,\"timestamp\":%ld}",
|
||||
(int)(arr_end - arr_start + 1), arr_start, (long)time(NULL));
|
||||
}
|
||||
} else {
|
||||
// Try to find raw array
|
||||
const char *arr_start = strchr(json_response, '[');
|
||||
const char *arr_end = arr_start ? strchr(arr_start, ']') : NULL;
|
||||
if (arr_start && arr_end) {
|
||||
fprintf(f, "{\"languages\":%.*s,\"timestamp\":%ld}",
|
||||
(int)(arr_end - arr_start + 1), arr_start, (long)time(NULL));
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// List supported languages (CLI helper)
|
||||
static int list_languages_cli(const UnsandboxCredentials *creds, int json_output) {
|
||||
// Try cache first
|
||||
char *cached = load_languages_cache();
|
||||
if (cached) {
|
||||
const char *langs_start = strstr(cached, "\"languages\":[");
|
||||
if (!langs_start) langs_start = strchr(cached, '[');
|
||||
|
||||
if (langs_start) {
|
||||
const char *arr_start = strchr(langs_start, '[');
|
||||
if (arr_start) {
|
||||
if (json_output) {
|
||||
const char *arr_end = strchr(arr_start, ']');
|
||||
if (arr_end) {
|
||||
printf("%.*s\n", (int)(arr_end - arr_start + 1), arr_start);
|
||||
}
|
||||
} else {
|
||||
const char *p = arr_start + 1;
|
||||
while (*p && *p != ']') {
|
||||
if (*p == '"') {
|
||||
p++;
|
||||
const char *end = strchr(p, '"');
|
||||
if (end) {
|
||||
printf("%.*s\n", (int)(end - p), p);
|
||||
p = end + 1;
|
||||
}
|
||||
} else {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(cached);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
free(cached);
|
||||
}
|
||||
|
||||
// Cache miss or invalid - fetch from API
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
|
|
@ -1911,6 +2059,9 @@ static int list_languages_cli(const UnsandboxCredentials *creds, int json_output
|
|||
}
|
||||
}
|
||||
|
||||
// Save to cache for future use
|
||||
save_languages_cache(response.data);
|
||||
|
||||
free(response.data);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@
|
|||
|
||||
(def portal-base "https://unsandbox.com")
|
||||
|
||||
(def languages-cache-ttl 3600) ;; 1 hour in seconds
|
||||
|
||||
(def ext-map
|
||||
{".hs" "haskell" ".ml" "ocaml" ".clj" "clojure" ".scm" "scheme"
|
||||
".lisp" "commonlisp" ".erl" "erlang" ".ex" "elixir" ".exs" "elixir"
|
||||
|
|
@ -518,9 +520,42 @@
|
|||
(let [api-key (get-api-key)]
|
||||
(validate-key api-key extend?)))
|
||||
|
||||
(defn get-languages-cache-path []
|
||||
(let [home (System/getenv "HOME")]
|
||||
(str home "/.unsandbox/languages.json")))
|
||||
|
||||
(defn load-languages-cache []
|
||||
(let [cache-path (get-languages-cache-path)]
|
||||
(when (.exists (io/file cache-path))
|
||||
(try
|
||||
(let [content (slurp cache-path)
|
||||
timestamp (extract-field "timestamp" content)]
|
||||
(when timestamp
|
||||
(let [cache-time (Long/parseLong timestamp)
|
||||
now (quot (System/currentTimeMillis) 1000)]
|
||||
(when (< (- now cache-time) languages-cache-ttl)
|
||||
content))))
|
||||
(catch Exception _ nil)))))
|
||||
|
||||
(defn save-languages-cache [languages-json]
|
||||
(let [cache-path (get-languages-cache-path)
|
||||
cache-dir (.getParent (io/file cache-path))]
|
||||
(.mkdirs (io/file cache-dir))
|
||||
(let [timestamp (quot (System/currentTimeMillis) 1000)
|
||||
cache-content (str "{\"languages\":" languages-json ",\"timestamp\":" timestamp "}")]
|
||||
(spit cache-path cache-content))))
|
||||
|
||||
(defn languages-command [json-output?]
|
||||
(let [api-key (get-api-key)
|
||||
response (curl-get api-key "/languages")]
|
||||
;; Try cache first
|
||||
cached (load-languages-cache)
|
||||
response (if cached
|
||||
cached
|
||||
(let [resp (curl-get api-key "/languages")]
|
||||
;; Extract and cache the languages array
|
||||
(when-let [match (re-find #"\"languages\":\s*\[([^\]]*)\]" resp)]
|
||||
(save-languages-cache (str "[" (second match) "]")))
|
||||
resp))]
|
||||
(if json-output?
|
||||
;; Extract language names and output as JSON array
|
||||
(let [langs (re-seq #"\"name\":\"([^\"]+)\"" response)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
01 WS-INPUT-FILES PIC X(1024).
|
||||
01 WS-PORTAL-BASE PIC X(256) VALUE
|
||||
"https://unsandbox.com".
|
||||
01 WS-LANGUAGES-CACHE-TTL PIC 9(8) VALUE 3600.
|
||||
01 WS-EXTEND-FLAG PIC X(8).
|
||||
01 WS-SVC-ENVS PIC X(2048).
|
||||
01 WS-SVC-ENV-FILE PIC X(256).
|
||||
|
|
@ -1038,9 +1039,18 @@
|
|||
PERFORM LANGUAGES-LIST.
|
||||
|
||||
LANGUAGES-LIST.
|
||||
* Languages list with 1-hour cache
|
||||
IF WS-JSON-OUTPUT = "true"
|
||||
* JSON output: extract language names as array
|
||||
STRING "TS=$(date +%s); "
|
||||
* JSON output: extract language names as array with caching
|
||||
STRING "CACHE_TTL=3600; "
|
||||
"CACHE_FILE=\"$HOME/.unsandbox/languages.json\"; "
|
||||
"if [ -f \"$CACHE_FILE\" ]; then "
|
||||
"CACHE_TS=$(jq -r '.timestamp // 0' \"$CACHE_FILE\" 2>/dev/null); "
|
||||
"CURRENT_TS=$(date +%s); "
|
||||
"AGE=$((CURRENT_TS - CACHE_TS)); "
|
||||
"if [ $AGE -lt $CACHE_TTL ]; then "
|
||||
"jq -c '.languages' \"$CACHE_FILE\"; exit 0; fi; fi; "
|
||||
"TS=$(date +%s); "
|
||||
"SIG=$(echo -n \"$TS:GET:/languages:\" | "
|
||||
"openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
|
|
@ -1051,12 +1061,24 @@
|
|||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG); "
|
||||
"echo \"$RESP\" | jq -c '[.languages[].name]'"
|
||||
"LANGS=$(echo \"$RESP\" | jq -c '[.languages[].name]'); "
|
||||
"mkdir -p \"$HOME/.unsandbox\"; "
|
||||
"echo \"{\\\"languages\\\":$LANGS,\\\"timestamp\\\":$(date +%s)}\" "
|
||||
"> \"$CACHE_FILE\"; "
|
||||
"echo \"$LANGS\""
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
ELSE
|
||||
* Plain output: one language per line
|
||||
STRING "TS=$(date +%s); "
|
||||
* Plain output: one language per line with caching
|
||||
STRING "CACHE_TTL=3600; "
|
||||
"CACHE_FILE=\"$HOME/.unsandbox/languages.json\"; "
|
||||
"if [ -f \"$CACHE_FILE\" ]; then "
|
||||
"CACHE_TS=$(jq -r '.timestamp // 0' \"$CACHE_FILE\" 2>/dev/null); "
|
||||
"CURRENT_TS=$(date +%s); "
|
||||
"AGE=$((CURRENT_TS - CACHE_TS)); "
|
||||
"if [ $AGE -lt $CACHE_TTL ]; then "
|
||||
"jq -r '.languages[]' \"$CACHE_FILE\"; exit 0; fi; fi; "
|
||||
"TS=$(date +%s); "
|
||||
"SIG=$(echo -n \"$TS:GET:/languages:\" | "
|
||||
"openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
|
|
@ -1067,6 +1089,10 @@
|
|||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG); "
|
||||
"LANGS=$(echo \"$RESP\" | jq -c '[.languages[].name]'); "
|
||||
"mkdir -p \"$HOME/.unsandbox\"; "
|
||||
"echo \"{\\\"languages\\\":$LANGS,\\\"timestamp\\\":$(date +%s)}\" "
|
||||
"> \"$CACHE_FILE\"; "
|
||||
"echo \"$RESP\" | jq -r '.languages[].name'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ using namespace std;
|
|||
|
||||
const string API_BASE = "https://api.unsandbox.com";
|
||||
const string PORTAL_BASE = "https://unsandbox.com";
|
||||
const int LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
|
||||
const string BLUE = "\033[34m";
|
||||
const string RED = "\033[31m";
|
||||
const string GREEN = "\033[32m";
|
||||
|
|
@ -209,6 +210,79 @@ string build_env_content(const vector<string>& envs, const string& env_file) {
|
|||
return parts.str();
|
||||
}
|
||||
|
||||
string get_languages_cache_path() {
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) home = ".";
|
||||
return string(home) + "/.unsandbox/languages.json";
|
||||
}
|
||||
|
||||
vector<string> load_languages_cache() {
|
||||
vector<string> empty;
|
||||
string cache_path = get_languages_cache_path();
|
||||
|
||||
struct stat st;
|
||||
if (stat(cache_path.c_str(), &st) != 0) {
|
||||
return empty; // File doesn't exist
|
||||
}
|
||||
|
||||
// Check if cache is fresh (< 1 hour old)
|
||||
time_t now = time(nullptr);
|
||||
if (now - st.st_mtime >= LANGUAGES_CACHE_TTL) {
|
||||
return empty; // Cache expired
|
||||
}
|
||||
|
||||
string content = read_file(cache_path);
|
||||
if (content.empty()) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
// Parse languages from JSON {"languages": [...], "timestamp": ...}
|
||||
vector<string> languages;
|
||||
size_t pos = content.find("\"languages\":");
|
||||
if (pos == string::npos) return empty;
|
||||
|
||||
pos = content.find('[', pos);
|
||||
if (pos == string::npos) return empty;
|
||||
pos++;
|
||||
|
||||
while (pos < content.length()) {
|
||||
// Skip whitespace
|
||||
while (pos < content.length() && (content[pos] == ' ' || content[pos] == '\n' || content[pos] == '\t')) pos++;
|
||||
if (content[pos] == ']') break;
|
||||
if (content[pos] == '"') {
|
||||
pos++;
|
||||
size_t end = content.find('"', pos);
|
||||
if (end != string::npos) {
|
||||
languages.push_back(content.substr(pos, end - pos));
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
// Skip comma
|
||||
while (pos < content.length() && (content[pos] == ',' || content[pos] == ' ' || content[pos] == '\n' || content[pos] == '\t')) pos++;
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
void save_languages_cache(const vector<string>& languages) {
|
||||
string cache_path = get_languages_cache_path();
|
||||
|
||||
// Create directory if needed
|
||||
string dir = cache_path.substr(0, cache_path.rfind('/'));
|
||||
mkdir(dir.c_str(), 0755);
|
||||
|
||||
ofstream f(cache_path);
|
||||
if (!f) return;
|
||||
|
||||
f << "{\"languages\":[";
|
||||
for (size_t i = 0; i < languages.size(); i++) {
|
||||
if (i > 0) f << ",";
|
||||
f << "\"" << languages[i] << "\"";
|
||||
}
|
||||
f << "],\"timestamp\":" << time(nullptr) << "}";
|
||||
f.close();
|
||||
}
|
||||
|
||||
string service_env_status(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/env";
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
|
|
@ -695,13 +769,16 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
}
|
||||
|
||||
void cmd_languages(bool json_output, const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("GET", "/languages", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/languages' " + auth_headers;
|
||||
string result = exec_curl(cmd);
|
||||
// Try cache first
|
||||
vector<string> names = load_languages_cache();
|
||||
|
||||
if (json_output) {
|
||||
// Extract language names and output as JSON array
|
||||
vector<string> names;
|
||||
if (names.empty()) {
|
||||
// Fetch from API
|
||||
string auth_headers = build_auth_headers("GET", "/languages", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/languages' " + auth_headers;
|
||||
string result = exec_curl(cmd);
|
||||
|
||||
// Extract language names from response
|
||||
size_t pos = 0;
|
||||
string search = "\"name\":\"";
|
||||
while ((pos = result.find(search, pos)) != string::npos) {
|
||||
|
|
@ -712,6 +789,14 @@ void cmd_languages(bool json_output, const string& public_key, const string& sec
|
|||
pos = end;
|
||||
}
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
if (!names.empty()) {
|
||||
save_languages_cache(names);
|
||||
}
|
||||
}
|
||||
|
||||
if (json_output) {
|
||||
cout << "[";
|
||||
for (size_t i = 0; i < names.size(); i++) {
|
||||
if (i > 0) cout << ",";
|
||||
|
|
@ -720,15 +805,8 @@ void cmd_languages(bool json_output, const string& public_key, const string& sec
|
|||
cout << "]" << endl;
|
||||
} else {
|
||||
// Output one language per line
|
||||
size_t pos = 0;
|
||||
string search = "\"name\":\"";
|
||||
while ((pos = result.find(search, pos)) != string::npos) {
|
||||
pos += search.length();
|
||||
size_t end = result.find("\"", pos);
|
||||
if (end != string::npos) {
|
||||
cout << result.substr(pos, end - pos) << endl;
|
||||
pos = end;
|
||||
}
|
||||
for (const auto& name : names) {
|
||||
cout << name << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,12 +70,68 @@ RESET = "\033[0m"
|
|||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
MAX_ENV_CONTENT_SIZE = 65536
|
||||
LANGUAGES_CACHE_TTL = 3600 # 1 hour in seconds
|
||||
|
||||
def detect_language(filename : String) : String
|
||||
ext = File.extname(filename).downcase
|
||||
EXT_MAP.fetch(ext, "unknown")
|
||||
end
|
||||
|
||||
def get_languages_cache_path : String?
|
||||
home = ENV["HOME"]?
|
||||
return nil if home.nil? || home.empty?
|
||||
File.join(home, ".unsandbox", "languages.json")
|
||||
end
|
||||
|
||||
def load_languages_cache : String?
|
||||
cache_path = get_languages_cache_path
|
||||
return nil if cache_path.nil? || !File.exists?(cache_path)
|
||||
|
||||
begin
|
||||
content = File.read(cache_path)
|
||||
# Parse timestamp from JSON
|
||||
parsed = JSON.parse(content)
|
||||
cached_time = parsed["timestamp"]?.try(&.as_i64?)
|
||||
return nil if cached_time.nil?
|
||||
|
||||
current_time = Time.utc.to_unix
|
||||
|
||||
# Check if cache is still valid (within TTL)
|
||||
if current_time - cached_time < LANGUAGES_CACHE_TTL
|
||||
return content
|
||||
end
|
||||
rescue
|
||||
# Cache read failed, return nil to fetch fresh
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def save_languages_cache(response : JSON::Any)
|
||||
cache_path = get_languages_cache_path
|
||||
return if cache_path.nil?
|
||||
|
||||
begin
|
||||
# Ensure directory exists
|
||||
cache_dir = File.dirname(cache_path)
|
||||
Dir.mkdir_p(cache_dir) unless Dir.exists?(cache_dir)
|
||||
|
||||
# Extract languages array from response
|
||||
languages = response["languages"]?
|
||||
return if languages.nil?
|
||||
|
||||
# Build cache JSON with timestamp
|
||||
timestamp = Time.utc.to_unix
|
||||
cache_data = {
|
||||
"languages" => languages,
|
||||
"timestamp" => timestamp
|
||||
}
|
||||
File.write(cache_path, cache_data.to_json)
|
||||
rescue
|
||||
# Cache write failed, ignore
|
||||
end
|
||||
end
|
||||
|
||||
def get_api_keys(args_key : String?) : {String, String?}
|
||||
public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
|
||||
secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
|
||||
|
|
@ -417,7 +473,20 @@ end
|
|||
def cmd_languages(args)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
result = api_request("/languages", public_key, secret_key)
|
||||
# Try to load from cache first
|
||||
cached_response = load_languages_cache
|
||||
result : JSON::Any
|
||||
|
||||
if cached_response
|
||||
result = JSON.parse(cached_response)
|
||||
else
|
||||
# Fetch from API
|
||||
result = api_request("/languages", public_key, secret_key)
|
||||
|
||||
# Save to cache
|
||||
save_languages_cache(result)
|
||||
end
|
||||
|
||||
languages = result["languages"]?.try(&.as_a?) || [] of JSON::Any
|
||||
|
||||
if args[:json]?.as?(Bool)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ immutable string GREEN = "\033[32m";
|
|||
immutable string YELLOW = "\033[33m";
|
||||
immutable string RESET = "\033[0m";
|
||||
immutable size_t MAX_ENV_CONTENT_SIZE = 65536;
|
||||
immutable int LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
|
||||
|
||||
string detectLanguage(string filename) {
|
||||
string[string] langMap = [
|
||||
|
|
@ -130,6 +131,88 @@ string getTimestamp() {
|
|||
return format("%d", Clock.currTime.toUnixTime());
|
||||
}
|
||||
|
||||
string getLanguagesCachePath() {
|
||||
string home = environment.get("HOME", "");
|
||||
if (home.empty) return "";
|
||||
return buildPath(home, ".unsandbox", "languages.json");
|
||||
}
|
||||
|
||||
string loadLanguagesCache() {
|
||||
import std.datetime.systime : Clock;
|
||||
string cachePath = getLanguagesCachePath();
|
||||
if (cachePath.empty || !exists(cachePath)) return "";
|
||||
|
||||
try {
|
||||
string content = readText(cachePath);
|
||||
// Parse timestamp from JSON
|
||||
import std.algorithm : findSplitAfter;
|
||||
auto tsSearch = content.findSplitAfter(`"timestamp":`);
|
||||
if (tsSearch[0].length == 0) return "";
|
||||
|
||||
// Find the end of the number
|
||||
string remaining = tsSearch[1];
|
||||
size_t numEnd = 0;
|
||||
while (numEnd < remaining.length && (remaining[numEnd] >= '0' && remaining[numEnd] <= '9')) {
|
||||
numEnd++;
|
||||
}
|
||||
if (numEnd == 0) return "";
|
||||
|
||||
long cachedTime = to!long(remaining[0..numEnd]);
|
||||
long currentTime = Clock.currTime.toUnixTime();
|
||||
|
||||
// Check if cache is still valid (within TTL)
|
||||
if (currentTime - cachedTime < LANGUAGES_CACHE_TTL) {
|
||||
return content;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Cache read failed, return empty to fetch fresh
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void saveLanguagesCache(string response) {
|
||||
import std.datetime.systime : Clock;
|
||||
string cachePath = getLanguagesCachePath();
|
||||
if (cachePath.empty) return;
|
||||
|
||||
try {
|
||||
// Ensure directory exists
|
||||
string cacheDir = dirName(cachePath);
|
||||
if (!exists(cacheDir)) {
|
||||
mkdirRecurse(cacheDir);
|
||||
}
|
||||
|
||||
// Extract languages array from response
|
||||
import std.algorithm : findSplitAfter;
|
||||
|
||||
// Find the languages array
|
||||
auto langSearch = response.findSplitAfter(`"languages":`);
|
||||
if (langSearch[0].length == 0) return;
|
||||
|
||||
// Find the array brackets
|
||||
auto bracketStart = langSearch[1].findSplitAfter("[");
|
||||
if (bracketStart[0].length == 0) return;
|
||||
|
||||
// Find matching closing bracket
|
||||
string rest = "[" ~ bracketStart[1];
|
||||
int depth = 1;
|
||||
size_t endPos = 1;
|
||||
while (endPos < rest.length && depth > 0) {
|
||||
if (rest[endPos] == '[') depth++;
|
||||
else if (rest[endPos] == ']') depth--;
|
||||
endPos++;
|
||||
}
|
||||
string languagesArray = rest[0..endPos];
|
||||
|
||||
// Build cache JSON with timestamp
|
||||
long timestamp = Clock.currTime.toUnixTime();
|
||||
string cacheJson = format(`{"languages":%s,"timestamp":%d}`, languagesArray, timestamp);
|
||||
std.file.write(cachePath, cacheJson);
|
||||
} catch (Exception e) {
|
||||
// Cache write failed, ignore
|
||||
}
|
||||
}
|
||||
|
||||
string buildAuthHeaders(string method, string path, string body, string publicKey, string secretKey) {
|
||||
if (secretKey.empty) {
|
||||
// Legacy mode: use public_key as bearer token
|
||||
|
|
@ -695,9 +778,21 @@ void cmdImage(bool list, string info, string del, string lock, string unlock,
|
|||
}
|
||||
|
||||
void cmdLanguages(bool jsonOutput, string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/languages' %s`, API_BASE, authHeaders);
|
||||
string result = execCurl(cmd);
|
||||
// Try to load from cache first
|
||||
string cachedResponse = loadLanguagesCache();
|
||||
string result;
|
||||
|
||||
if (!cachedResponse.empty) {
|
||||
result = cachedResponse;
|
||||
} else {
|
||||
// Fetch from API
|
||||
string authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/languages' %s`, API_BASE, authHeaders);
|
||||
result = execCurl(cmd);
|
||||
|
||||
// Save to cache
|
||||
saveLanguagesCache(result);
|
||||
}
|
||||
|
||||
if (jsonOutput) {
|
||||
// Extract language names and output as JSON array
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ const String red = '\x1B[31m';
|
|||
const String green = '\x1B[32m';
|
||||
const String yellow = '\x1B[33m';
|
||||
const String reset = '\x1B[0m';
|
||||
const int languagesCacheTtl = 3600; // 1 hour in seconds
|
||||
|
||||
const Map<String, String> extMap = {
|
||||
'.py': 'python', '.js': 'javascript', '.ts': 'typescript',
|
||||
|
|
@ -150,6 +151,69 @@ String detectLanguage(String filename) {
|
|||
return lang;
|
||||
}
|
||||
|
||||
String? getLanguagesCachePath() {
|
||||
final home = Platform.environment['HOME'];
|
||||
if (home == null || home.isEmpty) return null;
|
||||
return '$home/.unsandbox/languages.json';
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> loadLanguagesCache() async {
|
||||
final cachePath = getLanguagesCachePath();
|
||||
if (cachePath == null) return null;
|
||||
|
||||
final cacheFile = File(cachePath);
|
||||
if (!await cacheFile.exists()) return null;
|
||||
|
||||
try {
|
||||
final content = await cacheFile.readAsString();
|
||||
final data = jsonDecode(content) as Map<String, dynamic>;
|
||||
|
||||
final cachedTime = data['timestamp'] as int?;
|
||||
if (cachedTime == null) return null;
|
||||
|
||||
final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// Check if cache is still valid (within TTL)
|
||||
if (currentTime - cachedTime < languagesCacheTtl) {
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
// Cache read failed, return null to fetch fresh
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveLanguagesCache(Map<String, dynamic> response) async {
|
||||
final cachePath = getLanguagesCachePath();
|
||||
if (cachePath == null) return;
|
||||
|
||||
try {
|
||||
final cacheFile = File(cachePath);
|
||||
|
||||
// Ensure directory exists
|
||||
final cacheDir = cacheFile.parent;
|
||||
if (!await cacheDir.exists()) {
|
||||
await cacheDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// Extract languages from response
|
||||
final languages = response['languages'];
|
||||
if (languages == null) return;
|
||||
|
||||
// Build cache JSON with timestamp
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final cacheData = {
|
||||
'languages': languages,
|
||||
'timestamp': timestamp,
|
||||
};
|
||||
|
||||
await cacheFile.writeAsString(jsonEncode(cacheData));
|
||||
} catch (e) {
|
||||
// Cache write failed, ignore
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String publicKey, String? secretKey, {String? baseUrl}) async {
|
||||
final base = baseUrl ?? apiBase;
|
||||
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
|
||||
|
|
@ -722,7 +786,20 @@ Future<void> cmdLanguages(Args args) async {
|
|||
final publicKey = keys[0]!;
|
||||
final secretKey = keys[1];
|
||||
|
||||
final result = await apiRequestCurl('/languages', 'GET', null, publicKey, secretKey);
|
||||
// Try to load from cache first
|
||||
var cachedData = await loadLanguagesCache();
|
||||
Map<String, dynamic> result;
|
||||
|
||||
if (cachedData != null) {
|
||||
result = cachedData;
|
||||
} else {
|
||||
// Fetch from API
|
||||
result = await apiRequestCurl('/languages', 'GET', null, publicKey, secretKey);
|
||||
|
||||
// Save to cache
|
||||
await saveLanguagesCache(result);
|
||||
}
|
||||
|
||||
final languages = result['languages'] as List? ?? [];
|
||||
|
||||
if (args.languagesJson) {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ defmodule Un do
|
|||
@reset "\e[0m"
|
||||
|
||||
@portal_base "https://unsandbox.com"
|
||||
@languages_cache_ttl 3600
|
||||
|
||||
@ext_map %{
|
||||
".ex" => "elixir", ".exs" => "elixir", ".erl" => "erlang",
|
||||
|
|
@ -567,13 +568,103 @@ defmodule Un do
|
|||
System.halt(1)
|
||||
end
|
||||
|
||||
# Languages cache functions
|
||||
defp get_languages_cache_path do
|
||||
home = System.get_env("HOME") || "."
|
||||
Path.join([home, ".unsandbox", "languages.json"])
|
||||
end
|
||||
|
||||
defp load_languages_cache do
|
||||
cache_path = get_languages_cache_path()
|
||||
|
||||
case File.read(cache_path) do
|
||||
{:ok, content} ->
|
||||
case Jason.decode(content) do
|
||||
{:ok, data} ->
|
||||
timestamp = Map.get(data, "timestamp", 0)
|
||||
now = System.system_time(:second)
|
||||
|
||||
if now - timestamp < @languages_cache_ttl do
|
||||
Map.get(data, "languages", [])
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
# Fallback to manual JSON parsing for environments without Jason
|
||||
timestamp = extract_json_number(content, "timestamp")
|
||||
now = System.system_time(:second)
|
||||
|
||||
if timestamp != 0 and now - timestamp < @languages_cache_ttl do
|
||||
extract_json_array(content, "languages")
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
nil
|
||||
end
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
# Jason not available, use manual parsing
|
||||
cache_path = get_languages_cache_path()
|
||||
|
||||
case File.read(cache_path) do
|
||||
{:ok, content} ->
|
||||
timestamp = extract_json_number(content, "timestamp")
|
||||
now = System.system_time(:second)
|
||||
|
||||
if timestamp != 0 and now - timestamp < @languages_cache_ttl do
|
||||
extract_json_array(content, "languages")
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp save_languages_cache(languages) do
|
||||
cache_path = get_languages_cache_path()
|
||||
cache_dir = Path.dirname(cache_path)
|
||||
|
||||
# Ensure directory exists
|
||||
File.mkdir_p(cache_dir)
|
||||
|
||||
timestamp = System.system_time(:second)
|
||||
languages_json = "[" <> Enum.map_join(languages, ",", &("\"#{&1}\"")) <> "]"
|
||||
json = "{\"languages\":#{languages_json},\"timestamp\":#{timestamp}}"
|
||||
|
||||
File.write(cache_path, json)
|
||||
end
|
||||
|
||||
defp extract_json_number(json_str, key) do
|
||||
case Regex.run(~r/"#{key}"\s*:\s*(\d+)/, json_str) do
|
||||
[_, value] -> String.to_integer(value)
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
|
||||
# Languages command
|
||||
defp languages_command(args) do
|
||||
api_key = get_api_key()
|
||||
json_output = "--json" in args
|
||||
|
||||
response = curl_get(api_key, "/languages")
|
||||
languages = extract_json_array(response, "languages")
|
||||
# Try to load from cache first
|
||||
languages =
|
||||
case load_languages_cache() do
|
||||
nil ->
|
||||
# Cache miss or expired, fetch from API
|
||||
api_key = get_api_key()
|
||||
response = curl_get(api_key, "/languages")
|
||||
langs = extract_json_array(response, "languages")
|
||||
save_languages_cache(langs)
|
||||
langs
|
||||
|
||||
cached_languages ->
|
||||
cached_languages
|
||||
end
|
||||
|
||||
if json_output do
|
||||
# Output as JSON array
|
||||
|
|
|
|||
|
|
@ -561,23 +561,102 @@ build_spawn_json(Name, Ports) ->
|
|||
end,
|
||||
"{" ++ string:join(lists:reverse(Parts2), ",") ++ "}".
|
||||
|
||||
%% Languages cache TTL (1 hour in seconds)
|
||||
-define(LANGUAGES_CACHE_TTL, 3600).
|
||||
|
||||
%% Get languages cache path
|
||||
get_languages_cache_path() ->
|
||||
Home = os:getenv("HOME"),
|
||||
case Home of
|
||||
false -> "/tmp/.unsandbox/languages.json";
|
||||
_ -> Home ++ "/.unsandbox/languages.json"
|
||||
end.
|
||||
|
||||
%% Load languages from cache
|
||||
load_languages_cache() ->
|
||||
CachePath = get_languages_cache_path(),
|
||||
case file:read_file(CachePath) of
|
||||
{ok, Bin} ->
|
||||
Content = binary_to_list(Bin),
|
||||
Timestamp = extract_json_number(Content, "timestamp"),
|
||||
Now = erlang:system_time(second),
|
||||
if
|
||||
Timestamp > 0 andalso (Now - Timestamp) < ?LANGUAGES_CACHE_TTL ->
|
||||
extract_json_array(Content, "languages");
|
||||
true ->
|
||||
undefined
|
||||
end;
|
||||
{error, _} ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%% Save languages to cache
|
||||
save_languages_cache(Languages) ->
|
||||
CachePath = get_languages_cache_path(),
|
||||
CacheDir = filename:dirname(CachePath),
|
||||
filelib:ensure_dir(CachePath),
|
||||
file:make_dir(CacheDir),
|
||||
Timestamp = erlang:system_time(second),
|
||||
LanguagesJson = "[" ++ string:join(["\"" ++ L ++ "\"" || L <- Languages], ",") ++ "]",
|
||||
Json = "{\"languages\":" ++ LanguagesJson ++ ",\"timestamp\":" ++ integer_to_list(Timestamp) ++ "}",
|
||||
file:write_file(CachePath, Json).
|
||||
|
||||
%% Extract JSON number field
|
||||
extract_json_number(Json, Field) ->
|
||||
Pattern = "\"" ++ Field ++ "\":",
|
||||
case string:str(Json, Pattern) of
|
||||
0 -> 0;
|
||||
Pos ->
|
||||
Start = Pos + length(Pattern),
|
||||
Rest = lists:nthtail(Start - 1, Json),
|
||||
extract_number(Rest)
|
||||
end.
|
||||
|
||||
extract_number(Str) ->
|
||||
extract_number(Str, []).
|
||||
|
||||
extract_number([], Acc) ->
|
||||
case Acc of
|
||||
[] -> 0;
|
||||
_ -> list_to_integer(lists:reverse(Acc))
|
||||
end;
|
||||
extract_number([C | Rest], Acc) when C >= $0, C =< $9 ->
|
||||
extract_number(Rest, [C | Acc]);
|
||||
extract_number(_, Acc) ->
|
||||
case Acc of
|
||||
[] -> 0;
|
||||
_ -> list_to_integer(lists:reverse(Acc))
|
||||
end.
|
||||
|
||||
%% Languages command
|
||||
languages_command(Args) ->
|
||||
ApiKey = get_api_key(),
|
||||
Response = curl_get(ApiKey, "/languages"),
|
||||
JsonOutput = lists:member("--json", Args),
|
||||
|
||||
%% Try to load from cache first
|
||||
Languages = case load_languages_cache() of
|
||||
undefined ->
|
||||
%% Cache miss or expired, fetch from API
|
||||
ApiKey = get_api_key(),
|
||||
Response = curl_get(ApiKey, "/languages"),
|
||||
Langs = extract_json_array(Response, "languages"),
|
||||
save_languages_cache(Langs),
|
||||
Langs;
|
||||
CachedLanguages ->
|
||||
CachedLanguages
|
||||
end,
|
||||
|
||||
if
|
||||
JsonOutput ->
|
||||
%% Output raw JSON array
|
||||
case extract_json_array(Response, "languages") of
|
||||
case Languages of
|
||||
[] -> io:format("[]~n");
|
||||
Languages -> io:format("[~s]~n", [string:join(["\"" ++ L ++ "\"" || L <- Languages], ",")])
|
||||
_ -> io:format("[~s]~n", [string:join(["\"" ++ L ++ "\"" || L <- Languages], ",")])
|
||||
end;
|
||||
true ->
|
||||
%% Output one language per line
|
||||
case extract_json_array(Response, "languages") of
|
||||
case Languages of
|
||||
[] -> ok;
|
||||
Languages -> [io:format("~s~n", [L]) || L <- Languages]
|
||||
_ -> [io:format("~s~n", [L]) || L <- Languages]
|
||||
end
|
||||
end.
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@
|
|||
s" https://unsandbox.com"
|
||||
;
|
||||
|
||||
3600 constant LANGUAGES_CACHE_TTL
|
||||
|
||||
: languages-cache-file ( -- addr len )
|
||||
s" $HOME/.unsandbox/languages.json"
|
||||
;
|
||||
|
||||
\ Extension to language mapping (simple linear search)
|
||||
: ext-lang ( addr len -- addr len | 0 0 )
|
||||
2dup s" .jl" compare 0= if 2drop s" julia" exit then
|
||||
|
|
@ -986,27 +992,54 @@
|
|||
1 (bye)
|
||||
;
|
||||
|
||||
\ Languages list
|
||||
\ Languages list with caching
|
||||
: languages-list ( json-flag -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" CACHE_TTL=3600" r@ write-line throw
|
||||
s" CACHE_FILE=\"$HOME/.unsandbox/languages.json\"" r@ write-line throw
|
||||
s" JSON_OUTPUT=" r@ write-file throw
|
||||
if
|
||||
s" 1" r@ write-line throw
|
||||
else
|
||||
s" 0" r@ write-line throw
|
||||
then
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
\ Check cache first
|
||||
s" if [ -f \"$CACHE_FILE\" ]; then" r@ write-line throw
|
||||
s" CACHE_TS=$(jq -r '.timestamp // 0' \"$CACHE_FILE\" 2>/dev/null)" r@ write-line throw
|
||||
s" CURRENT_TS=$(date +%s)" r@ write-line throw
|
||||
s" AGE=$((CURRENT_TS - CACHE_TS))" r@ write-line throw
|
||||
s" if [ $AGE -lt $CACHE_TTL ]; then" r@ write-line throw
|
||||
s" if [ \"$JSON_OUTPUT\" = \"1\" ]; then" r@ write-line throw
|
||||
s" jq -c '.languages' \"$CACHE_FILE\"" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" jq -r '.languages[]' \"$CACHE_FILE\"" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" exit 0" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
\ Fetch from API
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/languages:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
if
|
||||
\ JSON output
|
||||
s" curl -s -X GET https://api.unsandbox.com/languages -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -c '.languages'" r@ write-line throw
|
||||
else
|
||||
\ One per line
|
||||
s" curl -s -X GET https://api.unsandbox.com/languages -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.languages[]'" r@ write-line throw
|
||||
then
|
||||
s" RESP=$(curl -s -X GET https://api.unsandbox.com/languages -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\")" r@ write-line throw
|
||||
s" LANGS=$(echo \"$RESP\" | jq -c '.languages // []')" r@ write-line throw
|
||||
\ Save to cache
|
||||
s" mkdir -p \"$HOME/.unsandbox\"" r@ write-line throw
|
||||
s" echo \"{\\\"languages\\\":$LANGS,\\\"timestamp\\\":$(date +%s)}\" > \"$CACHE_FILE\"" r@ write-line throw
|
||||
\ Output
|
||||
s" if [ \"$JSON_OUTPUT\" = \"1\" ]; then" r@ write-line throw
|
||||
s" echo \"$LANGS\"" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" echo \"$LANGS\" | jq -r '.[]'" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ module unsandbox_sdk
|
|||
character(len=*), parameter, public :: PORTAL_BASE = 'https://unsandbox.com'
|
||||
integer, parameter, public :: DEFAULT_TTL = 60
|
||||
integer, parameter, public :: DEFAULT_TIMEOUT = 300
|
||||
integer, parameter, public :: LANGUAGES_CACHE_TTL = 3600 ! 1 hour cache TTL
|
||||
|
||||
!--------------------------------------------------------------------------
|
||||
! Type: execution_result
|
||||
|
|
@ -1821,7 +1822,7 @@ contains
|
|||
end subroutine handle_key
|
||||
|
||||
subroutine handle_languages()
|
||||
character(len=4096) :: full_cmd
|
||||
character(len=8192) :: full_cmd
|
||||
character(len=256) :: arg
|
||||
character(len=1024) :: public_key, secret_key
|
||||
integer :: i, stat
|
||||
|
|
@ -1845,23 +1846,47 @@ contains
|
|||
end if
|
||||
|
||||
if (json_mode) then
|
||||
! Output as JSON array
|
||||
write(full_cmd, '(20A)') &
|
||||
! Output as JSON array with caching
|
||||
write(full_cmd, '(40A)') &
|
||||
'CACHE_TTL=3600; ', &
|
||||
'CACHE_FILE="$HOME/.unsandbox/languages.json"; ', &
|
||||
'if [ -f "$CACHE_FILE" ]; then ', &
|
||||
'CACHE_TS=$(jq -r ".timestamp // 0" "$CACHE_FILE" 2>/dev/null); ', &
|
||||
'CURRENT_TS=$(date +%s); ', &
|
||||
'AGE=$((CURRENT_TS - CACHE_TS)); ', &
|
||||
'if [ $AGE -lt $CACHE_TTL ]; then ', &
|
||||
'jq -c ".languages" "$CACHE_FILE"; exit 0; fi; fi; ', &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X GET https://api.unsandbox.com/languages ', &
|
||||
'RESP=$(curl -s -X GET https://api.unsandbox.com/languages ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" | jq -c ".languages"'
|
||||
'-H "X-Signature: $SIG"); ', &
|
||||
'LANGS=$(echo "$RESP" | jq -c ".languages // []"); ', &
|
||||
'mkdir -p "$HOME/.unsandbox"; ', &
|
||||
'echo "{\"languages\":$LANGS,\"timestamp\":$(date +%s)}" > "$CACHE_FILE"; ', &
|
||||
'echo "$LANGS"'
|
||||
else
|
||||
! Output one language per line
|
||||
write(full_cmd, '(20A)') &
|
||||
! Output one language per line with caching
|
||||
write(full_cmd, '(40A)') &
|
||||
'CACHE_TTL=3600; ', &
|
||||
'CACHE_FILE="$HOME/.unsandbox/languages.json"; ', &
|
||||
'if [ -f "$CACHE_FILE" ]; then ', &
|
||||
'CACHE_TS=$(jq -r ".timestamp // 0" "$CACHE_FILE" 2>/dev/null); ', &
|
||||
'CURRENT_TS=$(date +%s); ', &
|
||||
'AGE=$((CURRENT_TS - CACHE_TS)); ', &
|
||||
'if [ $AGE -lt $CACHE_TTL ]; then ', &
|
||||
'jq -r ".languages[]" "$CACHE_FILE"; exit 0; fi; fi; ', &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X GET https://api.unsandbox.com/languages ', &
|
||||
'RESP=$(curl -s -X GET https://api.unsandbox.com/languages ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" | jq -r ".languages[]"'
|
||||
'-H "X-Signature: $SIG"); ', &
|
||||
'LANGS=$(echo "$RESP" | jq -c ".languages // []"); ', &
|
||||
'mkdir -p "$HOME/.unsandbox"; ', &
|
||||
'echo "{\"languages\":$LANGS,\"timestamp\":$(date +%s)}" > "$CACHE_FILE"; ', &
|
||||
'echo "$RESP" | jq -r ".languages[]"'
|
||||
end if
|
||||
|
||||
call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ open System.Security.Cryptography
|
|||
|
||||
let apiBase = "https://api.unsandbox.com"
|
||||
let portalBase = "https://unsandbox.com"
|
||||
let languagesCacheTtl = 3600 // 1 hour in seconds
|
||||
|
||||
let blue = "\x1B[34m"
|
||||
let red = "\x1B[31m"
|
||||
let green = "\x1B[32m"
|
||||
|
|
@ -433,6 +435,38 @@ let apiRequestText (endpoint: string) (method: string) (body: string) (publicKey
|
|||
ex.Message
|
||||
failwithf "HTTP error - %s" errorMsg
|
||||
|
||||
let getLanguagesCachePath () =
|
||||
let home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
|
||||
Path.Combine(home, ".unsandbox", "languages.json")
|
||||
|
||||
let loadLanguagesCache () =
|
||||
let cachePath = getLanguagesCachePath ()
|
||||
if File.Exists(cachePath) then
|
||||
try
|
||||
let content = File.ReadAllText(cachePath)
|
||||
let timestamp = extractJsonValue content "timestamp"
|
||||
match timestamp with
|
||||
| Some ts ->
|
||||
let cacheTime = int64 ts
|
||||
let now = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
if now - cacheTime < int64 languagesCacheTtl then
|
||||
Some content
|
||||
else
|
||||
None
|
||||
| None -> None
|
||||
with _ -> None
|
||||
else
|
||||
None
|
||||
|
||||
let saveLanguagesCache (languages: string) =
|
||||
let cachePath = getLanguagesCachePath ()
|
||||
let cacheDir = Path.GetDirectoryName(cachePath)
|
||||
if not (Directory.Exists(cacheDir)) then
|
||||
Directory.CreateDirectory(cacheDir) |> ignore
|
||||
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
let cacheContent = sprintf "{\"languages\":%s,\"timestamp\":%d}" languages timestamp
|
||||
File.WriteAllText(cachePath, cacheContent)
|
||||
|
||||
let readEnvFile (path: string) =
|
||||
if not (File.Exists(path)) then
|
||||
failwithf "Env file not found: %s" path
|
||||
|
|
@ -735,33 +769,57 @@ let cmdKey (args: Args) =
|
|||
let cmdLanguages (args: Args) =
|
||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||
|
||||
let result = apiRequest "/languages" "GET" None publicKey secretKey
|
||||
// Try to load from cache first
|
||||
let cachedResponse = loadLanguagesCache ()
|
||||
let response =
|
||||
match cachedResponse with
|
||||
| Some cached -> cached
|
||||
| None ->
|
||||
// Fetch from API and cache the result
|
||||
let result = apiRequest "/languages" "GET" None publicKey secretKey
|
||||
// Extract the languages array as a string for caching
|
||||
match result.TryFind "languages" with
|
||||
| Some langs ->
|
||||
let langStr = langs.ToString()
|
||||
let languagesJson =
|
||||
if langStr.StartsWith("[") && langStr.EndsWith("]") then
|
||||
langStr
|
||||
else
|
||||
sprintf "[\"%s\"]" langStr
|
||||
saveLanguagesCache languagesJson
|
||||
| None -> ()
|
||||
// Return the result as a formatted string
|
||||
sprintf "{\"languages\":%s}" (match result.TryFind "languages" with | Some l -> l.ToString() | None -> "[]")
|
||||
|
||||
// Extract languages array from the response
|
||||
match result.TryFind "languages" with
|
||||
| Some langs ->
|
||||
// Parse the languages - they come as a string representation
|
||||
let langStr = langs.ToString()
|
||||
// Simple parsing for array of strings like: python, javascript, ...
|
||||
let languages =
|
||||
if langStr.StartsWith("[") && langStr.EndsWith("]") then
|
||||
langStr.Substring(1, langStr.Length - 2).Split(',')
|
||||
|> Array.map (fun s -> s.Trim().Trim('"'))
|
||||
|> Array.filter (fun s -> not (String.IsNullOrEmpty(s)))
|
||||
// Parse the response (either from cache or fresh)
|
||||
let langStr =
|
||||
match extractJsonValue response "languages" with
|
||||
| Some l -> l
|
||||
| None ->
|
||||
// Try to find the array directly
|
||||
let start = response.IndexOf("[")
|
||||
let endIdx = response.LastIndexOf("]")
|
||||
if start >= 0 && endIdx > start then
|
||||
response.Substring(start, endIdx - start + 1)
|
||||
else
|
||||
[| langStr |]
|
||||
"[]"
|
||||
|
||||
if args.LanguagesJson then
|
||||
// Output as JSON array
|
||||
let jsonArray = sprintf "[%s]" (languages |> Array.map (sprintf "\"%s\"") |> String.concat ",")
|
||||
printfn "%s" jsonArray
|
||||
let languages =
|
||||
if langStr.StartsWith("[") && langStr.EndsWith("]") then
|
||||
langStr.Substring(1, langStr.Length - 2).Split(',')
|
||||
|> Array.map (fun s -> s.Trim().Trim('"'))
|
||||
|> Array.filter (fun s -> not (String.IsNullOrEmpty(s)))
|
||||
else
|
||||
// Output one language per line
|
||||
for lang in languages do
|
||||
printfn "%s" lang
|
||||
| None ->
|
||||
// Fallback: try to extract from raw JSON using regex
|
||||
()
|
||||
[| langStr |]
|
||||
|
||||
if args.LanguagesJson then
|
||||
// Output as JSON array
|
||||
let jsonArray = sprintf "[%s]" (languages |> Array.map (sprintf "\"%s\"") |> String.concat ",")
|
||||
printfn "%s" jsonArray
|
||||
else
|
||||
// Output one language per line
|
||||
for lang in languages do
|
||||
printfn "%s" lang
|
||||
|
||||
let cmdImage (args: Args) =
|
||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||
|
|
|
|||
|
|
@ -65,12 +65,16 @@ import Data.List (isPrefixOf, intercalate)
|
|||
import Data.Char (isDigit, ord)
|
||||
import Text.Printf (printf)
|
||||
import Control.Monad (when, unless, forM_)
|
||||
import Control.Exception (try, catch, IOError)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BSC
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Crypto.Hash.SHA256 (hmac)
|
||||
import Numeric (showHex)
|
||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||
import Data.Time.Clock.POSIX (getPOSIXTime, posixSecondsToUTCTime)
|
||||
import Data.Time.Clock (diffUTCTime)
|
||||
import qualified Data.Time.Clock
|
||||
import qualified System.Posix.Files
|
||||
|
||||
-- API constants
|
||||
apiBase :: String
|
||||
|
|
@ -79,6 +83,9 @@ apiBase = "https://api.unsandbox.com"
|
|||
portalBase :: String
|
||||
portalBase = "https://unsandbox.com"
|
||||
|
||||
languagesCacheTtl :: Int
|
||||
languagesCacheTtl = 3600 -- 1 hour in seconds
|
||||
|
||||
-- ANSI colors
|
||||
blue, red, green, yellow, reset :: String
|
||||
blue = "\x1b[34m"
|
||||
|
|
@ -986,23 +993,79 @@ imageCommand opts = do
|
|||
putStrLn $ green ++ "Image cloned" ++ reset
|
||||
putStrLn stdout
|
||||
|
||||
-- Languages cache functions
|
||||
getLanguagesCachePath :: IO FilePath
|
||||
getLanguagesCachePath = do
|
||||
home <- lookupEnv "HOME"
|
||||
let homeDir = maybe "." id home
|
||||
return $ homeDir ++ "/.unsandbox/languages.json"
|
||||
|
||||
loadLanguagesCache :: IO (Maybe [String])
|
||||
loadLanguagesCache = do
|
||||
cachePath <- getLanguagesCachePath
|
||||
exists <- doesFileExist cachePath
|
||||
if not exists
|
||||
then return Nothing
|
||||
else do
|
||||
-- Check if cache is fresh (< 1 hour old)
|
||||
modTime <- getModificationTime cachePath
|
||||
now <- getCurrentTime
|
||||
let ageSeconds = floor $ diffUTCTime now modTime
|
||||
if ageSeconds >= languagesCacheTtl
|
||||
then return Nothing
|
||||
else do
|
||||
content <- readFile cachePath
|
||||
return $ extractJsonArray content "languages"
|
||||
where
|
||||
doesFileExist path = do
|
||||
result <- try (readFile path) :: IO (Either IOError String)
|
||||
case result of
|
||||
Left _ -> return False
|
||||
Right _ -> return True
|
||||
getModificationTime path = do
|
||||
status <- System.Posix.Files.getFileStatus path
|
||||
return $ posixSecondsToUTCTime $ realToFrac $ modificationTime status
|
||||
getCurrentTime = Data.Time.Clock.getCurrentTime
|
||||
|
||||
saveLanguagesCache :: [String] -> IO ()
|
||||
saveLanguagesCache languages = do
|
||||
cachePath <- getLanguagesCachePath
|
||||
let cacheDir = takeDirectory cachePath
|
||||
-- Create directory if needed
|
||||
createDirectoryIfMissing True cacheDir
|
||||
now <- getPOSIXTime
|
||||
let timestamp = show (floor now :: Integer)
|
||||
let langsJson = "[" ++ intercalate "," (map (\l -> "\"" ++ l ++ "\"") languages) ++ "]"
|
||||
let content = "{\"languages\":" ++ langsJson ++ ",\"timestamp\":" ++ timestamp ++ "}"
|
||||
writeFile cachePath content
|
||||
`catch` (\(_ :: IOError) -> return ()) -- Cache failures are non-fatal
|
||||
where
|
||||
takeDirectory path = reverse $ dropWhile (/= '/') $ reverse path
|
||||
|
||||
-- Languages command
|
||||
languagesCommand :: LanguagesOpts -> IO ()
|
||||
languagesCommand opts = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/languages"
|
||||
|
||||
-- Try cache first
|
||||
cached <- loadLanguagesCache
|
||||
langs <- case cached of
|
||||
Just languages -> return languages
|
||||
Nothing -> do
|
||||
(_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/languages"
|
||||
let languages = maybe [] id (extractJsonArray stdout "languages")
|
||||
-- Save to cache
|
||||
when (not (null languages)) $ saveLanguagesCache languages
|
||||
return languages
|
||||
|
||||
let jsonOutput = langJson opts
|
||||
if jsonOutput
|
||||
then do
|
||||
-- Extract languages array and print as JSON
|
||||
case extractJsonArray stdout "languages" of
|
||||
Just langs -> putStrLn $ "[" ++ intercalate "," (map (\l -> "\"" ++ l ++ "\"") langs) ++ "]"
|
||||
Nothing -> putStrLn stdout
|
||||
putStrLn $ "[" ++ intercalate "," (map (\l -> "\"" ++ l ++ "\"") langs) ++ "]"
|
||||
else do
|
||||
-- Print each language on its own line
|
||||
case extractJsonArray stdout "languages" of
|
||||
Just langs -> mapM_ putStrLn langs
|
||||
Nothing -> putStrLn stdout
|
||||
mapM_ putStrLn langs
|
||||
|
||||
-- Extract JSON array of strings from response
|
||||
extractJsonArray :: String -> String -> Maybe [String]
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ const RESET = "\033[0m"
|
|||
|
||||
const API_BASE = "https://api.unsandbox.com"
|
||||
const PORTAL_BASE = "https://unsandbox.com"
|
||||
const LANGUAGES_CACHE_TTL = 3600
|
||||
|
||||
function detect_language(filename::String)::String
|
||||
ext = lowercase(match(r"\.[^.]+$", filename).match)
|
||||
|
|
@ -762,11 +763,65 @@ function validate_key(api_key::String)
|
|||
end
|
||||
end
|
||||
|
||||
function cmd_languages(args)
|
||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
||||
function get_languages_cache_path()::String
|
||||
home = get(ENV, "HOME", ".")
|
||||
return joinpath(home, ".unsandbox", "languages.json")
|
||||
end
|
||||
|
||||
result = api_request("/languages", public_key, secret_key)
|
||||
langs = get(result, "languages", [])
|
||||
function load_languages_cache()::Union{Vector{String}, Nothing}
|
||||
cache_path = get_languages_cache_path()
|
||||
|
||||
if !isfile(cache_path)
|
||||
return nothing
|
||||
end
|
||||
|
||||
try
|
||||
content = read(cache_path, String)
|
||||
data = JSON.parse(content)
|
||||
timestamp = get(data, "timestamp", 0)
|
||||
now = Int64(floor(time()))
|
||||
|
||||
if now - timestamp < LANGUAGES_CACHE_TTL
|
||||
langs = get(data, "languages", [])
|
||||
return [string(l) for l in langs]
|
||||
else
|
||||
return nothing
|
||||
end
|
||||
catch
|
||||
return nothing
|
||||
end
|
||||
end
|
||||
|
||||
function save_languages_cache(languages::Vector)
|
||||
cache_path = get_languages_cache_path()
|
||||
cache_dir = dirname(cache_path)
|
||||
|
||||
# Ensure directory exists
|
||||
mkpath(cache_dir)
|
||||
|
||||
timestamp = Int64(floor(time()))
|
||||
data = Dict("languages" => languages, "timestamp" => timestamp)
|
||||
|
||||
try
|
||||
open(cache_path, "w") do f
|
||||
write(f, JSON.json(data))
|
||||
end
|
||||
catch
|
||||
# Ignore write errors
|
||||
end
|
||||
end
|
||||
|
||||
function cmd_languages(args)
|
||||
# Try to load from cache first
|
||||
langs = load_languages_cache()
|
||||
|
||||
if langs === nothing
|
||||
# Cache miss or expired, fetch from API
|
||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
||||
result = api_request("/languages", public_key, secret_key)
|
||||
langs = get(result, "languages", [])
|
||||
save_languages_cache(langs)
|
||||
end
|
||||
|
||||
if args["json"]
|
||||
println(JSON.json(langs))
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import javax.crypto.spec.SecretKeySpec
|
|||
|
||||
val API_BASE = "https://api.unsandbox.com"
|
||||
val PORTAL_BASE = "https://unsandbox.com"
|
||||
val LANGUAGES_CACHE_TTL = 3600L // 1 hour in seconds
|
||||
val BLUE = "\u001B[34m"
|
||||
val RED = "\u001B[31m"
|
||||
val GREEN = "\u001B[32m"
|
||||
|
|
@ -475,12 +476,56 @@ fun cmdService(args: Args) {
|
|||
exitProcess(1)
|
||||
}
|
||||
|
||||
fun getLanguagesCachePath(): String {
|
||||
val home = System.getenv("HOME") ?: System.getProperty("user.home") ?: "."
|
||||
return "$home/.unsandbox/languages.json"
|
||||
}
|
||||
|
||||
fun loadLanguagesCache(): List<String>? {
|
||||
try {
|
||||
val cacheFile = File(getLanguagesCachePath())
|
||||
if (!cacheFile.exists()) return null
|
||||
|
||||
// Check if cache is fresh (< 1 hour old)
|
||||
val ageSeconds = (System.currentTimeMillis() - cacheFile.lastModified()) / 1000
|
||||
if (ageSeconds >= LANGUAGES_CACHE_TTL) return null
|
||||
|
||||
val content = cacheFile.readText()
|
||||
val parsed = parseJson(content)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return parsed["languages"] as? List<String>
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLanguagesCache(languages: List<String>) {
|
||||
try {
|
||||
val cachePath = getLanguagesCachePath()
|
||||
val cacheDir = File(cachePath).parentFile
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs()
|
||||
}
|
||||
val timestamp = System.currentTimeMillis() / 1000
|
||||
val data = mapOf("languages" to languages, "timestamp" to timestamp)
|
||||
File(cachePath).writeText(toJson(data))
|
||||
} catch (e: Exception) {
|
||||
// Cache failures are non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
fun cmdLanguages(args: Args) {
|
||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
val result = apiRequest("/languages", "GET", null, publicKey, secretKey)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val languages = result["languages"] as? List<String> ?: emptyList()
|
||||
// Try cache first
|
||||
var languages = loadLanguagesCache()
|
||||
if (languages == null) {
|
||||
val result = apiRequest("/languages", "GET", null, publicKey, secretKey)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
languages = result["languages"] as? List<String> ?: emptyList()
|
||||
// Save to cache
|
||||
saveLanguagesCache(languages)
|
||||
}
|
||||
|
||||
if (args.jsonOutput) {
|
||||
println(toJson(languages))
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@
|
|||
|
||||
(defparameter *portal-base* "https://unsandbox.com")
|
||||
|
||||
(defparameter *languages-cache-ttl* 3600) ;; 1 hour in seconds
|
||||
|
||||
(defparameter *ext-map*
|
||||
'((".hs" . "haskell") (".ml" . "ocaml") (".clj" . "clojure")
|
||||
(".scm" . "scheme") (".lisp" . "commonlisp") (".erl" . "erlang")
|
||||
|
|
@ -555,10 +557,52 @@
|
|||
(uiop:quit 1))))))
|
||||
(nreverse files)))
|
||||
|
||||
(defun get-languages-cache-path ()
|
||||
"Get the path to the languages cache file"
|
||||
(let ((home (uiop:getenv "HOME")))
|
||||
(format nil "~a/.unsandbox/languages.json" home)))
|
||||
|
||||
(defun load-languages-cache ()
|
||||
"Load languages from cache if valid"
|
||||
(let ((cache-path (get-languages-cache-path)))
|
||||
(when (probe-file cache-path)
|
||||
(handler-case
|
||||
(let* ((content (read-file cache-path))
|
||||
(timestamp-str (parse-json-field content "timestamp")))
|
||||
(when timestamp-str
|
||||
(let* ((cache-time (parse-integer timestamp-str))
|
||||
(now (get-universal-time)))
|
||||
(when (< (- now cache-time) *languages-cache-ttl*)
|
||||
content))))
|
||||
(error () nil)))))
|
||||
|
||||
(defun save-languages-cache (languages-json)
|
||||
"Save languages to cache"
|
||||
(let* ((cache-path (get-languages-cache-path))
|
||||
(cache-dir (directory-namestring cache-path)))
|
||||
(ensure-directories-exist cache-path)
|
||||
(let* ((timestamp (get-universal-time))
|
||||
(cache-content (format nil "{\"languages\":~a,\"timestamp\":~a}" languages-json timestamp)))
|
||||
(with-open-file (stream cache-path :direction :output :if-exists :supersede)
|
||||
(write-string cache-content stream)))))
|
||||
|
||||
(defun languages-cmd (json-output)
|
||||
"List available programming languages"
|
||||
(let* ((api-key (get-api-key))
|
||||
(response (curl-get api-key "/languages"))
|
||||
;; Try cache first
|
||||
(cached (load-languages-cache))
|
||||
(response (if cached
|
||||
cached
|
||||
(let ((resp (curl-get api-key "/languages")))
|
||||
;; Extract and cache the languages array
|
||||
(let ((start (search "\"languages\":[" resp)))
|
||||
(when start
|
||||
(let* ((array-start (+ start (length "\"languages\":")))
|
||||
(array-end (position #\] resp :start array-start)))
|
||||
(when array-end
|
||||
(let ((languages-json (subseq resp array-start (1+ array-end))))
|
||||
(save-languages-cache languages-json))))))
|
||||
resp)))
|
||||
(languages-start (search "\"languages\":[" response)))
|
||||
(if json-output
|
||||
;; JSON output - extract and print the languages array
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const
|
|||
GREEN = "\x1b[32m"
|
||||
YELLOW = "\x1b[33m"
|
||||
RESET = "\x1b[0m"
|
||||
LANGUAGES_CACHE_TTL = 3600 # 1 hour in seconds
|
||||
|
||||
let langMap = {
|
||||
".py": "python", ".js": "javascript", ".ts": "typescript",
|
||||
|
|
@ -97,6 +98,82 @@ proc computeHmac(secretKey: string, message: string): string =
|
|||
proc getTimestamp(): string =
|
||||
result = $toUnix(getTime())
|
||||
|
||||
proc getLanguagesCachePath(): string =
|
||||
let home = getEnv("HOME", "")
|
||||
if home == "":
|
||||
return ""
|
||||
return joinPath(home, ".unsandbox", "languages.json")
|
||||
|
||||
proc loadLanguagesCache(): string =
|
||||
let cachePath = getLanguagesCachePath()
|
||||
if cachePath == "" or not fileExists(cachePath):
|
||||
return ""
|
||||
|
||||
try:
|
||||
let content = readFile(cachePath)
|
||||
# Parse timestamp from JSON
|
||||
let tsStart = content.find("\"timestamp\":")
|
||||
if tsStart < 0:
|
||||
return ""
|
||||
|
||||
let numStart = tsStart + 12 # Length of "\"timestamp\":"
|
||||
var numEnd = numStart
|
||||
while numEnd < content.len and content[numEnd] in {'0'..'9'}:
|
||||
inc numEnd
|
||||
|
||||
if numEnd == numStart:
|
||||
return ""
|
||||
|
||||
let cachedTime = parseInt(content[numStart..<numEnd])
|
||||
let currentTime = toUnix(getTime())
|
||||
|
||||
# Check if cache is still valid (within TTL)
|
||||
if currentTime - cachedTime < LANGUAGES_CACHE_TTL:
|
||||
return content
|
||||
except:
|
||||
# Cache read failed, return empty to fetch fresh
|
||||
discard
|
||||
|
||||
return ""
|
||||
|
||||
proc saveLanguagesCache(response: string) =
|
||||
let cachePath = getLanguagesCachePath()
|
||||
if cachePath == "":
|
||||
return
|
||||
|
||||
try:
|
||||
# Ensure directory exists
|
||||
let cacheDir = parentDir(cachePath)
|
||||
if not dirExists(cacheDir):
|
||||
createDir(cacheDir)
|
||||
|
||||
# Find the languages array in response
|
||||
let langStart = response.find("\"languages\":")
|
||||
if langStart < 0:
|
||||
return
|
||||
|
||||
var bracketStart = response.find("[", langStart)
|
||||
if bracketStart < 0:
|
||||
return
|
||||
|
||||
# Find matching closing bracket
|
||||
var depth = 1
|
||||
var bracketEnd = bracketStart + 1
|
||||
while bracketEnd < response.len and depth > 0:
|
||||
if response[bracketEnd] == '[': inc depth
|
||||
elif response[bracketEnd] == ']': dec depth
|
||||
inc bracketEnd
|
||||
|
||||
let languagesArray = response[bracketStart..<bracketEnd]
|
||||
|
||||
# Build cache JSON with timestamp
|
||||
let timestamp = toUnix(getTime())
|
||||
let cacheJson = fmt"""{{"languages":{languagesArray},"timestamp":{timestamp}}}"""
|
||||
writeFile(cachePath, cacheJson)
|
||||
except:
|
||||
# Cache write failed, ignore
|
||||
discard
|
||||
|
||||
proc buildAuthHeaders(meth: string, path: string, body: string, publicKey: string, secretKey: string): string =
|
||||
if secretKey == "":
|
||||
# Legacy mode: use public_key as bearer token
|
||||
|
|
@ -565,9 +642,17 @@ proc cmdKey(extend: bool, publicKey: string, secretKey: string) =
|
|||
echo "Error: " & response[errStart..<errEnd]
|
||||
|
||||
proc cmdLanguages(jsonOutput: bool, publicKey: string, secretKey: string) =
|
||||
let authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/languages' {authHeaders}"""
|
||||
let response = execCurl(cmd)
|
||||
# Try to load from cache first
|
||||
var response = loadLanguagesCache()
|
||||
|
||||
if response == "":
|
||||
# Fetch from API
|
||||
let authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/languages' {authHeaders}"""
|
||||
response = execCurl(cmd)
|
||||
|
||||
# Save to cache
|
||||
saveLanguagesCache(response)
|
||||
|
||||
if jsonOutput:
|
||||
# Extract languages array and output as JSON
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ let default_timeout = 300
|
|||
(** Default TTL for code execution *)
|
||||
let default_ttl = 60
|
||||
|
||||
(** Languages cache TTL in seconds (1 hour) *)
|
||||
let languages_cache_ttl = 3600
|
||||
|
||||
(* ANSI colors *)
|
||||
let blue = "\x1b[34m"
|
||||
let red = "\x1b[31m"
|
||||
|
|
@ -199,6 +202,41 @@ let get_extension filename =
|
|||
String.sub filename dot_pos (String.length filename - dot_pos)
|
||||
with Not_found -> ""
|
||||
|
||||
(** Get languages cache file path *)
|
||||
let get_languages_cache_path () =
|
||||
let home = try Sys.getenv "HOME" with Not_found -> "." in
|
||||
Filename.concat home ".unsandbox/languages.json"
|
||||
|
||||
(** Load languages from cache if valid *)
|
||||
let load_languages_cache () =
|
||||
let cache_path = get_languages_cache_path () in
|
||||
if Sys.file_exists cache_path then
|
||||
try
|
||||
let content = read_file cache_path in
|
||||
let timestamp = extract_json_int content "timestamp" in
|
||||
match timestamp with
|
||||
| Some ts ->
|
||||
let now = int_of_float (Unix.time ()) in
|
||||
if now - ts < languages_cache_ttl then
|
||||
Some content
|
||||
else
|
||||
None
|
||||
| None -> None
|
||||
with _ -> None
|
||||
else
|
||||
None
|
||||
|
||||
(** Save languages to cache *)
|
||||
let save_languages_cache languages_json =
|
||||
let cache_path = get_languages_cache_path () in
|
||||
let cache_dir = Filename.dirname cache_path in
|
||||
(try Unix.mkdir cache_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
|
||||
let timestamp = int_of_float (Unix.time ()) in
|
||||
let cache_content = Printf.sprintf "{\"languages\":%s,\"timestamp\":%d}" languages_json timestamp in
|
||||
let oc = open_out cache_path in
|
||||
output_string oc cache_content;
|
||||
close_out oc
|
||||
|
||||
(** Escape JSON string *)
|
||||
let escape_json s =
|
||||
let buf = Buffer.create (String.length s) in
|
||||
|
|
@ -717,13 +755,27 @@ let image ?public_key ?secret_key ?(model="") ?(size="1024x1024") ?(quality="sta
|
|||
|
||||
(**
|
||||
Get list of supported programming languages.
|
||||
Uses cached data if available and not expired (1 hour TTL).
|
||||
|
||||
@param public_key API public key (optional)
|
||||
@param secret_key API secret key (optional)
|
||||
@return JSON response with languages list
|
||||
*)
|
||||
let languages ?public_key ?secret_key () =
|
||||
api_get ?public_key ?secret_key "/languages"
|
||||
match load_languages_cache () with
|
||||
| Some cached -> cached
|
||||
| None ->
|
||||
let response = api_get ?public_key ?secret_key "/languages" in
|
||||
(* Extract and cache the languages array *)
|
||||
let langs_pattern = "\"languages\":\\s*\\[\\([^]]*\\)\\]" in
|
||||
let langs_regex = Str.regexp langs_pattern in
|
||||
(try
|
||||
let _ = Str.search_forward langs_regex response 0 in
|
||||
let langs_str = Str.matched_group 1 response in
|
||||
let languages_json = "[" ^ langs_str ^ "]" in
|
||||
save_languages_cache languages_json
|
||||
with Not_found -> ());
|
||||
response
|
||||
|
||||
(* ============================================================================
|
||||
Client Module
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
|
||||
$API_BASE = "https://api.unsandbox.com"
|
||||
$PORTAL_BASE = "https://unsandbox.com"
|
||||
$LANGUAGES_CACHE_TTL = 3600 # 1 hour in seconds
|
||||
|
||||
$EXT_MAP = @{
|
||||
".ps1" = "powershell"; ".py" = "python"; ".js" = "javascript"
|
||||
|
|
@ -398,13 +399,69 @@ function Invoke-Session {
|
|||
$result | ConvertTo-Json -Depth 5
|
||||
}
|
||||
|
||||
function Get-LanguagesCachePath {
|
||||
$cacheDir = Join-Path $env:HOME ".unsandbox"
|
||||
if (-not (Test-Path $cacheDir)) {
|
||||
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
||||
}
|
||||
return Join-Path $cacheDir "languages.json"
|
||||
}
|
||||
|
||||
function Get-CachedLanguages {
|
||||
$cachePath = Get-LanguagesCachePath
|
||||
if (-not (Test-Path $cachePath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$cacheContent = Get-Content -Raw $cachePath | ConvertFrom-Json
|
||||
$currentTime = [int][double]::Parse((Get-Date -UFormat %s))
|
||||
|
||||
if ($cacheContent.timestamp -and ($currentTime - $cacheContent.timestamp) -lt $LANGUAGES_CACHE_TTL) {
|
||||
return $cacheContent.languages
|
||||
}
|
||||
} catch {
|
||||
# Cache is invalid, return null
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Save-LanguagesCache {
|
||||
param($Languages)
|
||||
|
||||
$cachePath = Get-LanguagesCachePath
|
||||
$timestamp = [int][double]::Parse((Get-Date -UFormat %s))
|
||||
|
||||
$cacheData = @{
|
||||
languages = $Languages
|
||||
timestamp = $timestamp
|
||||
}
|
||||
|
||||
try {
|
||||
$cacheData | ConvertTo-Json -Compress | Set-Content -Path $cachePath
|
||||
} catch {
|
||||
# Silently fail if we can't write cache
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Languages {
|
||||
param($Args)
|
||||
|
||||
$jsonOutput = $Args -contains "--json"
|
||||
|
||||
$result = Invoke-Api -Endpoint "/languages"
|
||||
$languages = $result.languages
|
||||
# Check cache first
|
||||
$languages = Get-CachedLanguages
|
||||
|
||||
if (-not $languages) {
|
||||
# Fetch from API
|
||||
$result = Invoke-Api -Endpoint "/languages"
|
||||
$languages = $result.languages
|
||||
|
||||
# Save to cache
|
||||
if ($languages) {
|
||||
Save-LanguagesCache -Languages $languages
|
||||
}
|
||||
}
|
||||
|
||||
if ($jsonOutput) {
|
||||
# Output as JSON array
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@
|
|||
|
||||
% Constants
|
||||
portal_base('https://unsandbox.com').
|
||||
languages_cache_ttl(3600). % 1 hour cache TTL
|
||||
|
||||
% Get languages cache file path
|
||||
languages_cache_file(Path) :-
|
||||
getenv('HOME', Home),
|
||||
atomic_list_concat([Home, '/.unsandbox/languages.json'], Path).
|
||||
|
||||
% Extension to language mapping
|
||||
ext_lang('.jl', 'julia').
|
||||
|
|
@ -342,22 +348,64 @@ validate_key(Extend) :-
|
|||
),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Languages command
|
||||
languages_command(JsonOutput) :-
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
( JsonOutput = true
|
||||
-> % Output as JSON array
|
||||
format(atom(Cmd),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/languages:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/languages -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -c ".languages // []"',
|
||||
[SecretKey, PublicKey])
|
||||
; % Output one language per line
|
||||
format(atom(Cmd),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/languages:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/languages -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r ".languages[]"',
|
||||
[SecretKey, PublicKey])
|
||||
% Read languages cache
|
||||
read_languages_cache(Languages) :-
|
||||
languages_cache_file(CacheFile),
|
||||
exists_file(CacheFile),
|
||||
languages_cache_ttl(TTL),
|
||||
% Read cache file and check timestamp
|
||||
format(atom(Cmd),
|
||||
'CACHE=$(cat "~w" 2>/dev/null); if [ -n "$CACHE" ]; then TIMESTAMP=$(echo "$CACHE" | jq -r ".timestamp // 0"); CURRENT=$(date +%s); AGE=$((CURRENT - TIMESTAMP)); if [ $AGE -lt ~w ]; then echo "$CACHE" | jq -c ".languages // []"; fi; fi',
|
||||
[CacheFile, TTL]),
|
||||
setup_call_cleanup(
|
||||
process_create(path(sh), ['-c', Cmd], [stdout(pipe(Out))]),
|
||||
read_string(Out, _, Output),
|
||||
close(Out)
|
||||
),
|
||||
Output \= '',
|
||||
Output \= '\n',
|
||||
Languages = Output.
|
||||
|
||||
% Write languages cache
|
||||
write_languages_cache(LanguagesJson) :-
|
||||
languages_cache_file(CacheFile),
|
||||
getenv('HOME', Home),
|
||||
atomic_list_concat([Home, '/.unsandbox'], CacheDir),
|
||||
format(atom(MkdirCmd), 'mkdir -p "~w"', [CacheDir]),
|
||||
shell(MkdirCmd, 0),
|
||||
format(atom(Cmd),
|
||||
'TIMESTAMP=$(date +%s); echo \'\'\'\'{"languages":~w,"timestamp":\'\'\'\'$TIMESTAMP\'\'\'\'}\'\'\'\'> "~w"',
|
||||
[LanguagesJson, CacheFile]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Languages command with caching
|
||||
languages_command(JsonOutput) :-
|
||||
% Try to read from cache first
|
||||
( read_languages_cache(CachedLangs),
|
||||
CachedLangs \= ''
|
||||
-> % Use cached data
|
||||
( JsonOutput = true
|
||||
-> format('~w~n', [CachedLangs])
|
||||
; % Parse and print one per line
|
||||
format(atom(Cmd), 'echo \'\'~w\'\' | jq -r ".[]"', [CachedLangs]),
|
||||
shell(Cmd, 0)
|
||||
)
|
||||
; % No valid cache, fetch from API
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
( JsonOutput = true
|
||||
-> % Output as JSON array and cache
|
||||
format(atom(Cmd),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/languages:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X GET https://api.unsandbox.com/languages -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE"); LANGS=$(echo "$RESP" | jq -c ".languages // []"); echo "$LANGS"; CACHE_DIR="$HOME/.unsandbox"; mkdir -p "$CACHE_DIR"; echo "{\\\"languages\\\":$LANGS,\\\"timestamp\\\":$(date +%s)}" > "$CACHE_DIR/languages.json"',
|
||||
[SecretKey, PublicKey])
|
||||
; % Output one language per line and cache
|
||||
format(atom(Cmd),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/languages:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X GET https://api.unsandbox.com/languages -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE"); LANGS=$(echo "$RESP" | jq -c ".languages // []"); echo "$RESP" | jq -r ".languages[]"; CACHE_DIR="$HOME/.unsandbox"; mkdir -p "$CACHE_DIR"; echo "{\\\"languages\\\":$LANGS,\\\"timestamp\\\":$(date +%s)}" > "$CACHE_DIR/languages.json"',
|
||||
[SecretKey, PublicKey])
|
||||
),
|
||||
shell(Cmd, 0)
|
||||
).
|
||||
|
||||
% Handle languages subcommand
|
||||
handle_languages(['--json'|_]) :- languages_command(true).
|
||||
handle_languages(_) :- languages_command(false).
|
||||
|
|
|
|||
|
|
@ -95,6 +95,11 @@ API_BASE <- "https://api.unsandbox.com"
|
|||
#' @export
|
||||
PORTAL_BASE <- "https://unsandbox.com"
|
||||
|
||||
#' @title Languages Cache TTL
|
||||
#' @description Cache time-to-live in seconds (1 hour)
|
||||
#' @export
|
||||
LANGUAGES_CACHE_TTL <- 3600
|
||||
|
||||
MAX_ENV_CONTENT_SIZE <- 65536
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -1002,13 +1007,88 @@ cmd_session <- function(args) {
|
|||
cat(sprintf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n", YELLOW, RESET))
|
||||
}
|
||||
|
||||
cmd_languages <- function(args) {
|
||||
keys <- get_api_keys(args$api_key)
|
||||
public_key <- keys$public_key
|
||||
secret_key <- keys$secret_key
|
||||
#' Get Languages Cache Path
|
||||
#'
|
||||
#' Returns the path to the languages cache file.
|
||||
#'
|
||||
#' @return Path to ~/.unsandbox/languages.json
|
||||
#' @keywords internal
|
||||
get_languages_cache_path <- function() {
|
||||
home <- Sys.getenv("HOME")
|
||||
if (home == "") {
|
||||
home <- "."
|
||||
}
|
||||
return(file.path(home, ".unsandbox", "languages.json"))
|
||||
}
|
||||
|
||||
result <- api_request("/languages", public_key, secret_key)
|
||||
langs <- result$languages
|
||||
#' Load Languages Cache
|
||||
#'
|
||||
#' Loads languages from the cache file if it exists and is not expired.
|
||||
#'
|
||||
#' @return Vector of language names, or NULL if cache is missing/expired
|
||||
#' @keywords internal
|
||||
load_languages_cache <- function() {
|
||||
cache_path <- get_languages_cache_path()
|
||||
|
||||
if (!file.exists(cache_path)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
tryCatch({
|
||||
content <- paste(readLines(cache_path, warn = FALSE), collapse = "\n")
|
||||
data <- fromJSON(content)
|
||||
timestamp <- data$timestamp
|
||||
now <- as.integer(Sys.time())
|
||||
|
||||
if (!is.null(timestamp) && (now - timestamp) < LANGUAGES_CACHE_TTL) {
|
||||
return(data$languages)
|
||||
} else {
|
||||
return(NULL)
|
||||
}
|
||||
}, error = function(e) {
|
||||
return(NULL)
|
||||
})
|
||||
}
|
||||
|
||||
#' Save Languages Cache
|
||||
#'
|
||||
#' Saves languages to the cache file.
|
||||
#'
|
||||
#' @param languages Vector of language names
|
||||
#' @keywords internal
|
||||
save_languages_cache <- function(languages) {
|
||||
cache_path <- get_languages_cache_path()
|
||||
cache_dir <- dirname(cache_path)
|
||||
|
||||
# Ensure directory exists
|
||||
if (!dir.exists(cache_dir)) {
|
||||
dir.create(cache_dir, recursive = TRUE, showWarnings = FALSE)
|
||||
}
|
||||
|
||||
timestamp <- as.integer(Sys.time())
|
||||
data <- list(languages = languages, timestamp = timestamp)
|
||||
|
||||
tryCatch({
|
||||
writeLines(toJSON(data, auto_unbox = TRUE), cache_path)
|
||||
}, error = function(e) {
|
||||
# Ignore write errors
|
||||
})
|
||||
}
|
||||
|
||||
cmd_languages <- function(args) {
|
||||
# Try to load from cache first
|
||||
langs <- load_languages_cache()
|
||||
|
||||
if (is.null(langs)) {
|
||||
# Cache miss or expired, fetch from API
|
||||
keys <- get_api_keys(args$api_key)
|
||||
public_key <- keys$public_key
|
||||
secret_key <- keys$secret_key
|
||||
|
||||
result <- api_request("/languages", public_key, secret_key)
|
||||
langs <- result$languages
|
||||
save_languages_cache(langs)
|
||||
}
|
||||
|
||||
if (!is.null(args$json_output) && args$json_output) {
|
||||
# JSON output - print as JSON array
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@
|
|||
|
||||
(define portal-base "https://unsandbox.com")
|
||||
|
||||
(define languages-cache-ttl 3600) ;; 1 hour in seconds
|
||||
|
||||
(define ext-map
|
||||
'((".hs" . "haskell") (".ml" . "ocaml") (".clj" . "clojure")
|
||||
(".scm" . "scheme") (".lisp" . "commonlisp") (".erl" . "erlang")
|
||||
|
|
@ -396,9 +398,55 @@
|
|||
(close-pipe port)
|
||||
(filter (lambda (s) (> (string-length s) 0)) result)))
|
||||
|
||||
(define (get-languages-cache-path)
|
||||
"Get the path to the languages cache file"
|
||||
(let ((home (getenv "HOME")))
|
||||
(string-append home "/.unsandbox/languages.json")))
|
||||
|
||||
(define (load-languages-cache)
|
||||
"Load languages from cache if valid"
|
||||
(let ((cache-path (get-languages-cache-path)))
|
||||
(if (file-exists? cache-path)
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((content (read-file cache-path))
|
||||
(timestamp-str (json-extract-string content "timestamp")))
|
||||
(if timestamp-str
|
||||
(let* ((cache-time (string->number timestamp-str))
|
||||
(now (current-time)))
|
||||
(if (< (- now cache-time) languages-cache-ttl)
|
||||
content
|
||||
#f))
|
||||
#f)))
|
||||
(lambda args #f))
|
||||
#f)))
|
||||
|
||||
(define (save-languages-cache languages-json)
|
||||
"Save languages to cache"
|
||||
(let* ((cache-path (get-languages-cache-path))
|
||||
(cache-dir (dirname cache-path)))
|
||||
;; Create directory if it doesn't exist
|
||||
(catch #t
|
||||
(lambda () (mkdir cache-dir))
|
||||
(lambda args #f))
|
||||
(let* ((timestamp (current-time))
|
||||
(cache-content (format #f "{\"languages\":~a,\"timestamp\":~a}" languages-json timestamp)))
|
||||
(call-with-output-file cache-path
|
||||
(lambda (port) (display cache-content port))))))
|
||||
|
||||
(define (languages-cmd json-output)
|
||||
(let* ((api-key (get-api-key))
|
||||
(response (curl-get api-key "/languages"))
|
||||
;; Try cache first
|
||||
(cached (load-languages-cache))
|
||||
(response (if cached
|
||||
cached
|
||||
(let ((resp (curl-get api-key "/languages")))
|
||||
;; Extract and cache the languages array
|
||||
(let ((langs (json-extract-array resp "languages")))
|
||||
(when (not (null? langs))
|
||||
(let ((languages-json (string-append "[" (string-join (map (lambda (l) (format #f "\"~a\"" l)) langs) ",") "]")))
|
||||
(save-languages-cache languages-json))))
|
||||
resp)))
|
||||
(langs (json-extract-array response "languages")))
|
||||
(if json-output
|
||||
;; JSON array output
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ package require sha256
|
|||
|
||||
set API_BASE "https://api.unsandbox.com"
|
||||
set PORTAL_BASE "https://unsandbox.com"
|
||||
set LANGUAGES_CACHE_TTL 3600
|
||||
set LANGUAGES_CACHE_FILE [file join $::env(HOME) ".unsandbox" "languages.json"]
|
||||
set BLUE "\033\[34m"
|
||||
set RED "\033\[31m"
|
||||
set GREEN "\033\[32m"
|
||||
|
|
@ -573,8 +575,57 @@ proc cmd_session {args} {
|
|||
puts "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}"
|
||||
}
|
||||
|
||||
proc read_languages_cache {} {
|
||||
if {![file exists $::LANGUAGES_CACHE_FILE]} {
|
||||
return {}
|
||||
}
|
||||
|
||||
if {[catch {open $::LANGUAGES_CACHE_FILE r} fp]} {
|
||||
return {}
|
||||
}
|
||||
set content [read $fp]
|
||||
close $fp
|
||||
|
||||
if {[catch {::json::json2dict $content} cache_data]} {
|
||||
return {}
|
||||
}
|
||||
|
||||
# Check if cache is valid (within TTL)
|
||||
if {[dict exists $cache_data timestamp]} {
|
||||
set cache_time [dict get $cache_data timestamp]
|
||||
set current_time [clock seconds]
|
||||
if {($current_time - $cache_time) < $::LANGUAGES_CACHE_TTL} {
|
||||
if {[dict exists $cache_data languages]} {
|
||||
return [dict get $cache_data languages]
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
proc write_languages_cache {languages} {
|
||||
# Ensure ~/.unsandbox directory exists
|
||||
set cache_dir [file dirname $::LANGUAGES_CACHE_FILE]
|
||||
if {![file exists $cache_dir]} {
|
||||
file mkdir $cache_dir
|
||||
}
|
||||
|
||||
# Build cache JSON
|
||||
set json_langs [list]
|
||||
foreach lang $languages {
|
||||
lappend json_langs [::json::write string $lang]
|
||||
}
|
||||
set langs_array [::json::write array {*}$json_langs]
|
||||
set timestamp [clock seconds]
|
||||
set cache_json [::json::write object languages $langs_array timestamp $timestamp]
|
||||
|
||||
# Write cache file
|
||||
set fp [open $::LANGUAGES_CACHE_FILE w]
|
||||
puts -nonewline $fp $cache_json
|
||||
close $fp
|
||||
}
|
||||
|
||||
proc cmd_languages {args} {
|
||||
lassign [get_api_keys] public_key secret_key
|
||||
set json_output 0
|
||||
|
||||
# Parse arguments
|
||||
|
|
@ -585,9 +636,31 @@ proc cmd_languages {args} {
|
|||
}
|
||||
}
|
||||
|
||||
# Try to read from cache first
|
||||
set cached_langs [read_languages_cache]
|
||||
if {[llength $cached_langs] > 0} {
|
||||
if {$json_output} {
|
||||
set json_langs [list]
|
||||
foreach lang $cached_langs {
|
||||
lappend json_langs [::json::write string $lang]
|
||||
}
|
||||
puts [::json::write array {*}$json_langs]
|
||||
} else {
|
||||
foreach lang $cached_langs {
|
||||
puts $lang
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# No valid cache, fetch from API
|
||||
lassign [get_api_keys] public_key secret_key
|
||||
set result [api_request "/languages" "GET" {} $public_key $secret_key]
|
||||
set langs [dict get $result languages]
|
||||
|
||||
# Save to cache
|
||||
write_languages_cache $langs
|
||||
|
||||
if {$json_output} {
|
||||
# JSON array output
|
||||
set json_langs [list]
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import * as crypto from 'crypto';
|
|||
|
||||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
const LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
|
||||
const BLUE = "\x1b[34m";
|
||||
const RED = "\x1b[31m";
|
||||
const GREEN = "\x1b[32m";
|
||||
|
|
@ -408,6 +409,43 @@ function readEnvFile(filepath: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function getLanguagesCachePath(): string {
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
|
||||
return path.join(homeDir, '.unsandbox', 'languages.json');
|
||||
}
|
||||
|
||||
function loadLanguagesCache(): string[] | null {
|
||||
try {
|
||||
const cachePath = getLanguagesCachePath();
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return null;
|
||||
}
|
||||
const stat = fs.statSync(cachePath);
|
||||
const ageSeconds = (Date.now() - stat.mtimeMs) / 1000;
|
||||
if (ageSeconds >= LANGUAGES_CACHE_TTL) {
|
||||
return null;
|
||||
}
|
||||
const data = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||||
return data.languages || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveLanguagesCache(languages: string[]): void {
|
||||
try {
|
||||
const cachePath = getLanguagesCachePath();
|
||||
const cacheDir = path.dirname(cachePath);
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
const data = { languages: languages, timestamp: Math.floor(Date.now() / 1000) };
|
||||
fs.writeFileSync(cachePath, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
// Cache failures are non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
function buildEnvContent(envs: string[], envFile: string | null): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
|
|
@ -803,8 +841,15 @@ async function validateKey(keys: ApiKeys, shouldExtend: boolean): Promise<void>
|
|||
|
||||
async function cmdLanguages(args: Args): Promise<void> {
|
||||
const keys = getApiKeys(args.apiKey);
|
||||
const result = await apiRequest("/languages", "GET", null, keys);
|
||||
const langs = result.languages || [];
|
||||
|
||||
// Try cache first
|
||||
let langs = loadLanguagesCache();
|
||||
if (!langs) {
|
||||
const result = await apiRequest("/languages", "GET", null, keys);
|
||||
langs = result.languages || [];
|
||||
// Save to cache
|
||||
saveLanguagesCache(langs);
|
||||
}
|
||||
|
||||
if (args.jsonOutput) {
|
||||
// JSON array output
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import os
|
|||
const api_base = 'https://api.unsandbox.com'
|
||||
const portal_base = 'https://unsandbox.com'
|
||||
const max_env_content_size = 65536
|
||||
const languages_cache_ttl = 3600 // 1 hour in seconds
|
||||
const blue = '\x1b[34m'
|
||||
const red = '\x1b[31m'
|
||||
const green = '\x1b[32m'
|
||||
|
|
@ -687,55 +688,163 @@ fn cmd_image(list bool, info string, delete string, lock string, unlock string,
|
|||
exit(1)
|
||||
}
|
||||
|
||||
fn get_languages_cache_path() string {
|
||||
home := os.getenv('HOME')
|
||||
cache_dir := '${home}/.unsandbox'
|
||||
if !os.exists(cache_dir) {
|
||||
os.mkdir(cache_dir) or {}
|
||||
}
|
||||
return '${cache_dir}/languages.json'
|
||||
}
|
||||
|
||||
fn get_cached_languages() ?[]string {
|
||||
cache_path := get_languages_cache_path()
|
||||
if !os.exists(cache_path) {
|
||||
return none
|
||||
}
|
||||
|
||||
content := os.read_file(cache_path) or { return none }
|
||||
|
||||
// Extract timestamp
|
||||
timestamp_str := extract_json_string(content, 'timestamp')
|
||||
if timestamp_str == '' {
|
||||
return none
|
||||
}
|
||||
|
||||
// Parse timestamp (it's stored as a number, not a string)
|
||||
// Look for "timestamp": followed by digits
|
||||
ts_search := '"timestamp":'
|
||||
ts_idx := content.index(ts_search) or { return none }
|
||||
ts_start := ts_idx + ts_search.len
|
||||
mut ts_end := ts_start
|
||||
for ts_end < content.len && (content[ts_end].is_digit() || content[ts_end] == ` `) {
|
||||
ts_end++
|
||||
}
|
||||
timestamp_num := content[ts_start..ts_end].trim_space()
|
||||
cache_time := timestamp_num.i64()
|
||||
|
||||
// Get current time
|
||||
current_time_cmd := 'date +%s'
|
||||
current_time_result := os.execute(current_time_cmd)
|
||||
current_time := current_time_result.output.trim_space().i64()
|
||||
|
||||
// Check if cache is still valid
|
||||
if current_time - cache_time >= languages_cache_ttl {
|
||||
return none
|
||||
}
|
||||
|
||||
// Extract languages array
|
||||
start_idx := content.index('"languages":[') or { return none }
|
||||
arr_start := content.index_after('[', start_idx)
|
||||
if arr_start < 0 {
|
||||
return none
|
||||
}
|
||||
arr_end := content.index_after(']', arr_start)
|
||||
if arr_end < 0 {
|
||||
return none
|
||||
}
|
||||
|
||||
arr_content := content[arr_start + 1..arr_end]
|
||||
mut languages := []string{}
|
||||
for item in arr_content.split(',') {
|
||||
lang := item.trim_space().trim('"')
|
||||
if lang.len > 0 {
|
||||
languages << lang
|
||||
}
|
||||
}
|
||||
|
||||
return languages
|
||||
}
|
||||
|
||||
fn save_languages_cache(languages []string) {
|
||||
cache_path := get_languages_cache_path()
|
||||
|
||||
// Get current timestamp
|
||||
timestamp_cmd := 'date +%s'
|
||||
timestamp_result := os.execute(timestamp_cmd)
|
||||
timestamp := timestamp_result.output.trim_space()
|
||||
|
||||
// Build JSON array
|
||||
mut lang_json := '['
|
||||
for i, lang in languages {
|
||||
if i > 0 {
|
||||
lang_json += ','
|
||||
}
|
||||
lang_json += '"${lang}"'
|
||||
}
|
||||
lang_json += ']'
|
||||
|
||||
cache_content := '{"languages":${lang_json},"timestamp":${timestamp}}'
|
||||
os.write_file(cache_path, cache_content) or {}
|
||||
}
|
||||
|
||||
fn cmd_languages(json_output bool, api_key string) {
|
||||
// Check cache first
|
||||
cached := get_cached_languages()
|
||||
|
||||
if cached != none {
|
||||
languages := cached or { []string{} }
|
||||
if json_output {
|
||||
mut lang_json := '['
|
||||
for i, lang in languages {
|
||||
if i > 0 {
|
||||
lang_json += ','
|
||||
}
|
||||
lang_json += '"${lang}"'
|
||||
}
|
||||
lang_json += ']'
|
||||
println(lang_json)
|
||||
} else {
|
||||
for lang in languages {
|
||||
println(lang)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/languages:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/languages' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
result := exec_curl(cmd)
|
||||
|
||||
// Parse and cache the languages
|
||||
start_idx := result.index('"languages":[') or {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
arr_start := result.index_after('[', start_idx)
|
||||
if arr_start < 0 {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
arr_end := result.index_after(']', arr_start)
|
||||
if arr_end < 0 {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract languages for caching
|
||||
arr_content := result[arr_start + 1..arr_end]
|
||||
mut languages := []string{}
|
||||
for item in arr_content.split(',') {
|
||||
lang := item.trim_space().trim('"')
|
||||
if lang.len > 0 {
|
||||
languages << lang
|
||||
}
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
if languages.len > 0 {
|
||||
save_languages_cache(languages)
|
||||
}
|
||||
|
||||
if json_output {
|
||||
// Extract languages array and print as JSON
|
||||
// Find the languages array in the response
|
||||
start_idx := result.index('"languages":[') or {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
arr_start := result.index_after('[', start_idx)
|
||||
if arr_start < 0 {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
arr_end := result.index_after(']', arr_start)
|
||||
if arr_end < 0 {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
println(result[arr_start..arr_end + 1])
|
||||
} else {
|
||||
// Parse languages array and print one per line
|
||||
start_idx := result.index('"languages":[') or {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
arr_start := result.index_after('[', start_idx)
|
||||
if arr_start < 0 {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
arr_end := result.index_after(']', arr_start)
|
||||
if arr_end < 0 {
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
// Extract array content and parse
|
||||
arr_content := result[arr_start + 1..arr_end]
|
||||
// Split by comma and extract language names
|
||||
for item in arr_content.split(',') {
|
||||
lang := item.trim_space().trim('"')
|
||||
if lang.len > 0 {
|
||||
println(lang)
|
||||
}
|
||||
for lang in languages {
|
||||
println(lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ const time = std.time;
|
|||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
const MAX_ENV_CONTENT_SIZE: usize = 65536;
|
||||
const LANGUAGES_CACHE_TTL: i64 = 3600; // 1 hour in seconds
|
||||
const GREEN = "\x1b[32m";
|
||||
const RED = "\x1b[31m";
|
||||
const YELLOW = "\x1b[33m";
|
||||
|
|
@ -733,22 +734,90 @@ pub fn main() !u8 {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch languages from API
|
||||
const json_file = "/tmp/unsandbox_languages.json";
|
||||
const auth_headers = try buildAuthCmd(allocator, "GET", "/languages", "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/languages' {s} -o {s}", .{ API_BASE, auth_headers, json_file });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
// Get cache path (~/.unsandbox/languages.json)
|
||||
const home_env = std.process.getEnvVarOwned(allocator, "HOME") catch try allocator.dupe(u8, "/tmp");
|
||||
defer allocator.free(home_env);
|
||||
const cache_dir = try std.fmt.allocPrint(allocator, "{s}/.unsandbox", .{home_env});
|
||||
defer allocator.free(cache_dir);
|
||||
const cache_file = try std.fmt.allocPrint(allocator, "{s}/languages.json", .{cache_dir});
|
||||
defer allocator.free(cache_file);
|
||||
|
||||
// Read the JSON response
|
||||
const json_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
|
||||
std.debug.print("{s}Error reading languages response: {}{s}\n", .{ RED, err, RESET });
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
return 1;
|
||||
// Ensure cache directory exists
|
||||
fs.cwd().makeDir(cache_dir) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => {},
|
||||
};
|
||||
defer allocator.free(json_content);
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
|
||||
// Check cache first
|
||||
var use_cache = false;
|
||||
const cache_content = fs.cwd().readFileAlloc(allocator, cache_file, 1024 * 1024) catch null;
|
||||
defer if (cache_content) |cc| allocator.free(cc);
|
||||
|
||||
if (cache_content) |cc| {
|
||||
// Check timestamp
|
||||
const ts_prefix = "\"timestamp\":";
|
||||
if (mem.indexOf(u8, cc, ts_prefix)) |ts_start_idx| {
|
||||
const ts_value_start = ts_start_idx + ts_prefix.len;
|
||||
var ts_value_end = ts_value_start;
|
||||
while (ts_value_end < cc.len and (cc[ts_value_end] >= '0' and cc[ts_value_end] <= '9')) {
|
||||
ts_value_end += 1;
|
||||
}
|
||||
if (ts_value_end > ts_value_start) {
|
||||
const ts_str = cc[ts_value_start..ts_value_end];
|
||||
const cache_timestamp = std.fmt.parseInt(i64, ts_str, 10) catch 0;
|
||||
const current_timestamp = std.time.timestamp();
|
||||
if (current_timestamp - cache_timestamp < LANGUAGES_CACHE_TTL) {
|
||||
use_cache = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var json_content: []const u8 = undefined;
|
||||
var json_content_owned = false;
|
||||
|
||||
if (use_cache) {
|
||||
json_content = cache_content.?;
|
||||
} else {
|
||||
// Fetch languages from API
|
||||
const json_file = "/tmp/unsandbox_languages_tmp.json";
|
||||
const auth_headers = try buildAuthCmd(allocator, "GET", "/languages", "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/languages' {s} -o {s}", .{ API_BASE, auth_headers, json_file });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
|
||||
// Read the JSON response
|
||||
const api_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
|
||||
std.debug.print("{s}Error reading languages response: {}{s}\n", .{ RED, err, RESET });
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
return 1;
|
||||
};
|
||||
json_content = api_content;
|
||||
json_content_owned = true;
|
||||
std.fs.cwd().deleteFile(json_file) catch {};
|
||||
|
||||
// Save to cache with timestamp
|
||||
const arr_prefix = "\"languages\":[";
|
||||
if (mem.indexOf(u8, api_content, arr_prefix)) |start_idx| {
|
||||
const arr_start = start_idx + arr_prefix.len - 1; // Include the '['
|
||||
if (mem.indexOfPos(u8, api_content, arr_start, "]")) |end_idx| {
|
||||
const languages_array = api_content[arr_start .. end_idx + 1];
|
||||
const current_ts = std.time.timestamp();
|
||||
const cache_data = try std.fmt.allocPrint(allocator, "{{\"languages\":{s},\"timestamp\":{d}}}", .{ languages_array, current_ts });
|
||||
defer allocator.free(cache_data);
|
||||
|
||||
// Write cache file
|
||||
const cache_file_handle = fs.cwd().createFile(cache_file, .{}) catch null;
|
||||
if (cache_file_handle) |fh| {
|
||||
fh.writeAll(cache_data) catch {};
|
||||
fh.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer if (json_content_owned) allocator.free(json_content);
|
||||
|
||||
if (json_output) {
|
||||
// Find and print just the languages array
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue