11#!/usr/bin/env node
22
3- import { Command } from ' commander' ;
4- import { createDevbox } from ' ./commands/devbox/create.js' ;
5- import { listDevboxes } from ' ./commands/devbox/list.js' ;
6- import { deleteDevbox } from ' ./commands/devbox/delete.js' ;
7- import { execCommand } from ' ./commands/devbox/exec.js' ;
8- import { uploadFile } from ' ./commands/devbox/upload.js' ;
9- import { getConfig } from ' ./utils/config.js' ;
10- import { readFileSync } from 'fs' ;
11- import { fileURLToPath } from ' url' ;
12- import { dirname , join } from ' path' ;
3+ import { Command } from " commander" ;
4+ import { createDevbox } from " ./commands/devbox/create.js" ;
5+ import { listDevboxes } from " ./commands/devbox/list.js" ;
6+ import { deleteDevbox } from " ./commands/devbox/delete.js" ;
7+ import { execCommand } from " ./commands/devbox/exec.js" ;
8+ import { uploadFile } from " ./commands/devbox/upload.js" ;
9+ import { getConfig } from " ./utils/config.js" ;
10+ import { readFileSync } from "fs" ;
11+ import { fileURLToPath } from " url" ;
12+ import { dirname , join } from " path" ;
1313
1414// Get version from package.json
1515const __filename = fileURLToPath ( import . meta. url ) ;
1616const __dirname = dirname ( __filename ) ;
17- const packageJson = JSON . parse ( readFileSync ( join ( __dirname , '../package.json' ) , 'utf8' ) ) ;
17+ const packageJson = JSON . parse (
18+ readFileSync ( join ( __dirname , "../package.json" ) , "utf8" ) ,
19+ ) ;
1820export const VERSION = packageJson . version ;
1921
2022// Global Ctrl+C handler to ensure it always exits
21- process . on ( ' SIGINT' , ( ) => {
23+ process . on ( " SIGINT" , ( ) => {
2224 // Force exit immediately, clearing alternate screen buffer
23- process . stdout . write ( ' \x1b[?1049l' ) ;
24- process . stdout . write ( '\n' ) ;
25+ process . stdout . write ( " \x1b[?1049l" ) ;
26+ process . stdout . write ( "\n" ) ;
2527 process . exit ( 130 ) ; // Standard exit code for SIGINT
2628} ) ;
2729
2830const program = new Command ( ) ;
2931
30- program . name ( 'rln' ) . description ( 'Beautiful CLI for Runloop devbox management' ) . version ( VERSION ) ;
32+ program
33+ . name ( "rln" )
34+ . description ( "Beautiful CLI for Runloop devbox management" )
35+ . version ( VERSION ) ;
3136
3237program
33- . command ( ' auth' )
34- . description ( ' Configure API authentication' )
38+ . command ( " auth" )
39+ . description ( " Configure API authentication" )
3540 . action ( async ( ) => {
36- const { default : auth } = await import ( ' ./commands/auth.js' ) ;
41+ const { default : auth } = await import ( " ./commands/auth.js" ) ;
3742 auth ( ) ;
3843 } ) ;
3944
4045// Devbox commands
41- const devbox = program . command ( 'devbox' ) . description ( 'Manage devboxes' ) . alias ( 'd' ) ;
46+ const devbox = program
47+ . command ( "devbox" )
48+ . description ( "Manage devboxes" )
49+ . alias ( "d" ) ;
4250
4351devbox
44- . command ( 'create' )
45- . description ( 'Create a new devbox' )
46- . option ( '-n, --name <name>' , 'Devbox name' )
47- . option ( '-t, --template <template>' , 'Template to use' )
48- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
52+ . command ( "create" )
53+ . description ( "Create a new devbox" )
54+ . option ( "-n, --name <name>" , "Devbox name" )
55+ . option ( "-t, --template <template>" , "Template to use" )
56+ . option (
57+ "-o, --output [format]" ,
58+ "Output format: text|json|yaml (default: interactive)" ,
59+ )
4960 . action ( createDevbox ) ;
5061
5162devbox
52- . command ( 'list' )
53- . description ( 'List all devboxes' )
54- . option ( '-s, --status <status>' , 'Filter by status' )
55- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
56- . action ( async options => {
63+ . command ( "list" )
64+ . description ( "List all devboxes" )
65+ . option ( "-s, --status <status>" , "Filter by status" )
66+ . option (
67+ "-o, --output [format]" ,
68+ "Output format: text|json|yaml (default: interactive)" ,
69+ )
70+ . action ( async ( options ) => {
5771 // Only use alternate screen for interactive mode
5872 if ( ! options . output ) {
59- const { runInteractiveCommand } = await import ( './utils/interactiveCommand.js' ) ;
73+ const { runInteractiveCommand } = await import (
74+ "./utils/interactiveCommand.js"
75+ ) ;
6076 await runInteractiveCommand ( ( ) => listDevboxes ( options ) ) ;
6177 } else {
6278 await listDevboxes ( options ) ;
6379 }
6480 } ) ;
6581
6682devbox
67- . command ( 'delete <id>' )
68- . description ( 'Shutdown a devbox' )
69- . alias ( 'rm' )
70- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
83+ . command ( "delete <id>" )
84+ . description ( "Shutdown a devbox" )
85+ . alias ( "rm" )
86+ . option (
87+ "-o, --output [format]" ,
88+ "Output format: text|json|yaml (default: interactive)" ,
89+ )
7190 . action ( deleteDevbox ) ;
7291
7392devbox
74- . command ( 'exec <id> <command...>' )
75- . description ( 'Execute a command in a devbox' )
76- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
93+ . command ( "exec <id> <command...>" )
94+ . description ( "Execute a command in a devbox" )
95+ . option (
96+ "-o, --output [format]" ,
97+ "Output format: text|json|yaml (default: interactive)" ,
98+ )
7799 . action ( execCommand ) ;
78100
79101devbox
80- . command ( 'upload <id> <file>' )
81- . description ( 'Upload a file to a devbox' )
82- . option ( '-p, --path <path>' , 'Target path in devbox' )
83- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
102+ . command ( "upload <id> <file>" )
103+ . description ( "Upload a file to a devbox" )
104+ . option ( "-p, --path <path>" , "Target path in devbox" )
105+ . option (
106+ "-o, --output [format]" ,
107+ "Output format: text|json|yaml (default: interactive)" ,
108+ )
84109 . action ( uploadFile ) ;
85110
86111// Snapshot commands
87- const snapshot = program . command ( 'snapshot' ) . description ( 'Manage devbox snapshots' ) . alias ( 'snap' ) ;
112+ const snapshot = program
113+ . command ( "snapshot" )
114+ . description ( "Manage devbox snapshots" )
115+ . alias ( "snap" ) ;
88116
89117snapshot
90- . command ( 'list' )
91- . description ( 'List all snapshots' )
92- . option ( '-d, --devbox <id>' , 'Filter by devbox ID' )
93- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
94- . action ( async options => {
95- const { listSnapshots } = await import ( './commands/snapshot/list.js' ) ;
118+ . command ( "list" )
119+ . description ( "List all snapshots" )
120+ . option ( "-d, --devbox <id>" , "Filter by devbox ID" )
121+ . option (
122+ "-o, --output [format]" ,
123+ "Output format: text|json|yaml (default: interactive)" ,
124+ )
125+ . action ( async ( options ) => {
126+ const { listSnapshots } = await import ( "./commands/snapshot/list.js" ) ;
96127 if ( ! options . output ) {
97- const { runInteractiveCommand } = await import ( './utils/interactiveCommand.js' ) ;
128+ const { runInteractiveCommand } = await import (
129+ "./utils/interactiveCommand.js"
130+ ) ;
98131 await runInteractiveCommand ( ( ) => listSnapshots ( options ) ) ;
99132 } else {
100133 await listSnapshots ( options ) ;
101134 }
102135 } ) ;
103136
104137snapshot
105- . command ( 'create <devbox-id>' )
106- . description ( 'Create a snapshot of a devbox' )
107- . option ( '-n, --name <name>' , 'Snapshot name' )
108- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
138+ . command ( "create <devbox-id>" )
139+ . description ( "Create a snapshot of a devbox" )
140+ . option ( "-n, --name <name>" , "Snapshot name" )
141+ . option (
142+ "-o, --output [format]" ,
143+ "Output format: text|json|yaml (default: interactive)" ,
144+ )
109145 . action ( async ( devboxId , options ) => {
110- const { createSnapshot } = await import ( ' ./commands/snapshot/create.js' ) ;
146+ const { createSnapshot } = await import ( " ./commands/snapshot/create.js" ) ;
111147 createSnapshot ( devboxId , options ) ;
112148 } ) ;
113149
114150snapshot
115- . command ( 'delete <id>' )
116- . description ( 'Delete a snapshot' )
117- . alias ( 'rm' )
118- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
151+ . command ( "delete <id>" )
152+ . description ( "Delete a snapshot" )
153+ . alias ( "rm" )
154+ . option (
155+ "-o, --output [format]" ,
156+ "Output format: text|json|yaml (default: interactive)" ,
157+ )
119158 . action ( async ( id , options ) => {
120- const { deleteSnapshot } = await import ( ' ./commands/snapshot/delete.js' ) ;
159+ const { deleteSnapshot } = await import ( " ./commands/snapshot/delete.js" ) ;
121160 deleteSnapshot ( id , options ) ;
122161 } ) ;
123162
124163// Blueprint commands
125- const blueprint = program . command ( 'blueprint' ) . description ( 'Manage blueprints' ) . alias ( 'bp' ) ;
164+ const blueprint = program
165+ . command ( "blueprint" )
166+ . description ( "Manage blueprints" )
167+ . alias ( "bp" ) ;
126168
127169blueprint
128- . command ( 'list' )
129- . description ( 'List all blueprints' )
130- . option ( '-o, --output [format]' , 'Output format: text|json|yaml (default: interactive)' )
131- . action ( async options => {
132- const { listBlueprints } = await import ( './commands/blueprint/list.js' ) ;
170+ . command ( "list" )
171+ . description ( "List all blueprints" )
172+ . option (
173+ "-o, --output [format]" ,
174+ "Output format: text|json|yaml (default: interactive)" ,
175+ )
176+ . action ( async ( options ) => {
177+ const { listBlueprints } = await import ( "./commands/blueprint/list.js" ) ;
133178 if ( ! options . output ) {
134- const { runInteractiveCommand } = await import ( './utils/interactiveCommand.js' ) ;
179+ const { runInteractiveCommand } = await import (
180+ "./utils/interactiveCommand.js"
181+ ) ;
135182 await runInteractiveCommand ( ( ) => listBlueprints ( options ) ) ;
136183 } else {
137184 await listBlueprints ( options ) ;
138185 }
139186 } ) ;
140187
141188// MCP server commands
142- const mcp = program . command ( 'mcp' ) . description ( 'Model Context Protocol (MCP) server commands' ) ;
189+ const mcp = program
190+ . command ( "mcp" )
191+ . description ( "Model Context Protocol (MCP) server commands" ) ;
143192
144193mcp
145- . command ( 'start' )
146- . description ( 'Start the MCP server' )
147- . option ( '--http' , 'Use HTTP/SSE transport instead of stdio' )
148- . option ( '-p, --port <port>' , 'Port to listen on for HTTP mode (default: 3000)' , parseInt )
149- . action ( async options => {
194+ . command ( "start" )
195+ . description ( "Start the MCP server" )
196+ . option ( "--http" , "Use HTTP/SSE transport instead of stdio" )
197+ . option (
198+ "-p, --port <port>" ,
199+ "Port to listen on for HTTP mode (default: 3000)" ,
200+ parseInt ,
201+ )
202+ . action ( async ( options ) => {
150203 if ( options . http ) {
151- const { startMcpHttpServer } = await import ( ' ./commands/mcp-http.js' ) ;
204+ const { startMcpHttpServer } = await import ( " ./commands/mcp-http.js" ) ;
152205 await startMcpHttpServer ( options . port ) ;
153206 } else {
154- const { startMcpServer } = await import ( ' ./commands/mcp.js' ) ;
207+ const { startMcpServer } = await import ( " ./commands/mcp.js" ) ;
155208 await startMcpServer ( ) ;
156209 }
157210 } ) ;
158211
159212mcp
160- . command ( ' install' )
161- . description ( ' Install Runloop MCP server configuration in Claude Desktop' )
213+ . command ( " install" )
214+ . description ( " Install Runloop MCP server configuration in Claude Desktop" )
162215 . action ( async ( ) => {
163- const { installMcpConfig } = await import ( ' ./commands/mcp-install.js' ) ;
216+ const { installMcpConfig } = await import ( " ./commands/mcp-install.js" ) ;
164217 await installMcpConfig ( ) ;
165218 } ) ;
166219
167220// Hidden command: 'rln mcp' without subcommand starts the server (for Claude Desktop config compatibility)
168221program
169- . command ( 'mcp-server' , { hidden : true } )
170- . option ( '--http' , 'Use HTTP/SSE transport instead of stdio' )
171- . option ( '-p, --port <port>' , 'Port to listen on for HTTP mode (default: 3000)' , parseInt )
172- . action ( async options => {
222+ . command ( "mcp-server" , { hidden : true } )
223+ . option ( "--http" , "Use HTTP/SSE transport instead of stdio" )
224+ . option (
225+ "-p, --port <port>" ,
226+ "Port to listen on for HTTP mode (default: 3000)" ,
227+ parseInt ,
228+ )
229+ . action ( async ( options ) => {
173230 if ( options . http ) {
174- const { startMcpHttpServer } = await import ( ' ./commands/mcp-http.js' ) ;
231+ const { startMcpHttpServer } = await import ( " ./commands/mcp-http.js" ) ;
175232 await startMcpHttpServer ( options . port ) ;
176233 } else {
177- const { startMcpServer } = await import ( ' ./commands/mcp.js' ) ;
234+ const { startMcpServer } = await import ( " ./commands/mcp.js" ) ;
178235 await startMcpServer ( ) ;
179236 }
180237 } ) ;
@@ -184,23 +241,23 @@ program
184241 // Check if API key is configured (except for auth and mcp commands)
185242 const args = process . argv . slice ( 2 ) ;
186243 if (
187- args [ 0 ] !== ' auth' &&
188- args [ 0 ] !== ' mcp' &&
189- args [ 0 ] !== ' mcp-server' &&
190- args [ 0 ] !== ' --help' &&
191- args [ 0 ] !== '-h' &&
244+ args [ 0 ] !== " auth" &&
245+ args [ 0 ] !== " mcp" &&
246+ args [ 0 ] !== " mcp-server" &&
247+ args [ 0 ] !== " --help" &&
248+ args [ 0 ] !== "-h" &&
192249 args . length > 0
193250 ) {
194251 const config = getConfig ( ) ;
195252 if ( ! config . apiKey ) {
196- console . error ( ' \n❌ API key not configured. Run: rln auth\n' ) ;
253+ console . error ( " \n❌ API key not configured. Run: rln auth\n" ) ;
197254 process . exit ( 1 ) ;
198255 }
199256 }
200257
201258 // If no command provided, show main menu
202259 if ( args . length === 0 ) {
203- const { runMainMenu } = await import ( ' ./commands/menu.js' ) ;
260+ const { runMainMenu } = await import ( " ./commands/menu.js" ) ;
204261 runMainMenu ( ) ;
205262 } else {
206263 program . parse ( ) ;
0 commit comments