cliche

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 27 Imported by: 0

README

cliche

Go Reference CI

cliche is a Go framework that turns JavaScript command scripts into commands of your app's CLI. The app gives scripts its own API; a script declares its arguments and flags and becomes a cobra command with help, completion and strict parsing. Scripts run on goja, so users need neither Node.js nor any other external program.

cliche holds only the mechanisms: finding scripts, reading declarations, building cobra commands, running scripts and moving values and errors between Go and JavaScript. Everything about your domain stays in the app: the API module, output format, error codes, exit codes, login, versioning.

Install

go get github.com/hakastein/cliche@latest

Requires Go 1.26 or later.

Quick start

A script .demo/scripts/greet.js:

const app = require("app");

exports.definition = {
  short: "Greet someone",
  long: "Greet someone by name.",
  args: [{ name: "name", type: "string", usage: "who to greet" }],
  flags: [{ name: "shout", type: "bool", usage: "greet loudly" }],
};

exports.command = (name, flags) => {
  const text = "Hello, " + name + "!";
  return flags.shout ? app.upper(text) : text;
};

The app that runs it:

config := cliche.Config{Name: "demo"} // .demo/scripts of the project and of the user's home
env := cliche.Env{Modules: map[string]any{
	"app": cliche.NewObject([]string{"upper"}, []any{
		cliche.Func(func(_ context.Context, args ...any) (any, error) {
			return strings.ToUpper(args[0].(string)), nil
		}),
	}),
}}

root := &cobra.Command{Use: "demo"}
cliche.Merge(config.Scan()).AddTo(root, func(cmd *cobra.Command, command *cliche.Command, in cliche.Input) error {
	value, err := cliche.Run(cmd.Context(), command, in, env)
	if err != nil {
		return err
	}
	fmt.Println(value) // printing the answer is the app's choice
	return nil
})
_ = root.Execute()
$ demo greet world --shout
HELLO, WORLD!

The same program with an in-memory source runs as the package example under go test.

Command scripts

  • Path is the command. docs/map.js in a source is demo docs map; a directory holding commands is a group. A file without exports.definition is a library module that scripts of its source require by a relative path.
  • Sources. Config.Builtin (scripts embedded in the app) comes first, then Config.Sources. By default these are the nearest .<name>/scripts at or above the working directory and ~/.<name>/scripts. Merge keeps the command of the earlier root on a conflict and reports the other as a warning; an app with its own rule reads Scan() itself.
  • Declaration. exports.definition is a plain literal read without running the module: short, long, args, flags. Arguments may be optional, multiple or groups of slots; flags may be multiple, have choices and a default. Base types are string, path, int, bool, pair (Name=value) and duration; the app adds its own through Config.Types. The app also registers keys of its own beside short, long, args and flags through Config.Keys: it checks their literal values itself and reads the result from Command.Keys after Scan. Any other key makes the declaration unreadable.
  • Call. exports.command gets the arguments in declared order and the flags object last. A call the declaration does not take fails with *UsageError before the script runs.
  • Runtime. CommonJS in strict mode, synchronous, a fresh runtime per run; cancelling the context stops the script. There is no sandbox: a script is trusted like the repository it lives in.

Bridge between Go and JavaScript

The app gives scripts values through Env.Modules: nil, bool, string, int64, float64, *big.Int, []byte, []any, *Object, its own Map and List, Func and Getter. Values from Go are read-only in JavaScript. A script returns the same kinds of values back to Go.

Env.Node opts into a synchronous subset of Node.js: fs (readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync), Buffer, and fetch for GET requests.

Errors

An error of a Func reaches the script as a read-only Error; its fields come from the error's ScriptFields(). An app error the script leaves uncaught comes back from Run as the same Go error. Faults of cliche are types: *ScriptError, *UsageError, *FileError, *FetchError, *InterruptedError. Error codes, stderr format and exit codes are up to the app.

Documentation

Contributing

See CONTRIBUTING.md.

License

MIT

Documentation

Overview

Example

An app scans its script commands, puts them under its cobra root and runs a call with its own API module.

package main

import (
	"context"
	"fmt"
	"strings"
	"testing/fstest"

	"github.com/spf13/cobra"

	"github.com/hakastein/cliche"
)

func main() {
	scripts := fstest.MapFS{
		"greet.js": &fstest.MapFile{Data: []byte(`
const app = require("app");

exports.definition = {
  short: "Greet someone",
  long: "Greet someone by name.",
  args: [{ name: "name", type: "string", usage: "who to greet" }],
  flags: [{ name: "shout", type: "bool", usage: "greet loudly" }],
};

exports.command = (name, flags) => {
  const text = "Hello, " + name + "!";
  return flags.shout ? app.upper(text) : text;
};
`)},
	}

	config := cliche.Config{Name: "demo", Sources: []cliche.Source{cliche.FS("scripts", scripts)}}
	env := cliche.Env{Modules: map[string]any{
		"app": cliche.NewObject([]string{"upper"}, []any{
			cliche.Func(func(_ context.Context, args ...any) (any, error) {
				return strings.ToUpper(args[0].(string)), nil
			}),
		}),
	}}

	root := &cobra.Command{Use: "demo", SilenceUsage: true}
	cliche.Merge(config.Scan()).AddTo(root, func(cmd *cobra.Command, command *cliche.Command, in cliche.Input) error {
		value, err := cliche.Run(cmd.Context(), command, in, env)
		if err != nil {
			return err
		}
		fmt.Println(value)
		return nil
	})

	root.SetArgs([]string{"greet", "world", "--shout"})
	if err := root.Execute(); err != nil {
		fmt.Println(err)
	}
}
Output:
HELLO, WORLD!

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Place

func Place(ctx context.Context) (file string, line, column int)

Place is the line of the script that called the Func given ctx; an empty file when it is unknown.

func Run

func Run(ctx context.Context, c *Command, in Input, env Env) (any, error)

Run runs the script of c with the call in and returns what its command returned. An error of the app the script left uncaught is that error; a fault of cliche is one of its error types.

Types

type Arg

type Arg struct {
	// Line and Column are where the declaration writes the argument.
	Line, Column int
	Name         string
	Type         string
	Usage        string
	Group        []Slot
	Optional     bool
	Multiple     bool
}

Arg is one element of the call: a single value of Type, or a group of Slots.

type Broken

type Broken struct {
	Path []string
	Err  *ScriptError
}

Broken is a path that is no command: a call of it ends in Err. A root that cannot be read is broken at no path.

type Catalog

type Catalog struct {
	Commands []*Command
	Broken   []Broken
	Warnings []Warning
}

Catalog is the commands, the broken paths and the warnings of all roots after the rule of conflicts.

func Merge

func Merge(roots []Root) *Catalog

Merge is the default rule of conflicts: of two equal command paths the one of the earlier root is kept and the later one is a warning. A root that cannot be read is a broken path.

func (*Catalog) AddTo

func (c *Catalog) AddTo(root *cobra.Command, run func(cmd *cobra.Command, command *Command, in Input) error)

AddTo puts the commands of c under root: each script command under the group of its path, and a hidden command per broken path whose call is its error. A group is made for a word no command of root holds yet; called without a subcommand it knows, it is a *UsageError. A group made only for broken paths is hidden. run runs a script command.

type Command

type Command struct {
	Path  []string
	Short string
	Long  string
	Args  []Arg
	Flags []Flag
	// Keys holds what Key.Read gave for each key of Config.Keys the declaration writes; nil when Config.Keys is
	// empty.
	Keys map[string]any
	// contains filtered or unexported fields
}

Command is a script command, read from its declaration alone, without running the module.

func (*Command) Builtin

func (c *Command) Builtin() bool

Builtin tells a script of Config.Builtin from one of a source.

func (*Command) Cobra

func (c *Command) Cobra(run func(cmd *cobra.Command, in Input) error) *cobra.Command

Cobra builds the cobra command of c. run gets the parsed call; a call the declaration does not take is a *UsageError before run.

func (*Command) File

func (c *Command) File() string

File names the script as faults and Place name it.

type Config

type Config struct {
	// Name gives the directory .<Name>/scripts of the default sources.
	Name string
	// Builtin holds the scripts built into the app; it is always the first root, its files named "builtin:<path>".
	Builtin fs.FS
	// Sources nil stands for DefaultSources(Name, os.Getwd(), os.UserHomeDir()); empty stands for no sources.
	Sources []Source
	// Types are the types of the app beyond the base ones.
	Types map[string]Type
	// Keys are the keys of the app in exports.definition beside short, long, args and flags, by name.
	Keys map[string]Key
}

Config is what an app gives cliche to find its script commands.

func (Config) Scan

func (c Config) Scan() []Root

Scan reads every root in order: Builtin first, then Sources. It panics when Keys holds short, long, args or flags.

type Env

type Env struct {
	// Modules are the values require gives by name.
	Modules map[string]any
	Node    Node
	// Error turns an error of cliche a script may catch (*FetchError) into an error of the app before it is thrown.
	Error func(error) error
}

Env is what a run gives a script.

type FetchError

type FetchError struct {
	URL    string
	Reason string
}

FetchError is a fetch that got no whole response: a failed transport, a timeout, a redirect too many, or a body longer than maxBytes. A response of any status is no error.

func (*FetchError) Error

func (e *FetchError) Error() string

func (*FetchError) ScriptFields

func (e *FetchError) ScriptFields() Map

ScriptFields gives a script the url of the request it caught.

type FileError

type FileError struct {
	Message string
	Code    string
	Syscall string
	Path    string
}

FileError is an error of fs that the script left uncaught: the Node code, the syscall and the path.

func (*FileError) Error

func (e *FileError) Error() string

type Flag

type Flag struct {
	Line, Column int
	Name         string
	Type         string
	Usage        string
	Multiple     bool
	Choices      []string
	Default      any
}

Flag is a flag of the call. Default is nil when the declaration gives none.

type Func

type Func func(ctx context.Context, args ...any) (any, error)

Func is a function of the app's API. ctx is the context of Run and carries Place.

type Getter

type Getter func(ctx context.Context) (any, error)

Getter is a property computed each time a script reads it.

type Input

type Input struct {
	// Args holds a value per declared argument: a single one is its value, a group a *Object of its slots, a multiple
	// one a []any; an optional one not given is Undefined.
	Args []any
	// Flags holds the flags given and the ones with a default.
	Flags *Object
}

Input is a parsed call of a script command: the arguments in their declared order and the flags.

type InterruptedError

type InterruptedError struct {
	Cause error
}

InterruptedError is a script stopped by the cancellation of its context; Cause is the cause of that context.

func (*InterruptedError) Error

func (e *InterruptedError) Error() string

func (*InterruptedError) Unwrap

func (e *InterruptedError) Unwrap() error

type Key added in v0.2.0

type Key struct {
	// Required makes the key required of every declaration.
	Required bool
	// Read checks the value and gives what Command.Keys holds for the key. An error makes the declaration unreadable:
	// an error of Literal.Errorf names the place of that literal, any other error the value of the key. Read nil keeps
	// the Literal itself.
	Read func(value Literal) (any, error)
}

Key is a key of the app in exports.definition beside short, long, args and flags. cliche reads its value as the same pure literal as the rest of the declaration, without running the module; what the value may be is the app's.

type List

type List interface {
	Len() int
	Index(i int) any
}

List is a read-only list an app gives a script.

type Literal added in v0.2.0

type Literal struct {
	Line, Column int
	Value        any
}

Literal is a value of a declaration and its place in the script. Value is a string, a float64, a bool, nil, a []Literal or a *LiteralObject.

func (Literal) Errorf added in v0.2.0

func (l Literal) Errorf(format string, args ...any) error

Errorf is the error by which Key.Read refuses l: the *ScriptError of the declaration names the place of l.

type LiteralObject added in v0.2.0

type LiteralObject struct {
	Keys   []string
	Values map[string]Literal
}

LiteralObject is an object of a declaration: its keys in the order written and the value of each.

type Map

type Map interface {
	Keys() []string
	Get(key string) (any, bool)
}

Map is a read-only mapping an app gives a script, its keys in its own order.

type Node

type Node uint8

Node is the part of the Node subset an app gives scripts. Bytes cross the bridge as a Buffer whatever it holds.

const (
	// NodeFS is require("fs") with its *Sync functions.
	NodeFS Node = 1 << iota
	// NodeFetch is the global fetch: synchronous, GET only.
	NodeFetch
	// NodeBuffer is the global Buffer and require("buffer").
	NodeBuffer
)

type Object

type Object struct {
	// contains filtered or unexported fields
}

Object is a mapping with its keys in order: one a script built, or one an app builds with NewObject.

func NewObject

func NewObject(keys []string, values []any) *Object

NewObject pairs keys with values in order; a key given twice keeps its last value at its first place.

func (*Object) Get

func (o *Object) Get(key string) (any, bool)

func (*Object) Keys

func (o *Object) Keys() []string

type Root

type Root struct {
	// Name names the directory or the fs.FS of the source.
	Name    string
	Builtin bool
	// Err is the source that cannot be read at all.
	Err   *ScriptError
	Words []Word
}

Root is a source read as a catalog: its first words with their commands and broken paths.

type ScriptError

type ScriptError struct {
	Message string
	File    string
	Line    int
	Column  int
}

ScriptError is a fault of a script: an exception it left uncaught, an unreadable declaration, a broken path, or a value it returned that the bridge does not carry. File, Line and Column are the place when it is known.

func (*ScriptError) Error

func (e *ScriptError) Error() string

type ScriptFields

type ScriptFields interface {
	ScriptFields() Map
}

ScriptFields is implemented by an error of the app that a script reads fields of in a catch block.

type Slot

type Slot struct {
	Line, Column int
	Name         string
	Type         string
	Usage        string
}

Slot is one value of a group.

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source is a directory or an fs.FS of scripts: the path of a file in it is the path of a command.

func DefaultSources

func DefaultSources(app, workingDir, home string) []Source

DefaultSources are the nearest .<app>/scripts at or above workingDir and <home>/.<app>/scripts, in that order. The nearest one that is the user's own is no project source. An empty workingDir or home gives no source of its own; an existing path that cannot be read is a source that fails to scan.

func Dir

func Dir(dir string) Source

Dir is a source of scripts in a directory; its files are named by their absolute paths.

func FS

func FS(name string, fsys fs.FS) Source

FS is a source of scripts in fsys; its files are named "<name>:<path>".

type Type

type Type struct {
	// Usage is the fixed help of a value of the type; a declaration writes none.
	Usage string
	// Default makes default required of a flag of the type.
	Default bool
	// Parse reads every value of the call; nil keeps the text as it is.
	Parse func(text string) (any, error)
	// Value is what command gets for a flag of the type from its default and the values given; nil leaves the key
	// out. Value nil stands for the last value given, else the default.
	Value func(def any, given []any) any
}

Type is a type of the app for arguments, slots and flags beyond the base ones. A base type name is never an app type.

type Undefined

type Undefined struct{}

Undefined is the value a script reads as undefined: an optional argument the call did not give.

type UsageError

type UsageError struct {
	Message string
	// Cause is the error of pflag for a flag the call wrote wrong; nil for the arguments.
	Cause error
}

UsageError is a call of a script command that its declaration does not take; the script did not run.

func (*UsageError) Error

func (e *UsageError) Error() string

func (*UsageError) Unwrap

func (e *UsageError) Unwrap() error

type Warning

type Warning struct {
	Path []string
	// File is what lost; Kept is what won.
	File    string
	Kept    string
	Message string
}

Warning is a path a later root lost to an earlier one.

type Word

type Word struct {
	Word string
	// File names the file or the directory of the word.
	File     string
	Commands []*Command
	Broken   []Broken
}

Word is everything a root holds under one first word of a path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址