fix(system_prompts): resolve nested local command groups in CLI docs generator

The CLI docs generator anchored on the first `new Command()` in a file and
never resolved locally-defined command groups passed as
`.command("name", localCmd)`. For datatable this flattened the nested
`migrate` group: it emitted `datatable new/up/down` plus a bare
`datatable migrate`, and mislabeled the datatable command with the migrate
group's description. jobs was broken the same way (its description was pull's,
and pull/push rendered empty).

Anchor block extraction on the `export default`ed command, recurse into
locally-defined `const x = new Command()` groups mounted as subcommands, and
render nested sub-subcommands. Regenerated docs now show
`datatable migrate new/up/down` and `jobs pull/push` with their real
options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-07-04 00:11:34 +02:00
parent 12b9811e3c
commit 1555ea0df9
5 changed files with 144 additions and 82 deletions
+17 -18
View File
@@ -6566,22 +6566,22 @@ Show all available wmill.yaml configuration options
### datatable
manage datatable migrations
datatable related commands
**Subcommands:**
- \`datatable new <name:string>\` - scaffold a new migration (.up.sql / .down.sql files)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable up\` - apply all pending migrations to every datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable down\` - datatable related commands
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable list\` - list all datatables in the workspace
- \`--json\` - Output as JSON (for piping to jq)
- \`datatable run <sql:string>\` - run a SQL query on a datatable
- \`-n --name <name:string>\` - Datatable name (default: main)
- \`-s --silent\` - Output only the final result as JSON. Useful for scripting.
- \`datatable migrate\`
- \`datatable migrate\` - manage datatable migrations
- \`datatable migrate new <name:string>\` - scaffold a new migration (.up.sql / .down.sql files)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable migrate up\` - apply all pending migrations to every datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable migrate down\` - roll back the most recent migration on every datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable create [name:string]\` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- \`--resource <resource:string>\` - Back the datatable with an existing postgresql resource path instead of the instance database
- \`--force\` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
@@ -6850,19 +6850,18 @@ Manage jobs (list, inspect, cancel)
### jobs
Pull completed and queued jobs from workspace
**Arguments:** \`[workspace:string]\`
**Options:**
- \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
- \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
- \`--skip-worker-check\` - Skip checking for active workers before export
Manage jobs (import/export)
**Subcommands:**
- \`jobs pull\`
- \`jobs push\`
- \`jobs pull [workspace:string]\` - Pull completed and queued jobs from workspace
- \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
- \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
- \`--skip-worker-check\` - Skip checking for active workers before export
- \`jobs push [workspace:string]\` - Push completed and queued jobs to workspace
- \`-c, --completed-file <file:string>\` - Completed jobs input file (default: completed_jobs.json)
- \`-q, --queued-file <file:string>\` - Queued jobs input file (default: queued_jobs.json)
- \`--skip-worker-check\` - Skip checking for active workers before import
### lint
@@ -68,22 +68,22 @@ Show all available wmill.yaml configuration options
### datatable
manage datatable migrations
datatable related commands
**Subcommands:**
- `datatable new <name:string>` - scaffold a new migration (.up.sql / .down.sql files)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable up` - apply all pending migrations to every datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable down` - datatable related commands
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable list` - list all datatables in the workspace
- `--json` - Output as JSON (for piping to jq)
- `datatable run <sql:string>` - run a SQL query on a datatable
- `-n --name <name:string>` - Datatable name (default: main)
- `-s --silent` - Output only the final result as JSON. Useful for scripting.
- `datatable migrate`
- `datatable migrate` - manage datatable migrations
- `datatable migrate new <name:string>` - scaffold a new migration (.up.sql / .down.sql files)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable migrate up` - apply all pending migrations to every datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable migrate down` - roll back the most recent migration on every datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable create [name:string]` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- `--resource <resource:string>` - Back the datatable with an existing postgresql resource path instead of the instance database
- `--force` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
@@ -352,19 +352,18 @@ Manage jobs (list, inspect, cancel)
### jobs
Pull completed and queued jobs from workspace
**Arguments:** `[workspace:string]`
**Options:**
- `-c, --completed-output <file:string>` - Completed jobs output file (default: completed_jobs.json)
- `-q, --queued-output <file:string>` - Queued jobs output file (default: queued_jobs.json)
- `--skip-worker-check` - Skip checking for active workers before export
Manage jobs (import/export)
**Subcommands:**
- `jobs pull`
- `jobs push`
- `jobs pull [workspace:string]` - Pull completed and queued jobs from workspace
- `-c, --completed-output <file:string>` - Completed jobs output file (default: completed_jobs.json)
- `-q, --queued-output <file:string>` - Queued jobs output file (default: queued_jobs.json)
- `--skip-worker-check` - Skip checking for active workers before export
- `jobs push [workspace:string]` - Push completed and queued jobs to workspace
- `-c, --completed-file <file:string>` - Completed jobs input file (default: completed_jobs.json)
- `-q, --queued-file <file:string>` - Queued jobs input file (default: queued_jobs.json)
- `--skip-worker-check` - Skip checking for active workers before import
### lint
+17 -18
View File
@@ -2736,22 +2736,22 @@ Show all available wmill.yaml configuration options
### datatable
manage datatable migrations
datatable related commands
**Subcommands:**
- \`datatable new <name:string>\` - scaffold a new migration (.up.sql / .down.sql files)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable up\` - apply all pending migrations to every datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable down\` - datatable related commands
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable list\` - list all datatables in the workspace
- \`--json\` - Output as JSON (for piping to jq)
- \`datatable run <sql:string>\` - run a SQL query on a datatable
- \`-n --name <name:string>\` - Datatable name (default: main)
- \`-s --silent\` - Output only the final result as JSON. Useful for scripting.
- \`datatable migrate\`
- \`datatable migrate\` - manage datatable migrations
- \`datatable migrate new <name:string>\` - scaffold a new migration (.up.sql / .down.sql files)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable migrate up\` - apply all pending migrations to every datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable migrate down\` - roll back the most recent migration on every datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target a specific datatable (default: all datatables in the workspace)
- \`datatable create [name:string]\` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- \`--resource <resource:string>\` - Back the datatable with an existing postgresql resource path instead of the instance database
- \`--force\` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
@@ -3020,19 +3020,18 @@ Manage jobs (list, inspect, cancel)
### jobs
Pull completed and queued jobs from workspace
**Arguments:** \`[workspace:string]\`
**Options:**
- \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
- \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
- \`--skip-worker-check\` - Skip checking for active workers before export
Manage jobs (import/export)
**Subcommands:**
- \`jobs pull\`
- \`jobs push\`
- \`jobs pull [workspace:string]\` - Pull completed and queued jobs from workspace
- \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
- \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
- \`--skip-worker-check\` - Skip checking for active workers before export
- \`jobs push [workspace:string]\` - Push completed and queued jobs to workspace
- \`-c, --completed-file <file:string>\` - Completed jobs input file (default: completed_jobs.json)
- \`-q, --queued-file <file:string>\` - Queued jobs input file (default: queued_jobs.json)
- \`--skip-worker-check\` - Skip checking for active workers before import
### lint
@@ -73,22 +73,22 @@ Show all available wmill.yaml configuration options
### datatable
manage datatable migrations
datatable related commands
**Subcommands:**
- `datatable new <name:string>` - scaffold a new migration (.up.sql / .down.sql files)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable up` - apply all pending migrations to every datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable down` - datatable related commands
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable list` - list all datatables in the workspace
- `--json` - Output as JSON (for piping to jq)
- `datatable run <sql:string>` - run a SQL query on a datatable
- `-n --name <name:string>` - Datatable name (default: main)
- `-s --silent` - Output only the final result as JSON. Useful for scripting.
- `datatable migrate`
- `datatable migrate` - manage datatable migrations
- `datatable migrate new <name:string>` - scaffold a new migration (.up.sql / .down.sql files)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable migrate up` - apply all pending migrations to every datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable migrate down` - roll back the most recent migration on every datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target a specific datatable (default: all datatables in the workspace)
- `datatable create [name:string]` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- `--resource <resource:string>` - Back the datatable with an existing postgresql resource path instead of the instance database
- `--force` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
@@ -357,19 +357,18 @@ Manage jobs (list, inspect, cancel)
### jobs
Pull completed and queued jobs from workspace
**Arguments:** `[workspace:string]`
**Options:**
- `-c, --completed-output <file:string>` - Completed jobs output file (default: completed_jobs.json)
- `-q, --queued-output <file:string>` - Queued jobs output file (default: queued_jobs.json)
- `--skip-worker-check` - Skip checking for active workers before export
Manage jobs (import/export)
**Subcommands:**
- `jobs pull`
- `jobs push`
- `jobs pull [workspace:string]` - Pull completed and queued jobs from workspace
- `-c, --completed-output <file:string>` - Completed jobs output file (default: completed_jobs.json)
- `-q, --queued-output <file:string>` - Queued jobs output file (default: queued_jobs.json)
- `--skip-worker-check` - Skip checking for active workers before export
- `jobs push [workspace:string]` - Push completed and queued jobs to workspace
- `-c, --completed-file <file:string>` - Completed jobs input file (default: completed_jobs.json)
- `-q, --queued-file <file:string>` - Queued jobs input file (default: queued_jobs.json)
- `--skip-worker-check` - Skip checking for active workers before import
### lint
+76 -10
View File
@@ -340,13 +340,54 @@ def extract_description(section: str) -> str | None:
return ''.join(_unquote_js_string(p) for p in parts).strip() or None
def parse_command_block(content: str, file_path: Path | None = None) -> dict:
def extract_named_command_block(content: str, var_name: str) -> str | None:
"""Return the chained-call body of `const <var_name> = new Command() ...`,
from just after `new Command()` up to the next top-level statement.
Returns None when the var isn't a *direct* `new Command()` (e.g. it's wrapped
in a helper call like `auditListOptions(new Command()...)`), so callers can
fall back to a looser match.
"""
m = re.search(
r'const\s+' + re.escape(var_name) + r'\s*=\s*new\s+Command\(\)'
r'([\s\S]*?)(?=\n(?:const|let|var|async|function|export)\b)',
content,
)
return m.group(1) if m else None
def extract_exported_command_block(content: str) -> str | None:
"""Return the chained-call body of the command that is `export default`ed.
A command file may define helper `new Command()` groups (assigned to local
consts and mounted as nested subcommands via `.command("x", localCmd)`)
*before* the exported command. Anchoring on the first `new Command()` in the
file would merge those helpers into the top-level command, so resolve the
exported variable first and only then fall back to the first `new Command()`
(which covers inline/wrapped exports).
"""
export_match = re.search(r'export\s+default\s+(\w+)\s*;', content)
if export_match:
block = extract_named_command_block(content, export_match.group(1))
if block is not None:
return block
command_match = re.search(
r'(?:const\s+command\s*=\s*)?new\s+Command\(\)([\s\S]*?)(?=export\s+default)',
content,
)
return command_match.group(1) if command_match else None
def parse_command_block(
content: str, file_path: Path | None = None, block: str | None = None
) -> dict:
"""
Parse a Cliffy Command() definition block and extract metadata.
Returns a dict with: description, options, subcommands, arguments, alias
If file_path is provided, imported subcommands will be resolved by parsing
the imported files.
the imported files. `block` may be passed to parse a specific pre-extracted
command body (used to recurse into locally-defined nested command groups).
"""
result = {
'description': '',
@@ -357,15 +398,11 @@ def parse_command_block(content: str, file_path: Path | None = None) -> dict:
}
# Find the command block
command_match = re.search(
r'(?:const\s+command\s*=\s*)?new\s+Command\(\)([\s\S]*?)(?=export\s+default)',
content
)
if not command_match:
if block is None:
block = extract_exported_command_block(content)
if block is None:
return result
block = command_match.group(1)
# Find where subcommands start
first_subcommand_pos = block.find('.command(')
if first_subcommand_pos == -1:
@@ -451,12 +488,31 @@ def parse_command_block(content: str, file_path: Path | None = None) -> dict:
'name': cmd_name,
'description': imported_cmd.get('description', ''),
'arguments': imported_cmd.get('arguments', ''),
'options': imported_cmd.get('options', [])
'options': imported_cmd.get('options', []),
'subcommands': imported_cmd.get('subcommands', []),
})
continue
except Exception as e:
print(f" Warning: Could not parse imported command {second_arg}: {e}")
cmd_desc = ''
elif second_arg and re.search(
r'const\s+' + re.escape(second_arg) + r'\s*=\s*new\s+Command\(\)', content
):
# Locally-defined command group mounted as a subcommand
# (e.g. `.command("migrate", migrateCommand)`): recurse into its
# definition so its own subcommands/options are captured.
nested_block = extract_named_command_block(content, second_arg)
if nested_block is not None:
nested = parse_command_block(content, file_path, block=nested_block)
result['subcommands'].append({
'name': cmd_name,
'description': nested.get('description', ''),
'arguments': nested.get('arguments', ''),
'options': nested.get('options', []),
'subcommands': nested.get('subcommands', []),
})
continue
cmd_desc = ''
else:
cmd_desc = ''
@@ -628,6 +684,16 @@ def generate_cli_commands_markdown(cli_data: dict) -> str:
for opt in sub['options']:
md += f" - `{opt['flag']}` - {opt['description']}\n"
# Nested sub-subcommands (e.g. `datatable migrate new`)
for subsub in sub.get('subcommands', []):
ss_args = f" {subsub['arguments']}" if subsub.get('arguments') else ""
md += f" - `{cmd['name']} {sub_name} {subsub['name']}{ss_args}`"
if subsub.get('description'):
md += f" - {subsub['description']}"
md += "\n"
for opt in subsub.get('options', []):
md += f" - `{opt['flag']}` - {opt['description']}\n"
md += "\n"
return md