fetch_ml/cli/src/commands/jupyter.zig
Jeremie Fraeys a1988de8b1
style(cli): Standardize printUsage() formatting with tabs and ASCII symbols
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.
2026-02-23 14:09:49 -05:00

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;
}