Skip to content
Merged
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: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
golang 1.26.1
golang 1.26.3
golangci-lint 2.11.3
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,12 @@ private helper, test-only — is **not** a shared surface, so skip the mesh for
cmd/
auth/ login, logout, whoami commands
projects/ projects subcommands (list, get, delete)
skills/ skills subcommands (catalog, purchased)
root/ app wiring, Before hook, default action
internal/
api/ resty client, types, all API methods
config/ token storage (~/.createos/.token)
intro/ ASCII banner
ui/ interactive TUI components (skills catalog)
ui/ interactive TUI components
main.go entry point — error display only
```

Expand Down
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Your intelligent infrastructure CLI
```

The official command-line interface for [CreateOS](https://createos.nodeops.network?utm_source=createos-cli) — manage your projects and skills from the terminal.
The official command-line interface for [CreateOS](https://createos.nodeops.network?utm_source=createos-cli) — manage your projects from the terminal.

## Installation

Expand Down Expand Up @@ -304,6 +304,7 @@ Sandboxes are fast-booting VMs — isolated environments you can exec into, sync
| `createos sandbox rm` | Delete one or more sandboxes |
| `createos sandbox exec` | Run a command inside a sandbox |
| `createos sandbox shell` | Open an interactive shell inside a sandbox |
| `createos sandbox process` | Manage reconnectable processes and shell sessions |
| `createos sandbox sync` | Two-way file sync between your laptop and a sandbox |
| `createos sandbox push` | Copy a local file into a sandbox |
| `createos sandbox pull` | Copy a file out of a sandbox |
Expand All @@ -327,6 +328,28 @@ Sandboxes are fast-booting VMs — isolated environments you can exec into, sync
| `--ingress` | Give the sandbox a public HTTPS URL |
| `--auto-pause` | Auto-pause after inactivity (e.g. `10m`, `1h`). Omit to keep running. |

**When to use `exec`, `shell`, `process`, and PTY:**

Use `sandbox exec` for quick non-interactive one-shot commands. Use `sandbox shell` when you want an immediate interactive terminal and do not need to reconnect later. Use `sandbox process` when the command should be manageable after it starts — list it, reconnect to output, send input, wait for it, signal it, or stop it. Add `--pty`/`--tty`/`-t` to `process run` or `process start` when the managed command needs terminal behavior.

| Command | Description |
| -------------------------------------------- | ------------------------------------------------------------ |
| `createos sandbox process run <sb> -- <cmd>` | Run a managed command, stream output, and return its exit code |
| `createos sandbox process start <sb> -- <cmd>` | Start a managed command and print its process ID |
| `createos sandbox process shell <sb>` | Start a persistent shell session that can be reattached |
| `createos sandbox process attach <sb> <proc>` | Reconnect to process output or a shell session |
| `createos sandbox process attach <sb>` | Pick a running process or shell session and attach to it |
| `createos sandbox process list <sb>` | List managed processes and shell sessions |
| `createos sandbox process get <sb> <proc>` | Show details for one managed process |
| `createos sandbox process input <sb> <proc>` | Write input to a process or shell session |
| `createos sandbox process close-stdin <sb> <proc>` | Close stdin for a pipe process |
| `createos sandbox process resize <sb> <proc>` | Resize a managed shell session |
| `createos sandbox process signal <sb> <proc> <signal>` | Send a signal such as `SIGINT` or `SIGTERM` |
| `createos sandbox process wait <sb> <proc>` | Wait for a managed process to exit |
| `createos sandbox process stop <sb> <proc>` | Stop a process and anything it started |

Interactive attach without a process ID shows running managed processes. Pick a PTY shell for an interactive terminal, or pick a pipe process to follow its stdout/stderr output. Managed shell shortcuts are fixed: `Ctrl-]` detaches, `Ctrl-N` creates a new shell and switches to it, and `Ctrl-P` opens the process picker.

**Sandbox sub-resource commands:**

| Command | Description |
Expand Down Expand Up @@ -363,13 +386,6 @@ Sandboxes are fast-booting VMs — isolated environments you can exec into, sync
| `createos webhooks suspend` | Suspend a webhook endpoint |
| `createos webhooks resume` | Resume a suspended webhook endpoint|

### Skills

| Command | Description |
| --------------------------- | -------------------------- |
| `createos skills catalog` | Browse the skills catalog |
| `createos skills purchased` | List your purchased skills |

### Quick Actions

| Command | Description |
Expand Down Expand Up @@ -493,6 +509,19 @@ createos sandbox exec my-box -- uname -a
createos sandbox exec my-box --stream -- pip install requests
createos sandbox shell my-box
createos sandbox shell my-box --ssh
createos sandbox process run my-box -- npm test
createos sandbox process start my-box -- python -m http.server 8000
createos sandbox process shell my-box
createos sandbox process attach my-box proc_abc123
createos sandbox process attach my-box # pick a running process or shell session
createos sandbox process ps my-box
createos sandbox process input my-box proc_abc123 --text "hello\n"
createos sandbox process signal my-box proc_abc123 SIGINT
createos sandbox process wait my-box proc_abc123 --all
createos sandbox process stop my-box proc_abc123 --grace 1s
# Inside a managed shell session, the fixed bottom bar shows active shortcuts.
# detach closes the local attach; new creates a shell and switches;
# switch opens the process picker; `exit` closes the current shell.
createos sandbox push my-box ./script.py /root/script.py
createos sandbox pull my-box /root/output.csv ./output.csv
createos sandbox tunnel my-box --local 8080 --remote 8000
Expand Down Expand Up @@ -602,7 +631,7 @@ createos environments list --project <id> -o json
| `--output, -o <fmt>` | Output format: `json` or `table` (default). Auto-json when piped. |
| `--debug, -d` | Print HTTP request/response details (token is masked) |
| `--api-url` | Override the API base URL |
| `--sandbox-api-url` | Override the sandbox (fc-spawn) base URL |
| `--sandbox-api-url` | Override the sandbox API base URL |
| `--sandbox-gateway` | SSH gateway address (`host:port`) used by `sandbox shell --ssh` |

## Security
Expand Down
1 change: 0 additions & 1 deletion cmd/ask/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ Use the `bash` tool to run `createos` commands on behalf of the user. Always run
- `createos vms` — Deploy, list, get, resize, ssh, reboot, terminate VM instances (`--vm <id>`)
- `createos oauth-clients` — List, create, get instructions, delete OAuth clients (`--client <id>`)
- `createos me` — List and revoke OAuth consents (`--client <id>`)
- `createos skills` — Browse and list purchased skills
- `createos init` — Link the current directory to a project
- `createos status` — Show a project's health and deployment status
- `createos open` — Open a project's live URL in the browser
Expand Down
2 changes: 1 addition & 1 deletion cmd/auth/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewWhoamiCommand() *cli.Command {
createdAt, err := time.Parse(time.RFC3339Nano, u.CreatedAt)
memberSince := u.CreatedAt
if err == nil {
memberSince = createdAt.Format("January 2, 2006")
memberSince = createdAt.Local().Format("January 2, 2006")
}

fmt.Println()
Expand Down
2 changes: 1 addition & 1 deletion cmd/cronjobs/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func newCronjobsActivitiesCommand() *cli.Command {
a.ID,
success,
statusCode,
a.ScheduledAt.Format("2006-01-02 15:04:05"),
a.ScheduledAt.Local().Format("2006-01-02 15:04:05"),
log,
})
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/cronjobs/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func newCronjobsGetCommand() *cli.Command {

if cj.SuspendedAt != nil {
label.Print("Suspended At: ")
fmt.Println(cj.SuspendedAt.Format("2006-01-02 15:04:05"))
fmt.Println(cj.SuspendedAt.Local().Format("2006-01-02 15:04:05"))
}
if cj.SuspendText != nil && *cj.SuspendText != "" {
label.Print("Suspend Text: ")
Expand Down Expand Up @@ -93,9 +93,9 @@ func newCronjobsGetCommand() *cli.Command {
}

label.Print("Created At: ")
fmt.Println(cj.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Println(cj.CreatedAt.Local().Format("2006-01-02 15:04:05"))
label.Print("Updated At: ")
fmt.Println(cj.UpdatedAt.Format("2006-01-02 15:04:05"))
fmt.Println(cj.UpdatedAt.Local().Format("2006-01-02 15:04:05"))

return nil
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/cronjobs/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func newCronjobsListCommand() *cli.Command {
cj.Schedule,
cj.Type,
cj.Status,
cj.CreatedAt.Format("2006-01-02 15:04:05"),
cj.CreatedAt.Local().Format("2006-01-02 15:04:05"),
})
}
_ = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() //nolint:errcheck
Expand Down
6 changes: 3 additions & 3 deletions cmd/deployments/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func pickDeployment(client *api.APIClient, projectID string, statusFilter []stri
d.ID,
d.Status,
d.Extra.Endpoint,
d.CreatedAt.Format("2006-01-02 15:04:05"),
d.CreatedAt.Local().Format("2006-01-02 15:04:05"),
})
}
fmt.Println()
Expand All @@ -93,7 +93,7 @@ func pickDeployment(client *api.APIClient, projectID string, statusFilter []stri
if len(id) > 8 {
id = id[:8]
}
label := fmt.Sprintf("%s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, id)
label := fmt.Sprintf("%s %s %s", d.CreatedAt.Local().Format("Jan 02 15:04"), d.Status, id)
if d.Source != nil && d.Source.Commit != "" {
commit := d.Source.Commit
if len(commit) > 7 {
Expand All @@ -103,7 +103,7 @@ func pickDeployment(client *api.APIClient, projectID string, statusFilter []stri
if len(msg) > 50 {
msg = msg[:50] + "…"
}
label = fmt.Sprintf("%s %s %s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, id, commit, msg)
label = fmt.Sprintf("%s %s %s %s %s", d.CreatedAt.Local().Format("Jan 02 15:04"), d.Status, id, commit, msg)
}
options[i] = label
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/deployments/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func newDeploymentsListCommand() *cli.Command {
d.ID,
d.Status,
d.Extra.Endpoint,
d.CreatedAt.Format("2006-01-02 15:04:05"),
d.CreatedAt.Local().Format("2006-01-02 15:04:05"),
})
}
if err := pterm.DefaultTable.WithHasHeader().WithData(tableData).Render(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/environments/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func newEnvironmentsListCommand() *cli.Command {
env.Status,
env.Extra.Endpoint,
domains,
env.CreatedAt.Format("2006-01-02 15:04:05"),
env.CreatedAt.Local().Format("2006-01-02 15:04:05"),
})
}
if err := pterm.DefaultTable.WithHasHeader().WithData(tableData).Render(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/oauth/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func newListCommand() *cli.Command {
tableData = append(tableData, []string{
item.ID,
item.Name,
item.CreatedAt.Format("2006-01-02 15:04:05"),
item.CreatedAt.Local().Format("2006-01-02 15:04:05"),
})
}
if err := pterm.DefaultTable.WithHasHeader().WithData(tableData).Render(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/open/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func NewOpenCommand() *cli.Command {
if len(id) > 8 {
id = id[:8]
}
options[i] = fmt.Sprintf("%s %s %s", d.CreatedAt.Format("Jan 02 15:04"), d.Status, id)
options[i] = fmt.Sprintf("%s %s %s", d.CreatedAt.Local().Format("Jan 02 15:04"), d.Status, id)
}
if !terminal.IsInteractive() {
return fmt.Errorf("multiple deployments found — use 'createos deployments list' and pass the deployment ID")
Expand Down
2 changes: 1 addition & 1 deletion cmd/projects/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func newGetCommand() *cli.Command {
fmt.Println(project.Status)

cyan.Printf("Created At: ")
fmt.Println(project.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Println(project.CreatedAt.Local().Format("2006-01-02 15:04:05"))

return nil
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/projects/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func newListCommand() *cli.Command {
p.DisplayName,
p.Status,
p.Type,
p.CreatedAt.Format("2006-01-02 15:04:05"),
p.CreatedAt.Local().Format("2006-01-02 15:04:05"),
})
}
_ = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() //nolint:errcheck
Expand Down
33 changes: 29 additions & 4 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"github.com/NodeOps-app/createos-cli/cmd/projects"
"github.com/NodeOps-app/createos-cli/cmd/sandbox"
"github.com/NodeOps-app/createos-cli/cmd/scale"
"github.com/NodeOps-app/createos-cli/cmd/skills"
"github.com/NodeOps-app/createos-cli/cmd/status"
"github.com/NodeOps-app/createos-cli/cmd/templates"
"github.com/NodeOps-app/createos-cli/cmd/upgrade"
Expand Down Expand Up @@ -62,7 +61,7 @@ func NewApp() *cli.App {
},
&cli.StringFlag{
Name: "sandbox-api-url",
Usage: "Override the sandbox (fc-spawn) base URL",
Usage: "Override the sandbox API base URL",
EnvVars: []string{"CREATEOS_SANDBOX_URL"},
Value: api.DefaultSandboxBaseURL,
},
Expand Down Expand Up @@ -182,7 +181,6 @@ func NewApp() *cli.App {
fmt.Println(" projects Manage projects")
fmt.Println(" sandbox Manage sandboxes")
fmt.Println(" scale Adjust replicas and resources")
fmt.Println(" skills Manage skills")
fmt.Println(" status Show project health and deployment status")
fmt.Println(" templates Browse and scaffold from project templates")
fmt.Println(" vms Manage VM terminal instances")
Expand Down Expand Up @@ -219,7 +217,6 @@ func NewApp() *cli.App {
projects.NewProjectsCommand(),
sandbox.NewSandboxCommand(),
scale.NewScaleCommand(),
skills.NewSkillsCommand(),
status.NewStatusCommand(),
templates.NewTemplatesCommand(),
upgrade.NewUpgradeCommand(),
Expand All @@ -230,10 +227,38 @@ func NewApp() *cli.App {
versioncmd.NewVersionCommand(),
},
}
installTrailingHelpGuards(app.Commands)

return app
}

func installTrailingHelpGuards(commands []*cli.Command) {
for _, cmd := range commands {
if cmd == nil {
continue
}
if cmd.Action != nil {
original := cmd.Action
cmd.Action = func(c *cli.Context) error {
if argsRequestHelp(c) {
return cli.ShowSubcommandHelp(c)
}
return original(c)
}
}
installTrailingHelpGuards(cmd.Subcommands)
}
}

func argsRequestHelp(c *cli.Context) bool {
for _, arg := range c.Args().Slice() {
if arg == "-h" || arg == "--help" || arg == "help" {
return true
}
}
return false
}

// refreshOAuthSession exchanges the session's refresh token for a new
// access token, updates the session in place, and persists it to
// ~/.createos/.oauth. It returns the new access token. This is shared by
Expand Down
2 changes: 1 addition & 1 deletion cmd/sandbox/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func newCreateCommand() *cli.Command {
Aliases: []string{"c"},
Usage: "Create a new sandbox",
ArgsUsage: " ",
Description: `Create a new sandbox on fc-spawn.
Description: `Create a new sandbox.

Examples:
# Smallest possible sandbox
Expand Down
4 changes: 2 additions & 2 deletions cmd/sandbox/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func runDiskList(c *cli.Context) error {
for _, d := range disks {
table = append(table, []string{
d.Name, d.ID, d.Kind, d.Config.Bucket,
d.CreatedAt.Format("2006-01-02 15:04"),
d.CreatedAt.Local().Format("2006-01-02 15:04"),
})
}
_ = pterm.DefaultTable.WithHasHeader().WithData(table).Render() //nolint:errcheck
Expand Down Expand Up @@ -253,7 +253,7 @@ func runDiskShow(c *cli.Context) error {
if d.Config.UsePathStyle {
row("Path style", "yes")
}
row("Created", d.CreatedAt.Format("2006-01-02 15:04:05"))
row("Created", d.CreatedAt.Local().Format("2006-01-02 15:04:05"))
})
return nil
}
Expand Down
12 changes: 10 additions & 2 deletions cmd/sandbox/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@ func newExecCommand() *cli.Command {
Name: "exec",
Usage: "Run a command inside a sandbox",
ArgsUsage: "<sandbox> -- <cmd> [args…]",
Description: `Run a one-shot command inside a sandbox. Anything after the literal
'--' becomes the command. The default is a buffered exec — output
Description: `Run a one-shot command inside a sandbox.

Use this for quick non-interactive commands where you do not need a
process ID, reconnect, stdin after start, or a TTY. Anything after the
literal '--' becomes the command. The default is buffered exec — output
arrives all at once when the command finishes. Pass --stream to see
stdout/stderr live as it happens.

Use 'sandbox shell' for an immediate interactive terminal.
Use 'sandbox process' for long-running or reconnectable commands.
Use 'sandbox process run --pty' when the command needs a TTY but should
also be managed as a process.

Examples:
createos sandbox exec my-box -- uname -a
createos sandbox exec my-box -- python3 -c 'print("hi")'
Expand Down
2 changes: 1 addition & 1 deletion cmd/sandbox/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func printSandbox(s *api.SandboxView, bw *api.BandwidthView) {
if t == nil {
return
}
row(k, t.Format("2006-01-02 15:04:05"))
row(k, t.Local().Format("2006-01-02 15:04:05"))
}

// Header
Expand Down
2 changes: 1 addition & 1 deletion cmd/sandbox/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func runList(c *cli.Context) error {
r.Status,
r.Shape,
ptrOrDash(r.IP),
r.CreatedAt.Format("2006-01-02 15:04"),
r.CreatedAt.Local().Format("2006-01-02 15:04"),
})
}
_ = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() //nolint:errcheck
Expand Down
Loading