Refactored cli to make things more clear and easy to understand for developers

This commit is contained in:
anonpenguin23 2026-02-16 10:31:17 +02:00
parent 7163aad850
commit 865a4f3434
43 changed files with 1028 additions and 768 deletions

View File

@ -75,7 +75,7 @@ build: deps
@mkdir -p bin
go build -ldflags "$(LDFLAGS)" -o bin/identity ./cmd/identity
go build -ldflags "$(LDFLAGS)" -o bin/orama-node ./cmd/node
go build -ldflags "$(LDFLAGS)" -o bin/orama cmd/cli/main.go
go build -ldflags "$(LDFLAGS)" -o bin/orama ./cmd/cli/
# Inject gateway build metadata via pkg path variables
go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin/gateway ./cmd/gateway
@echo "Build complete! Run ./bin/orama version"
@ -84,7 +84,7 @@ build: deps
build-linux: deps
@echo "Cross-compiling CLI for linux/amd64 (version=$(VERSION))..."
@mkdir -p bin-linux
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/orama cmd/cli/main.go
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/orama ./cmd/cli/
@echo "✓ CLI built at bin-linux/orama"
@echo ""
@echo "Next steps:"

View File

@ -1,223 +1,5 @@
package main
import (
"fmt"
"os"
"time"
"github.com/DeBrosOfficial/network/pkg/cli"
)
var (
timeout = 30 * time.Second
format = "table"
)
// version metadata populated via -ldflags at build time
var (
version = "dev"
commit = ""
date = ""
)
func main() {
if len(os.Args) < 2 {
showHelp()
return
}
command := os.Args[1]
args := os.Args[2:]
// Parse global flags
parseGlobalFlags(args)
switch command {
case "version":
fmt.Printf("orama %s", version)
if commit != "" {
fmt.Printf(" (commit %s)", commit)
}
if date != "" {
fmt.Printf(" built %s", date)
}
fmt.Println()
return
// Production environment commands (legacy with 'prod' prefix)
case "prod":
cli.HandleProdCommand(args)
// Direct production commands (new simplified interface)
case "invite":
cli.HandleProdCommand(append([]string{"invite"}, args...))
case "install":
cli.HandleProdCommand(append([]string{"install"}, args...))
case "upgrade":
cli.HandleProdCommand(append([]string{"upgrade"}, args...))
case "migrate":
cli.HandleProdCommand(append([]string{"migrate"}, args...))
case "status":
cli.HandleProdCommand(append([]string{"status"}, args...))
case "start":
cli.HandleProdCommand(append([]string{"start"}, args...))
case "stop":
cli.HandleProdCommand(append([]string{"stop"}, args...))
case "restart":
cli.HandleProdCommand(append([]string{"restart"}, args...))
case "logs":
cli.HandleProdCommand(append([]string{"logs"}, args...))
case "uninstall":
cli.HandleProdCommand(append([]string{"uninstall"}, args...))
// Authentication commands
case "auth":
cli.HandleAuthCommand(args)
// Deployment commands
case "deploy":
cli.HandleDeployCommand(args)
case "deployments":
cli.HandleDeploymentsCommand(args)
// Database commands
case "db":
cli.HandleDBCommand(args)
// Cluster management
case "cluster":
cli.HandleClusterCommand(args)
// Cluster inspection
case "inspect":
cli.HandleInspectCommand(args)
// Namespace management
case "namespace":
cli.HandleNamespaceCommand(args)
// Environment management
case "env":
cli.HandleEnvCommand(args)
// Help
case "help", "--help", "-h":
showHelp()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
showHelp()
os.Exit(1)
}
}
func parseGlobalFlags(args []string) {
for i, arg := range args {
switch arg {
case "-f", "--format":
if i+1 < len(args) {
format = args[i+1]
}
case "-t", "--timeout":
if i+1 < len(args) {
if d, err := time.ParseDuration(args[i+1]); err == nil {
timeout = d
}
}
}
}
}
func showHelp() {
fmt.Printf("Orama CLI - Distributed P2P Network Management Tool\n\n")
fmt.Printf("Usage: orama <command> [args...]\n\n")
fmt.Printf("🚀 Production Deployment:\n")
fmt.Printf(" install - Install production node (requires root/sudo)\n")
fmt.Printf(" upgrade - Upgrade existing installation\n")
fmt.Printf(" status - Show production service status\n")
fmt.Printf(" start - Start all production services (requires root/sudo)\n")
fmt.Printf(" stop - Stop all production services (requires root/sudo)\n")
fmt.Printf(" restart - Restart all production services (requires root/sudo)\n")
fmt.Printf(" logs <service> - View production service logs\n")
fmt.Printf(" uninstall - Remove production services (requires root/sudo)\n\n")
fmt.Printf("🔐 Authentication:\n")
fmt.Printf(" auth login - Authenticate with wallet\n")
fmt.Printf(" auth logout - Clear stored credentials\n")
fmt.Printf(" auth whoami - Show current authentication\n")
fmt.Printf(" auth status - Show detailed auth info\n")
fmt.Printf(" auth help - Show auth command help\n\n")
fmt.Printf("📦 Deployments:\n")
fmt.Printf(" deploy static <path> - Deploy a static site (React, Vue, etc.)\n")
fmt.Printf(" deploy nextjs <path> - Deploy a Next.js application\n")
fmt.Printf(" deploy go <path> - Deploy a Go backend\n")
fmt.Printf(" deploy nodejs <path> - Deploy a Node.js backend\n")
fmt.Printf(" deployments list - List all deployments\n")
fmt.Printf(" deployments get <name> - Get deployment details\n")
fmt.Printf(" deployments logs <name> - View deployment logs\n")
fmt.Printf(" deployments delete <name> - Delete a deployment\n")
fmt.Printf(" deployments rollback <name> - Rollback to previous version\n\n")
fmt.Printf("🗄️ Databases:\n")
fmt.Printf(" db create <name> - Create a SQLite database\n")
fmt.Printf(" db query <name> \"<sql>\" - Execute SQL query\n")
fmt.Printf(" db list - List all databases\n")
fmt.Printf(" db backup <name> - Backup database to IPFS\n")
fmt.Printf(" db backups <name> - List database backups\n\n")
fmt.Printf("🏢 Namespaces:\n")
fmt.Printf(" namespace delete - Delete current namespace and all resources\n")
fmt.Printf(" namespace repair <name> - Repair under-provisioned cluster (add missing nodes)\n\n")
fmt.Printf("🔧 Cluster Management:\n")
fmt.Printf(" cluster status - Show cluster node status\n")
fmt.Printf(" cluster health - Run cluster health checks\n")
fmt.Printf(" cluster rqlite status - Show detailed Raft state\n")
fmt.Printf(" cluster rqlite voters - Show voter list\n")
fmt.Printf(" cluster rqlite backup - Trigger manual backup\n")
fmt.Printf(" cluster watch - Live cluster status monitor\n\n")
fmt.Printf("🔍 Cluster Inspection:\n")
fmt.Printf(" inspect - Inspect cluster health via SSH\n")
fmt.Printf(" inspect --env devnet - Inspect devnet nodes\n")
fmt.Printf(" inspect --subsystem rqlite - Inspect only RQLite subsystem\n")
fmt.Printf(" inspect --format json - Output as JSON\n\n")
fmt.Printf("🌍 Environments:\n")
fmt.Printf(" env list - List all environments\n")
fmt.Printf(" env current - Show current environment\n")
fmt.Printf(" env switch <name> - Switch to environment\n\n")
fmt.Printf("Global Flags:\n")
fmt.Printf(" -f, --format <format> - Output format: table, json (default: table)\n")
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n")
fmt.Printf(" --help, -h - Show this help message\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" # Deploy a React app\n")
fmt.Printf(" cd my-react-app && npm run build\n")
fmt.Printf(" orama deploy static ./dist --name my-app\n\n")
fmt.Printf(" # Deploy a Next.js app with SSR\n")
fmt.Printf(" cd my-nextjs-app && npm run build\n")
fmt.Printf(" orama deploy nextjs . --name my-nextjs --ssr\n\n")
fmt.Printf(" # Create and use a database\n")
fmt.Printf(" orama db create my-db\n")
fmt.Printf(" orama db query my-db \"CREATE TABLE users (id INT, name TEXT)\"\n")
fmt.Printf(" orama db query my-db \"INSERT INTO users VALUES (1, 'Alice')\"\n\n")
fmt.Printf(" # Manage deployments\n")
fmt.Printf(" orama deployments list\n")
fmt.Printf(" orama deployments get my-app\n")
fmt.Printf(" orama deployments logs my-app --follow\n\n")
fmt.Printf(" # First node (creates new cluster)\n")
fmt.Printf(" sudo orama install --vps-ip 203.0.113.1 --domain node-1.orama.network\n\n")
fmt.Printf(" # Service management\n")
fmt.Printf(" orama status\n")
fmt.Printf(" orama logs node --follow\n")
runCLI()
}

87
cmd/cli/root.go Normal file
View File

@ -0,0 +1,87 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
// Command groups
"github.com/DeBrosOfficial/network/pkg/cli/cmd/app"
"github.com/DeBrosOfficial/network/pkg/cli/cmd/authcmd"
"github.com/DeBrosOfficial/network/pkg/cli/cmd/dbcmd"
deploycmd "github.com/DeBrosOfficial/network/pkg/cli/cmd/deploy"
"github.com/DeBrosOfficial/network/pkg/cli/cmd/envcmd"
"github.com/DeBrosOfficial/network/pkg/cli/cmd/inspectcmd"
"github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd"
"github.com/DeBrosOfficial/network/pkg/cli/cmd/node"
)
// version metadata populated via -ldflags at build time
// Must match Makefile: -X 'main.version=...' -X 'main.commit=...' -X 'main.date=...'
var (
version = "dev"
commit = ""
date = ""
)
func newRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "orama",
Short: "Orama CLI - Distributed P2P Network Management Tool",
Long: `Orama CLI is a tool for managing nodes, deploying applications,
and interacting with the Orama distributed network.`,
SilenceUsage: true,
SilenceErrors: true,
}
// Version command
rootCmd.AddCommand(&cobra.Command{
Use: "version",
Short: "Show version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("orama %s", version)
if commit != "" {
fmt.Printf(" (commit %s)", commit)
}
if date != "" {
fmt.Printf(" built %s", date)
}
fmt.Println()
},
})
// Node operator commands (was "prod")
rootCmd.AddCommand(node.Cmd)
// Deploy command (top-level, upsert)
rootCmd.AddCommand(deploycmd.Cmd)
// App management (was "deployments")
rootCmd.AddCommand(app.Cmd)
// Database commands
rootCmd.AddCommand(dbcmd.Cmd)
// Namespace commands
rootCmd.AddCommand(namespacecmd.Cmd)
// Environment commands
rootCmd.AddCommand(envcmd.Cmd)
// Auth commands
rootCmd.AddCommand(authcmd.Cmd)
// Inspect command
rootCmd.AddCommand(inspectcmd.Cmd)
return rootCmd
}
func runCLI() {
rootCmd := newRootCmd()
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -163,7 +163,7 @@ orama deploy nextjs ./nextjs.tar.gz --name my-nextjs --ssr
# URLs:
# • https://my-nextjs.orama.network
#
# ⚠️ Note: SSR deployment may take a minute to start. Check status with: orama deployments get my-nextjs
# ⚠️ Note: SSR deployment may take a minute to start. Check status with: orama app get my-nextjs
```
### What Happens Behind the Scenes
@ -795,7 +795,7 @@ Open your browser to:
### List All Deployments
```bash
orama deployments list
orama app list
# Output:
# NAME TYPE STATUS VERSION CREATED
@ -809,7 +809,7 @@ orama deployments list
### Get Deployment Details
```bash
orama deployments get my-react-app
orama app get my-react-app
# Output:
# Deployment: my-react-app
@ -835,17 +835,17 @@ orama deployments get my-react-app
```bash
# View last 100 lines
orama deployments logs my-nextjs
orama app logs my-nextjs
# Follow logs in real-time
orama deployments logs my-nextjs --follow
orama app logs my-nextjs --follow
```
### Rollback to Previous Version
```bash
# Rollback to version 1
orama deployments rollback my-nextjs --version 1
orama app rollback my-nextjs --version 1
# Output:
# ⚠️ Rolling back 'my-nextjs' to version 1. Continue? (y/N): y
@ -862,7 +862,7 @@ orama deployments rollback my-nextjs --version 1
### Delete Deployment
```bash
orama deployments delete my-old-app
orama app delete my-old-app
# Output:
# ⚠️ Are you sure you want to delete deployment 'my-old-app'? (y/N): y
@ -880,10 +880,10 @@ orama deployments delete my-old-app
```bash
# Check deployment details
orama deployments get my-app
orama app get my-app
# View logs for errors
orama deployments logs my-app
orama app logs my-app
# Common issues:
# - Binary not compiled for Linux (GOOS=linux GOARCH=amd64)
@ -896,7 +896,7 @@ orama deployments logs my-app
```bash
# 1. Check deployment status
orama deployments get my-app
orama app get my-app
# 2. Verify DNS (may take up to 10 seconds to propagate)
dig my-app.orama.network
@ -980,7 +980,7 @@ orama auth status
- **Explore the API**: See `/docs/GATEWAY_API.md` for HTTP API details
- **Advanced Features**: Custom domains, load balancing, autoscaling (coming soon)
- **Production Deployment**: Install nodes with `orama install` for production clusters
- **Production Deployment**: Install nodes with `orama node install` for production clusters
- **Client SDK**: Use the Go/JS SDK for programmatic deployments
---

View File

@ -40,16 +40,16 @@ make build-linux
# Creates: /tmp/network-source.tar.gz
# 3. Install on a new VPS (handles SCP, extract, and remote install automatically)
./bin/orama install --vps-ip <ip> --nameserver --domain <domain> --base-domain <domain>
./bin/orama node install --vps-ip <ip> --nameserver --domain <domain> --base-domain <domain>
# Or upgrade an existing VPS
./bin/orama upgrade --restart
./bin/orama node upgrade --restart
```
The `orama install` command automatically:
The `orama node install` command automatically:
1. Uploads the source archive via SCP
2. Extracts source to `/opt/orama/src` and installs the CLI to `/usr/local/bin/orama`
3. Runs `orama install` on the VPS which builds all binaries from source (Go, CoreDNS, Caddy, Olric, etc.)
3. Runs `orama node install` on the VPS which builds all binaries from source (Go, CoreDNS, Caddy, Olric, etc.)
### Upgrading a Multi-Node Cluster (CRITICAL)
@ -84,7 +84,7 @@ done
ssh ubuntu@<any-node> 'curl -s http://localhost:5001/status | jq -r .store.raft.state'
# 6. Upgrade FOLLOWER nodes one at a time
ssh ubuntu@<follower-ip> 'sudo orama prod stop && sudo orama upgrade --restart'
ssh ubuntu@<follower-ip> 'sudo orama node stop && sudo orama node upgrade --restart'
# Wait for rejoin before proceeding to next node
ssh ubuntu@<leader-ip> 'curl -s http://localhost:5001/status | jq -r .store.raft.num_peers'
@ -93,13 +93,13 @@ ssh ubuntu@<leader-ip> 'curl -s http://localhost:5001/status | jq -r .store.raft
# Repeat for each follower...
# 7. Upgrade the LEADER node last
ssh ubuntu@<leader-ip> 'sudo orama prod stop && sudo orama upgrade --restart'
ssh ubuntu@<leader-ip> 'sudo orama node stop && sudo orama node upgrade --restart'
```
#### What NOT to Do
- **DON'T** stop all nodes, replace binaries, then start all nodes
- **DON'T** run `orama upgrade --restart` on multiple nodes in parallel
- **DON'T** run `orama node upgrade --restart` on multiple nodes in parallel
- **DON'T** clear RQLite data directories unless doing a full cluster rebuild
- **DON'T** use `systemctl stop orama-node` on multiple nodes simultaneously
@ -111,7 +111,7 @@ If nodes get stuck in "Candidate" state or show "leader not found" errors:
2. Keep that node running as the new leader
3. On each other node, clear RQLite data and restart:
```bash
sudo orama prod stop
sudo orama node stop
sudo rm -rf /opt/orama/.orama/data/rqlite
sudo systemctl start orama-node
```
@ -135,7 +135,7 @@ To deploy to all nodes, repeat steps 3-5 (dev) or 3-4 (production) for each VPS
### CLI Flags Reference
#### `orama install`
#### `orama node install`
| Flag | Description |
|------|-------------|
@ -144,7 +144,7 @@ To deploy to all nodes, repeat steps 3-5 (dev) or 3-4 (production) for each VPS
| `--base-domain <domain>` | Base domain for deployment routing (e.g., example.com) |
| `--nameserver` | Configure this node as a nameserver (CoreDNS + Caddy) |
| `--join <url>` | Join existing cluster via HTTPS URL (e.g., `https://node1.example.com`) |
| `--token <token>` | Invite token for joining (from `orama invite` on existing node) |
| `--token <token>` | Invite token for joining (from `orama node invite` on existing node) |
| `--force` | Force reconfiguration even if already installed |
| `--skip-firewall` | Skip UFW firewall setup |
| `--skip-checks` | Skip minimum resource checks (RAM/CPU) |
@ -159,7 +159,7 @@ To deploy to all nodes, repeat steps 3-5 (dev) or 3-4 (production) for each VPS
| `--anyone-bandwidth <pct>` | Limit relay to N% of VPS bandwidth (default: 30, 0=unlimited). Runs a speedtest during install to measure available bandwidth |
| `--anyone-accounting <GB>` | Monthly data cap for relay in GB (0=unlimited) |
#### `orama invite`
#### `orama node invite`
| Flag | Description |
|------|-------------|
@ -171,7 +171,7 @@ To deploy to all nodes, repeat steps 3-5 (dev) or 3-4 (production) for each VPS
- **Expiry is checked in UTC.** RQLite uses `datetime('now')` which is always UTC. If your local timezone differs, account for the offset when choosing expiry durations.
- **Use longer expiry for multi-node deployments.** When deploying multiple nodes, use `--expiry 24h` to avoid tokens expiring mid-deployment.
#### `orama upgrade`
#### `orama node upgrade`
| Flag | Description |
|------|-------------|
@ -180,41 +180,44 @@ To deploy to all nodes, repeat steps 3-5 (dev) or 3-4 (production) for each VPS
| `--anyone-bandwidth <pct>` | Limit relay to N% of VPS bandwidth (default: 30, 0=unlimited) |
| `--anyone-accounting <GB>` | Monthly data cap for relay in GB (0=unlimited) |
#### `orama prod` (Service Management)
#### `orama node` (Service Management)
Use these commands to manage services on production nodes:
```bash
# Stop all services (orama-node, coredns, caddy)
sudo orama prod stop
sudo orama node stop
# Start all services
sudo orama prod start
sudo orama node start
# Restart all services
sudo orama prod restart
sudo orama node restart
# Check service status
sudo orama prod status
sudo orama node status
# Diagnose common issues
sudo orama node doctor
```
**Note:** Always use `orama prod stop` instead of manually running `systemctl stop`. The CLI ensures all related services (including CoreDNS and Caddy on nameserver nodes) are handled correctly.
**Note:** Always use `orama node stop` instead of manually running `systemctl stop`. The CLI ensures all related services (including CoreDNS and Caddy on nameserver nodes) are handled correctly.
### Node Join Flow
```bash
# 1. Genesis node (first node, creates cluster)
# Nameserver nodes use the base domain as --domain
sudo orama install --vps-ip 1.2.3.4 --domain example.com \
sudo orama node install --vps-ip 1.2.3.4 --domain example.com \
--base-domain example.com --nameserver
# 2. On genesis node, generate an invite
orama invite
# Output: sudo orama install --join https://example.com --token <TOKEN> --vps-ip <IP>
orama node invite
# Output: sudo orama node install --join https://example.com --token <TOKEN> --vps-ip <IP>
# 3. On the new node, run the printed command
# Nameserver nodes use the base domain; non-nameserver nodes use subdomains (e.g., node-4.example.com)
sudo orama install --join https://example.com --token abc123... \
sudo orama node install --join https://example.com --token abc123... \
--vps-ip 5.6.7.8 --domain example.com --base-domain example.com --nameserver
```
@ -231,7 +234,7 @@ node's IP so that `node1.example.com` resolves publicly.
**If DNS is not yet configured**, you can use the genesis node's public IP with HTTP as a fallback:
```bash
sudo orama install --join http://1.2.3.4 --vps-ip 5.6.7.8 --token abc123... --nameserver
sudo orama node install --join http://1.2.3.4 --vps-ip 5.6.7.8 --token abc123... --nameserver
```
This works because Caddy's `:80` block proxies all HTTP traffic to the gateway. However, once DNS
@ -243,7 +246,7 @@ which proxies to the gateway internally.
## Pre-Install Checklist
Before running `orama install` on a VPS, ensure:
Before running `orama node install` on a VPS, ensure:
1. **Stop Docker if running.** Docker commonly binds ports 4001 and 8080 which conflict with IPFS. The installer checks for port conflicts and shows which process is using each port, but it's easier to stop Docker first:
```bash

View File

@ -1,423 +0,0 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"time"
"github.com/DeBrosOfficial/network/pkg/auth"
"github.com/DeBrosOfficial/network/pkg/client"
)
// HandleHealthCommand handles the health command
func HandleHealthCommand(format string, timeout time.Duration) {
cli, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer cli.Disconnect()
health, err := cli.Health()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get health: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(health)
} else {
printHealth(health)
}
}
// HandlePeersCommand handles the peers command
func HandlePeersCommand(format string, timeout time.Duration) {
cli, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer cli.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
peers, err := cli.Network().GetPeers(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get peers: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(peers)
} else {
printPeers(peers)
}
}
// HandleStatusCommand handles the status command
func HandleStatusCommand(format string, timeout time.Duration) {
cli, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer cli.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
status, err := cli.Network().GetStatus(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get status: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(status)
} else {
printStatus(status)
}
}
// HandleQueryCommand handles the query command
func HandleQueryCommand(sql, format string, timeout time.Duration) {
// Ensure user is authenticated
_ = ensureAuthenticated()
cli, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer cli.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
result, err := cli.Database().Query(ctx, sql)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to execute query: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(result)
} else {
printQueryResult(result)
}
}
// HandleConnectCommand handles the connect command
func HandleConnectCommand(peerAddr string, timeout time.Duration) {
cli, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer cli.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = cli.Network().ConnectToPeer(ctx, peerAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to peer: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Connected to peer: %s\n", peerAddr)
}
// HandlePeerIDCommand handles the peer-id command
func HandlePeerIDCommand(format string, timeout time.Duration) {
cli, err := createClient()
if err == nil {
defer cli.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if status, err := cli.Network().GetStatus(ctx); err == nil {
if format == "json" {
printJSON(map[string]string{"peer_id": status.NodeID})
} else {
fmt.Printf("🆔 Peer ID: %s\n", status.NodeID)
}
return
}
}
fmt.Fprintf(os.Stderr, "❌ Could not find peer ID. Make sure the node is running or identity files exist.\n")
os.Exit(1)
}
// HandlePubSubCommand handles pubsub commands
func HandlePubSubCommand(args []string, format string, timeout time.Duration) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: orama pubsub <publish|subscribe|topics> [args...]\n")
os.Exit(1)
}
// Ensure user is authenticated
_ = ensureAuthenticated()
cli, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer cli.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
subcommand := args[0]
switch subcommand {
case "publish":
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: orama pubsub publish <topic> <message>\n")
os.Exit(1)
}
err := cli.PubSub().Publish(ctx, args[1], []byte(args[2]))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to publish message: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Published message to topic: %s\n", args[1])
case "subscribe":
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: orama pubsub subscribe <topic> [duration]\n")
os.Exit(1)
}
duration := 30 * time.Second
if len(args) > 2 {
if d, err := time.ParseDuration(args[2]); err == nil {
duration = d
}
}
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
fmt.Printf("🔔 Subscribing to topic '%s' for %v...\n", args[1], duration)
messageHandler := func(topic string, data []byte) error {
fmt.Printf("📨 [%s] %s: %s\n", time.Now().Format("15:04:05"), topic, string(data))
return nil
}
err := cli.PubSub().Subscribe(ctx, args[1], messageHandler)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to subscribe: %v\n", err)
os.Exit(1)
}
<-ctx.Done()
fmt.Printf("✅ Subscription ended\n")
case "topics":
topics, err := cli.PubSub().ListTopics(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list topics: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(topics)
} else {
for _, topic := range topics {
fmt.Println(topic)
}
}
default:
fmt.Fprintf(os.Stderr, "Unknown pubsub command: %s\n", subcommand)
os.Exit(1)
}
}
// Helper functions
func createClient() (client.NetworkClient, error) {
config := client.DefaultClientConfig("orama")
// Use active environment's gateway URL
gatewayURL := getGatewayURL()
config.GatewayURL = gatewayURL
// Try to get peer configuration from active environment
env, err := GetActiveEnvironment()
if err == nil && env != nil {
// Environment loaded successfully - gateway URL already set above
_ = env // Reserve for future peer configuration
}
// Check for existing credentials using enhanced authentication
creds, err := auth.GetValidEnhancedCredentials()
if err != nil {
// No valid credentials found, use the enhanced authentication flow
newCreds, authErr := auth.GetOrPromptForCredentials(gatewayURL)
if authErr != nil {
return nil, fmt.Errorf("authentication failed: %w", authErr)
}
creds = newCreds
}
// Configure client with API key
config.APIKey = creds.APIKey
// Update last used time - the enhanced store handles saving automatically
creds.UpdateLastUsed()
networkClient, err := client.NewClient(config)
if err != nil {
return nil, err
}
if err := networkClient.Connect(); err != nil {
return nil, err
}
return networkClient, nil
}
func ensureAuthenticated() *auth.Credentials {
gatewayURL := getGatewayURL()
credentials, err := auth.GetOrPromptForCredentials(gatewayURL)
if err != nil {
fmt.Fprintf(os.Stderr, "Authentication failed: %v\n", err)
os.Exit(1)
}
return credentials
}
func printHealth(health *client.HealthStatus) {
fmt.Printf("🏥 Network Health\n")
fmt.Printf("Status: %s\n", getStatusEmoji(health.Status)+health.Status)
fmt.Printf("Last Updated: %s\n", health.LastUpdated.Format("2006-01-02 15:04:05"))
fmt.Printf("Response Time: %v\n", health.ResponseTime)
fmt.Printf("\nChecks:\n")
for check, status := range health.Checks {
emoji := "✅"
if status != "ok" {
emoji = "❌"
}
fmt.Printf(" %s %s: %s\n", emoji, check, status)
}
}
func printPeers(peers []client.PeerInfo) {
fmt.Printf("👥 Connected Peers (%d)\n\n", len(peers))
if len(peers) == 0 {
fmt.Printf("No peers connected\n")
return
}
for i, peer := range peers {
connEmoji := "🔴"
if peer.Connected {
connEmoji = "🟢"
}
fmt.Printf("%d. %s %s\n", i+1, connEmoji, peer.ID)
fmt.Printf(" Addresses: %v\n", peer.Addresses)
fmt.Printf(" Last Seen: %s\n", peer.LastSeen.Format("2006-01-02 15:04:05"))
fmt.Println()
}
}
func printStatus(status *client.NetworkStatus) {
fmt.Printf("🌐 Network Status\n")
fmt.Printf("Node ID: %s\n", status.NodeID)
fmt.Printf("Connected: %s\n", getBoolEmoji(status.Connected)+strconv.FormatBool(status.Connected))
fmt.Printf("Peer Count: %d\n", status.PeerCount)
fmt.Printf("Database Size: %s\n", formatBytes(status.DatabaseSize))
fmt.Printf("Uptime: %v\n", status.Uptime.Round(time.Second))
}
func printQueryResult(result *client.QueryResult) {
fmt.Printf("📊 Query Result\n")
fmt.Printf("Rows: %d\n\n", result.Count)
if len(result.Rows) == 0 {
fmt.Printf("No data returned\n")
return
}
// Print header
for i, col := range result.Columns {
if i > 0 {
fmt.Printf(" | ")
}
fmt.Printf("%-15s", col)
}
fmt.Println()
// Print separator
for i := range result.Columns {
if i > 0 {
fmt.Printf("-+-")
}
fmt.Printf("%-15s", "---------------")
}
fmt.Println()
// Print rows
for _, row := range result.Rows {
for i, cell := range row {
if i > 0 {
fmt.Printf(" | ")
}
fmt.Printf("%-15v", cell)
}
fmt.Println()
}
}
func printJSON(data interface{}) {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to marshal JSON: %v\n", err)
return
}
fmt.Println(string(jsonData))
}
func getStatusEmoji(status string) string {
switch status {
case "healthy":
return "🟢 "
case "degraded":
return "🟡 "
case "unhealthy":
return "🔴 "
default:
return "⚪ "
}
}
func getBoolEmoji(b bool) string {
if b {
return "✅ "
}
return "❌ "
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}

View File

@ -1,10 +0,0 @@
package cli
import (
"github.com/DeBrosOfficial/network/pkg/cli/cluster"
)
// HandleClusterCommand handles cluster management commands.
func HandleClusterCommand(args []string) {
cluster.HandleCommand(args)
}

23
pkg/cli/cmd/app/app.go Normal file
View File

@ -0,0 +1,23 @@
package app
import (
"github.com/DeBrosOfficial/network/pkg/cli/deployments"
"github.com/spf13/cobra"
)
// Cmd is the root command for managing deployed applications (was "deployments").
var Cmd = &cobra.Command{
Use: "app",
Aliases: []string{"apps"},
Short: "Manage deployed applications",
Long: `List, get, delete, rollback, and view logs/stats for your deployed applications.`,
}
func init() {
Cmd.AddCommand(deployments.ListCmd)
Cmd.AddCommand(deployments.GetCmd)
Cmd.AddCommand(deployments.DeleteCmd)
Cmd.AddCommand(deployments.RollbackCmd)
Cmd.AddCommand(deployments.LogsCmd)
Cmd.AddCommand(deployments.StatsCmd)
}

View File

@ -0,0 +1,72 @@
package authcmd
import (
"github.com/DeBrosOfficial/network/pkg/cli"
"github.com/spf13/cobra"
)
// Cmd is the root command for authentication.
var Cmd = &cobra.Command{
Use: "auth",
Short: "Authentication management",
Long: `Manage authentication with the Orama network.
Supports RootWallet (EVM) and Phantom (Solana) authentication methods.`,
}
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate with wallet",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleAuthCommand(append([]string{"login"}, args...))
},
DisableFlagParsing: true,
}
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Clear stored credentials",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleAuthCommand([]string{"logout"})
},
}
var whoamiCmd = &cobra.Command{
Use: "whoami",
Short: "Show current authentication status",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleAuthCommand([]string{"whoami"})
},
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "Show detailed authentication info",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleAuthCommand([]string{"status"})
},
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List all stored credentials",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleAuthCommand([]string{"list"})
},
}
var switchCmd = &cobra.Command{
Use: "switch",
Short: "Switch between stored credentials",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleAuthCommand([]string{"switch"})
},
}
func init() {
Cmd.AddCommand(loginCmd)
Cmd.AddCommand(logoutCmd)
Cmd.AddCommand(whoamiCmd)
Cmd.AddCommand(statusCmd)
Cmd.AddCommand(listCmd)
Cmd.AddCommand(switchCmd)
}

View File

@ -0,0 +1,74 @@
package cluster
import (
origCluster "github.com/DeBrosOfficial/network/pkg/cli/cluster"
"github.com/spf13/cobra"
)
// Cmd is the root command for cluster operations (flattened from cluster rqlite).
var Cmd = &cobra.Command{
Use: "cluster",
Short: "Cluster management and diagnostics",
Long: `View cluster status, run health checks, manage RQLite Raft state,
and monitor the cluster in real-time.`,
}
var statusSubCmd = &cobra.Command{
Use: "status",
Short: "Show cluster node status (RQLite + Olric)",
Run: func(cmd *cobra.Command, args []string) {
origCluster.HandleStatus(args)
},
}
var healthSubCmd = &cobra.Command{
Use: "health",
Short: "Run cluster health checks",
Run: func(cmd *cobra.Command, args []string) {
origCluster.HandleHealth(args)
},
}
var watchSubCmd = &cobra.Command{
Use: "watch",
Short: "Live cluster status monitor",
Run: func(cmd *cobra.Command, args []string) {
origCluster.HandleWatch(args)
},
DisableFlagParsing: true,
}
// Flattened rqlite commands (was cluster rqlite <cmd>)
var raftStatusCmd = &cobra.Command{
Use: "raft-status",
Short: "Show detailed Raft state for local node",
Run: func(cmd *cobra.Command, args []string) {
origCluster.HandleRQLite([]string{"status"})
},
}
var votersCmd = &cobra.Command{
Use: "voters",
Short: "Show current voter list",
Run: func(cmd *cobra.Command, args []string) {
origCluster.HandleRQLite([]string{"voters"})
},
}
var backupCmd = &cobra.Command{
Use: "backup",
Short: "Trigger manual RQLite backup",
Run: func(cmd *cobra.Command, args []string) {
origCluster.HandleRQLite(append([]string{"backup"}, args...))
},
DisableFlagParsing: true,
}
func init() {
Cmd.AddCommand(statusSubCmd)
Cmd.AddCommand(healthSubCmd)
Cmd.AddCommand(watchSubCmd)
Cmd.AddCommand(raftStatusCmd)
Cmd.AddCommand(votersCmd)
Cmd.AddCommand(backupCmd)
}

21
pkg/cli/cmd/dbcmd/db.go Normal file
View File

@ -0,0 +1,21 @@
package dbcmd
import (
"github.com/DeBrosOfficial/network/pkg/cli/db"
"github.com/spf13/cobra"
)
// Cmd is the root command for database operations.
var Cmd = &cobra.Command{
Use: "db",
Short: "Manage SQLite databases",
Long: `Create and manage per-namespace SQLite databases.`,
}
func init() {
Cmd.AddCommand(db.CreateCmd)
Cmd.AddCommand(db.QueryCmd)
Cmd.AddCommand(db.ListCmd)
Cmd.AddCommand(db.BackupCmd)
Cmd.AddCommand(db.BackupsCmd)
}

View File

@ -0,0 +1,21 @@
package deploy
import (
"github.com/DeBrosOfficial/network/pkg/cli/deployments"
"github.com/spf13/cobra"
)
// Cmd is the top-level deploy command (upsert: create or update).
var Cmd = &cobra.Command{
Use: "deploy",
Short: "Deploy applications to the Orama network",
Long: `Deploy static sites, Next.js apps, Go backends, and Node.js backends.
If a deployment with the same name exists, it will be updated.`,
}
func init() {
Cmd.AddCommand(deployments.DeployStaticCmd)
Cmd.AddCommand(deployments.DeployNextJSCmd)
Cmd.AddCommand(deployments.DeployGoCmd)
Cmd.AddCommand(deployments.DeployNodeJSCmd)
}

66
pkg/cli/cmd/envcmd/env.go Normal file
View File

@ -0,0 +1,66 @@
package envcmd
import (
"github.com/DeBrosOfficial/network/pkg/cli"
"github.com/spf13/cobra"
)
// Cmd is the root command for environment management.
var Cmd = &cobra.Command{
Use: "env",
Short: "Manage environments",
Long: `List, switch, add, and remove Orama network environments.
Available default environments: production, devnet, testnet.`,
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List all available environments",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleEnvCommand([]string{"list"})
},
}
var currentCmd = &cobra.Command{
Use: "current",
Short: "Show current active environment",
Run: func(cmd *cobra.Command, args []string) {
cli.HandleEnvCommand([]string{"current"})
},
}
var useCmd = &cobra.Command{
Use: "use <name>",
Aliases: []string{"switch"},
Short: "Switch to a different environment",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
cli.HandleEnvCommand(append([]string{"switch"}, args...))
},
}
var addCmd = &cobra.Command{
Use: "add <name> <gateway_url> [description]",
Short: "Add a custom environment",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
cli.HandleEnvCommand(append([]string{"add"}, args...))
},
}
var removeCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove an environment",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
cli.HandleEnvCommand(append([]string{"remove"}, args...))
},
}
func init() {
Cmd.AddCommand(listCmd)
Cmd.AddCommand(currentCmd)
Cmd.AddCommand(useCmd)
Cmd.AddCommand(addCmd)
Cmd.AddCommand(removeCmd)
}

View File

@ -0,0 +1,18 @@
package inspectcmd
import (
"github.com/DeBrosOfficial/network/pkg/cli"
"github.com/spf13/cobra"
)
// Cmd is the inspect command for SSH-based cluster inspection.
var Cmd = &cobra.Command{
Use: "inspect",
Short: "Inspect cluster health via SSH",
Long: `SSH into cluster nodes and run health checks.
Supports AI-powered failure analysis and result export.`,
Run: func(cmd *cobra.Command, args []string) {
cli.HandleInspectCommand(args)
},
DisableFlagParsing: true, // Pass all flags through to existing handler
}

View File

@ -0,0 +1,44 @@
package namespacecmd
import (
"github.com/DeBrosOfficial/network/pkg/cli"
"github.com/spf13/cobra"
)
// Cmd is the root command for namespace management.
var Cmd = &cobra.Command{
Use: "namespace",
Aliases: []string{"ns"},
Short: "Manage namespaces",
Long: `List, delete, and repair namespaces on the Orama network.`,
}
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete the current namespace and all its resources",
Run: func(cmd *cobra.Command, args []string) {
forceFlag, _ := cmd.Flags().GetBool("force")
var cliArgs []string
cliArgs = append(cliArgs, "delete")
if forceFlag {
cliArgs = append(cliArgs, "--force")
}
cli.HandleNamespaceCommand(cliArgs)
},
}
var repairCmd = &cobra.Command{
Use: "repair <namespace>",
Short: "Repair an under-provisioned namespace cluster",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
cli.HandleNamespaceCommand(append([]string{"repair"}, args...))
},
}
func init() {
deleteCmd.Flags().Bool("force", false, "Skip confirmation prompt")
Cmd.AddCommand(deleteCmd)
Cmd.AddCommand(repairCmd)
}

177
pkg/cli/cmd/node/doctor.go Normal file
View File

@ -0,0 +1,177 @@
package node
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/cli/utils"
"github.com/spf13/cobra"
)
var doctorCmd = &cobra.Command{
Use: "doctor",
Short: "Diagnose common node issues",
Long: `Run a series of diagnostic checks on this node to identify
common issues with services, connectivity, disk space, and more.`,
RunE: runDoctor,
}
type check struct {
Name string
Status string // PASS, FAIL, WARN
Detail string
}
func runDoctor(cmd *cobra.Command, args []string) error {
fmt.Println("Node Doctor")
fmt.Println("===========")
fmt.Println()
var checks []check
// 1. Check if services exist
services := utils.GetProductionServices()
if len(services) == 0 {
checks = append(checks, check{"Services installed", "FAIL", "No Orama services found. Run 'orama node install' first."})
} else {
checks = append(checks, check{"Services installed", "PASS", fmt.Sprintf("%d services found", len(services))})
}
// 2. Check each service status
running := 0
stopped := 0
for _, svc := range services {
active, _ := utils.IsServiceActive(svc)
if active {
running++
} else {
stopped++
}
}
if stopped > 0 {
checks = append(checks, check{"Services running", "WARN", fmt.Sprintf("%d running, %d stopped", running, stopped)})
} else if running > 0 {
checks = append(checks, check{"Services running", "PASS", fmt.Sprintf("All %d services running", running)})
}
// 3. Check RQLite health
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("http://localhost:5001/status")
if err != nil {
checks = append(checks, check{"RQLite reachable", "FAIL", fmt.Sprintf("Cannot connect: %v", err)})
} else {
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
checks = append(checks, check{"RQLite reachable", "PASS", "HTTP API responding on :5001"})
} else {
checks = append(checks, check{"RQLite reachable", "WARN", fmt.Sprintf("HTTP %d", resp.StatusCode)})
}
}
// 4. Check Olric health
resp, err = client.Get("http://localhost:3320/")
if err != nil {
checks = append(checks, check{"Olric reachable", "FAIL", fmt.Sprintf("Cannot connect: %v", err)})
} else {
resp.Body.Close()
checks = append(checks, check{"Olric reachable", "PASS", "Responding on :3320"})
}
// 5. Check Gateway health
resp, err = client.Get("http://localhost:8443/health")
if err != nil {
checks = append(checks, check{"Gateway reachable", "FAIL", fmt.Sprintf("Cannot connect: %v", err)})
} else {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var health map[string]interface{}
if json.Unmarshal(body, &health) == nil {
if s, ok := health["status"].(string); ok {
checks = append(checks, check{"Gateway reachable", "PASS", fmt.Sprintf("Status: %s", s)})
} else {
checks = append(checks, check{"Gateway reachable", "PASS", "Responding"})
}
} else {
checks = append(checks, check{"Gateway reachable", "PASS", "Responding"})
}
} else {
checks = append(checks, check{"Gateway reachable", "WARN", fmt.Sprintf("HTTP %d", resp.StatusCode)})
}
}
// 6. Check disk space
out, err := exec.Command("df", "-h", "/opt/orama").Output()
if err == nil {
lines := strings.Split(string(out), "\n")
if len(lines) > 1 {
fields := strings.Fields(lines[1])
if len(fields) >= 5 {
usePercent := fields[4]
checks = append(checks, check{"Disk space (/opt/orama)", "PASS", fmt.Sprintf("Usage: %s (available: %s)", usePercent, fields[3])})
}
}
}
// 7. Check DNS resolution (basic)
_, err = net.LookupHost("orama-devnet.network")
if err != nil {
checks = append(checks, check{"DNS resolution", "WARN", fmt.Sprintf("Cannot resolve orama-devnet.network: %v", err)})
} else {
checks = append(checks, check{"DNS resolution", "PASS", "orama-devnet.network resolves"})
}
// 8. Check if ports are conflicting (only for stopped services)
ports, err := utils.CollectPortsForServices(services, true)
if err == nil && len(ports) > 0 {
var conflicts []string
for _, spec := range ports {
ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", spec.Port))
if err != nil {
conflicts = append(conflicts, fmt.Sprintf("%s (:%d)", spec.Name, spec.Port))
} else {
ln.Close()
}
}
if len(conflicts) > 0 {
checks = append(checks, check{"Port conflicts", "WARN", fmt.Sprintf("Ports in use: %s", strings.Join(conflicts, ", "))})
} else {
checks = append(checks, check{"Port conflicts", "PASS", "No conflicts detected"})
}
}
// Print results
maxName := 0
for _, c := range checks {
if len(c.Name) > maxName {
maxName = len(c.Name)
}
}
pass, fail, warn := 0, 0, 0
for _, c := range checks {
fmt.Printf(" [%s] %-*s %s\n", c.Status, maxName, c.Name, c.Detail)
switch c.Status {
case "PASS":
pass++
case "FAIL":
fail++
case "WARN":
warn++
}
}
fmt.Printf("\nSummary: %d passed, %d failed, %d warnings\n", pass, fail, warn)
if fail > 0 {
os.Exit(1)
}
return nil
}

View File

@ -0,0 +1,18 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/install"
"github.com/spf13/cobra"
)
var installCmd = &cobra.Command{
Use: "install",
Short: "Install production node (requires sudo)",
Long: `Install and configure an Orama production node on this machine.
For the first node, this creates a new cluster. For subsequent nodes,
use --join and --token to join an existing cluster.`,
Run: func(cmd *cobra.Command, args []string) {
install.Handle(args)
},
DisableFlagParsing: true, // Pass flags through to existing handler
}

View File

@ -0,0 +1,18 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/invite"
"github.com/spf13/cobra"
)
var inviteCmd = &cobra.Command{
Use: "invite",
Short: "Manage invite tokens for joining the cluster",
Long: `Generate invite tokens that allow new nodes to join the cluster.
Running without a subcommand creates a new token (same as 'invite create').`,
Run: func(cmd *cobra.Command, args []string) {
// Default behavior: create a new invite token
invite.Handle(args)
},
DisableFlagParsing: true,
}

View File

@ -0,0 +1,45 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/lifecycle"
"github.com/spf13/cobra"
)
var forceFlag bool
var startCmd = &cobra.Command{
Use: "start",
Short: "Start all production services (requires sudo)",
Run: func(cmd *cobra.Command, args []string) {
lifecycle.HandleStart()
},
}
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop all production services (requires sudo)",
Long: `Stop all Orama services in dependency order and disable auto-start.
Includes namespace services, global services, and supporting services.
Use --force to bypass quorum safety check.`,
Run: func(cmd *cobra.Command, args []string) {
force, _ := cmd.Flags().GetBool("force")
lifecycle.HandleStopWithFlags(force)
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart all production services (requires sudo)",
Long: `Restart all Orama services. Stops in dependency order then restarts.
Includes explicit namespace service restart.
Use --force to bypass quorum safety check.`,
Run: func(cmd *cobra.Command, args []string) {
force, _ := cmd.Flags().GetBool("force")
lifecycle.HandleRestartWithFlags(force)
},
}
func init() {
stopCmd.Flags().Bool("force", false, "Bypass quorum safety check")
restartCmd.Flags().Bool("force", false, "Bypass quorum safety check")
}

17
pkg/cli/cmd/node/logs.go Normal file
View File

@ -0,0 +1,17 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/logs"
"github.com/spf13/cobra"
)
var logsCmd = &cobra.Command{
Use: "logs <service>",
Short: "View production service logs",
Long: `Stream logs for a specific Orama production service.
Service aliases: node, ipfs, cluster, gateway, olric`,
Run: func(cmd *cobra.Command, args []string) {
logs.Handle(args)
},
DisableFlagParsing: true,
}

View File

@ -0,0 +1,15 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/migrate"
"github.com/spf13/cobra"
)
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate from old unified setup (requires sudo)",
Run: func(cmd *cobra.Command, args []string) {
migrate.Handle(args)
},
DisableFlagParsing: true,
}

28
pkg/cli/cmd/node/node.go Normal file
View File

@ -0,0 +1,28 @@
package node
import (
"github.com/spf13/cobra"
)
// Cmd is the root command for node operator commands (was "prod").
var Cmd = &cobra.Command{
Use: "node",
Short: "Node operator commands (requires sudo for most operations)",
Long: `Manage the Orama node running on this machine.
Includes install, upgrade, start/stop/restart, status, logs, and more.
Most commands require root privileges (sudo).`,
}
func init() {
Cmd.AddCommand(installCmd)
Cmd.AddCommand(uninstallCmd)
Cmd.AddCommand(upgradeCmd)
Cmd.AddCommand(startCmd)
Cmd.AddCommand(stopCmd)
Cmd.AddCommand(restartCmd)
Cmd.AddCommand(statusCmd)
Cmd.AddCommand(logsCmd)
Cmd.AddCommand(inviteCmd)
Cmd.AddCommand(migrateCmd)
Cmd.AddCommand(doctorCmd)
}

View File

@ -0,0 +1,14 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/status"
"github.com/spf13/cobra"
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "Show production service status",
Run: func(cmd *cobra.Command, args []string) {
status.Handle()
},
}

View File

@ -0,0 +1,14 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/uninstall"
"github.com/spf13/cobra"
)
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Remove production services (requires sudo)",
Run: func(cmd *cobra.Command, args []string) {
uninstall.Handle()
},
}

View File

@ -0,0 +1,17 @@
package node
import (
"github.com/DeBrosOfficial/network/pkg/cli/production/upgrade"
"github.com/spf13/cobra"
)
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrade existing installation (requires sudo)",
Long: `Upgrade the Orama node binary and optionally restart services.
Uses rolling restart with quorum safety to ensure zero downtime.`,
Run: func(cmd *cobra.Command, args []string) {
upgrade.Handle(args)
},
DisableFlagParsing: true,
}

View File

@ -1,55 +0,0 @@
package cli
import (
"fmt"
"os"
"github.com/DeBrosOfficial/network/pkg/cli/db"
"github.com/DeBrosOfficial/network/pkg/cli/deployments"
)
// HandleDeployCommand handles deploy commands
func HandleDeployCommand(args []string) {
deployCmd := deployments.DeployCmd
deployCmd.SetArgs(args)
if err := deployCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// HandleDeploymentsCommand handles deployments management commands
func HandleDeploymentsCommand(args []string) {
// Create root command for deployments management
deploymentsCmd := deployments.DeployCmd
deploymentsCmd.Use = "deployments"
deploymentsCmd.Short = "Manage deployments"
deploymentsCmd.Long = "List, get, delete, rollback, and view logs for deployments"
// Add management subcommands
deploymentsCmd.AddCommand(deployments.ListCmd)
deploymentsCmd.AddCommand(deployments.GetCmd)
deploymentsCmd.AddCommand(deployments.DeleteCmd)
deploymentsCmd.AddCommand(deployments.RollbackCmd)
deploymentsCmd.AddCommand(deployments.LogsCmd)
deploymentsCmd.AddCommand(deployments.StatsCmd)
deploymentsCmd.SetArgs(args)
if err := deploymentsCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// HandleDBCommand handles database commands
func HandleDBCommand(args []string) {
dbCmd := db.DBCmd
dbCmd.SetArgs(args)
if err := dbCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -218,7 +218,7 @@ func deployNextJS(cmd *cobra.Command, args []string) error {
printDeploymentInfo(resp)
if deploySSR {
fmt.Printf("⚠️ Note: SSR deployment may take a minute to start. Check status with: orama deployments get %s\n", deployName)
fmt.Printf("⚠️ Note: SSR deployment may take a minute to start. Check status with: orama app get %s\n", deployName)
}
return nil

View File

@ -47,7 +47,7 @@ func showEnvHelp() {
fmt.Printf(" enable - Alias for 'switch' (e.g., 'devnet enable')\n\n")
fmt.Printf("Available Environments:\n")
fmt.Printf(" devnet - Development network (https://orama-devnet.network)\n")
fmt.Printf(" testnet - Test network (https://orama-tesetnet.network)\n\n")
fmt.Printf(" testnet - Test network (https://orama-testnet.network)\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" orama env list\n")
fmt.Printf(" orama env current\n")

View File

@ -39,7 +39,7 @@ var DefaultEnvironments = []Environment{
},
{
Name: "testnet",
GatewayURL: "https://orama-tesetnet.network",
GatewayURL: "https://orama-testnet.network",
Description: "Test network (staging)",
IsActive: false,
},

View File

@ -30,7 +30,7 @@ func NewValidator(flags *Flags, oramaDir string) *Validator {
// ValidateFlags validates required flags
func (v *Validator) ValidateFlags() error {
if v.flags.VpsIP == "" && !v.flags.DryRun {
return fmt.Errorf("--vps-ip is required for installation\nExample: orama prod install --vps-ip 1.2.3.4")
return fmt.Errorf("--vps-ip is required for installation\nExample: orama node install --vps-ip 1.2.3.4")
}
return nil
}

View File

@ -30,7 +30,7 @@ func HandleRestartWithFlags(force bool) {
if !force {
if warning := checkQuorumSafety(); warning != "" {
fmt.Fprintf(os.Stderr, "\nWARNING: %s\n", warning)
fmt.Fprintf(os.Stderr, "Use 'orama prod restart --force' to proceed anyway.\n\n")
fmt.Fprintf(os.Stderr, "Use 'orama node restart --force' to proceed anyway.\n\n")
os.Exit(1)
}
}
@ -43,6 +43,10 @@ func HandleRestartWithFlags(force bool) {
return
}
// Stop namespace services first (same as stop command)
fmt.Printf("\n Stopping namespace services...\n")
stopAllNamespaceServices()
// Ordered stop: gateway first, then node (RQLite), then supporting services
fmt.Printf("\n Stopping services (ordered)...\n")
shutdownOrder := [][]string{

View File

@ -51,7 +51,7 @@ func HandleStart() {
}
if active {
fmt.Printf(" %s already running\n", svc)
// Re-enable if disabled (in case it was stopped with 'orama prod stop')
// Re-enable if disabled (in case it was stopped with 'orama node stop')
enabled, err := utils.IsServiceEnabled(svc)
if err == nil && !enabled {
if err := exec.Command("systemctl", "enable", svc).Run(); err != nil {
@ -81,7 +81,7 @@ func HandleStart() {
os.Exit(1)
}
// Re-enable inactive services first (in case they were disabled by 'orama prod stop')
// Re-enable inactive services first (in case they were disabled by 'orama node stop')
for _, svc := range inactive {
enabled, err := utils.IsServiceEnabled(svc)
if err == nil && !enabled {

View File

@ -31,7 +31,7 @@ func HandleStopWithFlags(force bool) {
if !force {
if warning := checkQuorumSafety(); warning != "" {
fmt.Fprintf(os.Stderr, "\nWARNING: %s\n", warning)
fmt.Fprintf(os.Stderr, "Use 'orama prod stop --force' to proceed anyway.\n\n")
fmt.Fprintf(os.Stderr, "Use 'orama node stop --force' to proceed anyway.\n\n")
os.Exit(1)
}
}
@ -161,7 +161,7 @@ func HandleStopWithFlags(force bool) {
fmt.Fprintf(os.Stderr, " If services are still restarting, they may need manual intervention\n")
} else {
fmt.Printf("\n✅ All services stopped and disabled (will not auto-start on boot)\n")
fmt.Printf(" Use 'orama prod start' to start and re-enable services\n")
fmt.Printf(" Use 'orama node start' to start and re-enable services\n")
}
}

View File

@ -47,7 +47,7 @@ func Handle(args []string) {
}
func showUsage() {
fmt.Fprintf(os.Stderr, "Usage: orama prod logs <service> [--follow]\n")
fmt.Fprintf(os.Stderr, "Usage: orama node logs <service> [--follow]\n")
fmt.Fprintf(os.Stderr, "\nService aliases:\n")
fmt.Fprintf(os.Stderr, " node, ipfs, cluster, gateway, olric\n")
fmt.Fprintf(os.Stderr, "\nOr use full service name:\n")

View File

@ -54,5 +54,5 @@ func Handle() {
fmt.Printf(" ❌ %s not found\n", oramaDir)
}
fmt.Printf("\nView logs with: orama prod logs <service>\n")
fmt.Printf("\nView logs with: orama node logs <service>\n")
}

View File

@ -29,6 +29,11 @@ func Handle() {
return
}
// Stop and remove namespace services first
fmt.Printf("Stopping namespace services...\n")
stopNamespaceServices()
// All global services (was missing: orama-anyone-relay, coredns, caddy)
services := []string{
"orama-gateway",
"orama-node",
@ -36,9 +41,12 @@ func Handle() {
"orama-ipfs-cluster",
"orama-ipfs",
"orama-anyone-client",
"orama-anyone-relay",
"coredns",
"caddy",
}
fmt.Printf("Stopping services...\n")
fmt.Printf("Stopping global services...\n")
for _, svc := range services {
exec.Command("systemctl", "stop", svc).Run()
exec.Command("systemctl", "disable", svc).Run()
@ -46,8 +54,46 @@ func Handle() {
os.Remove(unitPath)
}
// Remove namespace template unit files
removeNamespaceTemplates()
exec.Command("systemctl", "daemon-reload").Run()
fmt.Printf("✅ Services uninstalled\n")
fmt.Printf(" Configuration and data preserved in /opt/orama/.orama\n")
fmt.Printf(" To remove all data: rm -rf /opt/orama/.orama\n\n")
}
// stopNamespaceServices discovers and stops all running namespace services
func stopNamespaceServices() {
cmd := exec.Command("systemctl", "list-units", "--type=service", "--all", "--no-pager", "--no-legend", "orama-namespace-*@*.service")
output, err := cmd.Output()
if err != nil {
return
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) > 0 && strings.HasPrefix(fields[0], "orama-namespace-") {
svc := fields[0]
exec.Command("systemctl", "stop", svc).Run()
exec.Command("systemctl", "disable", svc).Run()
fmt.Printf(" Stopped %s\n", svc)
}
}
}
// removeNamespaceTemplates removes namespace template unit files
func removeNamespaceTemplates() {
templatePatterns := []string{
"orama-namespace-rqlite@.service",
"orama-namespace-olric@.service",
"orama-namespace-gateway@.service",
}
for _, pattern := range templatePatterns {
unitPath := filepath.Join("/etc/systemd/system", pattern)
if _, err := os.Stat(unitPath); err == nil {
os.Remove(unitPath)
}
}
}

View File

@ -636,7 +636,7 @@ func (o *Orchestrator) restartServices() error {
services := utils.GetProductionServices()
// Re-enable all services BEFORE restarting them.
// orama prod stop disables services, and orama-node's PartOf= dependency
// orama node stop disables services, and orama-node's PartOf= dependency
// won't propagate restart to disabled services. We must re-enable first
// so that all services restart with the updated binary.
for _, svc := range services {

View File

@ -1,10 +0,0 @@
package cli
import (
"github.com/DeBrosOfficial/network/pkg/cli/production"
)
// HandleProdCommand handles production environment commands
func HandleProdCommand(args []string) {
production.HandleCommand(args)
}

40
pkg/cli/shared/api.go Normal file
View File

@ -0,0 +1,40 @@
package shared
import (
"fmt"
"os"
"github.com/DeBrosOfficial/network/pkg/auth"
)
// GetAPIURL returns the gateway/API URL from env var or active environment config.
func GetAPIURL() string {
if url := os.Getenv("ORAMA_API_URL"); url != "" {
return url
}
return auth.GetDefaultGatewayURL()
}
// GetAuthToken returns an auth token from env var or the credentials store.
func GetAuthToken() (string, error) {
if token := os.Getenv("ORAMA_TOKEN"); token != "" {
return token, nil
}
store, err := auth.LoadEnhancedCredentials()
if err != nil {
return "", fmt.Errorf("failed to load credentials: %w", err)
}
gatewayURL := auth.GetDefaultGatewayURL()
creds := store.GetDefaultCredential(gatewayURL)
if creds == nil {
return "", fmt.Errorf("no credentials found for %s. Run 'orama auth login' to authenticate", gatewayURL)
}
if !creds.IsValid() {
return "", fmt.Errorf("credentials expired for %s. Run 'orama auth login' to re-authenticate", gatewayURL)
}
return creds.APIKey, nil
}

33
pkg/cli/shared/confirm.go Normal file
View File

@ -0,0 +1,33 @@
package shared
import (
"bufio"
"fmt"
"os"
"strings"
)
// Confirm prompts the user for yes/no confirmation. Returns true if user confirms.
func Confirm(prompt string) bool {
fmt.Printf("%s (y/N): ", prompt)
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.ToLower(strings.TrimSpace(response))
return response == "y" || response == "yes"
}
// ConfirmExact prompts the user to type an exact string to confirm. Returns true if matched.
func ConfirmExact(prompt, expected string) bool {
fmt.Printf("%s: ", prompt)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return strings.TrimSpace(scanner.Text()) == expected
}
// RequireRoot exits with an error if the current user is not root.
func RequireRoot() {
if os.Geteuid() != 0 {
fmt.Fprintf(os.Stderr, "Error: This command must be run as root (use sudo)\n")
os.Exit(1)
}
}

44
pkg/cli/shared/format.go Normal file
View File

@ -0,0 +1,44 @@
package shared
import "fmt"
// FormatBytes formats a byte count into a human-readable string (KB, MB, GB, etc.)
func FormatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// FormatUptime formats seconds into a human-readable uptime string.
func FormatUptime(seconds float64) string {
s := int(seconds)
days := s / 86400
hours := (s % 86400) / 3600
mins := (s % 3600) / 60
if days > 0 {
return fmt.Sprintf("%dd %dh %dm", days, hours, mins)
}
if hours > 0 {
return fmt.Sprintf("%dh %dm", hours, mins)
}
return fmt.Sprintf("%dm", mins)
}
// FormatSize formats a megabyte value into a human-readable string.
func FormatSize(mb float64) string {
if mb < 0.1 {
return fmt.Sprintf("%.1f KB", mb*1024)
}
if mb >= 1024 {
return fmt.Sprintf("%.1f GB", mb/1024)
}
return fmt.Sprintf("%.1f MB", mb)
}

17
pkg/cli/shared/output.go Normal file
View File

@ -0,0 +1,17 @@
package shared
import (
"encoding/json"
"fmt"
"os"
)
// PrintJSON pretty-prints data as indented JSON to stdout.
func PrintJSON(data interface{}) {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to marshal JSON: %v\n", err)
return
}
fmt.Println(string(jsonData))
}

View File

@ -32,7 +32,7 @@ const systemPrompt = `You are a distributed systems expert analyzing health chec
- High TCP retransmission (>2%) indicates packet loss, often due to WireGuard MTU issues.
## Service Management
- ALWAYS use the CLI for service operations: ` + "`sudo orama prod restart`" + `, ` + "`sudo orama prod stop`" + `, ` + "`sudo orama prod start`" + `
- ALWAYS use the CLI for service operations: ` + "`sudo orama node restart`" + `, ` + "`sudo orama node stop`" + `, ` + "`sudo orama node start`" + `
- NEVER use raw systemctl commands (they skip important lifecycle hooks).
- For rolling restarts: upgrade followers first, leader LAST, one node at a time.
- Check RQLite leader: ` + "`curl -s localhost:4001/status | python3 -c \"import sys,json; print(json.load(sys.stdin)['store']['raft']['state'])\"`" + `