diff --git a/README.md b/README.md index 6613de2..f1e1ad3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ StarCLI is a versatile tool that provides a convenient environment for running S - Base64 encoding/decoding - Email functionality - LLM (Language Model) integration + - Templating, data formats & utilities (Liquid, TOML/YAML, QR codes, TOTP, emoji, in-memory cache) - And many more ## Installation @@ -77,7 +78,7 @@ Usage of ./starcli: --log-format string log file format: console (human) or json (machine) (default "console") --max-output uint max top-level output entries per run (0=unlimited) --max-steps uint max Starlark execution steps per run, guards runaway loops (0=unlimited) - -m, --module strings allowed modules to preload and load (default [args,atom,base64,cmd,csv,email,file,go_idiomatic,gum,hashlib,http,json,llm,log,markdown,math,net,path,random,re,regex,runtime,serial,sqlite,stats,string,struct,sys,time,web]) + -m, --module strings allowed modules to preload and load (default [args,atom,base64,cache,cmd,csv,email,emoji,file,go_idiomatic,gum,hashlib,http,json,liquid,llm,log,markdown,math,net,path,qrcode,random,re,regex,runtime,serial,sqlite,stats,string,struct,sys,time,toml,totp,web,yaml]) -o, --output string output printer: none,stdout,stderr,basic,lineno,since,auto (default "auto") --record string record the complete session output (stdout+stderr) to this transcript file -r, --recursion allow recursion in Starlark code diff --git a/cli/capability.go b/cli/capability.go index 3edbc90..e453d2c 100644 --- a/cli/capability.go +++ b/cli/capability.go @@ -31,6 +31,13 @@ const modCmd = "cmd" var starpkgCaps = map[string]starlet.ModuleCapability{ "args": starlet.CapPure, // argparse-style parsing of the captured argv "markdown": starlet.CapPure, // goldmark render, no host effect + "cache": starlet.CapPure, // in-process key/value cache, no host effect + "emoji": starlet.CapPure, // emoji/symbol text conversion + "liquid": starlet.CapPure, // Shopify Liquid template render + "qrcode": starlet.CapPure, // QR encoding to string/bytes, no file writes + "toml": starlet.CapPure, // TOML decode/encode + "totp": starlet.CapPure, // TOTP code generation/validation + "yaml": starlet.CapPure, // YAML decode/encode "sys": starlet.CapProcess, // argv/platform/host + stdin "gum": starlet.CapProcess, // interactive terminal I/O "email": starlet.CapNetwork, // Resend API diff --git a/cli/capability_test.go b/cli/capability_test.go index caea328..7faf21b 100644 --- a/cli/capability_test.go +++ b/cli/capability_test.go @@ -9,10 +9,13 @@ package cli // - allowedModules filtering // - open default loads everything; invalid tier errors // - end-to-end gating through Process under an explicit tier +// - wired pure domain modules are classified pure and pass the safe gate import ( "strings" "testing" + + "github.com/1set/starlet" ) func TestModuleAllowed(t *testing.T) { @@ -171,3 +174,44 @@ func TestProcess_CapabilityGate_EndToEnd(t *testing.T) { }) } } + +// TestAllWiredModulesClassified guards against wiring a module without +// classifying it: every name in cliMods must have a capability (starpkg here or +// a starlet builtin), or it would slip through the capability gate unpredictably. +func TestAllWiredModulesClassified(t *testing.T) { + for _, name := range cliMods { + if _, ok := moduleCaps(name); !ok { + t.Errorf("wired CLI module %q has no capability classification", name) + } + } +} + +// TestPureDomainModulesAreSafe pins the contract for the pure starpkg modules +// wired by default (cache/emoji/liquid/qrcode/toml/totp/yaml): each is classified +// CapPure, so it loads even under the strictest --caps safe gate. The emoji case +// also runs end-to-end to prove the module actually executes, not just resolves. +func TestPureDomainModulesAreSafe(t *testing.T) { + for _, name := range []string{"cache", "emoji", "liquid", "qrcode", "toml", "totp", "yaml"} { + caps, ok := moduleCaps(name) + if !ok { + t.Errorf("pure module %q is wired but not classified", name) + continue + } + if caps != starlet.CapPure { + t.Errorf("pure module %q caps=%v, want CapPure (no network/filesystem reach)", name, caps) + } + } + + a := baseArgs() + a.OutputPrinter = "stdout" + a.Caps = "safe" // strictest gate; a pure module must still load + a.CodeContent = `load("emoji", "emojize"); print(emojize("hi :wave:"))` + var rc int + so, se := captureStd(t, func() { rc = Process(a) }) + if rc != 0 { + t.Fatalf("safe gate should load pure emoji module: exit=%d stderr=%q", rc, se) + } + if !strings.Contains(so, "\U0001F44B") { // waving hand emoji + t.Errorf("emoji.emojize output=%q, want a waving-hand emoji", so) + } +} diff --git a/cli/characterization_test.go b/cli/characterization_test.go index 34818bf..6333391 100644 --- a/cli/characterization_test.go +++ b/cli/characterization_test.go @@ -199,7 +199,11 @@ func TestGetDefaultModules(t *testing.T) { } // every CLI module is in the default set... - for _, want := range []string{"args", "sys", "gum", "email", "llm", "markdown", "cmd", "sqlite", "web"} { + for _, want := range []string{ + "args", "sys", "gum", "email", "llm", "markdown", "cmd", "sqlite", "web", + // pure domain modules wired by default + "cache", "emoji", "liquid", "qrcode", "toml", "totp", "yaml", + } { if !seen[want] { t.Errorf("default modules missing CLI module %q", want) } diff --git a/cli/mods.go b/cli/mods.go index 2fcc2dc..55d6632 100644 --- a/cli/mods.go +++ b/cli/mods.go @@ -10,13 +10,20 @@ import ( "github.com/1set/starcli/module/sys" "github.com/1set/starlet" "github.com/samber/lo" + "github.com/starpkg/cache" "github.com/starpkg/cmd" "github.com/starpkg/email" + "github.com/starpkg/emoji" "github.com/starpkg/gum" + "github.com/starpkg/liquid" "github.com/starpkg/llm" "github.com/starpkg/markdown" + "github.com/starpkg/qrcode" "github.com/starpkg/sqlite" + "github.com/starpkg/toml" + "github.com/starpkg/totp" "github.com/starpkg/web" + "github.com/starpkg/yaml" ) var ( @@ -31,6 +38,15 @@ var ( cmd.ModuleName, sqlite.ModuleName, web.ModuleName, + // pure domain modules (no network / filesystem) — wired by default so + // the turnkey CLI matches the ecosystem's pure starpkg surface. + cache.ModuleName, + emoji.ModuleName, + liquid.ModuleName, + qrcode.ModuleName, + toml.ModuleName, + totp.ModuleName, + yaml.ModuleName, } ) @@ -90,6 +106,20 @@ func loadCLIModuleByName(opts *BoxOpts, name string) (starlet.ModuleLoader, erro return sqlite.NewModule().LoadModule(), nil case web.ModuleName: return web.NewModule().LoadModule(), nil + case cache.ModuleName: + return cache.NewModule().LoadModule(), nil + case emoji.ModuleName: + return emoji.NewModule().LoadModule(), nil + case liquid.ModuleName: + return liquid.NewModule().LoadModule(), nil + case qrcode.ModuleName: + return qrcode.NewModule().LoadModule(), nil + case toml.ModuleName: + return toml.NewModule().LoadModule(), nil + case totp.ModuleName: + return totp.NewModule().LoadModule(), nil + case yaml.ModuleName: + return yaml.NewModule().LoadModule(), nil // Add more modules as needed default: return nil, fmt.Errorf("unknown module: %s", name) diff --git a/go.mod b/go.mod index c5af90f..6867503 100644 --- a/go.mod +++ b/go.mod @@ -13,13 +13,20 @@ require ( github.com/samber/lo v1.50.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 + github.com/starpkg/cache v0.1.0 github.com/starpkg/cmd v0.1.0 github.com/starpkg/email v0.1.0 + github.com/starpkg/emoji v0.1.0 github.com/starpkg/gum v0.2.0 + github.com/starpkg/liquid v0.1.0 github.com/starpkg/llm v0.1.0 github.com/starpkg/markdown v0.1.0 + github.com/starpkg/qrcode v0.1.0 github.com/starpkg/sqlite v0.2.1 + github.com/starpkg/toml v0.1.0 + github.com/starpkg/totp v0.1.0 github.com/starpkg/web v0.1.0 + github.com/starpkg/yaml v0.1.0 go.starlark.net v0.0.0-20260324133313-ffb3f39dd27a go.uber.org/atomic v1.11.0 go.uber.org/zap v1.24.0 @@ -34,11 +41,13 @@ require ( charm.land/huh/v2 v2.0.3 // indirect charm.land/lipgloss/v2 v2.0.4 // indirect github.com/1set/starlight v0.2.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/boombuler/barcode v1.1.0 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect @@ -87,7 +96,10 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/osteele/liquid v1.4.0 // indirect + github.com/osteele/tuesday v1.0.3 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pquerna/otp v1.4.0 // indirect github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/resend/resend-go/v2 v2.11.0 // indirect @@ -120,6 +132,7 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect diff --git a/go.sum b/go.sum index ce4d614..9079a96 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,9 @@ 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/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= @@ -47,6 +48,9 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= +github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -192,6 +196,10 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/osteele/liquid v1.4.0 h1:WS6lT3MFWUAxNbveF22tMLluOWNghGnKCZHLn7NbJGs= +github.com/osteele/liquid v1.4.0/go.mod h1:VmzQQHa5v4E0GvGzqccfAfLgMwRk2V+s1QbxYx9dGak= +github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU= +github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -199,6 +207,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -234,20 +244,34 @@ 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/cache v0.1.0 h1:P3nJC2zIycblI+Xz57yyhaAJOLMsf8A6ngejGTG7GA4= +github.com/starpkg/cache v0.1.0/go.mod h1:iYiUOUPJlJ2Z3CQVAO1T6RsQ6gg6g4/Cw6ssGvx3X68= github.com/starpkg/cmd v0.1.0 h1:RyB0u6zixc05tbVj1Q7SoNumDlO1ZTy3sAaJA9xRdIA= github.com/starpkg/cmd v0.1.0/go.mod h1:gjMdstyJeMiK70fZVR8uqr7ub3XnmJjRHDqOnKqabfI= github.com/starpkg/email v0.1.0 h1:KgV+9pmaE4fI9RTijjuYtZRjI3jRLzroW4wt+NW0Nr8= github.com/starpkg/email v0.1.0/go.mod h1:V9CnDje/JydsHvGMNaukM8sn/bS6yUdT7qwgG8X8JQg= +github.com/starpkg/emoji v0.1.0 h1:GNYtnkZgD1rucptKtjfUVWiAbsqfzqPncoFqVcFVtlE= +github.com/starpkg/emoji v0.1.0/go.mod h1:X5D6vhF/IV3LRANYr7uBWQZ+OLZrwoZ72rwVDt4VbhY= github.com/starpkg/gum v0.2.0 h1:FTcodBpGoAHFr9+ohenuRSIriw1hfxf1fVx/rImFOA4= github.com/starpkg/gum v0.2.0/go.mod h1:roQiInvIbYI1KuUwWC/cfdtNBvXWQOanydc3FSXG+fA= +github.com/starpkg/liquid v0.1.0 h1:+CHfjJQaHmOqBIaiDxyCEo7r6t3LQfVicJo/MW4Be7o= +github.com/starpkg/liquid v0.1.0/go.mod h1:pzCS93ZJM8JqWk9cxzPFzfst/rNCMbcR6G5tU997SgA= github.com/starpkg/llm v0.1.0 h1:CtTUTfL6zUpQPADFpNt0BpV5Z/ReRuUEvRPf8kcWNxk= github.com/starpkg/llm v0.1.0/go.mod h1:/pvKpFCN1w2olXlmdw5f2PvYPuJPbCmop65hTj+IShA= github.com/starpkg/markdown v0.1.0 h1:buLZwTcZ9SQWrVOUVeTcHqw/DCkxKPE8rdvHpbhjrE8= github.com/starpkg/markdown v0.1.0/go.mod h1:JsRNXRFjvLPRrkjGrf6jXDTl3Qg9+d2/PByzW7WxYvg= +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/starpkg/sqlite v0.2.1 h1:oTVkPCTQYhud3qDXztJsb/1cBY4SgXLF2PKonRhJPFI= github.com/starpkg/sqlite v0.2.1/go.mod h1:Hfs3JZnFbDPefCL9ybSmRJIQ+IBVMYtj5FyZ+7XZpmc= +github.com/starpkg/toml v0.1.0 h1:geXbLw/SW7XcU9Tz/eqjxSNywsMbxqfndXYOlXXkSKE= +github.com/starpkg/toml v0.1.0/go.mod h1:8QtTjIjFNuw0WGYzDHFbynhQ21QM6BvdACzapKE1cr8= +github.com/starpkg/totp v0.1.0 h1:BQ1J5bbhR8iICQ+iRJ4LZgJNX3RhhOZtY2aoaf38WqY= +github.com/starpkg/totp v0.1.0/go.mod h1:rJSA5YkYkbVO6jc1ldSaenCuwpsY3rd7M5+bJoWLFww= github.com/starpkg/web v0.1.0 h1:AAGf4SzD2f10BJZyaRCtUm5W9At2EjIhmOqCNEXKK2E= github.com/starpkg/web v0.1.0/go.mod h1:1S47yJjbY5bye3Ioe5RfDWyA5WK0QWhMa2elEpJSAQE= +github.com/starpkg/yaml v0.1.0 h1:hA9SQBwJ/xEUAeF+ZpC3M1OgnX86zxWVd+cMTqIDOak= +github.com/starpkg/yaml v0.1.0/go.mod h1:PxvQ/rbS4ZoainKtjzDNRuWZAO8TwUmFCHF/Wji/rQM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=