mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 22:54:31 +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
|
||||
|
||||
* Add dynamic menu system with menu.d JSON definitions
|
||||
|
|
|
|||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
|||
# Modular nginx config
|
||||
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
|
||||
# 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
|
||||
|
||||
* Add metablogizerctl three-fold commands: components, access (JSON output)
|
||||
|
|
|
|||
|
|
@ -194,6 +194,155 @@ site_unpublish() {
|
|||
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() {
|
||||
echo "MetaBlogizer Sites:"
|
||||
echo "==================="
|
||||
|
|
@ -387,6 +536,12 @@ Sites:
|
|||
site unpublish <name> Unpublish site
|
||||
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:
|
||||
migrate [host] Migrate from OpenWrt
|
||||
|
||||
|
|
@ -394,6 +549,7 @@ Examples:
|
|||
metablogizerctl components # JSON components
|
||||
metablogizerctl site create myblog blog.example.com
|
||||
metablogizerctl site publish myblog
|
||||
metablogizerctl tor expose myblog # Emancipate via Tor
|
||||
metablogizerctl migrate 192.168.255.1
|
||||
|
||||
EOF
|
||||
|
|
@ -422,6 +578,8 @@ case "${1:-}" in
|
|||
*) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
|
||||
esac
|
||||
;;
|
||||
# Tor (Emancipate, issue #184)
|
||||
tor) shift; cmd_tor "$@" ;;
|
||||
migrate) shift; cmd_migrate "$@" ;;
|
||||
help|--help|-h|'') show_help ;;
|
||||
*) error "Unknown: $1"; exit 1 ;;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
# SecuBox MetaCtl — ISP Home Publish CLI
|
||||
# CyberMind — https://cybermind.fr
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# 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
|
||||
|
||||
VERSION="1.0.0"
|
||||
VERSION="2.0.0"
|
||||
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}"
|
||||
TOKEN_FILE="${SECUBOX_TOKEN_FILE:-/etc/secubox/secrets/jwt-token}"
|
||||
|
|
@ -408,8 +419,39 @@ cmd_health() {
|
|||
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
|
||||
case "${1:-help}" in
|
||||
# noun-verb grammar (issue #180)
|
||||
post) shift; cmd_post "$@" ;;
|
||||
# flat verbs (backward-compat — same callbacks)
|
||||
upload) shift; cmd_upload "$@" ;;
|
||||
publish) shift; cmd_publish "$@" ;;
|
||||
unpublish) shift; cmd_unpublish "$@" ;;
|
||||
|
|
@ -419,10 +461,10 @@ case "${1:-help}" in
|
|||
status) cmd_status ;;
|
||||
health) shift; cmd_health "$@" ;;
|
||||
-h|--help|help) usage ;;
|
||||
-v|--version) echo "metactl v${VERSION}" ;;
|
||||
-v|--version) echo "publishctl v${VERSION}" ;;
|
||||
*)
|
||||
echo -e "${RED}Unknown command:${NC} $1"
|
||||
echo "Run 'metactl --help' for usage"
|
||||
echo "Run 'publishctl --help' for usage"
|
||||
exit 1
|
||||
;;
|
||||
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
|
||||
|
||||
* Initial release
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ override_dh_auto_install:
|
|||
# Modular nginx config
|
||||
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
|
||||
# CLI tool
|
||||
# CLI tool — primary `publishctl` + `metactl` symlink for backward compat (#180)
|
||||
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
|
||||
install -d debian/secubox-publish/srv/secubox/modules/publish/plugins
|
||||
[ -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
|
||||
|
||||
* Add dynamic menu system with menu.d JSON definitions
|
||||
|
|
|
|||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
|||
# Modular nginx config
|
||||
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
|
||||
# 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
|
||||
|
||||
* streamlitctl v1.0.0: Full Debian LXC installation support
|
||||
|
|
|
|||
|
|
@ -318,6 +318,51 @@ EOF
|
|||
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() {
|
||||
local name="$1"
|
||||
local port="${2:-8501}"
|
||||
|
|
@ -694,7 +739,9 @@ case "${1:-}" in
|
|||
deploy) cmd_app_deploy "$3" "$4" ;;
|
||||
remove) cmd_app_remove "$3" ;;
|
||||
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
|
||||
;;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user