Skip to content

Commit cbb8eb8

Browse files
committed
Add docker like aliases
1 parent 0542167 commit cbb8eb8

File tree

9 files changed

+525
-10
lines changed

9 files changed

+525
-10
lines changed

README.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,35 @@ go run cmd/hypeman/main.go
3333

3434
## Usage
3535

36-
The CLI follows a resource-based command structure:
37-
3836
```sh
39-
hypeman [resource] [command] [flags]
40-
```
37+
# Pull an image
38+
hypeman pull nginx:alpine
4139

42-
```sh
43-
hypeman health check
40+
# Run an instance (auto-pulls image if needed)
41+
hypeman run nginx:alpine
42+
hypeman run --name my-app -e PORT=3000 nginx:alpine
43+
44+
# List running instances
45+
hypeman ps
46+
hypeman ps -a # show all instances
47+
48+
# View logs
49+
hypeman logs <instance-id>
50+
hypeman logs -f <instance-id> # follow logs
51+
52+
# Execute a command in a running instance
53+
hypeman exec <instance-id> -- /bin/sh
54+
hypeman exec -it <instance-id> # interactive shell
4455
```
4556

4657
For details about specific commands, use the `--help` flag.
4758

59+
The CLI also provides resource-based commands for more advanced usage:
60+
61+
```sh
62+
hypeman [resource] [command] [flags]
63+
```
64+
4865
## Global Flags
4966

5067
- `--debug` - Enable debug logging (includes HTTP request/response details)

pkg/cmd/cmd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ func init() {
6868
},
6969
Commands: []*cli.Command{
7070
&execCmd,
71+
&pullCmd,
72+
&runCmd,
73+
&psCmd,
74+
&logsCmd,
7175
{
7276
Name: "health",
7377
Category: "API RESOURCE",

pkg/cmd/exec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func handleExec(ctx context.Context, cmd *cli.Command) error {
131131
baseURL = os.Getenv("HYPEMAN_BASE_URL")
132132
}
133133
if baseURL == "" {
134-
baseURL = "https://api.onkernel.com"
134+
baseURL = "http://localhost:8080"
135135
}
136136

137137
apiKey := os.Getenv("HYPEMAN_API_KEY")

pkg/cmd/format.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
"time"
8+
)
9+
10+
// TableWriter provides simple table formatting for CLI output
11+
type TableWriter struct {
12+
w io.Writer
13+
headers []string
14+
widths []int
15+
rows [][]string
16+
}
17+
18+
// NewTableWriter creates a new table writer
19+
func NewTableWriter(w io.Writer, headers ...string) *TableWriter {
20+
widths := make([]int, len(headers))
21+
for i, h := range headers {
22+
widths[i] = len(h)
23+
}
24+
return &TableWriter{
25+
w: w,
26+
headers: headers,
27+
widths: widths,
28+
}
29+
}
30+
31+
// AddRow adds a row to the table
32+
func (t *TableWriter) AddRow(cells ...string) {
33+
// Pad or truncate to match header count
34+
row := make([]string, len(t.headers))
35+
for i := range row {
36+
if i < len(cells) {
37+
row[i] = cells[i]
38+
}
39+
if len(row[i]) > t.widths[i] {
40+
t.widths[i] = len(row[i])
41+
}
42+
}
43+
t.rows = append(t.rows, row)
44+
}
45+
46+
// Render outputs the table
47+
func (t *TableWriter) Render() {
48+
// Print headers
49+
for i, h := range t.headers {
50+
fmt.Fprintf(t.w, "%-*s", t.widths[i]+2, h)
51+
}
52+
fmt.Fprintln(t.w)
53+
54+
// Print rows
55+
for _, row := range t.rows {
56+
for i, cell := range row {
57+
fmt.Fprintf(t.w, "%-*s", t.widths[i]+2, cell)
58+
}
59+
fmt.Fprintln(t.w)
60+
}
61+
}
62+
63+
// FormatTimeAgo formats a time as "X ago" string
64+
func FormatTimeAgo(t time.Time) string {
65+
if t.IsZero() {
66+
return "N/A"
67+
}
68+
69+
d := time.Since(t)
70+
71+
switch {
72+
case d < time.Minute:
73+
return fmt.Sprintf("%d seconds ago", int(d.Seconds()))
74+
case d < time.Hour:
75+
mins := int(d.Minutes())
76+
if mins == 1 {
77+
return "1 minute ago"
78+
}
79+
return fmt.Sprintf("%d minutes ago", mins)
80+
case d < 24*time.Hour:
81+
hours := int(d.Hours())
82+
if hours == 1 {
83+
return "1 hour ago"
84+
}
85+
return fmt.Sprintf("%d hours ago", hours)
86+
default:
87+
days := int(d.Hours() / 24)
88+
if days == 1 {
89+
return "1 day ago"
90+
}
91+
return fmt.Sprintf("%d days ago", days)
92+
}
93+
}
94+
95+
// TruncateID truncates an ID to 12 characters (like Docker)
96+
func TruncateID(id string) string {
97+
if len(id) > 12 {
98+
return id[:12]
99+
}
100+
return id
101+
}
102+
103+
// TruncateString truncates a string to max length with ellipsis
104+
func TruncateString(s string, max int) string {
105+
if len(s) <= max {
106+
return s
107+
}
108+
if max <= 3 {
109+
return s[:max]
110+
}
111+
return s[:max-3] + "..."
112+
}
113+
114+
// GenerateInstanceName generates a name from image reference
115+
func GenerateInstanceName(image string) string {
116+
// Extract image name without registry/tag
117+
name := image
118+
119+
// Remove registry prefix
120+
if idx := strings.LastIndex(name, "/"); idx != -1 {
121+
name = name[idx+1:]
122+
}
123+
124+
// Remove tag/digest
125+
if idx := strings.Index(name, ":"); idx != -1 {
126+
name = name[:idx]
127+
}
128+
if idx := strings.Index(name, "@"); idx != -1 {
129+
name = name[:idx]
130+
}
131+
132+
// Add random suffix
133+
suffix := randomSuffix(4)
134+
return fmt.Sprintf("%s-%s", name, suffix)
135+
}
136+
137+
// randomSuffix generates a random alphanumeric suffix
138+
func randomSuffix(n int) string {
139+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
140+
b := make([]byte, n)
141+
for i := range b {
142+
// Simple pseudo-random using time
143+
b[i] = chars[(time.Now().UnixNano()+int64(i))%int64(len(chars))]
144+
}
145+
return string(b)
146+
}
147+
148+

pkg/cmd/logs.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/onkernel/hypeman-go"
8+
"github.com/onkernel/hypeman-go/option"
9+
"github.com/urfave/cli/v3"
10+
)
11+
12+
var logsCmd = cli.Command{
13+
Name: "logs",
14+
Usage: "Fetch the logs of an instance",
15+
ArgsUsage: "<instance>",
16+
Flags: []cli.Flag{
17+
&cli.BoolFlag{
18+
Name: "follow",
19+
Aliases: []string{"f"},
20+
Usage: "Follow log output",
21+
},
22+
&cli.IntFlag{
23+
Name: "tail",
24+
Usage: "Number of lines to show from the end of the logs",
25+
Value: 100,
26+
},
27+
},
28+
Action: handleLogs,
29+
HideHelpCommand: true,
30+
}
31+
32+
func handleLogs(ctx context.Context, cmd *cli.Command) error {
33+
args := cmd.Args().Slice()
34+
if len(args) < 1 {
35+
return fmt.Errorf("instance ID required\nUsage: hypeman logs [flags] <instance>")
36+
}
37+
38+
instanceID := args[0]
39+
40+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
41+
42+
params := hypeman.InstanceStreamLogsParams{}
43+
if cmd.IsSet("follow") {
44+
params.Follow = hypeman.Opt(cmd.Bool("follow"))
45+
}
46+
if cmd.IsSet("tail") {
47+
params.Tail = hypeman.Opt(int64(cmd.Int("tail")))
48+
}
49+
50+
stream := client.Instances.StreamLogsStreaming(
51+
ctx,
52+
instanceID,
53+
params,
54+
option.WithMiddleware(debugMiddleware(cmd.Root().Bool("debug"))),
55+
)
56+
defer stream.Close()
57+
58+
for stream.Next() {
59+
fmt.Printf("%s\n", stream.Current())
60+
}
61+
62+
return stream.Err()
63+
}
64+
65+

pkg/cmd/ps.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/onkernel/hypeman-go"
9+
"github.com/onkernel/hypeman-go/option"
10+
"github.com/urfave/cli/v3"
11+
)
12+
13+
var psCmd = cli.Command{
14+
Name: "ps",
15+
Usage: "List instances",
16+
Flags: []cli.Flag{
17+
&cli.BoolFlag{
18+
Name: "all",
19+
Aliases: []string{"a"},
20+
Usage: "Show all instances (default: running only)",
21+
},
22+
&cli.BoolFlag{
23+
Name: "quiet",
24+
Aliases: []string{"q"},
25+
Usage: "Only display instance IDs",
26+
},
27+
},
28+
Action: handlePs,
29+
HideHelpCommand: true,
30+
}
31+
32+
func handlePs(ctx context.Context, cmd *cli.Command) error {
33+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
34+
35+
instances, err := client.Instances.List(
36+
ctx,
37+
option.WithMiddleware(debugMiddleware(cmd.Root().Bool("debug"))),
38+
)
39+
if err != nil {
40+
return err
41+
}
42+
43+
showAll := cmd.Bool("all")
44+
quietMode := cmd.Bool("quiet")
45+
46+
// Filter instances
47+
var filtered []hypeman.Instance
48+
for _, inst := range *instances {
49+
if showAll || inst.State == "Running" {
50+
filtered = append(filtered, inst)
51+
}
52+
}
53+
54+
// Quiet mode - just IDs
55+
if quietMode {
56+
for _, inst := range filtered {
57+
fmt.Println(inst.ID)
58+
}
59+
return nil
60+
}
61+
62+
// Table output
63+
if len(filtered) == 0 {
64+
if !showAll {
65+
fmt.Fprintln(os.Stderr, "No running instances. Use -a to show all.")
66+
}
67+
return nil
68+
}
69+
70+
table := NewTableWriter(os.Stdout, "INSTANCE ID", "NAME", "IMAGE", "STATE", "CREATED")
71+
for _, inst := range filtered {
72+
table.AddRow(
73+
TruncateID(inst.ID),
74+
TruncateString(inst.Name, 20),
75+
TruncateString(inst.Image, 25),
76+
string(inst.State),
77+
FormatTimeAgo(inst.CreatedAt),
78+
)
79+
}
80+
table.Render()
81+
82+
return nil
83+
}
84+

0 commit comments

Comments
 (0)