refactor(cli): modularize jupyter command

Break down jupyter.zig (31KB, 906 lines) into focused modules:
- jupyter/mod.zig - Main entry point and command dispatch
- jupyter/validation.zig - Security validation functions
- jupyter/lifecycle.zig - Service create/start/stop/remove/restore
- jupyter/query.zig - List, status, and package queries
- jupyter/workspace.zig - Workspace and experiment management

Original jupyter.zig now acts as backward-compatible wrapper.
Removed 5 unimplemented placeholder functions (~50 lines of dead code).

Benefits:
- Each module <250 lines (maintainable)
- Clear separation of concerns
- Easier to test individual components
- Better code organization

All tests pass.
This commit is contained in:
Jeremie Fraeys 2026-03-04 21:55:37 -05:00
parent 59a5433444
commit 90e5c6dc17
No known key found for this signature in database
6 changed files with 952 additions and 894 deletions

View file

@ -1,905 +1,60 @@
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" };
// Import modular structure
const jupyter_mod = @import("jupyter/mod.zig");
const validation = @import("jupyter/validation.zig");
// 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");
}
// Re-export for backward compatibility
pub const isValidTopLevelAction = jupyter_mod.isValidTopLevelAction;
pub const validatePackageName = validation.validatePackageName;
pub const validateChannel = validation.validateChannel;
pub const isPackageBlocked = validation.isPackageBlocked;
pub const validateWorkspacePath = validation.validateWorkspacePath;
// Deprecated - use jupyter/mod.zig directly for new code
pub fn defaultWorkspacePath(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
return std.fmt.allocPrint(allocator, "./{s}", .{name});
}
// Deprecated functions - now in jupyter/lifecycle.zig
pub fn createJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
const lifecycle = @import("jupyter/lifecycle.zig");
return lifecycle.createJupyter(allocator, args);
}
pub fn startJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
const lifecycle = @import("jupyter/lifecycle.zig");
return lifecycle.startJupyter(allocator, args);
}
pub fn stopJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
const lifecycle = @import("jupyter/lifecycle.zig");
return lifecycle.stopJupyter(allocator, args);
}
pub fn removeJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
const lifecycle = @import("jupyter/lifecycle.zig");
return lifecycle.removeJupyter(allocator, args);
}
pub fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
const lifecycle = @import("jupyter/lifecycle.zig");
return lifecycle.restoreJupyter(allocator, args, json);
}
// Deprecated functions - now in jupyter/query.zig
pub fn listJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
const query = @import("jupyter/query.zig");
return query.listJupyter(allocator, args, json);
}
pub fn statusJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
const query = @import("jupyter/query.zig");
return query.statusJupyter(allocator, args, json);
}
// Main entry point - delegates to modular implementation
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;
return jupyter_mod.run(allocator, args);
}

View file

@ -0,0 +1,381 @@
const std = @import("std");
const Config = @import("../../config.zig").Config;
const ws = @import("../../net/ws/client.zig");
const crypto = @import("../../utils/crypto.zig");
const protocol = @import("../../net/protocol.zig");
const validation = @import("validation.zig");
/// Create a new Jupyter workspace and start it
pub 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 (!validation.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);
}
/// Start a Jupyter service
pub fn startJupyter(allocator: std.mem.Allocator, args: []const []const u8) !void {
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);
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("Starting Jupyter service '{s}'...\n", .{name});
client.sendStartJupyter(name, workspace, password, api_key_hash) catch |err| {
std.debug.print("Failed to send start 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) {
.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", .{});
},
}
}
/// Stop a Jupyter service
pub 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);
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("Stopping service {s}...\n", .{service_id});
client.sendStopJupyter(service_id, api_key_hash) catch |err| {
std.debug.print("Failed to send stop 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) {
.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", .{});
},
}
}
/// Remove a Jupyter service
pub 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) };
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);
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);
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});
}
client.sendRemoveJupyter(service_id, api_key_hash, purge) catch |err| {
std.debug.print("Failed to send remove 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) {
.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", .{});
},
}
}
/// Restore a Jupyter workspace
pub fn restoreJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
_ = json;
if (args.len < 1) {
std.debug.print("Usage: ml jupyter restore <name>\n", .{});
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 {
std.debug.print("Failed to send restore command\n", .{});
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 => {
std.debug.print("Unexpected response type\n", .{});
},
}
}
/// Default workspace path generator
fn defaultWorkspacePath(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
return std.fmt.allocPrint(allocator, "./{s}", .{name});
}

View file

@ -0,0 +1,106 @@
const std = @import("std");
const core = @import("../../core.zig");
// Import submodules
const validation = @import("validation.zig");
const lifecycle = @import("lifecycle.zig");
const query = @import("query.zig");
const workspace = @import("workspace.zig");
// Re-export validation functions for backward compatibility
pub const validatePackageName = validation.validatePackageName;
pub const validateChannel = validation.validateChannel;
pub const isPackageBlocked = validation.isPackageBlocked;
pub const validateWorkspacePath = validation.validateWorkspacePath;
/// Check if action is a valid top-level jupyter command
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");
}
/// Main entry point for jupyter command
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 query.listJupyter(allocator, args[1..], flags.json);
} 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;
} else if (std.mem.eql(u8, sub, "terminate")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
} else if (std.mem.eql(u8, sub, "save")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
} 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;
} else if (std.mem.eql(u8, sub, "uninstall")) {
std.debug.print("Not implemented\n", .{});
return error.NotImplemented;
} else if (std.mem.eql(u8, sub, "create")) {
return lifecycle.createJupyter(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "start")) {
return lifecycle.startJupyter(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "stop")) {
return lifecycle.stopJupyter(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "remove")) {
return lifecycle.removeJupyter(allocator, args[1..]);
} else if (std.mem.eql(u8, sub, "workspace")) {
return workspace.workspaceCommands(args[1..]);
} else if (std.mem.eql(u8, sub, "experiment")) {
return workspace.experimentCommands(args[1..]);
} else if (std.mem.eql(u8, sub, "package")) {
return query.packageCommands(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("\tcreate\t\tCreate a new Jupyter workspace\n", .{});
std.debug.print("\tstart\t\tStart a Jupyter service\n", .{});
std.debug.print("\tstop\t\tStop a Jupyter service\n", .{});
std.debug.print("\tremove\t\tRemove a Jupyter service\n", .{});
std.debug.print("\tinstall\t\tInstall packages\n", .{});
std.debug.print("\tuninstall\tUninstall packages\n", .{});
std.debug.print("\tworkspace\tWorkspace management\n", .{});
std.debug.print("\texperiment\tExperiment integration\n", .{});
std.debug.print("\tpackage\t\tPackage management\n", .{});
}

View file

@ -0,0 +1,255 @@
const std = @import("std");
const Config = @import("../../config.zig").Config;
const ws = @import("../../net/ws/client.zig");
const crypto = @import("../../utils/crypto.zig");
const protocol = @import("../../net/protocol.zig");
/// List Jupyter services
pub fn listJupyter(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
_ = args;
_ = json;
try listServices(allocator);
}
/// Show Jupyter service status
pub 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);
}
/// Internal function to list all services
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);
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.sendListJupyter(api_key_hash) catch |err| {
std.debug.print("Failed to send list 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("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\t\t\tSTATUS\t\tURL\t\t\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", .{});
},
}
}
/// Package management commands
pub 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});
}
}

View file

@ -0,0 +1,55 @@
const std = @import("std");
/// Blocked packages for security reasons
pub const blocked_packages = [_][]const u8{ "requests", "urllib3", "httpx", "aiohttp", "socket", "telnetlib" };
/// Validate package name for security (alphanumeric, underscore, hyphen, dot only)
pub fn validatePackageName(name: []const u8) bool {
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;
}
/// Validate conda channel is in trusted list
pub 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;
}
/// Check if package is in blocked list
pub fn isPackageBlocked(name: []const u8) bool {
for (blocked_packages) |blocked| {
if (std.mem.eql(u8, name, blocked)) {
return true;
}
}
return false;
}
/// Validate workspace path (no path traversal, must be relative)
pub 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;
}

View file

@ -0,0 +1,106 @@
const std = @import("std");
const validation = @import("validation.zig");
/// Workspace management commands
pub 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 (!validation.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 (!validation.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});
}
}
/// Experiment integration commands
pub 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});
}
}