@@ -66,8 +66,10 @@ export async function copySpecs(args: {
6666 specsDir ?: string ;
6767 /** When true, emit `mod custom;` + `custom::register(app)` in main.rs. */
6868 embedTypes ?: boolean ;
69+ /** When true, emit `mod sdk_glue;` in main.rs for the SDK bridge. */
70+ embedSdk ?: boolean ;
6971} ) : Promise < void > {
70- const { outputDir, binaryName, authBindings, specsDir, embedTypes } = args ;
72+ const { outputDir, binaryName, authBindings, specsDir, embedTypes, embedSdk } = args ;
7173 const manifest = await readSpecsManifest ( specsDir ) ;
7274 if ( manifest == null ) {
7375 return ;
@@ -90,12 +92,18 @@ export async function copySpecs(args: {
9092
9193 await writeFile (
9294 path . join ( binDir , "main.rs" ) ,
93- renderMainRs ( { binaryName, entries, authBindings, embedTypes : embedTypes ?? false } )
95+ renderMainRs ( {
96+ binaryName,
97+ entries,
98+ authBindings,
99+ embedTypes : embedTypes ?? false ,
100+ embedSdk : embedSdk ?? false
101+ } )
94102 ) ;
95103
96104 // Scaffold custom.rs for user-authored command handlers.
97105 if ( embedTypes === true ) {
98- await scaffoldCustomRs ( binDir , binaryName ) ;
106+ await scaffoldCustomRs ( binDir , binaryName , embedSdk ?? false ) ;
99107 }
100108}
101109
@@ -109,7 +117,7 @@ interface SpecEntry {
109117 * own async command handlers. Listed in `.fernignore` so `fern generate`
110118 * never overwrites user changes.
111119 */
112- async function scaffoldCustomRs ( binDir : string , binaryName : string ) : Promise < void > {
120+ async function scaffoldCustomRs ( binDir : string , binaryName : string , embedSdk : boolean ) : Promise < void > {
113121 const customRsPath = path . join ( binDir , "custom.rs" ) ;
114122 // Only create if it doesn't already exist (respects .fernignore).
115123 try {
@@ -118,27 +126,75 @@ async function scaffoldCustomRs(binDir: string, binaryName: string): Promise<voi
118126 } catch ( _e : unknown ) {
119127 // does not exist — scaffold it below
120128 }
121- const typesCrate = `${ binaryName . replace ( / - / g, "_" ) } _types` ;
122129 const sdkCrate = `${ binaryName . replace ( / - / g, "_" ) } _sdk` ;
123- const content = [
130+ const content = embedSdk ? renderCustomRsWithSdk ( sdkCrate ) : renderCustomRsTypesOnly ( binaryName ) ;
131+ await writeFile ( customRsPath , content ) ;
132+ }
133+
134+ /** Scaffold when the SDK crate is available (default). */
135+ function renderCustomRsWithSdk ( sdkCrate : string ) : string {
136+ return [
137+ "//! Custom command handlers." ,
138+ "//!" ,
139+ "//! This file is yours to edit — add it to `.fernignore` so" ,
140+ "//! `fern generate` will never overwrite your changes." ,
141+ "//!" ,
142+ "//! The generated `main.rs` calls `custom::register(app)` at" ,
143+ "//! startup, composing your commands into the CLI at compile time." ,
144+ "//!" ,
145+ "//! Each handler receives an `AppContext`. Use `sdk_glue::sdk_client(ctx)`" ,
146+ "//! to get a fully-wired SDK client that inherits the CLI's auth," ,
147+ "//! retries, TLS, and global headers. Use `sdk_glue::block_on(future)`" ,
148+ "//! to run async SDK calls from synchronous handler context." ,
149+ `//! Types are available via \`${ sdkCrate } ::api::*\`.` ,
150+ "" ,
151+ "use fern_cli_sdk::app::CliApp;" ,
152+ "" ,
153+ "/// Register custom commands on the CLI app builder." ,
154+ "///" ,
155+ "/// Called from `main.rs` during startup. Uncomment the example" ,
156+ "/// below and adapt it to your API to get started." ,
157+ "pub fn register(app: CliApp) -> CliApp {" ,
158+ " // Example: typed SDK client usage with the co-generated SDK." ,
159+ " //" ,
160+ ` // use ${ sdkCrate } ::api::*;` ,
161+ " //" ,
162+ " // let app = app.command(" ,
163+ ' // clap::Command::new("get-plant")' ,
164+ ' // .about("Fetch a plant by its ID")' ,
165+ ' // .arg(clap::Arg::new("plant-id").required(true)),' ,
166+ " // |matches, ctx| {" ,
167+ ' // let plant_id = matches.get_one::<String>("plant-id").unwrap();' ,
168+ " // let client = super::sdk_glue::sdk_client(ctx);" ,
169+ " // let plant = super::sdk_glue::block_on(" ,
170+ " // client.plants.get_plant(plant_id, None)," ,
171+ " // )?;" ,
172+ ' // println!("{}", serde_json::to_string_pretty(&plant).unwrap());' ,
173+ " // Ok(())" ,
174+ " // }," ,
175+ " // );" ,
176+ " app" ,
177+ "}" ,
178+ ""
179+ ] . join ( "\n" ) ;
180+ }
181+
182+ /** Scaffold when only the types crate is available (embedSdk: false). */
183+ function renderCustomRsTypesOnly ( binaryName : string ) : string {
184+ const typesCrate = `${ binaryName . replace ( / - / g, "_" ) } _types` ;
185+ return [
124186 "//! Custom command handlers." ,
125187 "//!" ,
126188 "//! This file is yours to edit — add it to `.fernignore` so" ,
127189 "//! `fern generate` will never overwrite your changes." ,
128190 "//!" ,
129191 "//! The generated `main.rs` calls `custom::register(app)` at" ,
130192 "//! startup, composing your commands into the CLI at compile time." ,
131- "//! This is the same pattern used by other Fern generators (e.g." ,
132- "//! Ruby's `requirePaths`) — the generated entrypoint references" ,
133- "//! this user-owned file, and `.fernignore` keeps it safe across" ,
134- "//! regenerations." ,
135193 "//!" ,
136194 "//! Each handler receives an `AppContext` whose `invoke()` and" ,
137195 "//! `execute()` methods use the CLI's native HTTP executor." ,
138196 `//! Combine these with the typed structs from \`${ typesCrate } \`` ,
139197 "//! for strongly-typed request/response serialization." ,
140- `//! The SDK crate (\`${ sdkCrate } \`) provides typed client helpers` ,
141- "//! via `ctx.sdk_client()` (available once the runtime handle lands)." ,
142198 "" ,
143199 "use fern_cli_sdk::app::CliApp;" ,
144200 "" ,
@@ -172,24 +228,20 @@ async function scaffoldCustomRs(binDir: string, binaryName: string): Promise<voi
172228 " // Ok(())" ,
173229 " // }," ,
174230 " // );" ,
175- " //" ,
176- " // SDK client usage (available after the runtime handle lands):" ,
177- ` // use ${ sdkCrate } ::prelude::*;` ,
178- " // let client = ctx.sdk_client();" ,
179231 " app" ,
180232 "}" ,
181233 ""
182234 ] . join ( "\n" ) ;
183- await writeFile ( customRsPath , content ) ;
184235}
185236
186237function renderMainRs ( args : {
187238 binaryName : string ;
188239 entries : SpecEntry [ ] ;
189240 authBindings : DetectedAuthBinding [ ] ;
190241 embedTypes : boolean ;
242+ embedSdk : boolean ;
191243} ) : string {
192- const { binaryName, entries, authBindings, embedTypes } = args ;
244+ const { binaryName, entries, authBindings, embedTypes, embedSdk } = args ;
193245
194246 // Separate root-level auth (typed builders) from binding-level auth
195247 const rootAuthBindings = authBindings . filter ( ( b ) => b . placement === "root" ) ;
@@ -217,6 +269,9 @@ function renderMainRs(args: {
217269
218270 if ( embedTypes ) {
219271 lines . push ( "mod custom;" ) ;
272+ if ( embedSdk ) {
273+ lines . push ( "mod sdk_glue;" ) ;
274+ }
220275 lines . push ( "" ) ;
221276 }
222277
0 commit comments