Skip to content

Commit 392016b

Browse files
Add package source kind to scanner results (#171)
* enhance package handling with kind and stats properties * changeset
1 parent de15caf commit 392016b

10 files changed

Lines changed: 108 additions & 8 deletions

File tree

.changeset/cuddly-games-cheer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
Adds package source-kind metadata to scanner results, tightens scan stats to match the runtime contract, and adds focused markdown destination rewrite coverage while preserving the current name-based allowlist behavior.

packages/intent/src/core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export function listIntentSkills(
147147
warningCount: result.warnings.length,
148148
noticeCount: result.notices.length,
149149
conflictCount: result.conflicts.length,
150-
scan: scan.stats ?? fsCache.getStats(),
150+
scan: scan.stats,
151151
}
152152
}
153153

@@ -344,7 +344,7 @@ function resolveIntentSkillInCwd(
344344
excludes: excludePatterns,
345345
resolution: 'full-scan',
346346
resolved,
347-
scan: scanResult.stats ?? fsCache.getStats(),
347+
scan: scanResult.stats,
348348
scope,
349349
})
350350
: undefined,

packages/intent/src/discovery/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface CreatePackageRegistrarOptions {
2323
deriveIntentConfig: (pkgJson: PackageJson) => IntentConfig | null
2424
discoverSkills: (skillsDir: string, packageName: string) => Array<SkillEntry>
2525
getPackageDepth: (packageRoot: string, projectRoot: string) => number
26+
getPackageKind: (packageRoot: string) => IntentPackage['kind']
2627
packageIndexes: Map<string, number>
2728
packages: Array<IntentPackage>
2829
projectRoot: string
@@ -120,6 +121,7 @@ export function createPackageRegistrar(opts: CreatePackageRegistrarOptions) {
120121
intent,
121122
skills,
122123
packageRoot: dirPath,
124+
kind: opts.getPackageKind(dirPath),
123125
source,
124126
}
125127
const existingIndex = opts.packageIndexes.get(name)

packages/intent/src/scanner.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
} from './utils.js'
2020
import { createIntentFsCache } from './fs-cache.js'
2121
import { detectPackageManager } from './package-manager.js'
22-
import { findWorkspaceRoot } from './workspace-patterns.js'
22+
import {
23+
findWorkspacePackages,
24+
findWorkspaceRoot,
25+
} from './workspace-patterns.js'
2326
import type { IntentFsCache } from './fs-cache.js'
2427
import type { ReadFs } from './utils.js'
2528
import type {
@@ -451,6 +454,28 @@ function getScanScope(options: ScanOptions): ScanScope {
451454
return options.scope ?? (options.includeGlobal ? 'local-and-global' : 'local')
452455
}
453456

457+
function createWorkspacePackageKeySet(
458+
workspaceRoot: string | null,
459+
getFsIdentity: (path: string) => string,
460+
): Set<string> {
461+
if (!workspaceRoot) return new Set()
462+
463+
return new Set(
464+
findWorkspacePackages(workspaceRoot).map((dir) => getFsIdentity(dir)),
465+
)
466+
}
467+
468+
function createPackageKindResolver(
469+
workspacePackageKeys: Set<string>,
470+
getFsIdentity: (path: string) => string,
471+
): (packageRoot: string) => IntentPackage['kind'] {
472+
return (packageRoot: string): IntentPackage['kind'] => {
473+
return workspacePackageKeys.has(getFsIdentity(packageRoot))
474+
? 'workspace'
475+
: 'npm'
476+
}
477+
}
478+
454479
export function scanForIntents(
455480
root?: string,
456481
options: ScanOptions = {},
@@ -495,6 +520,11 @@ export function scanForIntents(
495520
>()
496521
let pnpApi: PnpApi | null | undefined
497522

523+
const getPackageKind = createPackageKindResolver(
524+
createWorkspacePackageKeySet(workspaceRoot, fsCache.getFsIdentity),
525+
fsCache.getFsIdentity,
526+
)
527+
498528
function getPnpApi(): PnpApi | null {
499529
if (scanScope === 'global') return null
500530
if (pnpApi === undefined) {
@@ -545,6 +575,7 @@ export function scanForIntents(
545575
deriveIntentConfig,
546576
discoverSkills: (skillsDir) => discoverSkills(skillsDir, fsCache),
547577
getPackageDepth,
578+
getPackageKind,
548579
getFsIdentity: fsCache.getFsIdentity,
549580
exists: fsCache.exists,
550581
packageIndexes,
@@ -726,6 +757,13 @@ export function scanIntentPackageAtRoot(
726757
const warnings: Array<string> = []
727758
const packageIndexes = new Map<string, number>()
728759
const fsCache = options.fsCache ?? createIntentFsCache()
760+
const getPackageKind = createPackageKindResolver(
761+
createWorkspacePackageKeySet(
762+
findWorkspaceRoot(projectRoot),
763+
fsCache.getFsIdentity,
764+
),
765+
fsCache.getFsIdentity,
766+
)
729767

730768
function readPkgJson(dirPath: string): Record<string, unknown> | null {
731769
return fsCache.readPackageJson(dirPath)
@@ -744,6 +782,7 @@ export function scanIntentPackageAtRoot(
744782
)
745783
: (skillsDir) => discoverSkills(skillsDir, fsCache),
746784
getPackageDepth,
785+
getPackageKind,
747786
getFsIdentity: fsCache.getFsIdentity,
748787
exists: fsCache.exists,
749788
packageIndexes,

packages/intent/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface ScanResult {
2323
local: NodeModulesScanTarget
2424
global: NodeModulesScanTarget
2525
}
26-
stats?: ScanStats
26+
stats: ScanStats
2727
}
2828

2929
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown'
@@ -54,6 +54,7 @@ export interface IntentPackage {
5454
intent: IntentConfig
5555
skills: Array<SkillEntry>
5656
packageRoot: string
57+
kind: 'npm' | 'workspace'
5758
source: 'local' | 'global'
5859
}
5960

packages/intent/tests/install-writer.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function pkg(overrides: Partial<IntentPackage>): IntentPackage {
4747
intent: { version: 1, repo: 'test/pkg', docs: 'docs/' },
4848
skills: [],
4949
packageRoot: 'node_modules/pkg',
50+
kind: 'npm',
5051
source: 'local',
5152
...overrides,
5253
}
@@ -73,6 +74,10 @@ function scanResult(packages: Array<IntentPackage>): ScanResult {
7374
scanned: false,
7475
},
7576
},
77+
stats: {
78+
packageJsonCacheHits: 0,
79+
packageJsonReadCount: 0,
80+
},
7681
}
7782
}
7883

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { join } from 'node:path'
2+
import { describe, expect, it } from 'vitest'
3+
import { rewriteLoadedSkillMarkdownDestinations } from '../src/core/markdown.js'
4+
5+
const cwd = '/repo'
6+
const packageRoot = join(cwd, 'node_modules', 'pkg')
7+
const skillFilePath = join(packageRoot, 'skills', 'core', 'SKILL.md')
8+
9+
function rewrite(content: string): string {
10+
return rewriteLoadedSkillMarkdownDestinations({
11+
content,
12+
cwd,
13+
packageRoot,
14+
skillFilePath,
15+
})
16+
}
17+
18+
describe('rewriteLoadedSkillMarkdownDestinations', () => {
19+
it('rewrites nested-label links while preserving query and hash suffixes', () => {
20+
expect(rewrite('[API [v1]](docs/api.md?raw=1#setup)')).toBe(
21+
'[API [v1]](node_modules/pkg/skills/core/docs/api.md?raw=1#setup)',
22+
)
23+
})
24+
25+
it('rewrites image destinations with escaped closing parens', () => {
26+
expect(rewrite('![Diagram](assets/flow\\).png)')).toBe(
27+
'![Diagram](node_modules/pkg/skills/core/assets/flow\\).png)',
28+
)
29+
})
30+
31+
it('preserves malformed inline links', () => {
32+
expect(rewrite('[Broken](docs/api.md')).toBe('[Broken](docs/api.md')
33+
})
34+
35+
it('does not rewrite links in fenced code blocks', () => {
36+
expect(rewrite('~~~md\n[Keep](docs/api.md)\n~~~')).toBe(
37+
'~~~md\n[Keep](docs/api.md)\n~~~',
38+
)
39+
})
40+
})

packages/intent/tests/resolver.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ function intentPackage(
2929
},
3030
packageRoot: `node_modules/${overrides.name}`,
3131
skills: [skill('core')],
32+
kind: 'npm',
3233
source: 'local',
3334
version: '1.0.0',
3435
...overrides,
@@ -64,6 +65,10 @@ function scanResult(
6465
},
6566
packageManager: 'npm',
6667
packages,
68+
stats: {
69+
packageJsonCacheHits: 0,
70+
packageJsonReadCount: 0,
71+
},
6772
warnings,
6873
}
6974
}

packages/intent/tests/scanner.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ describe('scanForIntents', () => {
115115
const result = scanForIntents(root)
116116
expect(result.packages).toHaveLength(1)
117117
expect(result.packages[0]!.name).toBe('@tanstack/db')
118+
expect(result.packages[0]!.kind).toBe('npm')
118119
expect(result.packages[0]!.version).toBe('0.5.2')
119120
expect(result.packages[0]!.packageRoot).toBe(pkgDir)
120121
expect(result.packages[0]!.skills).toHaveLength(1)
@@ -128,7 +129,7 @@ describe('scanForIntents', () => {
128129
packageJsonCacheHits: expect.any(Number),
129130
}),
130131
)
131-
expect(result.stats!.packageJsonReadCount).toBeGreaterThan(0)
132+
expect(result.stats.packageJsonReadCount).toBeGreaterThan(0)
132133
})
133134

134135
it('does not throw when skills exists but is not a directory', () => {
@@ -1556,7 +1557,7 @@ describe('scanForIntents', () => {
15561557
'@tanstack/query',
15571558
'@tanstack/store',
15581559
])
1559-
expect(result.stats!.packageJsonReadCount).toBeLessThan(10)
1560+
expect(result.stats.packageJsonReadCount).toBeLessThan(10)
15601561
})
15611562

15621563
it('does not crawl package source trees during nested node_modules discovery', () => {
@@ -1591,7 +1592,7 @@ describe('scanForIntents', () => {
15911592
const result = scanForIntents(root)
15921593

15931594
expect(result.packages).toEqual([])
1594-
expect(result.stats!.packageJsonReadCount).toBeLessThan(4)
1595+
expect(result.stats.packageJsonReadCount).toBeLessThan(4)
15951596
})
15961597

15971598
it('dedupes recursive workspace symlink paths by real package identity', () => {
@@ -1639,7 +1640,8 @@ describe('scanForIntents', () => {
16391640

16401641
expect(result.packages).toHaveLength(1)
16411642
expect(result.packages[0]!.name).toBe('b')
1642-
expect(result.stats!.packageJsonReadCount).toBeLessThan(10)
1643+
expect(result.packages[0]!.kind).toBe('workspace')
1644+
expect(result.stats.packageJsonReadCount).toBeLessThan(10)
16431645
})
16441646

16451647
it('prefers valid semver versions over invalid ones at the same depth', () => {

packages/intent/tests/source-policy.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function pkg(name: string, skillNames: Array<string>): IntentPackage {
3232
intent: { version: 1, repo: 'owner/repo', docs: '' },
3333
skills: skillNames.map(skill),
3434
packageRoot: `/root/node_modules/${name}`,
35+
kind: 'npm',
3536
source: 'local',
3637
}
3738
}

0 commit comments

Comments
 (0)