diff --git a/.forgejo/workflows/build-cli.yml b/.forgejo/workflows/build-cli.yml new file mode 100644 index 0000000..31ea11c --- /dev/null +++ b/.forgejo/workflows/build-cli.yml @@ -0,0 +1,80 @@ +name: Build CLI with Embedded SQLite + +on: + push: + branches: [main, master] + paths: + - 'cli/**' + - '.forgejo/workflows/build-cli.yml' + pull_request: + branches: [main, master] + paths: + - 'cli/**' + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - x86_64-linux + - aarch64-linux + include: + - target: x86_64-linux + arch: x86_64 + - target: aarch64-linux + arch: arm64 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.15.0 + + - name: Fetch SQLite Amalgamation + run: | + cd cli + make build-sqlite SQLITE_VERSION=3450000 + + - name: Build Release Binary + run: | + cd cli + zig build prod -Dtarget=${{ matrix.target }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ml-cli-${{ matrix.target }} + path: cli/zig-out/bin/ml + + build-macos: + runs-on: macos-latest + strategy: + matrix: + arch: [x86_64, arm64] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.15.0 + + - name: Fetch SQLite Amalgamation + run: | + cd cli + make build-sqlite SQLITE_VERSION=3450000 + + - name: Build Release Binary + run: | + cd cli + zig build prod -Dtarget=${{ matrix.arch }}-macos + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ml-cli-${{ matrix.arch }}-macos + path: cli/zig-out/bin/ml diff --git a/cli/scripts/build_sqlite.sh b/cli/scripts/build_sqlite.sh new file mode 100644 index 0000000..2c8733a --- /dev/null +++ b/cli/scripts/build_sqlite.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Build/fetch SQLite amalgamation for embedding +# Mirrors the rsync pattern: assets/sqlite_release__/ + +set -euo pipefail + +SQLITE_VERSION="${SQLITE_VERSION:-3450000}" # 3.45.0 +SQLITE_YEAR="${SQLITE_YEAR:-2024}" +SQLITE_SRC_BASE="${SQLITE_SRC_BASE:-https://www.sqlite.org/${SQLITE_YEAR}}" + +os="$(uname -s | tr '[:upper:]' '[:lower:]')" +arch="$(uname -m)" +if [[ "${arch}" == "aarch64" || "${arch}" == "arm64" ]]; then arch="arm64"; fi +if [[ "${arch}" == "x86_64" ]]; then arch="x86_64"; fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +out_dir="${repo_root}/src/assets/sqlite_release_${os}_${arch}" + +echo "Building SQLite ${SQLITE_VERSION} for ${os}/${arch}..." + +# Create output directory +mkdir -p "${out_dir}" + +# Download if not present +if [[ ! -f "${out_dir}/sqlite3.c" ]]; then + echo "Fetching SQLite amalgamation..." + tmp="$(mktemp -d)" + cleanup() { rm -rf "${tmp}"; } + trap cleanup EXIT + + url="${SQLITE_SRC_BASE}/sqlite-amalgamation-${SQLITE_VERSION}.zip" + echo "Fetching ${url}" + curl -fsSL "${url}" -o "${tmp}/sqlite.zip" + + unzip -q "${tmp}/sqlite.zip" -d "${tmp}" + mv "${tmp}/sqlite-amalgamation-${SQLITE_VERSION}"/* "${out_dir}/" + + echo "✓ SQLite fetched to ${out_dir}" +else + echo "✓ SQLite already present at ${out_dir}" +fi + +# Verify +if [[ -f "${out_dir}/sqlite3.c" && -f "${out_dir}/sqlite3.h" ]]; then + echo "✓ SQLite ready:" + ls -lh "${out_dir}/sqlite3.c" "${out_dir}/sqlite3.h" +else + echo "Error: SQLite files not found in ${out_dir}" + exit 1 +fi diff --git a/cli/src/commands/run.zig b/cli/src/commands/run.zig new file mode 100644 index 0000000..c48afa0 --- /dev/null +++ b/cli/src/commands/run.zig @@ -0,0 +1,325 @@ +const std = @import("std"); +const config = @import("../config.zig"); +const db = @import("../db.zig"); +const colors = @import("../utils/colors.zig"); + +const RunOptions = struct { + json: bool = false, + help: bool = false, +}; + +pub fn execute(allocator: std.mem.Allocator, args: []const []const u8) !void { + var options = RunOptions{}; + var command_args = std.ArrayList([]const u8).initCapacity(allocator, 10) catch |err| { + return err; + }; + defer command_args.deinit(allocator); + + // Parse flags + var i: usize = 0; + while (i < args.len) : (i += 1) { + const arg = args[i]; + if (std.mem.eql(u8, arg, "--json")) { + options.json = true; + } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + options.help = true; + } else { + try command_args.append(allocator, arg); + } + } + + if (command_args.items.len < 1 or options.help) { + try printUsage(); + return; + } + + const command = command_args.items[0]; + + // Load config to determine mode + const cfg = try config.Config.load(allocator); + defer { + var mut_cfg = cfg; + mut_cfg.deinit(allocator); + } + + if (std.mem.eql(u8, command, "start")) { + if (cfg.isLocalMode()) { + try executeStartLocal(allocator, command_args.items[1..], &options); + } else { + try executeStartServer(allocator, command_args.items[1..], &options); + } + } else if (std.mem.eql(u8, command, "finish")) { + if (cfg.isLocalMode()) { + try executeFinishLocal(allocator, command_args.items[1..], &options); + } else { + try executeFinishServer(allocator, command_args.items[1..], &options); + } + } else if (std.mem.eql(u8, command, "fail")) { + if (cfg.isLocalMode()) { + try executeFailLocal(allocator, command_args.items[1..], &options); + } else { + try executeFailServer(allocator, command_args.items[1..], &options); + } + } else if (std.mem.eql(u8, command, "list")) { + if (cfg.isLocalMode()) { + try executeListLocal(allocator, &options); + } else { + try executeListServer(allocator, &options); + } + } else { + if (options.json) { + std.debug.print("{{\"success\":false,\"command\":\"run\",\"error\":\"Unknown command: {s}\"}}\n", .{command}); + } else { + colors.printError("Unknown command: {s}\n", .{command}); + try printUsage(); + } + } +} + +// Local mode implementations +fn executeStartLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const RunOptions) !void { + var experiment_id: ?[]const u8 = null; + var run_name: ?[]const u8 = null; + + var i: usize = 0; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--experiment") and i + 1 < args.len) { + experiment_id = args[i + 1]; + i += 1; + } else if (std.mem.eql(u8, args[i], "--name") and i + 1 < args.len) { + run_name = args[i + 1]; + i += 1; + } + } + + if (experiment_id == null) { + if (options.json) { + std.debug.print("{{\"success\":false,\"command\":\"run.start\",\"error\":\"--experiment is required\"}}\n", .{}); + } else { + colors.printError("Error: --experiment is required\n", .{}); + } + return error.MissingArgument; + } + + const cfg = try config.Config.load(allocator); + defer { + var mut_cfg = cfg; + mut_cfg.deinit(allocator); + } + + const db_path = try cfg.getDBPath(allocator); + defer allocator.free(db_path); + + var database = try db.DB.init(allocator, db_path); + defer database.close(); + + const run_id = try db.generateUUID(allocator); + defer allocator.free(run_id); + + const timestamp = try db.currentTimestamp(allocator); + defer allocator.free(timestamp); + + const sql = "INSERT INTO ml_runs (run_id, experiment_id, name, status, start_time) VALUES (?, ?, ?, 'RUNNING', ?);"; + const stmt = try database.prepare(sql); + defer db.DB.finalize(stmt); + + try db.DB.bindText(stmt, 1, run_id); + try db.DB.bindText(stmt, 2, experiment_id.?); + try db.DB.bindText(stmt, 3, run_name orelse "unnamed-run"); + try db.DB.bindText(stmt, 4, timestamp); + + _ = try db.DB.step(stmt); + + if (options.json) { + std.debug.print("{{\"success\":true,\"command\":\"run.start\",\"data\":{{\"run_id\":\"{s}\",\"experiment_id\":\"{s}\",\"name\":\"{s}\",\"status\":\"RUNNING\"}}}}\n", .{ run_id, experiment_id.?, run_name orelse "unnamed-run" }); + } else { + colors.printSuccess("✓ Started run: {s}\n", .{run_name orelse "unnamed-run"}); + colors.printInfo(" run_id: {s}\n", .{run_id}); + colors.printInfo(" experiment_id: {s}\n", .{experiment_id.?}); + } +} + +fn executeFinishLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const RunOptions) !void { + var run_id: ?[]const u8 = null; + + var i: usize = 0; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--run") and i + 1 < args.len) { + run_id = args[i + 1]; + i += 1; + } + } + + if (run_id == null) { + if (options.json) { + std.debug.print("{{\"success\":false,\"command\":\"run.finish\",\"error\":\"--run is required\"}}\n", .{}); + } else { + colors.printError("Error: --run is required\n", .{}); + } + return error.MissingArgument; + } + + const cfg = try config.Config.load(allocator); + defer { + var mut_cfg = cfg; + mut_cfg.deinit(allocator); + } + + const db_path = try cfg.getDBPath(allocator); + defer allocator.free(db_path); + + var database = try db.DB.init(allocator, db_path); + defer database.close(); + + const timestamp = try db.currentTimestamp(allocator); + defer allocator.free(timestamp); + + const sql = "UPDATE ml_runs SET status = 'FINISHED', end_time = ? WHERE run_id = ?;"; + const stmt = try database.prepare(sql); + defer db.DB.finalize(stmt); + + try db.DB.bindText(stmt, 1, timestamp); + try db.DB.bindText(stmt, 2, run_id.?); + + _ = try db.DB.step(stmt); + database.checkpointOnExit(); + + if (options.json) { + std.debug.print("{{\"success\":true,\"command\":\"run.finish\",\"data\":{{\"run_id\":\"{s}\",\"status\":\"FINISHED\"}}}}\n", .{run_id.?}); + } else { + colors.printSuccess("✓ Finished run: {s}\n", .{run_id.?}); + } +} + +fn executeFailLocal(allocator: std.mem.Allocator, args: []const []const u8, options: *const RunOptions) !void { + var run_id: ?[]const u8 = null; + + var i: usize = 0; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--run") and i + 1 < args.len) { + run_id = args[i + 1]; + i += 1; + } + } + + if (run_id == null) { + if (options.json) { + std.debug.print("{{\"success\":false,\"command\":\"run.fail\",\"error\":\"--run is required\"}}\n", .{}); + } else { + colors.printError("Error: --run is required\n", .{}); + } + return error.MissingArgument; + } + + const cfg = try config.Config.load(allocator); + defer { + var mut_cfg = cfg; + mut_cfg.deinit(allocator); + } + + const db_path = try cfg.getDBPath(allocator); + defer allocator.free(db_path); + + var database = try db.DB.init(allocator, db_path); + defer database.close(); + + const timestamp = try db.currentTimestamp(allocator); + defer allocator.free(timestamp); + + const sql = "UPDATE ml_runs SET status = 'FAILED', end_time = ? WHERE run_id = ?;"; + const stmt = try database.prepare(sql); + defer db.DB.finalize(stmt); + + try db.DB.bindText(stmt, 1, timestamp); + try db.DB.bindText(stmt, 2, run_id.?); + + _ = try db.DB.step(stmt); + database.checkpointOnExit(); + + if (options.json) { + std.debug.print("{{\"success\":true,\"command\":\"run.fail\",\"data\":{{\"run_id\":\"{s}\",\"status\":\"FAILED\"}}}}\n", .{run_id.?}); + } else { + colors.printSuccess("✓ Marked run as failed: {s}\n", .{run_id.?}); + } +} + +fn executeListLocal(allocator: std.mem.Allocator, options: *const RunOptions) !void { + const cfg = try config.Config.load(allocator); + defer { + var mut_cfg = cfg; + mut_cfg.deinit(allocator); + } + + const db_path = try cfg.getDBPath(allocator); + defer allocator.free(db_path); + + var database = try db.DB.init(allocator, db_path); + defer database.close(); + + const sql = "SELECT run_id, experiment_id, name, status, start_time FROM ml_runs ORDER BY start_time DESC;"; + const stmt = try database.prepare(sql); + defer db.DB.finalize(stmt); + + if (options.json) { + std.debug.print("{{\"success\":true,\"command\":\"run.list\",\"data\":{{\"runs\":[", .{}); + var first = true; + while (try db.DB.step(stmt)) { + if (!first) std.debug.print(",", .{}); + first = false; + const run_id = db.DB.columnText(stmt, 0); + const exp_id = db.DB.columnText(stmt, 1); + const name = db.DB.columnText(stmt, 2); + const status = db.DB.columnText(stmt, 3); + const start = db.DB.columnText(stmt, 4); + std.debug.print("{{\"run_id\":\"{s}\",\"experiment_id\":\"{s}\",\"name\":\"{s}\",\"status\":\"{s}\",\"start_time\":\"{s}\"}}", .{ run_id, exp_id, name, status, start }); + } + std.debug.print("]}}}\n", .{}); + } else { + colors.printInfo("\nRuns:\n", .{}); + colors.printInfo("{s:-<80}\n", .{""}); + var count: usize = 0; + while (try db.DB.step(stmt)) { + const run_id = db.DB.columnText(stmt, 0); + const exp_id = db.DB.columnText(stmt, 1); + const name = db.DB.columnText(stmt, 2); + const status = db.DB.columnText(stmt, 3); + std.debug.print("{s} | {s} | {s} | {s}\n", .{ run_id, exp_id, name, status }); + count += 1; + } + if (count == 0) { + colors.printWarning("No runs found. Start one with: ml run start --experiment \n", .{}); + } + } +} + +// Server mode stubs (to be implemented) +fn executeStartServer(_: std.mem.Allocator, _: []const []const u8, _: *const RunOptions) !void { + std.debug.print("Server mode run start not yet implemented\n", .{}); +} + +fn executeFinishServer(_: std.mem.Allocator, _: []const []const u8, _: *const RunOptions) !void { + std.debug.print("Server mode run finish not yet implemented\n", .{}); +} + +fn executeFailServer(_: std.mem.Allocator, _: []const []const u8, _: *const RunOptions) !void { + std.debug.print("Server mode run fail not yet implemented\n", .{}); +} + +fn executeListServer(_: std.mem.Allocator, _: *const RunOptions) !void { + std.debug.print("Server mode run list not yet implemented\n", .{}); +} + +fn printUsage() !void { + colors.printInfo("Usage: ml run [options] [args]\n", .{}); + colors.printInfo("\nOptions:\n", .{}); + colors.printInfo(" --json Output structured JSON\n", .{}); + colors.printInfo(" --help, -h Show this help message\n", .{}); + colors.printInfo("\nCommands:\n", .{}); + colors.printInfo(" start --experiment [--name ] Start a new run\n", .{}); + colors.printInfo(" finish --run Mark run as finished\n", .{}); + colors.printInfo(" fail --run Mark run as failed\n", .{}); + colors.printInfo(" list List all runs\n", .{}); + colors.printInfo("\nExamples:\n", .{}); + colors.printInfo(" ml run start --experiment --name training\n", .{}); + colors.printInfo(" ml run finish --run \n", .{}); +} diff --git a/cli/src/db.zig b/cli/src/db.zig new file mode 100644 index 0000000..070c477 --- /dev/null +++ b/cli/src/db.zig @@ -0,0 +1,251 @@ +const std = @import("std"); + +// SQLite C bindings +const c = @cImport({ + @cInclude("sqlite3.h"); +}); + +// Schema for ML tracking tables +const SCHEMA = + \\ CREATE TABLE IF NOT EXISTS ml_experiments ( + \\ experiment_id TEXT PRIMARY KEY, + \\ name TEXT NOT NULL, + \\ artifact_path TEXT, + \\ lifecycle TEXT DEFAULT 'active', + \\ created_at DATETIME DEFAULT CURRENT_TIMESTAMP + \\ ); + \\ CREATE TABLE IF NOT EXISTS ml_runs ( + \\ run_id TEXT PRIMARY KEY, + \\ experiment_id TEXT NOT NULL, + \\ name TEXT, + \\ status TEXT, + \\ start_time DATETIME, + \\ end_time DATETIME, + \\ artifact_uri TEXT, + \\ synced INTEGER DEFAULT 0 + \\ ); + \\ CREATE TABLE IF NOT EXISTS ml_metrics ( + \\ run_id TEXT REFERENCES ml_runs(run_id), + \\ key TEXT, + \\ value REAL, + \\ step INTEGER DEFAULT 0, + \\ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + \\ ); + \\ CREATE TABLE IF NOT EXISTS ml_params ( + \\ run_id TEXT REFERENCES ml_runs(run_id), + \\ key TEXT, + \\ value TEXT + \\ ); + \\ CREATE TABLE IF NOT EXISTS ml_tags ( + \\ run_id TEXT REFERENCES ml_runs(run_id), + \\ key TEXT, + \\ value TEXT + \\ ); +; + +/// Database connection handle +pub const DB = struct { + handle: ?*c.sqlite3, + path: []const u8, + + /// Initialize database with WAL mode and schema + pub fn init(allocator: std.mem.Allocator, db_path: []const u8) !DB { + var db: ?*c.sqlite3 = null; + + // Open database + const rc = c.sqlite3_open(db_path, &db); + if (rc != c.SQLITE_OK) { + std.log.err("Failed to open database: {s}", .{c.sqlite3_errmsg(db)}); + return error.DBOpenFailed; + } + + // Enable WAL mode - required for concurrent CLI writes and TUI reads + var errmsg: [*c]u8 = null; + _ = c.sqlite3_exec(db, "PRAGMA journal_mode=WAL;", null, null, &errmsg); + if (errmsg != null) { + c.sqlite3_free(errmsg); + } + + // Set synchronous=NORMAL for performance under WAL + _ = c.sqlite3_exec(db, "PRAGMA synchronous=NORMAL;", null, null, &errmsg); + if (errmsg != null) { + c.sqlite3_free(errmsg); + } + + // Apply schema + _ = c.sqlite3_exec(db, SCHEMA, null, null, &errmsg); + if (errmsg != null) { + std.log.err("Schema creation failed: {s}", .{errmsg}); + c.sqlite3_free(errmsg); + _ = c.sqlite3_close(db); + return error.SchemaFailed; + } + + const path_copy = try allocator.dupe(u8, db_path); + + return DB{ + .handle = db, + .path = path_copy, + }; + } + + /// Close database connection + pub fn close(self: *DB) void { + if (self.handle) |db| { + _ = c.sqlite3_close(db); + self.handle = null; + } + } + + /// Checkpoint WAL on clean shutdown + pub fn checkpointOnExit(self: *DB) void { + if (self.handle) |db| { + var errmsg: [*c]u8 = null; + _ = c.sqlite3_exec(db, "PRAGMA wal_checkpoint(TRUNCATE);", null, null, &errmsg); + if (errmsg != null) { + c.sqlite3_free(errmsg); + } + } + } + + /// Execute a simple SQL statement + pub fn exec(self: DB, sql: []const u8) !void { + if (self.handle == null) return error.DBNotOpen; + + var errmsg: [*c]u8 = null; + const rc = c.sqlite3_exec(self.handle, sql.ptr, null, null, &errmsg); + + if (rc != c.SQLITE_OK) { + if (errmsg) |e| { + std.log.err("SQL error: {s}", .{e}); + c.sqlite3_free(errmsg); + } + return error.SQLExecFailed; + } + } + + /// Prepare a statement + pub fn prepare(self: DB, sql: []const u8) !?*c.sqlite3_stmt { + if (self.handle == null) return error.DBNotOpen; + + var stmt: ?*c.sqlite3_stmt = null; + const rc = c.sqlite3_prepare_v2(self.handle, sql.ptr, @intCast(sql.len), &stmt, null); + + if (rc != c.SQLITE_OK) { + std.log.err("Prepare failed: {s}", .{c.sqlite3_errmsg(self.handle)}); + return error.PrepareFailed; + } + + return stmt; + } + + /// Finalize a prepared statement + pub fn finalize(stmt: ?*c.sqlite3_stmt) void { + if (stmt) |s| { + _ = c.sqlite3_finalize(s); + } + } + + /// Bind text parameter to statement + pub fn bindText(stmt: ?*c.sqlite3_stmt, idx: i32, value: []const u8) !void { + if (stmt == null) return error.InvalidStatement; + const rc = c.sqlite3_bind_text(stmt, idx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT); + if (rc != c.SQLITE_OK) return error.BindFailed; + } + + /// Bind int64 parameter to statement + pub fn bindInt64(stmt: ?*c.sqlite3_stmt, idx: i32, value: i64) !void { + if (stmt == null) return error.InvalidStatement; + const rc = c.sqlite3_bind_int64(stmt, idx, value); + if (rc != c.SQLITE_OK) return error.BindFailed; + } + + /// Bind double parameter to statement + pub fn bindDouble(stmt: ?*c.sqlite3_stmt, idx: i32, value: f64) !void { + if (stmt == null) return error.InvalidStatement; + const rc = c.sqlite3_bind_double(stmt, idx, value); + if (rc != c.SQLITE_OK) return error.BindFailed; + } + + /// Step statement (execute) + pub fn step(stmt: ?*c.sqlite3_stmt) !bool { + if (stmt == null) return error.InvalidStatement; + const rc = c.sqlite3_step(stmt); + return rc == c.SQLITE_ROW; // true if has row, false if done + } + + /// Reset statement for reuse + pub fn reset(stmt: ?*c.sqlite3_stmt) !void { + if (stmt == null) return error.InvalidStatement; + _ = c.sqlite3_reset(stmt); + _ = c.sqlite3_clear_bindings(stmt); + } + + /// Get column text + pub fn columnText(stmt: ?*c.sqlite3_stmt, idx: i32) []const u8 { + if (stmt == null) return ""; + const ptr = c.sqlite3_column_text(stmt, idx); + const len = c.sqlite3_column_bytes(stmt, idx); + if (ptr == null or len == 0) return ""; + return ptr[0..@intCast(len)]; + } + + /// Get column int64 + pub fn columnInt64(stmt: ?*c.sqlite3_stmt, idx: i32) i64 { + if (stmt == null) return 0; + return c.sqlite3_column_int64(stmt, idx); + } + + /// Get column double + pub fn columnDouble(stmt: ?*c.sqlite3_stmt, idx: i32) f64 { + if (stmt == null) return 0.0; + return c.sqlite3_column_double(stmt, idx); + } +}; + +/// Generate UUID v4 (simple random-based) +pub fn generateUUID(allocator: std.mem.Allocator) ![]const u8 { + var buf: [36]u8 = undefined; + const hex_chars = "0123456789abcdef"; + + // Random bytes (simplified - in production use crypto RNG) + var bytes: [16]u8 = undefined; + std.crypto.random.bytes(&bytes); + + // Set version (4) and variant bits + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + // Format as UUID string + var idx: usize = 0; + for (0..16) |i| { + if (i == 4 or i == 6 or i == 8 or i == 10) { + buf[idx] = '-'; + idx += 1; + } + buf[idx] = hex_chars[bytes[i] >> 4]; + buf[idx + 1] = hex_chars[bytes[i] & 0x0f]; + idx += 2; + } + + return try allocator.dupe(u8, &buf); +} + +/// Get current timestamp as ISO8601 string +pub fn currentTimestamp(allocator: std.mem.Allocator) ![]const u8 { + const now = std.time.timestamp(); + const tm = std.time.epoch.EpochSeconds{ .secs = @intCast(now) }; + const dt = tm.getDaySeconds(); + + var buf: [20]u8 = undefined; + const len = try std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{ + tm.getEpochDay().year, + tm.getEpochDay().month, + tm.getEpochDay().day, + dt.getHoursIntoDay(), + dt.getMinutesIntoHour(), + dt.getSecondsIntoMinute(), + }); + + return try allocator.dupe(u8, len); +} diff --git a/cli/src/utils/sqlite_embedded.zig b/cli/src/utils/sqlite_embedded.zig new file mode 100644 index 0000000..22f43f0 --- /dev/null +++ b/cli/src/utils/sqlite_embedded.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const build_options = @import("build_options"); + +/// SQLite embedding strategy (mirrors rsync pattern) +/// +/// For dev builds: link against system SQLite library +/// For release builds: compile SQLite from downloaded amalgamation +/// +/// To prepare for release: +/// 1. Run: make build-sqlite +/// 2. Build with: zig build prod + +pub const USE_EMBEDDED_SQLITE = build_options.has_sqlite_release; + +/// Compile flags for embedded SQLite +pub const SQLITE_FLAGS = &[_][]const u8{ + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_ENABLE_JSON1", + "-DSQLITE_THREADSAFE=1", + "-DSQLITE_USE_URI", + "-DSQLITE_ENABLE_COLUMN_METADATA", + "-DSQLITE_ENABLE_STAT4", +}; + +/// Get SQLite include path for embedded builds +pub fn getSqliteIncludePath(b: *std.Build) ?std.Build.LazyPath { + if (!USE_EMBEDDED_SQLITE) return null; + return b.path(build_options.sqlite_release_path); +} + +/// Get SQLite source file path for embedded builds +pub fn getSqliteSourcePath(b: *std.Build) ?std.Build.LazyPath { + if (!USE_EMBEDDED_SQLITE) return null; + const path = std.fs.path.join(b.allocator, &.{ build_options.sqlite_release_path, "sqlite3.c" }) catch return null; + return b.path(path); +}