Skip to content

feat(cli): read a debug handler from loaded integrations - #5998

Merged
josephfarina merged 1 commit into
mainfrom
integration-debug-handlers
Sep 4, 2026
Merged

feat(cli): read a debug handler from loaded integrations#5998
josephfarina merged 1 commit into
mainfrom
integration-debug-handlers

Conversation

@josephfarina

@josephfarina josephfarina commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What changes for people

Installing an integration now turns on its debug logs. Nothing in the app changes. No line in astryx.config, no codemod, no coordinated rollout across the ~4,242 Nest apps that load @nest/xds-meta. The integration exports a handler; every app that already lists it starts reporting runs.

Before this, debug in the app's own astryx.config was the only way to attach a run handler, and the integration manifest schema had no way to contribute one. Getting fleet coverage meant editing every app.

How

An integration exports debug from its astryx.integration.* module:

import type {DebugEvent} from '@astryxdesign/cli/authoring';

export function debug(event: DebugEvent): void {
  reportSomewhere(event);            // synchronous — the process is exiting
}

export default {components: './src'};

A named export, not a manifest key — this is the whole reason the change is safe. A manifest key has to be understood by every CLI version already installed against that integration, and an older one rejects an unknown key by discarding the entire manifest: components, templates, codemods, docs, all of it, silently. That is #5119, and it cost @nest/xds-meta a week of invisibility. A named export is simply not read by a CLI that does not know to look for it, which is why transformTemplateSource already travels that way.

So this is purely additive. An integration that adds a debug export keeps working unchanged on every CLI released before today.

Both handlers run — this is the design decision worth arguing about

The app's own debug and every integration's handler all receive every event.

The tempting alternative is "app config wins, integration is the fallback". That is a trap: any app that adds a debug handler to debug itself would silently remove itself from the integration's debug logs. Nobody would ever notice, because the failure mode is a handler that is never called — and fleet coverage would erode as more people used the hook for its ordinary purpose.

  • Order, defined and documented: the app's own handler first, then integrations in the order the config lists them.
  • Dedupe by function identity, so a handler reached twice is called once.
  • Isolation per handler, not per loop. Each gets its own structuredClone of the sealed event and its own guarded call, reusing the containment feat(cli): report every command run to a debug function in astryx.config #4812 already built: one that throws, writes to stdout, or calls process.exit cannot change the command's exit code, corrupt a --json envelope, or stop the handlers after it.
  • Replace, not append. Project.load is a plain factory and a single command can run it twice (the pre-parse load, then the command's own), so registration replaces the set rather than growing it.

The gate widening, without which this PR does nothing

loadProjectDebugHandler runs before Commander parses, and it is gated on the config file text containing the string debug — deliberately, so astryx --version does not evaluate a project's config for a project that never opted in.

An app that just installs an integration has the word debug nowhere in its config. Measured on a scratch app, with only the named-export plumbing and this gate untouched:

command recorded
component, search, docs yes — these load a Project for their own reasons
--version, --help, theme list, parse errors no

Partial coverage biased toward heavy commands is not what anyone wants out of a usage dataset, so the gate now also opens on integrations. An app that lists integrations has asked for those packages' code to run — that is what an integration is.

Cost, measured, 5-run medians on an integration whose manifest is TypeScript (the expensive case — jiti):

before after
astryx --version (project declares integrations) 228 ms 277 ms
astryx docs <topic> (already loaded the project) 262 ms 260 ms
any project with no config, or no integrations unchanged unchanged

~50 ms, only on commands that never touched the config, only for projects that declare integrations. e2e-smoke.test.mjs is updated to encode the new contract: a config mentioning neither word is still left unevaluated.

Opting out

{"astryx": {"inheritDebug": false}}

in the app's package.json. It suppresses inherited handlers only — the app's own debug still runs. package.json rather than astryx.config because config parsing is strict, so an unknown config key is a hard error on an older CLI, while an astryx block in package.json is inert to every version that does not read it.

Verified in a real app, not just in tests

A scratch app whose astryx.config.mjs contains no occurrence of the word debug, with fake integrations exporting debug:

  • 6/6 commands recorded--version, --help, theme list, docs, component, and an unknown-command parse error. Exactly one row per run.
  • Both fire: with debug in the config and an integration exporting one, both are called, app first.
  • A hostile handler changes nothing. An integration handler that writes to stdout, sets process.exitCode = 77 and calls process.exit(3): exit code stayed 0, stdout was byte-identical (cmp) to the same command in a clean app, the write was diverted to stderr, and the other two handlers still fired.
  • --json stays parseable with a handler printing garbage mid-flight.
  • Broken integrations stay broken quietly: one whose manifest module throws on import, and one whose default export fails validation, neither crashed the CLI nor blocked the working integration. (A manifest that fails to load contributes nothing, its handler included — consistent with the existing rule, and documented.)
  • Ctrl-C is still recorded when the only handler came from an integration.

Repo gates, all green on this branch: pnpm build, pnpm test (14920 passed, 0 failed), pnpm lint:strict (0 errors), check:cli-structure, typecheck:authoring, typecheck:strict, readme:check, cli-api-types-verify, check:repo, check-knowledge. 26 new tests across three files.

Also in this PR: one line that unbreaks pnpm build on main

main does not build right now, and it is not something this PR caused. #5320 added matched/total to what scoreQuery returns; #5289 added an early return for the exact-keyword promote tier. Both landed, and the combination does not type-check, so sync:api-types — and therefore pnpm build, and therefore this PR's CI — fails. It is a one-line fix in its own commit (52556ee), reusing the asFull helper that already means "this hit answered the whole query".

Context

Consumer of the DebugEvent contract merged in #5971. The @nest/xds-meta side (an internal diff) already exports debug from its integration manifest and maps the event to Scuba; it is waiting on this.

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Sep 4, 2026 7:18pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Sep 4, 2026
github-actions Bot added a commit that referenced this pull request Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

Install an integration and its debug logs turns on. No line in the app's
astryx.config, no codemod, nothing in the app at all.

An integration exports `debug` from astryx.integration.*. It is a NAMED
export, not a manifest key: a key has to be understood by every CLI already
installed against that integration, and an older one rejects an unknown key
by discarding the whole manifest - components, templates and codemods with
it (#5119). A named export is simply not read by a CLI that does not know
about it.

Both handlers run. An app that sets its own `debug` still gets every event
and so does every integration. Fallback semantics would mean an app that
adds a handler to debug itself silently drops out of the integration's
debug logs, and nobody would ever notice - the failure is a handler that is
never called. Order is the app's own first, then integrations in config
order, each with its own copy of the event inside the existing isolation, so
one that throws, prints, or calls process.exit cannot reach the command or
the other handlers.

Also widens the pre-parse config gate, which is what makes this reach
anything. It opened only on the word `debug`, and an app that just installs
an integration has that word nowhere in its config - so the handler would
only have fired for commands that load a Project for their own reasons, and
never for --version, --help, `theme *`, or a parse error. It now opens on
`integrations` too. Measured cost on an integration with a TypeScript
manifest: ~50ms on those cheap commands, nothing on commands that were
loading the project anyway, and nothing at all for a project that declares
no integrations.

Opt out with {"astryx": {"inheritDebug": false}} in package.json - inherited
handlers only; the app's own `debug` is untouched.
@josephfarina
josephfarina force-pushed the integration-debug-handlers branch from 32f50be to a2c773f Compare September 4, 2026 19:14
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Sep 4, 2026
github-actions Bot added a commit that referenced this pull request Sep 4, 2026
@josephfarina
josephfarina merged commit 99282eb into main Sep 4, 2026
25 checks passed
@josephfarina
josephfarina deleted the integration-debug-handlers branch September 4, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant