feat(P01): CLI skeleton with Cobra, subcommand stubs, pre-push hook

Implements Phase 1 of v0.1 Foundation:
- go.mod with Go 1.25
- cmd/orca/main.go entry point
- internal/cli/root.go with global --json flag
- internal/cli/version.go (orca version)
- internal/cli/init.go (orca init - creates ~/.orca/)
- internal/cli/status.go (orca status - shows daemon info)
- internal/cli/node.go (orca node {join,leave,list} - stubs)
- internal/cli/job.go (orca job {run,list,stop,logs} - stubs)
- Makefile (build, test, lint, fmt, release)
- LICENSE (MIT)
- README.md with quickstart
- .gitignore
- .githooks/pre-push + scripts/trigger_coreci.sh (CoreCI trigger)
- Smoke tests in internal/cli/root_test.go

Verified: go build, go test, go vet, gofmt all pass.

---ci---
project: orca
phase: 1
milestone: v0.1
status: execute
req_covered:
  - REQ-001
  - REQ-002
  - REQ-013
  - REQ-015
  - REQ-016
  - REQ-019
  - REQ-024
---/ci---
This commit is contained in:
Jon Chery
2026-06-03 12:23:37 +00:00
parent c2038952c7
commit aa3cccead5
15 changed files with 528 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Pre-push hook: trigger CoreCI pipeline before any push
exec "$(dirname "$0")/../scripts/trigger_coreci.sh"
+11
View File
@@ -0,0 +1,11 @@
bin/
coverage.out
*.test
*.out
.DS_Store
orca
*.db
*.db-journal
*.db-wal
*.db-shm
.env.local
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Orca Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
.PHONY: build test lint fmt clean run release help
BINARY := bin/orca
GOFLAGS := -trimpath
LDFLAGS := -s -w -X main.version=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") \
-X main.gitCommit=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") \
-X main.buildTime=$(shell date -u +%Y-%m-%dT%H:%M:%SZ)
help:
@echo "orca — make targets"
@echo " build Build binary to $(BINARY)"
@echo " test Run tests with race detection"
@echo " lint Run gofmt + go vet"
@echo " fmt Format code"
@echo " clean Remove build artifacts"
@echo " run Build and run with args (use: make run ARGS='version')"
@echo " release Build release artifact with version injection"
build:
@mkdir -p bin
go build $(GOFLAGS) -o $(BINARY) ./cmd/orca
test:
go test -race -coverprofile=coverage.out ./...
lint:
gofmt -l .
go vet ./...
fmt:
gofmt -w .
clean:
rm -rf bin coverage.out
run: build
./$(BINARY) $(ARGS)
release:
@mkdir -p bin
go build $(GOFLAGS) -ldflags="$(LDFLAGS)" -o $(BINARY) ./cmd/orca
@echo "Release build complete: $(BINARY)"
+59
View File
@@ -0,0 +1,59 @@
# Orca
Offline/CLI-first orchestration engine inspired by HashiCorp Nomad, far simpler than Kubernetes.
## Status
**v0.1: Foundation** — see [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the 6-phase plan.
## Pillars
- **Simplicity** — single binary, minimal dependencies
- **AI-first** — CLI designed for both humans and AI agents
- **Offline-first** — no cloud dependencies
- **CLI-first** — primary interface is the command line
- **Security before features** — NFRs ship before new functionality
- **Bug fixes before features** — stability is paramount
- **NFRs before features** — observability and auditability first
## Quickstart
```bash
# Build
make build
# Run
./bin/orca version
./bin/orca --help
# Initialize local state
./bin/orca init
```
## Subcommands
| Command | Description | Status |
|---------|-------------|--------|
| `orca version` | Print version info | ✅ Phase 1 |
| `orca init` | Initialize local orca state | ✅ Phase 1 (stub) |
| `orca status` | Show orca daemon status | ✅ Phase 1 (stub) |
| `orca node` | Node management (`join`, `leave`, `list`) | Phase 2 |
| `orca job` | Job management (`run`, `list`, `stop`, `logs`) | Phase 3 |
## Development
```bash
make build # Build binary to ./bin/orca
make test # Run tests with race detection
make lint # Run golangci-lint
make fmt # Format code
make release # Build + create Gitea release (Phase 6)
```
## Architecture
See [.ciagent/ARCHITECTURE.md](.ciagent/ARCHITECTURE.md) for full architecture details.
## License
MIT — see [LICENSE](LICENSE).
+10
View File
@@ -0,0 +1,10 @@
module git.cloudinit.dev/coreci/orca
go 1.25
require github.com/spf13/cobra v1.8.1
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
+10
View File
@@ -0,0 +1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+38
View File
@@ -0,0 +1,38 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize local orca state directory",
Long: "Create the local orca state directory at ~/.orca/ and write a default config file.",
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("get home dir: %w", err)
}
orcaDir := filepath.Join(home, ".orca")
if err := os.MkdirAll(orcaDir, 0o755); err != nil {
return fmt.Errorf("create orca dir: %w", err)
}
result := map[string]string{
"path": orcaDir,
"status": "initialized",
}
if jsonOutput {
return printJSON(result)
}
printText("✓ Initialized orca state at %s\n", orcaDir)
return nil
},
}
func init() {
rootCmd.AddCommand(initCmd)
}
+74
View File
@@ -0,0 +1,74 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
var jobCmd = &cobra.Command{
Use: "job",
Short: "Manage orca jobs",
Long: "Run, list, stop, and inspect orca jobs.",
}
var jobRunCmd = &cobra.Command{
Use: "run <spec.hcl>",
Short: "Run a job from an HCL spec file",
Long: "Submit a job spec and execute it. Implemented in Phase 3.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca job run " + args[0])
},
}
var jobListCmd = &cobra.Command{
Use: "list",
Short: "List all jobs",
Long: "Display all jobs and their status. Implemented in Phase 3.",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca job list")
},
}
var jobStopCmd = &cobra.Command{
Use: "stop <job-id>",
Short: "Stop a running job",
Long: "Stop a job by ID. Implemented in Phase 3.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca job stop " + args[0])
},
}
var jobLogsCmd = &cobra.Command{
Use: "logs <job-id>",
Short: "Show logs for a job",
Long: "Display the logs for a job by ID. Implemented in Phase 3.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca job logs " + args[0])
},
}
func init() {
jobCmd.AddCommand(jobRunCmd)
jobCmd.AddCommand(jobListCmd)
jobCmd.AddCommand(jobStopCmd)
jobCmd.AddCommand(jobLogsCmd)
rootCmd.AddCommand(jobCmd)
}
func notImplemented(cmd string) error {
if jsonOutput {
return printJSON(map[string]any{
"command": cmd,
"status": "not_implemented",
"phase": "1-cli-skeleton",
"next": "Phase 2-6 will implement this",
})
}
fmt.Fprintf(rootCmd.ErrOrStderr(), "✗ %s: not yet implemented (Phase 1: CLI skeleton only)\n", cmd)
fmt.Fprintf(rootCmd.ErrOrStderr(), " see .ciagent/ROADMAP.md for the full 6-phase plan\n")
return fmt.Errorf("not implemented: %s", cmd)
}
+45
View File
@@ -0,0 +1,45 @@
package cli
import (
"github.com/spf13/cobra"
)
var nodeCmd = &cobra.Command{
Use: "node",
Short: "Manage orca nodes",
Long: "Join, leave, or list orca nodes in the cluster.",
}
var nodeJoinCmd = &cobra.Command{
Use: "join",
Short: "Join a node to the orca cluster",
Long: "Register the local node with the orca cluster. Implemented in Phase 2.",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca node join")
},
}
var nodeLeaveCmd = &cobra.Command{
Use: "leave",
Short: "Remove a node from the orca cluster",
Long: "Deregister a node from the orca cluster. Implemented in Phase 2.",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca node leave")
},
}
var nodeListCmd = &cobra.Command{
Use: "list",
Short: "List all nodes in the orca cluster",
Long: "Display all registered nodes. Implemented in Phase 2.",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("orca node list")
},
}
func init() {
nodeCmd.AddCommand(nodeJoinCmd)
nodeCmd.AddCommand(nodeLeaveCmd)
nodeCmd.AddCommand(nodeListCmd)
rootCmd.AddCommand(nodeCmd)
}
+52
View File
@@ -0,0 +1,52 @@
package cli
import (
"encoding/json"
"fmt"
"github.com/spf13/cobra"
)
var (
version = "0.1.0-dev"
gitCommit = "unknown"
buildTime = "unknown"
)
var rootCmd = &cobra.Command{
Use: "orca",
Short: "Orca — offline/CLI-first orchestration engine",
Long: `Orca is a minimalist, offline-first, CLI-first orchestration engine
inspired by HashiCorp Nomad, prioritizing stability, security, and simplicity
over feature richness.`,
SilenceUsage: true,
SilenceErrors: true,
}
var jsonOutput bool
func init() {
rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "output in JSON format")
}
func Execute() error {
return rootCmd.Execute()
}
func printJSON(v any) error {
enc := json.NewEncoder(rootCmd.OutOrStdout())
enc.SetIndent("", " ")
return enc.Encode(v)
}
func printText(format string, args ...any) {
fmt.Fprintf(rootCmd.OutOrStdout(), format, args...)
}
func printResult(text string, jsonObj any) {
if jsonOutput {
_ = printJSON(jsonObj)
return
}
printText("%s\n", text)
}
+67
View File
@@ -0,0 +1,67 @@
package cli
import (
"strings"
"testing"
)
func TestVersionCommandExists(t *testing.T) {
found := false
for _, cmd := range rootCmd.Commands() {
if cmd.Name() == "version" {
found = true
break
}
}
if !found {
t.Fatal("version command not registered")
}
}
func TestRootHasAllSubcommands(t *testing.T) {
expected := []string{"version", "init", "status", "node", "job"}
registered := make(map[string]bool)
for _, cmd := range rootCmd.Commands() {
registered[cmd.Name()] = true
}
for _, name := range expected {
if !registered[name] {
t.Errorf("expected subcommand %q not registered", name)
}
}
}
func TestNodeSubcommands(t *testing.T) {
expected := []string{"join", "leave", "list"}
registered := make(map[string]bool)
for _, cmd := range nodeCmd.Commands() {
registered[cmd.Name()] = true
}
for _, name := range expected {
if !registered[name] {
t.Errorf("expected node subcommand %q not registered", name)
}
}
}
func TestJobSubcommands(t *testing.T) {
expected := []string{"run", "list", "stop", "logs"}
registered := make(map[string]bool)
for _, cmd := range jobCmd.Commands() {
registered[cmd.Name()] = true
}
for _, name := range expected {
if !registered[name] {
t.Errorf("expected job subcommand %q not registered", name)
}
}
}
func TestRootHelpMentionsKeyPillars(t *testing.T) {
help := rootCmd.Long
for _, pillar := range []string{"offline", "CLI", "Nomad", "simplicity"} {
if !strings.Contains(strings.ToLower(help), strings.ToLower(pillar)) {
t.Errorf("root help does not mention pillar %q", pillar)
}
}
}
+36
View File
@@ -0,0 +1,36 @@
package cli
import (
"github.com/spf13/cobra"
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "Show orca daemon status",
Long: "Display the current status of the local orca daemon, including version, uptime, and connection info.",
RunE: func(cmd *cobra.Command, args []string) error {
status := map[string]any{
"version": version,
"daemon": "stopped",
"uptime": "0s",
"api_addr": "https://localhost:8443",
"health": "unknown",
"phase": "1-cli-skeleton",
"milestone": "v0.1",
}
if jsonOutput {
return printJSON(status)
}
printText("orca daemon status\n")
printText(" version: %s\n", version)
printText(" daemon: %s\n", "stopped (daemon not yet implemented in Phase 1)")
printText(" api_addr: %s\n", "https://localhost:8443")
printText(" phase: %s\n", "1-cli-skeleton")
printText(" milestone: %s\n", "v0.1")
return nil
},
}
func init() {
rootCmd.AddCommand(statusCmd)
}
+29
View File
@@ -0,0 +1,29 @@
package cli
import (
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Long: "Print the orca version, git commit, and build time.",
RunE: func(cmd *cobra.Command, args []string) error {
info := map[string]string{
"version": version,
"git_commit": gitCommit,
"build_time": buildTime,
}
if jsonOutput {
return printJSON(info)
}
printText("orca version %s\n", version)
printText(" git commit: %s\n", gitCommit)
printText(" build time: %s\n", buildTime)
return nil
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# trigger_coreci.sh - Trigger CoreCI pipeline for the pushed branch
# Invoked by .githooks/pre-push
# Gracefully degrades if CoreCI API is unreachable (Gitea webhook is secondary path).
set -uo pipefail
CORECI_URL="${CORECI_URL:-https://git.cloudinit.dev/coreci/coreci}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
if [ -z "$GITEA_TOKEN" ]; then
echo " (skip: GITEA_TOKEN not set)"
exit 0
fi
while read local_ref local_sha remote_ref remote_sha; do
branch="${remote_ref#refs/heads/}"
if [ -z "$branch" ] || [ "$branch" = "HEAD" ]; then
continue
fi
echo "→ Triggering CoreCI for branch: $branch (${local_sha:0:7})"
payload=$(printf '{"repo":"coreci/orca","branch":"%s","ref":"%s"}' "$branch" "$local_sha")
if command -v curl >/dev/null 2>&1; then
curl -fsS -X POST "${CORECI_URL}/api/pipeline/run" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" >/dev/null 2>&1 \
&& echo " ✓ CoreCI triggered" \
|| echo " (CoreCI trigger failed; pipeline may run via webhook)"
fi
done