Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/commands/author.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ export function detectMonorepoPackages(cwd: string): MonorepoPackage[] | null {
if (!existsSync(scanDir))
continue

const directResult = readPackageJsonSafe(join(scanDir, 'package.json'))
if (directResult) {
const directPkg = directResult.parsed as Record<string, any>
if (!directPkg.private && directPkg.name) {
const repoUrl = typeof directPkg.repository === 'string'
? directPkg.repository
: directPkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '')

packages.push({
name: directPkg.name,
version: directPkg.version || '0.0.0',
description: directPkg.description,
repoUrl,
dir: scanDir,
})
continue
}
}

Comment on lines +105 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard direct package detection to non-glob workspace entries.

Line 105 runs direct scanDir/package.json detection for every workspace pattern. For glob patterns (e.g., packages/*), if packages/package.json exists, Line 120 continues and child packages are skipped.

This can cause detectMonorepoPackages() to miss valid workspace packages.

Proposed fix
 for (const pattern of patterns) {
   // Expand simple glob: "packages/*" → scan packages/*/package.json
   const base = pattern.replace(/\/?\*+$/, '')
   const scanDir = resolve(cwd, base)
+  const isGlobPattern = pattern.includes('*')
   if (!existsSync(scanDir))
     continue

-  const directResult = readPackageJsonSafe(join(scanDir, 'package.json'))
-  if (directResult) {
-    const directPkg = directResult.parsed as Record<string, any>
-    if (!directPkg.private && directPkg.name) {
-      const repoUrl = typeof directPkg.repository === 'string'
-        ? directPkg.repository
-        : directPkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '')
-
-      packages.push({
-        name: directPkg.name,
-        version: directPkg.version || '0.0.0',
-        description: directPkg.description,
-        repoUrl,
-        dir: scanDir,
-      })
-      continue
-    }
-  }
+  if (!isGlobPattern) {
+    const directResult = readPackageJsonSafe(join(scanDir, 'package.json'))
+    if (directResult) {
+      const directPkg = directResult.parsed as Record<string, any>
+      if (!directPkg.private && directPkg.name) {
+        const repoUrl = typeof directPkg.repository === 'string'
+          ? directPkg.repository
+          : directPkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '')
+
+        packages.push({
+          name: directPkg.name,
+          version: directPkg.version || '0.0.0',
+          description: directPkg.description,
+          repoUrl,
+          dir: scanDir,
+        })
+        continue
+      }
+    }
+  }

   for (const entry of readdirSync(scanDir, { withFileTypes: true })) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const directResult = readPackageJsonSafe(join(scanDir, 'package.json'))
if (directResult) {
const directPkg = directResult.parsed as Record<string, any>
if (!directPkg.private && directPkg.name) {
const repoUrl = typeof directPkg.repository === 'string'
? directPkg.repository
: directPkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '')
packages.push({
name: directPkg.name,
version: directPkg.version || '0.0.0',
description: directPkg.description,
repoUrl,
dir: scanDir,
})
continue
}
}
const scanDir = resolve(cwd, base)
const isGlobPattern = pattern.includes('*')
if (!existsSync(scanDir))
continue
if (!isGlobPattern) {
const directResult = readPackageJsonSafe(join(scanDir, 'package.json'))
if (directResult) {
const directPkg = directResult.parsed as Record<string, any>
if (!directPkg.private && directPkg.name) {
const repoUrl = typeof directPkg.repository === 'string'
? directPkg.repository
: directPkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '')
packages.push({
name: directPkg.name,
version: directPkg.version || '0.0.0',
description: directPkg.description,
repoUrl,
dir: scanDir,
})
continue
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/author.ts` around lines 105 - 123, The direct package.json
detection inside detectMonorepoPackages incorrectly runs for glob workspace
entries and causes parent dirs like "packages" to short-circuit child package
discovery; before calling readPackageJsonSafe(scanDir + '/package.json') add a
guard that skips the directResult check when the original workspace pattern
contains glob characters (e.g., '*' '?' or '[') — use the workspace pattern
variable used to derive scanDir (or keep the pattern in scope) and only perform
the directPkg logic (the directResult/directPkg branch that pushes to packages
and continues) when the pattern is a non-glob literal; this preserves the
existing behavior for literal workspace entries while preventing glob parents
from swallowing child packages.

for (const entry of readdirSync(scanDir, { withFileTypes: true })) {
if (!entry.isDirectory())
continue
Expand Down
36 changes: 36 additions & 0 deletions test/unit/author.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,42 @@ describe('author', () => {
expect(result![0].name).toBe('lib-a')
})

it('detects workspace entries that point to a package directory directly', async () => {
const { existsSync, readFileSync, readdirSync } = await import('node:fs')

vi.mocked(existsSync).mockImplementation((p: any) => {
const s = String(p)
if (s === '/project/package.json')
return true
if (s === '/project/packages/foo')
return true
if (s === '/project/packages/foo/package.json')
return true
return false
})

vi.mocked(readFileSync).mockImplementation((p: any) => {
const s = String(p)
if (s === '/project/package.json')
return JSON.stringify({ private: true, workspaces: ['packages/foo'] })
if (s === '/project/packages/foo/package.json')
return JSON.stringify({ name: 'foo', version: '1.2.3' })
return ''
})

vi.mocked(readdirSync).mockReturnValue([])

const { detectMonorepoPackages } = await import('../../src/commands/author')
const result = detectMonorepoPackages('/project')

expect(result).toHaveLength(1)
expect(result![0]).toMatchObject({
name: 'foo',
version: '1.2.3',
dir: '/project/packages/foo',
})
})

it('resolves repository URL from object form', async () => {
const { existsSync, readFileSync, readdirSync } = await import('node:fs')

Expand Down
Loading