Skip to content

Commit 59da735

Browse files
Copilotvobu
andcommitted
refactor: Simplify plugin scanning with functional approach
- Extract helper functions for plugin validation and scanning - Replace nested if-else with functional forEach and filter - Improve code readability and maintainability - No functional changes, all tests pass Co-authored-by: vobu <6573426+vobu@users.noreply.github.com>
1 parent 7b68661 commit 59da735

File tree

1 file changed

+114
-69
lines changed

1 file changed

+114
-69
lines changed

src/commands/plugins.ts

Lines changed: 114 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,66 @@ function hasPluginFile(packagePath: string): boolean {
9393
existsSync(join(packagePath, 'c8ctl-plugin.ts'));
9494
}
9595

96+
/**
97+
* Check if a package is a valid c8ctl plugin
98+
*/
99+
function isValidPlugin(pkgPath: string): boolean {
100+
const pkgJsonPath = join(pkgPath, 'package.json');
101+
if (!existsSync(pkgJsonPath)) return false;
102+
103+
try {
104+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
105+
return hasPluginFile(pkgPath) && pkgJson.keywords?.includes('c8ctl');
106+
} catch {
107+
return false;
108+
}
109+
}
110+
111+
/**
112+
* Get package name from a valid plugin directory
113+
*/
114+
function getPackageName(pkgPath: string): string | null {
115+
const pkgJsonPath = join(pkgPath, 'package.json');
116+
try {
117+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
118+
return pkgJson.name;
119+
} catch {
120+
return null;
121+
}
122+
}
123+
124+
/**
125+
* Scan directory entries for c8ctl plugins
126+
*/
127+
function scanForPlugin(nodeModulesPath: string, entries: string[]): string | null {
128+
for (const entry of entries.filter(e => !e.startsWith('.'))) {
129+
const pkgPath = entry.startsWith('@')
130+
? null // Scoped packages handled separately
131+
: join(nodeModulesPath, entry);
132+
133+
if (pkgPath && isValidPlugin(pkgPath)) {
134+
return getPackageName(pkgPath);
135+
}
136+
137+
// Handle scoped packages
138+
if (entry.startsWith('@')) {
139+
const scopePath = join(nodeModulesPath, entry);
140+
try {
141+
const scopedPackages = readdirSync(scopePath);
142+
for (const scopedPkg of scopedPackages) {
143+
const pkgPath = join(scopePath, scopedPkg);
144+
if (isValidPlugin(pkgPath)) {
145+
return getPackageName(pkgPath);
146+
}
147+
}
148+
} catch {
149+
// Skip scoped packages that can't be read
150+
}
151+
}
152+
}
153+
return null;
154+
}
155+
96156
/**
97157
* Extract package name from URL or installed package
98158
* Tries to read package.json from installed package, falls back to URL parsing
@@ -103,37 +163,8 @@ function extractPackageNameFromUrl(url: string, pluginsDir: string): string {
103163
const nodeModulesPath = join(pluginsDir, 'node_modules');
104164
if (existsSync(nodeModulesPath)) {
105165
const entries = readdirSync(nodeModulesPath);
106-
for (const entry of entries) {
107-
if (entry.startsWith('.')) continue;
108-
109-
if (entry.startsWith('@')) {
110-
// Scoped package
111-
const scopePath = join(nodeModulesPath, entry);
112-
const scopedPackages = readdirSync(scopePath);
113-
for (const scopedPkg of scopedPackages) {
114-
const pkgPath = join(scopePath, scopedPkg);
115-
const pkgJsonPath = join(pkgPath, 'package.json');
116-
if (existsSync(pkgJsonPath)) {
117-
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
118-
// Check if this package has c8ctl-plugin file
119-
if (hasPluginFile(pkgPath) && pkgJson.keywords?.includes('c8ctl')) {
120-
return pkgJson.name;
121-
}
122-
}
123-
}
124-
} else {
125-
// Regular package
126-
const pkgPath = join(nodeModulesPath, entry);
127-
const pkgJsonPath = join(pkgPath, 'package.json');
128-
if (existsSync(pkgJsonPath)) {
129-
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
130-
// Check if this package has c8ctl-plugin file
131-
if (hasPluginFile(pkgPath) && pkgJson.keywords?.includes('c8ctl')) {
132-
return pkgJson.name;
133-
}
134-
}
135-
}
136-
}
166+
const foundName = scanForPlugin(nodeModulesPath, entries);
167+
if (foundName) return foundName;
137168
}
138169
} catch (error) {
139170
// Fall through to URL-based name extraction
@@ -180,6 +211,57 @@ export async function unloadPlugin(packageName: string): Promise<void> {
180211
}
181212
}
182213

214+
/**
215+
* Scan a directory entry for c8ctl plugins and add to the set
216+
*/
217+
function addPluginIfFound(entry: string, nodeModulesPath: string, installedPlugins: Set<string>): void {
218+
if (entry.startsWith('.')) return;
219+
220+
if (entry.startsWith('@')) {
221+
// Scoped package - scan subdirectories
222+
const scopePath = join(nodeModulesPath, entry);
223+
try {
224+
readdirSync(scopePath)
225+
.filter(pkg => !pkg.startsWith('.'))
226+
.forEach(scopedPkg => {
227+
const packageNameWithScope = `${entry}/${scopedPkg}`;
228+
const packagePath = join(nodeModulesPath, entry, scopedPkg);
229+
if (hasPluginFile(packagePath)) {
230+
installedPlugins.add(packageNameWithScope);
231+
}
232+
});
233+
} catch {
234+
// Skip packages that can't be read
235+
}
236+
} else {
237+
// Regular package
238+
const packagePath = join(nodeModulesPath, entry);
239+
if (hasPluginFile(packagePath)) {
240+
installedPlugins.add(entry);
241+
}
242+
}
243+
}
244+
245+
/**
246+
* Scan node_modules for installed plugins
247+
*/
248+
function scanInstalledPlugins(nodeModulesPath: string): Set<string> {
249+
const installedPlugins = new Set<string>();
250+
251+
if (!existsSync(nodeModulesPath)) {
252+
return installedPlugins;
253+
}
254+
255+
try {
256+
const entries = readdirSync(nodeModulesPath);
257+
entries.forEach(entry => addPluginIfFound(entry, nodeModulesPath, installedPlugins));
258+
} catch (error) {
259+
getLogger().debug('Error scanning global plugins directory:', error);
260+
}
261+
262+
return installedPlugins;
263+
}
264+
183265
/**
184266
* List installed plugins
185267
*/
@@ -193,44 +275,7 @@ export function listPlugins(): void {
193275
// Check global plugins directory
194276
const pluginsDir = ensurePluginsDir();
195277
const nodeModulesPath = join(pluginsDir, 'node_modules');
196-
let installedPlugins: Set<string> = new Set();
197-
198-
if (existsSync(nodeModulesPath)) {
199-
// Scan for installed plugins in global directory
200-
try {
201-
const entries = readdirSync(nodeModulesPath);
202-
for (const entry of entries) {
203-
if (entry.startsWith('.')) continue;
204-
205-
if (entry.startsWith('@')) {
206-
// Scoped package - scan subdirectories
207-
const scopePath = join(nodeModulesPath, entry);
208-
try {
209-
const scopedPackages = readdirSync(scopePath);
210-
for (const scopedPkg of scopedPackages) {
211-
if (!scopedPkg.startsWith('.')) {
212-
const packageNameWithScope = `${entry}/${scopedPkg}`;
213-
const packagePath = join(nodeModulesPath, entry, scopedPkg);
214-
if (hasPluginFile(packagePath)) {
215-
installedPlugins.add(packageNameWithScope);
216-
}
217-
}
218-
}
219-
} catch {
220-
// Skip packages that can't be read
221-
}
222-
} else {
223-
// Regular package
224-
const packagePath = join(nodeModulesPath, entry);
225-
if (hasPluginFile(packagePath)) {
226-
installedPlugins.add(entry);
227-
}
228-
}
229-
}
230-
} catch (error) {
231-
logger.debug('Error scanning global plugins directory:', error);
232-
}
233-
}
278+
const installedPlugins = scanInstalledPlugins(nodeModulesPath);
234279

235280
// Build unified list with status
236281
const plugins: Array<{Name: string, Status: string, Source: string, 'Installed At': string}> = [];

0 commit comments

Comments
 (0)