feat(cli): add tinyforge terminal client
New zero-dependency Go CLI (cmd/cli) that drives the existing HTTP API: login/logout, apps list, deploy (synchronous, --timeout), logs (one-shot + -f SSE follow), and status. Caches a 24h JWT in ~/.tinyforge/config.json (0600, Chmod-enforced on overwrite); Bearer-header auth keeps the token out of server/proxy logs; no-echo password prompt (kernel32 on Windows, stty elsewhere). Server/token resolved via flags, TINYFORGE_URL/TINYFORGE_TOKEN env, or config. README CLI section + root-anchored .gitignore entries for the build output.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func runDeploy(args []string) error {
|
||||
fs := flag.NewFlagSet("deploy", flag.ExitOnError)
|
||||
g := addGlobalFlags(fs)
|
||||
ref := fs.String("ref", "", "image tag / git ref / source-specific deploy target")
|
||||
note := fs.String("note", "", "free-text note recorded with the deploy")
|
||||
timeout := fs.Duration("timeout", 15*time.Minute, "max time to wait for the deploy to finish")
|
||||
fs.Usage = func() {
|
||||
fmt.Fprint(os.Stderr, "Usage: tinyforge deploy <app> [--ref TAG] [--note TEXT] [--timeout DUR]\n\n"+
|
||||
"Trigger a deploy and wait for it to finish. Requires an admin token.\n")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() != 1 {
|
||||
fs.Usage()
|
||||
return fmt.Errorf("expected exactly one app (name or id)")
|
||||
}
|
||||
|
||||
sess, err := newSession(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the app on a short deadline; the deploy itself gets the full one.
|
||||
resolveCtx, cancelResolve := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancelResolve()
|
||||
app, err := resolveApp(resolveCtx, sess.client, fs.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := map[string]string{}
|
||||
if *ref != "" {
|
||||
body["reference"] = *ref
|
||||
}
|
||||
if *note != "" {
|
||||
body["note"] = *note
|
||||
}
|
||||
|
||||
fmt.Printf("Deploying %s%s…\n", app.Name, refSuffix(*ref))
|
||||
|
||||
// The endpoint returns 202 but blocks until the deploy completes, so a
|
||||
// success here means it finished; allow plenty of time for pull/build.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
|
||||
var result DeployResult
|
||||
if err := sess.client.doJSON(ctx, "POST", "/api/workloads/"+app.ID+"/deploy", body, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Deploy of %s completed (triggered by %s).\n", app.Name, result.TriggeredBy)
|
||||
fmt.Printf("Follow with: tinyforge logs %s -f\n", app.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func refSuffix(ref string) string {
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" @ %s", ref)
|
||||
}
|
||||
Reference in New Issue
Block a user