feat: generate SDK for TypeScript and Python #623
Conversation
…sive tests - Fix var=true boolean flag typed as Optional[bool] in Python - Fix -- separator ordering for double_dash=required args - Fix Python var flag default type mismatch - Align SDK tests across Python and TypeScript - Add ~30 new Python tests and ~14 new TypeScript tests - Add compile/import validation tests for Python and TypeScript - Mark Rust SDK as coming soon in docs - Add Typer and Click integration links
- Python: add client_edge_cases, config_and_flag_edge_cases, double_dash_automatic - TypeScript: add config_boolean_default_false, config_string_with_default, example_without_lang, flag_edge_cases, global_flags_flags_only, double_dash_automatic
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #623 +/- ##
==========================================
+ Coverage 78.94% 79.64% +0.70%
==========================================
Files 49 55 +6
Lines 7284 10106 +2822
Branches 7284 10106 +2822
==========================================
+ Hits 5750 8049 +2299
- Misses 1147 1322 +175
- Partials 387 735 +348 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Remove Rust SDK module, language enum variant, CLI option, and all snapshot files. Rust SDK will be re-added in a future update.
f9986cc to
44a8480
Compare
|
Warning Gemini is experiencing higher than usual traffic and was unable to create the review. Please try again in a few hours by commenting |
Greptile SummaryThis PR adds TypeScript and Python SDK generation to
Confidence Score: 4/5Safe to merge for the vast majority of specs; a spec whose flag default value contains a literal newline character would generate a broken Python module. The two unguarded lib/src/sdk/python/mod.rs — the two Important Files Changed
Reviews (12): Last reviewed commit: "style: apply formatting after render" | Re-trigger Greptile |
- P2: re-export CliResult/CliRunner from Python __init__.py and CliResult from TypeScript index.ts - P2: escape */ in JSDoc and triple-quote in Python docstrings to prevent premature termination from user-supplied spec text - P2: fix unreachable "True"/"False"/"None" in sanitize_py_ident (heck::AsSnakeCase lowercases before match) - P2: remove incorrect await from docs synchronous API example
Replace spawnSync with spawn + Promise wrapper in runtime.ts. All exec() methods now return Promise<CliResult> and are marked async, which is more idiomatic for Node.js and avoids blocking the event loop on subprocess calls. Update docs examples to use await. Update all TypeScript snapshots.
059c78e to
5b958fc
Compare
…types
- P1: escape backslashes and double quotes in Python string defaults
(config props, arg defaults, flag defaults, VERSION/ABOUT/AUTHOR,
bin_path). Add escape_py_string() and escape_ts_string() helpers.
- P2: export CliError from TypeScript index.ts
- P2: stop emitting empty XxxFlags extends GlobalFlags {} interfaces
in types.ts when subcommands only have global flags with no local
flags — client.ts already uses GlobalFlags directly in this case
- Python: escape choice values, cmd path elements, flag_arg_name, negate flag names in string literals; escape alias docstrings - TypeScript: escape choice values in type unions, cmd path elements in subcmd_path, bin_name in constructor, flag_arg_name, negate flag names in string literals Note: flag short names (char type) cannot contain " or \ so they do not need escaping.
…gment - Python: render multi-part exec docstrings as proper multiline docstrings instead of single-line with embedded \n - docs: fix broken sentence from Rust SDK removal (child_process.spawn / not -> child_process.spawn, not)
20bf555 to
6c483d9
Compare
Python render_command_types was generating {name}Flags dataclasses
for subcommands that only inherit global flags (no local flags).
Since client.py uses GlobalFlags directly for these cases, the
generated dataclass was dead code. Now only emit Flags dataclass
when there are local visible flags, matching the TypeScript fix.
1f8596e to
dd222a2
Compare
| #[cfg(feature = "docs")] | ||
| pub mod docs; | ||
| pub mod parse; | ||
| #[cfg(feature = "sdk")] |
There was a problem hiding this comment.
Why add a feature?
I found it relatively independent and make it a feature. Should we remove it?
…ze newlines in Python comments
| if has_global_flags { | ||
| w.line(""); | ||
| w.line("/** Global flags available on all subcommands. */"); | ||
| w.line("export interface GlobalFlags {"); | ||
| w.indent(); | ||
| for flag in &root_global_flags { | ||
| let prop = flag_property_name(flag); | ||
| let ts_type = flag_ts_simple(flag); | ||
| let optional = if flag.required { "" } else { "?" }; | ||
| let mut doc_parts = Vec::new(); | ||
| if let Some(help) = &flag.help { | ||
| doc_parts.push(help.clone()); | ||
| } | ||
| if let Some(env) = &flag.env { | ||
| doc_parts.push(format!("Environment variable: {env}")); | ||
| } | ||
| if !doc_parts.is_empty() { | ||
| w.line(&format!("/** {} */", escape_jsdoc(&doc_parts.join(". ")))); | ||
| } | ||
| w.line(&format!("{prop}{optional}: {ts_type};")); | ||
| } | ||
| w.dedent(); | ||
| w.line("}"); | ||
| } |
There was a problem hiding this comment.
GlobalFlags drops choice type constraints for global flags with choices
flag_ts_simple is used to render the GlobalFlags interface and always returns string for any value flag, regardless of whether the flag has a choices constraint. A global flag declared as flag "--env <env>" global=#true { choices "dev" "prod" } would generate env?: string in GlobalFlags instead of env?: EnvChoice. The Python generator correctly calls flag_py_type(flag, "", choice_types) (using cmd_name = "" to match the root-level choice entry) for the equivalent GlobalFlags dataclass, so there is a concrete asymmetry between the two targets.
The fix is to replace flag_ts_simple with flag_ts_type(flag, "", choice_types) (passing "" as the cmd_name, consistent with how root-level choice entries are stored) for the GlobalFlags rendering loop.
| } else if flag.var { | ||
| // var flag with a default — list defaults are mutable and forbidden in dataclasses. | ||
| // use None and preserve the intended default in a comment. | ||
| format!("{prop_name}: Optional[{py_type}] = None # default: {default_val}") | ||
| } else { |
There was a problem hiding this comment.
Both fallback comment paths embed
default_val directly into a # ... line comment without sanitizing newlines. A spec default like default="first second" would emit second as a bare Python expression, causing a NameError at import time. All other help-comment sites in this file use sanitize_py_comment.
| } else if flag.var { | |
| // var flag with a default — list defaults are mutable and forbidden in dataclasses. | |
| // use None and preserve the intended default in a comment. | |
| format!("{prop_name}: Optional[{py_type}] = None # default: {default_val}") | |
| } else { | |
| } else if flag.var { | |
| // var flag with a default — list defaults are mutable and forbidden in dataclasses. | |
| // use None and preserve the intended default in a comment. | |
| format!("{prop_name}: Optional[{py_type}] = None # default: {}", sanitize_py_comment(default_val)) | |
| } else { |
No description provided.