-
Notifications
You must be signed in to change notification settings - Fork 1
fix(prompts): replace removed inquirer "list" prompt type with "select" #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright 2026 Archgate | ||
|
|
||
| // Custom oxlint JS plugin: validate `type:` literals passed to | ||
| // inquirer.prompt() against the prompt types registered by the installed | ||
| // inquirer version. | ||
| // | ||
| // Why this exists: inquirer v14 removed the legacy "list" prompt type | ||
| // (renamed "select" in v10), which crashed `archgate login` at runtime with | ||
| // 'Prompt type "list" is not registered'. Nothing else can catch this class | ||
| // of bug statically or in CI: | ||
| // - tsc cannot: inquirer's legacy prompt() types accept ANY `type: string` | ||
| // via the CustomQuestion escape hatch that exists for registerPrompt(). | ||
| // - tests cannot: interactive prompts need a TTY, so every test mocks the | ||
| // inquirer module entirely and the runtime prompt registry never runs. | ||
| // The invariant is purely syntactic, so it belongs in the linter. | ||
| // | ||
| // The registered set is read from the installed inquirer at plugin load, so | ||
| // the rule self-updates on dependency bumps — a future rename/removal makes | ||
| // stale call sites fail lint in the bump PR itself. | ||
| // | ||
| // The plugin runs natively as TypeScript under Bun, so there is no build step. | ||
|
|
||
| /** Minimal ESTree-ish node shape. The oxlint AST is ESLint-compatible. */ | ||
| type AstNode = { type: string } & Record<string, unknown>; | ||
|
|
||
| /** Narrow an unknown value to an AST node (an object with a string `type`). */ | ||
| function asNode(value: unknown): AstNode | undefined { | ||
| if ( | ||
| value !== null && | ||
| typeof value === "object" && | ||
| typeof (value as { type?: unknown }).type === "string" | ||
| ) { | ||
| return value as AstNode; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Prompt types registered by the INSTALLED inquirer, read at plugin load. | ||
| * | ||
| * Deliberately not a hardcoded allowlist: when a future inquirer version | ||
| * renames or removes a prompt type (as v10 did with "list" -> "select"), | ||
| * the registry shrinks and stale call sites fail `bun run lint` immediately | ||
| * in the dependency-bump PR — no manual list maintenance. | ||
| * | ||
| * If inquirer ever changes the registry's API shape, the loud throw below | ||
| * fails the whole lint run rather than silently disabling the rule. | ||
| */ | ||
| const { default: inquirer } = await import("inquirer"); | ||
| const REGISTERED_PROMPT_TYPES = new Set(Object.keys(inquirer.prompt.prompts)); | ||
| if (REGISTERED_PROMPT_TYPES.size === 0) { | ||
| throw new Error( | ||
| "archgate/valid-inquirer-prompt-type: inquirer.prompt.prompts is empty — the registry API may have changed; update .archgate/lint/oxlint.ts" | ||
| ); | ||
| } | ||
|
|
||
| /** True when the callee is exactly `inquirer.prompt`. */ | ||
| function isInquirerPromptCallee(callee: AstNode | undefined): boolean { | ||
| if (callee?.type !== "MemberExpression") return false; | ||
| const object = asNode(callee.object); | ||
| const property = asNode(callee.property); | ||
| return ( | ||
| object?.type === "Identifier" && | ||
| object.name === "inquirer" && | ||
| property?.type === "Identifier" && | ||
| property.name === "prompt" | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Collect the question ObjectExpressions from inquirer.prompt() arguments. | ||
| * Handles both the single-object form `prompt({...})` and the array form | ||
| * `prompt([{...}, {...}])`. | ||
| */ | ||
| function questionObjects(args: unknown): AstNode[] { | ||
| const result: AstNode[] = []; | ||
| const list = Array.isArray(args) ? args : []; | ||
| for (const arg of list) { | ||
| const node = asNode(arg); | ||
| if (node?.type === "ObjectExpression") { | ||
| result.push(node); | ||
| } else if (node?.type === "ArrayExpression") { | ||
| const elements = Array.isArray(node.elements) ? node.elements : []; | ||
| for (const element of elements) { | ||
| const child = asNode(element); | ||
| if (child?.type === "ObjectExpression") result.push(child); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** Find the value node of the `type` property in a question object, if any. */ | ||
| function typeValueNode(question: AstNode): AstNode | undefined { | ||
| const properties = Array.isArray(question.properties) | ||
| ? question.properties | ||
| : []; | ||
| for (const item of properties) { | ||
| const property = asNode(item); | ||
| if (property?.type !== "Property") continue; | ||
| const key = asNode(property.key); | ||
| const keyName = | ||
| key?.type === "Identifier" | ||
| ? key.name | ||
| : key?.type === "Literal" | ||
| ? key.value | ||
| : undefined; | ||
| if (keyName !== "type") continue; | ||
| return asNode(property.value); | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| interface ReportDescriptor { | ||
| node: AstNode; | ||
| message: string; | ||
| } | ||
|
|
||
| interface RuleContext { | ||
| report(descriptor: ReportDescriptor): void; | ||
| } | ||
|
|
||
| const validInquirerPromptType = { | ||
| create(context: RuleContext) { | ||
| return { | ||
| CallExpression(node: AstNode) { | ||
| if (!isInquirerPromptCallee(asNode(node.callee))) return; | ||
|
|
||
| for (const question of questionObjects(node.arguments)) { | ||
| const value = typeValueNode(question); | ||
| if (value?.type !== "Literal" || typeof value.value !== "string") | ||
| continue; | ||
| if (REGISTERED_PROMPT_TYPES.has(value.value)) continue; | ||
|
|
||
| context.report({ | ||
| node: value, | ||
| message: `Prompt type "${value.value}" is not registered in the installed inquirer and will crash at runtime. Registered types: ${[...REGISTERED_PROMPT_TYPES].join(", ")}. (The legacy "list" type was renamed "select" in inquirer v10.)`, | ||
| }); | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
| }; | ||
|
|
||
| const plugin = { | ||
| meta: { name: "archgate" }, | ||
| rules: { "valid-inquirer-prompt-type": validInquirerPromptType }, | ||
| }; | ||
|
|
||
| export default plugin; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
.claude/agent-memory/archgate-developer/feedback_prefer_tests_over_adr_rules.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| name: pick-the-right-enforcement-layer | ||
| description: Static syntactic invariants belong in a custom oxlint rule (.archgate/lint/oxlint.ts), not in ADR .rules.ts and not in tests | ||
| metadata: | ||
| type: feedback | ||
| --- | ||
|
|
||
| Pick the enforcement layer by the nature of the invariant — don't default to ADR rules. | ||
|
|
||
| **Why:** After the inquirer v14 `"list"` → `"select"` crash, I first drafted an ARCH-019 `.rules.ts` allowlist rule — user rejected it ("making an adr rule is stupid. we should make a proper test"). I then wrote a bun test scanning source files — user rejected that too ("if we have no tty, then this is more a linting rule than proper testing"). The final shape is a custom oxlint JS plugin rule (`archgate/valid-inquirer-prompt-type` in `.archgate/lint/oxlint.ts`), which gets real AST access instead of line-scanning and runs in the existing `bun run lint` gate. | ||
|
|
||
| **How to apply:** | ||
|
|
||
| - **Tests** verify _behavior_ — if the check can't actually execute the code path (e.g. interactive prompts with no TTY in CI), it is not a test, no matter where the file lives. | ||
| - **Custom oxlint rules** (`.archgate/lint/oxlint.ts`, registered in `.oxlintrc.json` `jsPlugins`, enabled as `archgate/<rule>`) enforce _static syntactic invariants_ — pattern X in source must/must-not look like Y. AST-based, precise spans, IDE-visible. | ||
| - **ADR `.rules.ts`** are for _project-structure/governance checks_ that don't fit a per-file lint model (cross-file sync, docs parity, dependency policy). | ||
| - `.archgate/lint/` is the archgate-standard home for ADR-complementing linter rules (see its README); the plugin file is NOT in tsconfig `include` or knip `project`, but oxlint lints it, so it must itself pass all oxlint rules. | ||
| - **oxlint jsPlugins have full module resolution and top-level await** — a plugin can `await import("inquirer")` at load and derive its allowlist from the installed dependency's runtime state (e.g. `Object.keys(inquirer.prompt.prompts)`). Prefer this over hardcoded allowlists: the rule then self-updates on dependency bumps and stale call sites fail lint in the bump PR itself. Guard with a loud throw if the upstream API shape changes, so the rule can never silently disable itself. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.