diff --git a/examples/osal/tmux.vsh b/examples/osal/tmux.vsh index 2b44abe7..dba1fc32 100755 --- a/examples/osal/tmux.vsh +++ b/examples/osal/tmux.vsh @@ -10,5 +10,11 @@ if !t.is_running()! { if t.session_exist('main') { t.session_delete('main')! } -t.window_new(name: 'test', cmd: 'mc', reset: true)! +// Create session first, then create window +mut session := t.session_create(name: 'main')! +session.window_new(name: 'test', cmd: 'mc', reset: true)! + +// Or use the convenience method +// t.window_new(session_name: 'main', name: 'test', cmd: 'mc', reset: true)! + println(t) diff --git a/lib/osal/linux/factory.v b/lib/osal/linux/factory.v new file mode 100644 index 00000000..ba22a2ab --- /dev/null +++ b/lib/osal/linux/factory.v @@ -0,0 +1,29 @@ +module linux + +// import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.core.texttools +// import freeflowuniverse.herolib.screen +import os +import time +// import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.osal.core as osal + +@[heap] +pub struct LinuxFactory { +pub mut: + username string +} + +@[params] +pub struct LinuxNewArgs { +pub: + username string +} + +// return screen instance +pub fn new(args LinuxNewArgs) !LinuxFactory { + mut t := LinuxFactory{ + username: args.username + } + return t +} diff --git a/lib/osal/linux/templates/user_add.sh b/lib/osal/linux/templates/user_add.sh new file mode 100644 index 00000000..cedda289 --- /dev/null +++ b/lib/osal/linux/templates/user_add.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "❌ Must be run as root" + exit 1 +fi + +# --- ask for username --- +read -rp "Enter username to create: " NEWUSER + +# --- ask for SSH public key --- +read -rp "Enter SSH public key (or path to pubkey file): " PUBKEYINPUT +if [ -f "$PUBKEYINPUT" ]; then + PUBKEY="$(cat "$PUBKEYINPUT")" +else + PUBKEY="$PUBKEYINPUT" +fi + +# --- ensure user exists --- +if id "$NEWUSER" >/dev/null 2>&1; then + echo "✅ User $NEWUSER already exists" +else + echo "➕ Creating user $NEWUSER" + useradd -m -s /bin/bash "$NEWUSER" +fi + +USERHOME=$(eval echo "~$NEWUSER") + +# --- setup SSH authorized_keys --- +mkdir -p "$USERHOME/.ssh" +chmod 700 "$USERHOME/.ssh" +echo "$PUBKEY" > "$USERHOME/.ssh/authorized_keys" +chmod 600 "$USERHOME/.ssh/authorized_keys" +chown -R "$NEWUSER":"$NEWUSER" "$USERHOME/.ssh" +echo "✅ SSH key installed for $NEWUSER" + +# --- ensure ourworld group exists --- +if getent group ourworld >/dev/null 2>&1; then + echo "✅ Group 'ourworld' exists" +else + echo "➕ Creating group 'ourworld'" + groupadd ourworld +fi + +# --- add user to group --- +if id -nG "$NEWUSER" | grep -qw ourworld; then + echo "✅ $NEWUSER already in 'ourworld'" +else + usermod -aG ourworld "$NEWUSER" + echo "✅ Added $NEWUSER to 'ourworld' group" +fi + +# --- setup /code --- +mkdir -p /code +chown root:ourworld /code +chmod 2775 /code # rwx for user+group, SGID bit so new files inherit group +echo "✅ /code prepared (group=ourworld, rwx for group, SGID bit set)" + +# --- create login helper script for gpg-agent --- +PROFILE_SCRIPT="$USERHOME/.profile_gpgagent" +cat > "$PROFILE_SCRIPT" <<'EOF' +# Auto-start gpg-agent with SSH support if not running +mkdir -p "$HOME/.gnupg" +chmod 700 "$HOME/.gnupg" + +# Always overwrite gpg-agent.conf with required config +cat > "$HOME/.gnupg/gpg-agent.conf" </dev/null || true + +# Launch gpg-agent +gpgconf --launch gpg-agent + +# Export socket path so ssh-add works +export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" + +# Load all private keys found in ~/.ssh +if [ -d "$HOME/.ssh" ]; then + for KEY in "$HOME"/.ssh/*; do + if [ -f "$KEY" ] && grep -q "PRIVATE KEY" "$KEY" 2>/dev/null; then + ssh-add "$KEY" >/dev/null 2>&1 && echo "🔑 Loaded key: $KEY" + fi + done +fi + +# For interactive shells +if [[ $- == *i* ]]; then + echo "🔑 GPG Agent ready at \$SSH_AUTH_SOCK" +fi + +EOF + +chown "$NEWUSER":"$NEWUSER" "$PROFILE_SCRIPT" +chmod 644 "$PROFILE_SCRIPT" + +# --- source it on login --- +if ! grep -q ".profile_gpgagent" "$USERHOME/.bashrc"; then + echo "[ -f ~/.profile_gpgagent ] && source ~/.profile_gpgagent" >> "$USERHOME/.bashrc" +fi + +echo "🎉 Setup complete for user $NEWUSER" diff --git a/lib/osal/linux/user_mgmt.v b/lib/osal/linux/user_mgmt.v new file mode 100644 index 00000000..ba22a2ab --- /dev/null +++ b/lib/osal/linux/user_mgmt.v @@ -0,0 +1,29 @@ +module linux + +// import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.core.texttools +// import freeflowuniverse.herolib.screen +import os +import time +// import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.osal.core as osal + +@[heap] +pub struct LinuxFactory { +pub mut: + username string +} + +@[params] +pub struct LinuxNewArgs { +pub: + username string +} + +// return screen instance +pub fn new(args LinuxNewArgs) !LinuxFactory { + mut t := LinuxFactory{ + username: args.username + } + return t +} diff --git a/lib/osal/tmux/tmux.v b/lib/osal/tmux/tmux.v index 3e1f2c6e..1d9b09d3 100644 --- a/lib/osal/tmux/tmux.v +++ b/lib/osal/tmux/tmux.v @@ -1,6 +1,7 @@ module tmux import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.core.texttools // import freeflowuniverse.herolib.session import os import time @@ -98,6 +99,33 @@ pub fn new(args TmuxNewArgs) !Tmux { return t } +@[params] +pub struct WindowNewArgs { +pub mut: + session_name string = 'main' + name string + cmd string + env map[string]string + reset bool +} + +pub fn (mut t Tmux) window_new(args WindowNewArgs) !&Window { + // Get or create session + mut session := if t.session_exist(args.session_name) { + t.session_get(args.session_name)! + } else { + t.session_create(name: args.session_name)! + } + + // Create window in session + return session.window_new( + name: args.name + cmd: args.cmd + env: args.env + reset: args.reset + )! +} + pub fn (mut t Tmux) stop() ! { $if debug { @@ -128,19 +156,6 @@ pub fn (mut t Tmux) start() ! { t.scan()! } -enum ProcessStatus { - running - finished_ok - finished_error - not_found - } - -pub struct LogEntry { -pub mut: - content string - timestamp time.Time - offset int -} // print list of tmux sessions pub fn (mut t Tmux) list_print() { diff --git a/lib/osal/tmux/tmux_pane.v b/lib/osal/tmux/tmux_pane.v index 5e959dfd..1b786cc4 100644 --- a/lib/osal/tmux/tmux_pane.v +++ b/lib/osal/tmux/tmux_pane.v @@ -1,9 +1,10 @@ module tmux import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.data.ourtime +import time // import freeflowuniverse.herolib.session import os -import time import freeflowuniverse.herolib.ui.console @[heap] @@ -48,14 +49,14 @@ pub fn (mut p Pane) stats() !ProcessStats { } -pub struct LogEntry { +pub struct TMuxLogEntry { pub mut: content string timestamp time.Time offset int } -pub fn (mut p Pane) logs_get_new(reset bool) ![]LogEntry { +pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry { if reset{ p.last_output_offset = 0 @@ -64,15 +65,15 @@ pub fn (mut p Pane) logs_get_new(reset bool) ![]LogEntry { cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S ${p.last_output_offset} -p' result := osal.execute_silent(cmd) or { return error('Cannot capture pane output: ${err}') - } + } lines := result.split_into_lines() - mut entries := []LogEntry{} + mut entries := []TMuxLogEntry{} mut i:= 0 for line in lines { if line.trim_space() != '' { - entries << LogEntry{ + entries << TMuxLogEntry{ content: line timestamp: time.now() offset: p.last_output_offset + i + 1 @@ -83,7 +84,7 @@ pub fn (mut p Pane) logs_get_new(reset bool) ![]LogEntry { if entries.len > 0 { p.last_output_offset = entries.last().offset } - return entries + return entries } pub fn (mut p Pane) exit_status() !ProcessStatus { diff --git a/lib/osal/tmux/tmux_scan.v b/lib/osal/tmux/tmux_scan.v index 90f25ca0..65274a6a 100644 --- a/lib/osal/tmux/tmux_scan.v +++ b/lib/osal/tmux/tmux_scan.v @@ -3,6 +3,7 @@ module tmux import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.ui.console +import time fn (mut t Tmux) scan_add(line string) !&Pane { // Parse the line to get session, window, and pane info diff --git a/lib/osal/tmux/tmux_session.v b/lib/osal/tmux/tmux_session.v index 33daee3b..76d73f73 100644 --- a/lib/osal/tmux/tmux_session.v +++ b/lib/osal/tmux/tmux_session.v @@ -17,6 +17,8 @@ pub mut: pub struct WindowArgs { pub mut: name string + cmd string + env map[string]string reset bool } @[params] @@ -27,6 +29,25 @@ pub mut: } +pub fn (mut s Session) create() ! { + // Check if session already exists + cmd_check := "tmux has-session -t ${s.name}" + check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or { + // Session doesn't exist, this is expected + osal.Job{} + } + + if check_result.exit_code == 0 { + return error('duplicate session: ${s.name}') + } + + // Create new session + cmd := "tmux new-session -d -s ${s.name}" + osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or { + return error("Can't create session ${s.name}: ${err}") + } +} + //load info from reality pub fn (mut s Session) scan() ! { // Get current windows from tmux for this session @@ -115,21 +136,10 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window { } s.windows << &w - res_opt := "-P -F '#\{window_id\}'" - cmd := "tmux new-session ${res_opt} -d -s ${s.name} 'sh'" - window_id_ := osal.execute_silent(cmd) or { - return error("Can't create tmux session ${s.name} \n${cmd}\n${err}") - } - - cmd3 := 'tmux set-option remain-on-exit on' - osal.execute_silent(cmd3) or { return error("Can't execute ${cmd3}\n${err}") } - - window_id := window_id_.trim(' \n') - cmd2 := "tmux rename-window -t ${window_id} 'notused'" - osal.execute_silent(cmd2) or { - return error("Can't rename window ${window_id} to notused \n${cmd2}\n${err}") - } + // Create the window with the specified command + w.create(args.cmd)! s.scan()! + return w }