11#!/usr/bin/env node
2- import * as fs from 'fs-extra' ;
32import * as path from 'node:path' ;
3+ import * as fs from 'fs-extra' ;
44import fetch from 'node-fetch' ; // Use node-fetch v3 which supports ESM
55
66// --- Constants ---
@@ -16,48 +16,55 @@ const CUSTOM_MODES_FILENAME = 'custom_modes.json';
1616const ROO_EXTENSION_ID = 'rooveterinaryinc.roo-cline' ;
1717// For now, using the provided Windows path. This WILL break on other systems.
1818// biome-ignore lint/style: Required by TS's noUncheckedIndexedAccess for process.env
19- const CUSTOM_MODES_DIR_WINDOWS = path . join ( process . env [ 'APPDATA' ] || '' , 'Code' , 'User' , 'globalStorage' , ROO_EXTENSION_ID , 'settings' ) ;
19+ const CUSTOM_MODES_DIR_WINDOWS = path . join (
20+ process . env . APPDATA || '' ,
21+ 'Code' ,
22+ 'User' ,
23+ 'globalStorage' ,
24+ ROO_EXTENSION_ID ,
25+ 'settings' ,
26+ ) ;
2027const CUSTOM_MODES_PATH = path . join ( CUSTOM_MODES_DIR_WINDOWS , CUSTOM_MODES_FILENAME ) ; // Adjust this based on OS detection later
2128
2229const SYLPHX_MODE_SLUG = 'sylphx' ;
2330const SYLPHX_MODE_NAME = '🪽 Sylphx' ;
24- const SYLPHX_MODE_GROUPS = [ " read" , " edit" , " browser" , " command" , " mcp" ] ;
31+ const SYLPHX_MODE_GROUPS = [ ' read' , ' edit' , ' browser' , ' command' , ' mcp' ] ;
2532const SYLPHX_MODE_SOURCE = 'global' ; // Assuming 'global' is correct
2633
2734/**
2835 * Represents the response structure from the GitHub Contents API.
2936 */
3037interface GitHubContentResponse {
31- /** Base64 encoded content of the file. */
32- content ? : string ;
33- /** Encoding type (should be 'base64'). */
34- encoding ? : string ;
35- /** Error message if the request failed. */
36- message ? : string ;
38+ /** Base64 encoded content of the file. */
39+ content ? : string ;
40+ /** Encoding type (should be 'base64'). */
41+ encoding ? : string ;
42+ /** Error message if the request failed. */
43+ message ? : string ;
3744}
3845
3946/**
4047 * Represents the structure of a single custom mode definition.
4148 */
4249interface CustomMode {
43- /** Unique identifier for the mode. */
44- slug: string ;
45- /** Display name of the mode. */
46- name: string ;
47- /** The core instructions or prompt defining the mode's behavior. */
48- roleDefinition: string ;
49- /** List of capability groups enabled for the mode. */
50- groups: string [ ] ;
51- /** Source of the mode definition (e.g., 'global', 'user'). */
52- source: string ;
50+ /** Unique identifier for the mode. */
51+ slug: string ;
52+ /** Display name of the mode. */
53+ name: string ;
54+ /** The core instructions or prompt defining the mode's behavior. */
55+ roleDefinition: string ;
56+ /** List of capability groups enabled for the mode. */
57+ groups: string [ ] ;
58+ /** Source of the mode definition (e.g., 'global', 'user'). */
59+ source: string ;
5360}
5461
5562/**
5663 * Represents the structure of the custom_modes.json file.
5764 */
5865interface CustomModesFile {
59- /** An array containing all custom mode definitions. */
60- customModes: CustomMode [ ] ;
66+ /** An array containing all custom mode definitions. */
67+ customModes: CustomMode [ ] ;
6168}
6269
6370// --- Helper Functions ---
@@ -68,36 +75,35 @@ interface CustomModesFile {
6875 * @throws Throws an error if the fetch request fails, the API returns an error, or the content is invalid.
6976 */
7077export async function fetchLatestInstructions ( ) : Promise < string > {
71- console. log ( `Fetching latest instructions from ${ GITHUB_REPO_OWNER } /${ GITHUB_REPO_NAME } ...` ) ;
72- try {
73- const response = await fetch ( GITHUB_API_URL , {
74- headers : {
75- 'Accept' : 'application/vnd.github.v3+json' ,
76- // Add 'Authorization': `token YOUR_GITHUB_TOKEN` if hitting rate limits
77- } ,
78- } ) ;
79-
80- if ( ! response . ok ) {
81- throw new Error ( `GitHub API request failed: ${ response . status } ${ response . statusText } ` ) ;
82- }
83-
84- const data = await response . json ( ) as GitHubContentResponse ;
85-
86- if ( data . message ) {
87- throw new Error ( `GitHub API error: ${ data . message } ` ) ;
88- }
89-
90- if ( ! data . content || data . encoding !== 'base64' ) {
91- throw new Error ( 'Invalid content received from GitHub API.' ) ;
92- }
93-
94- const decodedContent = Buffer . from ( data . content , 'base64' ) . toString ( 'utf-8' ) ;
95- console . log ( 'Successfully fetched instructions.' ) ;
96- return decodedContent ;
97- } catch ( error ) {
98- console . error ( 'Error fetching from GitHub:' , error ) ;
99- throw new Error ( `Failed to fetch instructions from GitHub: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
78+ try {
79+ const response = await fetch ( GITHUB_API_URL , {
80+ headers : {
81+ Accept : 'application/vnd.github.v3+json' ,
82+ // Add 'Authorization': `token YOUR_GITHUB_TOKEN` if hitting rate limits
83+ } ,
84+ } ) ;
85+
86+ if ( ! response . ok ) {
87+ throw new Error ( `GitHub API request failed: ${ response . status } ${ response . statusText } ` ) ;
10088 }
89+
90+ const data = ( await response . json ( ) ) as GitHubContentResponse ;
91+
92+ if ( data . message ) {
93+ throw new Error ( `GitHub API error: ${ data . message } ` ) ;
94+ }
95+
96+ if ( ! data . content || data . encoding !== 'base64' ) {
97+ throw new Error ( 'Invalid content received from GitHub API.' ) ;
98+ }
99+
100+ const decodedContent = Buffer . from ( data . content , 'base64' ) . toString ( 'utf-8' ) ;
101+ return decodedContent ;
102+ } catch ( error ) {
103+ throw new Error (
104+ `Failed to fetch instructions from GitHub: ${ error instanceof Error ? error . message : String ( error ) } ` ,
105+ ) ;
106+ }
101107}
102108
103109/**
@@ -108,35 +114,30 @@ export async function fetchLatestInstructions(): Promise<string> {
108114 * @throws Throws an error if reading the file fails (excluding file not found or parse errors, which are handled).
109115 */
110116export async function readCustomModes ( ) : Promise < CustomModesFile > {
111- console. log ( `Reading custom modes file: ${ CUSTOM_MODES_PATH } ` ) ;
112- try {
113- // Ensure the directory exists
114- await fs . ensureDir ( path . dirname ( CUSTOM_MODES_PATH ) ) ;
115-
116- if ( ! ( await fs . pathExists ( CUSTOM_MODES_PATH ) ) ) {
117- console . log ( 'custom_modes.json not found. Creating a new one.' ) ;
118- const initialData : CustomModesFile = { customModes : [ ] } ;
119- await fs . writeJson ( CUSTOM_MODES_PATH , initialData , { spaces : 2 } ) ;
120- return initialData ;
121- }
122-
123- const fileContent = await fs . readJson ( CUSTOM_MODES_PATH ) ;
124- // Basic validation
125- if ( ! fileContent || ! Array . isArray ( fileContent . customModes ) ) {
126- console . warn ( 'custom_modes.json seems malformed. Resetting to default structure.' ) ;
127- return { customModes : [ ] } ; // Return default structure if format is wrong
128- }
129- console . log ( 'Successfully read custom modes file.' ) ;
130- return fileContent as CustomModesFile ;
131- } catch ( error ) {
132- if ( error instanceof SyntaxError ) {
133- console . error ( `Error parsing JSON in ${ CUSTOM_MODES_PATH } :` , error ) ;
134- console . warn ( 'File content might be corrupted. Attempting to reset.' ) ;
135- return { customModes : [ ] } ; // Return default structure on parse error
136- }
137- console . error ( 'Error reading custom modes file:' , error ) ;
138- throw new Error ( `Failed to read or parse ${ CUSTOM_MODES_PATH } : ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
117+ try {
118+ // Ensure the directory exists
119+ await fs . ensureDir ( path . dirname ( CUSTOM_MODES_PATH ) ) ;
120+
121+ if ( ! ( await fs . pathExists ( CUSTOM_MODES_PATH ) ) ) {
122+ const initialData : CustomModesFile = { customModes : [ ] } ;
123+ await fs . writeJson ( CUSTOM_MODES_PATH , initialData , { spaces : 2 } ) ;
124+ return initialData ;
125+ }
126+
127+ const fileContent = await fs . readJson ( CUSTOM_MODES_PATH ) ;
128+ // Basic validation
129+ if ( ! fileContent || ! Array . isArray ( fileContent . customModes ) ) {
130+ return { customModes : [ ] } ; // Return default structure if format is wrong
131+ }
132+ return fileContent as CustomModesFile ;
133+ } catch ( error ) {
134+ if ( error instanceof SyntaxError ) {
135+ return { customModes : [ ] } ; // Return default structure on parse error
139136 }
137+ throw new Error (
138+ `Failed to read or parse ${ CUSTOM_MODES_PATH } : ${ error instanceof Error ? error . message : String ( error ) } ` ,
139+ ) ;
140+ }
140141}
141142
142143/**
@@ -146,39 +147,34 @@ export async function readCustomModes(): Promise<CustomModesFile> {
146147 * @param instructions The latest role definition (instructions) for the Sylphx mode.
147148 * @returns A new CustomModesFile object with the updated/added Sylphx mode.
148149 */
149- export function updateModesData (
150- modesData : CustomModesFile ,
151- instructions : string ,
152- ) : CustomModesFile {
153- const existingModeIndex = modesData . customModes . findIndex (
154- ( mode ) => mode . slug === SYLPHX_MODE_SLUG ,
155- ) ;
156-
157- const newMode : CustomMode = {
158- slug : SYLPHX_MODE_SLUG ,
159- name : SYLPHX_MODE_NAME ,
160- roleDefinition : instructions ,
161- groups : SYLPHX_MODE_GROUPS ,
162- source : SYLPHX_MODE_SOURCE ,
163- } ;
164-
165- if ( existingModeIndex !== - 1 ) {
166- console . log ( `Updating existing '${ SYLPHX_MODE_SLUG } ' mode definition.` ) ;
167- // Create a new array with the updated mode
168- const updatedCustomModes = modesData . customModes . map ( ( mode , index ) =>
169- index === existingModeIndex ? newMode : mode ,
170- ) ;
171- return {
172- ...modesData , // Spread other potential properties if any
173- customModes : updatedCustomModes ,
174- } ;
175- }
176- console . log ( `Adding new '${ SYLPHX_MODE_SLUG } ' mode definition.` ) ;
177- // Create a new array with the new mode added
178- return {
179- ...modesData ,
180- customModes : [ ...modesData . customModes , newMode ] ,
181- } ;
150+ export function updateModesData ( modesData : CustomModesFile , instructions : string ) : CustomModesFile {
151+ const existingModeIndex = modesData . customModes . findIndex (
152+ ( mode ) => mode . slug === SYLPHX_MODE_SLUG ,
153+ ) ;
154+
155+ const newMode : CustomMode = {
156+ slug : SYLPHX_MODE_SLUG ,
157+ name : SYLPHX_MODE_NAME ,
158+ roleDefinition : instructions ,
159+ groups : SYLPHX_MODE_GROUPS ,
160+ source : SYLPHX_MODE_SOURCE ,
161+ } ;
162+
163+ if ( existingModeIndex !== - 1 ) {
164+ // Create a new array with the updated mode
165+ const updatedCustomModes = modesData . customModes . map ( ( mode , index ) =>
166+ index === existingModeIndex ? newMode : mode ,
167+ ) ;
168+ return {
169+ ...modesData , // Spread other potential properties if any
170+ customModes : updatedCustomModes ,
171+ } ;
172+ }
173+ // Create a new array with the new mode added
174+ return {
175+ ...modesData ,
176+ customModes : [ ...modesData . customModes , newMode ] ,
177+ } ;
182178}
183179
184180/**
@@ -189,49 +185,45 @@ export function updateModesData(
189185 * @throws Throws an error if writing the file fails.
190186 */
191187export async function writeCustomModes ( modesData : CustomModesFile ) : Promise < void > {
192- console . log ( `Writing updated modes to ${ CUSTOM_MODES_PATH } ` ) ;
193- try {
194- await fs . writeJson ( CUSTOM_MODES_PATH , modesData , { spaces : 2 } ) ;
195- console . log ( 'Successfully updated custom_modes.json.' ) ;
196- } catch ( error ) {
197- console . error ( 'Error writing custom modes file:' , error ) ;
198- throw new Error ( `Failed to write updates to ${ CUSTOM_MODES_PATH } : ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
199- }
188+ try {
189+ await fs . writeJson ( CUSTOM_MODES_PATH , modesData , { spaces : 2 } ) ;
190+ } catch ( error ) {
191+ throw new Error (
192+ `Failed to write updates to ${ CUSTOM_MODES_PATH } : ${ error instanceof Error ? error . message : String ( error ) } ` ,
193+ ) ;
194+ }
200195}
201196
202197// --- Main Execution ---
203198async function main ( ) {
204- console . log ( 'Starting @sylphx/setup_mode...' ) ;
205- try {
206- // 1. Fetch latest instructions
207- const latestInstructions = await fetchLatestInstructions ( ) ;
208-
209- // 2. Read existing custom modes file (or create if needed)
210- let customModesData = await readCustomModes ( ) ;
211-
212- // 3. Update the data with the new/updated Sylphx mode
213- customModesData = updateModesData ( customModesData , latestInstructions ) ;
214-
215- // 4. Write the updated data back to the file
216- await writeCustomModes ( customModesData ) ;
217-
218- console . log ( `✅ Successfully added/updated the '${ SYLPHX_MODE_SLUG } ' mode in ${ CUSTOM_MODES_PATH } ` ) ;
219- process . exit ( 0 ) ; // Success
220-
221- } catch ( error ) {
222- console . error ( '\n❌ An error occurred during setup:' ) ;
223- console . error ( error instanceof Error ? error . message : String ( error ) ) ;
224- process . exit ( 1 ) ; // Failure
225- }
199+ try {
200+ // 1. Fetch latest instructions
201+ const latestInstructions = await fetchLatestInstructions ( ) ;
202+
203+ // 2. Read existing custom modes file (or create if needed)
204+ let customModesData = await readCustomModes ( ) ;
205+
206+ // 3. Update the data with the new/updated Sylphx mode
207+ customModesData = updateModesData ( customModesData , latestInstructions ) ;
208+
209+ // 4. Write the updated data back to the file
210+ await writeCustomModes ( customModesData ) ;
211+ process . exit ( 0 ) ; // Success
212+ } catch ( _error ) {
213+ process . exit ( 1 ) ; // Failure
214+ }
226215}
227216
228217// Execute the main function only if the script is run directly
229218// This prevents it from running when imported as a module (e.g., in tests)
230219// Note: process.argv[1] might not be reliable in all execution contexts (e.g., pkg).
231220// A more robust check might be needed depending on packaging/distribution method.
232- if ( import . meta. url . startsWith ( 'file:' ) && process . argv [ 1 ] && import . meta. url . endsWith ( path . basename ( process . argv [ 1 ] ) ) ) {
233- main ( ) . catch ( err => {
234- console . error ( "Unhandled error in main execution:" , err ) ;
235- process . exit ( 1 ) ; // Ensure exit on unhandled main rejection
236- } ) ;
237- }
221+ if (
222+ import . meta. url . startsWith ( 'file:' ) &&
223+ process . argv [ 1 ] &&
224+ import . meta. url . endsWith ( path . basename ( process . argv [ 1 ] ) )
225+ ) {
226+ main ( ) . catch ( ( _err ) => {
227+ process . exit ( 1 ) ; // Ensure exit on unhandled main rejection
228+ } ) ;
229+ }
0 commit comments