@maykonpaulo/maestro-cli
v0.3.3
Published
Command-line interface for the Maestro declarative engine — offline generate, validate, diff, snapshot and introspect commands over @maykonpaulo/maestro-core.
Readme
@maykonpaulo/maestro-cli
📖 Guia completo — criar o admin de qualquer sistema: está no README do pacote
@maykonpaulo/maestro-serverno npm → https://www.npmjs.com/package/@maykonpaulo/maestro-server
Command-line interface for the Maestro declarative engine.
Status: foundation + operational commands
The Phase 5 foundation shipped the maestro binary, --help, --version, and an extensible
command architecture. maestro generate (wraps the core's Declarative Config Generator),
maestro validate (wraps the core's Declarative File Loader), maestro diff (compares two
declarative config files), maestro snapshot (produces a canonical, deterministic snapshot of a
declarative config file) and maestro introspect (canonicalizes a local IntrospectionResult, or
connects to a real datasource via --provider) are the operational commands shipped so far.
maestro introspect resolves a real provider from the consumer's project via --provider <alias>
(12 aliases → 9 published @maykonpaulo/maestro-provider-* packages) and connects to live databases;
the CLI itself stays driver-free — the database driver lives inside the provider the consumer
installs. The CLI is a pure consumer of @maykonpaulo/maestro-core; it introduces no capability that
doesn't already exist there.
The package is now publishable (private removed, publishConfig.access: public); its first
public release is prepared via a Changeset and ships through the normal release flow
(next → rc → latest). The CLI is not the owner of the repo-wide GitHub Release latest
pointer — that stays with @maykonpaulo/maestro-core (the CLI's releases are tagged with
--latest=false). Publishing happens only via merge/promotion in the release workflow, never from a
local command. See RELEASE.md.
Usage
maestro --help
maestro --version
maestro generate --input ./metadata.json --type metadata --format json
maestro validate --input ./maestro.config.yaml
maestro diff --from ./maestro.config.old.yaml --to ./maestro.config.new.yaml
maestro snapshot --input ./maestro.config.yaml --out ./maestro.snapshot.json
maestro introspect --input ./introspection.json --out ./maestro.introspection.json$ maestro --help
maestro — Maestro CLI
Command-line interface for the Maestro declarative engine.
Usage:
maestro <command> [options]
Options:
-h, --help Show this help message
-v, --version Show the CLI version
Available commands:
generate Generate a declarative config from a local metadata/schema JSON file
validate Validate a declarative config file (YAML or JSON)
diff Compare two declarative config files (YAML or JSON)
snapshot Produce a canonical snapshot of a declarative config file (YAML or JSON)
introspect Introspect a schema (offline file or real provider) into a canonical artifactRunning an unrecognized command exits with a non-zero status and a message pointing back to
--help:
$ maestro unknown-thing
Unknown command 'unknown-thing'. Run "maestro --help" to see available commands.
$ echo $?
1maestro generate
Reads a local JSON file containing either EntityMetadata[] or EntitySchema[] (plus optional
relations/operations) and generates a declarative config (EntityDeclaration[] + optional
consumers) using the core's generateDeclarativeConfigFromMetadata/generateDeclarativeConfigFromSchema
and serializeDeclarativeConfig.
What it does NOT do: no introspection of a live datasource, no database/provider connection, no
createMaestro() call. It only reads a file, transforms it via the core's public APIs, and writes
text (stdout or a file). Introspecting a real datasource is a separate command (maestro introspect
--provider <alias>).
Run maestro generate --help for the full flag reference and examples inline.
What input is expected
--type metadataexpects the JSON file to be an object matching{ entities: EntityMetadata[], relations?: RelationMetadata[], operations?: OperationMetadata[] }— typically produced by serializingengine.getMetadata()(or theentitiessubset of it).--type schemaexpects{ entities: EntitySchema[], relations?: RelationSchema[] }— the low-level config shape you'd otherwise write by hand or get from introspection tooling.
Minimal examples of both shapes are checked into
packages/cli/examples/metadata.json and
packages/cli/examples/schema.json.
JSON output
$ maestro generate --input packages/cli/examples/metadata.json --type metadata --format json
{
"entities": [
{
"entity": "user",
"label": "User",
"pluralLabel": "Users",
"fields": {
"email": { "type": "email", "label": "Email", "required": true, "searchable": true },
"id": { "type": "uuid", "label": "Id", "readonly": true, "primary": true }
}
}
]
}YAML output
$ maestro generate --input packages/cli/examples/schema.json --type schema --format yaml
entities:
- entity: user
label: User
pluralLabel: Users
fields:
email:
type: email
label: Email
required: true
searchable: true
id:
type: uuid
label: Id
readonly: true
primary: trueWriting to a file
maestro generate --input ./metadata.json --type metadata --format yaml --out ./maestro.config.yaml
# Declarative config written to ./maestro.config.yaml| Flag | Required | Description |
|---|---|---|
| --input <path> | yes | Path to a local JSON file |
| --type <metadata\|schema> | yes | Whether the input matches EntityMetadata[] or EntitySchema[] shape |
| --format <json\|yaml> | no (default json) | Output format |
| --out <path> | no (default: stdout) | Writes the result to a file instead of printing it |
Errors (missing flags, unreadable file, invalid JSON, malformed shape) print a single readable line
to stderr prefixed with maestro generate: and exit with status 1.
Recommended flow: metadata/schema → generate → maestro.config → createMaestro
EntityMetadata[] / EntitySchema[] (already exists)
↓ maestro generate --input ... --type ... --out maestro.config.yaml
maestro.config.yaml
↓ loadDeclarativeConfigFromFile('./maestro.config.yaml', { yamlParser })
DeclarativeFileConfig (validated)
↓
createMaestro({ datasources, declarations })The file maestro generate writes is a normal declarative config — it round-trips through the
existing Declarative File Loader and
createMaestro({ declarations }) unchanged, exactly like a hand-written maestro.config.yaml. See
docs/specs/cli-generate.md for the full walkthrough, including
how to validate the generated file end-to-end.
maestro validate
Reads a local declarative config file (maestro.config.yaml/.yml/.json) and validates its
structure using the core's loadDeclarativeConfigFromString. It is the natural counterpart to
maestro generate: generate writes the file, validate confirms it is well-formed before you use
it at runtime.
$ maestro validate --input packages/cli/examples/maestro.config.yaml
Valid declarative config: packages/cli/examples/maestro.config.yaml
Entities: 2
Consumers: 1The format is detected from the file extension (.json → JSON, .yaml/.yml → YAML) unless
--format overrides it. YAML is parsed by the yaml package the CLI provides — the core has no
bundled YAML dependency.
What it does NOT do: no createMaestro() call, no database/provider connection, and it does NOT
require declared operations to have a real handler binding. It only reads a file and validates its
declarative structure. Wiring operations to code and starting the runtime is what createMaestro()
does separately — see the validate spec
for the exact difference.
| Flag | Required | Description |
|---|---|---|
| --input, -i <path> | yes | Path to a local declarative config file (.yaml, .yml or .json) |
| --format <json\|yaml> | no | Overrides the format detected from the file extension |
Errors (missing flag, unreadable file, unknown extension, invalid format, parse failure, invalid
structure) print a single readable line to stderr prefixed with maestro validate: and exit with a
non-zero status. Run maestro validate --help for the full reference, and see
docs/specs/cli-validate.md for the complete walkthrough.
maestro diff
Compares two local declarative config files and prints a readable, deterministic report of what
changed. Both files are loaded and validated with the core's loadDeclarativeConfigFromString (the
same validation as maestro validate), then their normalised structures are compared. It's the tool
for reviewing, promoting or versioning a declarative change: validate confirms one file is
well-formed; diff shows exactly what moved between two versions.
$ maestro diff --from ./before.json --to ./after.json
Differences found.
Entities:
+ invoice
- legacyCustomer
~ user
fields:
+ email
~ name
label: "Full name" → "Name"
operations:
- archive
Consumers:
+ admin:user
~ backoffice:user
list: {"fields":["id"]} → {"fields":["id","name"]}When the two files are structurally equal it prints No differences found.. The format of each file
is detected from its extension (.json → JSON, .yaml/.yml → YAML) unless --from-format /
--to-format override it, so you can diff a .json against a .yaml.
What it compares: entities (label/pluralLabel/description/capabilities), fields
(type/label/required/readonly/description/enumOptions/relationEntity/…), operations, relationships,
and consumers (list/detail/forms/actions). What it does NOT do: no createMaestro(), no
database/provider connection, no operation-binding requirement, and it never compares against a live
schema — it diffs declarative files, not introspection metadata. The core's DiffEngine operates
on IntrospectionResult (real DB schema), which is a different, not-yet-wired path.
| Flag | Required | Description |
|---|---|---|
| --from, -f <path> | yes | Baseline declarative config file |
| --to, -t <path> | yes | Changed declarative config file |
| --from-format <json\|yaml> | no | Overrides the format detected from the --from extension |
| --to-format <json\|yaml> | no | Overrides the format detected from the --to extension |
Exit codes: 0 = valid files, no differences; 1 = valid files, with differences; 2 = usage,
read, parse or validation error. Errors print a single readable line to stderr prefixed with
maestro diff:. Run maestro diff --help for the full reference, and see
docs/specs/cli-diff.md for the complete walkthrough.
maestro snapshot
Loads a local declarative config file, validates it with the core's loadDeclarativeConfigFromString
(the same validation as maestro validate), and emits a canonical, deterministic JSON snapshot of
its declarations. The snapshot is a stable, versionable artifact: commit it alongside your config, review
it in PRs, and use it as the baseline for future comparison. The same (semantically equivalent) input
always produces byte-identical output.
$ maestro snapshot --input packages/cli/examples/maestro.config.yaml
{
"declarations": {
"consumers": [ ... ],
"entities": [ ... ],
"relations": [ { "fromEntity": "order", "fromField": "userId", "toEntity": "user", "toField": "id", "type": "many-to-one" } ]
},
"kind": "maestro.declarative.snapshot",
"schemaVersion": 1,
"source": { "format": "yaml", "path": "packages/cli/examples/maestro.config.yaml" },
"summary": { "consumers": 1, "entities": 2, "relations": 1 }
}Writing to a file prints a readable summary instead of the JSON:
$ maestro snapshot --input packages/cli/examples/maestro.config.yaml --out ./maestro.snapshot.json
Snapshot written: ./maestro.snapshot.json
Entities: 2
Consumers: 1
Relations: 1The snapshot is canonical: entities are sorted by name, consumers by name, relationships (and the
flattened top-level relations index, each tagged with its fromEntity) by derived id, and every object
key is sorted. It is deterministic by design — it never includes a timestamp, an absolute path, a
random hash or any value that varies between runs of the same input, so re-running it only changes the
output when the config actually changed.
Declarative snapshot vs. introspection snapshot. This command snapshots a declarative file, not a
live database or runtime introspection. The core's Snapshot Engine (IntrospectionSnapshot /
SnapshotRepository) captures an IntrospectionResult from a real datasource and intentionally carries
volatile fields (id, timestamp) — a different, not-yet-wired path. maestro snapshot is local,
offline and reproducible.
What it does NOT do: no createMaestro(), no database/provider connection, no operation execution,
and it does NOT require declared operations to have a real handler binding — operations are captured as
declared. It never modifies the input file.
| Flag | Required | Description |
|---|---|---|
| --input, -i <path> | yes | Path to a local declarative config file (.yaml, .yml or .json) |
| --input-format <json\|yaml> | no | Overrides the format detected from the --input extension |
| --format json | no (default json) | Snapshot output format. Only json is supported for now |
| --out <path> | no (default: stdout) | Writes the snapshot to a file instead of printing it |
Exit codes: 0 = snapshot produced; 1 = usage, read, parse, validation or write error. Errors
print a single readable line to stderr prefixed with maestro snapshot:. Run maestro snapshot --help
for the full reference, and see docs/specs/cli-snapshot.md for the
complete walkthrough, including how validate, diff and snapshot relate.
maestro introspect
Reads a local JSON file describing an IntrospectionResult — the core's introspection contract
({ entities: EntityIntrospectionSchema[], relations: RelationIntrospectionSchema[] }) — validates it
against that contract, and emits a canonical, deterministic JSON artifact. The same (semantically
equivalent) input always produces byte-identical output, so the artifact is a stable, versionable input
for future comparison, generation or review.
$ maestro introspect --input packages/cli/examples/introspection.json
{
"kind": "maestro.introspection",
"result": {
"entities": [ { "fields": [ ... ], "table": "orders" }, { "fields": [ ... ], "table": "users" } ],
"relations": [ { "confidence": "definite", "from": { ... }, "id": "orders.user_id->users.id", ... } ]
},
"schemaVersion": 1,
"source": { "path": "packages/cli/examples/introspection.json" },
"summary": { "entities": 2, "relations": 1 }
}Writing to a file prints a readable summary instead of the JSON:
$ maestro introspect --input packages/cli/examples/introspection.json --out ./maestro.introspection.json
Introspection written: ./maestro.introspection.json
Entities: 2
Relations: 1Two modes. maestro introspect runs either offline (--input, a local IntrospectionResult
JSON file) or against a real datasource (--provider <alias>), producing the same canonical
artifact in both. In offline mode it does not connect to any database, read a connection string
or .env, load a driver, or execute a query — it operates only on the local JSON file. In provider
mode it resolves a real IntrospectionProvider from the consumer's project (via --provider
<alias> or --provider-package <name>), connects to the live database and reads its schema. The CLI
itself stays driver-free: the database driver lives inside the provider the consumer installs, not
in the CLI's dependencies.
The CLI resolves 12 aliases → 9 published packages: sqlite, postgres, mysql and mssql all
map to @maykonpaulo/maestro-provider-sql (dialect selected per alias); mongodb, redis,
elasticsearch, dynamodb, cassandra, couchbase, firestore and neo4j each map to their own
@maykonpaulo/maestro-provider-* package. Install the provider in your project, then:
# SQLite via the --database shortcut
maestro introspect --provider sqlite --database ./app.db --out introspection.json
# MongoDB via generic --provider-option key=value (repeatable)
maestro introspect --provider mongodb \
--provider-option uri=mongodb://localhost:27017 \
--provider-option database=app \
--out introspection.jsonSee docs/specs/cli-provider-resolution.md for the full
alias table, package resolution, provider options and error handling.
The artifact is canonical: entities are sorted by table, relations by id, and every object key is
sorted. Field (column) order within an entity is preserved. It is deterministic by design — no
timestamp, absolute path or hash is ever included, so re-running it only changes the output when the input
actually changed.
introspect vs. snapshot vs. diff. All three are read-only and never call createMaestro() or
execute operations, but they operate on different inputs:
introspectproduces a canonicalIntrospectionResult(the real DB-schema contract) — either from a local file (--input, offline) or by connecting to a real datasource (--provider).snapshotcanonicalizes a declarative config file (maestro.config.yaml/.json), offline.diffcompares two declarative config files, offline.
The core's Introspection Runtime (DiffEngine, Snapshot Engine, ReportGenerator) also operates on
IntrospectionResult, but those paths assume a live/real introspection and carry volatile data (e.g.
IntrospectionSnapshot's id/timestamp, the report's completedAt). maestro introspect keeps its
output canonical and reproducible, so it does not use them — see the
spec for the exact boundary.
What it does NOT do: no createMaestro(), no operation execution, and no operation-binding
requirement. It never modifies the --input file. (In offline mode it also makes no database connection
whatsoever; in --provider mode the connection is owned by the resolved provider, not the CLI.)
| Flag | Required | Description |
|---|---|---|
| --input, -i <path> | offline mode | Path to a local IntrospectionResult JSON file |
| --provider <alias> | provider mode | Official alias (e.g. sqlite, mongodb) resolved to a published @maykonpaulo/maestro-provider-* package in your project |
| --provider-package <name> | provider mode | Explicit provider package name (bypasses the alias table; supports third-party providers) |
| --database <value> | no | Alias shortcut for the provider's primary connection option (e.g. databasePath for SQLite) |
| --provider-option <k=v> | no | Generic, repeatable provider option passed to the provider factory |
| --config <path> | no | Config file with an introspection section; flags take precedence over it |
| --out <path> | no (default: stdout) | Writes the artifact to a file instead of printing it |
--input (offline) and --provider/--provider-package (real introspection) are mutually exclusive. The
CLI declares no provider or database driver as a dependency — the provider is loaded from the
consumer's node_modules at runtime.
Exit codes: 0 = artifact produced; 1 = usage, read, parse, validation or write error. Errors print a
single readable line to stderr prefixed with maestro introspect:. Run maestro introspect --help for the
full reference, and see docs/specs/cli-introspect.md for the
complete walkthrough.
Architectural rule: the CLI consumes, it doesn't implement
The CLI never re-implements a capability that already exists in @maykonpaulo/maestro-core. It
calls generateDeclarativeConfigFromMetadata/generateDeclarativeConfigFromSchema and
serializeDeclarativeConfig directly — there is no parallel generation or serialization logic in
this package. maestro diff reuses the core's loadDeclarativeConfigFromString for loading and
validation, adding only the small, local, deterministic structural comparison of two
DeclarativeFileConfig objects (the core's DiffEngine targets IntrospectionResult, not
declarative files). maestro snapshot likewise reuses loadDeclarativeConfigFromString for
load+validation and adds only a small, local, deterministic canonicalizer over the resulting
DeclarativeFileConfig (the core's Snapshot Engine, IntrospectionSnapshot/SnapshotRepository,
targets a runtime IntrospectionResult with volatile id/timestamp fields — incompatible with a
reproducible declarative snapshot). maestro introspect consumes the core's validateIntrospectionResult and
normalizeIntrospectionResult directly for validation/canonicalization, and — in --provider mode —
runs the resolved provider through the core's runIntrospectionProvider (never calling introspect()
raw). It adds only a small, local envelope + deterministic key-sorting for byte-stable output; the
core's higher-level runtime helpers (DiffEngine, Snapshot Engine, ReportGenerator) carry volatile
fields (id/timestamp/completedAt), so a reproducible artifact can't reuse them here — see
docs/specs/cli-introspect.md.
Command architecture
Command— interface a command implements:name,description, optionalhelpText(shown formaestro <name> --help; falls back to the global help when omitted),run(context).CommandRegistry— a simple map of registered commands (register,get,list).parseArgs— minimal global parser: detects--help/-hand--version/-v, and the first non-flag argument as the command name; everything else (including a command's own flags) passes through untouched.runCli— orchestrates parsing → dispatch → exit code; when--helpis combined with a known command, prints that command's ownhelpTextinstead of the global help. Accepts an injectablestdout/stderrandCommandRegistry, so it's fully testable without spawning a process.createGenerateCommand— builds thegenerateCommand; accepts injectablereadFile/writeFilefor testing without touching the real filesystem.createValidateCommand— builds thevalidateCommand; accepts an injectablereadFilefor testing without touching the real filesystem.createDiffCommand— builds thediffCommand; accepts an injectablereadFilefor testing without touching the real filesystem. The pure comparison lives indiffDeclarativeConfigs(diffDeclarativeConfigs+formatDeclarativeDiff), tested in isolation from file I/O.createSnapshotCommand— builds thesnapshotCommand; accepts injectablereadFile/writeFilefor testing without touching the real filesystem. The pure canonicalization lives inbuildDeclarativeSnapshot(buildDeclarativeSnapshot+serializeSnapshot), tested in isolation from file I/O.createIntrospectCommand— builds theintrospectCommand; accepts injectablereadFile/writeFilefor testing without touching the real filesystem. The pure validation/canonicalization lives inbuildIntrospection(validateIntrospectionResult+buildIntrospectionArtifact+serializeIntrospection), tested in isolation from file I/O.
Adding a future command means implementing Command (with its own helpText) and calling
defaultRegistry.register(...) in runCli.ts — no change to runCli, parseArgs, or the
entrypoint is needed.
Development
pnpm --filter @maykonpaulo/maestro-cli build
pnpm --filter @maykonpaulo/maestro-cli test
node packages/cli/dist/cli.js --help
node packages/cli/dist/cli.js generate --help
node packages/cli/dist/cli.js generate --input packages/cli/examples/metadata.json --type metadata
node packages/cli/dist/cli.js validate --help
node packages/cli/dist/cli.js validate --input packages/cli/examples/maestro.config.yaml
node packages/cli/dist/cli.js diff --help
node packages/cli/dist/cli.js diff --from packages/cli/examples/maestro.config.yaml --to packages/cli/examples/maestro.config.yaml
node packages/cli/dist/cli.js snapshot --help
node packages/cli/dist/cli.js snapshot --input packages/cli/examples/maestro.config.yaml
node packages/cli/dist/cli.js introspect --help
node packages/cli/dist/cli.js introspect --input packages/cli/examples/introspection.jsonSee docs/cli.md for the consolidated CLI overview (recommended flow, command
table, limitations and next steps), docs/specs/cli-foundation.md
for the CLI foundation architecture, docs/specs/cli-generate.md for the full
maestro generate walkthrough, docs/specs/cli-validate.md for
maestro validate, docs/specs/cli-diff.md for maestro diff,
docs/specs/cli-snapshot.md for maestro snapshot, and
docs/specs/cli-introspect.md for maestro introspect.
