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
17 changes: 17 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,20 @@ jobs:
# footprint gates stay off (as on starbox).
cov-min: 65
secrets: inherit

# The examples/ build-your-own demos are their own Go module (minimal deps, out
# of the main coverage gate), so they build in a separate, non-gating job rather
# than inside the reusable CI. This just proves the demos and the kit API they
# use still compile and vet clean.
examples:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- name: Build and vet the examples module
working-directory: examples
run: |
go build ./...
go vet ./...
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,27 @@ direct.star:1:7: undefined: undefined_name
check: 1 problem(s) found
```

## Build your own CLI

StarCLI is the **standard, fully-loaded** build. It is assembled from a small
reusable core — the [`kit`](kit) package — that you can use to build your **own**
CLI: a few-line Go shell that embeds your Starlark scripts and wires only the
modules you need. Your shell and the standard StarCLI construct their runtime
through the same path, so they behave identically.

```go
//go:embed app.star
var app string

func main() {
// embed the script + pick modules + run, in one call
kit.Run(app, kit.WithModules("json", "math"))
}
```

See [`examples/`](examples) for runnable demos (a minimal shell, and one wiring a
single starpkg module) and the build-your-own quickstart.

## Configuration

StarCLI can be configured through a config file (YAML format) using the `-C` or `--config` flag:
Expand Down
83 changes: 39 additions & 44 deletions cli/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"sync"

"github.com/1set/starbox"
"github.com/1set/starcli/kit"
"github.com/1set/starlet"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
Expand Down Expand Up @@ -43,10 +45,15 @@ type BoxOpts struct {
execCmd bool // derived from the grant: construct cmd ENABLED (allow-all)
}

// BuildBox creates a new Starbox with the given options. By default every wired
// module is available (the open posture); a restrictive --caps tier / STAR_CAPS
// or an --allow-* flag installs a capability load gate so only the permitted
// modules may be loaded.
// BuildBox creates a new Starbox with the given options. It is the standard,
// fully-loaded instance of the shared kit core (kit.Box): the CLI-specific
// concerns — capability gating, scenario-driven printer, file logging — are
// resolved here and handed to kit as options, so the turnkey CLI and any
// build-your-own shell construct their runtime through exactly the same path.
//
// By default every wired module is available (the open posture); a restrictive
// --caps tier / STAR_CAPS or an --allow-* flag installs a capability load gate so
// only the permitted modules may be loaded.
func BuildBox(opts *BoxOpts) (*starbox.Starbox, error) {
if !validCapsTier(opts.caps) {
return nil, fmt.Errorf("unknown --caps value %q (want: open, full, network, or safe)", opts.caps)
Expand All @@ -56,14 +63,33 @@ func BuildBox(opts *BoxOpts) (*starbox.Starbox, error) {
// only when the grant permits execution; otherwise cmd loads disabled.
opts.execCmd = grant.execCmd

var box *starbox.Starbox
if grant.unrestricted() {
// Default open posture: no load gate, every wired module is loadable.
box = starbox.New(opts.name)
} else {
// set print function: TODO: for scenario, and throw errors
pf, err := getPrinterFunc(opts.scenario, opts.printerName)
if err != nil {
return nil, err
}

kitOpts := []kit.Option{
// execution budgets (0 == unlimited): a step budget bounds runaway loops
// that a wall-clock timeout cannot stop; an output cap bounds result size.
kit.WithMaxSteps(opts.maxSteps),
kit.WithMaxOutputEntries(opts.maxOutput),
// the CLI prints results itself, so keep raw Starlark values.
kit.WithOutputConversion(false),
kit.WithGlobalReassign(opts.globalReassign),
kit.WithRecursion(opts.recursion),
kit.WithPrintFunc(pf),
// every starpkg module is resolved on demand from the CLI's registry.
kit.WithDynamicLoader(func(name string) (starlet.ModuleLoader, error) {
return loadCLIModuleByName(opts, name)
}),
kit.WithModules(opts.moduleToLoad...),
}

if !grant.unrestricted() {
// A tier/flag narrowed the grant: gate loading to the permitted set.
policy := starbox.Policy{Modules: starbox.ModuleAllow{Names: grant.allowedModules(getDefaultModules())}}
box = starbox.NewWithPolicy(opts.name, policy)
kitOpts = append(kitOpts, kit.WithPolicy(policy))
}

// Route the script's `log` module output to a file when requested (C-4):
Expand All @@ -74,45 +100,14 @@ func BuildBox(opts *BoxOpts) (*starbox.Starbox, error) {
if err != nil {
return nil, err
}
box.SetLogger(lg)
kitOpts = append(kitOpts, kit.WithLogger(lg))
}

if strings.TrimSpace(opts.includePath) != "" {
box.SetFS(os.DirFS(opts.includePath))
kitOpts = append(kitOpts, kit.WithFS(os.DirFS(opts.includePath)))
}

// execution budgets (0 == unlimited): a step budget bounds runaway loops
// that a wall-clock timeout cannot stop; an output cap bounds result size.
box.SetMaxExecutionSteps(opts.maxSteps)
box.SetMaxOutputEntries(opts.maxOutput)

// machine-level knobs
mac := box.GetMachine()
mac.SetOutputConversionEnabled(false)
if opts.globalReassign {
mac.EnableGlobalReassign()
} else {
mac.DisableGlobalReassign()
}
if opts.recursion {
mac.EnableRecursionSupport()
} else {
mac.DisableRecursionSupport()
}

// set print function: TODO: for scenario, and throw errors
pf, err := getPrinterFunc(opts.scenario, opts.printerName)
if err != nil {
return nil, err
}
box.SetPrintFunc(pf)

// load modules
box.SetModuleSet(starbox.EmptyModuleSet) // force clean the module set
if err := loadModules(box, opts); err != nil {
return nil, err
}
return box, nil
return kit.New(opts.name, kitOpts...).Box()
}

var (
Expand Down
24 changes: 4 additions & 20 deletions cli/mods.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"sort"

"github.com/1set/starbox"
"github.com/1set/starcli/config"
"github.com/1set/starcli/module/args"
"github.com/1set/starcli/module/sys"
Expand Down Expand Up @@ -57,25 +56,10 @@ func getDefaultModules() []string {
return allMods
}

// loadModules loads the given modules into the Starbox instance.
func loadModules(box *starbox.Starbox, opts *BoxOpts) error {
usrMods := opts.moduleToLoad
if len(usrMods) == 0 {
// no modules to load
log.Debugw("no modules to load", "user_modules", usrMods)
return nil
}

// set dynamic module loader
box.SetDynamicModuleLoader(func(name string) (starlet.ModuleLoader, error) {
return loadCLIModuleByName(opts, name)
})
box.AddModulesByName(usrMods...)

// all is well
return nil
}

// loadCLIModuleByName is the CLI's module registry: it resolves a module name to
// its loader on demand. BuildBox installs it as the kit dynamic loader, so the
// turnkey CLI exposes every starpkg module while a build-your-own shell wires
// only the few it imports.
func loadCLIModuleByName(opts *BoxOpts, name string) (starlet.ModuleLoader, error) {
switch name {
case args.ModuleName:
Expand Down
91 changes: 91 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Build your own — starcli `kit` examples

`starcli` is the **standard, fully-loaded** Star\* CLI. But it is built on a small
reusable core — the [`kit`](../kit) package — that you can use to assemble your
**own** CLI: a single Go shell that embeds your Starlark scripts and wires only
the modules you need. The standard `starcli` and your shell construct their
runtime through exactly the same path, so they behave the same.

> **The idea:** Go is the shell, Starlark is the app. Your `main.go` stays a few
> lines and never changes; your logic lives in `.star` files you `//go:embed`.

This folder is a **separate Go module** on purpose: each demo depends on only the
runtime plus the modules it wires, so its `go.mod` is the honest, minimal
dependency tree a real shell would have.

## Run the demos

```bash
cd examples
go run ./hello # embed one .star, run it with a builtin module
go run ./qrcard # wire one starpkg module (qrcode) into the shell
```

## `hello` — the smallest shell

The entire program is the embed + one call. Everything else is Starlark.

```go
//go:embed app.star
var app string

func main() {
if _, err := kit.Run(app, kit.WithModules("math")); err != nil {
log.Fatal(err)
}
}
```

## `qrcard` — wiring a starpkg module

A build-your-own shell picks the domain modules it wants and hands their loaders
to the kit. Here the shell imports just `github.com/starpkg/qrcode`:

```go
import "github.com/starpkg/qrcode"

func main() {
kit.Run(app, kit.WithLoader(qrcode.ModuleName, qrcode.NewModule().LoadModule()))
}
```

Its dependency tree is `starbox` + `qrcode` and nothing else — none of the other
starpkg modules the turnkey `starcli` carries.

## How modules resolve

`kit` follows starbox's resolution order, so you mix three styles freely:

| You write | Resolves as | Use for |
|---|---|---|
| `kit.WithModules("json", "math")` | starlet **builtins** (auto) | the standard library |
| `kit.WithLoader("qrcode", loader)` | an **explicit** loader you import | a specific starpkg/custom module |
| `kit.WithDynamicLoader(fn)` | a **registry** resolved on demand | exposing many modules at once (this is how `starcli` itself wires every starpkg module) |

## Shipping a tree of scripts

For more than one script, embed a whole directory and run an entry point — the
other files are reachable via `load()`:

```go
//go:embed scripts/*.star
var scripts embed.FS

func main() {
kit.RunFS(scripts, "scripts/main.star", kit.WithModules("json"))
}
```

## Common knobs

```go
kit.Run(app,
kit.WithModules("json", "math"),
kit.WithGlobal("env", "prod"), // inject host values as script globals
kit.WithMaxSteps(1_000_000), // bound runaway loops
kit.WithMaxOutputEntries(100), // bound result size
kit.WithPrintFunc(myPrinter), // control how print() renders
)
```

See the [`kit` package docs](../kit/kit.go) for the full option set.
35 changes: 35 additions & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Separate module on purpose: these build-your-own demos each depend on only the
// runtime plus the modules they wire (not starcli's full module set), so this is
// the honest, minimal dependency tree a real shell would have. It also keeps the
// example mains out of the main module's coverage gate. CI builds it in its own
// non-gating job. The replace points at the in-repo kit until starcli is tagged.
module github.com/1set/starcli/examples

go 1.25.8

require (
github.com/1set/starcli v0.0.0
github.com/starpkg/qrcode v0.1.0
)

require (
github.com/1set/starbox v0.2.0 // indirect
github.com/1set/starlet v0.2.2 // indirect
github.com/1set/starlight v0.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/chzyer/readline v1.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/h2so5/here v0.0.0-20200815043652-5e14eb691fae // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/spyzhov/ajson v0.9.6 // indirect
github.com/starpkg/base v0.1.1 // indirect
go.starlark.net v0.0.0-20260324133313-ffb3f39dd27a // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/sys v0.45.0 // indirect
)

replace github.com/1set/starcli => ../
61 changes: 61 additions & 0 deletions examples/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
github.com/1set/starbox v0.2.0 h1:49VR41rprA0r5SBS2CQcwe1eRz/2WbmQ61J3RAPIzP4=
github.com/1set/starbox v0.2.0/go.mod h1:9OXM57bi3G9tGf/oxPo4rKAZFMHznHkFIROAusmNvBk=
github.com/1set/starlet v0.2.2 h1:TWEbuY5O3291GvRwxHLMiJCSGWhI1ZMdXe+nCtBejW0=
github.com/1set/starlet v0.2.2/go.mod h1:3Sz9ToVkumS1OMojGmb37iLqd69UVTr1PWFOXaiC+rA=
github.com/1set/starlight v0.2.0 h1:W9yulJYANolyMLMOH0M4xcW8RQVpi68opSdNmoipcic=
github.com/1set/starlight v0.2.0/go.mod h1:o9KiJBpy92daHyNHBUwS0nFIjjLxLM/XmRxAZf4FIaE=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/h2so5/here v0.0.0-20200815043652-5e14eb691fae h1:ghqI9EdSyyIL2iuOM9UIGVO7kEYQFVLKAUIFoOea5MY=
github.com/h2so5/here v0.0.0-20200815043652-5e14eb691fae/go.mod h1:Q+Ziz4FsuRTHql1UqcQ3iZwl9LcKpi7mVVgn20Rj+IU=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e h1:51xcRlSMBU5rhM9KahnJGfEsBPVPz3182TgFRowA8yY=
github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ=
github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
github.com/starpkg/base v0.1.1 h1:AeNOEIyuyQJoQjgU+AxAE3+1ebgLlsF1HmA49tdTOtU=
github.com/starpkg/base v0.1.1/go.mod h1:0QLSawzBUCFLNk1jMRpzNs6AZH9SVpumd2J7WLOPrR0=
github.com/starpkg/qrcode v0.1.0 h1:VULD2XBVacMq0c2WxJgdG0d3k0Z4EOSaFdYdkOB7bME=
github.com/starpkg/qrcode v0.1.0/go.mod h1:itKiBubTuioyMM78bA+RTfwm/hH3EYG+W6NX0cscgh0=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
go.starlark.net v0.0.0-20260324133313-ffb3f39dd27a h1:w7OMj6r/AoxBpbfncRXaV18hjzIAFRytYaRILymmMRE=
go.starlark.net v0.0.0-20260324133313-ffb3f39dd27a/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading
Loading