An elm-pages Script is a way to execute an elm-pages BackendTask.
Read more about using the elm-pages CLI to run (or bundle) scripts, plus a brief tutorial, at https://elm-pages.com/docs/elm-pages-scripts.
Same as withoutCliOptions, but allows you to define a CLI Options Parser so the user can
pass in additional options for the script.
Uses https://package.elm-lang.org/packages/dillonkearns/elm-cli-options-parser/latest/.
Read more at https://elm-pages.com/docs/elm-pages-scripts/#adding-command-line-options.
Define a simple Script (no CLI Options).
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script
run =
Script.withoutCliOptions
(Script.log "Hello!"
|> BackendTask.allowFatal
)Like withCliOptions, but with a typed output schema.
The return value of your run function is automatically JSON-encoded and
printed to stdout using the provided encoder. Running with --introspect-cli
short-circuits execution and prints metadata including the script's
inputSchema and output JSON Schema instead, so that tools and LLM agents
can discover how to call the script and what it returns without running it.
The schema is derived from the same Encoder that does the actual encoding,
so it can never drift out of sync with the real output.
description is a short summary of what the script does. It appears in
--introspect-cli output so that tools can decide whether to call it. The
help field in the output is the usage synopsis from --help (without
ANSI colors). Scripts defined with withSchema are also automatically
included in elm-pages introspect; you do not need to expose any extra
top-level values for that to work. To invoke a script with JSON input
directly, pass the JSON object as a single shell argument.
Example: a script that checks whether a URL is reachable.
-- file: script/src/CheckStatus.elm
run : Script
run =
Script.withSchema
{ description =
"Check whether a URL is reachable"
, cliOptions =
Program.config
|> Program.add
(OptionsParser.build identity
|> OptionsParser.with
(Option.requiredKeywordArg "url")
)
, encoder =
TsEncode.object
[ TsEncode.required "reachable"
.reachable
TsEncode.bool
, TsEncode.required "statusCode"
.statusCode
TsEncode.int
]
, run = checkStatus
}
elm-pages run CheckStatus.elm --url https://example.com
# { "reachable": true, "statusCode": 200 }
elm-pages run CheckStatus.elm '{"url":"https://example.com","$cli":{}}'
# { "reachable": true, "statusCode": 200 }
elm-pages run CheckStatus.elm --introspect-cli
# { "name": "CheckStatus",
# "description": "Check whether a URL is reachable",
# "help": "CheckStatus --url <URL>",
# "inputSchema": { ... },
# "outputSchema": { ... } }For internal use by generated code. Most users should not need this.
Configure the default database file path for Pages.Db.default in this script run.
Use this when you want the shared default connection path.
For explicit connection-based paths (for example from CLI options),
use Pages.Db.open.
import Pages.Db
import Pages.Script as Script
run : Script
run =
Script.withoutCliOptions
(Pages.Db.update Pages.Db.default (\db -> db)
|> BackendTask.allowFatal
)
|> Script.withDatabasePath ".elm-pages-data/prefs.db.bin"Write a file to the file system.
File paths are relative to the root of your elm-pages project (next to the elm.json file and src/ directory), or you can pass in absolute paths beginning with a /.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script
run =
Script.withoutCliOptions
(Script.writeFile
{ path = "hello.json"
, body = """{ "message": "Hello, World!" }"""
}
|> BackendTask.allowFatal
)Remove a file. Silently succeeds if the file doesn't exist (like rm -f).
Script.writeFile { path = "temp.txt", body = "..." }
|> BackendTask.allowFatal
|> BackendTask.andThen (\_ -> Script.removeFile "temp.txt")Copy a single file. Auto-creates parent directories of the destination (matching writeFile behavior).
Script.copyFile { from = "src/config.json", to = "dist/config.json" }Move (rename) a file or directory. Atomic on the same filesystem. Auto-creates parent directories of the destination.
Script.move { from = "build/output.js", to = "dist/app.js" }Create a directory.
The { recursive : Bool } flag controls whether parent directories are created (like mkdir -p).
-- Create nested directories
Script.makeDirectory { recursive = True } "dist/assets/images"
-- Create a single directory (parent must exist)
Script.makeDirectory { recursive = False } "output"Remove a directory. Silently succeeds if the directory doesn't exist (like rm -f on a missing path).
The { recursive : Bool } flag only controls whether non-empty directories can be removed (rm -r behavior).
It does not control force semantics.
-- Remove a directory and all its contents
Script.removeDirectory { recursive = True } "build"
-- Remove only if empty
Script.removeDirectory { recursive = False } "empty-dir"Create a temporary directory with a given prefix. Returns the absolute path to the created directory.
Pairs naturally with BackendTask.finally for cleanup:
Script.makeTempDirectory "my-build-"
|> BackendTask.andThen
(\tmpDir ->
doWork tmpDir
|> BackendTask.finally
(Script.removeDirectory { recursive = True } tmpDir)
)Run a single command and return stderr and stdout combined as a single String.
If you want to do more advanced things like piping together multiple commands in a pipeline, or piping in a file to a command, etc., see the BackendTask.Stream module.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script exposing (Script)
run : Script
run =
Script.withoutCliOptions
(Script.command "ls" []
|> BackendTask.andThen
(\files ->
Script.log ("Files: " ++ files)
)
)Like command, but prints stderr and stdout to the console as the command runs instead of capturing them.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script exposing (Script)
run : Script
run =
Script.withoutCliOptions
(Script.exec "ls" [])Log to stdout.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script
run =
Script.withoutCliOptions
(Script.log "Hello!"
|> BackendTask.allowFatal
)Sleep for a number of milliseconds.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script
run =
Script.withoutCliOptions
(Script.log "Hello..."
|> Script.doThen
(Script.sleep 1000)
|> Script.doThen
(Script.log "World!")
)Run a command with no output, then run another command.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script
run =
Script.withoutCliOptions
(Script.log "Hello!"
|> Script.doThen
(Script.log "World!")
)Same as expectWhich, but returns Nothing if the command is not found instead of failing with a FatalError.
Check if a command is available on the system. If it is, return the full path to the command, otherwise fail with a FatalError.
module MyScript exposing (run)
import BackendTask
import Pages.Script as Script
run : Script
run =
Script.withoutCliOptions
(Script.expectWhich "elm-review"
|> BackendTask.andThen
(\path ->
Script.log ("The path to `elm-review` is: " ++ path)
)
)If you run it with a command that is not available, you will see an error like this:
Script.expectWhich "hype-script"-- COMMAND NOT FOUND ---------------
I expected to find `hype-script`, but it was not on your PATH. Make sure it is installed and included in your PATH.module QuestionDemo exposing (run)
import BackendTask
run : Script
run =
Script.withoutCliOptions
(Script.question "What is your name? "
|> BackendTask.andThen
(\name ->
Script.log ("Hello, " ++ name ++ "!")
)
)Read a single keypress from stdin without requiring Enter.
This is useful for interactive prompts where you want immediate response to a single key, like confirmation dialogs (y/n) or menu navigation.
module ConfirmDemo exposing (run)
import BackendTask
run : Script
run =
Script.withoutCliOptions
(Script.log "Approve this change? [y/n] "
|> BackendTask.andThen (\_ -> Script.readKey)
|> BackendTask.andThen
(\key ->
if key == "y" then
Script.log "Approved!"
else
Script.log "Rejected."
)
)Note: Returns the raw key character. Control characters like Ctrl+C will terminate the process.
When not running in an interactive terminal (e.g., piped input or CI),
falls back to line-buffered input and returns the first character of the line.
This allows scripts to work both interactively and with piped input like
echo "y" | elm-pages run MyScript.elm.
Like readKey, but returns a default value when Enter is pressed.
Script.log "Continue? [Y/n] "
|> BackendTask.andThen (\_ -> Script.readKeyWithDefault "y")
|> BackendTask.andThen
(\key ->
if String.toLower key == "y" then
continue
else
abort
)Useful for prompts where pressing Enter should accept a default option.
The recoverable error type for file writes. You can use BackendTask.allowFatal if you want to allow the program to crash
with an error message if a file write is unsuccessful.
The type for your
runfunction that can be executed byelm-pages run.