Skip to content

feat: generate SDK for TypeScript and Python #623

Draft
gaojunran wants to merge 20 commits into
jdx:mainfrom
gaojunran:feat-sdk-python-ts
Draft

feat: generate SDK for TypeScript and Python #623
gaojunran wants to merge 20 commits into
jdx:mainfrom
gaojunran:feat-sdk-python-ts

Conversation

@gaojunran
Copy link
Copy Markdown
Contributor

No description provided.

gaojunran and others added 8 commits May 3, 2026 18:20
…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
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 96.82660% with 84 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.64%. Comparing base (beb1a66) to head (48bf65c).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
lib/src/sdk/python/mod.rs 97.29% 25 Missing and 7 partials ⚠️
cli/src/cli/generate/sdk.rs 0.00% 22 Missing ⚠️
lib/src/sdk/typescript/types.rs 97.71% 14 Missing and 5 partials ⚠️
lib/src/sdk/typescript/wrappers.rs 97.98% 4 Missing and 2 partials ⚠️
lib/src/sdk/mod.rs 98.92% 1 Missing and 2 partials ⚠️
cli/src/cli/generate/mod.rs 0.00% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Remove Rust SDK module, language enum variant, CLI option, and all
snapshot files. Rust SDK will be re-added in a future update.
@gaojunran gaojunran force-pushed the feat-sdk-python-ts branch from f9986cc to 44a8480 Compare May 4, 2026 08:17
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

Gemini is experiencing higher than usual traffic and was unable to create the review. Please try again in a few hours by commenting /gemini review.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR adds TypeScript and Python SDK generation to usage-cli, producing type-safe subprocess-wrapper clients from a usage spec. It introduces lib/src/sdk/{typescript,python}/ with types, client, runtime, and entry-point modules, a new usage generate sdk CLI subcommand, integration compile tests, and documentation.

  • String escaping and comment-newline sanitization (sanitize_py_comment) are applied consistently throughout both generators, and reserved-word collisions are guarded in sanitize_ident.
  • One remaining gap: two fallback # default: {default_val} comment paths in render_flags_dataclass do not route through sanitize_py_comment, so a spec default containing \ would produce broken Python at import time.
  • Integration tests skip gracefully when tools are absent; snapshot coverage is thorough.

Confidence Score: 4/5

Safe 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 # default: {default_val} comment paths in render_flags_dataclass are the only places where user-controlled content reaches a # line comment without newline sanitization. Every other comment path correctly uses sanitize_py_comment. The fix is a one-line change at each site.

lib/src/sdk/python/mod.rs — the two # default: {default_val} fallback branches in render_flags_dataclass.

Important Files Changed

Filename Overview
lib/src/sdk/python/mod.rs Python SDK generator: most escaping/sanitization issues addressed, but default_val in two inline # default: comment fallback paths is not passed through sanitize_py_comment, allowing embedded newlines to break generated Python.
lib/src/sdk/typescript/wrappers.rs TypeScript client generator: string escaping for bin_name, flag arg names, and full_cmd path elements all look correct.
lib/src/sdk/typescript/types.rs TypeScript types generator: choice escaping, flag deduplication in Flags extends GlobalFlags, and reserved-word sanitization all correctly implemented.
lib/src/sdk/mod.rs Shared SDK utilities: escape helpers, ChoiceTypeMap collision detection, and collect_type_imports all look correctly implemented.
cli/src/cli/generate/sdk.rs New CLI subcommand wiring: language enum dispatch, file output loop, and error handling are straightforward and correct.
lib/tests/sdk_compile.rs Integration compile tests for both Python and TypeScript — skipped when tools are absent, otherwise comprehensive.

Reviews (12): Last reviewed commit: "style: apply formatting after render" | Re-trigger Greptile

Comment thread lib/src/sdk/python/runtime.rs
Comment thread lib/src/sdk/typescript/mod.rs
Comment thread lib/src/sdk/typescript/wrappers.rs
Comment thread lib/src/sdk/python/mod.rs
Comment thread docs/cli/sdk.md Outdated
gaojunran added 2 commits May 4, 2026 16:35
- 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.
@gaojunran gaojunran force-pushed the feat-sdk-python-ts branch from 059c78e to 5b958fc Compare May 4, 2026 08:42
Comment thread lib/src/sdk/python/mod.rs Outdated
…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
Comment thread lib/src/sdk/typescript/wrappers.rs Outdated
Comment thread lib/src/sdk/typescript/types.rs
Comment thread lib/src/sdk/typescript/wrappers.rs
gaojunran added 2 commits May 4, 2026 17:40
- 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)
@gaojunran gaojunran force-pushed the feat-sdk-python-ts branch 2 times, most recently from 20bf555 to 6c483d9 Compare May 4, 2026 10:51
Comment thread lib/src/sdk/typescript/types.rs
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.
@gaojunran gaojunran force-pushed the feat-sdk-python-ts branch from 1f8596e to dd222a2 Compare May 4, 2026 11:05
@gaojunran gaojunran closed this May 4, 2026
@gaojunran gaojunran reopened this May 4, 2026
@gaojunran gaojunran marked this pull request as ready for review May 4, 2026 11:38
Comment thread lib/src/lib.rs Outdated
#[cfg(feature = "docs")]
pub mod docs;
pub mod parse;
#[cfg(feature = "sdk")]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add a feature?

Copy link
Copy Markdown
Contributor Author

@gaojunran gaojunran May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add a feature?

I found it relatively independent and make it a feature. Should we remove it?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

@gaojunran
Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread lib/src/sdk/typescript/types.rs
Comment thread lib/src/sdk/python/mod.rs
@gaojunran gaojunran marked this pull request as draft May 17, 2026 14:05
@gaojunran
Copy link
Copy Markdown
Contributor Author

@greptileai

Comment on lines +63 to +86
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("}");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread lib/src/sdk/python/mod.rs
Comment on lines +313 to +317
} 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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
} 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 {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants