110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import { defineCommand } from 'citty'
|
||
|
||
import type { AnalyzeClustersResult } from '@open-pencil/core/rpc'
|
||
import { calcClusterConfidence } from '@open-pencil/core/tools'
|
||
|
||
import { appTargetOptions } from '#cli/app-target'
|
||
import { bold, fmtList, fmtSummary } from '#cli/format'
|
||
import { loadRpcData } from '#cli/rpc-data'
|
||
|
||
function formatSignature(sig: string): string {
|
||
const [typeSize, children] = sig.split('|')
|
||
const type = typeSize.split(':')[0]
|
||
if (!type) return sig
|
||
const typeName = type.charAt(0) + type.slice(1).toLowerCase()
|
||
if (!children) return typeName
|
||
|
||
const childParts = children.split(',').map((c) => {
|
||
const [t, count] = c.split(':')
|
||
if (!t) return ''
|
||
const name = t.charAt(0) + t.slice(1).toLowerCase()
|
||
return Number(count) > 1 ? `${name}×${count}` : name
|
||
})
|
||
|
||
return `${typeName} > [${childParts.join(', ')}]`
|
||
}
|
||
|
||
export default defineCommand({
|
||
meta: { description: 'Find repeated design patterns (potential components)' },
|
||
args: {
|
||
file: {
|
||
type: 'positional',
|
||
description: '.fig file path (omit to connect to running app)',
|
||
required: false
|
||
},
|
||
limit: { type: 'string', description: 'Max clusters to show', default: '20' },
|
||
'min-size': { type: 'string', description: 'Min node size in px', default: '30' },
|
||
'min-count': { type: 'string', description: 'Min instances to form cluster', default: '2' },
|
||
...appTargetOptions,
|
||
json: { type: 'boolean', description: 'Output as JSON' }
|
||
},
|
||
async run({ args }) {
|
||
const data = await loadRpcData<AnalyzeClustersResult>(
|
||
args.file,
|
||
'analyze_clusters',
|
||
{
|
||
limit: Number(args.limit),
|
||
minSize: Number(args['min-size']),
|
||
minCount: Number(args['min-count'])
|
||
},
|
||
args
|
||
)
|
||
|
||
if (args.json) {
|
||
console.log(JSON.stringify(data, null, 2))
|
||
return
|
||
}
|
||
|
||
if (data.clusters.length === 0) {
|
||
console.log('No repeated patterns found.')
|
||
return
|
||
}
|
||
|
||
console.log('')
|
||
console.log(bold(' Repeated patterns'))
|
||
console.log('')
|
||
|
||
const items = data.clusters.map((c) => {
|
||
const first = c.nodes[0]
|
||
const confidence = calcClusterConfidence(c.nodes)
|
||
|
||
const widths = c.nodes.map((n) => n.width)
|
||
const heights = c.nodes.map((n) => n.height)
|
||
const wRange = Math.max(...widths) - Math.min(...widths)
|
||
const hRange = Math.max(...heights) - Math.min(...heights)
|
||
const avgW = Math.round(widths.reduce((a, b) => a + b, 0) / widths.length)
|
||
const avgH = Math.round(heights.reduce((a, b) => a + b, 0) / heights.length)
|
||
|
||
const sizeStr =
|
||
wRange <= 4 && hRange <= 4
|
||
? `${avgW}×${avgH}`
|
||
: `${avgW}×${avgH} (±${Math.max(wRange, hRange)}px)`
|
||
|
||
return {
|
||
header: `${c.nodes.length}× ${first.type.toLowerCase()} "${first.name}" (${confidence}% match)`,
|
||
details: {
|
||
size: sizeStr,
|
||
structure: formatSignature(c.signature),
|
||
examples: c.nodes
|
||
.slice(0, 3)
|
||
.map((n) => n.id)
|
||
.join(', ')
|
||
}
|
||
}
|
||
})
|
||
|
||
console.log(fmtList(items, { numbered: true }))
|
||
|
||
const clusteredNodes = data.clusters.reduce((sum, c) => sum + c.nodes.length, 0)
|
||
console.log('')
|
||
console.log(
|
||
fmtSummary({
|
||
clusters: data.clusters.length,
|
||
'total nodes': data.totalNodes,
|
||
clustered: clusteredNodes
|
||
})
|
||
)
|
||
console.log('')
|
||
}
|
||
})
|