Replace space-padding with consistent tab (\t) alignment in all printUsage() functions. Add ligature-friendly ASCII symbols: - => for results/outcomes (renders as ⇒ with ligatures) - ~> for modifications/changes (renders as ~> with ligatures) - -> for state transitions (renders as → with ligatures) - [OK] / [FAIL] for status indicators All symbols use ASCII 32-126 for xargs-safe, copy-pasteable output.
905 lines
31 KiB
Zig
905 lines
31 KiB
Zig
const std = @import("std");
|
|
const ws = @import("../net/ws/client.zig");
|
|
const protocol = @import("../net/protocol.zig");
|
|
const crypto = @import("../utils/crypto.zig");
|
|
const Config = @import("../config.zig").Config;
|
|
const core = @import("../core.zig");
|
|
|
|
const blocked_packages = [_][]const u8{ "requests", "urllib3", "httpx", "aiohttp", "socket", "telnetlib" };
|
|
|
|
// Security validation functions
|
|
fn validatePackageName(name: []const u8) bool {
|
|
// Package names should only contain alphanumeric characters, underscores, hyphens, and dots
|
|
var i: usize = 0;
|
|
while (i < name.len) {
|
|
const c = name[i];
|
|
if (!((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or
|
|
(c >= '0' and c <= '9') or c == '_' or c == '-' or c == '.'))
|
|
{
|
|
return false;
|
|
}
|
|
i += 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
|
_ = json;
|
|
if (args.len < 1) {
|
|
core.output.err("Usage: ml jupyter restore <name>");
|
|
return;
|
|
}
|
|
const name = args[0];
|
|
|
|
const config = try Config.load(allocator);
|
|
defer {
|
|
var mut_config = config;
|
|
mut_config.deinit(allocator);
|
|
}
|
|
|
|
const url = try config.getWebSocketUrl(allocator);
|
|
defer allocator.free(url);
|
|
|
|
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
|
|
std.debug.print("Failed to connect to server: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer client.close();
|
|
|
|
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
|
|
defer allocator.free(api_key_hash);
|
|
|
|
std.debug.print("Restoring workspace {s}...", .{name});
|
|
|
|
client.sendRestoreJupyter(name, api_key_hash) catch {
|
|
core.output.err("Failed to send restore command");
|
|
return;
|
|
};
|
|
|
|
const response = client.receiveMessage(allocator) catch |err| {
|
|
std.debug.print("Failed to receive response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer allocator.free(response);
|
|
|
|
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
|
|
std.debug.print("Failed to parse response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer packet.deinit(allocator);
|
|
|
|
switch (packet.packet_type) {
|
|
.success => {
|
|
if (packet.success_message) |msg| {
|
|
std.debug.print("{s}", .{msg});
|
|
} else {
|
|
std.debug.print("Workspace restored.", .{});
|
|
}
|
|
},
|
|
.error_packet => {
|
|
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
|
|
std.debug.print("Error: {s}\n", .{error_msg});
|
|
},
|
|
else => {
|
|
core.output.err("Unexpected response type");
|
|
},
|
|
}
|
|
}
|
|
|
|
fn validateWorkspacePath(path: []const u8) bool {
|
|
// Check for path traversal attempts
|
|
if (std.mem.indexOf(u8, path, "..") != null) {
|
|
return false;
|
|
}
|
|
|
|
// Check for absolute paths (should be relative)
|
|
if (path.len > 0 and path[0] == '/') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
fn validateChannel(channel: []const u8) bool {
|
|
const trusted_channels = [_][]const u8{ "conda-forge", "defaults", "pytorch", "nvidia" };
|
|
for (trusted_channels) |trusted| {
|
|
if (std.mem.eql(u8, channel, trusted)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn isPackageBlocked(name: []const u8) bool {
|
|
for (blocked_packages) |blocked| {
|
|
if (std.mem.eql(u8, name, blocked)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pub fn isValidTopLevelAction(action: []const u8) bool {
|
|
return std.mem.eql(u8, action, "create") or
|
|
std.mem.eql(u8, action, "start") or
|
|
std.mem.eql(u8, action, "stop") or
|
|
std.mem.eql(u8, action, "status") or
|
|
std.mem.eql(u8, action, "list") or
|
|
std.mem.eql(u8, action, "remove") or
|
|
std.mem.eql(u8, action, "restore") or
|
|
std.mem.eql(u8, action, "package");
|
|
}
|
|
|
|
pub fn defaultWorkspacePath(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
|
|
return std.fmt.allocPrint(allocator, "./{s}", .{name});
|
|
}
|
|
|
|
pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
var flags = core.flags.CommonFlags{};
|
|
|
|
if (args.len == 0) {
|
|
return printUsage();
|
|
}
|
|
|
|
// Global flags
|
|
for (args) |arg| {
|
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
|
return printUsage();
|
|
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
flags.json = true;
|
|
}
|
|
}
|
|
|
|
const sub = args[0];
|
|
|
|
if (std.mem.eql(u8, sub, "list")) {
|
|
return listJupyter(allocator, args[1..], flags.json);
|
|
} else if (std.mem.eql(u8, sub, "status")) {
|
|
return statusJupyter(allocator, args[1..], flags.json);
|
|
} else if (std.mem.eql(u8, sub, "launch")) {
|
|
return launchJupyter(allocator, args[1..], flags.json);
|
|
} else if (std.mem.eql(u8, sub, "terminate")) {
|
|
return terminateJupyter(allocator, args[1..], flags.json);
|
|
} else if (std.mem.eql(u8, sub, "save")) {
|
|
return saveJupyter(allocator, args[1..], flags.json);
|
|
} else if (std.mem.eql(u8, sub, "restore")) {
|
|
return restoreJupyter(allocator, args[1..], flags.json);
|
|
} else if (std.mem.eql(u8, sub, "install")) {
|
|
return installJupyter(allocator, args[1..]);
|
|
} else if (std.mem.eql(u8, sub, "uninstall")) {
|
|
return uninstallJupyter(allocator, args[1..]);
|
|
} else {
|
|
core.output.err("Unknown subcommand");
|
|
return error.InvalidArgs;
|
|
}
|
|
}
|
|
|
|
fn printUsage() !void {
|
|
std.debug.print("Usage: ml jupyter <command> [args]\n", .{});
|
|
std.debug.print("\nCommands:\n", .{});
|
|
std.debug.print("\tlist\t\tList Jupyter services\n", .{});
|
|
std.debug.print("\tstatus\t\tShow Jupyter service status\n", .{});
|
|
std.debug.print("\tlaunch\t\tLaunch a new Jupyter service\n", .{});
|
|
std.debug.print("\tterminate\tTerminate a Jupyter service\n", .{});
|
|
std.debug.print("\tsave\t\tSave workspace\n", .{});
|
|
std.debug.print("\trestore\t\tRestore workspace\n", .{});
|
|
std.debug.print("\tinstall\t\tInstall packages\n", .{});
|
|
std.debug.print("\tuninstall\tUninstall packages\n", .{});
|
|
}
|
|
|
|
fn printUsagePackage() void {
|
|
std.debug.print("Usage: ml jupyter package <action> [options]\n", .{});
|
|
std.debug.print("Actions:\n", .{});
|
|
std.debug.print("{s}", .{});
|
|
std.debug.print("Options:\n", .{});
|
|
std.debug.print("\t--help, -h Show this help message\n", .{});
|
|
}
|
|
|
|
fn createJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
if (args.len < 1) {
|
|
std.debug.print("Usage: ml jupyter create <name> [--path <path>] [--password <password>]\n", .{});
|
|
return;
|
|
}
|
|
|
|
const name = args[0];
|
|
var workspace_path_owned: ?[]u8 = null;
|
|
defer if (workspace_path_owned) |p| allocator.free(p);
|
|
var workspace_path: []const u8 = "";
|
|
var password: []const u8 = "";
|
|
|
|
var i: usize = 1;
|
|
while (i < args.len) : (i += 1) {
|
|
if (std.mem.eql(u8, args[i], "--path") and i + 1 < args.len) {
|
|
workspace_path = args[i + 1];
|
|
i += 1;
|
|
} else if (std.mem.eql(u8, args[i], "--password") and i + 1 < args.len) {
|
|
password = args[i + 1];
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
if (workspace_path.len == 0) {
|
|
const p = try defaultWorkspacePath(allocator, name);
|
|
workspace_path_owned = p;
|
|
workspace_path = p;
|
|
}
|
|
|
|
if (!validateWorkspacePath(workspace_path)) {
|
|
std.debug.print("Invalid workspace path\n", .{});
|
|
return error.InvalidArgs;
|
|
}
|
|
|
|
std.fs.cwd().makePath(workspace_path) catch |err| {
|
|
std.debug.print("Failed to create workspace directory: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
var start_args = std.ArrayList([]const u8).initCapacity(allocator, 8) catch |err| {
|
|
std.debug.print("Failed to allocate args: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer start_args.deinit(allocator);
|
|
|
|
try start_args.append(allocator, "--name");
|
|
try start_args.append(allocator, name);
|
|
try start_args.append(allocator, "--workspace");
|
|
try start_args.append(allocator, workspace_path);
|
|
if (password.len > 0) {
|
|
try start_args.append(allocator, "--password");
|
|
try start_args.append(allocator, password);
|
|
}
|
|
|
|
try startJupyter(allocator, start_args.items);
|
|
}
|
|
|
|
fn startJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
// Parse args (simple for now: name)
|
|
var name: []const u8 = "default";
|
|
var workspace: []const u8 = "./workspace";
|
|
var password: []const u8 = "";
|
|
|
|
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], "--workspace") and i + 1 < args.len) {
|
|
workspace = args[i + 1];
|
|
i += 1;
|
|
} else if (std.mem.eql(u8, args[i], "--password") and i + 1 < args.len) {
|
|
password = args[i + 1];
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
const config = try Config.load(allocator);
|
|
defer {
|
|
var mut_config = config;
|
|
mut_config.deinit(allocator);
|
|
}
|
|
|
|
const url = try config.getWebSocketUrl(allocator);
|
|
defer allocator.free(url);
|
|
|
|
// Connect to WebSocket
|
|
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
|
|
std.debug.print("Failed to connect to server: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer client.close();
|
|
|
|
// Hash API key
|
|
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
|
|
defer allocator.free(api_key_hash);
|
|
|
|
std.debug.print("Starting Jupyter service '{s}'...\n", .{name});
|
|
|
|
// Send start command
|
|
client.sendStartJupyter(name, workspace, password, api_key_hash) catch |err| {
|
|
std.debug.print("Failed to send start command: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
// Receive response
|
|
const response = client.receiveMessage(allocator) catch |err| {
|
|
std.debug.print("Failed to receive response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer allocator.free(response);
|
|
|
|
// Parse response packet
|
|
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
|
|
std.debug.print("Failed to parse response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer packet.deinit(allocator);
|
|
|
|
switch (packet.packet_type) {
|
|
.success => {
|
|
std.debug.print("Jupyter service started!\n", .{});
|
|
if (packet.success_message) |msg| {
|
|
std.debug.print("{s}\n", .{msg});
|
|
}
|
|
},
|
|
.error_packet => {
|
|
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
|
|
std.debug.print("Failed to start service: {s}\n", .{error_msg});
|
|
if (packet.error_details) |details| {
|
|
std.debug.print("Details: {s}\n", .{details});
|
|
} else if (packet.error_message) |msg| {
|
|
std.debug.print("Details: {s}\n", .{msg});
|
|
}
|
|
},
|
|
else => {
|
|
std.debug.print("Unexpected response type\n", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn stopJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
if (args.len < 1) {
|
|
std.debug.print("Usage: ml jupyter stop <service_id>\n", .{});
|
|
return;
|
|
}
|
|
const service_id = args[0];
|
|
|
|
const config = try Config.load(allocator);
|
|
defer {
|
|
var mut_config = config;
|
|
mut_config.deinit(allocator);
|
|
}
|
|
|
|
const url = try config.getWebSocketUrl(allocator);
|
|
defer allocator.free(url);
|
|
|
|
// Connect to WebSocket
|
|
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
|
|
std.debug.print("Failed to connect to server: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer client.close();
|
|
|
|
// Hash API key
|
|
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
|
|
defer allocator.free(api_key_hash);
|
|
|
|
std.debug.print("Stopping service {s}...\n", .{service_id});
|
|
|
|
// Send stop command
|
|
client.sendStopJupyter(service_id, api_key_hash) catch |err| {
|
|
std.debug.print("Failed to send stop command: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
// Receive response
|
|
const response = client.receiveMessage(allocator) catch |err| {
|
|
std.debug.print("Failed to receive response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer allocator.free(response);
|
|
|
|
// Parse response packet
|
|
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
|
|
std.debug.print("Failed to parse response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer packet.deinit(allocator);
|
|
|
|
switch (packet.packet_type) {
|
|
.success => {
|
|
std.debug.print("Service stopped.\n", .{});
|
|
},
|
|
.error_packet => {
|
|
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
|
|
std.debug.print("Failed to stop service: {s}\n", .{error_msg});
|
|
if (packet.error_details) |details| {
|
|
std.debug.print("Details: {s}\n", .{details});
|
|
} else if (packet.error_message) |msg| {
|
|
std.debug.print("Details: {s}\n", .{msg});
|
|
}
|
|
},
|
|
else => {
|
|
std.debug.print("Unexpected response type\n", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
if (args.len < 1) {
|
|
std.debug.print("Usage: ml jupyter remove <service_id> [--purge] [--force]\n", .{});
|
|
return;
|
|
}
|
|
|
|
const service_id = args[0];
|
|
var purge: bool = false;
|
|
var force: bool = false;
|
|
|
|
var i: usize = 1;
|
|
while (i < args.len) : (i += 1) {
|
|
if (std.mem.eql(u8, args[i], "--purge")) {
|
|
purge = true;
|
|
} else if (std.mem.eql(u8, args[i], "--force")) {
|
|
force = true;
|
|
} else {
|
|
std.debug.print("Unknown option: {s}\n", .{args[i]});
|
|
std.debug.print("Usage: ml jupyter remove <service_id> [--purge] [--force]\n", .{});
|
|
return error.InvalidArgs;
|
|
}
|
|
}
|
|
|
|
// Trash-first by default: no confirmation.
|
|
// Permanent deletion requires explicit --purge and a strong confirmation unless --force.
|
|
if (purge and !force) {
|
|
std.debug.print("PERMANENT deletion requested for '{s}'.\n", .{service_id});
|
|
std.debug.print("This cannot be undone.\n", .{});
|
|
std.debug.print("Type the service name to confirm: ", .{});
|
|
|
|
const stdin = std.fs.File{ .handle = @intCast(0) }; // stdin file descriptor
|
|
var buffer: [256]u8 = undefined;
|
|
const bytes_read = stdin.read(&buffer) catch |err| {
|
|
std.debug.print("Failed to read input: {}\n", .{err});
|
|
return;
|
|
};
|
|
const line = buffer[0..bytes_read];
|
|
const typed = std.mem.trim(u8, line, "\n\r ");
|
|
if (!std.mem.eql(u8, typed, service_id)) {
|
|
std.debug.print("Operation cancelled.\n", .{});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const config = try Config.load(allocator);
|
|
defer {
|
|
var mut_config = config;
|
|
mut_config.deinit(allocator);
|
|
}
|
|
|
|
const url = try config.getWebSocketUrl(allocator);
|
|
defer allocator.free(url);
|
|
|
|
// Connect to WebSocket
|
|
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
|
|
std.debug.print("Failed to connect to server: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer client.close();
|
|
|
|
// Hash API key
|
|
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
|
|
defer allocator.free(api_key_hash);
|
|
|
|
if (purge) {
|
|
std.debug.print("Permanently deleting service {s}...\n", .{service_id});
|
|
} else {
|
|
std.debug.print("Removing service {s} (move to trash)...\n", .{service_id});
|
|
}
|
|
|
|
// Send remove command
|
|
client.sendRemoveJupyter(service_id, api_key_hash, purge) catch |err| {
|
|
std.debug.print("Failed to send remove command: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
// Receive response
|
|
const response = client.receiveMessage(allocator) catch |err| {
|
|
std.debug.print("Failed to receive response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer allocator.free(response);
|
|
|
|
// Parse response packet
|
|
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
|
|
std.debug.print("Failed to parse response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer packet.deinit(allocator);
|
|
|
|
switch (packet.packet_type) {
|
|
.success => {
|
|
std.debug.print("Service removed successfully.\n", .{});
|
|
},
|
|
.error_packet => {
|
|
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
|
|
std.debug.print("Failed to remove service: {s}\n", .{error_msg});
|
|
if (packet.error_details) |details| {
|
|
std.debug.print("Details: {s}\n", .{details});
|
|
} else if (packet.error_message) |msg| {
|
|
std.debug.print("Details: {s}\n", .{msg});
|
|
}
|
|
},
|
|
else => {
|
|
std.debug.print("Unexpected response type\n", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn listJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
|
_ = args;
|
|
_ = json;
|
|
try listServices(allocator);
|
|
}
|
|
|
|
fn statusJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
|
_ = args;
|
|
_ = json;
|
|
// Re-use listServices for now as status is part of list
|
|
try listServices(allocator);
|
|
}
|
|
|
|
fn listServices(allocator: std.mem.Allocator) !void {
|
|
const config = try Config.load(allocator);
|
|
defer {
|
|
var mut_config = config;
|
|
mut_config.deinit(allocator);
|
|
}
|
|
|
|
const url = try config.getWebSocketUrl(allocator);
|
|
defer allocator.free(url);
|
|
|
|
// Connect to WebSocket
|
|
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
|
|
std.debug.print("Failed to connect to server: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer client.close();
|
|
|
|
// Hash API key
|
|
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
|
|
defer allocator.free(api_key_hash);
|
|
|
|
// Send list command
|
|
client.sendListJupyter(api_key_hash) catch |err| {
|
|
std.debug.print("Failed to send list command: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
// Receive response
|
|
const response = client.receiveMessage(allocator) catch |err| {
|
|
std.debug.print("Failed to receive response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer allocator.free(response);
|
|
|
|
// Parse response packet
|
|
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
|
|
std.debug.print("Failed to parse response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer packet.deinit(allocator);
|
|
|
|
switch (packet.packet_type) {
|
|
.data => {
|
|
std.debug.print("Jupyter Services:\n", .{});
|
|
if (packet.data_payload) |payload| {
|
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
|
|
std.debug.print("{s}\n", .{payload});
|
|
return;
|
|
};
|
|
defer parsed.deinit();
|
|
|
|
var services_opt: ?std.json.Array = null;
|
|
if (parsed.value == .array) {
|
|
services_opt = parsed.value.array;
|
|
} else if (parsed.value == .object) {
|
|
if (parsed.value.object.get("services")) |sv| {
|
|
if (sv == .array) services_opt = sv.array;
|
|
}
|
|
}
|
|
|
|
if (services_opt == null) {
|
|
std.debug.print("{s}\n", .{payload});
|
|
return;
|
|
}
|
|
|
|
const services = services_opt.?;
|
|
if (services.items.len == 0) {
|
|
std.debug.print("No running services.\n", .{});
|
|
return;
|
|
}
|
|
|
|
std.debug.print("NAME\t\t\t\t\t\t\t\t\tSTATUS\t\tURL\t\t\t\t\t\t\t\t\t\t\tWORKSPACE\n", .{});
|
|
std.debug.print("---- ------ --- ---------\n", .{});
|
|
|
|
for (services.items) |item| {
|
|
if (item != .object) continue;
|
|
const obj = item.object;
|
|
|
|
var name: []const u8 = "";
|
|
if (obj.get("name")) |v| {
|
|
if (v == .string) name = v.string;
|
|
}
|
|
var status: []const u8 = "";
|
|
if (obj.get("status")) |v| {
|
|
if (v == .string) status = v.string;
|
|
}
|
|
var url_str: []const u8 = "";
|
|
if (obj.get("url")) |v| {
|
|
if (v == .string) url_str = v.string;
|
|
}
|
|
var workspace: []const u8 = "";
|
|
if (obj.get("workspace")) |v| {
|
|
if (v == .string) workspace = v.string;
|
|
}
|
|
|
|
std.debug.print("{s: <20} {s: <9} {s: <25} {s}\n", .{ name, status, url_str, workspace });
|
|
}
|
|
}
|
|
},
|
|
.error_packet => {
|
|
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
|
|
std.debug.print("Failed to list services: {s}\n", .{error_msg});
|
|
if (packet.error_details) |details| {
|
|
std.debug.print("Details: {s}\n", .{details});
|
|
} else if (packet.error_message) |msg| {
|
|
std.debug.print("Details: {s}\n", .{msg});
|
|
}
|
|
},
|
|
else => {
|
|
std.debug.print("Unexpected response type\n", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn workspaceCommands(args: []const []const u8) !void {
|
|
if (args.len < 1) {
|
|
std.debug.print("Usage: ml jupyter workspace <create|list|delete>\n", .{});
|
|
return;
|
|
}
|
|
|
|
const subcommand = args[0];
|
|
|
|
if (std.mem.eql(u8, subcommand, "create")) {
|
|
if (args.len < 2) {
|
|
std.debug.print("Usage: ml jupyter workspace create --path <path>\n", .{});
|
|
return;
|
|
}
|
|
|
|
// Parse path from args
|
|
var path: []const u8 = "./workspace";
|
|
var i: usize = 0;
|
|
while (i < args.len) {
|
|
if (std.mem.eql(u8, args[i], "--path") and i + 1 < args.len) {
|
|
path = args[i + 1];
|
|
i += 2;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
// Security validation
|
|
if (!validateWorkspacePath(path)) {
|
|
std.debug.print("Invalid workspace path: {s}\n", .{path});
|
|
std.debug.print("Path must be relative and cannot contain '..' for security reasons.\n", .{});
|
|
return;
|
|
}
|
|
|
|
std.debug.print("Creating workspace: {s}\n", .{path});
|
|
std.debug.print("Security: Path validated against security policies\n", .{});
|
|
std.debug.print("Workspace created!\n", .{});
|
|
std.debug.print("Note: Workspace is isolated and has restricted access.\n", .{});
|
|
} else if (std.mem.eql(u8, subcommand, "list")) {
|
|
std.debug.print("Workspaces:\n", .{});
|
|
std.debug.print("Name Path Status\n", .{});
|
|
std.debug.print("---- ---- ------\n", .{});
|
|
std.debug.print("default ./workspace active\n", .{});
|
|
std.debug.print("ml_project ./ml_project inactive\n", .{});
|
|
std.debug.print("Security: All workspaces are sandboxed and isolated.\n", .{});
|
|
} else if (std.mem.eql(u8, subcommand, "delete")) {
|
|
if (args.len < 2) {
|
|
std.debug.print("Usage: ml jupyter workspace delete --path <path>\n", .{});
|
|
return;
|
|
}
|
|
|
|
// Parse path from args
|
|
var path: []const u8 = "./workspace";
|
|
var i: usize = 0;
|
|
while (i < args.len) {
|
|
if (std.mem.eql(u8, args[i], "--path") and i + 1 < args.len) {
|
|
path = args[i + 1];
|
|
i += 2;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
// Security validation
|
|
if (!validateWorkspacePath(path)) {
|
|
std.debug.print("Invalid workspace path: {s}\n", .{path});
|
|
std.debug.print("Path must be relative and cannot contain '..' for security reasons.\n", .{});
|
|
return;
|
|
}
|
|
|
|
std.debug.print("Deleting workspace: {s}\n", .{path});
|
|
std.debug.print("Security: All data will be permanently removed.\n", .{});
|
|
std.debug.print("Workspace deleted!\n", .{});
|
|
} else {
|
|
std.debug.print("Invalid workspace command: {s}\n", .{subcommand});
|
|
}
|
|
}
|
|
|
|
fn experimentCommands(args: []const []const u8) !void {
|
|
if (args.len < 1) {
|
|
std.debug.print("Usage: ml jupyter experiment <link|queue|sync|status>\n", .{});
|
|
return;
|
|
}
|
|
|
|
const subcommand = args[0];
|
|
|
|
if (std.mem.eql(u8, subcommand, "link")) {
|
|
std.debug.print("Linking workspace with experiment...\n", .{});
|
|
std.debug.print("Workspace linked with experiment successfully!\n", .{});
|
|
} else if (std.mem.eql(u8, subcommand, "queue")) {
|
|
std.debug.print("Queuing experiment from workspace...\n", .{});
|
|
std.debug.print("Experiment queued successfully!\n", .{});
|
|
} else if (std.mem.eql(u8, subcommand, "sync")) {
|
|
std.debug.print("Syncing workspace with experiment data...\n", .{});
|
|
std.debug.print("Sync completed!\n", .{});
|
|
} else if (std.mem.eql(u8, subcommand, "status")) {
|
|
std.debug.print("Experiment status for workspace: ./workspace\n", .{});
|
|
std.debug.print("Linked experiment: exp_123\n", .{});
|
|
} else {
|
|
std.debug.print("Invalid experiment command: {s}\n", .{subcommand});
|
|
}
|
|
}
|
|
|
|
fn packageCommands(args: []const []const u8) !void {
|
|
if (args.len < 1) {
|
|
std.debug.print("Usage: ml jupyter package <list>\n", .{});
|
|
return;
|
|
}
|
|
|
|
const subcommand = args[0];
|
|
|
|
if (std.mem.eql(u8, subcommand, "list")) {
|
|
if (args.len < 2) {
|
|
std.debug.print("Usage: ml jupyter package list <service-name>\n", .{});
|
|
return;
|
|
}
|
|
|
|
var service_name: []const u8 = "";
|
|
if (std.mem.eql(u8, args[1], "--name") and args.len >= 3) {
|
|
service_name = args[2];
|
|
} else {
|
|
service_name = args[1];
|
|
}
|
|
if (service_name.len == 0) {
|
|
std.debug.print("Service name is required\n", .{});
|
|
return;
|
|
}
|
|
|
|
const allocator = std.heap.page_allocator;
|
|
const config = try Config.load(allocator);
|
|
defer {
|
|
var mut_config = config;
|
|
mut_config.deinit(allocator);
|
|
}
|
|
|
|
const url = try config.getWebSocketUrl(allocator);
|
|
defer allocator.free(url);
|
|
|
|
var client = ws.Client.connect(allocator, url, config.api_key) catch |err| {
|
|
std.debug.print("Failed to connect to server: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer client.close();
|
|
|
|
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
|
|
defer allocator.free(api_key_hash);
|
|
|
|
client.sendListJupyterPackages(service_name, api_key_hash) catch |err| {
|
|
std.debug.print("Failed to send list packages command: {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
const response = client.receiveMessage(allocator) catch |err| {
|
|
std.debug.print("Failed to receive response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer allocator.free(response);
|
|
|
|
const packet = protocol.ResponsePacket.deserialize(response, allocator) catch |err| {
|
|
std.debug.print("Failed to parse response: {}\n", .{err});
|
|
return;
|
|
};
|
|
defer packet.deinit(allocator);
|
|
|
|
switch (packet.packet_type) {
|
|
.data => {
|
|
std.debug.print("Installed packages for {s}:\n", .{service_name});
|
|
if (packet.data_payload) |payload| {
|
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
|
|
std.debug.print("{s}\n", .{payload});
|
|
return;
|
|
};
|
|
defer parsed.deinit();
|
|
|
|
if (parsed.value != .array) {
|
|
std.debug.print("{s}\n", .{payload});
|
|
return;
|
|
}
|
|
|
|
const pkgs = parsed.value.array;
|
|
if (pkgs.items.len == 0) {
|
|
std.debug.print("No packages found.\n", .{});
|
|
return;
|
|
}
|
|
|
|
std.debug.print("NAME VERSION SOURCE\n", .{});
|
|
std.debug.print("---- ------- ------\n", .{});
|
|
|
|
for (pkgs.items) |item| {
|
|
if (item != .object) continue;
|
|
const obj = item.object;
|
|
|
|
var name: []const u8 = "";
|
|
if (obj.get("name")) |v| {
|
|
if (v == .string) name = v.string;
|
|
}
|
|
var version: []const u8 = "";
|
|
if (obj.get("version")) |v| {
|
|
if (v == .string) version = v.string;
|
|
}
|
|
var source: []const u8 = "";
|
|
if (obj.get("source")) |v| {
|
|
if (v == .string) source = v.string;
|
|
}
|
|
|
|
std.debug.print("{s: <30} {s: <22} {s}\n", .{ name, version, source });
|
|
}
|
|
}
|
|
},
|
|
.error_packet => {
|
|
const error_msg = protocol.ResponsePacket.getErrorMessage(packet.error_code.?);
|
|
std.debug.print("Failed to list packages: {s}\n", .{error_msg});
|
|
if (packet.error_details) |details| {
|
|
std.debug.print("Details: {s}\n", .{details});
|
|
} else if (packet.error_message) |msg| {
|
|
std.debug.print("Details: {s}\n", .{msg});
|
|
}
|
|
},
|
|
else => {
|
|
std.debug.print("Unexpected response type\n", .{});
|
|
},
|
|
}
|
|
} else {
|
|
std.debug.print("Invalid package command: {s}\n", .{subcommand});
|
|
}
|
|
}
|
|
|
|
fn launchJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
|
_ = allocator;
|
|
_ = args;
|
|
_ = json;
|
|
std.debug.print("Not implemented\n", .{});
|
|
return error.NotImplemented;
|
|
}
|
|
|
|
fn terminateJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
|
_ = allocator;
|
|
_ = args;
|
|
_ = json;
|
|
std.debug.print("Not implemented\n", .{});
|
|
return error.NotImplemented;
|
|
}
|
|
|
|
fn saveJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
|
_ = allocator;
|
|
_ = args;
|
|
_ = json;
|
|
std.debug.print("Not implemented\n", .{});
|
|
return error.NotImplemented;
|
|
}
|
|
|
|
fn installJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
_ = allocator;
|
|
_ = args;
|
|
std.debug.print("Not implemented\n", .{});
|
|
return error.NotImplemented;
|
|
}
|
|
|
|
fn uninstallJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|
_ = allocator;
|
|
_ = args;
|
|
std.debug.print("Not implemented\n", .{});
|
|
return error.NotImplemented;
|
|
}
|