Skip to content

Commit 9814f54

Browse files
authored
Merge pull request #268 from PaulJPhilp/codex/ep-admin-help-usage-paths
fix(ep-admin): show full command paths in contextual help usage
2 parents 0f059fd + 63f5aae commit 9814f54

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

packages/ep-admin/src/__tests__/cli-auth-contract.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ describe.sequential("CLI auth and DX contract", () => {
109109
}
110110
});
111111

112+
it("renders contextual help usage with full command paths", async () => {
113+
const { tempDir, env } = await createAuthEnv();
114+
try {
115+
const releasePreviewHelp = runCli(["release", "preview", "--help"], env);
116+
expect(releasePreviewHelp.status).toBe(0);
117+
expect(releasePreviewHelp.stdout).toContain("$ ep-admin release preview");
118+
expect(releasePreviewHelp.stdout).not.toContain("\n$ preview\n");
119+
120+
const nestedHelp = runCli(
121+
["pattern", "skills", "generate-from-db", "--help"],
122+
env
123+
);
124+
expect(nestedHelp.status).toBe(0);
125+
expect(nestedHelp.stdout).toContain(
126+
"$ ep-admin pattern skills generate-from-db"
127+
);
128+
129+
const dbLeafHelp = runCli(["db", "show", "patterns", "--help"], env);
130+
expect(dbLeafHelp.status).toBe(0);
131+
expect(dbLeafHelp.stdout).toContain("$ ep-admin db show patterns");
132+
} finally {
133+
await rm(tempDir, { recursive: true, force: true });
134+
}
135+
});
136+
112137
it("allows protected commands after auth init/login", async () => {
113138
const { tempDir, env } = await createAuthEnv();
114139
try {

packages/ep-admin/src/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { Args, Command } from "@effect/cli";
1010
import { Effect } from "effect";
1111
import { Console } from "effect";
12+
import { spawnSync } from "node:child_process";
1213
import { dirname } from "node:path";
1314
import { fileURLToPath } from "node:url";
1415
import { authCommand } from "./auth-commands.js";
@@ -147,6 +148,7 @@ const adminCliRunner = Command.run(adminRootCommand, {
147148

148149
const EP_ADMIN_DOCS_URL =
149150
"https://github.com/PaulJPhilp/EffectPatterns/tree/main/EP-ADMIN-CLI-README.md";
151+
const HELP_RENDER_BYPASS_ENV = "EP_ADMIN_HELP_RENDER_BYPASS";
150152

151153
type PreparedArgv = {
152154
readonly argv: ReadonlyArray<string>;
@@ -357,6 +359,26 @@ const isRootHelpRequest = (argv: ReadonlyArray<string>): boolean => {
357359
const normalizeArgsForSuggestion = (argv: ReadonlyArray<string>): string[] =>
358360
argv.slice(2).filter((token) => token.length > 0 && !token.startsWith("-"));
359361

362+
const isNestedHelpRequest = (argv: ReadonlyArray<string>): boolean => {
363+
const args = argv.slice(2);
364+
const hasHelpFlag = args.includes("--help") || args.includes("-h");
365+
if (!hasHelpFlag) return false;
366+
return normalizeArgsForSuggestion(argv).length > 0;
367+
};
368+
369+
const rewriteHelpUsageWithFullPath = (
370+
helpOutput: string,
371+
argv: ReadonlyArray<string>
372+
): string => {
373+
const pathTokens = normalizeArgsForSuggestion(argv);
374+
if (pathTokens.length === 0) {
375+
return helpOutput;
376+
}
377+
378+
const fullPath = `ep-admin ${pathTokens.join(" ")}`;
379+
return helpOutput.replace(/^\$ \S+/m, `$ ${fullPath}`);
380+
};
381+
360382
const levenshtein = (a: string, b: string): number => {
361383
const dp: number[][] = Array.from({ length: a.length + 1 }, () =>
362384
Array.from({ length: b.length + 1 }, () => 0)
@@ -759,6 +781,54 @@ export const runCli = (
759781
return yield* Effect.fail(new Error(mismatch.message));
760782
}
761783

784+
if (
785+
isNestedHelpRequest(prepared.argv) &&
786+
process.env[HELP_RENDER_BYPASS_ENV] !== "1"
787+
) {
788+
const [runtime, ...childArgs] = prepared.argv;
789+
const helpOutput = yield* Effect.try({
790+
try: () =>
791+
spawnSync(runtime ?? "bun", childArgs, {
792+
encoding: "utf8",
793+
env: {
794+
...process.env,
795+
[HELP_RENDER_BYPASS_ENV]: "1",
796+
},
797+
}),
798+
catch: (error) =>
799+
new Error(
800+
`Failed to render contextual help: ${
801+
error instanceof Error ? error.message : String(error)
802+
}`
803+
),
804+
});
805+
806+
const rewrittenStdout = rewriteHelpUsageWithFullPath(
807+
helpOutput.stdout ?? "",
808+
prepared.argv
809+
);
810+
811+
yield* Effect.sync(() => {
812+
if (rewrittenStdout.length > 0) {
813+
process.stdout.write(rewrittenStdout);
814+
}
815+
if ((helpOutput.stderr ?? "").length > 0) {
816+
process.stderr.write(helpOutput.stderr ?? "");
817+
}
818+
});
819+
820+
if (helpOutput.status !== 0) {
821+
return yield* Effect.fail(
822+
new Error(
823+
`Contextual help command failed with exit code ${
824+
helpOutput.status ?? "unknown"
825+
}.`
826+
)
827+
);
828+
}
829+
return;
830+
}
831+
762832
// Validate env before runtime operations.
763833
yield* validateEnvironment;
764834

0 commit comments

Comments
 (0)