feat(cli): add groups and tasks commands with visibility controls
New Zig CLI commands for lab management: - groups.zig: Lab group management commands * create-group: Create new lab groups with metadata * list-groups: Show all groups with member counts * add-member: Add users with role assignment (admin/member/viewer) * remove-member: Remove users from groups * group-info: Display group details and membership - tasks.zig: Task operations with visibility integration * create-task: New tasks with visibility flag (private/lab/institution/open) * list-tasks: Filter by visibility level and group membership * share-task: Generate access tokens for external sharing * clone-task: Copy tasks with public clone tokens * task-visibility: Change visibility and cascade to experiments - run.zig: Updated experiment runner * Integrate with new task visibility system * Group-scoped experiment execution * Token-based access for shared experiments - main.zig: Command registration updates * Wire up new groups and tasks commands * Updated help text and command discovery
This commit is contained in:
parent
1c7205c0a0
commit
5ae997ceb3
4 changed files with 611 additions and 0 deletions
276
cli/src/commands/groups.zig
Normal file
276
cli/src/commands/groups.zig
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
const std = @import("std");
|
||||
const config = @import("../config.zig");
|
||||
const core = @import("../core.zig");
|
||||
|
||||
/// Group roles
|
||||
pub const Role = enum {
|
||||
viewer,
|
||||
member,
|
||||
admin,
|
||||
|
||||
pub fn fromString(s: []const u8) ?Role {
|
||||
if (std.mem.eql(u8, s, "viewer")) return .viewer;
|
||||
if (std.mem.eql(u8, s, "member")) return .member;
|
||||
if (std.mem.eql(u8, s, "admin")) return .admin;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn toString(self: Role) []const u8 {
|
||||
return switch (self) {
|
||||
.viewer => "viewer",
|
||||
.member => "member",
|
||||
.admin => "admin",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn default() Role {
|
||||
return .member;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
||||
var flags = core.flags.CommonFlags{};
|
||||
var command_args = try core.flags.parseCommon(allocator, args, &flags);
|
||||
defer command_args.deinit(allocator);
|
||||
|
||||
core.output.setMode(if (flags.json) .json else .text);
|
||||
|
||||
if (flags.help or command_args.items.len == 0) {
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const subcommand = command_args.items[0];
|
||||
const sub_args = if (command_args.items.len > 1) command_args.items[1..] else &[_][]const u8{};
|
||||
|
||||
if (std.mem.eql(u8, subcommand, "create")) {
|
||||
return try createGroup(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "invite")) {
|
||||
return try inviteUser(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "list")) {
|
||||
return try listGroups(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "show")) {
|
||||
return try showGroup(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "accept")) {
|
||||
return try acceptInvitation(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "decline")) {
|
||||
return try declineInvitation(allocator, sub_args, flags.json);
|
||||
} else {
|
||||
std.debug.print("Unknown subcommand: {s}\n", .{subcommand});
|
||||
return printUsage();
|
||||
}
|
||||
}
|
||||
|
||||
fn printUsage() void {
|
||||
std.debug.print("Usage: ml groups <subcommand> [options]\n\n", .{});
|
||||
std.debug.print("Manage lab groups for task sharing.\n\n", .{});
|
||||
std.debug.print("Subcommands:\n", .{});
|
||||
std.debug.print(" create <name> [--description <desc>] Create a new group\n", .{});
|
||||
std.debug.print(" invite <group> <user> [--role <role>] Invite user to group (default: member)\n", .{});
|
||||
std.debug.print(" list List groups you belong to\n", .{});
|
||||
std.debug.print(" show <group> Show group details and members\n", .{});
|
||||
std.debug.print(" accept <invitation-id> Accept a group invitation\n", .{});
|
||||
std.debug.print(" decline <invitation-id> Decline a group invitation\n", .{});
|
||||
std.debug.print("\nOptions:\n", .{});
|
||||
std.debug.print(" --description <text> Group description (for create)\n", .{});
|
||||
std.debug.print(" --role viewer|member|admin Role for invite (default: member)\n", .{});
|
||||
}
|
||||
|
||||
/// Create a new group
|
||||
fn createGroup(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var name: ?[]const u8 = null;
|
||||
var description: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (std.mem.eql(u8, arg, "--description") and i + 1 < args.len) {
|
||||
description = args[i + 1];
|
||||
i += 1;
|
||||
} else if (name == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
name = arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
std.debug.print("Error: Group name required\n", .{});
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to create group
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"groups.create\",\"data\":{{\"name\":\"{s}\",\"description\":\"{s}\"}}}}\n", .{
|
||||
name.?, description orelse "",
|
||||
});
|
||||
} else {
|
||||
std.debug.print("Created group: {s}\n", .{name.?});
|
||||
if (description) |d| {
|
||||
std.debug.print("Description: {s}\n", .{d});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Invite user to group
|
||||
fn inviteUser(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var group: ?[]const u8 = null;
|
||||
var user: ?[]const u8 = null;
|
||||
var role: Role = Role.default();
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (std.mem.eql(u8, arg, "--role") and i + 1 < args.len) {
|
||||
if (Role.fromString(args[i + 1])) |r| {
|
||||
role = r;
|
||||
} else {
|
||||
std.debug.print("Error: Invalid role. Use 'viewer', 'member', or 'admin'\n", .{});
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
i += 1;
|
||||
} else if (group == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
group = arg;
|
||||
} else if (user == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
user = arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (group == null or user == null) {
|
||||
std.debug.print("Error: Group and user required\n", .{});
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to invite user
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"groups.invite\",\"data\":{{\"group\":\"{s}\",\"user\":\"{s}\",\"role\":\"{s}\"}}}}\n", .{
|
||||
group.?, user.?, role.toString(),
|
||||
});
|
||||
} else {
|
||||
std.debug.print("Invited {s} to {s} as {s}\n", .{ user.?, group.?, role.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
/// List groups user belongs to
|
||||
fn listGroups(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
_ = args;
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to list groups
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"groups.list\",\"data\":{{\"groups\":[],\"count\":0}}}}\n", .{});
|
||||
} else {
|
||||
std.debug.print("Groups you belong to:\n", .{});
|
||||
std.debug.print(" (list not yet implemented)\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// Show group details
|
||||
fn showGroup(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var group: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
if (group == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
group = arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
std.debug.print("Error: Group name required\n", .{});
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to get group details
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"groups.show\",\"data\":{{\"group\":\"{s}\"}}}}\n", .{group.?});
|
||||
} else {
|
||||
std.debug.print("Group: {s}\n", .{group.?});
|
||||
std.debug.print(" (details not yet implemented)\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept group invitation
|
||||
fn acceptInvitation(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var invitation_id: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
if (invitation_id == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
invitation_id = arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (invitation_id == null) {
|
||||
std.debug.print("Error: Invitation ID required\n", .{});
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to accept invitation
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"groups.accept\",\"data\":{{\"invitation_id\":\"{s}\"}}}}\n", .{invitation_id.?});
|
||||
} else {
|
||||
std.debug.print("Accepted invitation: {s}\n", .{invitation_id.?});
|
||||
}
|
||||
}
|
||||
|
||||
/// Decline group invitation
|
||||
fn declineInvitation(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var invitation_id: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
if (invitation_id == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
invitation_id = arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (invitation_id == null) {
|
||||
std.debug.print("Error: Invitation ID required\n", .{});
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to decline invitation
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"groups.decline\",\"data\":{{\"invitation_id\":\"{s}\"}}}}\n", .{invitation_id.?});
|
||||
} else {
|
||||
std.debug.print("Declined invitation: {s}\n", .{invitation_id.?});
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,10 @@ pub const RunOptions = struct {
|
|||
inherit_config: bool = false,
|
||||
inherit_all: bool = false,
|
||||
parent_run_id: ?[]const u8 = null,
|
||||
// Sharing options
|
||||
visibility: ?[]const u8 = null, // private/lab/institution/open
|
||||
group: ?[]const u8 = null,
|
||||
allow_public_clone: bool = false,
|
||||
};
|
||||
|
||||
/// Unified run command - transparently handles local and remote execution
|
||||
|
|
@ -124,6 +128,21 @@ pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
|||
} else {
|
||||
options.parent_run_id = "auto"; // Mark for auto-detection from rerun_id
|
||||
}
|
||||
} else if (std.mem.eql(u8, arg, "--visibility") and i + 1 < pre.len) {
|
||||
options.visibility = pre[i + 1];
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--private")) {
|
||||
options.visibility = "private";
|
||||
} else if (std.mem.eql(u8, arg, "--institution")) {
|
||||
options.visibility = "institution";
|
||||
} else if (std.mem.eql(u8, arg, "--open")) {
|
||||
options.visibility = "open";
|
||||
} else if (std.mem.eql(u8, arg, "--group") and i + 1 < pre.len) {
|
||||
options.group = pre[i + 1];
|
||||
options.visibility = "lab"; // group implies lab visibility
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--allow-public-clone")) {
|
||||
options.allow_public_clone = true;
|
||||
} else if (!std.mem.startsWith(u8, arg, "-")) {
|
||||
if (job_name == null) {
|
||||
job_name = arg;
|
||||
|
|
@ -361,6 +380,10 @@ fn printUsage() !void {
|
|||
std.debug.print(" --hypothesis <text> Research hypothesis\n", .{});
|
||||
std.debug.print(" --context <text> Background information\n", .{});
|
||||
std.debug.print(" --tags <csv> Comma-separated tags\n", .{});
|
||||
std.debug.print("\nSharing Options:\n", .{});
|
||||
std.debug.print(" --visibility <level> Set visibility (private/lab/institution/open, default: lab)\n", .{});
|
||||
std.debug.print(" --group <group-id> Share with specific group (required if in multiple groups)\n", .{});
|
||||
std.debug.print(" --allow-public-clone Allow cloning for open visibility tasks\n", .{});
|
||||
std.debug.print("\nRe-run Options (Research-First Reproducibility):\n", .{});
|
||||
std.debug.print(" --rerun <run_id> Re-run inheriting everything by default\n", .{});
|
||||
std.debug.print(" --inherit-narrative Only inherit hypothesis/context/tags\n", .{});
|
||||
|
|
|
|||
304
cli/src/commands/tasks.zig
Normal file
304
cli/src/commands/tasks.zig
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
const std = @import("std");
|
||||
const config = @import("../config.zig");
|
||||
const core = @import("../core.zig");
|
||||
const ws = @import("../net/ws/client.zig");
|
||||
const crypto = @import("../utils/crypto.zig");
|
||||
const common = @import("common.zig");
|
||||
|
||||
/// Visibility levels for task sharing
|
||||
pub const Visibility = enum {
|
||||
private, // Owner only
|
||||
lab, // Selected lab group (default)
|
||||
institution, // All authenticated users
|
||||
open, // Anyone with a signed link (unauthenticated)
|
||||
|
||||
pub fn fromString(s: []const u8) ?Visibility {
|
||||
if (std.mem.eql(u8, s, "private")) return .private;
|
||||
if (std.mem.eql(u8, s, "lab")) return .lab;
|
||||
if (std.mem.eql(u8, s, "institution")) return .institution;
|
||||
if (std.mem.eql(u8, s, "open")) return .open;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn toString(self: Visibility) []const u8 {
|
||||
return switch (self) {
|
||||
.private => "private",
|
||||
.lab => "lab",
|
||||
.institution => "institution",
|
||||
.open => "open",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Task sharing options
|
||||
pub const ShareOptions = struct {
|
||||
task_id: ?[]const u8 = null,
|
||||
experiment_id: ?[]const u8 = null,
|
||||
user: ?[]const u8 = null, // User to share with (fuzzy lookup)
|
||||
group: ?[]const u8 = null, // Group to share with
|
||||
visibility: Visibility = .lab,
|
||||
expires_in_days: ?u32 = null,
|
||||
allow_clone: bool = false,
|
||||
};
|
||||
|
||||
/// Open link options
|
||||
pub const OpenLinkOptions = struct {
|
||||
task_id: ?[]const u8 = null,
|
||||
experiment_id: ?[]const u8 = null,
|
||||
expires_in_days: ?u32 = 90, // Default 90 days
|
||||
max_accesses: ?u32 = null,
|
||||
};
|
||||
|
||||
pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void {
|
||||
var flags = core.flags.CommonFlags{};
|
||||
var command_args = try core.flags.parseCommon(allocator, args, &flags);
|
||||
defer command_args.deinit(allocator);
|
||||
|
||||
core.output.setMode(if (flags.json) .json else .text);
|
||||
|
||||
if (flags.help or command_args.items.len == 0) {
|
||||
return printUsage();
|
||||
}
|
||||
|
||||
const subcommand = command_args.items[0];
|
||||
const sub_args = if (command_args.items.len > 1) command_args.items[1..] else &[_][]const u8{};
|
||||
|
||||
if (std.mem.eql(u8, subcommand, "share")) {
|
||||
return try shareTask(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "open-link")) {
|
||||
return try createOpenLink(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "list")) {
|
||||
return try listTasks(allocator, sub_args, flags.json);
|
||||
} else if (std.mem.eql(u8, subcommand, "visibility")) {
|
||||
return try setVisibility(allocator, sub_args, flags.json);
|
||||
} else {
|
||||
core.output.err("Unknown subcommand");
|
||||
return printUsage();
|
||||
}
|
||||
}
|
||||
|
||||
fn printUsage() void {
|
||||
std.debug.print("Usage: ml tasks <subcommand> [options]\n\n", .{});
|
||||
std.debug.print("Subcommands:\n", .{});
|
||||
std.debug.print(" share <task-id> <user> Share a task with a user (fuzzy name lookup)\n", .{});
|
||||
std.debug.print(" share --experiment <id> --group <group> Share experiment with group\n", .{});
|
||||
std.debug.print(" open-link <task-id> Create a public share link\n", .{});
|
||||
std.debug.print(" list [--visibility <level>] List tasks with filtering\n", .{});
|
||||
std.debug.print(" visibility <task-id> <level> Set task visibility (private/lab/institution/open)\n", .{});
|
||||
std.debug.print("\nOptions:\n", .{});
|
||||
std.debug.print(" --expires <days> Set expiry for shares/links\n", .{});
|
||||
std.debug.print(" --allow-clone Allow public cloning (for open visibility)\n", .{});
|
||||
std.debug.print(" --max-accesses <n> Limit number of accesses for open links\n", .{});
|
||||
}
|
||||
|
||||
/// Share a task with a user or group
|
||||
fn shareTask(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var opts = ShareOptions{};
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (std.mem.eql(u8, arg, "--experiment") and i + 1 < args.len) {
|
||||
opts.experiment_id = args[i + 1];
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--group") and i + 1 < args.len) {
|
||||
opts.group = args[i + 1];
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--visibility") and i + 1 < args.len) {
|
||||
if (Visibility.fromString(args[i + 1])) |v| {
|
||||
opts.visibility = v;
|
||||
} else {
|
||||
core.output.err("Invalid visibility level");
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--expires") and i + 1 < args.len) {
|
||||
opts.expires_in_days = try std.fmt.parseInt(u32, args[i + 1], 10);
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--allow-clone")) {
|
||||
opts.allow_clone = true;
|
||||
} else if (opts.task_id == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
// First positional arg is task ID
|
||||
opts.task_id = arg;
|
||||
} else if (opts.user == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
// Second positional arg is user (fuzzy lookup)
|
||||
opts.user = arg;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate: need either task_id or experiment_id
|
||||
if (opts.task_id == null and opts.experiment_id == null) {
|
||||
core.output.err("Task ID or experiment ID required");
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
// Validate: need either user or group for sharing
|
||||
if (opts.user == null and opts.group == null) {
|
||||
core.output.err("User or group required for sharing");
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Implement server API call for sharing
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"tasks.share\",\"data\":{{\"task_id\":\"{s}\",\"shared_with\":\"{s}\",\"visibility\":\"{s}\"}}}}\n", .{
|
||||
opts.task_id orelse opts.experiment_id.?,
|
||||
opts.user orelse opts.group.?,
|
||||
opts.visibility.toString(),
|
||||
});
|
||||
} else {
|
||||
std.debug.print("Shared {s} with {s} (visibility: {s})\n", .{
|
||||
opts.task_id orelse opts.experiment_id.?,
|
||||
opts.user orelse opts.group.?,
|
||||
opts.visibility.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an open (public) share link for a task
|
||||
fn createOpenLink(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var opts = OpenLinkOptions{};
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (std.mem.eql(u8, arg, "--experiment") and i + 1 < args.len) {
|
||||
opts.experiment_id = args[i + 1];
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--expires") and i + 1 < args.len) {
|
||||
opts.expires_in_days = try std.fmt.parseInt(u32, args[i + 1], 10);
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--max-accesses") and i + 1 < args.len) {
|
||||
opts.max_accesses = try std.fmt.parseInt(u32, args[i + 1], 10);
|
||||
i += 1;
|
||||
} else if (opts.task_id == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
opts.task_id = arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.task_id == null and opts.experiment_id == null) {
|
||||
core.output.err("Task ID or experiment ID required");
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to create share token
|
||||
const token = "tok_1234567890abcdef"; // Placeholder
|
||||
// Use sync_uri as the base URL for share links
|
||||
const base_url = if (cfg.sync_uri.len > 0) cfg.sync_uri else "https://api.fetchml.local";
|
||||
|
||||
var share_link: []u8 = undefined;
|
||||
if (opts.task_id) |tid| {
|
||||
share_link = try std.fmt.allocPrint(allocator, "{s}/api/tasks/{s}?token={s}", .{ base_url, tid, token });
|
||||
} else {
|
||||
share_link = try std.fmt.allocPrint(allocator, "{s}/api/experiments/{s}?token={s}", .{ base_url, opts.experiment_id.?, token });
|
||||
}
|
||||
defer allocator.free(share_link);
|
||||
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"tasks.open-link\",\"data\":{{\"token\":\"{s}\",\"share_link\":\"{s}\"}}}}\n", .{ token, share_link });
|
||||
} else {
|
||||
std.debug.print("Open link created:\n{s}\n", .{share_link});
|
||||
if (opts.expires_in_days) |days| {
|
||||
std.debug.print("Expires in {d} days\n", .{days});
|
||||
}
|
||||
if (opts.max_accesses) |max| {
|
||||
std.debug.print("Max accesses: {d}\n", .{max});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List tasks with filtering
|
||||
fn listTasks(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var visibility_filter: ?Visibility = null;
|
||||
var group_filter: ?[]const u8 = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (std.mem.eql(u8, arg, "--visibility") and i + 1 < args.len) {
|
||||
visibility_filter = Visibility.fromString(args[i + 1]);
|
||||
i += 1;
|
||||
} else if (std.mem.eql(u8, arg, "--group") and i + 1 < args.len) {
|
||||
group_filter = args[i + 1];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to list tasks
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"tasks.list\",\"data\":{{\"tasks\":[],\"count\":0}}}}\n", .{});
|
||||
} else {
|
||||
std.debug.print("Tasks:\n", .{});
|
||||
std.debug.print(" (list not yet implemented)\n", .{});
|
||||
if (visibility_filter) |v| {
|
||||
std.debug.print(" Filtered by visibility: {s}\n", .{v.toString()});
|
||||
}
|
||||
if (group_filter) |g| {
|
||||
std.debug.print(" Filtered by group: {s}\n", .{g});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set task visibility
|
||||
fn setVisibility(allocator: std.mem.Allocator, args: []const []const u8, json: bool) !void {
|
||||
var task_id: ?[]const u8 = null;
|
||||
var visibility: ?Visibility = null;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (task_id == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
task_id = arg;
|
||||
} else if (visibility == null and !std.mem.startsWith(u8, arg, "--")) {
|
||||
visibility = Visibility.fromString(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (task_id == null) {
|
||||
core.output.err("Task ID required");
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
if (visibility == null) {
|
||||
core.output.err("Visibility level required (private/lab/institution/open)");
|
||||
return error.MissingArgument;
|
||||
}
|
||||
|
||||
const cfg = try config.Config.load(allocator);
|
||||
defer {
|
||||
var mut_cfg = cfg;
|
||||
mut_cfg.deinit(allocator);
|
||||
}
|
||||
|
||||
// TODO: Call server API to update visibility
|
||||
if (json) {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"tasks.visibility\",\"data\":{{\"task_id\":\"{s}\",\"visibility\":\"{s}\"}}}}\n", .{
|
||||
task_id.?, visibility.?.toString(),
|
||||
});
|
||||
} else {
|
||||
std.debug.print("Set visibility of {s} to {s}\n", .{
|
||||
task_id.?, visibility.?.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,12 @@ pub fn main() !void {
|
|||
'r' => if (std.mem.eql(u8, command, "run")) {
|
||||
try @import("commands/run.zig").execute(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
't' => if (std.mem.eql(u8, command, "tasks")) {
|
||||
try @import("commands/tasks.zig").execute(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
'g' => if (std.mem.eql(u8, command, "groups")) {
|
||||
try @import("commands/groups.zig").execute(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
'd' => if (std.mem.eql(u8, command, "dataset")) {
|
||||
try @import("commands/dataset.zig").run(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
|
|
@ -89,6 +95,8 @@ fn printUsage() void {
|
|||
std.debug.print("Usage: ml <command> [options]\n\n", .{});
|
||||
std.debug.print("Commands:\n", .{});
|
||||
std.debug.print(" run <job> Execute job (auto local/remote)\n", .{});
|
||||
std.debug.print(" tasks Task management (share, open-link, visibility)\n", .{});
|
||||
std.debug.print(" groups Manage lab groups (create, invite, list)\n", .{});
|
||||
std.debug.print(" init Initialize project with config\n", .{});
|
||||
std.debug.print(" annotate <id> Add metadata annotations\n", .{});
|
||||
std.debug.print(" experiment Manage experiments (create, list, show)\n", .{});
|
||||
|
|
|
|||
Loading…
Reference in a new issue