Add --account N flag and fix credential resolution priority in un.m, un.f90, un.zig, un.nim

Implements correct 5-tier credential priority in all four implementations:
  1. Explicit -p/-k flags
  2. --account N -> accounts.csv row N (bypasses env vars)
  3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
  4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
  5. ./accounts.csv row 0

un.m (Objective-C): adds UNLoadCredentialsFromCSV helper, updates UNGetCredentials
  to use g_accountIndex global, parses --account N in main() pre-scan.

un.f90 (Fortran): adds load_csv_row subroutine, updates get_credentials with
  optional account_index parameter, parses --account N in main program pre-scan,
  passes account_index via host association to all handle_* subroutines.

un.zig (Zig): adds loadCsvRow and resolveCredentials functions, parses --account N
  in main() pre-scan, updates execute-mode arg loop to skip known flags.

un.nim (Nim): adds loadCredentialsFromCsv and resolveCredentials procs, parses
  --account N in main() pre-scan, updates execute-mode loop to skip --account.
This commit is contained in:
russell@unturf.com 2026-03-23 15:15:19 -04:00
parent 13f15c8abc
commit 5373da4108
4 changed files with 375 additions and 103 deletions

View file

@ -64,9 +64,11 @@
! ./un key [--extend]
!
! Authentication (in priority order):
! 1. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
! 2. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
! 3. Legacy: UNSANDBOX_API_KEY (deprecated)
! 1. --account N flag -> accounts.csv row N (bypasses env vars)
! 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
! 3. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT)
! 4. ./accounts.csv row 0
! 5. Legacy: UNSANDBOX_API_KEY (deprecated)
!
! Compile:
! gfortran -o un un.f90
@ -190,32 +192,97 @@ module unsandbox_sdk
contains
!--------------------------------------------------------------------------
! Subroutine: load_csv_row
! Description: Load public_key,secret_key from a CSV file at row_index
! (0-based, skipping blank lines and '#' comments).
!
! Arguments:
! csv_path - Path to CSV file
! row_index - Zero-based data row to read
! public_key - Output: public key (empty if not found)
! secret_key - Output: secret key (empty if not found)
!--------------------------------------------------------------------------
subroutine load_csv_row(csv_path, row_index, public_key, secret_key)
character(len=*), intent(in) :: csv_path
integer, intent(in) :: row_index
character(len=*), intent(out) :: public_key, secret_key
character(len=1024) :: line
integer :: unit_num, ios, data_index
logical :: file_exists
public_key = ''
secret_key = ''
data_index = 0
inquire(file=trim(csv_path), exist=file_exists)
if (.not. file_exists) return
open(newunit=unit_num, file=trim(csv_path), status='old', action='read', iostat=ios)
if (ios /= 0) return
do
read(unit_num, '(A)', iostat=ios) line
if (ios /= 0) exit
line = adjustl(line)
if (len_trim(line) == 0) cycle
if (line(1:1) == '#') cycle
if (data_index == row_index) then
call parse_csv_line(line, public_key, secret_key)
close(unit_num)
return
end if
data_index = data_index + 1
end do
close(unit_num)
end subroutine load_csv_row
!--------------------------------------------------------------------------
! Subroutine: get_credentials
! Description: Get API credentials from environment or config file
!
! Priority order:
! 1. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
! 2. Config file (~/.unsandbox/accounts.csv)
! 3. Legacy UNSANDBOX_API_KEY (deprecated)
! 1. account_index >= 0 -> accounts.csv row N (bypasses env vars)
! 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
! 3. Config file (~/.unsandbox/accounts.csv row 0 or UNSANDBOX_ACCOUNT)
! 4. ./accounts.csv row 0
! 5. Legacy UNSANDBOX_API_KEY (deprecated)
!
! Arguments:
! public_key - Output: API public key
! secret_key - Output: API secret key
! status - Output: 0 on success, non-zero on error
! public_key - Output: API public key
! secret_key - Output: API secret key
! status - Output: 0 on success, non-zero on error
! account_index - Optional input: if >= 0, load that CSV row directly
!--------------------------------------------------------------------------
subroutine get_credentials(public_key, secret_key, status)
subroutine get_credentials(public_key, secret_key, status, account_index)
character(len=*), intent(out) :: public_key, secret_key
integer, intent(out) :: status
character(len=1024) :: home_dir, accounts_path, line, api_key
integer :: unit_num, ios
logical :: file_exists
integer, intent(in), optional :: account_index
character(len=1024) :: home_dir, accounts_path, api_key, acct_env
integer :: ios, acct_idx, default_index
status = 0
public_key = ''
secret_key = ''
! Priority 1: Environment variables
! Priority 1: account_index >= 0 -> load that CSV row (bypasses env vars)
if (present(account_index)) then
if (account_index >= 0) then
acct_idx = account_index
call get_environment_variable('HOME', home_dir, status=ios)
if (ios == 0) then
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
call load_csv_row(accounts_path, acct_idx, public_key, secret_key)
if (len_trim(public_key) > 0) return
end if
call load_csv_row('accounts.csv', acct_idx, public_key, secret_key)
if (len_trim(public_key) > 0) return
status = 1
return
end if
end if
! Priority 2: Environment variables
call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=ios)
if (ios == 0 .and. len_trim(public_key) > 0) then
call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=ios)
@ -224,37 +291,27 @@ contains
end if
end if
! Priority 2: Config file
! Priority 3: ~/.unsandbox/accounts.csv (default row)
call get_environment_variable('UNSANDBOX_ACCOUNT', acct_env, status=ios)
if (ios == 0 .and. len_trim(acct_env) > 0) then
read(acct_env, *, iostat=ios) default_index
if (ios /= 0) default_index = 0
else
default_index = 0
end if
call get_environment_variable('HOME', home_dir, status=ios)
if (ios == 0) then
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
inquire(file=trim(accounts_path), exist=file_exists)
if (file_exists) then
open(newunit=unit_num, file=trim(accounts_path), status='old', &
action='read', iostat=ios)
if (ios == 0) then
do
read(unit_num, '(A)', iostat=ios) line
if (ios /= 0) exit
line = adjustl(line)
if (len_trim(line) == 0) cycle
if (line(1:1) == '#') cycle
! Parse CSV: public_key,secret_key
call parse_csv_line(line, public_key, secret_key)
if (len_trim(public_key) > 0 .and. len_trim(secret_key) > 0) then
if (public_key(1:8) == 'unsb-pk-' .and. &
secret_key(1:8) == 'unsb-sk-') then
close(unit_num)
return
end if
end if
end do
close(unit_num)
end if
end if
call load_csv_row(accounts_path, default_index, public_key, secret_key)
if (len_trim(public_key) > 0) return
end if
! Priority 3: Legacy API key
! Priority 4: ./accounts.csv
call load_csv_row('accounts.csv', default_index, public_key, secret_key)
if (len_trim(public_key) > 0) return
! Priority 5: Legacy API key
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=ios)
if (ios == 0 .and. len_trim(api_key) > 0) then
public_key = api_key
@ -850,6 +907,7 @@ program unsandbox_cli
character(len=1024) :: filename, language, api_key, ext, arg, subcommand
character(len=256) :: session_id, service_id
integer :: stat, i, nargs, dot_pos
integer :: account_index ! -1 = not set; >= 0 means use that CSV row
logical :: list_flag, is_session, is_service, is_key
! Initialize
@ -860,6 +918,7 @@ program unsandbox_cli
is_key = .false.
session_id = ''
service_id = ''
account_index = -1
! Get command line arguments count
nargs = command_argument_count()
@ -868,6 +927,17 @@ program unsandbox_cli
stop 1
end if
! Pre-scan all arguments for --account N
do i = 1, nargs - 1
call get_command_argument(i, arg)
if (trim(arg) == '--account') then
call get_command_argument(i + 1, arg)
read(arg, *, iostat=stat) account_index
if (stat /= 0) account_index = -1
exit
end if
end do
! Check for subcommands
call get_command_argument(1, arg, status=stat)
if (trim(arg) == '-h' .or. trim(arg) == '--help') then
@ -974,6 +1044,9 @@ contains
write(*, '(A)') 'Languages options:'
write(*, '(A)') ' --json Output as JSON array'
write(*, '(A)') ''
write(*, '(A)') 'Credential options (global):'
write(*, '(A)') ' --account N Use row N from accounts.csv (bypasses env vars)'
write(*, '(A)') ''
write(*, '(A)') 'Library Usage:'
write(*, '(A)') ' use unsandbox_sdk'
write(*, '(A)') ' type(unsandbox_client) :: client'
@ -1003,7 +1076,7 @@ contains
end if
! Get API keys
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY'
stop 1
@ -1074,6 +1147,8 @@ contains
input_files = trim(arg)
end if
end if
else if (trim(arg) == '--account') then
! already processed in main pre-scan; skip this token and its value
else
if (len_trim(arg) > 0) then
if (arg(1:1) == '-') then
@ -1086,7 +1161,7 @@ contains
end do
! Get API keys
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
@ -1289,7 +1364,7 @@ contains
end do
! Get API keys
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
@ -1617,7 +1692,7 @@ contains
end do
! Get credentials
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
@ -1771,7 +1846,7 @@ contains
list_mode = .false.
! Get credentials
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
@ -2046,7 +2121,7 @@ contains
end do
! Get API key
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
@ -2143,7 +2218,7 @@ contains
end do
! Get API keys
call get_credentials(public_key, secret_key, stat)
call get_credentials(public_key, secret_key, stat, account_index)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1

View file

@ -43,7 +43,7 @@
# un.nim session --list
# un.nim service --name web --ports 8080
import os, strutils, osproc, strformat, times, sequtils
import os, strutils, osproc, strformat, times
const
API_BASE = "https://api.unsandbox.com"
@ -1252,15 +1252,15 @@ proc resolveCredentials(argPk: string, argSk: string, accountIndex: int): tuple[
if argPk != "" and argSk != "":
return (argPk, argSk)
# Tier 2: --account N bypasses env vars
let homeDir = getHomeDir()
if accountIndex >= 0:
let homeDir = getHomeDir()
let homeCsv = homeDir / ".unsandbox" / "accounts.csv"
var creds = loadCredentialsFromCsv(homeCsv, accountIndex)
if creds.pk != "":
return creds
creds = loadCredentialsFromCsv("accounts.csv", accountIndex)
if creds.pk != "":
return creds
let homeCsv2 = homeDir / ".unsandbox" / "accounts.csv"
var creds2 = loadCredentialsFromCsv(homeCsv2, accountIndex)
if creds2.pk != "":
return creds2
creds2 = loadCredentialsFromCsv("accounts.csv", accountIndex)
if creds2.pk != "":
return creds2
stderr.writeLine(RED & fmt"Error: No credentials found for account index {accountIndex} in accounts.csv" & RESET)
quit(1)
# Tier 3: env vars
@ -1270,7 +1270,6 @@ proc resolveCredentials(argPk: string, argSk: string, accountIndex: int): tuple[
return (envPk, envSk)
# Tier 4: ~/.unsandbox/accounts.csv (default row or UNSANDBOX_ACCOUNT)
let defaultIndex = parseInt(getEnv("UNSANDBOX_ACCOUNT", "0"))
let homeDir = getHomeDir()
let homeCsv = homeDir / ".unsandbox" / "accounts.csv"
var creds = loadCredentialsFromCsv(homeCsv, defaultIndex)
if creds.pk != "":
@ -1296,7 +1295,10 @@ proc main() =
var i = 0
while i < rawArgs.len:
if rawArgs[i] == "--account" and i + 1 < rawArgs.len:
accountIndex = parseInt(rawArgs[i + 1])
try:
accountIndex = parseInt(rawArgs[i + 1])
except ValueError:
discard
i.inc
elif rawArgs[i] == "-p" and i + 1 < rawArgs.len:
argPublicKey = rawArgs[i + 1]

View file

@ -50,8 +50,10 @@
//
// Authentication (in priority order):
// 1. UNClient initWithPublicKey:secretKey: constructor arguments
// 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
// 3. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
// 2. --account N flag -> accounts.csv row N (bypasses env vars)
// 3. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
// 4. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT)
// 5. ./accounts.csv row 0
#!/usr/bin/env -S clang -x objective-c -framework Foundation -o /tmp/un_objc && /tmp/un_objc
@ -207,9 +209,54 @@ NSString* UNComputeSignature(NSString* secretKey, long timestamp, NSString* meth
// Credentials Loading
// ============================================================================
// Global account index: -1 means not set (use env vars / default CSV row).
// Set by main() when --account N is parsed.
static NSInteger g_accountIndex = -1;
/**
* Load public_key,secret_key from a CSV file at the given row index (0-based,
* skipping blank lines and comment lines starting with '#').
*
* @param csvPath Path to the CSV file
* @param rowIndex Zero-based data row to read
* @param outPk Output: public key string (nil if not found)
* @param outSk Output: secret key string (nil if not found)
*/
void UNLoadCredentialsFromCSV(NSString* csvPath, NSInteger rowIndex, NSString** outPk, NSString** outSk) {
*outPk = nil;
*outSk = nil;
NSFileManager* fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:csvPath]) return;
NSString* content = [NSString stringWithContentsOfFile:csvPath encoding:NSUTF8StringEncoding error:nil];
if (!content) return;
NSArray* lines = [content componentsSeparatedByString:@"\n"];
NSInteger dataIndex = 0;
for (NSString* line in lines) {
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
if (dataIndex == rowIndex) {
NSArray* parts = [trimmed componentsSeparatedByString:@","];
if ([parts count] >= 2) {
*outPk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
*outSk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
return;
}
dataIndex++;
}
}
/**
* Get API credentials from environment or config file.
* Priority: 1. Arguments, 2. Environment vars, 3. ~/.unsandbox/accounts.csv
*
* Priority order:
* 1. Function arguments (argPublicKey / argSecretKey)
* 2. g_accountIndex >= 0 -> accounts.csv row N (bypasses env vars)
* 3. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
* 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
* 5. ./accounts.csv row 0
*
* @param publicKey Output public key
* @param secretKey Output secret key
@ -226,7 +273,22 @@ BOOL UNGetCredentials(NSString** publicKey, NSString** secretKey, NSString* argP
return YES;
}
// Priority 2: Environment variables
// Priority 2: --account N -> accounts.csv row N (bypasses env vars)
if (g_accountIndex >= 0) {
NSString* home = NSHomeDirectory();
NSString* homeCsv = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
UNLoadCredentialsFromCSV(homeCsv, g_accountIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
UNLoadCredentialsFromCSV(@"accounts.csv", g_accountIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
if (error) {
*error = [UNAuthenticationError errorWithMessage:
[NSString stringWithFormat:@"No credentials found for account index %ld in accounts.csv", (long)g_accountIndex]];
}
return NO;
}
// Priority 3: Environment variables
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
*secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
@ -242,32 +304,17 @@ BOOL UNGetCredentials(NSString** publicKey, NSString** secretKey, NSString* argP
return YES;
}
// Priority 3: Config file ~/.unsandbox/accounts.csv
// Priority 4: Config file ~/.unsandbox/accounts.csv (default row)
NSString* home = NSHomeDirectory();
NSString* accountsPath = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
NSFileManager* fm = [NSFileManager defaultManager];
NSString* accountIndexStr = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_ACCOUNT"];
NSInteger defaultIndex = accountIndexStr ? [accountIndexStr integerValue] : 0;
NSString* homeCsv = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
UNLoadCredentialsFromCSV(homeCsv, defaultIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
if ([fm fileExistsAtPath:accountsPath]) {
NSString* content = [NSString stringWithContentsOfFile:accountsPath encoding:NSUTF8StringEncoding error:nil];
if (content) {
NSArray* lines = [content componentsSeparatedByString:@"\n"];
for (NSString* line in lines) {
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
NSArray* parts = [trimmed componentsSeparatedByString:@","];
if ([parts count] >= 2) {
NSString* pk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString* sk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([pk hasPrefix:@"unsb-pk-"] && [sk hasPrefix:@"unsb-sk-"]) {
*publicKey = pk;
*secretKey = sk;
return YES;
}
}
}
}
}
// Priority 5: ./accounts.csv
UNLoadCredentialsFromCSV(@"accounts.csv", defaultIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
if (error) {
*error = [UNAuthenticationError errorWithMessage:
@ -2378,6 +2425,14 @@ int main(int argc, const char* argv[]) {
[args addObject:[NSString stringWithUTF8String:argv[i]]];
}
// Pre-scan for --account N before subcommand dispatch
for (NSUInteger i = 0; i < [args count]; i++) {
if ([args[i] isEqualToString:@"--account"] && i + 1 < [args count]) {
g_accountIndex = [args[i + 1] integerValue];
break;
}
}
NSString* firstArg = args[0];
if ([firstArg isEqualToString:@"--help"] || [firstArg isEqualToString:@"-h"]) {

View file

@ -806,6 +806,116 @@ fn buildInputFilesJson(allocator: std.mem.Allocator, files: std.ArrayList([]cons
return list.toOwnedSlice();
}
/// Load credentials from a CSV file at the given 0-based row index,
/// skipping blank lines and lines starting with '#'.
/// Returns allocated pk and sk slices, or null if not found.
fn loadCsvRow(allocator: std.mem.Allocator, csv_path: []const u8, row_index: usize) !?struct { pk: []const u8, sk: []const u8 } {
const file = fs.cwd().openFile(csv_path, .{}) catch return null;
defer file.close();
var buf_reader = std.io.bufferedReader(file.reader());
var reader = buf_reader.reader();
var line_buf: [4096]u8 = undefined;
var data_index: usize = 0;
while (true) {
const line = reader.readUntilDelimiterOrEof(&line_buf, '\n') catch break orelse break;
const trimmed = std.mem.trim(u8, line, " \t\r\n");
if (trimmed.len == 0 or trimmed[0] == '#') continue;
if (data_index == row_index) {
if (std.mem.indexOfScalar(u8, trimmed, ',')) |comma_pos| {
const pk = std.mem.trim(u8, trimmed[0..comma_pos], " \t");
const sk = std.mem.trim(u8, trimmed[comma_pos + 1 ..], " \t");
if (pk.len > 0 and sk.len > 0) {
return .{ .pk = try allocator.dupe(u8, pk), .sk = try allocator.dupe(u8, sk) };
}
}
return null;
}
data_index += 1;
}
return null;
}
/// Resolve credentials using 5-tier priority:
/// 1. arg_pk / arg_sk (explicit -p/-k flags) if both non-empty
/// 2. account_index >= 0 -> accounts.csv row N (bypasses env vars)
/// 3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
/// 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
/// 5. ./accounts.csv row 0
/// Returns allocated pk and sk slices.
fn resolveCredentials(allocator: std.mem.Allocator, arg_pk: []const u8, arg_sk: []const u8, account_index: i64) !struct { pk: []u8, sk: []u8 } {
// Tier 1: explicit key flags
if (arg_pk.len > 0 and arg_sk.len > 0) {
return .{ .pk = try allocator.dupe(u8, arg_pk), .sk = try allocator.dupe(u8, arg_sk) };
}
// Tier 2: --account N bypasses env vars
if (account_index >= 0) {
const acct_idx: usize = @intCast(account_index);
// try ~/.unsandbox/accounts.csv
const home_opt = process.getEnvVarOwned(allocator, "HOME") catch null;
if (home_opt) |home| {
defer allocator.free(home);
const home_csv = try std.fmt.allocPrint(allocator, "{s}/.unsandbox/accounts.csv", .{home});
defer allocator.free(home_csv);
if (try loadCsvRow(allocator, home_csv, acct_idx)) |creds| {
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
}
// try ./accounts.csv
if (try loadCsvRow(allocator, "accounts.csv", acct_idx)) |creds| {
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
const stderr = std.io.getStdErr().writer();
try stderr.print("{s}Error: No credentials found for account index {d} in accounts.csv{s}\n", .{ RED, account_index, RESET });
std.process.exit(1);
}
// Tier 3: env vars
const env_pk = process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
break :blk process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
};
const env_sk = process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch try allocator.dupe(u8, "");
if (env_pk.len > 0 and env_sk.len > 0) {
return .{ .pk = env_pk, .sk = env_sk };
}
// Tier 4: ~/.unsandbox/accounts.csv (default row)
const default_index_str = process.getEnvVarOwned(allocator, "UNSANDBOX_ACCOUNT") catch try allocator.dupe(u8, "0");
defer allocator.free(default_index_str);
const default_index = std.fmt.parseInt(usize, std.mem.trim(u8, default_index_str, " \t"), 10) catch 0;
const home_opt = process.getEnvVarOwned(allocator, "HOME") catch null;
if (home_opt) |home| {
defer allocator.free(home);
const home_csv = try std.fmt.allocPrint(allocator, "{s}/.unsandbox/accounts.csv", .{home});
defer allocator.free(home_csv);
if (try loadCsvRow(allocator, home_csv, default_index)) |creds| {
allocator.free(env_pk);
allocator.free(env_sk);
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
}
// Tier 5: ./accounts.csv
if (try loadCsvRow(allocator, "accounts.csv", default_index)) |creds| {
allocator.free(env_pk);
allocator.free(env_sk);
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
if (env_pk.len > 0) {
return .{ .pk = env_pk, .sk = env_sk };
}
allocator.free(env_pk);
allocator.free(env_sk);
const stderr = std.io.getStdErr().writer();
try stderr.print("{s}Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY{s}\n", .{ RED, RESET });
std.process.exit(1);
}
pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
@ -837,18 +947,37 @@ pub fn main() !u8 {
std.debug.print(" image --visibility ID MODE Set visibility (private/unlisted/public)\n", .{});
std.debug.print(" image --spawn ID --name NAME Spawn service from image\n", .{});
std.debug.print(" image --clone ID --name NAME Clone an image\n", .{});
std.debug.print("\nCredential options (global):\n", .{});
std.debug.print(" -p PUBLIC_KEY Explicit public key\n", .{});
std.debug.print(" -k SECRET_KEY Explicit secret key\n", .{});
std.debug.print(" --account N Use row N from accounts.csv (bypasses env vars)\n", .{});
return 1;
}
var public_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
break :blk std.process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
};
defer allocator.free(public_key);
// Pre-scan for --account N, -p, and -k before subcommand dispatch
var account_index: i64 = -1;
var arg_pk: []const u8 = "";
var arg_sk: []const u8 = "";
{
var scan_i: usize = 1;
while (scan_i < args.len) : (scan_i += 1) {
if (mem.eql(u8, args[scan_i], "--account") and scan_i + 1 < args.len) {
scan_i += 1;
account_index = std.fmt.parseInt(i64, args[scan_i], 10) catch -1;
} else if (mem.eql(u8, args[scan_i], "-p") and scan_i + 1 < args.len) {
scan_i += 1;
arg_pk = args[scan_i];
} else if (mem.eql(u8, args[scan_i], "-k") and scan_i + 1 < args.len) {
scan_i += 1;
arg_sk = args[scan_i];
}
}
}
const secret_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch blk: {
break :blk try allocator.dupe(u8, "");
};
const creds = try resolveCredentials(allocator, arg_pk, arg_sk, account_index);
var public_key = creds.pk;
defer allocator.free(public_key);
const secret_key = creds.sk;
defer allocator.free(secret_key);
// Handle session command
@ -1727,16 +1856,27 @@ pub fn main() !u8 {
return 0;
}
// Execute mode - find source file
// Execute mode - find source file (skip known flags and their values)
var source_file: ?[]const u8 = null;
for (args[1..]) |arg| {
if (mem.startsWith(u8, arg, "-")) {
const stderr = std.io.getStdErr().writer();
stderr.print("{s}Unknown option: {s}{s}\n", .{ RED, arg, RESET }) catch {};
std.os.exit(1);
} else {
source_file = arg;
break;
{
var exec_i: usize = 1;
while (exec_i < args.len) : (exec_i += 1) {
const arg = args[exec_i];
if (mem.eql(u8, arg, "--account") or mem.eql(u8, arg, "-p") or
mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "-e") or
mem.eql(u8, arg, "-n") or mem.eql(u8, arg, "-v"))
{
exec_i += 1; // skip value
} else if (mem.eql(u8, arg, "-a")) {
// no value
} else if (mem.startsWith(u8, arg, "-")) {
const stderr = std.io.getStdErr().writer();
stderr.print("{s}Unknown option: {s}{s}\n", .{ RED, arg, RESET }) catch {};
std.os.exit(1);
} else {
source_file = arg;
break;
}
}
}