feat(codegen): add CLI command generator with agent-ready docs from GraphQL schema#725
feat(codegen): add CLI command generator with agent-ready docs from GraphQL schema#725pyramation wants to merge 4 commits intomainfrom
Conversation
- Add cli config option (CliConfig | boolean) to GraphQLSDKConfigTarget - Add komoji dependency for toKebabCase casing - Create CLI generator orchestrator (cli/index.ts) - Create arg-mapper: converts CleanTypeRef to inquirerer Question[] - Create infra-generator: context + auth commands via Babel AST - Create executor-generator: ORM client init with appstash credentials - Create table-command-generator: per-table CRUD using ORM methods - Create custom-command-generator: per-operation using ORM methods - Create command-map-generator: command registry (Record<string, Function>) - Wire CLI generation into generate.ts alongside ORM/React Query - Update root barrel to include CLI exports Architecture: CLI -> ORM -> GraphQL (not CLI -> raw GraphQL) CLI commands: prompt for args -> call ORM method -> print JSON result Uses appstash@0.4.0 config-store for context/credential management
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
graphql/codegen/src/core/generate.ts
Outdated
| const runOrm = | ||
| runReactQuery || (options.orm !== undefined ? !!options.orm : false); | ||
| const runCli = !!config.cli; |
There was a problem hiding this comment.
🔴 CLI generator enabled without ORM produces broken import { createClient } from '../orm'
The generated cli/executor.ts always emits import { createClient } from '../orm' (executor-generator.ts:34). However, in generate.ts:53-55, runCli can be true while runOrm is false — a user can set { cli: true } without { orm: true }. When ORM generation is skipped, no orm/ directory is created, so the generated CLI code has a broken import that will fail at compile/runtime.
Root Cause
In graphql/codegen/src/core/generate.ts:53-55:
const runOrm =
runReactQuery || (options.orm !== undefined ? !!options.orm : false);
const runCli = !!config.cli;Unlike runReactQuery which forces runOrm = true, runCli does not force ORM generation. The CLI's executor (executor-generator.ts:34) unconditionally generates import { createClient } from '../orm', assuming the ORM output exists.
Impact: Any user enabling only CLI generation (e.g., { cli: true, orm: false }) will get generated code that cannot compile or run due to the missing ../orm module.
| const runOrm = | |
| runReactQuery || (options.orm !== undefined ? !!options.orm : false); | |
| const runCli = !!config.cli; | |
| const runOrm = | |
| runReactQuery || runCli || (options.orm !== undefined ? !!options.orm : false); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| t.callExpression( | ||
| t.memberExpression( | ||
| t.callExpression( | ||
| t.memberExpression( | ||
| t.identifier('String'), | ||
| t.identifier('call'), | ||
| ), | ||
| [ | ||
| t.logicalExpression( | ||
| '||', | ||
| t.identifier('tokenValue'), | ||
| t.stringLiteral(''), | ||
| ), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
🔴 String.call(value) in generated set-token handler always produces empty string
The generated handleSetToken function builds AST that produces String.call(tokenValue || "").trim(). This always evaluates to "", silently discarding the user's token input.
Root Cause
At infra-generator.ts:1647-1658, the AST constructs:
t.callExpression(
t.memberExpression(t.identifier('String'), t.identifier('call')),
[t.logicalExpression('||', t.identifier('tokenValue'), t.stringLiteral(''))]
)This generates String.call(tokenValue || ""). In JavaScript, String.call(x) invokes the String function with this = x and zero arguments. String() with no arguments returns "". So the token value is always lost.
The intended code was String(tokenValue || "").trim(), which would be:
t.callExpression(t.identifier('String'), [t.logicalExpression(...)])Impact: The generated CLI's auth set-token command will always save an empty token, making authentication silently broken.
| t.callExpression( | |
| t.memberExpression( | |
| t.callExpression( | |
| t.memberExpression( | |
| t.identifier('String'), | |
| t.identifier('call'), | |
| ), | |
| [ | |
| t.logicalExpression( | |
| '||', | |
| t.identifier('tokenValue'), | |
| t.stringLiteral(''), | |
| ), | |
| ], | |
| ), | |
| t.callExpression( | |
| t.memberExpression( | |
| t.callExpression( | |
| t.identifier('String'), | |
| [ | |
| t.logicalExpression( | |
| '||', | |
| t.identifier('tokenValue'), | |
| t.stringLiteral(''), | |
| ), | |
| ], | |
| ), | |
| t.identifier('trim'), | |
| ), | |
| [], | |
| ), |
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (hasCli) { | ||
| statements.push(exportAllFrom('./cli')); | ||
| } |
There was a problem hiding this comment.
🔴 No cli/index.ts barrel generated, causing root barrel export * from './cli' to fail
The root barrel (barrel.ts:218-220) generates export * from './cli' when hasCli is true, but the CLI generator never produces a cli/index.ts file. Module resolution will fail because there is no entry point for the ./cli directory.
Root Cause
Looking at the files generated by generateCli in cli/index.ts:40-69, the output files are:
executor.tscommands/context.tscommands/auth.tscommands/<table>.ts(per table)commands/<custom-op>.ts(per custom op)commands.ts
No index.ts barrel is generated for the cli/ directory. Compare with the ORM generator, which produces orm/index.ts via client-generator.ts:338.
When the root barrel emits export * from './cli', TypeScript/Node will try to resolve cli/index.ts (or cli/index.js), which doesn't exist.
Impact: The root index.ts barrel will fail to compile due to the unresolvable ./cli re-export.
Prompt for agents
The CLI generator needs to produce a cli/index.ts barrel file, similar to how the ORM generator produces orm/index.ts (see graphql/codegen/src/core/codegen/orm/client-generator.ts:338). Add a barrel file generation step in graphql/codegen/src/core/codegen/cli/index.ts inside the generateCli function (around line 69) that creates an index.ts file re-exporting from the appropriate CLI modules (e.g., commands.ts, executor.ts). This file should be added to the files array before the function returns.
Was this helpful? React with 👍 or 👎 to provide feedback.
- Add cli-generator.test.ts with 13 tests covering all generated files - Snapshot executor, context, auth, table commands, custom commands, command map - Verify ORM method calls in table and custom commands - Verify appstash config-store usage in executor - Fix: runOrm now includes runCli so ORM is auto-enabled when CLI is enabled
- Add docs-generator.ts that produces README.md (overview, setup, commands) and COMMANDS.md (man-page style reference with synopsis, options, examples) - Wire into CLI orchestrator so docs are always generated alongside commands - Update snapshot tests: 15 tests, 10 snapshots covering all generated files
…CP, Skills)
- Add DocsConfig interface: { readme, agents, mcp, skills } with boolean flags
- Defaults: readme + agents on, mcp + skills opt-in. docs: true enables all.
- generateAgentsDocs(): structured AGENTS.md for LLM consumption with TOOL
sections, INPUT/OUTPUT schemas, WORKFLOWS, and ERROR HANDLING
- generateMcpConfig(): mcp.json with typed inputSchema per command (JSON Schema)
- generateSkills(): per-command .md skill files for agent systems
- Wire into cli/index.ts orchestrator with config-driven conditional generation
- 22 tests, 17 snapshots covering all formats and config combinations
feat(codegen): add CLI command generator with agent-ready docs from GraphQL schema
Summary
Adds a new
cligenerator target to@constructive-io/graphql-codegen, alongside the existingormandreactQuerygenerators. When enabled (cli: trueorcli: { toolName: 'myapp' }), the codegen emits a full set ofinquirerer-based CLI command files that use the generated ORM client internally — not raw GraphQL.Architecture: CLI → ORM → GraphQL (mirrors how React hooks use the ORM)
Generated files:
cli/executor.ts— initializes ORM client usingappstashconfig-store for endpoint + authcli/commands/context.ts— kubectl-style context management (create/list/use/current/delete)cli/commands/auth.ts— token management per context (set-token/status/logout)cli/commands/{table}.ts— per-table CRUD (list/get/create/update/delete via ORM methods)cli/commands/{operation}.ts— per-custom-operation commands (queries + mutations via ORM)cli/commands.ts— command map registry (Record<string, Function>)cli/README.md— human-readable CLI overview, setup, and command referencecli/AGENTS.md— structured markdown for LLM/agent consumption (TOOL sections, INPUT/OUTPUT schemas, WORKFLOWS, ERROR HANDLING)cli/mcp.json— MCP tool definitions with typedinputSchema(JSON Schema) per commandcli/skills/*.md— per-command skill files for agent systems (Devin, etc.)Docs generation is configurable via
DocsConfig:docs: trueenables all 4 formats;docs: falsedisables allreadme+agentsalways on,mcp+skillsopt-inNew files:
cli/index.ts— orchestratorcli/arg-mapper.ts— convertsCleanTypeRef→inquirererQuestion[]cli/infra-generator.ts— generates context + auth commands (2055 lines of Babel AST)cli/executor-generator.ts— generates ORM client initializationcli/table-command-generator.ts— generates per-table CRUD commandscli/custom-command-generator.ts— generates per-custom-operation commandscli/command-map-generator.ts— generates command registrycli/docs-generator.ts— generates README, AGENTS.md, mcp.json, and skills filesDependencies:
komojifortoKebabCasecasinginflekt(already present) for GraphQL casing (ucFirst,lcFirst, etc.)appstash@0.4.0config-store (merged in dev-utils#63)All code generation uses Babel AST (
@babel/types+generateCode()) — no string concatenation. Docs generation uses string concatenation (intentional for markdown/JSON output).Updates since last revision
runOrmnow auto-enables whenrunCliis true, so{ cli: true }without explicit{ orm: true }no longer produces brokenimport { createClient } from '../orm'(caught by Devin review)cli-generator.test.tswith 13 tests and 8 snapshots covering all generated file types (executor, context, auth, table commands, custom commands, command map). Tests exercise a 2-table + 2-custom-operation example schema.DocsConfiginterface with 4 doc formats (README, AGENTS.md, MCP, Skills). Defaults: README + AGENTS.md always on, MCP + Skills opt-in. 22 tests, 17 snapshots covering all doc formats and config combinations.Review & Testing Checklist for Human
cli: trueand verify the generated CLI code compiles (TypeScript + ESLint). This is the most important validation step — snapshots only prove the generator is deterministic, not that its output is correct.client.{table}.findMany({...}).execute(),client.query.{op}({...}).execute()) match the actual ORM client API shape. The table-command-generator calls methods likeclient.car.findMany(...)but the ORM may use a different accessor pattern.{ name, endpoint }for context commands) that may not match actual runtime behavior. Verify these against real CLI execution.mcp.jsonstructure against the MCP spec. The_meta.fieldson{tool}_fieldstools is non-standard and may need adjustment.String.call(tokenValue || "")in auth.ts snapshot line 117 — this looks like a bug; should beString(tokenValue || "")without.call).buildInputObjectQuestiononly uses the first field of an INPUT_OBJECT, which may be incomplete for nested input types. Verify this works for real schemas with complex input types.requiredfield logic — table-command-generator marks ALL fields asrequired: truefor create operations (line 338). Check if this is correct for nullable fields in your schema.Notes
cligenerator is opt-in via config — existing codegen behavior unchangedgqlTypeToJsonSchemaTypemapping is simplistic (UUID → string, Datetime → string) but reasonable for CLI argsLink to Devin run: https://app.devin.ai/sessions/e9c668834d394cdc8ebcb371b6ebf430
Requested by: @pyramation