Skip to content
This repository was archived by the owner on Nov 3, 2025. It is now read-only.

Commit 798b230

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent 880be0b commit 798b230

File tree

8 files changed

+1517
-2
lines changed

8 files changed

+1517
-2
lines changed

packages/setup-mode/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# @sylphx/setup_mode
2+
3+
This tool automatically adds or updates the 'sylphx' custom mode definition in Roo's `custom_modes.json` configuration file using the latest instructions from the `sylphlab/Playbook` GitHub repository.
4+
5+
## Purpose
6+
7+
Ensures that the 'sylphx' mode in Roo utilizes the most up-to-date core instructions defined in the central Playbook repository.
8+
9+
## Features
10+
11+
- Fetches the latest `custom_instructions_core.md` from `sylphlab/Playbook` on GitHub.
12+
- Locates the Roo extension's global storage settings directory.
13+
- **Note:** Currently uses a hardcoded Windows path (`%APPDATA%\Code\User\globalStorage\rooveterinaryinc.roo-cline\settings`). Needs improvement for cross-platform compatibility.
14+
- Reads the existing `custom_modes.json`, creating it if it doesn't exist.
15+
- Adds or updates the mode definition with `slug: 'sylphx'`.
16+
- Writes the changes back to `custom_modes.json`.
17+
- Logs progress and errors to the console.
18+
19+
## Usage
20+
21+
### Running via npx (after publishing)
22+
23+
```bash
24+
npx @sylphx/setup_mode
25+
```
26+
27+
### Local Development
28+
29+
1. **Clone the repository (or ensure you have this directory).**
30+
2. **Install dependencies:**
31+
```bash
32+
npm install
33+
```
34+
3. **Build the TypeScript code:**
35+
```bash
36+
npm run build
37+
```
38+
4. **Run the compiled code directly:**
39+
```bash
40+
node dist/index.js
41+
```
42+
*or* **Run using ts-node (for development):**
43+
```bash
44+
npm run dev
45+
```
46+
5. **Run tests:**
47+
```bash
48+
npm test
49+
```
50+
51+
## TODO
52+
53+
- Implement robust, cross-platform detection of the VS Code extension global storage path for `rooveterinaryinc.roo-cline`. Consider libraries like `env-paths` or OS-specific logic. Fallback to prompting the user if detection fails.
54+
- Add more comprehensive error handling and user feedback.
55+
- Consider adding command-line arguments (e.g., specifying a path, using a specific GitHub branch/token).

packages/setup-mode/biome.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["@sylphlab/biome-config"]
3+
}

packages/setup-mode/package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@sylphx/setup-mode",
3+
"version": "0.1.0",
4+
"description": "Updates Roo's custom_modes.json with the latest sylphx mode definition from GitHub.",
5+
"type": "module",
6+
"bin": {
7+
"@sylphx/setup_mode": "./dist/index.js"
8+
},
9+
"main": "./dist/index.js",
10+
"scripts": {
11+
"format": "pnpx biome format --write .",
12+
"check": "pnpx biome check --write --unsafe .",
13+
"lint": "pnpm run check",
14+
"typecheck": "tsc --noEmit",
15+
"test": "vitest run",
16+
"test:watch": "vitest",
17+
"build": "tsup",
18+
"validate": "pnpm run check && pnpm run build && pnpm run test",
19+
"start": "node ./dist/index.js"
20+
},
21+
"keywords": [
22+
"roo",
23+
"cline",
24+
"sylphx",
25+
"custom-mode",
26+
"setup"
27+
],
28+
"author": "Sylph AI (via Roo)",
29+
"license": "MIT",
30+
"dependencies": {
31+
"fs-extra": "^11.2.0",
32+
"node-fetch": "^3.3.2"
33+
},
34+
"devDependencies": {
35+
"@biomejs/biome": "^1.8.0",
36+
"@sylphlab/biome-config": "workspace:*",
37+
"@types/fs-extra": "^11.0.4",
38+
"@types/node": "^20.14.2",
39+
"@types/node-fetch": "^2.6.11",
40+
"tsup": "^8.1.0",
41+
"vitest": "^1.6.0",
42+
"@sylphlab/typescript-config": "workspace:*",
43+
"typescript": "^5.4.5"
44+
},
45+
"engines": {
46+
"node": ">=18.0.0"
47+
},
48+
"publishConfig": {
49+
"access": "public"
50+
}
51+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import type { MockedFunction } from 'vitest';
3+
// Removed type-only import for fs
4+
import fetch from 'node-fetch';
5+
import type { Response } from 'node-fetch'; // Separate type import
6+
import { Buffer } from 'node:buffer'; // Explicitly import Buffer
7+
8+
// --- Mocking ---
9+
// Mock node-fetch
10+
// We need to mock the default export for ESM
11+
vi.mock('node-fetch', () => ({
12+
__esModule: true, // This is important for ESM modules
13+
default: vi.fn(),
14+
}));
15+
const mockedFetch = fetch as MockedFunction<typeof fetch>;
16+
17+
18+
// Mock fs-extra
19+
vi.mock('fs-extra', async () => {
20+
// Define mocks entirely inside the factory, but without default implementations for readJson/pathExists
21+
const mockEnsureDir = vi.fn();
22+
const mockPathExists = vi.fn();
23+
const mockReadJson = vi.fn();
24+
const mockWriteJson = vi.fn();
25+
26+
return {
27+
__esModule: true,
28+
ensureDir: mockEnsureDir,
29+
pathExists: mockPathExists,
30+
readJson: mockReadJson,
31+
writeJson: mockWriteJson,
32+
// Mock default export if necessary
33+
default: {
34+
ensureDir: mockEnsureDir,
35+
pathExists: mockPathExists,
36+
readJson: mockReadJson,
37+
writeJson: mockWriteJson,
38+
},
39+
};
40+
});
41+
42+
// Import the mocked module directly. Vitest ensures this is the mocked version.
43+
import * as fs from 'fs-extra';
44+
45+
// Import the functions to test
46+
import {
47+
fetchLatestInstructions,
48+
readCustomModes,
49+
updateModesData,
50+
writeCustomModes,
51+
} from './index.js'; // Updated import path
52+
53+
// --- Test Suite ---
54+
describe('@sylphx/setup_mode core logic', () => {
55+
56+
// Define constants used in tests (or export from src/index.ts)
57+
const SYLPHX_MODE_SLUG = 'sylphx';
58+
const SYLPHX_MODE_NAME = '🪽 Sylphx';
59+
const SYLPHX_MODE_GROUPS = ["read", "edit", "browser", "command", "mcp"];
60+
const SYLPHX_MODE_SOURCE = 'global';
61+
62+
beforeEach(() => {
63+
// Reset mocks before each test
64+
vi.clearAllMocks();
65+
vi.resetAllMocks(); // Resets all mocks including spies and call history
66+
// Mock console methods to prevent test output clutter
67+
vi.spyOn(console, 'log').mockImplementation(() => {});
68+
vi.spyOn(console, 'warn').mockImplementation(() => {});
69+
vi.spyOn(console, 'error').mockImplementation(() => {});
70+
});
71+
72+
afterEach(() => {
73+
// Restore console mocks
74+
vi.restoreAllMocks();
75+
});
76+
77+
// --- fetchLatestInstructions Tests ---
78+
describe('fetchLatestInstructions', () => {
79+
// No dynamic import needed here anymore
80+
81+
it('should fetch and decode instructions successfully', async () => {
82+
const mockContent = 'Test Markdown Content';
83+
const encodedContent = Buffer.from(mockContent).toString('base64');
84+
mockedFetch.mockResolvedValueOnce({
85+
ok: true,
86+
// Return the expected structure directly
87+
json: async () => ({ content: encodedContent, encoding: 'base64' }),
88+
} as Response); // Use the imported Response type
89+
90+
const instructions = await fetchLatestInstructions();
91+
expect(instructions).toBe(mockContent);
92+
expect(mockedFetch).toHaveBeenCalledTimes(1);
93+
// Add check for URL if needed: expect(mockedFetch).toHaveBeenCalledWith(EXPECTED_URL, expect.any(Object));
94+
});
95+
96+
it('should throw error on fetch failure', async () => {
97+
mockedFetch.mockResolvedValueOnce({
98+
ok: false,
99+
status: 404,
100+
statusText: 'Not Found',
101+
} as Response);
102+
103+
await expect(fetchLatestInstructions()).rejects.toThrow(/GitHub API request failed: 404 Not Found/);
104+
expect(console.error).toHaveBeenCalledWith('Error fetching from GitHub:', expect.any(Error));
105+
});
106+
107+
it('should throw error on API error message', async () => {
108+
mockedFetch.mockResolvedValueOnce({
109+
ok: true,
110+
json: async () => ({ message: 'API rate limit exceeded' }),
111+
} as Response);
112+
113+
await expect(fetchLatestInstructions()).rejects.toThrow(/GitHub API error: API rate limit exceeded/);
114+
expect(console.error).toHaveBeenCalledWith('Error fetching from GitHub:', expect.any(Error));
115+
});
116+
117+
it('should throw error on invalid content structure', async () => {
118+
mockedFetch.mockResolvedValueOnce({
119+
ok: true,
120+
json: async () => ({ content: 'some content', encoding: 'utf-8' }), // Wrong encoding
121+
} as Response);
122+
123+
await expect(fetchLatestInstructions()).rejects.toThrow(/Invalid content received from GitHub API/);
124+
expect(console.error).toHaveBeenCalledWith('Error fetching from GitHub:', expect.any(Error));
125+
});
126+
});
127+
128+
// --- readCustomModes Tests ---
129+
describe('readCustomModes', () => {
130+
// No dynamic import needed here anymore
131+
132+
it('should read existing valid JSON', async () => {
133+
const mockData = { customModes: [{ slug: 'test', name: 'Test', roleDefinition: '...', groups: [], source: 'user' }] };
134+
// Explicitly set mocks for this test case
135+
vi.mocked(fs.pathExists).mockImplementationOnce(async () => true); // Ensure pathExists is true
136+
vi.mocked(fs.readJson).mockResolvedValueOnce(mockData); // Use Once for safety
137+
138+
const data = await readCustomModes();
139+
expect(data).toEqual(mockData);
140+
expect(fs.ensureDir).toHaveBeenCalledTimes(1);
141+
expect(fs.readJson).toHaveBeenCalledTimes(1);
142+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Successfully read custom modes file.'));
143+
});
144+
145+
it('should create file if it does not exist', async () => {
146+
vi.mocked(fs.pathExists).mockImplementationOnce(async () => false); // Use mockImplementationOnce
147+
const initialData = { customModes: [] };
148+
// Ensure writeJson mock is reset/doesn't interfere if needed, though not strictly necessary here
149+
vi.mocked(fs.writeJson).mockResolvedValueOnce(undefined);
150+
151+
const data = await readCustomModes();
152+
expect(data).toEqual(initialData);
153+
expect(fs.ensureDir).toHaveBeenCalledTimes(1);
154+
expect(fs.writeJson).toHaveBeenCalledWith(expect.any(String), initialData, { spaces: 2 });
155+
expect(fs.readJson).not.toHaveBeenCalled();
156+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('custom_modes.json not found. Creating a new one.'));
157+
});
158+
159+
it('should return default and warn on invalid JSON', async () => {
160+
vi.mocked(fs.pathExists).mockImplementationOnce(async () => true); // Use mockImplementationOnce
161+
vi.mocked(fs.readJson).mockRejectedValueOnce(new SyntaxError('Invalid JSON')); // Use Once
162+
const initialData = { customModes: [] };
163+
164+
const data = await readCustomModes();
165+
expect(data).toEqual(initialData);
166+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Error parsing JSON'), expect.any(SyntaxError));
167+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('File content might be corrupted. Attempting to reset.'));
168+
});
169+
170+
it('should return default and warn on malformed structure', async () => {
171+
vi.mocked(fs.pathExists).mockImplementationOnce(async () => true); // Use mockImplementationOnce
172+
vi.mocked(fs.readJson).mockResolvedValueOnce({ someOtherProperty: [] }); // Use Once, Missing customModes array
173+
const initialData = { customModes: [] };
174+
175+
const data = await readCustomModes();
176+
expect(data).toEqual(initialData);
177+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('custom_modes.json seems malformed. Resetting to default structure.'));
178+
});
179+
180+
it('should throw error on other fs read errors', async () => {
181+
vi.mocked(fs.pathExists).mockImplementationOnce(async () => true); // Use mockImplementationOnce
182+
const readError = new Error('Permission denied');
183+
vi.mocked(fs.readJson).mockRejectedValueOnce(readError); // Use Once
184+
185+
await expect(readCustomModes()).rejects.toThrow(/Failed to read or parse.*Permission denied/);
186+
expect(console.error).toHaveBeenCalledWith('Error reading custom modes file:', readError);
187+
});
188+
});
189+
190+
// --- updateModesData Tests ---
191+
describe('updateModesData', () => {
192+
// No dynamic import needed here anymore
193+
194+
it('should add new mode if not exists', () => {
195+
const initialData = { customModes: [] };
196+
const instructions = 'New Instructions';
197+
const updatedData = updateModesData(initialData, instructions);
198+
199+
expect(updatedData.customModes).toHaveLength(1);
200+
// Assign to variable and check existence for type safety
201+
const addedMode = updatedData.customModes[0];
202+
expect(addedMode).toBeDefined();
203+
if (addedMode) {
204+
expect(addedMode.slug).toBe(SYLPHX_MODE_SLUG);
205+
expect(addedMode.roleDefinition).toBe(instructions);
206+
expect(addedMode.name).toBe(SYLPHX_MODE_NAME);
207+
expect(addedMode.groups).toEqual(SYLPHX_MODE_GROUPS);
208+
expect(addedMode.source).toBe(SYLPHX_MODE_SOURCE);
209+
}
210+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining(`Adding new '${SYLPHX_MODE_SLUG}' mode definition.`));
211+
});
212+
213+
it('should update existing mode', () => {
214+
const initialData = { customModes: [{ slug: SYLPHX_MODE_SLUG, name: 'Old Name', roleDefinition: 'Old Def', groups: [], source: 'user' }] };
215+
const instructions = 'Updated Instructions';
216+
const updatedData = updateModesData(initialData, instructions);
217+
218+
expect(updatedData.customModes).toHaveLength(1);
219+
// Assign to variable and check existence
220+
const updatedMode = updatedData.customModes[0];
221+
expect(updatedMode).toBeDefined();
222+
if (updatedMode) {
223+
expect(updatedMode.slug).toBe(SYLPHX_MODE_SLUG);
224+
expect(updatedMode.roleDefinition).toBe(instructions);
225+
expect(updatedMode.name).toBe(SYLPHX_MODE_NAME); // Name gets updated
226+
expect(updatedMode.groups).toEqual(SYLPHX_MODE_GROUPS); // Groups updated
227+
expect(updatedMode.source).toBe(SYLPHX_MODE_SOURCE); // Source updated
228+
}
229+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining(`Updating existing '${SYLPHX_MODE_SLUG}' mode definition.`));
230+
});
231+
});
232+
233+
// --- writeCustomModes Tests ---
234+
describe('writeCustomModes', () => {
235+
// No dynamic import needed here anymore
236+
237+
it('should call fs.writeJson with correct data', async () => {
238+
const dataToWrite = { customModes: [{ slug: SYLPHX_MODE_SLUG, name: SYLPHX_MODE_NAME, roleDefinition: 'Test Def', groups: SYLPHX_MODE_GROUPS, source: SYLPHX_MODE_SOURCE }] };
239+
await writeCustomModes(dataToWrite);
240+
241+
expect(fs.writeJson).toHaveBeenCalledTimes(1);
242+
expect(fs.writeJson).toHaveBeenCalledWith(expect.any(String), dataToWrite, { spaces: 2 });
243+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Successfully updated custom_modes.json.'));
244+
});
245+
246+
it('should throw error on fs write error', async () => {
247+
const writeError = new Error('Disk full');
248+
vi.mocked(fs.writeJson).mockRejectedValueOnce(writeError); // Use Once
249+
const dataToWrite = { customModes: [] };
250+
251+
await expect(writeCustomModes(dataToWrite)).rejects.toThrow(/Failed to write updates.*Disk full/);
252+
expect(console.error).toHaveBeenCalledWith('Error writing custom modes file:', writeError);
253+
});
254+
});
255+
});

0 commit comments

Comments
 (0)