Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli-engine/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export function createCli(spec: {
Record<string, { readonly brief: string; readonly description?: string }>
>;
readonly commands: MountedTree;
/** Words for the root help card; the engine formats. */
readonly help?: {
readonly tagline?: string;
readonly description?: string;
readonly examples?: readonly string[];
readonly docsUrl?: string;
};
/**
* Declaring this, together with a `Runtime.spawnTelemetry` seam,
* turns telemetry on: the engine reads the user's preference,
Expand Down
28 changes: 27 additions & 1 deletion packages/cli-engine/src/execution/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,32 @@ export function buildManagementApiClient(
});
}

/**
* Loads the SDK, surviving a Node ESM resolver fault observed in the
* wild: the resolver can return the package's un-realpathed pnpm
* symlink URL (instead of the .pnpm real path), from which the SDK's
* own 'openapi-fetch' import cannot resolve. The fault is
* state-dependent — two fstatSync calls at process start reliably
* provoke it, and a module-loader hook masks it — so nothing here can
* rule it out. The fallback resolves through CJS require, which
* realpaths, and imports the real location directly; it never fires
* when the normal import works.
*/
async function importManagementApiSdk(): Promise<
typeof import("@prisma/management-api-sdk")
> {
try {
return await import("@prisma/management-api-sdk");
} catch {
const { createRequire } = await import("node:module");
const { pathToFileURL } = await import("node:url");
const real = createRequire(import.meta.url).resolve(
"@prisma/management-api-sdk",
);
return await import(pathToFileURL(real).href);
}
}

async function constructClient(
invocation: Invocation,
debug: DebugLog,
Expand Down Expand Up @@ -118,7 +144,7 @@ async function constructClient(
debug,
probe,
);
const { createManagementApiSdk } = await import("@prisma/management-api-sdk");
const { createManagementApiSdk } = await importManagementApiSdk();
const sdk = createManagementApiSdk({
clientId: config.clientId,
redirectUri: config.redirectUri,
Expand Down
45 changes: 43 additions & 2 deletions packages/cli-engine/src/execution/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,17 @@ import {
buildCommandTree,
buildRedirectTable,
type CommandTreeEntry,
type CommandTreeNode,
matchFlagRedirect,
matchVerbRedirect,
type RedirectTable,
} from "./command-tree";
import {
bareGroupInvocation,
helpFlagGiven,
preParseColorEnabled,
renderHelp,
} from "./help";
import { checkNeeds, type NeedsOutcome } from "./needs";
import {
configFlagGivenNoValue,
Expand Down Expand Up @@ -83,6 +90,16 @@ export interface EngineSpec {
Record<string, { readonly brief: string; readonly description?: string }>
>;
readonly commands: MountedTree;
/** Words for the root help card; the engine formats. */
readonly help?: {
/** One line after the binary name: what this CLI is. */
readonly tagline?: string;
/** A sentence or two under the command list. */
readonly description?: string;
/** Same {bin} substitution rule as command examples. */
readonly examples?: readonly string[];
readonly docsUrl?: string;
};
/** Absent means this CLI reports nothing. */
readonly telemetry?: TelemetryDeclaration;
}
Expand Down Expand Up @@ -257,6 +274,7 @@ type ErasedServerHandler = (

export class EngineImpl implements Engine {
private readonly spec: EngineSpec;
private readonly tree: CommandTreeNode;
private readonly root: StricliRouteMap<EngineRunContext>;
private readonly redirects: RedirectTable;
private readonly now: () => Date;
Expand All @@ -272,9 +290,10 @@ export class EngineImpl implements Engine {
this.now = now;
this.delay = delay;
this.configSections = declaredConfigSections(spec);
this.tree = buildCommandTree(spec);
this.root = buildRoutes(
spec,
buildCommandTree(spec),
this.tree,
"",
(invocation, entry, flags, values) =>
this.executeMounted(invocation, entry, flags, values),
Expand All @@ -297,7 +316,10 @@ export class EngineImpl implements Engine {
yes: false,
confirmValues: [],
interactive: defaultInteractive(runtime),
colorEnabled: false,
/** Pre-parse resolution so a run that never mounts a command — an
* unknown command, a parse failure — still colours its
* diagnostics; applySharedFlags re-resolves after parsing. */
colorEnabled: preParseColorEnabled(argv, runtime, "stderr"),
configPath: undefined,
resolved: false,
settledExitCode: undefined,
Expand Down Expand Up @@ -353,6 +375,25 @@ export class EngineImpl implements Engine {
settleErrored(invocation, configFlagGivenNoValueError());
return 2;
}
if (helpFlagGiven(argv) || bareGroupInvocation(this.tree, argv)) {
unsubscribe();
/** Help prose follows stricli's channel rule: stdout in human
* mode, stderr in json mode so stdout stays a clean frame
* stream. Never fires telemetry, like --version. */
const stream = format === "human" ? runtime.stdout : runtime.stderr;
renderHelp(
this.spec,
this.tree,
argv,
preParseColorEnabled(
argv,
runtime,
format === "human" ? "stdout" : "stderr",
),
stream,
);
return 0;
}
const stricliProcess = {
/** stricli writes only help text here. In json mode stdout carries
* exactly the frame stream, so help prose goes to stderr instead. */
Expand Down
Loading
Loading