Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Files

Everything evoke reads or writes in a project, and who owns each file.

<project>/
├── evoke.toml            yours          the adapter, the reflexes, their settings
├── evoke.lock            evoke's        written whole by add, update and remove; realised by sync
├── evoke.d.ts            evoke's        written with the lock: Reflexes, for load<Reflexes>
├── overlays/<name>.toml  yours          your wording for one reflex, by local name
└── vocab/<name>.toml     yours          your words, by vocabulary name

<reflex>/
├── reflex.toml           the author's   the manifest
├── <body>.mts            the author's   the file run names, or nothing for an argv
└── reflex.d.ts           evoke's        written by check: Args, Config, Context, Result, Reflex

evoke edits your files in place, keeping comments and order. It verifies each write by reading it back. On failure it writes nothing, and prints the line. The lock and the two .d.ts files are rendered whole.

evoke.toml

adapter = "jev"                    # required; a name resolved against evoke's built-ins, never a path

[reflexes]                         # local name = ref
lights = "radhi/home/lights"       # owner/repo[/dir]: unpinned, update moves it
timer  = "radhi/timer@1.0.1"       # @tag pins it
clock  = "https://git.example.com/clock.git#clock@2.0.0"   # any git host; https or ssh; #dir and @tag optional
hello  = "./hello"                 # a local directory, relative to this file; never locked

[config.lights]                    # per reflex, the keys its [config] declares
bridge = "10.0.0.2"                # plain
token  = { env = "HUE_TOKEN" }     # from a variable; the only form a secret may take

[adapters.jev]                     # per adapter, under its name; inert unless selected
gate = { route = 0.5, fits = 0.3, read = 0.6, write = 0.8 }   # each a probability; read ≤ write

Local names match [a-z][a-z0-9_]*. The names none, unstated and fits are reserved. A missing evoke.toml at a root means the default project: adapter = "jev", nothing installed.

evoke.lock

lock  = 1
evoke = "0.1.0"

[adapter]
name = "jev"
id   = "jev-1.13.0"

[reflexes.lights]
ref    = "radhi/home/lights"
tag    = "1.2.0"
commit = "7a498d11d5375f3cb65c575c3186bc64ecac7f52"
h1     = "h1:a30b1bbcf52390c4b1311685ceee9f886b81b7699cde1374040979ad00c25d3a"
effect = "write"

Per remote reflex: the ref without its pin, the tag, the commit, the content hash of the reflex directory, and the effect you consented to. Local reflexes are not locked. h1 is a SHA-256 over sha256sum's own lines for every file in the directory. Files are taken in byte order of path, and the project's five names are skipped:

find . -type f | sed 's|^\./||' | LC_ALL=C sort | xargs sha256sum | sha256sum

overlays/<name>.toml

description = "…"                                   # replaces whole
not_for     = ["…"]                                 # replaces whole
tags        = ["…"]                                 # replaces whole
effect      = "destructive"                         # may only tighten
confirm     = "…"                                   # placeholders name required arguments

[args.state]                                        # by its current name, or a former one declared with was
ask = "…"
options.dim = "…"                                    # new wording for an existing key only

[examples]                                          # sent; may assert vocab arguments
"kill the lights" = { state = "off" }

[tests]                                             # held out; a shipped example named here stops being sent
"light a candle" = false

A contract key makes the reflex inactive. That means run, a new argument, a new option key, a source, a range, or config.

vocab/<name>.toml

den    = "The TV room downstairs; also 'the snug'."
office = { what = "The upstairs study.", value = "group-7" }

A word is one clean line, trimmed, spaces allowed, and unique under identity. none and unstated are reserved. what is what the classifier reads. value is what the body receives instead of the word.

evoke.d.ts

// Generated by evoke add, update and remove; do not edit.
// What a decision's args carry, per installed reflex: load<Reflexes> narrows them by reflex.

export type Option<K extends string> = { type: "option"; key: K }
export type Word = { type: "word"; word: string; value?: string }
export type Pick<T extends string, V> = { type: "pick"; span: { start: number; end: number; text: string }; value: { type: T; value: V } }
export type Flag = { type: "flag" }

export interface Reflexes {
  timer: {
    /** How long? */
    duration: Pick<"duration", number>
    /** What is the timer for? */
    label?: Pick<"quoted", string>
  }
}

A reflex whose manifest does not read is left out. The file imports nothing.

reflex.d.ts

// Generated by evoke check; do not edit.

export interface Args {
  /** How long? */
  duration: number
  /** What is the timer for? */
  label?: string
}

export interface Config {}

/** What the body receives beside its arguments. */
export interface Context {
  /** The input as it was typed. */
  input: string
  config: Config
  /** Aborted at the deadline or on a decline: stop, never guess. */
  signal: AbortSignal
}

/** What the body returns: the text a person reads, and data an app may use. */
export type Result = string | { text: string; data?: unknown }

/** The body: the default export of the file `run` names. */
export type Reflex = (args: Args, context: Context) => Result | Promise<Result>

Written for a file body only, and only when it changed.

Machine-local files

These live outside every project, under XDG paths. The store, the answer cache and the test baselines are in ~/.cache/evoke/. The log, trust, the runtime path and the REPL's history are in ~/.local/state/evoke/. Environment lists each.