mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 10:00:52 +00:00
Compare commits
5 Commits
39b0665678
...
81168ff49a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81168ff49a | ||
|
|
199e52b5cb | ||
|
|
b97e36cdeb | ||
|
|
7c273e2132 | ||
|
|
986b18b163 |
|
|
@ -1,3 +1,14 @@
|
||||||
|
secubox-droplet (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Forge dropletctl (issue #181) — third routing verb on the publishing
|
||||||
|
layer, parallel to giteactl repo mirror (#176) and mitmproxyctl route
|
||||||
|
(#173). Subcommands: lifecycle (start/stop/restart/status/logs),
|
||||||
|
Three-fold JSON (components/access), and file noun verbs (upload,
|
||||||
|
remove, rename, list, info) wrapping the /api/v1/droplet/* endpoints
|
||||||
|
over the Unix socket.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:20:54 +0200
|
||||||
|
|
||||||
secubox-droplet (1.0.1-1~bookworm1) bookworm; urgency=medium
|
secubox-droplet (1.0.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add dynamic menu system with menu.d JSON definitions
|
* Add dynamic menu system with menu.d JSON definitions
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-droplet/etc/nginx/secubox.d
|
install -d debian/secubox-droplet/etc/nginx/secubox.d
|
||||||
[ -f nginx/droplet.conf ] && cp nginx/droplet.conf debian/secubox-droplet/etc/nginx/secubox.d/ || true
|
[ -f nginx/droplet.conf ] && cp nginx/droplet.conf debian/secubox-droplet/etc/nginx/secubox.d/ || true
|
||||||
|
# dropletctl (issue #181)
|
||||||
|
install -d debian/secubox-droplet/usr/sbin
|
||||||
|
install -m 755 sbin/dropletctl debian/secubox-droplet/usr/sbin/
|
||||||
|
|
|
||||||
225
packages/secubox-droplet/sbin/dropletctl
Normal file
225
packages/secubox-droplet/sbin/dropletctl
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||||
|
# See LICENCE-CMSD-1.0.md for terms.
|
||||||
|
#
|
||||||
|
# dropletctl — SecuBox Droplet File Publisher control (issue #181)
|
||||||
|
#
|
||||||
|
# Third routing verb on the publishing layer, parallel to:
|
||||||
|
# haproxyctl vhost add/remove (routing)
|
||||||
|
# mitmproxyctl route add/remove (interception, #173)
|
||||||
|
# giteactl repo mirror add (replication, #176)
|
||||||
|
# dropletctl file upload/list (publishing, this)
|
||||||
|
#
|
||||||
|
# The Droplet API exposes /upload, /list, /remove, /rename over a Unix
|
||||||
|
# socket; this ctl wraps those endpoints under a coherent <noun> <verb>
|
||||||
|
# grammar so operators don't have to curl by hand.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="0.1.0"
|
||||||
|
SOCKET="${DROPLET_SOCKET:-/run/secubox/droplet.sock}"
|
||||||
|
API_BASE="http://localhost/api/v1/droplet"
|
||||||
|
SERVICE="secubox-droplet.service"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
log() { printf "${GREEN}[DROPLET]${NC} %s\n" "$*"; }
|
||||||
|
warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
|
||||||
|
error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; }
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_socket() {
|
||||||
|
if [ ! -S "$SOCKET" ]; then
|
||||||
|
error "API socket $SOCKET not present — start $SERVICE first"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local method="$1" path="$2"
|
||||||
|
shift 2
|
||||||
|
require_socket
|
||||||
|
curl --unix-socket "$SOCKET" -sS -X "$method" \
|
||||||
|
-w "\nHTTP_CODE:%{http_code}\n" \
|
||||||
|
"${API_BASE}${path}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_code() {
|
||||||
|
echo "$1" | grep '^HTTP_CODE:' | cut -d: -f2
|
||||||
|
}
|
||||||
|
|
||||||
|
api_body() {
|
||||||
|
echo "$1" | sed '/^HTTP_CODE:/d'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Lifecycle (top-level, mirrors mitmproxyctl/giteactl) ──────────────────
|
||||||
|
|
||||||
|
cmd_install() { log "install via dpkg; this ctl assumes package already installed"; return 0; }
|
||||||
|
cmd_start() { systemctl start "$SERVICE" && log "started"; }
|
||||||
|
cmd_stop() { systemctl stop "$SERVICE" && log "stopped"; }
|
||||||
|
cmd_restart() { systemctl restart "$SERVICE" && log "restarted";}
|
||||||
|
cmd_status() {
|
||||||
|
systemctl is-active "$SERVICE" >/dev/null 2>&1 \
|
||||||
|
&& echo "active" || echo "inactive"
|
||||||
|
}
|
||||||
|
cmd_logs() {
|
||||||
|
journalctl -u "$SERVICE" -n "${1:-50}" --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Three-fold (giteactl convention: components / status / access JSON) ───
|
||||||
|
|
||||||
|
cmd_components() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"service": "$SERVICE",
|
||||||
|
"socket": "$SOCKET",
|
||||||
|
"api_base": "$API_BASE",
|
||||||
|
"ctl_version": "$VERSION"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_access() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"socket": "$SOCKET",
|
||||||
|
"api_paths": {
|
||||||
|
"upload": "POST /api/v1/droplet/upload",
|
||||||
|
"list": "GET /api/v1/droplet/list",
|
||||||
|
"remove": "POST /api/v1/droplet/remove",
|
||||||
|
"rename": "POST /api/v1/droplet/rename",
|
||||||
|
"info": "GET /api/v1/droplet/droplet/{name}",
|
||||||
|
"stats": "GET /api/v1/droplet/stats",
|
||||||
|
"storage":"GET /api/v1/droplet/storage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── file noun verbs (the missing grammar — issue #181) ────────────────────
|
||||||
|
|
||||||
|
cmd_file() {
|
||||||
|
local action="${1:-}"; shift || true
|
||||||
|
case "$action" in
|
||||||
|
upload) cmd_file_upload "$@" ;;
|
||||||
|
remove|rm|del) cmd_file_remove "$@" ;;
|
||||||
|
rename) cmd_file_rename "$@" ;;
|
||||||
|
list|ls) cmd_file_list "$@" ;;
|
||||||
|
info) cmd_file_info "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
File commands:
|
||||||
|
file upload <path> [--public] [--ttl <e.g. 7d>]
|
||||||
|
file remove <name>
|
||||||
|
file rename <old> <new>
|
||||||
|
file list [--limit N]
|
||||||
|
file info <name>
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_upload() {
|
||||||
|
local path="$1"; shift || { error "file upload <path> required"; return 1; }
|
||||||
|
[ -f "$path" ] || { error "file not found: $path"; return 1; }
|
||||||
|
local public=false ttl=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--public) public=true ;;
|
||||||
|
--ttl) ttl="$2"; shift ;;
|
||||||
|
*) error "unknown flag: $1"; return 1 ;;
|
||||||
|
esac; shift
|
||||||
|
done
|
||||||
|
log "uploading $path (public=$public ttl=${ttl:-default})"
|
||||||
|
local args=("-F" "file=@${path}")
|
||||||
|
$public && args+=("-F" "public=true")
|
||||||
|
[ -n "$ttl" ] && args+=("-F" "ttl=$ttl")
|
||||||
|
local out
|
||||||
|
out=$(api POST "/upload" "${args[@]}")
|
||||||
|
local code; code=$(api_code "$out")
|
||||||
|
case "$code" in
|
||||||
|
200|201) api_body "$out" ;;
|
||||||
|
*) error "upload failed (HTTP $code): $(api_body "$out" | head -3)"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_remove() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "file remove <name> required"; return 1; }
|
||||||
|
log "removing $name"
|
||||||
|
local out
|
||||||
|
out=$(api POST "/remove" -H "Content-Type: application/json" \
|
||||||
|
-d "$(printf '{"name":"%s"}' "$name")")
|
||||||
|
local code; code=$(api_code "$out")
|
||||||
|
[ "$code" = "200" ] || { error "remove failed (HTTP $code): $(api_body "$out")"; return 1; }
|
||||||
|
log "removed $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_rename() {
|
||||||
|
local old="$1" new="$2"
|
||||||
|
[ -z "$old" ] || [ -z "$new" ] && { error "file rename <old> <new> required"; return 1; }
|
||||||
|
log "renaming $old -> $new"
|
||||||
|
local out
|
||||||
|
out=$(api POST "/rename" -H "Content-Type: application/json" \
|
||||||
|
-d "$(printf '{"old":"%s","new":"%s"}' "$old" "$new")")
|
||||||
|
local code; code=$(api_code "$out")
|
||||||
|
[ "$code" = "200" ] || { error "rename failed (HTTP $code): $(api_body "$out")"; return 1; }
|
||||||
|
log "renamed"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_list() {
|
||||||
|
local limit=""
|
||||||
|
[ "${1:-}" = "--limit" ] && { limit="?limit=$2"; }
|
||||||
|
local out
|
||||||
|
out=$(api GET "/list${limit}")
|
||||||
|
api_body "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_info() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "file info <name> required"; return 1; }
|
||||||
|
local out
|
||||||
|
out=$(api GET "/droplet/${name}")
|
||||||
|
api_body "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main dispatch ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
SecuBox Droplet Controller v$VERSION (issue #181)
|
||||||
|
File publisher CLI — parallel to giteactl, mitmproxyctl
|
||||||
|
|
||||||
|
Usage: dropletctl <command> [options]
|
||||||
|
|
||||||
|
Lifecycle:
|
||||||
|
install / start / stop / restart / status / logs
|
||||||
|
|
||||||
|
Three-fold (JSON):
|
||||||
|
components / access
|
||||||
|
|
||||||
|
Files (issue #181):
|
||||||
|
file upload <path> [--public] [--ttl 7d]
|
||||||
|
file remove <name>
|
||||||
|
file rename <old> <new>
|
||||||
|
file list [--limit N]
|
||||||
|
file info <name>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dropletctl file upload /tmp/report.pdf --public --ttl 7d
|
||||||
|
dropletctl file list
|
||||||
|
dropletctl access | jq .api_paths
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
install|start|stop|restart|status) cmd="$1"; shift; cmd_$cmd "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
components) cmd_components ;;
|
||||||
|
access) cmd_access ;;
|
||||||
|
file) shift; cmd_file "$@" ;;
|
||||||
|
help|--help|-h|'') show_help ;;
|
||||||
|
*) error "unknown command: $1"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
secubox-metablogizer (1.1.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* metablogizerctl: forge tor noun verbs (issue #184) — the Emancipate
|
||||||
|
verb of the Punk Exposure Engine at the publishing layer.
|
||||||
|
Subcommands:
|
||||||
|
tor expose <site> Publish site via Tor hidden service
|
||||||
|
tor revoke <site> Stop publishing via Tor
|
||||||
|
tor list List Tor-exposed sites with onion addresses
|
||||||
|
tor status <site> Show stanza + onion + tor service state
|
||||||
|
When secubox-exposure is installed, delegates to it for consistency
|
||||||
|
with other exposure channels (DNS+SSL, Mesh). Otherwise falls back
|
||||||
|
to direct /etc/tor/secubox-metablogizer.d/ stanza management.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:23:12 +0200
|
||||||
|
|
||||||
secubox-metablogizer (1.1.0-1~bookworm1) bookworm; urgency=medium
|
secubox-metablogizer (1.1.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add metablogizerctl three-fold commands: components, access (JSON output)
|
* Add metablogizerctl three-fold commands: components, access (JSON output)
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,155 @@ site_unpublish() {
|
||||||
log "Site unpublished: $name"
|
log "Site unpublished: $name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tor — Emancipate verb (Punk Exposure Engine, issue #184)
|
||||||
|
#
|
||||||
|
# Per CLAUDE.md the Punk Exposure Engine has three verbs (Peek/Poke/Emancipate)
|
||||||
|
# and Tor is one of the three exposure channels. `metablogizerctl tor expose`
|
||||||
|
# is the Emancipate verb at the publishing layer for static sites.
|
||||||
|
#
|
||||||
|
# Implementation: write a per-site Tor HiddenService stanza, reload tor,
|
||||||
|
# read the generated .onion hostname back. If secubox-exposure is installed,
|
||||||
|
# delegate to it for consistency with other channels; otherwise fall back
|
||||||
|
# to direct torrc manipulation.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
TOR_DROPIN_DIR="${TOR_DROPIN_DIR:-/etc/tor/secubox-metablogizer.d}"
|
||||||
|
TOR_DATA_DIR="${TOR_DATA_DIR:-/var/lib/tor/secubox-metablogizer}"
|
||||||
|
|
||||||
|
_tor_drop_path() { echo "$TOR_DROPIN_DIR/${1}.conf"; }
|
||||||
|
_tor_data_path() { echo "$TOR_DATA_DIR/${1}"; }
|
||||||
|
|
||||||
|
_have_exposure() { command -v secubox-exposure >/dev/null 2>&1; }
|
||||||
|
_have_tor() { command -v tor >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
tor_expose() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: metablogizerctl tor expose <site>"; return 1; }
|
||||||
|
|
||||||
|
local site_dir="$SITES_ROOT/$name"
|
||||||
|
[ -d "$site_dir" ] || { error "site not found: $name (run: metablogizerctl site create $name first)"; return 1; }
|
||||||
|
|
||||||
|
# Prefer secubox-exposure when available so all 3 exposure channels share
|
||||||
|
# the same orchestration (Tor / DNS+SSL / Mesh) and Peek shows it.
|
||||||
|
if _have_exposure; then
|
||||||
|
log "delegating to secubox-exposure emancipate (tor channel)"
|
||||||
|
# The static site is served via nginx on port 80 (per site_publish);
|
||||||
|
# secubox-exposure handles the HiddenService wiring and revocation.
|
||||||
|
secubox-exposure emancipate "metablogizer-${name}" 80 --tor
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: write a tor drop-in directly.
|
||||||
|
_have_tor || { error "tor not installed and secubox-exposure unavailable"; return 1; }
|
||||||
|
mkdir -p "$TOR_DROPIN_DIR"
|
||||||
|
mkdir -p "$TOR_DATA_DIR"
|
||||||
|
local data; data=$(_tor_data_path "$name")
|
||||||
|
local drop; drop=$(_tor_drop_path "$name")
|
||||||
|
log "writing Tor HiddenService stanza for $name"
|
||||||
|
cat > "$drop" <<EOF
|
||||||
|
# metablogizer site: $name (#184 — Emancipate via Tor)
|
||||||
|
HiddenServiceDir $data
|
||||||
|
HiddenServicePort 80 127.0.0.1:80
|
||||||
|
EOF
|
||||||
|
chown -R debian-tor:debian-tor "$TOR_DATA_DIR" 2>/dev/null || true
|
||||||
|
chmod 700 "$data" 2>/dev/null || true
|
||||||
|
log "reloading tor"
|
||||||
|
systemctl reload tor 2>/dev/null || systemctl restart tor
|
||||||
|
# Wait briefly for tor to publish the hostname file
|
||||||
|
local i=0
|
||||||
|
while [ $i -lt 10 ] && [ ! -f "$data/hostname" ]; do sleep 1; i=$((i+1)); done
|
||||||
|
if [ -f "$data/hostname" ]; then
|
||||||
|
local onion; onion=$(cat "$data/hostname")
|
||||||
|
log "site emancipated via Tor: $onion"
|
||||||
|
# Persist the address back into site.json for future Peek calls
|
||||||
|
if [ -f "$site_dir/site.json" ] && command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
p='$site_dir/site.json'
|
||||||
|
d=json.load(open(p))
|
||||||
|
d.setdefault('exposure',{})['tor']='$onion'
|
||||||
|
json.dump(d, open(p,'w'), indent=2)
|
||||||
|
" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "tor reload OK but hostname not yet written; check 'metablogizerctl tor status $name'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_revoke() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: metablogizerctl tor revoke <site>"; return 1; }
|
||||||
|
if _have_exposure; then
|
||||||
|
log "delegating to secubox-exposure revoke"
|
||||||
|
secubox-exposure revoke "metablogizer-${name}" --tor
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
local drop; drop=$(_tor_drop_path "$name")
|
||||||
|
[ -f "$drop" ] || { warn "no Tor stanza for $name"; return 0; }
|
||||||
|
rm -f "$drop"
|
||||||
|
systemctl reload tor 2>/dev/null || systemctl restart tor
|
||||||
|
log "tor stanza removed for $name (data dir kept under $TOR_DATA_DIR/$name — delete manually if desired)"
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_list() {
|
||||||
|
if _have_exposure; then
|
||||||
|
log "(delegate) secubox-exposure list --tor"
|
||||||
|
secubox-exposure list --tor 2>/dev/null && return
|
||||||
|
fi
|
||||||
|
if [ ! -d "$TOR_DROPIN_DIR" ]; then
|
||||||
|
echo "(no Tor-exposed sites)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local any=0
|
||||||
|
for d in "$TOR_DROPIN_DIR"/*.conf; do
|
||||||
|
[ -f "$d" ] || continue
|
||||||
|
any=1
|
||||||
|
local n; n=$(basename "$d" .conf)
|
||||||
|
local h="$TOR_DATA_DIR/$n/hostname"
|
||||||
|
if [ -f "$h" ]; then
|
||||||
|
printf " %-30s -> %s\n" "$n" "$(cat "$h")"
|
||||||
|
else
|
||||||
|
printf " %-30s -> (publishing...)\n" "$n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ $any = 0 ] && echo "(no Tor-exposed sites)"
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_status() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: metablogizerctl tor status <site>"; return 1; }
|
||||||
|
local data; data=$(_tor_data_path "$name")
|
||||||
|
local drop; drop=$(_tor_drop_path "$name")
|
||||||
|
echo "site: $name"
|
||||||
|
echo "stanza present: $([ -f "$drop" ] && echo yes || echo no)"
|
||||||
|
if [ -f "$data/hostname" ]; then
|
||||||
|
echo "onion: $(cat "$data/hostname")"
|
||||||
|
else
|
||||||
|
echo "onion: (not yet published)"
|
||||||
|
fi
|
||||||
|
systemctl is-active tor >/dev/null 2>&1 && echo "tor service: active" || echo "tor service: inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_tor() {
|
||||||
|
local action="${1:-}"; shift || true
|
||||||
|
case "$action" in
|
||||||
|
expose) tor_expose "$@" ;;
|
||||||
|
revoke|remove) tor_revoke "$@" ;;
|
||||||
|
list|ls) tor_list ;;
|
||||||
|
status) tor_status "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Tor commands (Punk Exposure / Emancipate verb, issue #184):
|
||||||
|
tor expose <site> - publish site via Tor hidden service
|
||||||
|
tor revoke <site> - stop publishing via Tor
|
||||||
|
tor list - list Tor-exposed sites + their onion addresses
|
||||||
|
tor status <site> - show stanza presence + onion + tor service state
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
site_list() {
|
site_list() {
|
||||||
echo "MetaBlogizer Sites:"
|
echo "MetaBlogizer Sites:"
|
||||||
echo "==================="
|
echo "==================="
|
||||||
|
|
@ -387,6 +536,12 @@ Sites:
|
||||||
site unpublish <name> Unpublish site
|
site unpublish <name> Unpublish site
|
||||||
site list List all sites
|
site list List all sites
|
||||||
|
|
||||||
|
Tor (Punk Exposure / Emancipate, issue #184):
|
||||||
|
tor expose <site> Publish site via Tor hidden service
|
||||||
|
tor revoke <site> Stop publishing via Tor
|
||||||
|
tor list List Tor-exposed sites + onions
|
||||||
|
tor status <site> Stanza + onion + tor service state
|
||||||
|
|
||||||
Service:
|
Service:
|
||||||
migrate [host] Migrate from OpenWrt
|
migrate [host] Migrate from OpenWrt
|
||||||
|
|
||||||
|
|
@ -394,6 +549,7 @@ Examples:
|
||||||
metablogizerctl components # JSON components
|
metablogizerctl components # JSON components
|
||||||
metablogizerctl site create myblog blog.example.com
|
metablogizerctl site create myblog blog.example.com
|
||||||
metablogizerctl site publish myblog
|
metablogizerctl site publish myblog
|
||||||
|
metablogizerctl tor expose myblog # Emancipate via Tor
|
||||||
metablogizerctl migrate 192.168.255.1
|
metablogizerctl migrate 192.168.255.1
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -422,6 +578,8 @@ case "${1:-}" in
|
||||||
*) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
|
*) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
# Tor (Emancipate, issue #184)
|
||||||
|
tor) shift; cmd_tor "$@" ;;
|
||||||
migrate) shift; cmd_migrate "$@" ;;
|
migrate) shift; cmd_migrate "$@" ;;
|
||||||
help|--help|-h|'') show_help ;;
|
help|--help|-h|'') show_help ;;
|
||||||
*) error "Unknown: $1"; exit 1 ;;
|
*) error "Unknown: $1"; exit 1 ;;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SecuBox MetaCtl — ISP Home Publish CLI
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
# CyberMind — https://cybermind.fr
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# publishctl — SecuBox ISP Home Publish CLI (issue #180)
|
||||||
|
#
|
||||||
|
# Renamed from `metactl` for naming consistency with the rest of the
|
||||||
|
# SecuBox grammar (haproxyctl/giteactl/mitmproxyctl/metablogizerctl/
|
||||||
|
# dropletctl/streamlitctl/streamforgectl). The old `metactl` name remains
|
||||||
|
# as a symlink for backward compatibility — to drop in a future major.
|
||||||
|
#
|
||||||
|
# Flat verbs are now also reachable under the `post` noun dispatch
|
||||||
|
# for grammar consistency (publishctl post upload <file>, etc). Flat
|
||||||
|
# top-level verbs preserved for backward compatibility.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="1.0.0"
|
VERSION="2.0.0"
|
||||||
API_BASE="${SECUBOX_API_BASE:-http://127.0.0.1/api/v1/publish}"
|
API_BASE="${SECUBOX_API_BASE:-http://127.0.0.1/api/v1/publish}"
|
||||||
METABLOGIZER_API="${SECUBOX_METABLOGIZER_API:-http://127.0.0.1/api/v1/metablogizer}"
|
METABLOGIZER_API="${SECUBOX_METABLOGIZER_API:-http://127.0.0.1/api/v1/metablogizer}"
|
||||||
TOKEN_FILE="${SECUBOX_TOKEN_FILE:-/etc/secubox/secrets/jwt-token}"
|
TOKEN_FILE="${SECUBOX_TOKEN_FILE:-/etc/secubox/secrets/jwt-token}"
|
||||||
|
|
@ -408,8 +419,39 @@ cmd_health() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# post noun dispatch (issue #180 — grammar consistency, parallel to
|
||||||
|
# `giteactl repo`, `mitmproxyctl route`, `dropletctl file`, etc).
|
||||||
|
# Delegates to the existing flat cmd_* functions; both grammars supported.
|
||||||
|
cmd_post() {
|
||||||
|
local act="${1:-}"; shift || true
|
||||||
|
case "$act" in
|
||||||
|
upload) cmd_upload "$@" ;;
|
||||||
|
publish) cmd_publish "$@" ;;
|
||||||
|
unpublish) cmd_unpublish "$@" ;;
|
||||||
|
list|ls) cmd_list ;;
|
||||||
|
download) cmd_download "$@" ;;
|
||||||
|
qrcode|qr) cmd_qrcode "$@" ;;
|
||||||
|
health) cmd_health "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Post commands (issue #180):
|
||||||
|
post upload <file.zip> [name] [--domain=D] [--auto-publish]
|
||||||
|
post publish <name>
|
||||||
|
post unpublish <name>
|
||||||
|
post list
|
||||||
|
post download <name> [output.zip]
|
||||||
|
post qrcode <name>
|
||||||
|
post health <domain>
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
|
# noun-verb grammar (issue #180)
|
||||||
|
post) shift; cmd_post "$@" ;;
|
||||||
|
# flat verbs (backward-compat — same callbacks)
|
||||||
upload) shift; cmd_upload "$@" ;;
|
upload) shift; cmd_upload "$@" ;;
|
||||||
publish) shift; cmd_publish "$@" ;;
|
publish) shift; cmd_publish "$@" ;;
|
||||||
unpublish) shift; cmd_unpublish "$@" ;;
|
unpublish) shift; cmd_unpublish "$@" ;;
|
||||||
|
|
@ -419,10 +461,10 @@ case "${1:-help}" in
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
health) shift; cmd_health "$@" ;;
|
health) shift; cmd_health "$@" ;;
|
||||||
-h|--help|help) usage ;;
|
-h|--help|help) usage ;;
|
||||||
-v|--version) echo "metactl v${VERSION}" ;;
|
-v|--version) echo "publishctl v${VERSION}" ;;
|
||||||
*)
|
*)
|
||||||
echo -e "${RED}Unknown command:${NC} $1"
|
echo -e "${RED}Unknown command:${NC} $1"
|
||||||
echo "Run 'metactl --help' for usage"
|
echo "Run 'publishctl --help' for usage"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
secubox-publish (2.0.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Rename `metactl` -> `publishctl` for naming consistency with the rest
|
||||||
|
of the SecuBox ctl grammar (issue #180). The `metactl` name remains
|
||||||
|
as a symlink for backward compatibility — to drop in a future major.
|
||||||
|
* publishctl: add `post` noun dispatch so verbs are grouped under a
|
||||||
|
coherent <noun> <verb> schema parallel to giteactl/dropletctl/
|
||||||
|
metablogizerctl. Flat top-level verbs preserved as alias.
|
||||||
|
|
||||||
|
publishctl post upload <file.zip> [name] [--auto-publish]
|
||||||
|
publishctl post publish/unpublish <name>
|
||||||
|
publishctl post list/download/qrcode/health ...
|
||||||
|
|
||||||
|
* Bumped to 2.0.0 (CLI surface rename).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:38:19 +0200
|
||||||
|
|
||||||
secubox-publish (1.0.0-1~bookworm1) bookworm; urgency=medium
|
secubox-publish (1.0.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ override_dh_auto_install:
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-publish/etc/nginx/secubox.d
|
install -d debian/secubox-publish/etc/nginx/secubox.d
|
||||||
[ -f nginx/publish.conf ] && cp nginx/publish.conf debian/secubox-publish/etc/nginx/secubox.d/ || true
|
[ -f nginx/publish.conf ] && cp nginx/publish.conf debian/secubox-publish/etc/nginx/secubox.d/ || true
|
||||||
# CLI tool
|
# CLI tool — primary `publishctl` + `metactl` symlink for backward compat (#180)
|
||||||
install -d debian/secubox-publish/usr/sbin
|
install -d debian/secubox-publish/usr/sbin
|
||||||
[ -f bin/metactl ] && install -m 755 bin/metactl debian/secubox-publish/usr/sbin/metactl || true
|
[ -f bin/publishctl ] && install -m 755 bin/publishctl debian/secubox-publish/usr/sbin/publishctl || true
|
||||||
|
[ -f debian/secubox-publish/usr/sbin/publishctl ] && \
|
||||||
|
ln -sf publishctl debian/secubox-publish/usr/sbin/metactl || true
|
||||||
# Plugins directory
|
# Plugins directory
|
||||||
install -d debian/secubox-publish/srv/secubox/modules/publish/plugins
|
install -d debian/secubox-publish/srv/secubox/modules/publish/plugins
|
||||||
[ -d plugins ] && cp -r plugins/. debian/secubox-publish/srv/secubox/modules/publish/plugins/ || true
|
[ -d plugins ] && cp -r plugins/. debian/secubox-publish/srv/secubox/modules/publish/plugins/ || true
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
|
secubox-streamforge (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Forge streamforgectl (issue #183). Subcommands: lifecycle (start/stop/
|
||||||
|
restart/status/logs), Three-fold JSON (components/access), project
|
||||||
|
noun (create/remove/list/start/stop/restart/info/templates) wrapping
|
||||||
|
the /api/v1/streamforge/app* endpoints over the Unix socket.
|
||||||
|
* Paired with streamlitctl on the hosting side — forge -> host workflow
|
||||||
|
expressible end-to-end (forge create/edit -> export to git -> stream
|
||||||
|
deploy).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:34:28 +0200
|
||||||
|
|
||||||
secubox-streamforge (1.0.1-1~bookworm1) bookworm; urgency=medium
|
secubox-streamforge (1.0.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add dynamic menu system with menu.d JSON definitions
|
* Add dynamic menu system with menu.d JSON definitions
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-streamforge/etc/nginx/secubox.d
|
install -d debian/secubox-streamforge/etc/nginx/secubox.d
|
||||||
[ -f nginx/streamforge.conf ] && cp nginx/streamforge.conf debian/secubox-streamforge/etc/nginx/secubox.d/ || true
|
[ -f nginx/streamforge.conf ] && cp nginx/streamforge.conf debian/secubox-streamforge/etc/nginx/secubox.d/ || true
|
||||||
|
# streamforgectl (#183)
|
||||||
|
install -d debian/secubox-streamforge/usr/sbin
|
||||||
|
install -m 755 sbin/streamforgectl debian/secubox-streamforge/usr/sbin/
|
||||||
|
|
|
||||||
143
packages/secubox-streamforge/sbin/streamforgectl
Executable file
143
packages/secubox-streamforge/sbin/streamforgectl
Executable file
|
|
@ -0,0 +1,143 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
# streamforgectl — SecuBox StreamForge (Streamlit dev workbench) control (#183)
|
||||||
|
#
|
||||||
|
# Parallel to streamlitctl (the hosting side). The Forge ↔ Streamlit pair:
|
||||||
|
# streamforgectl project create <name> --template hello
|
||||||
|
# streamforgectl project export <name> <gitea_url>
|
||||||
|
# streamlitctl app deploy <name> <gitea_url>
|
||||||
|
# Three verbs, two layers (dev → hosting), one expressible workflow.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="0.1.0"
|
||||||
|
SOCKET="${STREAMFORGE_SOCKET:-/run/secubox/streamforge.sock}"
|
||||||
|
API="http://localhost/api/v1/streamforge"
|
||||||
|
SERVICE="secubox-streamforge.service"
|
||||||
|
|
||||||
|
G='\033[0;32m'; Y='\033[1;33m'; R='\033[0;31m'; N='\033[0m'
|
||||||
|
log() { printf "${G}[FORGE]${N} %s\n" "$*"; }
|
||||||
|
warn() { printf "${Y}[WARN]${N} %s\n" "$*"; }
|
||||||
|
error() { printf "${R}[ERROR]${N} %s\n" "$*" >&2; }
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local m="$1" p="$2"; shift 2
|
||||||
|
[ -S "$SOCKET" ] || { error "socket $SOCKET absent — start $SERVICE"; exit 2; }
|
||||||
|
curl --unix-socket "$SOCKET" -sS -X "$m" -w "\nHTTP_CODE:%{http_code}\n" "${API}${p}" "$@"
|
||||||
|
}
|
||||||
|
api_code() { echo "$1" | grep '^HTTP_CODE:' | cut -d: -f2; }
|
||||||
|
api_body() { echo "$1" | sed '/^HTTP_CODE:/d'; }
|
||||||
|
|
||||||
|
# Lifecycle
|
||||||
|
cmd_start() { systemctl start "$SERVICE" && log started; }
|
||||||
|
cmd_stop() { systemctl stop "$SERVICE" && log stopped; }
|
||||||
|
cmd_restart() { systemctl restart "$SERVICE" && log restarted; }
|
||||||
|
cmd_status() { systemctl is-active "$SERVICE" >/dev/null && echo active || echo inactive; }
|
||||||
|
cmd_logs() { journalctl -u "$SERVICE" -n "${1:-50}" --no-pager; }
|
||||||
|
|
||||||
|
cmd_components() {
|
||||||
|
cat <<EOF
|
||||||
|
{"service":"$SERVICE","socket":"$SOCKET","api_base":"$API","ctl_version":"$VERSION"}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
cmd_access() {
|
||||||
|
cat <<EOF
|
||||||
|
{"socket":"$SOCKET","endpoints":{
|
||||||
|
"apps":"GET /apps","templates":"GET /templates",
|
||||||
|
"create":"POST /app","get":"GET /app/{name}","remove":"DELETE /app/{name}",
|
||||||
|
"start":"POST /app/{name}/start","stop":"POST /app/{name}/stop","restart":"POST /app/{name}/restart",
|
||||||
|
"file_get":"GET /app/{name}/file/{path}","file_put":"PUT /app/{name}/file/{path}"
|
||||||
|
}}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Project (the noun, #183)
|
||||||
|
cmd_project() {
|
||||||
|
local act="${1:-}"; shift || true
|
||||||
|
case "$act" in
|
||||||
|
create) project_create "$@" ;;
|
||||||
|
remove|rm|delete) project_remove "$@" ;;
|
||||||
|
list|ls) project_list "$@" ;;
|
||||||
|
start) project_start "$@" ;;
|
||||||
|
stop) project_stop "$@" ;;
|
||||||
|
restart) project_restart "$@" ;;
|
||||||
|
info) project_info "$@" ;;
|
||||||
|
templates) project_templates ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Project commands:
|
||||||
|
project create <name> [--template hello] [--description "..."]
|
||||||
|
project remove <name>
|
||||||
|
project list
|
||||||
|
project start <name> (start the project's streamlit dev server)
|
||||||
|
project stop <name>
|
||||||
|
project restart <name>
|
||||||
|
project info <name>
|
||||||
|
project templates (list available templates)
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
project_create() {
|
||||||
|
local name="$1"; shift || { error "project create <name> required"; return 1; }
|
||||||
|
local template="hello" desc=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--template) template="$2"; shift ;;
|
||||||
|
--description) desc="$2"; shift ;;
|
||||||
|
*) error "unknown flag: $1"; return 1 ;;
|
||||||
|
esac; shift
|
||||||
|
done
|
||||||
|
log "creating project '$name' from template '$template'"
|
||||||
|
local body
|
||||||
|
body=$(printf '{"name":"%s","template":"%s","description":"%s"}' "$name" "$template" "$desc")
|
||||||
|
local out; out=$(api POST "/app" -H "Content-Type: application/json" -d "$body")
|
||||||
|
[ "$(api_code "$out")" = "200" ] || { error "create failed: $(api_body "$out" | head -2)"; return 1; }
|
||||||
|
log "created"; api_body "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
project_remove() {
|
||||||
|
local name="$1"; [ -z "$name" ] && { error "project remove <name>"; return 1; }
|
||||||
|
local out; out=$(api DELETE "/app/${name}")
|
||||||
|
case "$(api_code "$out")" in
|
||||||
|
200|204) log "removed $name" ;;
|
||||||
|
*) error "remove failed: $(api_body "$out")"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
project_list() { api_body "$(api GET "/apps")"; }
|
||||||
|
|
||||||
|
project_start() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/start")"; }
|
||||||
|
project_stop() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/stop")"; }
|
||||||
|
project_restart() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/restart")"; }
|
||||||
|
project_info() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api GET "/app/${n}")"; }
|
||||||
|
project_templates() { api_body "$(api GET "/templates")"; }
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
SecuBox StreamForge Controller v$VERSION (issue #183)
|
||||||
|
Streamlit dev workbench CLI — paired with streamlitctl (hosting layer)
|
||||||
|
|
||||||
|
Lifecycle: start / stop / restart / status / logs
|
||||||
|
Three-fold: components / access (JSON)
|
||||||
|
Project (#183): project create / remove / list / start / stop / restart / info / templates
|
||||||
|
|
||||||
|
Example workflow (forge → host):
|
||||||
|
streamforgectl project create dashboard --template basic
|
||||||
|
streamforgectl project start dashboard
|
||||||
|
# ...iterate via the webui at /streamforge/...
|
||||||
|
streamforgectl project export dashboard gitea://secubox/dashboard.git # TODO
|
||||||
|
streamlitctl app deploy dashboard gitea://secubox/dashboard.git
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start|stop|restart|status) c="$1"; shift; cmd_$c "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
components) cmd_components ;;
|
||||||
|
access) cmd_access ;;
|
||||||
|
project) shift; cmd_project "$@" ;;
|
||||||
|
help|--help|-h|'') show_help ;;
|
||||||
|
*) error "unknown: $1"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
|
secubox-streamlit (1.2.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* streamlitctl: add `app info <name>` and `app restart <name>` verbs
|
||||||
|
(issue #182). The original audit underestimated the existing ctl
|
||||||
|
surface — app list/start/stop/deploy/remove/logs were already wired.
|
||||||
|
What was actually missing: `info` (metadata + runtime state) and
|
||||||
|
`restart` (stop+start with port preservation from .streamlit.toml).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:36:06 +0200
|
||||||
|
|
||||||
secubox-streamlit (1.2.0-1~bookworm1) bookworm; urgency=medium
|
secubox-streamlit (1.2.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* streamlitctl v1.0.0: Full Debian LXC installation support
|
* streamlitctl v1.0.0: Full Debian LXC installation support
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,51 @@ EOF
|
||||||
echo ']}'
|
echo ']}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# app info <name> — print metadata + runtime state from manifest + pid file (#182)
|
||||||
|
cmd_app_info() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: streamlitctl app info <name>"; return 1; }
|
||||||
|
local d="$APPS_PATH/$name"
|
||||||
|
[ -d "$d" ] || { error "app not found: $name"; return 1; }
|
||||||
|
# Entry point detection (same logic as cmd_app_start)
|
||||||
|
local entry=""
|
||||||
|
for c in app.py main.py streamlit_app.py; do
|
||||||
|
[ -f "$d/$c" ] && entry="$c" && break
|
||||||
|
done
|
||||||
|
local port=""
|
||||||
|
[ -f "$d/.streamlit.toml" ] && port=$(grep -E "^port" "$d/.streamlit.toml" 2>/dev/null | cut -d= -f2 | tr -d ' ')
|
||||||
|
local pidf="/var/run/streamlit-${name}.pid"
|
||||||
|
local pid="" alive="no"
|
||||||
|
if lxc_running; then
|
||||||
|
pid=$(lxc-attach -n "$LXC_NAME" -- cat "$pidf" 2>/dev/null || true)
|
||||||
|
if [ -n "$pid" ]; then
|
||||||
|
lxc-attach -n "$LXC_NAME" -- kill -0 "$pid" >/dev/null 2>&1 && alive="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cat <<EOF
|
||||||
|
name: $name
|
||||||
|
path: $d
|
||||||
|
entrypoint: ${entry:-(none)}
|
||||||
|
port: ${port:-(unset)}
|
||||||
|
pid_file: $pidf
|
||||||
|
pid: ${pid:-(none)}
|
||||||
|
running: $alive
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# app restart <name> — stop + start (#182)
|
||||||
|
cmd_app_restart() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: streamlitctl app restart <name>"; return 1; }
|
||||||
|
cmd_app_stop "$name" || true
|
||||||
|
sleep 1
|
||||||
|
# Recover the previous port from .streamlit.toml so restart preserves it
|
||||||
|
local port=""
|
||||||
|
[ -f "$APPS_PATH/$name/.streamlit.toml" ] && \
|
||||||
|
port=$(grep -E "^port" "$APPS_PATH/$name/.streamlit.toml" 2>/dev/null | cut -d= -f2 | tr -d ' ')
|
||||||
|
cmd_app_start "$name" "${port:-8501}"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_app_start() {
|
cmd_app_start() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local port="${2:-8501}"
|
local port="${2:-8501}"
|
||||||
|
|
@ -694,7 +739,9 @@ case "${1:-}" in
|
||||||
deploy) cmd_app_deploy "$3" "$4" ;;
|
deploy) cmd_app_deploy "$3" "$4" ;;
|
||||||
remove) cmd_app_remove "$3" ;;
|
remove) cmd_app_remove "$3" ;;
|
||||||
logs) cmd_app_logs "$3" "$4" ;;
|
logs) cmd_app_logs "$3" "$4" ;;
|
||||||
*) echo "Usage: streamlitctl app {list|start|stop|deploy|remove|logs} [args]" ;;
|
info) cmd_app_info "$3" ;;
|
||||||
|
restart) cmd_app_restart "$3" ;;
|
||||||
|
*) echo "Usage: streamlitctl app {list|start|stop|restart|deploy|remove|logs|info} [args]" ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user