Command line
The @graftland/cli package provides the graft command. It needs Node.js
22.19 or later, which matches its Effect Node adapter.
| Command | What it does | Section |
|---|---|---|
graft start [config] |
Runs the application a config file describes. | graft start |
graft create [directory] |
Creates a new project. | graft create |
graft add <package...> |
Adds dependencies and installs them. | graft add |
graft remove <package...> |
Removes dependencies. | graft remove |
graft schema |
Writes the editor schema of graft.yml. |
graft schema |
graft check [file] |
Checks manifests against a host. | graft check |
graft start runs both remote hosts and
runners. Command plugins add subcommands such as
graft remote certificates; they run only when explicitly enabled.
Command plugins
Section titled “Command plugins”graft add --plugin remotegraft remote certificates --out ./certs --server localhostgraft add --plugin remote --globaladd --plugin installs the plugin package and enables its /cli entry. A short
name becomes whichever of @graftland/plugin-<name> and graft-plugin-<name>
the registry has; if it has both, name the package in full. --global installs
and enables it in the user configuration.
Enabled commands appear in --help and --completions bash. Built-in command
names and aliases are reserved; duplicates stop startup. Commands are loaded
once per invocation, including help. --no-plugins skips discovery so you can
repair a broken declaration. Business entries do not run during discovery.
With start --watch, an invalid project config temporarily contributes no
command plugins. The CLI logs the cause and the application watcher waits for a
valid file; restart the CLI to load its command entries.
graft start
Section titled “graft start”graft start [config] [--watch] [--env-file <path>] [--env-dir <path>] [--stdio]Runs the application that a config file describes. config
defaults to graft.yml. The process runs until it gets
Ctrl+C or SIGTERM. Then it unloads every plugin in
order.
Protocol output
Section titled “Protocol output”Use graft start runner.yml --stdio for a runner with one stdio listener, and
graft start --stdio for an MCP server. The flag
reserves stdout for protocol bytes before loading plugins or parsing help and
version flags. Logs, Console output (including worker output), help, version and
errors go to stderr; interactive wizard and completion output are unavailable in
this mode. The runner rejects a mismatched listener configuration. At stdin EOF
it stops its execution units, confirms termination and exits; startup or cleanup
failure gives a nonzero exit.
Options
Section titled “Options”| Option | What it does |
|---|---|
--watch, -w |
Watches the config file and plugin modules. See Watch mode. |
--env-file <path> |
Reads a .env file for !env tags. See Env files. |
--env-dir <path> |
Reads a directory with one file per variable for !env tags. See Env directories. |
Env files
Section titled “Env files”| Behavior | Detail |
|---|---|
| Path | A relative path is relative to the current directory. |
| Priority | A variable set in the process environment wins over the file, even when set to an empty string. |
| Missing file | The command stops. |
With --watch |
Saving the file reads it again and retries the loader, so entries whose values changed are updated. If the file cannot be read, the previous values stay in effect. |
| Scope | The file feeds Effect’s ConfigProvider, so both !env and any Config a plugin reads use it. It does not change process.env. |
With --watch, .env files also use polling and watcher recovery:
| Item | Behavior |
|---|---|
.env path polling |
The requested path is reread every second, resolving symbolic links again. This detects saves to targets outside the config directory, link retargeting and missed notifications. |
| Changed contents | Only changed contents retry the loader. |
| Read failures | Keep the last values and are logged; polling continues. |
| Watcher failures | Logged. The watcher is installed again after one second. |
Env directories
Section titled “Env directories”graft start --env-dir /run/secrets reads secrets mounted as files, such as
Docker secrets or a Kubernetes Secret volume.
| Behavior | Detail |
|---|---|
| Variables | Each regular file is one variable, named after the file. Symbolic links to files count; hidden entries and subdirectories are skipped. |
| Value | The file’s contents without trailing line breaks. An empty file sets an empty value. |
| Priority | After the process environment and --env-file. |
| Missing directory | The command stops. |
With --watch |
Watched and polled every second like env files. Only changed values retry the loader. If the directory cannot be read, the previous values stay in effect. |
| Path and scope | As for env files. |
Invalid first configuration
Section titled “Invalid first configuration”| Mode | Behavior |
|---|---|
Without --watch |
The CLI reports the cause and exits with a nonzero status. |
With --watch |
The CLI logs the first failure and waits for a valid configuration. It does not restart. The loader has no entries until it reads the first valid document, and edits through its API still cannot overwrite an invalid document. |
Watch mode
Section titled “Watch mode”With --watch, the CLI watches two kinds of change:
| Change | Handled by |
|---|---|
| YAML configuration | The loader. See Changes while running. |
.js, .mjs, .cjs, .ts, .mts and .cts files under the config directory (not node_modules) |
Incremental module reloads. |
How the two interact:
- Editing YAML does not import all entries again, and does not restart every plugin that comes from a module. Unchanged healthy entries and their environment values are left alone.
- Removing or disabling an entry releases its module lease, so an invalid module that nobody uses cannot block reloads of other entries. Pending and failed entries keep their leases.
- If plugin exports that used to be shared now differ, or replacement identities conflict, the CLI reports the affected modules and asks for a restart. Running plugins stay as they are.
How the CLI drives reload:
- It calls
hmr.watchwith a module-file filter. - After each attempt that ends with
Reloaded,RolledBack,RecoveryFailedorSkipped, it callsloader.retry, to retry failed entries and refresh!envvalues.RolledBacklogs one warning per plugin that failed to start or timed out, withgraft.forkandgraft.plugin, and theCauseof a failure.RecoveryFailedlogs errors for both the replacement and recovery failures, with the same annotations and full causes. It says that the previous code failed to restart. - Reload and retry failures are logged with their complete
Cause, and watching goes on.
YAML and module watchers use native filesystem notifications, without polling. The YAML watcher rereads the file after installation to catch changes during setup. If either watcher backend fails, the failure is logged; that watcher is not reinstalled.
Shutdown failures are logged with their complete Cause. A cause combining
interruption with an error or defect is logged in full; pure interruption is
normal cancellation.
For how changes are detected, see Hot reload.
Restart requirements
Section titled “Restart requirements”The CLI reports Restart required instead of a successful reload in two cases.
All existing plugins stay running.
| Case |
|---|
Native loading abilities (node:module, including createRequire and cached require(esm)). |
| A local entry or helper symlink points to a new target. |
Known native-loading requirements are reported even without a file event, so a missed startup notification cannot leave the limitation silent. Unchanged periodic reports are suppressed. Symlink target checks are part of the periodic check, so identical source bytes and lost native notifications cannot hide a target change.
Logs and intercepts
Section titled “Logs and intercepts”Logs are printed in a readable format. Each line a plugin writes has the plugin’s name and fork id. For JSON output or exported spans, embed Graft; see Choose the output in your host.
The CLI registers two references for the intercept setting of entries,
applying to the entry and its child plugins:
| Reference | Behavior |
|---|---|
effect/References/CurrentLogAnnotations |
An object merged with outer annotations. |
effect/References/MinimumLogLevel |
A replacement log level. |
The config file page lists
accepted values and an example. Invalid values fail the entry with
InvalidIntercept. The CLI does not register other references.
For a shared HTTP listener, load HTTP server with ordinary
graft start; it does not need --stdio.
graft create
Section titled “graft create”npm create @graftland@latest [directory] [-- <options>]graft create [directory] [options]Creates a new project. Both commands are the same command. In a terminal it asks
a few questions, each with a default you accept by pressing Enter.
With --yes, or when stdin or stdout is not a terminal, it asks nothing and
every question takes its default.
| Option | Question | Default |
|---|---|---|
[directory] |
Where to create the project. | graft-app |
--template, -t |
Which template. | app |
--name <package> |
Asked only for plugin. |
The directory name; graft-plugin-<directory> for plugin |
--plugin <package> |
Not asked. Repeat it for more plugins. | None |
--package-manager <npm|pnpm|yarn> |
Not asked. Add @<version> to record it. |
The package manager that runs the command, otherwise npm |
--install, --no-install |
Whether to install dependencies. | Yes |
--git, --no-git |
Whether to run git init. |
Yes |
--yes, -y |
Takes every default. |
| Rule | Behavior |
|---|---|
| Target directory | Must be new, empty, or hold only .git and .DS_Store, so . works in a freshly cloned repository. Otherwise it asks for another directory, or fails with CreateError when it cannot ask. It never removes or replaces a file. |
| Questions first | Every question comes before anything is written. Ctrl+C prints Cancelled, exits with a nonzero status and leaves nothing on disk. |
--plugin |
Takes name or name@range; short names map as in graft add --plugin. The package goes into dependencies and gets a - name: entry in graft.yml. Without a range, Graft’s own plugins use Graft’s version and others latest. |
--package-manager |
A version, as in pnpm@9.15.9, is written to the packageManager field of package.json. |
| Git | git init only, without a first commit. Skipped, with the reason, when the project is inside a repository or git is not installed. |
| Schema | After installing, the project’s own @graftland/cli runs graft schema. |
| Afterwards | Prints the commands to run next, such as cd my-app and pnpm dev, including an install, graft schema or git init that was skipped or failed. A failed step keeps the project and exits with a nonzero status. |
Templates
Section titled “Templates”| Template | For | Files besides package.json and graft.yml |
|---|---|---|
app |
An application. graft.yml loads the example plugins: hello, and ticker using the service of greeter. |
src/hello.ts, src/greeter.ts, src/ticker.ts, tsconfig.json, README.md, .vscode/extensions.json, .gitignore |
plugin |
A plugin package to publish. | src/index.ts, test/index.test.ts, tsconfig.json, tsconfig.build.json, README.md, .vscode/extensions.json, .gitignore |
minimal |
Only the config file, with no entries besides those from --plugin. |
.gitignore |
| Script | app |
plugin |
minimal |
|---|---|---|---|
dev |
graft start --watch |
graft start --watch, loading src/index.ts |
graft start --watch |
start |
graft start |
graft start |
|
test |
vitest run |
||
typecheck |
effect-tsgo tsc |
effect-tsgo tsc |
|
build |
Compiles src to dist |
- Every project depends on the same Graft version as the command, and
graft.ymlpoints to its editor schema,./graft.schema.json. - Node runs the TypeScript of
appdirectly, so it has no build step. Apluginpackage is built, because Node does not strip types insidenode_modules. .vscode/extensions.jsonrecommends the YAML extension, which completesgraft.ymlfrom its schema. The TypeScript projects also report Effect diagnostics intypecheck.
graft add
Section titled “graft add”graft add <package...>graft add --plugin <name> [--global]Adds dependencies to the project in the current directory, and installs them.
Each package can have a version range, such as graft-plugin-review@^1.2.0.
Without one, Graft uses the latest version.
graft remove
Section titled “graft remove”graft remove <package...>Removes dependencies from the project in the current directory.
graft schema
Section titled “graft schema”graft schema [--output <file>]Writes graft.schema.json, the editor schema
of graft.yml, for the project in the current directory. Besides what the
released schema checks, it checks the config of every installed plugin that
registers one.
| Case | Behavior |
|---|---|
| Which packages | Those in dependencies, devDependencies and optionalDependencies that list @graftland/graft as a dependency or peer and export ./schema. |
| What is imported | Only their ./schema modules. No plugin is loaded. |
| A dependency is not installed | A warning. Its config stays open. |
A ./schema is not valid |
SchemaRegistrationError names the package, and no file is written. |
| Kept current | graft add and graft remove rewrite graft.schema.json after installing when the project has one, and exit with a nonzero status if that fails. graft create writes it. |
--output, -o |
The path to write. Defaults to graft.schema.json. |
Run it yourself after installing or upgrading packages with the package manager directly.
Package managers
Section titled “Package managers”add and remove use the project’s package manager. create uses
--package-manager, otherwise the package manager that runs it, otherwise npm.
add and remove detect it in this order:
- The
packageManagerfield ofpackage.json. - The lockfile.
- The command that ran
graft. - npm, if none of these say.
npm, pnpm and Yarn are supported. Yarn Plug’n’Play is not.
Installation ownership
Section titled “Installation ownership”An install needs exclusive write access to the project until it finishes, including recovery.
- Do not edit project files or run another installer during
add,removeor the install ofcreate. - Calls through one PackageManager run one after another. Graft does not use a lock against other managers, editors or processes.
Rollback
Section titled “Rollback”| Situation | Behavior |
|---|---|
| The install fails or is interrupted | Graft restores the backed-up package files byte for byte, including package.json and lockfiles, and removes tracked files that did not exist before. |
| Changes made during the install | Rollback overwrites them. It does not detect conflicts or merge edits. |
| The project had a lockfile for this manager | Graft also tries to repair node_modules. |
| Recovery errors | Reported together with the original failure. |
| Process crash | Graft does not promise recovery. |
Managed package files must be regular files, or must not exist. Graft rejects symbolic links, including dangling links, and other file types before the install writes anything.
Managed upgrades
Section titled “Managed upgrades”graft start loads
managed Remote entries through the same
public Loader and driver APIs as an embedded host. Use an application-owned
operator endpoint that calls Graft.transition with the entry id. There is no
built-in transition subcommand. --watch does not upgrade managed units: HMR
skips managed runtimes and Loader rejects managed-tree refreshes before changing
its target.
graft check
Section titled “graft check”graft --no-plugins check graft.check.jsonChecks published manifests against an explicit host before loading. Paths are
relative to the preflight file, which defaults to graft.check.json.
Compatibility checks define the file and
diagnostics. The check does not install packages or alter the application’s
configuration.
graft remote manifest
Section titled “graft remote manifest”graft remote manifest ./releasegraft remote manifest ./release --check --provenance ./provenance.jsonProvided by the enabled remote/cli command plugin. Generates or verifies the
graft field from built declarations and optionally records package provenance.
See Plugin manifests for the publication workflow.
Application releases
Section titled “Application releases”Enable @graftland/plugin-release/cli to use graft release build, plan,
apply, rollback, history and status. Commands require --socket;
apply and rollback also require --expected. Use --json for CI output.
Rejections and unsuccessful transition outcomes exit nonzero. See
Application releases for the source format,
revocations and durable startup selection.
graft approve
Section titled “graft approve”Enable @graftland/plugin-release/approve-cli to use
graft approve statement.json --who operator@example --socket /app/control.sock --json.
Enable @graftland/plugin-release/cli for the release commands.
graft release approvals returns reviewable statements;
graft release revoke GRANT --who operator@example records revocation and
terminates affected units. Both release commands require --socket. Rejections
exit nonzero. See trust and isolation.
graft add disables lifecycle scripts for all packages, including recovery
installs. This also applies to add --plugin; enabling a CLI extension still
grants it execution in the host.