diff --git a/Cargo.lock b/Cargo.lock index 51d0e89a..a0263565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2174,6 +2174,7 @@ dependencies = [ "serde", "shell-words", "strum", + "tempfile", "tera", "thiserror", "versions", diff --git a/cli/assets/fig.ts b/cli/assets/fig.ts index 433bb830..29499a60 100644 --- a/cli/assets/fig.ts +++ b/cli/assets/fig.ts @@ -456,6 +456,55 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: "sdk", + description: "Generate a type-safe SDK from a usage spec", + options: [ + { + name: ["-f", "--file"], + description: "A usage spec taken in as a file", + isRepeatable: false, + args: { + name: "file", + template: "filepaths", + }, + }, + { + name: ["-l", "--language"], + description: "Target language for the SDK", + isRepeatable: false, + args: { + name: "language", + suggestions: ["typescript", "python"], + }, + }, + { + name: ["-o", "--output"], + description: "Output directory for generated SDK files", + isRepeatable: false, + args: { + name: "output", + }, + }, + { + name: ["-p", "--package-name"], + description: + "Override the package/module name (defaults to spec bin name)", + isRepeatable: false, + args: { + name: "package_name", + }, + }, + { + name: "--spec", + description: "Raw string spec input", + isRepeatable: false, + args: { + name: "spec", + }, + }, + ], + }, ], }, { diff --git a/cli/assets/usage.1 b/cli/assets/usage.1 index 6320211c..0055e5f8 100644 --- a/cli/assets/usage.1 +++ b/cli/assets/usage.1 @@ -69,6 +69,9 @@ Generate markdown documentation from usage specs \fIAliases: \fRmd .RE .TP +\fBgenerate sdk\fR +Generate a type\-safe SDK from a usage spec +.TP \fBlint\fR Lint a usage spec file for common issues .TP @@ -310,6 +313,28 @@ Replace `
` tags with markdown code fences
 .TP
 \fB\-\-url\-prefix\fR \fI\fR
 Prefix to add to all URLs
+.SH "USAGE GENERATE SDK"
+Generate a type\-safe SDK from a usage spec
+.PP
+\fBUsage:\fR usage generate sdk [OPTIONS]
+.PP
+\fBOptions:\fR
+.PP
+.TP
+\fB\-f, \-\-file\fR \fI\fR
+A usage spec taken in as a file
+.TP
+\fB\-l, \-\-language\fR \fI\fR
+Target language for the SDK
+.TP
+\fB\-o, \-\-output\fR \fI\fR
+Output directory for generated SDK files
+.TP
+\fB\-p, \-\-package\-name\fR \fI\fR
+Override the package/module name (defaults to spec bin name)
+.TP
+\fB\-\-spec\fR \fI\fR
+Raw string spec input
 .SH "USAGE LINT"
 Lint a usage spec file for common issues
 .PP
diff --git a/cli/src/cli/generate/mod.rs b/cli/src/cli/generate/mod.rs
index 4943bc8b..38d6b5e5 100644
--- a/cli/src/cli/generate/mod.rs
+++ b/cli/src/cli/generate/mod.rs
@@ -10,6 +10,7 @@ mod fig;
 mod json;
 mod manpage;
 mod markdown;
+mod sdk;
 
 /// Generate completions, documentation, and other artifacts from usage specs
 #[derive(clap::Args)]
@@ -27,6 +28,7 @@ pub enum Command {
     Json(json::Json),
     Manpage(manpage::Manpage),
     Markdown(markdown::Markdown),
+    Sdk(sdk::Sdk),
 }
 
 impl Generate {
@@ -38,6 +40,7 @@ impl Generate {
             Command::Json(cmd) => cmd.run(),
             Command::Manpage(cmd) => cmd.run(),
             Command::Markdown(cmd) => cmd.run(),
+            Command::Sdk(cmd) => cmd.run(),
         }
     }
 }
diff --git a/cli/src/cli/generate/sdk.rs b/cli/src/cli/generate/sdk.rs
new file mode 100644
index 00000000..76949eb6
--- /dev/null
+++ b/cli/src/cli/generate/sdk.rs
@@ -0,0 +1,66 @@
+use std::path::PathBuf;
+
+use clap::Args;
+
+use crate::cli::generate;
+
+use usage::sdk::{SdkLanguage, SdkOptions};
+
+#[derive(Args)]
+#[clap(about = "Generate a type-safe SDK from a usage spec")]
+pub struct Sdk {
+    /// A usage spec taken in as a file
+    #[clap(short, long)]
+    file: Option,
+
+    /// Target language for the SDK
+    #[clap(short, long, value_parser = ["typescript", "python"])]
+    language: String,
+
+    /// Output directory for generated SDK files
+    #[clap(short, long)]
+    output: PathBuf,
+
+    /// Override the package/module name (defaults to spec bin name)
+    #[clap(short, long)]
+    package_name: Option,
+
+    /// Raw string spec input
+    #[clap(long, required_unless_present = "file", overrides_with = "file")]
+    spec: Option,
+}
+
+impl Sdk {
+    pub fn run(&self) -> miette::Result<()> {
+        let spec = generate::file_or_spec(&self.file, &self.spec)?;
+
+        let language = match self.language.as_str() {
+            "typescript" => SdkLanguage::TypeScript,
+            "python" => SdkLanguage::Python,
+            other => {
+                return Err(miette::miette!("unsupported language: {other}"));
+            }
+        };
+
+        let source_file = self.file.as_ref().map(|p| p.display().to_string());
+
+        let opts = SdkOptions {
+            language,
+            package_name: self.package_name.clone(),
+            source_file,
+        };
+
+        let output = usage::sdk::generate(&spec, &opts);
+
+        std::fs::create_dir_all(&self.output)
+            .map_err(|e| miette::miette!("failed to create output directory: {e}"))?;
+
+        for file in &output.files {
+            let path = self.output.join(&file.path);
+            println!("writing to {}", path.display());
+            xx::file::write(&path, &file.content)?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/cli/usage.usage.kdl b/cli/usage.usage.kdl
index 5bea24b1..cabaaf46 100644
--- a/cli/usage.usage.kdl
+++ b/cli/usage.usage.kdl
@@ -135,6 +135,25 @@ cmd generate subcommand_required=#true help="Generate completions, documentation
             arg 
         }
     }
+    cmd sdk help="Generate a type-safe SDK from a usage spec" {
+        flag "-f --file" help="A usage spec taken in as a file" {
+            arg 
+        }
+        flag "-l --language" help="Target language for the SDK" required=#true {
+            arg  {
+                choices typescript python
+            }
+        }
+        flag "-o --output" help="Output directory for generated SDK files" required=#true {
+            arg 
+        }
+        flag "-p --package-name" help="Override the package/module name (defaults to spec bin name)" {
+            arg 
+        }
+        flag --spec help="Raw string spec input" {
+            arg 
+        }
+    }
 }
 cmd lint help="Lint a usage spec file for common issues" {
     flag "-f --format" help="Output format" default=text {
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 4e029e35..c0a53f28 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -57,6 +57,7 @@ export default defineConfig({
           { text: "Completions", link: "/cli/completions" },
           { text: "Manpages", link: "/cli/manpages" },
           { text: "Markdown", link: "/cli/markdown" },
+          { text: "SDK Generation", link: "/cli/sdk" },
           { text: "Scripts", link: "/cli/scripts" },
           {
             text: "CLI Reference", link: "/cli/reference/", items:
diff --git a/docs/cli/reference/commands.json b/docs/cli/reference/commands.json
index b9565066..f7400728 100644
--- a/docs/cli/reference/commands.json
+++ b/docs/cli/reference/commands.json
@@ -740,6 +740,111 @@
             "aliases": ["md"],
             "hidden_aliases": [],
             "examples": []
+          },
+          "sdk": {
+            "full_cmd": ["generate", "sdk"],
+            "usage": "generate sdk ",
+            "subcommands": {},
+            "args": [],
+            "flags": [
+              {
+                "name": "file",
+                "usage": "-f --file ",
+                "help": "A usage spec taken in as a file",
+                "help_first_line": "A usage spec taken in as a file",
+                "short": ["f"],
+                "long": ["file"],
+                "hide": false,
+                "global": false,
+                "arg": {
+                  "name": "FILE",
+                  "usage": "",
+                  "required": true,
+                  "double_dash": "Optional",
+                  "hide": false
+                }
+              },
+              {
+                "name": "language",
+                "usage": "-l --language ",
+                "help": "Target language for the SDK",
+                "help_first_line": "Target language for the SDK",
+                "short": ["l"],
+                "long": ["language"],
+                "required": true,
+                "hide": false,
+                "global": false,
+                "arg": {
+                  "name": "LANGUAGE",
+                  "usage": "",
+                  "required": true,
+                  "double_dash": "Optional",
+                  "hide": false,
+                  "choices": {
+                    "choices": ["typescript", "python"]
+                  }
+                }
+              },
+              {
+                "name": "output",
+                "usage": "-o --output ",
+                "help": "Output directory for generated SDK files",
+                "help_first_line": "Output directory for generated SDK files",
+                "short": ["o"],
+                "long": ["output"],
+                "required": true,
+                "hide": false,
+                "global": false,
+                "arg": {
+                  "name": "OUTPUT",
+                  "usage": "",
+                  "required": true,
+                  "double_dash": "Optional",
+                  "hide": false
+                }
+              },
+              {
+                "name": "package-name",
+                "usage": "-p --package-name ",
+                "help": "Override the package/module name (defaults to spec bin name)",
+                "help_first_line": "Override the package/module name (defaults to spec bin name)",
+                "short": ["p"],
+                "long": ["package-name"],
+                "hide": false,
+                "global": false,
+                "arg": {
+                  "name": "PACKAGE_NAME",
+                  "usage": "",
+                  "required": true,
+                  "double_dash": "Optional",
+                  "hide": false
+                }
+              },
+              {
+                "name": "spec",
+                "usage": "--spec ",
+                "help": "Raw string spec input",
+                "help_first_line": "Raw string spec input",
+                "short": [],
+                "long": ["spec"],
+                "hide": false,
+                "global": false,
+                "arg": {
+                  "name": "SPEC",
+                  "usage": "",
+                  "required": true,
+                  "double_dash": "Optional",
+                  "hide": false
+                }
+              }
+            ],
+            "mounts": [],
+            "hide": false,
+            "help": "Generate a type-safe SDK from a usage spec",
+            "name": "sdk",
+            "aliases": [],
+            "hidden_aliases": [],
+            "examples": []
           }
         },
         "args": [],
diff --git a/docs/cli/reference/generate.md b/docs/cli/reference/generate.md
index 00ea4d0e..2971788b 100644
--- a/docs/cli/reference/generate.md
+++ b/docs/cli/reference/generate.md
@@ -16,3 +16,4 @@ Generate completions, documentation, and other artifacts from usage specs
 - [`usage generate json [-f --file ] [--spec ]`](/cli/reference/generate/json.md)
 - [`usage generate manpage `](/cli/reference/generate/manpage.md)
 - [`usage generate markdown `](/cli/reference/generate/markdown.md)
+- [`usage generate sdk `](/cli/reference/generate/sdk.md)
diff --git a/docs/cli/reference/generate/sdk.md b/docs/cli/reference/generate/sdk.md
new file mode 100644
index 00000000..a1d2ab41
--- /dev/null
+++ b/docs/cli/reference/generate/sdk.md
@@ -0,0 +1,35 @@
+
+
+# `usage generate sdk`
+
+- **Usage**: `usage generate sdk `
+- **Source code**: [`cli/src/cli/generate/sdk.rs`](https://github.com/jdx/usage/blob/main/cli/src/cli/generate/sdk.rs)
+
+Generate a type-safe SDK from a usage spec
+
+## Flags
+
+### `-f --file `
+
+A usage spec taken in as a file
+
+### `-l --language `
+
+Target language for the SDK
+
+**Choices:**
+
+- `typescript`
+- `python`
+
+### `-o --output `
+
+Output directory for generated SDK files
+
+### `-p --package-name `
+
+Override the package/module name (defaults to spec bin name)
+
+### `--spec `
+
+Raw string spec input
diff --git a/docs/cli/reference/index.md b/docs/cli/reference/index.md
index fcb0ac6d..84694296 100644
--- a/docs/cli/reference/index.md
+++ b/docs/cli/reference/index.md
@@ -33,6 +33,7 @@ Outputs a `usage.kdl` spec for this CLI itself
 - [`usage generate json [-f --file ] [--spec ]`](/cli/reference/generate/json.md)
 - [`usage generate manpage `](/cli/reference/generate/manpage.md)
 - [`usage generate markdown `](/cli/reference/generate/markdown.md)
+- [`usage generate sdk `](/cli/reference/generate/sdk.md)
 - [`usage lint [-f --format ] [-W --warnings-as-errors] `](/cli/reference/lint.md)
 - [`usage powershell [-h] [--help]