dillonkearns/elm-pages/12.2.1

NOTE: the latest version is 12.3.0

Pages.Script

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.

type alias Script =
Script

The type for your run function that can be executed by elm-pages run.

Defining Scripts

Config cliOptions
-> (cliOptions -> BackendTask FatalError ())
-> Script

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.

withoutCliOptions : BackendTask FatalError () -> Script

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
        )
{ description : String
, cliOptions : Config cliFlags
, encoder : Encoder outputType
, run : cliFlags -> BackendTask FatalError outputType
}
-> Script

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": { ... } }
{ moduleName : String
, path : String
}
-> Script
-> Maybe Value

For internal use by generated code. Most users should not need this.

withDatabasePath : String -> Script -> Script

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"

File System Utilities

{ path : String
, body : String
}
-> BackendTask
{ fatal : FatalError
, recoverable : Error
}
()

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
        )
removeFile : String -> BackendTask FatalError ()

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")
{ from : String
, to : String
}
-> BackendTask FatalError ()

Copy a single file. Auto-creates parent directories of the destination (matching writeFile behavior).

Script.copyFile { from = "src/config.json", to = "dist/config.json" }
{ from : String
, to : String
}
-> BackendTask FatalError ()

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" }
makeDirectory : { recursive : Bool } -> String -> BackendTask FatalError ()

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"
removeDirectory : { recursive : Bool } -> String -> BackendTask FatalError ()

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"
makeTempDirectory : String -> BackendTask FatalError String

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)
        )

Shell Commands

command : String -> List String -> BackendTask FatalError String

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)
                )
        )
exec : String -> List String -> BackendTask FatalError ()

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" [])

Utilities

log : String -> BackendTask error ()

Log to stdout.

module MyScript exposing (run)


import BackendTask
import Pages.Script as Script


run =
    Script.withoutCliOptions
        (Script.log "Hello!"
            |> BackendTask.allowFatal
        )
sleep : Int -> BackendTask error ()

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!")
        )
BackendTask error value
-> BackendTask error ()
-> BackendTask error value

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!")
        )
which : String -> BackendTask error (Maybe String)

Same as expectWhich, but returns Nothing if the command is not found instead of failing with a FatalError.

expectWhich : String -> BackendTask FatalError String

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.
question : String -> BackendTask error String
module QuestionDemo exposing (run)


import BackendTask


run : Script
run =
    Script.withoutCliOptions
        (Script.question "What is your name? "
            |> BackendTask.andThen
                (\name ->
                    Script.log ("Hello, " ++ name ++ "!")
                )
        )
readKey : BackendTask error String

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.

readKeyWithDefault : String -> BackendTask error String

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.

Errors

type Error
= FileWriteError

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.