Compare commits

..

5 Commits

Author SHA1 Message Date
CyberMind
81168ff49a
feat(publishctl): rename metactl -> publishctl + add post noun (closes #180) (#189)
Some checks failed
License Headers / check (push) Failing after 5s
Naming consistency with the rest of the SecuBox CTL grammar:
  haproxyctl / giteactl / mitmproxyctl / dropletctl / metablogizerctl /
  streamlitctl / streamforgectl / publishctl

The old metactl name stays as a symlink so existing scripts keep working.
Added `post` noun dispatch that wraps the existing flat verbs:

  publishctl post upload <file.zip>     (= publishctl upload, kept as alias)
  publishctl post publish <name>
  publishctl post list / download / qrcode / health

This closes the publishing-layer grammar gap and aligns with #181
(dropletctl file), #184 (metablogizerctl site/tor), and the rest of
the modular ctl pattern documented in CLAUDE.md.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:38:46 +02:00
CyberMind
199e52b5cb
feat(streamlitctl): add app info + app restart verbs (closes #182) (#188)
The #182 audit overstated the gap — the existing dispatch already wired
app list/start/stop/deploy/remove/logs (and instance/gitea sub-nouns).
The real missing pieces were:

  app info <name>      Metadata (entrypoint, port, pid) + runtime state
  app restart <name>   Stop + start, preserving port from .streamlit.toml

Both implemented as thin wrappers over existing lifecycle. Existing
verbs left untouched.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:38:44 +02:00
CyberMind
b97e36cdeb
feat(streamforgectl): forge project noun verbs (closes #183) (#187)
streamforgectl is the dev workbench counterpart of streamlitctl (hosting).
Together they express the forge -> host workflow:

  streamforgectl project create dashboard --template basic
  streamforgectl project export dashboard gitea://secubox/dashboard.git   (TODO)
  streamlitctl   app   deploy   dashboard gitea://secubox/dashboard.git   (#182)

Subcommands wrap /api/v1/streamforge/app* endpoints. Three-fold JSON
(components/access) for ecosystem consistency.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:38:41 +02:00
CyberMind
7c273e2132
feat(metablogizerctl): forge tor noun verbs — Emancipate (closes #184) (#186)
Per CLAUDE.md the Punk Exposure Engine has three verbs (Peek/Poke/
Emancipate) and Tor is one of the three exposure channels. metablogizerctl
already handled site create/delete/publish/unpublish/list (the Poke verb
at the publishing layer); this adds the Emancipate verb:

  tor expose <site>   Publish site via Tor hidden service
  tor revoke <site>   Stop publishing via Tor
  tor list            List Tor-exposed sites + onion addresses
  tor status <site>   Stanza + onion + tor service state

When secubox-exposure is installed, the verbs delegate to it for
consistency with other exposure channels (DNS+SSL, Mesh) — one
orchestrator across all three channels. When unavailable, falls back
to direct /etc/tor/secubox-metablogizer.d/<site>.conf stanza writes
and `systemctl reload tor`, reading the resulting .onion hostname from
/var/lib/tor/secubox-metablogizer/<site>/hostname.

The onion address is persisted back into the site's site.json under
exposure.tor so the Peek verb (later, separate ticket) can surface it
without re-running tor status.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:32:24 +02:00
CyberMind
986b18b163
feat(dropletctl): forge file noun verbs (closes #181) (#185)
dropletctl is SecuBox's publishing-layer routing verb, parallel to:
  haproxyctl   vhost  add/remove    (routing)
  mitmproxyctl route  add/remove    (interception, #173)
  giteactl     repo   mirror add    (replication, #176)
  dropletctl   file   upload/...    (publishing, this)

The Droplet API exposes /upload, /list, /remove, /rename over a Unix
socket — operators had to curl them by hand. This ctl wraps them under
a coherent `<noun> <verb>` grammar.

Subcommands:
  Lifecycle (top-level): 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>

debian/rules installs the binary at /usr/sbin/dropletctl. Bumped to
0.1.1.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:32:21 +02:00
13 changed files with 696 additions and 8 deletions

View File

@ -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

View File

@ -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/

View 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

View File

@ -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)

View File

@ -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 ;;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/

View 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

View File

@ -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

View File

@ -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
;;