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:
russell@unturf.com 2026-01-17 12:01:42 -05:00
parent cc7674f7d7
commit 1077543b7c
27 changed files with 1952 additions and 192 deletions

View file

@ -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]
}
}
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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)

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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.

View file

@ -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
;

View file

@ -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)

View file

@ -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

View file

@ -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]

View file

@ -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))

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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