Introspection & AI-first Tooling
Introspection & AI-first Tooling
Agents and tooling need to read an application's shape without assembling it. JSails exposes four read-only CLI surfaces plus one opt-in runtime endpoint that return machine-readable state — routes, plugin enablement, the app definition, and the request pipeline — without importing page or API modules and without opening a connection.
All CLI surfaces are read-only and own their own subcommands and flags, so they are routed before the shared flag-gating and never import an app config.
jsails inspect routes
inspect routes wraps discoverRoutes and prints the route manifest. It never
imports page/API modules or opens a connection.
jsails inspect routes --config jsails.app.js
jsails inspect routes --config jsails.app.js --json
With --json it emits one line with resolved absolute file paths.
jsails plugins resolve
plugins resolve wraps loadPluginEnablement and prints the merged enablement
from both sources — the code list and managed state. It never imports plugin
code, runs setup, or mutates state; the database config is consulted only when
the app is managed.
jsails plugins resolve --config jsails.app.js
jsails plugins resolve --config jsails.app.js --db-config jsails.config.js --json
The output carries { enabled, conflicts, disabledManaged, sources }. conflicts
explicitly flags ids present in both the code list and managed state — those
ids are excluded from enabled and must be reported rather than silently
resolved.
jsails describe
describe composes one machine-readable snapshot of an app's static
definition — no runtime state — as { app, routes, plugins, components, schema }.
jsails describe --config jsails.app.js
jsails describe --config jsails.app.js --db-config jsails.config.js --json
appcarries the resolved dirs, host, port,healthPath, andpublicOrigin(when set).routesis{ kind, route, dynamic, catchAll, params }and omitsfile(no absolute paths).pluginsis{ enabled, conflicts, disabledManaged, sources }.componentsis{ name, actions, writableKeys }[].schemais{ tables: [{ name, columns, indexes, uniques, foreignKeys }] }, or[]when there are no entities or no db config.
Default config is jsails.app.js; --db-config defaults to jsails.config.js
and is used only to extract the entity schema via getModelSchema(), which
builds metadata without connecting.
Like the other surfaces it never imports page/API modules, never opens a
connection, and never runs setup.
jsails explain <path>
explain resolves one request path against the discovered route manifest and
reports which route matches, its params, and the fixed request pipeline it would
traverse.
jsails explain /dashboard --config jsails.app.js
jsails explain /dashboard --config jsails.app.js --json
It is a pure CLI command: it never assembles an application, imports page/API
modules, or opens a connection. A path in the reserved /_jsails namespace
reports reserved: true, matched: false.
Runtime endpoint — GET /_jsails/introspect
Beyond the CLI, the runtime can expose the same kind of state over HTTP through
an opt-in, default-off, default-deny endpoint. Configure it in
jsails.app.js:
export default {
introspect: {
enabled: true,
authorize: (context) => context.session?.role === 'admin',
sections: ['routes', 'plugins', 'components', 'pipeline', 'config', 'health'],
},
};
authorize is required when enabled is true — the loader raises a
load-time AppConfigError otherwise — and follows the same exact-true
default-deny rule as the rest of the runtime.
curl http://127.0.0.1:3000/_jsails/introspect
curl 'http://127.0.0.1:3000/_jsails/introspect?section=migrations'
The full valid section set is routes, plugins, components, pipeline, config, migrations, diagnostics, jobs, health. The safe subset (routes, plugins,
components, pipeline, config, health) ships by default, and sections in
config is the default set returned when no ?section= is given. An explicit
?section= may name any valid section, including the live ones, which then
report unavailable if no provider handles them.
Three live sections are available:
migrations—{ applied: [{ name, appliedAt }], pending: string[] }; names/timestamps only, never checksums or operation JSON;unavailablewhen there is no data source or no tracking table.diagnostics—stats()counts/durations only, never rawentries;unavailablewhen the diagnostics plugin is not enabled.jobs—{ metrics, failed: [{ id, job, failedAt, attempts, error }] }; metadata only, never job payloads (data) or stack traces;unavailablewhen there is no jobs runtime.
The pipeline section reports the fixed stage list, the registered middleware
names, and the global middleware count. The config section reports a
redacted projection of the resolved app config — booleans for callbacks and
directory basenames only, never absolute paths, secrets, or callback identities.
Each response is an envelope of { generatedAt, sections: { [name]: { status, data? | error? } } }, with every section collected independently so one failure
never fails the rest. The endpoint never exposes secrets, session ids, CSRF
tokens, raw bodies, job payloads, or absolute filesystem paths.
Next steps
- Data Layer — entities, the portable schema, and migrations.