CLI
@monospace/cli 0.2 and @monospace/extension-kit 0.3. Overview
The @monospace/cli package provides the monospace binary. Two commands work with extensions:
| Command | Does |
|---|---|
monospace extension create [directory] | Scaffolds an extension project from a template. The only template today scaffolds a data connector extension with one entry |
monospace extension build | Builds the project into a manifest and one JavaScript file per entry, and optionally installs it |
Neither command needs a login or a running instance (a Monospace deployment). Both run locally against your files. The CLI needs Node.js 22.12 or later.
Add the CLI to an extension project as a dev dependency:
npm install -D @monospace/cli
pnpm add -D @monospace/cli
yarn add -D @monospace/cli
bun add -D @monospace/cli
A project that monospace extension create writes already lists it. To create that project, run the CLI without installing it; see Create an Extension.
Use Global Flags
These flags work with every command.
| Flag | Effect |
|---|---|
--json | Print one JSON result on stdout. See Read JSON Output |
--no-input | Never prompt. A question that would have been asked fails the command, unless it has an answer for unattended runs (e.g., create's directory) |
-h, --help | Show help for the command |
-V, --version | Show the CLI version |
Without --json, results print to stdout, and prompts, warnings and errors go to stderr. With --json, stdout carries only the JSON result.
--json doesn't turn prompts off, so pass --no-input in scripts. Prompts are also off when stdin or stderr isn't a terminal, or when the CI environment variable is set.
The CLI reads a .env file in the working directory. Variables already set in your environment take precedence over it, and a CI entry there also turns prompts off.
Create an Extension
monospace extension create writes a new extension project. Its template contains one data connector entry, and it's the only template today.
The project doesn't exist yet, so run the CLI without installing it. Each package manager fetches @monospace/cli and runs its monospace binary:
npx @monospace/cli@0.2 extension create ./artic --id example/artic --name "Art Institute of Chicago"
pnpx @monospace/cli@0.2 extension create ./artic --id example/artic --name "Art Institute of Chicago"
yarn dlx @monospace/cli@0.2 extension create ./artic --id example/artic --name "Art Institute of Chicago"
bunx @monospace/cli@0.2 extension create ./artic --id example/artic --name "Art Institute of Chicago"
| Argument or flag | Description |
|---|---|
directory | Directory to create the project in. It must not exist, or be empty apart from .git. When it has other files, the command asks for another directory |
--id <id> | Extension id, as namespace/name. Prompted for when omitted. See Id Format |
--name <name> | Display name. Defaults to a name derived from the id (example/artic gives Example Artic). When omitted, the command asks for it with that name filled in; without prompts, it uses that name. Must contain a visible character and no control characters |
Without directory, the command asks for one first, and offers the working directory when it's empty. Without prompts, it uses the working directory, which must then be empty. A directory you type at the prompt as ~, or starting with ~/, expands to your home directory.
With a new or empty directory, --id and --name, the command asks nothing. Without prompts, a missing --id, or a directory with other files, fails the command with cli.confirmation_required.
Scaffolded Files
The data connector template writes these files and nothing else:
| File | Contents |
|---|---|
extension.config.json | The extension config, version 0.1.0, with an editor $schema and one data connector entry. The entry has no icon, no engine.permissions and no engine.capabilities |
package.json | A private ES module package named after the id (example/artic becomes example-artic). Its build script runs monospace extension build, and its typecheck script runs tsc --noEmit. It depends on @monospace/extension-kit, with @monospace/cli, rolldown and typescript as dev dependencies |
tsconfig.json | strict, noEmit and verbatimModuleSyntax, with an ES2023 target and bundler module resolution, covering src |
src/data-connectors/<namespace>/index.ts | The data connector entry's entrypoint. setup returns introspect, a query that answers every read with two fixed items (single data objects), and a testConnection that answers { ok: true } |
src/data-connectors/<namespace>/schema.ts | The connector's schema: one collection, items, with the string fields id and name, a primary index on id, and readMany with output: fields.all() |
README.md | Build, type-check and install instructions, including the install location and automatic reloading settings |
.gitignore | Ignores dist and node_modules |
The scaffold typechecks and builds as written. Installed and added as a data source, it serves the two items from its items collection.
The scaffold declares only readMany, with no filter, sort or paging, and its query ignores the request. Declare more only together with the code that answers it.
The template's entry id is <namespace>/data-connector, where <namespace> is the first segment of --id. --id example/artic produces the entry example/data-connector. Two projects with the same namespace get the same entry id, so rename the entry before you install both. The Data Connector Quickstart renames it to example/artic-connector.
The command doesn't install dependencies. It prints what it created, the extension id, entry id, directory and file list, then the next two commands:
Created the Art Institute of Chicago extension
Extension example/artic
Entry example/data-connector
Directory /home/you/artic
- .gitignore
- README.md
- extension.config.json
- package.json
- src/data-connectors/example/index.ts
- src/data-connectors/example/schema.ts
- tsconfig.json
Next steps
1. cd ./artic && npm i
2. npm run build
The first step is written for a POSIX shell (cd … && …, with POSIX quoting), not for cmd.exe or PowerShell. The install and build commands match your package manager. The CLI looks in the working directory and its parents for a lockfile, or a packageManager or devEngines.packageManager field in package.json. Without one, it uses the package manager that launched it, and falls back to npm.
Build an Extension
monospace extension build reads extension.config.json from the working directory and writes the built extension to ./dist. Run it from the project root:
npm run build
pnpm build
yarn build
bun run build
| Flag | Description |
|---|---|
--out-dir <path> | Write the build here instead of ./dist |
--install-to <path> | After a successful build, copy it into this instance install directory |
--watch | Rebuild whenever a source file or extension.config.json changes. Stop with Ctrl-C |
The build requires rolldown ^1.2.0 in the project's dependencies. The CLI uses the project's copy, not its own.
Build Steps
- Parse and validate
extension.config.jsonagainst the extension config schema. - Build each entry's code as its entry type defines. See Data Connector Bundles.
- Write
manifest.json. - Swap the new build in place of the old output directory.
Without --watch, every entry builds into a staging directory first, so a failed build leaves the previous output untouched.
The build doesn't type-check your TypeScript or run your entry code. A successful build or install can still fail to load. Run the scaffolded typecheck script for type errors.
Data Connector Bundles
For each data connector entry, the build:
- Bundles
engine.entrypointinto one ES module:platformneutral, no code splitting, no minification, no source maps. - Checks the bundle: it must be a single JavaScript chunk, have a default export, and contain no
import,export ... from, dynamicimport()or directrequire(...)call. The check reads the code without running it and doesn't catch every form (e.g.,require(...args)). - Writes the bundle as
<entry id>.js.
The build checks that a default export exists, not that it has a setup function. To run a data connector, see Test a Data Connector.
Output Directory
A build of the Data Connector Quickstart project, whose entry id is example/artic-connector, writes this layout:
dist/
├── .monospace-build
├── manifest.json
└── example/
└── artic-connector.js
.monospace-build marks the directory as owned by the extension. The CLI replaces an output directory only when it is missing, empty, or already holds the same extension id, read from its manifest.json or, without a readable manifest, from .monospace-build. It refuses a directory that contains the project, the working directory, any entrypoint source, or any other source file the bundler reports the entrypoints import.
Install a Build
--install-to copies a successful build into an instance's install directory. See Choose the Install Location for where that directory is.
npm run build -- --install-to ../monospace/extensions
pnpm build --install-to ../monospace/extensions
yarn build --install-to ../monospace/extensions
bun run build --install-to ../monospace/extensions
The CLI writes the build to <install-to>/<namespace>__<name>/, named after the extension id. example/artic installs to example__artic/. The .monospace-build marker isn't copied.
| Rule | Behavior |
|---|---|
Target holds another extension, or files but neither an extension manifest nor a .monospace-build marker | Refused with extension.install_dir_not_ours |
Target and --out-dir inside each other | Refused with extension.install_overlaps_artifact |
| Target is a symlink | The CLI updates the directory it points to. The instance doesn't load a symlinked extension directory, so install into a real directory |
| Existing install of the same extension | Replaced whole, never merged. Files you added there are deleted |
| Copy fails | The previous install stays in place |
Without --watch, the CLI installs before it replaces the output directory. If replacing --out-dir then fails, the command fails with the new build already installed.
After you rename the extension id, remove the old output directory or pass a new --out-dir. The build refuses an output directory that holds another extension id.
The renamed build installs to a new directory. The CLI never removes the old install, so delete it yourself.
A running instance picks up the install on its next start. With MONOSPACE_EXTENSIONS__AUTO_RELOAD on, it reloads the install automatically. Auto-reload is off by default, and the install directory must exist when the instance starts.
Without --json, the CLI prints Restart the engine to load it, unless it runs with MONOSPACE_EXTENSIONS__AUTO_RELOAD=true. after a build that installed. A build without --install-to ends with To load it, build with --install-to pointing at an engine's extensions directory. instead. In watch mode, it prints one of them once, when you stop a session whose last build succeeded.
Watch for Changes
--watch builds, then rebuilds when a source file or extension.config.json changes:
npm run build -- --watch --install-to ../monospace/extensions
pnpm build --watch --install-to ../monospace/extensions
yarn build --watch --install-to ../monospace/extensions
bun run build --watch --install-to ../monospace/extensions
- Each rebuild writes in place into the output directory. Without
--json, a successful rebuild printsBuilt 1 entry.orBuilt 2 entries., and so on. - A failed rebuild prints a warning and keeps the last good file for the failed entry.
- With
--install-to, the CLI installs only when every entry in the cycle built. A good entry never installs next to a broken one. - After each successful rebuild, the CLI deletes every file in the output directory except
manifest.json,.monospace-buildand the current entry files.
When preparing a cycle fails (e.g., an invalid extension.config.json or a missing Rolldown), watch retries only after the next change to extension.config.json. Installing a missing dependency doesn't trigger a retry: save the config again or restart the command.
Watch mode doesn't reload the instance. Combine it with MONOSPACE_EXTENSIONS__AUTO_RELOAD to see changes without a restart.
Read JSON Output
With --json, each command prints exactly one JSON object on stdout when it finishes, including on failure:
{
"data": {
"artifactDir": "/home/you/artic/dist",
"builds": 1,
"entries": [{ "id": "example/artic-connector", "output": "example/artic-connector.js" }],
"installedTo": null
},
"effectState": "complete",
"error": null,
"outcome": "ok",
"schema": "monospace.extension.build/1"
}
| Field | Values |
|---|---|
schema | monospace.extension.create/1 or monospace.extension.build/1. monospace.error/1 when the CLI fails before it identifies a command, or when a command stops before finishing |
outcome | ok, failed or cancelled |
effectState | complete, none (the failing step changed nothing) or unknown (files may have been written). In watch mode, a failure can report none even though earlier rebuilds wrote files |
data | The command's result, or null |
error | null, or { category, code, message, fix? }. fix is present only when the CLI has a suggestion |
The data fields depend on the command:
| Command | data fields |
|---|---|
create | id, entryId, name, dir, files, agent (the detected package manager), next (the install and build commands) |
build | artifactDir, entries, installedTo, builds (the number of build cycles, failed ones included) |
Check outcome, not data, to detect failure. A failed watch session still reports the last successful build in data. Without any successful build, entries is empty and installedTo is null.
--help and --version print to stderr under --json, so stdout stays empty.
Handle Exit Codes
| Exit code | When |
|---|---|
0 | Success; a command cancelled by SIGINT or SIGTERM (outcome cancelled: effects may have occurred); or a watch session stopped with Ctrl-C after a cycle that built |
1 | Any error, including a watch session whose last cycle failed, and a second SIGINT or SIGTERM before the command finished |
130, 143 | A second SIGINT (130) or SIGTERM (143) after the command finished but before the process exited |
Exit code 0 alone doesn't prove a build completed: check outcome under --json. A second signal stops the command at once. If the command hadn't finished, it reports cli.unfinished; under --json, that result uses the monospace.error/1 schema.
--watch runs until you stop it, or until it can no longer watch the files. Stopped with Ctrl-C, it exits 0 with outcome cancelled when the last cycle built, and 1 with that cycle's error when it failed. When it loses the watch, it ends on its own and exits 1 with that error.
Fix Common Errors
Every error has a code, shown in --json output. Most errors also print a fix. This table covers the config, bundle, output and create errors:
| Code | Cause | Fix |
|---|---|---|
extension.config_missing | No extension.config.json in the working directory | Run the build from the project root |
extension.config_invalid | The config breaks the schema: bad id or version, unknown field, missing field | Correct the field the message names |
extension.config_unparseable | Invalid JSON, including comments and trailing commas | Fix the JSON syntax |
extension.config_duplicate_key | A key appears twice in one object | Remove the repeated key |
extension.entry_id_duplicated | Two entries share an id | Give each entry its own id |
extension.rolldown_missing | rolldown isn't installed in the project. When package.json declares it, the message reads "This project declares rolldown, but its dependencies are not installed." | Install the project's dependencies, or add rolldown@^1.2.0 as a dev dependency |
extension.rolldown_unsupported | The project's rolldown isn't ^1.2.0 | Install a supported version |
extension.build_failed | Rolldown couldn't bundle the source. The message names the first error, with its file, line and column when Rolldown reports them. With several errors, it says how many more there are | Fix the source error it names |
extension.built_not_self_contained | A data connector bundle still imports a module, often a Node built-in such as node:fs | Import only code that bundles; Node built-ins aren't available |
extension.built_without_default_export | A data connector entrypoint has no default export, or the output is CommonJS | Export the entry's code (e.g., the connector) as the module default |
extension.built_split | A data connector bundle isn't exactly one JavaScript chunk | Remove code splitting or plugins that emit extra files |
extension.artifact_dir_not_ours | --out-dir holds another extension, or files but neither an extension manifest nor a .monospace-build marker | Choose a new or empty directory |
extension.output_holds_sources | --out-dir, or the <install-to>/<namespace>__<name> directory, contains the project, the working directory, an entrypoint source, or a source file an entrypoint imports | Choose a directory that contains none of them, such as ./dist |
extension.create_dir_not_empty | The create target gained files while the command asked its questions | Create the extension in a new directory |
extension.create_dir_unusable | The create target isn't a directory, can't be read, or a file can't be written into it | Point the command at a directory, or at a path that doesn't exist yet |
extension.create_id_invalid | --id isn't a valid extension id | Pass an id in id format, such as --id acme/store |
extension.create_name_invalid | --name has no visible character, or contains a control character | Pass visible text on one line, such as --name "Acme Store" |
cli.confirmation_required | The command needed an answer and prompts are off | Pass the argument or flag the message names |
See Also
- Extension Config and Manifest: every field the build validates
- Install and Manage Extensions: where installs go and how the instance reloads them
- Runtime Limits: why a data connector bundle must be one file with no imports
- Data Connector Quickstart: build and install a first data connector