Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ target

# Build outputs
vdf-test*

.cursor
398 changes: 398 additions & 0 deletions client/README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ func init() {
ConfigCmd.AddCommand(ClientConfigPublicRpcCmd)
ConfigCmd.AddCommand(ClientConfigSetCustomRpcCmd)
ConfigCmd.AddCommand(ClientConfigSignatureCheckCmd)
ConfigCmd.AddCommand(ClientConfigServiceNameCmd)
}
28 changes: 27 additions & 1 deletion client/cmd/config/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,35 @@ var ClientConfigPrintCmd = &cobra.Command{
}

// Print the config in a readable format
fmt.Printf("Data Directory: %s\n", config.DataDir)
fmt.Printf("QClient Install Dir: %s\n", utils.GetQClientInstallDir())
fmt.Printf(" QClient Binary Dir: %s\n", utils.GetQClientBinaryDir())
fmt.Printf("Symlink Path: %s\n", config.SymlinkPath)
fmt.Printf("Signature Check: %v\n", config.SignatureCheck)
fmt.Printf("Quiet: %v\n", config.Quiet)
fmt.Printf("Public RPC: %v\n", config.PublicRpc)
serviceName := config.NodeServiceName
if serviceName == "" {
serviceName = utils.DefaultNodeServiceName
}
fmt.Printf("Node Service Name: %s\n", serviceName)

fmt.Printf("Node Install Dir: %s\n", utils.GetNodeInstallDir())
fmt.Printf(" Node Binary Dir: %s\n", utils.GetNodeBinaryDir())
fmt.Printf("Node State Dir: %s\n", utils.GetNodeStateDir())
fmt.Printf(" Node Env File: %s\n", utils.GetNodeEnvFilePath())
// Node log location lives in the node config's logger.path, not
// the client config. Show the active one for convenience.
if resolved, err := utils.ResolveActiveNodeLog(); err == nil {
if resolved.FileBased {
fmt.Printf("Node Log Dir: %s (from %s/config.yml)\n",
resolved.LogDir, resolved.ConfigDir)
} else {
fmt.Printf("Node Log Dir: (none; active config %q has no logger block, node logs to system log)\n",
resolved.ConfigName)
}
}
fmt.Printf("Node Symlink Dir: %s\n", utils.GetNodeSymlinkDir())
fmt.Printf(" Node Symlink: %s\n", utils.GetNodeSymlinkPath())
fmt.Printf("Node Configs Dir: %s\n", utils.GetNodeConfigsDir())
},
}
172 changes: 172 additions & 0 deletions client/cmd/config/service-name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package config

import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/cmd/node"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)

var ClientConfigServiceNameCmd = &cobra.Command{
Use: "service-name [name]",
Short: "Set the Linux systemd service name used by the node",
Long: `Set the name of the systemd service unit for the Quilibrium node.

On Linux, this controls the name used for commands like:
sudo systemctl start <name>
sudo systemctl status <name>
and the unit file written at /etc/systemd/system/<name>.service.

The default is "quilibrium-node". The binary symlink at /usr/local/bin is
always created as quilibrium-node regardless of this value.

If a systemd unit is already installed under the previous name, this command
will migrate it: the old service is stopped/disabled/removed and a new unit
file is created under the new name (preserving enabled/active state).

Examples:
qclient config service-name my-node
qclient config service-name # prints current value`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
cfg, err := utils.LoadClientConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
os.Exit(1)
}

current := cfg.NodeServiceName
if current == "" {
current = utils.DefaultNodeServiceName
}

if len(args) == 0 {
fmt.Printf("Node service name: %s\n", current)
return
}

newName := strings.TrimSpace(args[0])
if err := utils.ValidateNodeServiceName(newName); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if newName == current {
fmt.Printf("Node service name is already %q, nothing to do.\n", current)
return
}

// On Linux, if the old unit file exists we need sudo up front to be
// able to migrate cleanly.
oldUnitPath := "/etc/systemd/system/" + current + ".service"
needsMigration := utils.OsType == "linux" && utils.FileExists(oldUnitPath)

if needsMigration {
if err := utils.CheckAndRequestSudo(
"Renaming the installed systemd service requires root privileges",
); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

// Capture prior state before we touch anything.
wasActive := needsMigration && systemctlCheck("is-active", current)
wasEnabled := needsMigration && systemctlCheck("is-enabled", current)

if needsMigration {
fmt.Printf("Migrating installed service %q -> %q...\n", current, newName)

if wasActive {
if err := runSystemctl("stop", current); err != nil {
fmt.Fprintf(os.Stderr,
"Warning: failed to stop %q: %v\n", current, err,
)
}
}
if wasEnabled {
if err := runSystemctl("disable", current); err != nil {
fmt.Fprintf(os.Stderr,
"Warning: failed to disable %q: %v\n", current, err,
)
}
}

// Remove old unit file directly (RemoveSystemdServiceFile reads
// the configured name, which we haven't rotated yet — but we
// know the exact path here).
if err := os.Remove(oldUnitPath); err != nil && !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr,
"Warning: failed to remove old unit file %s: %v\n",
oldUnitPath, err,
)
}

if err := runSystemctl("daemon-reload"); err != nil {
fmt.Fprintf(os.Stderr,
"Warning: systemctl daemon-reload failed: %v\n", err,
)
}
}

// Persist the new name before writing the new unit file so that
// CreateSystemdServiceFile picks it up via GetNodeServiceName().
cfg.NodeServiceName = newName
if err := utils.SaveClientConfig(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error saving config: %v\n", err)
os.Exit(1)
}

if needsMigration {
if err := node.CreateSystemdServiceFile(false); err != nil {
fmt.Fprintf(os.Stderr,
"Error creating new systemd service file: %v\n", err,
)
os.Exit(1)
}

if wasEnabled {
if err := runSystemctl("enable", newName); err != nil {
fmt.Fprintf(os.Stderr,
"Warning: failed to enable %q: %v\n", newName, err,
)
}
}
if wasActive {
if err := runSystemctl("start", newName); err != nil {
fmt.Fprintf(os.Stderr,
"Warning: failed to start %q: %v\n", newName, err,
)
}
}

fmt.Printf("Service migrated. Active=%v Enabled=%v\n", wasActive, wasEnabled)
}

fmt.Printf("Node service name set to %q.\n", newName)
if !needsMigration && utils.OsType == "linux" {
fmt.Println(
"No existing systemd unit was found under the previous name; " +
"the new name will take effect the next time you install or " +
"update the service (e.g. `sudo qclient node service install`).",
)
}
},
}

// systemctlCheck returns true when `systemctl <subcmd> <unit>` exits 0.
func systemctlCheck(subcmd, unit string) bool {
cmd := exec.Command("systemctl", subcmd, unit)
return cmd.Run() == nil
}

// runSystemctl runs `sudo systemctl <args...>` and returns its error.
func runSystemctl(args ...string) error {
full := append([]string{"systemctl"}, args...)
cmd := exec.Command("sudo", full...)
return cmd.Run()
}
19 changes: 15 additions & 4 deletions client/cmd/crossMint.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import (
)

var (
NodeConfig *config.Config
ConfigDirectory string
NodeConfig *config.Config
ConfigDirectory string
resolvedConfigDirectory string
)

var CrossMintCmd = &cobra.Command{
Expand All @@ -44,8 +45,18 @@ var CrossMintCmd = &cobra.Command{
var nodeConfig *config.Config
var err error
if ConfigDirectory != "" && ConfigDirectory != "default" {
nodeConfig, err = utils.LoadNodeConfig(ConfigDirectory)
resolvedConfigDirectory, err = utils.ResolveNodeConfigDir(ConfigDirectory)
if err != nil {
fmt.Printf("error loading node config: %s\n", err)
os.Exit(1)
}
nodeConfig, err = utils.LoadNodeConfig(resolvedConfigDirectory)
} else {
resolvedConfigDirectory, err = utils.GetDefaultNodeConfigDir()
if err != nil {
fmt.Printf("error loading node config: %s\n", err)
os.Exit(1)
}
nodeConfig, err = utils.LoadDefaultNodeConfig()
}
if err != nil {
Expand Down Expand Up @@ -88,7 +99,7 @@ var CrossMintCmd = &cobra.Command{
// account if it was changed.
if !filepath.IsAbs(NodeConfig.Key.KeyStoreFile.Path) {
NodeConfig.Key.KeyStoreFile.Path = filepath.Join(
ConfigDirectory,
resolvedConfigDirectory,
filepath.Base(NodeConfig.Key.KeyStoreFile.Path),
)
}
Expand Down
106 changes: 106 additions & 0 deletions client/cmd/dev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)

var DevCmd = &cobra.Command{
Use: "dev [enable|disable]",
Short: "Toggle developer-friendly defaults for custom qclient builds",
Long: `Dev mode applies sane defaults for locally-built / unsigned qclient binaries:

enable:
- signatureCheck = false (skip signature verification)
- quiet = true (suppress informational output)

disable:
- signatureCheck = true (restore signature verification)
- quiet = false (restore informational output)

With no argument, the current state is toggled based on the signatureCheck flag
(dev mode is considered "enabled" when signatureCheck is false).`,
Run: func(_ *cobra.Command, args []string) {
cfg, err := utils.LoadClientConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}

var enable bool
if len(args) > 0 {
switch strings.ToLower(args[0]) {
case "enable":
enable = true
case "disable":
enable = false
default:
fmt.Printf("Error: Invalid value '%s'. Please use 'enable' or 'disable'.\n", args[0])
os.Exit(1)
}
} else {
enable = cfg.SignatureCheck
}

if enable {
cfg.SignatureCheck = false
cfg.Quiet = true
} else {
cfg.SignatureCheck = true
cfg.Quiet = false
}

if err := utils.SaveClientConfig(cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}

status := "disabled"
if enable {
status = "enabled"
}
fmt.Printf("Dev mode has been %s (signatureCheck=%v, quiet=%v).\n",
status, cfg.SignatureCheck, cfg.Quiet)

if enable {
maybeLinkDevBinary()
}
},
}

func maybeLinkDevBinary() {
execPath, err := os.Executable()
if err != nil {
fmt.Printf("Skipping link prompt: cannot resolve current executable: %v\n", err)
return
}

if existing, err := os.Readlink(symlinkPath); err == nil && existing == execPath {
fmt.Printf("%s already points at this binary.\n", symlinkPath)
return
}

fmt.Printf("Link this dev binary at %s -> %s? (y/n): ", symlinkPath, execPath)
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "y" && response != "yes" {
fmt.Println("Skipping symlink.")
return
}

if !utils.IsSudo() {
fmt.Printf("Cannot create symlink at %s without sudo. Re-run: sudo qclient link\n", symlinkPath)
return
}

if err := utils.CreateSymlink(execPath, symlinkPath); err != nil {
fmt.Printf("Failed to create symlink: %v\n", err)
return
}
fmt.Printf("Symlink created at %s\n", symlinkPath)
}
Loading