Complete key command across all 42 CLI implementations: - un.go, un.v, un.fs, un.groovy (systems/JVM) - un.dart, un_inception.c, un_deno.ts, un.ps1 (mixed) - un.hs, un.ml, un.clj, un.scm (functional) - un.lisp, un.erl, un.ex, un.f90 (functional/scientific) - un.cob, un.pro, un.forth, un.raku (exotic) - un.m, un.awk (other) - un.js, un.ts, un.pl (fixes to existing) All implementations now support: - key: Validate API key and show status - key --extend: Open browser to renew key
405 lines
14 KiB
Erlang
Executable file
405 lines
14 KiB
Erlang
Executable file
%% PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
|
%%
|
|
%% This is free public domain software for the public good of a permacomputer hosted
|
|
%% at permacomputer.com - an always-on computer by the people, for the people. One
|
|
%% which is durable, easy to repair, and distributed like tap water for machine
|
|
%% learning intelligence.
|
|
%%
|
|
%% The permacomputer is community-owned infrastructure optimized around four values:
|
|
%%
|
|
%% TRUTH - First principles, math & science, open source code freely distributed
|
|
%% FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
|
|
%% HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
|
|
%% LOVE - Be yourself without hurting others, cooperation through natural law
|
|
%%
|
|
%% This software contributes to that vision by enabling code execution across 42+
|
|
%% programming languages through a unified interface, accessible to all. Code is
|
|
%% seeds to sprout on any abandoned technology.
|
|
%%
|
|
%% Learn more: https://www.permacomputer.com
|
|
%%
|
|
%% Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
|
%% software, either in source code form or as a compiled binary, for any purpose,
|
|
%% commercial or non-commercial, and by any means.
|
|
%%
|
|
%% NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
|
|
%%
|
|
%% That said, our permacomputer's digital membrane stratum continuously runs unit,
|
|
%% integration, and functional tests on all of it's own software - with our
|
|
%% permacomputer monitoring itself, repairing itself, with minimal human in the
|
|
%% loop guidance. Our agents do their best.
|
|
%%
|
|
%% Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
|
%% https://www.timehexon.com
|
|
%% https://www.foxhop.net
|
|
%% https://www.unturf.com/software
|
|
|
|
|
|
#!/usr/bin/env escript
|
|
|
|
%%% Erlang UN CLI - Unsandbox CLI Client
|
|
%%%
|
|
%%% Full-featured CLI matching un.py capabilities
|
|
%%% Uses curl for HTTP (no external dependencies)
|
|
|
|
main([]) ->
|
|
io:format("Usage: un.erl [options] <source_file>~n"),
|
|
io:format(" un.erl session [options]~n"),
|
|
io:format(" un.erl service [options]~n"),
|
|
io:format(" un.erl key [options]~n"),
|
|
halt(1);
|
|
|
|
main(["session" | Rest]) ->
|
|
session_command(Rest);
|
|
|
|
main(["service" | Rest]) ->
|
|
service_command(Rest);
|
|
|
|
main(["key" | Rest]) ->
|
|
key_command(Rest);
|
|
|
|
main(Args) ->
|
|
execute_command(Args).
|
|
|
|
%% Execute command
|
|
execute_command(Args) ->
|
|
{File, _Opts} = parse_exec_args(Args, #{file => undefined}),
|
|
case File of
|
|
undefined ->
|
|
io:format("Error: No source file specified~n"),
|
|
halt(1);
|
|
_ ->
|
|
ApiKey = get_api_key(),
|
|
Ext = filename:extension(File),
|
|
case ext_to_lang(Ext) of
|
|
{ok, Language} ->
|
|
case file:read_file(File) of
|
|
{ok, CodeBin} ->
|
|
Code = binary_to_list(CodeBin),
|
|
Json = build_json(Language, Code),
|
|
TmpFile = write_temp_file(Json),
|
|
Response = curl_post(ApiKey, "/execute", TmpFile),
|
|
file:delete(TmpFile),
|
|
io:format("~s~n", [Response]);
|
|
{error, Reason} ->
|
|
io:format("Error reading file: ~p~n", [Reason]),
|
|
halt(1)
|
|
end;
|
|
{error, Ext} ->
|
|
io:format("Error: Unknown extension: ~s~n", [Ext]),
|
|
halt(1)
|
|
end
|
|
end.
|
|
|
|
%% Session command
|
|
session_command(["--list" | _]) ->
|
|
ApiKey = get_api_key(),
|
|
Response = curl_get(ApiKey, "/sessions"),
|
|
io:format("~s~n", [Response]);
|
|
|
|
session_command(["--kill", SessionId | _]) ->
|
|
ApiKey = get_api_key(),
|
|
_ = curl_delete(ApiKey, "/sessions/" ++ SessionId),
|
|
io:format("\033[32mSession terminated: ~s\033[0m~n", [SessionId]);
|
|
|
|
session_command(Args) ->
|
|
ApiKey = get_api_key(),
|
|
Shell = get_shell_opt(Args, "bash"),
|
|
Json = "{\"shell\":\"" ++ Shell ++ "\"}",
|
|
TmpFile = write_temp_file(Json),
|
|
Response = curl_post(ApiKey, "/sessions", TmpFile),
|
|
file:delete(TmpFile),
|
|
io:format("\033[33mSession created (WebSocket required)\033[0m~n"),
|
|
io:format("~s~n", [Response]).
|
|
|
|
%% Service command
|
|
service_command(["--list" | _]) ->
|
|
ApiKey = get_api_key(),
|
|
Response = curl_get(ApiKey, "/services"),
|
|
io:format("~s~n", [Response]);
|
|
|
|
service_command(["--info", ServiceId | _]) ->
|
|
ApiKey = get_api_key(),
|
|
Response = curl_get(ApiKey, "/services/" ++ ServiceId),
|
|
io:format("~s~n", [Response]);
|
|
|
|
service_command(["--logs", ServiceId | _]) ->
|
|
ApiKey = get_api_key(),
|
|
Response = curl_get(ApiKey, "/services/" ++ ServiceId ++ "/logs"),
|
|
io:format("~s~n", [Response]);
|
|
|
|
service_command(["--sleep", ServiceId | _]) ->
|
|
ApiKey = get_api_key(),
|
|
TmpFile = write_temp_file("{}"),
|
|
_ = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/sleep", TmpFile),
|
|
file:delete(TmpFile),
|
|
io:format("\033[32mService sleeping: ~s\033[0m~n", [ServiceId]);
|
|
|
|
service_command(["--wake", ServiceId | _]) ->
|
|
ApiKey = get_api_key(),
|
|
TmpFile = write_temp_file("{}"),
|
|
_ = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/wake", TmpFile),
|
|
file:delete(TmpFile),
|
|
io:format("\033[32mService waking: ~s\033[0m~n", [ServiceId]);
|
|
|
|
service_command(["--destroy", ServiceId | _]) ->
|
|
ApiKey = get_api_key(),
|
|
_ = curl_delete(ApiKey, "/services/" ++ ServiceId),
|
|
io:format("\033[32mService destroyed: ~s\033[0m~n", [ServiceId]);
|
|
|
|
service_command(Args) ->
|
|
case get_service_name(Args) of
|
|
undefined ->
|
|
io:format("Error: --name required to create service~n"),
|
|
halt(1);
|
|
Name ->
|
|
ApiKey = get_api_key(),
|
|
Ports = get_service_ports(Args),
|
|
Bootstrap = get_service_bootstrap(Args),
|
|
Type = get_service_type(Args),
|
|
PortsJson = case Ports of
|
|
undefined -> "";
|
|
P -> ",\"ports\":[" ++ P ++ "]"
|
|
end,
|
|
BootstrapJson = case Bootstrap of
|
|
undefined -> "";
|
|
B -> ",\"bootstrap\":\"" ++ escape_json(B) ++ "\""
|
|
end,
|
|
TypeJson = case Type of
|
|
undefined -> "";
|
|
T -> ",\"service_type\":\"" ++ T ++ "\""
|
|
end,
|
|
Json = "{\"name\":\"" ++ Name ++ "\"" ++ PortsJson ++ BootstrapJson ++ TypeJson ++ "}",
|
|
TmpFile = write_temp_file(Json),
|
|
Response = curl_post(ApiKey, "/services", TmpFile),
|
|
file:delete(TmpFile),
|
|
io:format("\033[32mService created\033[0m~n"),
|
|
io:format("~s~n", [Response])
|
|
end.
|
|
|
|
%% Key command
|
|
key_command(Args) ->
|
|
ApiKey = get_api_key(),
|
|
case has_extend_flag(Args) of
|
|
true ->
|
|
validate_and_extend_key(ApiKey);
|
|
false ->
|
|
validate_key(ApiKey)
|
|
end.
|
|
|
|
validate_key(ApiKey) ->
|
|
Response = curl_post_portal(ApiKey, "/keys/validate", "{}"),
|
|
parse_and_display_key_status(Response, false).
|
|
|
|
validate_and_extend_key(ApiKey) ->
|
|
Response = curl_post_portal(ApiKey, "/keys/validate", "{}"),
|
|
parse_and_display_key_status(Response, true).
|
|
|
|
parse_and_display_key_status(Response, ShouldExtend) ->
|
|
%% Parse JSON response (simple extraction for fields we need)
|
|
Status = extract_json_field(Response, "status"),
|
|
PublicKey = extract_json_field(Response, "public_key"),
|
|
Tier = extract_json_field(Response, "tier"),
|
|
ExpiresAt = extract_json_field(Response, "expires_at"),
|
|
TimeRemaining = extract_json_field(Response, "time_remaining"),
|
|
RateLimit = extract_json_field(Response, "rate_limit"),
|
|
Burst = extract_json_field(Response, "burst"),
|
|
Concurrency = extract_json_field(Response, "concurrency"),
|
|
|
|
case Status of
|
|
"valid" ->
|
|
io:format("\033[32mValid\033[0m~n"),
|
|
io:format("Public Key: ~s~n", [PublicKey]),
|
|
io:format("Tier: ~s~n", [Tier]),
|
|
io:format("Status: ~s~n", [Status]),
|
|
io:format("Expires: ~s~n", [ExpiresAt]),
|
|
if TimeRemaining =/= "" -> io:format("Time Remaining: ~s~n", [TimeRemaining]); true -> ok end,
|
|
if RateLimit =/= "" -> io:format("Rate Limit: ~s~n", [RateLimit]); true -> ok end,
|
|
if Burst =/= "" -> io:format("Burst: ~s~n", [Burst]); true -> ok end,
|
|
if Concurrency =/= "" -> io:format("Concurrency: ~s~n", [Concurrency]); true -> ok end,
|
|
if ShouldExtend ->
|
|
open_extend_page(PublicKey);
|
|
true -> ok
|
|
end;
|
|
"expired" ->
|
|
io:format("\033[31mExpired\033[0m~n"),
|
|
io:format("Public Key: ~s~n", [PublicKey]),
|
|
io:format("Tier: ~s~n", [Tier]),
|
|
io:format("Expired: ~s~n", [ExpiresAt]),
|
|
io:format("\033[33mTo renew: Visit https://unsandbox.com/keys/extend\033[0m~n"),
|
|
if ShouldExtend ->
|
|
open_extend_page(PublicKey);
|
|
true -> ok
|
|
end;
|
|
"invalid" ->
|
|
io:format("\033[31mInvalid\033[0m~n"),
|
|
io:format("The API key is not valid.~n");
|
|
_ ->
|
|
io:format("~s~n", [Response])
|
|
end.
|
|
|
|
open_extend_page(PublicKey) ->
|
|
Url = "https://unsandbox.com/keys/extend?pk=" ++ PublicKey,
|
|
io:format("\033[33mOpening browser to extend key...\033[0m~n"),
|
|
case os:type() of
|
|
{unix, darwin} ->
|
|
os:cmd("open '" ++ Url ++ "'");
|
|
{unix, _} ->
|
|
os:cmd("xdg-open '" ++ Url ++ "' 2>/dev/null || sensible-browser '" ++ Url ++ "' 2>/dev/null &");
|
|
{win32, _} ->
|
|
os:cmd("start " ++ Url)
|
|
end.
|
|
|
|
%% Helpers
|
|
get_api_key() ->
|
|
case os:getenv("UNSANDBOX_API_KEY") of
|
|
false ->
|
|
io:format("Error: UNSANDBOX_API_KEY not set~n"),
|
|
halt(1);
|
|
Key -> Key
|
|
end.
|
|
|
|
ext_to_lang(".hs") -> {ok, "haskell"};
|
|
ext_to_lang(".ml") -> {ok, "ocaml"};
|
|
ext_to_lang(".clj") -> {ok, "clojure"};
|
|
ext_to_lang(".scm") -> {ok, "scheme"};
|
|
ext_to_lang(".lisp") -> {ok, "commonlisp"};
|
|
ext_to_lang(".erl") -> {ok, "erlang"};
|
|
ext_to_lang(".ex") -> {ok, "elixir"};
|
|
ext_to_lang(".exs") -> {ok, "elixir"};
|
|
ext_to_lang(".py") -> {ok, "python"};
|
|
ext_to_lang(".js") -> {ok, "javascript"};
|
|
ext_to_lang(".ts") -> {ok, "typescript"};
|
|
ext_to_lang(".rb") -> {ok, "ruby"};
|
|
ext_to_lang(".go") -> {ok, "go"};
|
|
ext_to_lang(".rs") -> {ok, "rust"};
|
|
ext_to_lang(".c") -> {ok, "c"};
|
|
ext_to_lang(".cpp") -> {ok, "cpp"};
|
|
ext_to_lang(".cc") -> {ok, "cpp"};
|
|
ext_to_lang(".java") -> {ok, "java"};
|
|
ext_to_lang(".kt") -> {ok, "kotlin"};
|
|
ext_to_lang(".cs") -> {ok, "csharp"};
|
|
ext_to_lang(".fs") -> {ok, "fsharp"};
|
|
ext_to_lang(".jl") -> {ok, "julia"};
|
|
ext_to_lang(".r") -> {ok, "r"};
|
|
ext_to_lang(".cr") -> {ok, "crystal"};
|
|
ext_to_lang(".d") -> {ok, "d"};
|
|
ext_to_lang(".nim") -> {ok, "nim"};
|
|
ext_to_lang(".zig") -> {ok, "zig"};
|
|
ext_to_lang(".v") -> {ok, "v"};
|
|
ext_to_lang(".dart") -> {ok, "dart"};
|
|
ext_to_lang(".sh") -> {ok, "bash"};
|
|
ext_to_lang(".pl") -> {ok, "perl"};
|
|
ext_to_lang(".lua") -> {ok, "lua"};
|
|
ext_to_lang(".php") -> {ok, "php"};
|
|
ext_to_lang(Ext) -> {error, Ext}.
|
|
|
|
escape_json(Str) ->
|
|
escape_json(Str, []).
|
|
|
|
escape_json([], Acc) ->
|
|
lists:reverse(Acc);
|
|
escape_json([$\\ | Rest], Acc) ->
|
|
escape_json(Rest, [$\\, $\\ | Acc]);
|
|
escape_json([$\" | Rest], Acc) ->
|
|
escape_json(Rest, [$\", $\\ | Acc]);
|
|
escape_json([$\n | Rest], Acc) ->
|
|
escape_json(Rest, [$n, $\\ | Acc]);
|
|
escape_json([$\r | Rest], Acc) ->
|
|
escape_json(Rest, [$r, $\\ | Acc]);
|
|
escape_json([$\t | Rest], Acc) ->
|
|
escape_json(Rest, [$t, $\\ | Acc]);
|
|
escape_json([C | Rest], Acc) ->
|
|
escape_json(Rest, [C | Acc]).
|
|
|
|
build_json(Language, Code) ->
|
|
"{\"language\":\"" ++ Language ++ "\",\"code\":\"" ++ escape_json(Code) ++ "\"}".
|
|
|
|
write_temp_file(Data) ->
|
|
TmpFile = "/tmp/un_erl_" ++ integer_to_list(rand:uniform(999999)) ++ ".json",
|
|
file:write_file(TmpFile, Data),
|
|
TmpFile.
|
|
|
|
curl_post(ApiKey, Endpoint, TmpFile) ->
|
|
Cmd = "curl -s -X POST https://api.unsandbox.com" ++ Endpoint ++
|
|
" -H 'Content-Type: application/json'" ++
|
|
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
|
|
" -d @" ++ TmpFile,
|
|
os:cmd(Cmd).
|
|
|
|
curl_post_portal(ApiKey, Endpoint, Data) ->
|
|
TmpFile = write_temp_file(Data),
|
|
Cmd = "curl -s -X POST https://unsandbox.com" ++ Endpoint ++
|
|
" -H 'Content-Type: application/json'" ++
|
|
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
|
|
" -d @" ++ TmpFile,
|
|
Result = os:cmd(Cmd),
|
|
file:delete(TmpFile),
|
|
Result.
|
|
|
|
curl_get(ApiKey, Endpoint) ->
|
|
Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++
|
|
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
|
|
os:cmd(Cmd).
|
|
|
|
curl_delete(ApiKey, Endpoint) ->
|
|
Cmd = "curl -s -X DELETE https://api.unsandbox.com" ++ Endpoint ++
|
|
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
|
|
os:cmd(Cmd).
|
|
|
|
%% Argument parsing
|
|
parse_exec_args([], Opts) ->
|
|
{maps:get(file, Opts), Opts};
|
|
parse_exec_args([Arg | Rest], Opts) ->
|
|
case Arg of
|
|
"-" ++ _ -> parse_exec_args(Rest, Opts);
|
|
_ -> {Arg, Opts}
|
|
end.
|
|
|
|
get_shell_opt([], Default) -> Default;
|
|
get_shell_opt(["--shell", Shell | _], _) -> Shell;
|
|
get_shell_opt(["-s", Shell | _], _) -> Shell;
|
|
get_shell_opt([_ | Rest], Default) -> get_shell_opt(Rest, Default).
|
|
|
|
get_service_name([]) -> undefined;
|
|
get_service_name(["--name", Name | _]) -> Name;
|
|
get_service_name([_ | Rest]) -> get_service_name(Rest).
|
|
|
|
get_service_ports([]) -> undefined;
|
|
get_service_ports(["--ports", Ports | _]) -> Ports;
|
|
get_service_ports([_ | Rest]) -> get_service_ports(Rest).
|
|
|
|
get_service_bootstrap([]) -> undefined;
|
|
get_service_bootstrap(["--bootstrap", Bootstrap | _]) -> Bootstrap;
|
|
get_service_bootstrap([_ | Rest]) -> get_service_bootstrap(Rest).
|
|
|
|
get_service_type([]) -> undefined;
|
|
get_service_type(["--type", Type | _]) -> Type;
|
|
get_service_type([_ | Rest]) -> get_service_type(Rest).
|
|
|
|
has_extend_flag([]) -> false;
|
|
has_extend_flag(["--extend" | _]) -> true;
|
|
has_extend_flag([_ | Rest]) -> has_extend_flag(Rest).
|
|
|
|
%% Simple JSON field extraction (works for simple string fields)
|
|
extract_json_field(Json, Field) ->
|
|
Pattern = "\"" ++ Field ++ "\":\"",
|
|
case string:str(Json, Pattern) of
|
|
0 -> "";
|
|
Pos ->
|
|
Start = Pos + length(Pattern),
|
|
Rest = lists:nthtail(Start - 1, Json),
|
|
extract_until_quote(Rest)
|
|
end.
|
|
|
|
extract_until_quote(Str) ->
|
|
extract_until_quote(Str, []).
|
|
|
|
extract_until_quote([], Acc) ->
|
|
lists:reverse(Acc);
|
|
extract_until_quote([$\" | _], Acc) ->
|
|
lists:reverse(Acc);
|
|
extract_until_quote([$\\, $\" | Rest], Acc) ->
|
|
extract_until_quote(Rest, [$\" | Acc]);
|
|
extract_until_quote([C | Rest], Acc) ->
|
|
extract_until_quote(Rest, [C | Acc]).
|