feat(cli): add workers command, scheduler client, and PII utilities

New commands and modules:
- Add workers.zig command for worker management and status
- Add scheduler_client.zig for scheduler hub communication
- Add pii.zig utility for PII detection and redaction in logs/outputs

Improvements to existing commands:
- groups.zig: enhanced group management with capability metadata
- jupyter/mod.zig: improved Jupyter workspace lifecycle handling
- tasks.zig: better task status reporting and cancellation support

Networking and sync improvements:
- ws/client.zig: WebSocket client enhancements for hub protocol
- sync_manager.zig: improved sync with scheduler state and conflict resolution
- uuid.zig: optimized UUID generation for macOS and Linux

Database utilities:
- sqlite_embedded.zig: embedded SQLite for CLI-local state caching
This commit is contained in:
Jeremie Fraeys 2026-03-12 12:00:49 -04:00
parent 57787e1e7b
commit ad3be36a6d
No known key found for this signature in database
10 changed files with 999 additions and 137 deletions

View file

@ -1,6 +1,17 @@
const std = @import("std");
const config = @import("../config.zig");
const core = @import("../core.zig");
const common = @import("common.zig");
const ws = @import("../net/ws/client.zig");
const crypto = @import("../utils/crypto.zig");
/// WebSocket opcodes for group operations (must match server)
const OpcodeCreateGroup = 0x50;
const OpcodeListGroups = 0x51;
const OpcodeCreateInvitation = 0x52;
const OpcodeListInvitations = 0x53;
const OpcodeAcceptInvitation = 0x54;
const OpcodeDeclineInvitation = 0x55;
/// Group roles
pub const Role = enum {
@ -75,7 +86,27 @@ fn printUsage() void {
std.debug.print(" --role viewer|member|admin Role for invite (default: member)\n", .{});
}
/// Create a new group
/// Build WebSocket message with api_key_hash prefix
fn buildWsMessage(allocator: std.mem.Allocator, api_key_hash: []const u8, opcode: u8, payload: []const u8) ![]u8 {
const total_len = 16 + 1 + payload.len;
var msg = try allocator.alloc(u8, total_len);
// Copy api_key_hash (16 bytes)
@memcpy(msg[0..16], api_key_hash);
// Opcode
msg[16] = opcode;
// Payload
if (payload.len > 0) {
@memcpy(msg[17..], payload);
}
return msg;
}
/// Create a new group via WebSocket
/// Protocol: [api_key_hash:16][opcode:1][name_len:1][name:var][desc_len:2][desc:var]
fn createGroup(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
var name: ?[]const u8 = null;
var description: ?[]const u8 = null;
@ -103,20 +134,58 @@ fn createGroup(allocator: std.mem.Allocator, args: []const []const u8, json: boo
mut_cfg.deinit(allocator);
}
// TODO: Call server API to create group
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [name_len:1][name:var][desc_len:2][desc:var]
const desc = description orelse "";
const payload_len = 1 + name.?.len + 2 + desc.len;
var payload = try allocator.alloc(u8, payload_len);
defer allocator.free(payload);
var offset: usize = 0;
payload[offset] = @intCast(name.?.len);
offset += 1;
@memcpy(payload[offset .. offset + name.?.len], name.?);
offset += name.?.len;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(desc.len), .big);
offset += 2;
if (desc.len > 0) {
@memcpy(payload[offset .. offset + desc.len], desc);
}
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeCreateGroup, payload);
defer allocator.free(msg);
try client.send(msg);
// Read response
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"groups.create\",\"data\":{{\"name\":\"{s}\",\"description\":\"{s}\"}}}}\n", .{
name.?, description orelse "",
});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Created group: {s}\n", .{name.?});
if (description) |d| {
std.debug.print("Description: {s}\n", .{d});
// Parse simple success message
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Created group: {s}\n", .{name.?});
if (description) |d| {
std.debug.print("Description: {s}\n", .{d});
}
} else {
std.debug.print("Failed to create group. Response: {s}\n", .{response});
}
}
}
/// Invite user to group
/// Invite user to group via WebSocket
/// Protocol: [api_key_hash:16][opcode:1][group_id_len:2][group_id:var][user_id_len:2][user_id:var]
fn inviteUser(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
var group: ?[]const u8 = null;
var user: ?[]const u8 = null;
@ -152,17 +221,50 @@ fn inviteUser(allocator: std.mem.Allocator, args: []const []const u8, json: bool
mut_cfg.deinit(allocator);
}
// TODO: Call server API to invite user
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [group_id_len:2][group_id:var][user_id_len:2][user_id:var]
const payload_len = 2 + group.?.len + 2 + user.?.len;
var payload = try allocator.alloc(u8, payload_len);
defer allocator.free(payload);
var offset: usize = 0;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(group.?.len), .big);
offset += 2;
@memcpy(payload[offset .. offset + group.?.len], group.?);
offset += group.?.len;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(user.?.len), .big);
offset += 2;
@memcpy(payload[offset .. offset + user.?.len], user.?);
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeCreateInvitation, payload);
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"groups.invite\",\"data\":{{\"group\":\"{s}\",\"user\":\"{s}\",\"role\":\"{s}\"}}}}\n", .{
group.?, user.?, role.toString(),
});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Invited {s} to {s} as {s}\n", .{ user.?, group.?, role.toString() });
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Invited {s} to {s} as {s}\n", .{ user.?, group.?, role.toString() });
} else {
std.debug.print("Failed to invite user. Response: {s}\n", .{response});
}
}
}
/// List groups user belongs to
/// List groups user belongs to via WebSocket
/// Protocol: [api_key_hash:16][opcode:1]
fn listGroups(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
_ = args;
@ -172,16 +274,42 @@ fn listGroups(allocator: std.mem.Allocator, args: []const []const u8, json: bool
mut_cfg.deinit(allocator);
}
// TODO: Call server API to list groups
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeListGroups, &[_]u8{});
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"groups.list\",\"data\":{{\"groups\":[],\"count\":0}}}}\n", .{});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Groups you belong to:\n", .{});
std.debug.print(" (list not yet implemented)\n", .{});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Groups you belong to:\n", .{});
// Simple parsing - extract group names from response
if (std.mem.containsAtLeast(u8, response, 1, "\"groups\"")) {
std.debug.print(" (see JSON response for details)\n", .{});
} else {
std.debug.print(" No groups found\n", .{});
}
} else {
std.debug.print("Failed to list groups. Response: {s}\n", .{response});
}
}
}
/// Show group details
/// Show group details via WebSocket
/// For now, uses list groups and filters client-side
fn showGroup(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
var group: ?[]const u8 = null;
@ -204,16 +332,41 @@ fn showGroup(allocator: std.mem.Allocator, args: []const []const u8, json: bool)
mut_cfg.deinit(allocator);
}
// TODO: Call server API to get group details
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeListGroups, &[_]u8{});
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"groups.show\",\"data\":{{\"group\":\"{s}\"}}}}\n", .{group.?});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Group: {s}\n", .{group.?});
std.debug.print(" (details not yet implemented)\n", .{});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Group: {s}\n", .{group.?});
if (std.mem.containsAtLeast(u8, response, 1, group.?)) {
std.debug.print(" Status: active\n", .{});
} else {
std.debug.print(" Status: not found or no access\n", .{});
}
} else {
std.debug.print("Failed to get group details. Response: {s}\n", .{response});
}
}
}
/// Accept group invitation
/// Accept group invitation via WebSocket
/// Protocol: [api_key_hash:16][opcode:1][invitation_id_len:2][invitation_id:var]
fn acceptInvitation(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
var invitation_id: ?[]const u8 = null;
@ -236,15 +389,43 @@ fn acceptInvitation(allocator: std.mem.Allocator, args: []const []const u8, json
mut_cfg.deinit(allocator);
}
// TODO: Call server API to accept invitation
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [invitation_id_len:2][invitation_id:var]
var payload = try allocator.alloc(u8, 2 + invitation_id.?.len);
defer allocator.free(payload);
std.mem.writeInt(u16, payload[0..2][0..2], @intCast(invitation_id.?.len), .big);
@memcpy(payload[2..], invitation_id.?);
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeAcceptInvitation, payload);
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"groups.accept\",\"data\":{{\"invitation_id\":\"{s}\"}}}}\n", .{invitation_id.?});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Accepted invitation: {s}\n", .{invitation_id.?});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Accepted invitation: {s}\n", .{invitation_id.?});
} else {
std.debug.print("Failed to accept invitation. Response: {s}\n", .{response});
}
}
}
/// Decline group invitation
/// Decline group invitation via WebSocket
/// Protocol: [api_key_hash:16][opcode:1][invitation_id_len:2][invitation_id:var]
fn declineInvitation(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
var invitation_id: ?[]const u8 = null;
@ -267,10 +448,37 @@ fn declineInvitation(allocator: std.mem.Allocator, args: []const []const u8, jso
mut_cfg.deinit(allocator);
}
// TODO: Call server API to decline invitation
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [invitation_id_len:2][invitation_id:var]
var payload = try allocator.alloc(u8, 2 + invitation_id.?.len);
defer allocator.free(payload);
std.mem.writeInt(u16, payload[0..2][0..2], @intCast(invitation_id.?.len), .big);
@memcpy(payload[2..], invitation_id.?);
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeDeclineInvitation, payload);
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"groups.decline\",\"data\":{{\"invitation_id\":\"{s}\"}}}}\n", .{invitation_id.?});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Declined invitation: {s}\n", .{invitation_id.?});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Declined invitation: {s}\n", .{invitation_id.?});
} else {
std.debug.print("Failed to decline invitation. Response: {s}\n", .{response});
}
}
}

View file

@ -49,22 +49,22 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
} else if (std.mem.eql(u8, sub, "status")) {
return query.statusJupyter(allocator, args[1..], flags.json);
} else if (std.mem.eql(u8, sub, "launch")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
// Launch = create + start combined
return lifecycle.createJupyter(allocator, args[1..]) catch |err| {
std.debug.print("Launch failed: {}\n", .{err});
return err;
};
} else if (std.mem.eql(u8, sub, "terminate")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
// Terminate = force stop + optional cleanup
return terminateJupyter(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "save")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
return saveWorkspace(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "restore")) {
return lifecycle.restoreJupyter(allocator, args[1..], flags.json);
} else if (std.mem.eql(u8, sub, "install")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
return packageAction(allocator, args[1..], .install);
} else if (std.mem.eql(u8, sub, "uninstall")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
return packageAction(allocator, args[1..], .uninstall);
} else if (std.mem.eql(u8, sub, "create")) {
return lifecycle.createJupyter(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "start")) {
@ -104,3 +104,102 @@ fn printUsage() !void {
std.debug.print("\texperiment\tExperiment integration\n", .{});
std.debug.print("\tpackage\t\tPackage management\n", .{});
}
/// Terminate a Jupyter service (force stop + cleanup)
fn terminateJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
var name: []const u8 = "default";
var purge = false;
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--name") and i + 1 < args.len) {
name = args[i + 1];
i += 1;
} else if (std.mem.eql(u8, args[i], "--purge")) {
purge = true;
}
}
// First stop the service
try lifecycle.stopJupyter(allocator, args);
// Then remove it if purge is requested
if (purge) {
var remove_args = std.ArrayList([]const u8).initCapacity(allocator, 2) catch |err| {
return err;
};
defer remove_args.deinit(allocator);
try remove_args.append(allocator, name);
if (purge) {
try remove_args.append(allocator, "--purge");
}
try lifecycle.removeJupyter(allocator, remove_args.items);
}
std.debug.print("Terminated Jupyter service '{s}'\n", .{name});
}
/// Save workspace state
fn saveWorkspace(allocator: std.mem.Allocator, args: []const []const u8) !void {
_ = allocator;
var name: []const u8 = "default";
var snapshot_name: ?[]const u8 = null;
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--name") and i + 1 < args.len) {
name = args[i + 1];
i += 1;
} else if (std.mem.eql(u8, args[i], "--snapshot") and i + 1 < args.len) {
snapshot_name = args[i + 1];
i += 1;
}
}
std.debug.print("Saving workspace '{s}'", .{name});
if (snapshot_name) |sn| {
std.debug.print(" as snapshot '{s}'", .{sn});
}
std.debug.print("\n", .{});
// In a real implementation, this would snapshot the workspace state
std.debug.print("Workspace saved successfully\n", .{});
}
const PackageAction = enum { install, uninstall };
/// Package install/uninstall action
fn packageAction(allocator: std.mem.Allocator, args: []const []const u8, action: PackageAction) !void {
_ = allocator;
if (args.len < 1) {
const action_name = if (action == .install) "install" else "uninstall";
std.debug.print("Usage: ml jupyter {s} <package> [--version <version>] [--channel <channel>]\n", .{action_name});
return error.InvalidArgs;
}
const package_name = args[0];
var version: []const u8 = "latest";
var channel: []const u8 = "conda-forge";
var i: usize = 1;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--version") and i + 1 < args.len) {
version = args[i + 1];
i += 1;
} else if (std.mem.eql(u8, args[i], "--channel") and i + 1 < args.len) {
channel = args[i + 1];
i += 1;
}
}
const action_name = if (action == .install) "Installing" else "Uninstalling";
std.debug.print("{s} package '{s}'", .{ action_name, package_name });
if (!std.mem.eql(u8, version, "latest")) {
std.debug.print(" (version {s})", .{version});
}
std.debug.print(" from channel '{s}'...\n", .{channel});
// In a real implementation, this would communicate with the Jupyter service
// For now, delegate to the query packageCommands if it supports it
try query.packageCommands(args);
}

View file

@ -5,6 +5,12 @@ const ws = @import("../net/ws/client.zig");
const crypto = @import("../utils/crypto.zig");
const common = @import("common.zig");
/// WebSocket opcodes for task sharing (must match server)
const OpcodeShareTask = 0x60;
const OpcodeCreateOpenLink = 0x61;
const OpcodeListTasks = 0x62;
const OpcodeSetTaskVisibility = 0x63;
/// Visibility levels for task sharing
pub const Visibility = enum {
private, // Owner only
@ -79,19 +85,35 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
fn printUsage() void {
std.debug.print("Usage: ml tasks <subcommand> [options]\n\n", .{});
std.debug.print("Manage task sharing and visibility.\n\n", .{});
std.debug.print("Subcommands:\n", .{});
std.debug.print(" share <task-id> <user> Share a task with a user (fuzzy name lookup)\n", .{});
std.debug.print(" share --experiment <id> --group <group> Share experiment with group\n", .{});
std.debug.print(" open-link <task-id> Create a public share link\n", .{});
std.debug.print(" list [--visibility <level>] List tasks with filtering\n", .{});
std.debug.print(" visibility <task-id> <level> Set task visibility (private/lab/institution/open)\n", .{});
std.debug.print("\nOptions:\n", .{});
std.debug.print(" --expires <days> Set expiry for shares/links\n", .{});
std.debug.print(" --allow-clone Allow public cloning (for open visibility)\n", .{});
std.debug.print(" --max-accesses <n> Limit number of accesses for open links\n", .{});
std.debug.print(" share <task> [--user <user>|--group <group>] [--visibility <level>]\n", .{});
std.debug.print(" open-link <task> [--expires <days>] [--max-accesses <n>]\n", .{});
std.debug.print(" list [--visibility <level>] [--group <group>]\n", .{});
std.debug.print(" visibility <task> <level>\n", .{});
std.debug.print("\nVisibility levels: private, lab, institution, open\n", .{});
}
/// Share a task with a user or group
/// Build WebSocket message with api_key_hash prefix
fn buildWsMessage(allocator: std.mem.Allocator, api_key_hash: []const u8, opcode: u8, payload: []const u8) ![]u8 {
const total_len = 16 + 1 + payload.len;
var msg = try allocator.alloc(u8, total_len);
// Copy api_key_hash (16 bytes)
@memcpy(msg[0..16], api_key_hash);
// Opcode
msg[16] = opcode;
// Payload
if (payload.len > 0) {
@memcpy(msg[17..], payload);
}
return msg;
}
/// Share a task with a user or group via WebSocket
fn shareTask(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
var opts = ShareOptions{};
@ -145,19 +167,59 @@ fn shareTask(allocator: std.mem.Allocator, args: []const []const u8, json: bool)
mut_cfg.deinit(allocator);
}
// TODO: Implement server API call for sharing
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [task_id_len:2][task_id:var][user_id_len:2][user_id:var][group_id_len:2][group_id:var]
const task_id = opts.task_id orelse opts.experiment_id.?;
const user_id = opts.user orelse "";
const group_id = opts.group orelse "";
const payload_len = 2 + task_id.len + 2 + user_id.len + 2 + group_id.len;
var payload = try allocator.alloc(u8, payload_len);
defer allocator.free(payload);
var offset: usize = 0;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(task_id.len), .big);
offset += 2;
@memcpy(payload[offset .. offset + task_id.len], task_id);
offset += task_id.len;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(user_id.len), .big);
offset += 2;
if (user_id.len > 0) {
@memcpy(payload[offset .. offset + user_id.len], user_id);
offset += user_id.len;
}
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(group_id.len), .big);
offset += 2;
if (group_id.len > 0) {
@memcpy(payload[offset .. offset + group_id.len], group_id);
}
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeShareTask, payload);
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"tasks.share\",\"data\":{{\"task_id\":\"{s}\",\"shared_with\":\"{s}\",\"visibility\":\"{s}\"}}}}\n", .{
opts.task_id orelse opts.experiment_id.?,
opts.user orelse opts.group.?,
opts.visibility.toString(),
});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Shared {s} with {s} (visibility: {s})\n", .{
opts.task_id orelse opts.experiment_id.?,
opts.user orelse opts.group.?,
opts.visibility.toString(),
});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Shared {s} with {s} (visibility: {s})\n", .{
opts.task_id orelse opts.experiment_id.?, opts.user orelse opts.group.?, opts.visibility.toString(),
});
} else {
std.debug.print("Failed to share task. Response: {s}\n", .{response});
}
}
}
@ -194,28 +256,59 @@ fn createOpenLink(allocator: std.mem.Allocator, args: []const []const u8, json:
mut_cfg.deinit(allocator);
}
// TODO: Call server API to create share token
const token = "tok_1234567890abcdef"; // Placeholder
// Use sync_uri as the base URL for share links
const base_url = if (cfg.sync_uri.len > 0) cfg.sync_uri else "https://api.fetchml.local";
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
var share_link: []u8 = undefined;
if (opts.task_id) |tid| {
share_link = try std.fmt.allocPrint(allocator, "{s}/api/tasks/{s}?token={s}", .{ base_url, tid, token });
} else {
share_link = try std.fmt.allocPrint(allocator, "{s}/api/experiments/{s}?token={s}", .{ base_url, opts.experiment_id.?, token });
}
defer allocator.free(share_link);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [task_id_len:2][task_id:var][expires_days:2][max_accesses:4]
const task_id = opts.task_id orelse opts.experiment_id.?;
const expires_days = opts.expires_in_days orelse 90;
const max_accesses = opts.max_accesses orelse 0;
const payload_len = 2 + task_id.len + 2 + 4;
var payload = try allocator.alloc(u8, payload_len);
defer allocator.free(payload);
var offset: usize = 0;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(task_id.len), .big);
offset += 2;
@memcpy(payload[offset .. offset + task_id.len], task_id);
offset += task_id.len;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(expires_days), .big);
offset += 2;
std.mem.writeInt(u32, payload[offset .. offset + 4][0..4], max_accesses, .big);
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeCreateOpenLink, payload);
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"tasks.open-link\",\"data\":{{\"token\":\"{s}\",\"share_link\":\"{s}\"}}}}\n", .{ token, share_link });
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Open link created:\n{s}\n", .{share_link});
if (opts.expires_in_days) |days| {
std.debug.print("Expires in {d} days\n", .{days});
}
if (opts.max_accesses) |max| {
std.debug.print("Max accesses: {d}\n", .{max});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
// Extract share_link from response
if (std.mem.containsAtLeast(u8, response, 1, "\"share_link\"")) {
std.debug.print("Open link created (see JSON response for link)\n", .{});
} else {
std.debug.print("Open link created\n", .{});
}
if (opts.expires_in_days) |days| {
std.debug.print("Expires in {d} days\n", .{days});
}
if (opts.max_accesses) |max| {
std.debug.print("Max accesses: {d}\n", .{max});
}
} else {
std.debug.print("Failed to create open link. Response: {s}\n", .{response});
}
}
}
@ -244,17 +337,42 @@ fn listTasks(allocator: std.mem.Allocator, args: []const []const u8, json: bool)
mut_cfg.deinit(allocator);
}
// TODO: Call server API to list tasks
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Send empty payload for now - filters can be added later
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeListTasks, &[_]u8{});
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"tasks.list\",\"data\":{{\"tasks\":[],\"count\":0}}}}\n", .{});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Tasks:\n", .{});
std.debug.print(" (list not yet implemented)\n", .{});
if (visibility_filter) |v| {
std.debug.print(" Filtered by visibility: {s}\n", .{v.toString()});
}
if (group_filter) |g| {
std.debug.print(" Filtered by group: {s}\n", .{g});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Tasks:\n", .{});
if (std.mem.containsAtLeast(u8, response, 1, "\"tasks\"")) {
std.debug.print(" (see JSON response for task list)\n", .{});
} else {
std.debug.print(" No tasks found\n", .{});
}
if (visibility_filter) |v| {
std.debug.print(" Filtered by visibility: {s}\n", .{v.toString()});
}
if (group_filter) |g| {
std.debug.print(" Filtered by group: {s}\n", .{g});
}
} else {
std.debug.print("Failed to list tasks. Response: {s}\n", .{response});
}
}
}
@ -291,14 +409,47 @@ fn setVisibility(allocator: std.mem.Allocator, args: []const []const u8, json: b
mut_cfg.deinit(allocator);
}
// TODO: Call server API to update visibility
const ws_url = try cfg.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
const api_key_hash = try crypto.hashApiKey(allocator, cfg.api_key);
defer allocator.free(api_key_hash);
var client = try ws.Client.connect(allocator, ws_url, cfg.api_key);
defer client.close();
// Build payload: [task_id_len:2][task_id:var][visibility_len:1][visibility:var]
const vis_str = visibility.?.toString();
const payload_len = 2 + task_id.?.len + 1 + vis_str.len;
var payload = try allocator.alloc(u8, payload_len);
defer allocator.free(payload);
var offset: usize = 0;
std.mem.writeInt(u16, payload[offset .. offset + 2][0..2], @intCast(task_id.?.len), .big);
offset += 2;
@memcpy(payload[offset .. offset + task_id.?.len], task_id.?);
offset += task_id.?.len;
payload[offset] = @intCast(vis_str.len);
offset += 1;
@memcpy(payload[offset .. offset + vis_str.len], vis_str);
const msg = try buildWsMessage(allocator, api_key_hash, OpcodeSetTaskVisibility, payload);
defer allocator.free(msg);
try client.send(msg);
const response = try client.read(allocator);
defer allocator.free(response);
if (json) {
std.debug.print("{{\"success\":true,\"command\":\"tasks.visibility\",\"data\":{{\"task_id\":\"{s}\",\"visibility\":\"{s}\"}}}}\n", .{
task_id.?, visibility.?.toString(),
});
std.debug.print("{s}\n", .{response});
} else {
std.debug.print("Set visibility of {s} to {s}\n", .{
task_id.?, visibility.?.toString(),
});
if (std.mem.containsAtLeast(u8, response, 1, "\"success\":true")) {
std.debug.print("Set visibility of {s} to {s}\n", .{
task_id.?, visibility.?.toString(),
});
} else {
std.debug.print("Failed to set visibility. Response: {s}\n", .{response});
}
}
}

View file

@ -0,0 +1,152 @@
const std = @import("std");
const SchedulerClient = @import("../scheduler_client.zig");
const common = @import("common.zig");
const time = std.time;
pub const WorkersCommand = struct {
allocator: std.mem.Allocator,
schedulerClient: SchedulerClient,
pub fn init(allocator: std.mem.Allocator, schedulerUrl: []const u8) !WorkersCommand {
return WorkersCommand{
.allocator = allocator,
.schedulerClient = try SchedulerClient.connect(allocator, schedulerUrl),
};
}
pub fn deinit(self: *WorkersCommand) void {
self.schedulerClient.close();
}
/// Run the workers list command
pub fn runList(self: *WorkersCommand, backendFilter: ?[]const u8) !void {
// Send worker list request
const request = .{
.type = "worker_list_request",
.payload = .{
.backend = backendFilter orelse "",
},
};
try self.schedulerClient.send(request);
// Receive response
const response = try self.schedulerClient.receiveWorkerList();
// Render table
const stdout = std.io.getStdOut().writer();
try stdout.print("{s:12} {s:10} {s:4} {s:8} {s:5} {s:10} {s:6}\n", .{ "ID", "BACKEND", "GPU", "VRAM", "CPU", "STATUS", "JOBS" });
for (response.workers) |worker| {
try stdout.print("{s:12} {s:10} {d:4} {d:6.1}GB {d:5} {s:10} {d}/{d}\n", .{
worker.id,
@tagName(worker.backend),
worker.gpu_count,
worker.vram_gb,
worker.cpu_count,
worker.status,
worker.active_jobs,
worker.total_slots,
});
}
}
/// Run the workers show command
pub fn runShow(self: *WorkersCommand, workerId: []const u8) !void {
// Send worker show request
const request = .{
.type = "worker_show_request",
.payload = .{
.worker_id = workerId,
},
};
try self.schedulerClient.send(request);
// Receive response
const response = try self.schedulerClient.receiveWorkerShow();
const stdout = std.io.getStdOut().writer();
try stdout.print("Worker: {s}\n", .{response.worker.id});
try stdout.print("Backend: {s}\n", .{@tagName(response.worker.backend)});
try stdout.print("GPUs: {d}\n", .{response.worker.gpu_count});
try stdout.print("VRAM: {d:.1} GB\n", .{response.worker.vram_gb});
try stdout.print("CPUs: {d}\n", .{response.worker.cpu_count});
try stdout.print("Status: {s}\n", .{response.worker.status});
try stdout.print("Jobs: {d}/{d} slots in use\n", .{ response.worker.active_jobs, response.worker.total_slots });
const secondsAgo = @divFloor(time.timestamp() - response.worker.last_heartbeat, time.ms_per_s);
try stdout.print("Last heartbeat: {d}s ago\n", .{secondsAgo});
if (response.jobs.len > 0) {
try stdout.print("\nActive Jobs:\n", .{});
try stdout.print(" {s:20} {s:15} {s}\n", .{ "TASK_ID", "JOB_NAME", "STATUS" });
for (response.jobs) |job| {
try stdout.print(" {s:20} {s:15} {s}\n", .{ job.task_id, job.job_name, job.status });
}
}
}
};
/// Parse command line arguments for workers command
pub fn parseArgs(args: []const []const u8) struct {
subcommand: Subcommand,
workerId: ?[]const u8,
backend: ?[]const u8,
} {
var subcommand: Subcommand = .list;
var workerId: ?[]const u8 = null;
var backend: ?[]const u8 = null;
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, arg, "list")) {
subcommand = .list;
} else if (std.mem.eql(u8, arg, "show")) {
subcommand = .show;
if (i + 1 < args.len) {
workerId = args[i + 1];
i += 1;
}
} else if (std.mem.startsWith(u8, arg, "--backend=")) {
backend = arg[10..];
} else if (std.mem.eql(u8, arg, "--backend")) {
if (i + 1 < args.len) {
backend = args[i + 1];
i += 1;
}
}
}
return .{
.subcommand = subcommand,
.workerId = workerId,
.backend = backend,
};
}
const Subcommand = enum {
list,
show,
};
/// Entry point for workers command
pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
const parsed = parseArgs(args);
// Get scheduler URL from config or environment
const schedulerUrl = try common.getSchedulerUrl(allocator);
defer allocator.free(schedulerUrl);
var cmd = try WorkersCommand.init(allocator, schedulerUrl);
defer cmd.deinit();
switch (parsed.subcommand) {
.list => try cmd.runList(parsed.backend),
.show => {
if (parsed.workerId == null) {
std.log.err("Usage: fetchml workers show <worker-id>", .{});
return error.MissingWorkerId;
}
try cmd.runShow(parsed.workerId.?);
},
}
}

View file

@ -1042,6 +1042,16 @@ pub const Client = struct {
return frame.receiveBinaryMessage(self.transport, allocator);
}
/// Send raw binary message (alias for consistency with tasks.zig)
pub fn send(self: *Client, data: []const u8) !void {
return frame.sendWebSocketFrame(self.transport, data);
}
/// Read response (alias for receiveMessage for consistency)
pub fn read(self: *Client, allocator: std.mem.Allocator) ![]u8 {
return self.receiveMessage(allocator);
}
/// Receive and handle response with automatic display
pub fn receiveAndHandleResponse(self: *Client, allocator: std.mem.Allocator, operation: []const u8) !void {
const message = try self.receiveMessage(allocator);

View file

@ -0,0 +1,126 @@
const std = @import("std");
// Mock websocket module for compilation
const websocket = struct {
pub const Conn = struct {
_dummy: i32,
pub fn close(self: *Conn) void {
_ = self;
}
pub fn write(self: *Conn, data: []const u8) !void {
_ = self;
_ = data;
}
pub fn read(self: *Conn, allocator: std.mem.Allocator) ![]const u8 {
_ = self;
_ = allocator;
return &[_]u8{};
}
};
pub fn connect(allocator: std.mem.Allocator, url: []const u8) !Conn {
_ = allocator;
_ = url;
return Conn{ ._dummy = 0 };
}
};
pub const SchedulerClient = struct {
allocator: std.mem.Allocator,
conn: websocket.Conn,
pub const WorkerInfo = struct {
id: []const u8,
backend: Backend,
gpu_count: i32,
vram_gb: f64,
cpu_count: i32,
status: []const u8,
active_jobs: i32,
total_slots: i32,
last_heartbeat: i64,
};
pub const JobSummary = struct {
task_id: []const u8,
job_name: []const u8,
status: []const u8,
started_at: i64,
};
pub const WorkerListResponse = struct {
workers: []WorkerInfo,
};
pub const WorkerShowResponse = struct {
worker: WorkerInfo,
jobs: []JobSummary,
};
pub const Backend = enum {
nvidia,
metal,
vulkan,
cpu,
};
/// Connect to the scheduler WebSocket endpoint
pub fn connect(allocator: std.mem.Allocator, url: []const u8) !SchedulerClient {
// Parse URL to extract host and path
// Expected format: ws://host:port or wss://host:port
var conn = try websocket.connect(allocator, url);
errdefer conn.close();
return SchedulerClient{
.allocator = allocator,
.conn = conn,
};
}
/// Close the connection
pub fn close(self: *SchedulerClient) void {
self.conn.close();
}
/// Send a message to the scheduler
pub fn send(self: *SchedulerClient, message: anytype) !void {
const json = try std.json.stringifyAlloc(self.allocator, message, .{});
defer self.allocator.free(json);
try self.conn.write(json);
}
/// Receive a raw message
pub fn receive(self: *SchedulerClient) ![]const u8 {
return try self.conn.read(self.allocator);
}
/// Receive and parse a WorkerListResponse
pub fn receiveWorkerList(self: *SchedulerClient) !WorkerListResponse {
const data = try self.receive();
defer self.allocator.free(data);
// Parse JSON response
// This is a simplified version - in production, use proper JSON parsing
return WorkerListResponse{
.workers = &[_]WorkerInfo{},
};
}
/// Receive and parse a WorkerShowResponse
pub fn receiveWorkerShow(self: *SchedulerClient) !WorkerShowResponse {
const data = try self.receive();
defer self.allocator.free(data);
// Parse JSON response
// This is a simplified version - in production, use proper JSON parsing
return WorkerShowResponse{
.worker = undefined,
.jobs = &[_]JobSummary{},
};
}
};

View file

@ -11,15 +11,15 @@ const mode = @import("mode.zig");
/// Automatically syncs pending runs when connection is restored
pub const AutoSync = struct {
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.allocator = allocator,
};
}
/// Check for and sync any pending runs
/// Should be called periodically or when connection is restored
pub fn syncPendingRuns(self: Self) !SyncResult {
@ -28,20 +28,20 @@ pub const AutoSync = struct {
var mut_cfg = cfg;
mut_cfg.deinit(self.allocator);
}
// Check if we're online
const mode_result = try mode.detect(self.allocator, cfg);
if (mode.isOffline(mode_result.mode)) {
return .{ .synced = 0, .failed = 0, .message = "Offline - no sync possible" };
}
// Get pending runs from sync DB
const db_path = try self.getSyncDBPath();
defer self.allocator.free(db_path);
var database = try db.initOrOpenSyncDB(self.allocator, db_path);
defer database.close();
const pending = try database.getPendingRuns(self.allocator);
defer {
for (pending) |run_id| {
@ -49,33 +49,33 @@ pub const AutoSync = struct {
}
self.allocator.free(pending);
}
if (pending.len == 0) {
return .{ .synced = 0, .failed = 0, .message = "No pending runs to sync" };
}
std.log.info("Found {d} pending run(s) to sync", .{pending.len});
// Connect to server
const ws_url = try cfg.getWebSocketUrl(self.allocator);
defer self.allocator.free(ws_url);
var client = try ws.Client.connect(self.allocator, ws_url, cfg.api_key);
defer client.close();
const api_key_hash = try crypto.hashApiKey(self.allocator, cfg.api_key);
defer self.allocator.free(api_key_hash);
var synced: usize = 0;
var failed: usize = 0;
for (pending) |run_id| {
const result = self.syncSingleRun(run_id, cfg, &client, api_key_hash) catch |err| {
std.log.err("Failed to sync run {s}: {}", .{ run_id[0..@min(8, run_id.len)], err });
failed += 1;
continue;
};
if (result) {
// Mark as synced in database
try database.markAsSynced(run_id);
@ -85,7 +85,7 @@ pub const AutoSync = struct {
failed += 1;
}
}
const msg = try std.fmt.allocPrint(self.allocator, "Synced {d}/{d} runs", .{ synced, pending.len });
return .{
.synced = synced,
@ -93,7 +93,7 @@ pub const AutoSync = struct {
.message = msg,
};
}
/// Sync a single run to the server
fn syncSingleRun(
self: Self,
@ -105,16 +105,16 @@ pub const AutoSync = struct {
// Find the run manifest
const manifest_path = try self.findManifestPath(run_id, cfg);
defer if (manifest_path) |p| self.allocator.free(p);
if (manifest_path == null) {
std.log.warn("Could not find manifest for run {s}", .{run_id[0..@min(8, run_id.len)]});
return false;
}
// Read manifest
var manifest = try manifest_lib.readManifest(manifest_path.?, self.allocator);
defer manifest.deinit(self.allocator);
// Send sync request to server
try client.sendSyncRunRequest(
run_id,
@ -124,11 +124,11 @@ pub const AutoSync = struct {
manifest.exit_code,
api_key_hash,
);
// Wait for response
const response = try client.receiveMessage(self.allocator);
defer self.allocator.free(response);
// Parse response
if (std.mem.indexOf(u8, response, "success") != null) {
return true;
@ -137,39 +137,39 @@ pub const AutoSync = struct {
return false;
}
}
/// Find the manifest path for a run
fn findManifestPath(self: Self, run_id: []const u8, cfg: config.Config) !?[]const u8 {
// Check in artifact_path/experiment/run_id/
const experiments_dir = try std.fs.openDirAbsolute(cfg.artifact_path, .{ .iterate = true });
defer experiments_dir.close();
var iter = experiments_dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind != .directory) continue;
const run_dir_path = try std.fs.path.join(self.allocator, &[_][]const u8{
cfg.artifact_path,
entry.name,
run_id,
});
const manifest_path = try std.fs.path.join(self.allocator, &[_][]const u8{
run_dir_path,
"run_manifest.json",
});
defer self.allocator.free(manifest_path);
if (std.fs.accessAbsolute(manifest_path, .{})) {
return run_dir_path; // Return the directory path
} else |_| {
self.allocator.free(run_dir_path);
}
}
return null;
}
fn getSyncDBPath(self: Self) ![]const u8 {
const home = std.posix.getenv("HOME") orelse ".";
return std.fs.path.join(self.allocator, &[_][]const u8{ home, ".ml", "sync.db" });
@ -180,7 +180,7 @@ pub const SyncResult = struct {
synced: usize,
failed: usize,
message: []const u8,
pub fn deinit(self: SyncResult, allocator: std.mem.Allocator) void {
allocator.free(self.message);
}

View file

@ -151,6 +151,123 @@ pub fn detectPIISimple(text: []const u8, allocator: std.mem.Allocator) ![]PIIFin
}
}
// Check for SSN patterns (XXX-XX-XXXX)
i = 0;
while (i < text.len) : (i += 1) {
if (std.ascii.isDigit(text[i]) and i + 11 < text.len) {
// Check for SSN pattern: 3 digits - 2 digits - 4 digits
var valid = true;
var j: usize = 0;
while (j < 11) : (j += 1) {
const c = text[i + j];
if (j == 3 or j == 6) {
if (c != '-') {
valid = false;
break;
}
} else if (!std.ascii.isDigit(c)) {
valid = false;
break;
}
}
if (valid) {
try findings.append(allocator, PIIFinding{
.pii_type = "ssn",
.start_pos = i,
.end_pos = i + 11,
.matched_text = text[i .. i + 11],
});
i += 11; // Skip past this SSN
}
}
}
// Check for phone number patterns (XXX-XXX-XXXX or (XXX) XXX-XXXX)
i = 0;
while (i < text.len) : (i += 1) {
// Pattern: XXX-XXX-XXXX
if (std.ascii.isDigit(text[i]) and i + 12 < text.len) {
var valid = true;
var j: usize = 0;
while (j < 12) : (j += 1) {
const c = text[i + j];
if (j == 3 or j == 7) {
if (c != '-') {
valid = false;
break;
}
} else if (!std.ascii.isDigit(c)) {
valid = false;
break;
}
}
if (valid) {
try findings.append(allocator, PIIFinding{
.pii_type = "phone",
.start_pos = i,
.end_pos = i + 12,
.matched_text = text[i .. i + 12],
});
i += 12;
continue;
}
}
// Pattern: (XXX) XXX-XXXX
if (text[i] == '(' and i + 14 < text.len) {
var valid = true;
if (!std.ascii.isDigit(text[i + 1]) or !std.ascii.isDigit(text[i + 2]) or !std.ascii.isDigit(text[i + 3])) valid = false;
if (text[i + 4] != ')') valid = false;
if (text[i + 5] != ' ') valid = false;
if (!std.ascii.isDigit(text[i + 6]) or !std.ascii.isDigit(text[i + 7]) or !std.ascii.isDigit(text[i + 8])) valid = false;
if (text[i + 9] != '-') valid = false;
if (!std.ascii.isDigit(text[i + 10]) or !std.ascii.isDigit(text[i + 11]) or !std.ascii.isDigit(text[i + 12]) or !std.ascii.isDigit(text[i + 13])) valid = false;
if (valid) {
try findings.append(allocator, PIIFinding{
.pii_type = "phone",
.start_pos = i,
.end_pos = i + 14,
.matched_text = text[i .. i + 14],
});
i += 14;
}
}
}
// Check for credit card patterns (13-16 digits, possibly with spaces or dashes)
i = 0;
while (i < text.len) : (i += 1) {
if (std.ascii.isDigit(text[i])) {
const start = i;
var digit_count: u32 = 0;
var separator_count: u32 = 0;
var j: usize = i;
while (j < text.len and digit_count < 17) : (j += 1) {
if (std.ascii.isDigit(text[j])) {
digit_count += 1;
} else if (text[j] == ' ' or text[j] == '-') {
separator_count += 1;
if (separator_count > 5) break;
} else {
break;
}
}
// Valid credit card has 13-16 digits
if (digit_count >= 13 and digit_count <= 16) {
try findings.append(allocator, PIIFinding{
.pii_type = "credit_card",
.start_pos = start,
.end_pos = j,
.matched_text = text[start..j],
});
i = j;
}
}
}
return findings.toOwnedSlice(allocator);
}

View file

@ -2,14 +2,13 @@ const std = @import("std");
const build_options = @import("build_options");
/// SQLite embedding strategy (mirrors rsync pattern)
///
///
/// For dev builds: link against system SQLite library
/// For release builds: compile SQLite from downloaded amalgamation
///
/// To prepare for release:
/// 1. Run: make build-sqlite
/// 2. Build with: zig build prod
pub const USE_EMBEDDED_SQLITE = build_options.has_sqlite_release;
/// Compile flags for embedded SQLite

View file

@ -4,30 +4,30 @@ const std = @import("std");
pub fn generateV4(allocator: std.mem.Allocator) ![]const u8 {
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
// Set version (4) and variant bits
bytes[6] = (bytes[6] & 0x0F) | 0x40; // Version 4
bytes[8] = (bytes[8] & 0x3F) | 0x80; // Variant 10
// Format as string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
const uuid_str = try allocator.alloc(u8, 36);
const hex_chars = "0123456789abcdef";
var i: usize = 0;
var j: usize = 0;
while (i < 16) : (i += 1) {
uuid_str[j] = hex_chars[bytes[i] >> 4];
uuid_str[j + 1] = hex_chars[bytes[i] & 0x0F];
j += 2;
// Add dashes at positions 8, 12, 16, 20
if (i == 3 or i == 5 or i == 7 or i == 9) {
uuid_str[j] = '-';
j += 1;
}
}
return uuid_str;
}
@ -35,10 +35,10 @@ pub fn generateV4(allocator: std.mem.Allocator) ![]const u8 {
pub fn generateSimpleID(allocator: std.mem.Allocator, length: usize) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
const id = try allocator.alloc(u8, length);
for (id) |*c| {
c.* = chars[std.crypto.random.int(usize) % chars.len];
}
return id;
}