Skip to content

Commit 2d35daa

Browse files
authored
Merge pull request #261 from PaulJPhilp/codex/ep-admin-help-discoverability
ep-admin DX: compact root help, actionable error endings, and next-step nudges
2 parents 8ea1a68 + 76061ed commit 2d35daa

5 files changed

Lines changed: 121 additions & 6 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe.sequential("CLI auth and DX contract", () => {
5959
const typo = runCli(["opz"], env);
6060
expect(typo.status).toBe(1);
6161
expect(typo.stderr).toContain("Did you mean: ep-admin ops");
62+
expect(typo.stderr).toContain("Run: ep-admin --help");
6263
expect(typo.stderr).not.toContain("auth has not been initialized");
6364
} finally {
6465
await rm(tempDir, { recursive: true, force: true });
@@ -71,6 +72,9 @@ describe.sequential("CLI auth and DX contract", () => {
7172
const help = runCli(["--help"], env);
7273
expect(help.status).toBe(0);
7374
expect(help.stdout).toContain("ep-admin");
75+
expect(help.stdout).toContain("Top-level commands:");
76+
expect(help.stdout).not.toContain("pattern pattern");
77+
expect(help.stdout).not.toContain("data data");
7478

7579
const version = runCli(["--version"], env);
7680
expect(version.status).toBe(0);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,13 @@ describe.sequential("CLI JSON machine contract", () => {
111111
expect(dbFailure.status).toBe(1);
112112
expect(dbFailure.stdout.trim()).toBe("");
113113
expect(dbFailure.stderr.trim().length).toBeGreaterThan(0);
114+
expect(dbFailure.stderr).toContain("Run: ep-admin db --help");
114115

115116
const ingestFailure = runCli(["data", "ingest", "status", "--json"], automationEnv);
116117
expect(ingestFailure.status).toBe(1);
117118
expect(ingestFailure.stdout.trim()).toBe("");
118119
expect(ingestFailure.stderr.trim().length).toBeGreaterThan(0);
120+
expect(ingestFailure.stderr).toContain("Run: ep-admin data --help");
119121
} finally {
120122
await rm(tempDir, { recursive: true, force: true });
121123
}

packages/ep-admin/src/index.ts

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,63 @@ const NESTED_COMMANDS: Record<string, readonly string[]> = {
198198
release: ["preview", "create"],
199199
};
200200

201+
const ROOT_HELP_COMMANDS: ReadonlyArray<{
202+
readonly name: string;
203+
readonly description: string;
204+
}> = [
205+
{ name: "auth", description: "Initialize/login/logout and check admin session state" },
206+
{ name: "publish", description: "Validate, test, publish, and generate release artifacts" },
207+
{ name: "pattern", description: "Search patterns and generate skill assets" },
208+
{ name: "data", description: "Run ingest, Discord, and QA workflows" },
209+
{ name: "db", description: "Inspect, test, and migrate database state" },
210+
{ name: "dev", description: "Run development utilities and autofix tools" },
211+
{ name: "ops", description: "Run health checks and maintenance commands" },
212+
{ name: "config", description: "Install rules/skills and manage entities" },
213+
{ name: "system", description: "Generate/install shell completions" },
214+
{ name: "release", description: "Preview and create tagged releases" },
215+
];
216+
217+
const renderCompactRootHelp = (): string => {
218+
const lines = [
219+
`${CLI.RUNNER_NAME} ${CLI.VERSION}`,
220+
"",
221+
CLI.DESCRIPTION,
222+
"",
223+
"USAGE",
224+
" ep-admin <command> [subcommand] [options]",
225+
"",
226+
"Top-level commands:",
227+
...ROOT_HELP_COMMANDS.map(
228+
(command) => ` ${command.name.padEnd(10)} ${command.description}`
229+
),
230+
"",
231+
"Get detailed help:",
232+
" ep-admin <command> --help",
233+
"",
234+
"Quick start:",
235+
" ep-admin auth init",
236+
" ep-admin auth login",
237+
" ep-admin ops health-check",
238+
" ep-admin publish pipeline",
239+
"",
240+
`Docs: ${EP_ADMIN_DOCS_URL}`,
241+
];
242+
return lines.join("\n");
243+
};
244+
245+
const isRootHelpRequest = (argv: ReadonlyArray<string>): boolean => {
246+
const args = argv.slice(2);
247+
if (args.length === 0) return true;
248+
249+
const hasHelpFlag = args.includes("--help") || args.includes("-h");
250+
if (!hasHelpFlag) return false;
251+
252+
const hasPositional = args.some(
253+
(token) => token.length > 0 && !token.startsWith("-")
254+
);
255+
return !hasPositional;
256+
};
257+
201258
const normalizeArgsForSuggestion = (argv: ReadonlyArray<string>): string[] =>
202259
argv.slice(2).filter((token) => token.length > 0 && !token.startsWith("-"));
203260

@@ -280,6 +337,34 @@ const getPreAuthCommandMismatchMessage = (argv: ReadonlyArray<string>): string |
280337
return null;
281338
};
282339

340+
const getContextualHelpCommand = (argv: ReadonlyArray<string>): string => {
341+
const args = normalizeArgsForSuggestion(argv);
342+
const first = args[0];
343+
if (first && ROOT_COMMANDS.includes(first as (typeof ROOT_COMMANDS)[number])) {
344+
return `ep-admin ${first} --help`;
345+
}
346+
return "ep-admin --help";
347+
};
348+
349+
const appendStandardErrorGuidance = (
350+
message: string,
351+
argv: ReadonlyArray<string>
352+
): string => {
353+
const trimmed = message.trim();
354+
const lines = trimmed.length > 0 ? [trimmed] : [];
355+
const hasRunLine = /\bRun:\s*ep-admin\b/i.test(trimmed);
356+
const hasDocsLine = /\bDocs:\s*https?:\/\//i.test(trimmed);
357+
358+
if (!hasRunLine) {
359+
lines.push(`Run: ${getContextualHelpCommand(argv)}`);
360+
}
361+
if (!hasDocsLine) {
362+
lines.push(`Docs: ${EP_ADMIN_DOCS_URL}`);
363+
}
364+
365+
return lines.join("\n");
366+
};
367+
283368
const isAuthExemptArgv = (argv: ReadonlyArray<string>): boolean => {
284369
const args = argv.slice(2);
285370
if (args.length === 0) return true;
@@ -478,24 +563,26 @@ const extractErrorMessage = (error: unknown, argv: ReadonlyArray<string>): strin
478563
}
479564
}
480565

481-
if (typeof resolvedError === "string") return resolvedError;
566+
if (typeof resolvedError === "string") {
567+
return appendStandardErrorGuidance(resolvedError, argv);
568+
}
482569

483570
if (resolvedError instanceof Error) {
484571
const combined = [resolvedError.message, resolvedError.stack].filter(Boolean).join("\n");
485572
if (combined.includes("CommandMismatch")) {
486573
const suggestion = getCommandSuggestion(argv);
487-
return [
574+
return appendStandardErrorGuidance(
488575
suggestion ? suggestion : "Need command help? Run 'ep-admin --help'.",
489-
`Docs: ${EP_ADMIN_DOCS_URL}`,
490-
].join("\n");
576+
argv
577+
);
491578
}
492579

493580
if (resolvedError.message.trim()) {
494-
return `${resolvedError.message.trim()}\nDocs: ${EP_ADMIN_DOCS_URL}`;
581+
return appendStandardErrorGuidance(resolvedError.message, argv);
495582
}
496583
}
497584

498-
return String(resolvedError);
585+
return appendStandardErrorGuidance(String(resolvedError), argv);
499586
};
500587

501588
export const createAdminProgram = (
@@ -506,6 +593,11 @@ export const runCli = (
506593
argv: ReadonlyArray<string> = process.argv
507594
): Effect.Effect<void, unknown, never> =>
508595
Effect.gen(function* () {
596+
if (isRootHelpRequest(argv)) {
597+
yield* Console.log(renderCompactRootHelp());
598+
return;
599+
}
600+
509601
const prepared = normalizeLegacyArgs(argv);
510602
for (const warning of prepared.warnings) {
511603
yield* Console.error(`⚠ ${warning}`);

packages/ep-admin/src/ingest-commands.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export const ingestProcessCommand = Command.make("process", {
9393
}
9494

9595
yield* Display.showSuccess(MESSAGES.SUCCESS.PATTERNS_PROCESSED);
96+
if (summary.failed === 0) {
97+
yield* Display.showInfo("Next: ep-admin data ingest validate");
98+
}
9699
})
97100
)
98101
);
@@ -142,6 +145,7 @@ export const ingestProcessOneCommand = Command.make("process-one", {
142145
yield* Display.showSuccess(
143146
`Pattern ${positional.patternFile} processed! ID: ${result.id}`
144147
);
148+
yield* Display.showInfo("Next: ep-admin data ingest validate");
145149
})
146150
)
147151
);
@@ -202,6 +206,9 @@ export const ingestValidateCommand = Command.make("validate", {
202206
}
203207

204208
yield* Display.showSuccess(MESSAGES.SUCCESS.INGEST_VALIDATION_COMPLETE);
209+
if (invalid === 0) {
210+
yield* Display.showInfo("Next: ep-admin data ingest test");
211+
}
205212
})
206213
)
207214
);
@@ -256,6 +263,7 @@ export const ingestTestCommand = Command.make("test", {
256263

257264
if (failed === 0) {
258265
yield* Display.showSuccess(MESSAGES.SUCCESS.INGEST_TESTS_PASSED);
266+
yield* Display.showInfo("Next: ep-admin data ingest pipeline");
259267
} else {
260268
for (const r of tested.filter((r) => r.valid && !r.testPassed)) {
261269
yield* Display.showError(` - ${r.pattern.id}: test failed`);
@@ -408,6 +416,9 @@ export const ingestPipelineCommand = Command.make("pipeline", {
408416
}
409417

410418
yield* Display.showSuccess(MESSAGES.SUCCESS.INGEST_PIPELINE_COMPLETED);
419+
if (report.failed === 0) {
420+
yield* Display.showInfo("Next: ep-admin publish validate");
421+
}
411422
})
412423
)
413424
);

packages/ep-admin/src/publish-commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export const publishValidateCommand = Command.make("validate", {
131131
}
132132

133133
yield* Display.showSuccess(MESSAGES.SUCCESS.PATTERNS_VALIDATED);
134+
yield* Display.showInfo("Next: ep-admin publish test");
134135
})
135136
)
136137
);
@@ -183,6 +184,7 @@ export const publishTestCommand = Command.make("test", {
183184
}
184185

185186
yield* Display.showSuccess(MESSAGES.SUCCESS.ALL_EXAMPLES_PASSED);
187+
yield* Display.showInfo("Next: ep-admin publish run");
186188
})
187189
)
188190
);
@@ -241,6 +243,7 @@ export const publishRunCommand = Command.make("run", {
241243
}
242244

243245
yield* Display.showSuccess(MESSAGES.SUCCESS.PATTERNS_PUBLISHED);
246+
yield* Display.showInfo("Next: ep-admin publish generate --readme");
244247
})
245248
)
246249
);
@@ -300,6 +303,7 @@ export const publishGenerateCommand = Command.make("generate", {
300303
}
301304

302305
yield* Display.showSuccess(MESSAGES.SUCCESS.DOCUMENTATION_GENERATED);
306+
yield* Display.showInfo("Next: ep-admin release preview");
303307
})
304308
)
305309
);
@@ -355,6 +359,7 @@ export const publishLintCommand = Command.make("lint", {
355359
}
356360

357361
yield* Display.showSuccess(MESSAGES.SUCCESS.LINTING_COMPLETE);
362+
yield* Display.showInfo("Next: ep-admin publish pipeline");
358363
})
359364
)
360365
);
@@ -418,6 +423,7 @@ export const publishPipelineCommand = Command.make("pipeline", {
418423
yield* Display.showInfo(`Duration: ${result.duration}ms`);
419424

420425
yield* Display.showSuccess(MESSAGES.SUCCESS.PIPELINE_COMPLETED);
426+
yield* Display.showInfo("Next: ep-admin release preview");
421427
})
422428
)
423429
);

0 commit comments

Comments
 (0)