aihand 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -2
- package/dist/chunk-2NTK7H4W.js +10 -0
- package/dist/chunk-3X4FTHLC.cjs +369 -0
- package/dist/chunk-BXVNR4E2.js +399 -0
- package/dist/chunk-C7DGE6MY.cjs +1456 -0
- package/dist/chunk-DUUCVLC3.cjs +254 -0
- package/dist/chunk-FAHI53KO.cjs +125 -0
- package/dist/chunk-G7KVJ7NF.js +369 -0
- package/dist/chunk-GNEUSRGP.js +52 -0
- package/dist/chunk-IGNEAOLT.cjs +130 -0
- package/dist/chunk-IS5XFUDB.js +125 -0
- package/dist/chunk-JLYC76XL.js +2448 -0
- package/dist/chunk-KQOABC2O.cjs +52 -0
- package/dist/chunk-OVMK33AC.cjs +104 -0
- package/dist/chunk-OWYK2IGV.js +250 -0
- package/dist/chunk-PQSQN4CN.js +126 -0
- package/dist/chunk-QF6AG3M5.cjs +410 -0
- package/dist/chunk-QSAMLXML.js +1456 -0
- package/dist/chunk-VEKYRKPF.cjs +399 -0
- package/dist/chunk-Y6H7W7PI.cjs +2451 -0
- package/dist/chunk-YKSYW77R.js +410 -0
- package/dist/chunk-Z2Y65YOY.cjs +7 -0
- package/dist/chunk-ZJQRNIK7.js +104 -0
- package/dist/cli-FDS2C2CZ.cjs +651 -0
- package/dist/cli-HHRGYPSM.js +649 -0
- package/dist/cli-JQEIE7RQ.js +120 -0
- package/dist/cli-K3OS2QQH.cjs +122 -0
- package/dist/cli-OSYG6LJD.cjs +89 -0
- package/dist/cli-TXRW5PG6.js +89 -0
- package/dist/cli.cjs +81 -0
- package/dist/cli.js +81 -0
- package/dist/config-5KEQLN6L.cjs +13 -0
- package/dist/config-PJPYKDLQ.js +13 -0
- package/dist/graph-IH56SCPK.js +8 -0
- package/dist/graph-ZUXXCJ5A.cjs +8 -0
- package/dist/index.cjs +481 -0
- package/dist/index.d.cts +461 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +479 -0
- package/dist/locate-5XFSXJ5J.cjs +15 -0
- package/dist/locate-NKSUGL3A.js +15 -0
- package/dist/refactor-5FWSZIBN.cjs +19 -0
- package/dist/refactor-BOB3SZSA.js +19 -0
- package/dist/scan-4R7GQG2W.cjs +9 -0
- package/dist/scan-VF54GAAX.js +9 -0
- package/dist/ui/probe/server.cjs +505 -0
- package/dist/ui/probe/server.js +507 -0
- package/dist/vite.cjs +12 -0
- package/dist/vite.d.cts +12 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +12 -0
- package/package.json +82 -9
- package/src/cli.ts +107 -0
- package/src/index.ts +54 -0
- package/src/read/cli.ts +650 -0
- package/src/read/compact.ts +286 -0
- package/src/read/config.ts +62 -0
- package/src/read/graph.ts +182 -0
- package/src/read/index.ts +12 -0
- package/src/read/inject.ts +121 -0
- package/src/read/locate.ts +104 -0
- package/src/read/panel.ts +335 -0
- package/src/read/pipeline.ts +78 -0
- package/src/read/refactor.ts +576 -0
- package/src/read/render.ts +1118 -0
- package/src/read/scan.ts +61 -0
- package/src/read/seam.ts +0 -0
- package/src/read/security.ts +171 -0
- package/src/read/signals.ts +333 -0
- package/src/read/state.ts +71 -0
- package/src/read/stategraph.ts +205 -0
- package/src/read/types.ts +162 -0
- package/src/read/vite.ts +77 -0
- package/src/ui/babel/line-profiler.ts +197 -0
- package/src/ui/babel/source-loc.ts +68 -0
- package/src/ui/bridge/cdp-bridge.ts +138 -0
- package/src/ui/bridge/compile-probe.ts +80 -0
- package/src/ui/bridge/transport.ts +26 -0
- package/src/ui/bridge/vite-bridge.ts +116 -0
- package/src/ui/client/client-patch.ts +899 -0
- package/src/ui/client/client.ts +2562 -0
- package/src/ui/core/action.ts +747 -0
- package/src/ui/core/candidates.ts +348 -0
- package/src/ui/core/canvas.ts +305 -0
- package/src/ui/core/check.ts +34 -0
- package/src/ui/core/compact.ts +314 -0
- package/src/ui/core/detail.ts +244 -0
- package/src/ui/core/diff.ts +253 -0
- package/src/ui/core/emit.ts +198 -0
- package/src/ui/core/knob-exec.ts +137 -0
- package/src/ui/core/perf.ts +254 -0
- package/src/ui/core/types.ts +164 -0
- package/src/ui/core/util.ts +221 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/probe/cli.ts +139 -0
- package/src/ui/probe/server.ts +468 -0
- package/src/ui/self/act.ts +47 -0
- package/src/ui/self/discover.ts +101 -0
- package/src/ui/self/grow.ts +121 -0
- package/src/ui/self/install.ts +100 -0
- package/src/ui/self/probe.ts +105 -0
- package/src/ui/self/screen-hook.ts +44 -0
- package/src/ui/self/self.ts +48 -0
- package/src/ui/self/store-refs.ts +123 -0
- package/src/ui/self/store-schema.ts +65 -0
- package/src/ui/self/synth.ts +37 -0
- package/src/ui/server/cli.ts +102 -0
- package/src/ui/server/dispatch.ts +276 -0
- package/src/ui/server/help-text.ts +237 -0
- package/src/ui/server/knob-schema.ts +87 -0
- package/src/ui/server/plugin.ts +1151 -0
- package/src/vite.ts +39 -0
- package/index.js +0 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// 神经图引擎 —— 用 ts-morph 的类型检查器把 App 的「状态空间」从全量引用流里长出来。
|
|
2
|
+
//
|
|
3
|
+
// 坍缩两处独立的 `const X = create({...})` 词法硬编码(state.ts 的 CREATE_QUERY、grow.ts
|
|
4
|
+
// 的 callee.text==='create')。它俩本质是同一真理(状态空间)的两个投影,却退化成两份匹配器,
|
|
5
|
+
// 换工厂名/换 import 别名/store 挪窝两边各自静默漏。这里一处取代,且回答了局部匹配答不出的
|
|
6
|
+
// 问题:**哪些是控制大局的中心状态点**——从引用流的位置(中心性)涌现,不是看节点长什么样。
|
|
7
|
+
//
|
|
8
|
+
// 引擎离线:跑在 `aihand read` / build 时,绝不进浏览器、绝不进 per-HMR 热路径(ts-morph 拖
|
|
9
|
+
// 整个 tsconfig Project,冷启 ~2s)。grow 的 tree-sitter 路径作浏览器零-AST 兜底保留。
|
|
10
|
+
//
|
|
11
|
+
// 节点分类(两条正交静态信号,零正则):module-level VariableDeclaration —
|
|
12
|
+
// ① 初值 CallExpression ∧ 绑定类型含响应式 marker 字段 → STATE(store)
|
|
13
|
+
// ② 初值 arrow/fn/class → VIEW(纯函数/组件,排除;Button/Dialog/Card 落这里)
|
|
14
|
+
// 中心性只在 STATE 子图上算:findReferencesAsNodes → 引用次数 + 跨文件辐射,rank 降序。
|
|
15
|
+
//
|
|
16
|
+
// 信号① 是【构造无关】判据(给整个前端世界用的兑现):身份来自返回类型的【结构特征】——
|
|
17
|
+
// 工厂调用造出的对象带一个框架注入的响应式指纹字段(MobX 本仓 `_loading`、zustand `getState`、
|
|
18
|
+
// valtio 的 proxy symbol)——不来自工厂【名字】。`v.getType().getProperty(marker)` 命中即 store,
|
|
19
|
+
// 不看 callee 叫 create / createStore / defineStore / 任意名,换框架/换别名/挪窝全不漏。
|
|
20
|
+
// marker 默认 `_loading`(本仓 lib/mobx.ts 约定),由 config.runtime.storeMarker 覆盖。
|
|
21
|
+
//
|
|
22
|
+
// 三条被证伪的弯路(实跑本仓 284 文件 ts-morph 探针,化石留此免重走):
|
|
23
|
+
// · callee 名 ==='create':局部名字匹配,换框架瞎——这是本判据取代掉的旧实现。
|
|
24
|
+
// · 读写图 writes-count(被多处写=状态枢纽):既漏(topicStore/bot.data 等 5 个 getter-only
|
|
25
|
+
// 派生 store 外部 writes=0)又纳(dbPromise/syncing 等 6 个私有缓存 writes≥2)。写不写是
|
|
26
|
+
// 使用模式不是身份。
|
|
27
|
+
// · 中心性硬阈值 files≥N:topicStore 跨 20 文件 vs bot.data 仅 1 文件,与 defineSkill/
|
|
28
|
+
// createBrowserRouter 同辐射层,纯图位置切不开真 store 与另类工厂。中心性只配作排序,非身份。
|
|
29
|
+
//
|
|
30
|
+
// 曾有第三信号「let/var → state」实跑全仓证伪砍掉:抓 17 个私有缓存/标志 0 个真 store。
|
|
31
|
+
|
|
32
|
+
import type { Node, Project, VariableDeclaration } from 'ts-morph'
|
|
33
|
+
import type { Field, Grown } from '../ui/self/grow.js'
|
|
34
|
+
import { existsSync } from 'node:fs'
|
|
35
|
+
import { resolve } from 'node:path'
|
|
36
|
+
import process from 'node:process'
|
|
37
|
+
import { Node as TsNode, SyntaxKind } from 'ts-morph'
|
|
38
|
+
|
|
39
|
+
// 一个状态节点 = 它的静态结构(Grown) + 在状态子图里的中心性度量。
|
|
40
|
+
export interface StateNode extends Grown {
|
|
41
|
+
refs: number // findReferencesAsNodes 总引用数(引用密度)
|
|
42
|
+
files: number // 引用去重源文件数(跨文件辐射)
|
|
43
|
+
rank: number // refs + files*3 —— 辐射加权,实跑足以让 store 置顶、组件出局
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 镜像 refactor.ts 的 openProject:带 compilerOptions.paths + 真 include 的 tsconfig。
|
|
47
|
+
// solution-style 根 tsconfig(files:[] + references)没有源图,优先 app 那份。
|
|
48
|
+
export async function openProject(cwd: string = process.cwd()): Promise<Project> {
|
|
49
|
+
const { Project } = await import('ts-morph')
|
|
50
|
+
const tsConfigFilePath = ['tsconfig.app.json', 'tsconfig.json'].map(f => resolve(cwd, f)).find(existsSync)
|
|
51
|
+
// 抛错而非 process.exit:openProject 是导出库函数,被 discover() 在 vite 插件(storeSchema.load)
|
|
52
|
+
// 里调——process.exit 会静默杀掉用户 dev server,连 catch 机会都没有(违诊断纤维律:把可恢复的
|
|
53
|
+
// 配置缺失坍缩成杀进程死局)。抛 Error 让调用者(CLI 入口 / vite)各自决定如何报。
|
|
54
|
+
if (!tsConfigFilePath)
|
|
55
|
+
throw new Error(`stategraph: no tsconfig.json (or tsconfig.app.json) found under ${cwd}`)
|
|
56
|
+
return new Project({ tsConfigFilePath })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 只看 module-level(变量语句的直接父是 SourceFile)——局部变量不是 App 的状态空间。
|
|
60
|
+
function isModuleLevel(v: VariableDeclaration): boolean {
|
|
61
|
+
const stmt = v.getVariableStatement()
|
|
62
|
+
return !!stmt && stmt.getParent()?.getKind() === SyntaxKind.SourceFile
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEFAULT_STORE_MARKER = '_loading'
|
|
66
|
+
|
|
67
|
+
// 构造无关的 store 判据:绑定 v 的初值是工厂【调用】,且 v 的【类型】含响应式指纹字段 marker。
|
|
68
|
+
// 不看 callee 名——身份来自返回类型的结构特征(框架注入的 `_loading`/`getState`/…),换工厂名/
|
|
69
|
+
// 换 import 别名/挪窝全命中。判据要 v 的类型而非 init 本身:`create(state, opts)` 的 init 是
|
|
70
|
+
// CallExpression,其【返回类型】(即 v 的类型)才带 marker。getProperty 走 checker,解推断/交叉类型。
|
|
71
|
+
function isStateFactory(v: VariableDeclaration, marker: string): boolean {
|
|
72
|
+
const init = v.getInitializer()
|
|
73
|
+
if (!init || !TsNode.isCallExpression(init))
|
|
74
|
+
return false
|
|
75
|
+
return !!v.getType().getProperty(marker)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 两信号分类。null = 既非状态也非视图(普通 const 数据/re-export/hook/let 缓存等),不进任何子图。
|
|
79
|
+
export function classify(v: VariableDeclaration, marker: string = DEFAULT_STORE_MARKER): 'state' | 'view' | null {
|
|
80
|
+
const init = v.getInitializer()
|
|
81
|
+
if (!init)
|
|
82
|
+
return null
|
|
83
|
+
const k = init.getKind()
|
|
84
|
+
if (k === SyntaxKind.ArrowFunction || k === SyntaxKind.FunctionExpression || k === SyntaxKind.ClassExpression)
|
|
85
|
+
return 'view' // 纯函数/组件
|
|
86
|
+
if (isStateFactory(v, marker))
|
|
87
|
+
return 'state' // 工厂调用造出带响应式 marker 的对象 = store
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// store 的一个状态字段从 create 第一个对象字面量长出的全部静态信息。两个投影各取所需:
|
|
92
|
+
// 运行时 Grown.Field 取 {name,type,domain,line}; 读面板 StoreState 取 {name,init,domain}。
|
|
93
|
+
// 单一提取点,取代 grow.typeOf(as_expression 文本走法) + state.domainOf(union 递归摊平)。
|
|
94
|
+
export interface StoreFieldRaw {
|
|
95
|
+
name: string
|
|
96
|
+
init: string // 初值原文('false' / '0' / "'chat'");`x as T` 取 x 那侧
|
|
97
|
+
type: string // checker 解出的类型文本('boolean' / 'string | null' / "'chat' | 'im'")
|
|
98
|
+
domain: string[] // 可枚举值域:字符串字面量联合的成员;非枚举 → []
|
|
99
|
+
line: number // 1-based 声明行
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 一个属性赋值 → StoreFieldRaw。domain/type 走 type checker:严格强于文本走法
|
|
103
|
+
// (解推断/别名 union、解被截断 union、TS 子类型吸收 `'foo'|string`→string、统一 `as T` 与 `let x:'a'`)。
|
|
104
|
+
function fieldOf(pa: Node): StoreFieldRaw | null {
|
|
105
|
+
if (!TsNode.isPropertyAssignment(pa))
|
|
106
|
+
return null // 跳过展开/方法/简写等非数据项
|
|
107
|
+
const valueInit = pa.getInitializer()
|
|
108
|
+
if (!valueInit)
|
|
109
|
+
return null
|
|
110
|
+
// 初值原文:`x as T` 取 as 左侧的值表达式,否则整个初值文本。
|
|
111
|
+
const init = TsNode.isAsExpression(valueInit) ? valueInit.getExpression().getText() : valueInit.getText()
|
|
112
|
+
const t = valueInit.getType()
|
|
113
|
+
// 值域 = 字符串字面量「联合」(≥2 档,可枚举的离散态机)。单个裸字面量(error='')不是值域 ——
|
|
114
|
+
// 它只是初值,TS 把它窄化成单字面量类型,语义上仍是普通 string。只认 union(与旧 domainOf 同)。
|
|
115
|
+
const domain = t.isUnion()
|
|
116
|
+
? t.getUnionTypes().filter(u => u.isStringLiteral()).map(u => u.getLiteralValue() as string)
|
|
117
|
+
: []
|
|
118
|
+
return { name: pa.getName(), init, type: t.getText(valueInit), domain, line: pa.getStartLineNumber() }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// create 的第一个参数解成对象字面量。内联 `create({...})` 直接是;`create(state, …)`
|
|
122
|
+
// (state 是变量,如 schedulerStore)经 checker 跟到符号的 value declaration 的初值 ——
|
|
123
|
+
// 这正是引擎比 tree-sitter 强的兑现点(后者只会找内联 object,标识符引用全漏)。
|
|
124
|
+
function resolveStateObject(arg: Node): Node | null {
|
|
125
|
+
if (TsNode.isObjectLiteralExpression(arg))
|
|
126
|
+
return arg
|
|
127
|
+
if (TsNode.isIdentifier(arg)) {
|
|
128
|
+
const decl = arg.getSymbol()?.getDeclarations()?.[0]
|
|
129
|
+
if (decl && TsNode.isVariableDeclaration(decl)) {
|
|
130
|
+
const di = decl.getInitializer()
|
|
131
|
+
if (di && TsNode.isObjectLiteralExpression(di))
|
|
132
|
+
return di
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 一个被分类为 state 的 store 声明 → 它暴露的字段全集。
|
|
139
|
+
// 字段从工厂第一个对象字面量参数长(marker 是工厂注入的、不在源对象里,故天然不进 fields)。
|
|
140
|
+
export function extractStoreFields(v: VariableDeclaration, marker: string = DEFAULT_STORE_MARKER): StoreFieldRaw[] {
|
|
141
|
+
if (!isStateFactory(v, marker))
|
|
142
|
+
return [] // 非 store(无响应式 marker)没有可投影的状态字段
|
|
143
|
+
const init = v.getInitializer() as Node // isStateFactory 已确认是 CallExpression
|
|
144
|
+
if (!TsNode.isCallExpression(init))
|
|
145
|
+
return []
|
|
146
|
+
const arg = init.getArguments()[0]
|
|
147
|
+
const stateObj = arg && resolveStateObject(arg)
|
|
148
|
+
if (!stateObj || !TsNode.isObjectLiteralExpression(stateObj))
|
|
149
|
+
return []
|
|
150
|
+
const out: StoreFieldRaw[] = []
|
|
151
|
+
for (const pa of stateObj.getProperties()) {
|
|
152
|
+
const f = fieldOf(pa)
|
|
153
|
+
if (f)
|
|
154
|
+
out.push(f)
|
|
155
|
+
}
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 一个 state 声明 → Grown(运行时投影:name/type/domain/line + anchor)。
|
|
160
|
+
export function growFromDecl(v: VariableDeclaration, file: string, marker: string = DEFAULT_STORE_MARKER): Grown {
|
|
161
|
+
const fields: Field[] = extractStoreFields(v, marker).map(f => ({
|
|
162
|
+
name: f.name,
|
|
163
|
+
type: f.type,
|
|
164
|
+
domain: f.domain,
|
|
165
|
+
line: f.line,
|
|
166
|
+
}))
|
|
167
|
+
return {
|
|
168
|
+
store: v.getName(),
|
|
169
|
+
anchor: `${file}:${v.getStartLineNumber()}`,
|
|
170
|
+
fields,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 整仓 → 状态子图。扫每个源文件的 module-level 变量声明,classify,对 state 节点测中心性。
|
|
175
|
+
// fileFor 把绝对路径归一成 anchor 用的相对/短路径(默认取末两段,与 discover 的 anchor 同形)。
|
|
176
|
+
export function buildStateGraph(
|
|
177
|
+
project: Project,
|
|
178
|
+
fileFor: (absPath: string) => string = p => p.split('/').slice(-2).join('/'),
|
|
179
|
+
marker: string = DEFAULT_STORE_MARKER,
|
|
180
|
+
): { nodes: StateNode[] } {
|
|
181
|
+
const nodes: StateNode[] = []
|
|
182
|
+
for (const sf of project.getSourceFiles()) {
|
|
183
|
+
const fp = sf.getFilePath()
|
|
184
|
+
if (fp.endsWith('.d.ts') || fp.includes('/node_modules/'))
|
|
185
|
+
continue
|
|
186
|
+
const file = fileFor(fp)
|
|
187
|
+
for (const v of sf.getVariableDeclarations()) {
|
|
188
|
+
if (!isModuleLevel(v) || classify(v, marker) !== 'state')
|
|
189
|
+
continue
|
|
190
|
+
const grown = growFromDecl(v, file, marker)
|
|
191
|
+
const seen = new Set<string>()
|
|
192
|
+
let refs = 0
|
|
193
|
+
try {
|
|
194
|
+
for (const ref of v.findReferencesAsNodes()) {
|
|
195
|
+
refs++
|
|
196
|
+
seen.add(ref.getSourceFile().getFilePath())
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* checker hiccup on a single binding → 0 refs, still a node */ }
|
|
200
|
+
nodes.push({ ...grown, refs, files: seen.size, rank: refs + seen.size * 3 })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
nodes.sort((a, b) => b.rank - a.rank)
|
|
204
|
+
return { nodes }
|
|
205
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export type FileDetailLevel = 'tree' | 'compact' | 'full'
|
|
2
|
+
|
|
3
|
+
// ── User-facing config schema (what `defineConfig({...})` accepts) ──────────────
|
|
4
|
+
// aihand = three capability modules: read ⊕ refactor ⊕ ui. The config IS that structure.
|
|
5
|
+
// Each module is tri-state: undefined/true → on with defaults · false → off ·
|
|
6
|
+
// object → on + override that module's sub-options. Tuning knobs live INSIDE a module —
|
|
7
|
+
// an off module's knobs never appear.
|
|
8
|
+
|
|
9
|
+
/** read module: the codemap that gets injected into CLAUDE.md/AGENTS.md. */
|
|
10
|
+
export interface ReadOptions {
|
|
11
|
+
include?: string[]
|
|
12
|
+
ignore?: string[]
|
|
13
|
+
/** Per-glob detail level overrides. Unmatched files default to 'compact'. */
|
|
14
|
+
fileDetailLevel?: Record<string, FileDetailLevel>
|
|
15
|
+
/** Detail level used when injecting into target files (default: tree) */
|
|
16
|
+
injectDetailLevel?: FileDetailLevel
|
|
17
|
+
/** Files to inject the codemap into (default: CLAUDE.md, AGENTS.md) */
|
|
18
|
+
injectTargetFiles?: string[]
|
|
19
|
+
/** Include zero-parameter function signatures (default: false) */
|
|
20
|
+
showNoParamFn?: boolean
|
|
21
|
+
/** Non-code file count per directory in file tree that triggers folding (default: 8) */
|
|
22
|
+
treeFoldThreshold?: number
|
|
23
|
+
/** Inject the one-line `aihand` dev-tool pointer into target files (default: true) */
|
|
24
|
+
injectDevToolInstructions?: boolean
|
|
25
|
+
/** Inject the full codemap (panel/state/tree). false → Overview + a pointer to `aihand read --stdout`, so the map isn't resident every session (default: true) */
|
|
26
|
+
injectCodemap?: boolean
|
|
27
|
+
/** Max tokens to inject. Enables smart allocation via git recency + import centrality. (default: 5000) */
|
|
28
|
+
maxTokens?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ModuleSwitch<O> = boolean | O
|
|
32
|
+
|
|
33
|
+
/** runtime module: the browser probe + store discovery. */
|
|
34
|
+
export interface RuntimeOptions {
|
|
35
|
+
/** Reactive fingerprint field the store factory injects into its return type — the
|
|
36
|
+
* construction-agnostic store criterion (a binding `const X = factory(...)` is a store iff
|
|
37
|
+
* `X`'s type has this property), so discovery never hardcodes the factory name `create`.
|
|
38
|
+
* MobX (this repo) → `_loading`; zustand → `getState`; valtio → its proxy symbol. (default: '_loading') */
|
|
39
|
+
storeMarker?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AidevConfig {
|
|
43
|
+
/** Codemap injection into CLAUDE.md/AGENTS.md. */
|
|
44
|
+
read?: ModuleSwitch<ReadOptions>
|
|
45
|
+
/** AST refactor commands (move-file/rename/move-symbol). Stateless → plain on/off. */
|
|
46
|
+
refactor?: boolean
|
|
47
|
+
/** Runtime browser probe + knob projection — inspect & drive the running app.
|
|
48
|
+
* Store discovery is automatic (scans read.include, grows every binding whose type carries
|
|
49
|
+
* the reactive marker); the only knob is `storeMarker` for non-MobX factories. */
|
|
50
|
+
runtime?: ModuleSwitch<RuntimeOptions>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Internal resolved config (what loadConfig produces, what the pipeline consumes) ──
|
|
54
|
+
// Same nested shape, fully populated (no undefined), each module carrying `enabled`.
|
|
55
|
+
|
|
56
|
+
export interface ResolvedRead extends Required<ReadOptions> {
|
|
57
|
+
enabled: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Config {
|
|
61
|
+
read: ResolvedRead
|
|
62
|
+
refactor: { enabled: boolean }
|
|
63
|
+
runtime: { enabled: boolean } & Required<RuntimeOptions>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const READ_DEFAULTS: Required<ReadOptions> = {
|
|
67
|
+
include: [
|
|
68
|
+
'package.json',
|
|
69
|
+
'index.html',
|
|
70
|
+
'tsconfig.json',
|
|
71
|
+
'tsconfig.*.json',
|
|
72
|
+
'*.config.{js,ts,mjs,cjs}',
|
|
73
|
+
],
|
|
74
|
+
ignore: [],
|
|
75
|
+
fileDetailLevel: {
|
|
76
|
+
'package-lock.json': 'tree',
|
|
77
|
+
'pnpm-lock.yaml': 'tree',
|
|
78
|
+
'yarn.lock': 'tree',
|
|
79
|
+
'bun.lock': 'tree',
|
|
80
|
+
'**/*.test.ts': 'tree',
|
|
81
|
+
'**/*.test.tsx': 'tree',
|
|
82
|
+
'**/*.spec.ts': 'tree',
|
|
83
|
+
'**/*.spec.tsx': 'tree',
|
|
84
|
+
},
|
|
85
|
+
injectDetailLevel: 'tree',
|
|
86
|
+
injectTargetFiles: ['CLAUDE.md', 'AGENTS.md'],
|
|
87
|
+
showNoParamFn: false,
|
|
88
|
+
treeFoldThreshold: 8,
|
|
89
|
+
injectDevToolInstructions: true,
|
|
90
|
+
injectCodemap: true,
|
|
91
|
+
maxTokens: 5_000,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const DEFAULT_CONFIG: Config = {
|
|
95
|
+
read: { enabled: true, ...READ_DEFAULTS },
|
|
96
|
+
refactor: { enabled: true },
|
|
97
|
+
runtime: { enabled: true, storeMarker: '_loading' },
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Tri-state module switch → resolved {enabled, ...options}. Shared by all three modules.
|
|
102
|
+
* undefined/true → on with defaults · false → off (defaults retained, just gated) ·
|
|
103
|
+
* object → on + shallow-override the module's options.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveModule<O extends object>(
|
|
106
|
+
sw: ModuleSwitch<O> | undefined,
|
|
107
|
+
defaults: O,
|
|
108
|
+
): { enabled: boolean } & O {
|
|
109
|
+
if (sw === false)
|
|
110
|
+
return { enabled: false, ...defaults }
|
|
111
|
+
if (sw === undefined || sw === true)
|
|
112
|
+
return { enabled: true, ...defaults }
|
|
113
|
+
return { enabled: true, ...defaults, ...sw }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface RepodexData {
|
|
117
|
+
overview: string
|
|
118
|
+
panel?: string // 控制面旋钮(输入端):拨它把哪个 store 状态推向什么。排在 tree 前
|
|
119
|
+
state?: string // 状态显示(输出端/可观测量):系统由哪些状态量构成、初值、值域。与 panel 配成洗衣机面板
|
|
120
|
+
tree: string
|
|
121
|
+
signatures: string
|
|
122
|
+
full?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface FileBlock {
|
|
126
|
+
path: string
|
|
127
|
+
level: FileDetailLevel
|
|
128
|
+
content: string
|
|
129
|
+
/** Resolved relative import specifiers (TS/JS files only, used for import graph) */
|
|
130
|
+
imports?: string[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function defineConfig(config: AidevConfig): AidevConfig {
|
|
134
|
+
return config
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Config template body (between `defineConfig({` and `})`) shared by initConfig and README.
|
|
139
|
+
* Three module switches are the main surface; tuning knobs live inside `read: {...}`.
|
|
140
|
+
*/
|
|
141
|
+
export function configTemplate(): string {
|
|
142
|
+
return ` // aihand = three capability modules. Each: true (on, defaults) · false (off) · { ...overrides }.
|
|
143
|
+
// Most repos only need the three switches below — tuning knobs have smart defaults.
|
|
144
|
+
|
|
145
|
+
// Codemap injection into CLAUDE.md/AGENTS.md
|
|
146
|
+
read: true,
|
|
147
|
+
// AST refactor commands (move-file / rename / move-symbol)
|
|
148
|
+
refactor: true,
|
|
149
|
+
// Inspect & drive the running app (runtime browser probe — needs the vite plugin);
|
|
150
|
+
// set false on repos without a dev server.
|
|
151
|
+
// Non-MobX store factory? runtime: { storeMarker: 'getState' } — the reactive
|
|
152
|
+
// fingerprint field on the store's return type (zustand 'getState', default '_loading').
|
|
153
|
+
runtime: true,
|
|
154
|
+
|
|
155
|
+
// Need to tune read? Open it up (all optional, shown with defaults):
|
|
156
|
+
// read: {
|
|
157
|
+
// include: ['src/**'], // what to scan (root config files always included)
|
|
158
|
+
// ignore: [], // extra skips (.gitignore already applied)
|
|
159
|
+
// maxTokens: 5_000, // budget — auto-downgrades less important files
|
|
160
|
+
// injectCodemap: true, // false → Overview + a pointer to \`aihand read --stdout\`
|
|
161
|
+
// },`
|
|
162
|
+
}
|
package/src/read/vite.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
export interface RepodexPluginOptions {
|
|
7
|
+
/** 透传给 `repodex --watch` 的额外参数 */
|
|
8
|
+
args?: string[]
|
|
9
|
+
/** stdio 模式,默认 'inherit',嫌吵改 'ignore' */
|
|
10
|
+
stdio?: 'inherit' | 'ignore'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 解析顺序,从最近的源码到线上包(aihand 统一入口在上一级 src/cli.ts):
|
|
15
|
+
* 1. ../cli.ts —— dev 用 tsx 跑源码,改完即生效,免编译
|
|
16
|
+
* 2. ../cli.js / ../../dist/cli.js —— 已编译产物
|
|
17
|
+
* 3. npx aihand —— 线上发布包
|
|
18
|
+
* 返回 args 已带 `read` 模块段,调用方再补 `--watch`。
|
|
19
|
+
*/
|
|
20
|
+
function resolveCliCmd(): { cmd: string, args: string[] } {
|
|
21
|
+
try {
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
// this file is src/read/vite.ts; the unified entry sits one level up at src/cli.ts
|
|
24
|
+
const src = resolve(here, '..', 'cli.ts')
|
|
25
|
+
if (existsSync(src)) {
|
|
26
|
+
// node --import tsx:父子直连,无 .bin/tsx wrapper 中间进程,
|
|
27
|
+
// 让 cli.ts 的父进程守卫能精确监测 vite 存活。
|
|
28
|
+
return { cmd: process.execPath, args: ['--import', 'tsx', src, 'read'] }
|
|
29
|
+
}
|
|
30
|
+
for (const rel of ['../cli.js', '../../dist/cli.js']) {
|
|
31
|
+
const p = resolve(here, rel)
|
|
32
|
+
if (existsSync(p))
|
|
33
|
+
return { cmd: process.execPath, args: [p, 'read'] }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {}
|
|
37
|
+
return { cmd: 'npx', args: ['aihand', 'read'] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function repodexWatchPlugin(opts: RepodexPluginOptions = {}) {
|
|
41
|
+
const { args = [], stdio = 'inherit' } = opts
|
|
42
|
+
return {
|
|
43
|
+
name: 'aihand-read-watch',
|
|
44
|
+
apply: 'serve' as const,
|
|
45
|
+
configureServer(server: any) {
|
|
46
|
+
// setup:spawn 唯一的 watch 子进程(叶子进程,无孙进程)
|
|
47
|
+
const { cmd, args: prefix } = resolveCliCmd()
|
|
48
|
+
const child = spawn(cmd, [...prefix, '--watch', ...args], {
|
|
49
|
+
stdio,
|
|
50
|
+
env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? '1' },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// teardown:精准 kill 这一个 child,幂等只跑一次,绝不波及其它进程
|
|
54
|
+
let done = false
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
if (done)
|
|
57
|
+
return
|
|
58
|
+
done = true
|
|
59
|
+
if (!child.killed)
|
|
60
|
+
child.kill()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 子进程先死则无需再清;覆盖 vite 正常关闭与各类进程退出(含被强杀前的最后机会)
|
|
64
|
+
child.once('exit', () => {
|
|
65
|
+
done = true
|
|
66
|
+
})
|
|
67
|
+
server.httpServer?.once('close', cleanup)
|
|
68
|
+
process.once('exit', cleanup)
|
|
69
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
70
|
+
process.once(sig, () => {
|
|
71
|
+
cleanup()
|
|
72
|
+
process.kill(process.pid, sig) // 清完重抛信号,不吞掉 vite 的退出
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Source both the types and the value namespace from @babel/core so they share one
|
|
2
|
+
// module identity. Importing types separately from @babel/types pins them to a
|
|
3
|
+
// different copy when multiple @babel/types versions resolve (NodePath variance error).
|
|
4
|
+
import type { NodePath, PluginObj, types as BabelTypes } from '@babel/core'
|
|
5
|
+
import { types as t } from '@babel/core'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
|
|
8
|
+
type Statement = BabelTypes.Statement
|
|
9
|
+
type VariableDeclaration = BabelTypes.VariableDeclaration
|
|
10
|
+
type ReturnStatement = BabelTypes.ReturnStatement
|
|
11
|
+
type ExpressionStatement = BabelTypes.ExpressionStatement
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Babel plugin: wrap statements with __line(label, () => ...) for line-level profiling.
|
|
15
|
+
*
|
|
16
|
+
* Only wraps statements that have measurable cost:
|
|
17
|
+
* - VariableDeclaration with initializer
|
|
18
|
+
* - ExpressionStatement (function calls, assignments)
|
|
19
|
+
* - ReturnStatement with argument
|
|
20
|
+
*
|
|
21
|
+
* Skips:
|
|
22
|
+
* - import/export (module syntax)
|
|
23
|
+
* - control flow (if/for/while/switch) — their bodies get wrapped, not the structure
|
|
24
|
+
* - empty statements, type declarations
|
|
25
|
+
*/
|
|
26
|
+
export function lineProfilerPlugin(): PluginObj {
|
|
27
|
+
return {
|
|
28
|
+
name: 'aihand-line-profiler',
|
|
29
|
+
visitor: {
|
|
30
|
+
VariableDeclaration(nodePath: NodePath<VariableDeclaration>, state) {
|
|
31
|
+
if (shouldSkip(nodePath, state)) return
|
|
32
|
+
const decl = nodePath.node.declarations[0]
|
|
33
|
+
if (!decl?.init) return // skip `let x;`
|
|
34
|
+
if (hasAwaitOrYield(decl.init)) return // wrapping in arrow fn breaks async semantics
|
|
35
|
+
|
|
36
|
+
const label = makeLabel(nodePath, state, decl.id.type === 'Identifier' ? decl.id.name : undefined)
|
|
37
|
+
const wrapped = wrapExpr(decl.init, label)
|
|
38
|
+
decl.init = wrapped
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
ExpressionStatement(nodePath: NodePath<ExpressionStatement>, state) {
|
|
42
|
+
if (shouldSkip(nodePath, state)) return
|
|
43
|
+
if (hasAwaitOrYield(nodePath.node.expression)) return
|
|
44
|
+
|
|
45
|
+
const label = makeLabel(nodePath, state)
|
|
46
|
+
const wrapped = wrapExpr(nodePath.node.expression, label)
|
|
47
|
+
nodePath.node.expression = wrapped
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
ReturnStatement(nodePath: NodePath<ReturnStatement>, state) {
|
|
51
|
+
if (shouldSkip(nodePath, state)) return
|
|
52
|
+
if (!nodePath.node.argument) return // skip `return;`
|
|
53
|
+
if (hasAwaitOrYield(nodePath.node.argument)) return
|
|
54
|
+
|
|
55
|
+
const label = makeLabel(nodePath, state)
|
|
56
|
+
const wrapped = wrapExpr(nodePath.node.argument, label)
|
|
57
|
+
nodePath.node.argument = wrapped
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shouldSkip(nodePath: NodePath<Statement>, state: any): boolean {
|
|
64
|
+
const filename: string = state.file?.opts?.filename ?? ''
|
|
65
|
+
// Skip node_modules — third-party cost isn't the app's to fix.
|
|
66
|
+
if (filename.includes('node_modules')) return true
|
|
67
|
+
// Skip aihand's own source: the profiler reads state by walking the React fiber tree
|
|
68
|
+
// (client.ts walkFiber/walkChildren) on every /ui|/dom|/screen. That's MEASUREMENT cost,
|
|
69
|
+
// not the app's — instrumenting it lets the observer dominate its own report (290ms self-top).
|
|
70
|
+
if (filename.includes('packages/aidev/')) return true
|
|
71
|
+
|
|
72
|
+
// Skip module syntax — import/export carry no runtime cost to measure
|
|
73
|
+
const node = nodePath.node
|
|
74
|
+
if (t.isImportDeclaration(node)) return true
|
|
75
|
+
if (t.isExportNamedDeclaration(node)) return true
|
|
76
|
+
if (t.isExportDefaultDeclaration(node)) return true
|
|
77
|
+
if (t.isExportAllDeclaration(node)) return true
|
|
78
|
+
|
|
79
|
+
// Skip if already wrapped
|
|
80
|
+
if (t.isExpressionStatement(node) && t.isCallExpression(node.expression)) {
|
|
81
|
+
const callee = node.expression.callee
|
|
82
|
+
if (t.isIdentifier(callee) && callee.name === '__line') return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeLabel(nodePath: NodePath<Statement>, state: any, varName?: string): string {
|
|
89
|
+
const filename = state.file.opts.filename ?? 'unknown'
|
|
90
|
+
const basename = path.basename(filename)
|
|
91
|
+
const line = resolveLine(nodePath)
|
|
92
|
+
|
|
93
|
+
if (varName) {
|
|
94
|
+
return `${basename}:${line}:${varName}`
|
|
95
|
+
}
|
|
96
|
+
return `${basename}:${line}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Resolve the source line a wrapped statement should point to.
|
|
100
|
+
//
|
|
101
|
+
// A node synthesized by an upstream transform (e.g. mobx-react-observer rewriting
|
|
102
|
+
// `function Root(){…}` → `const Root = observer(function Root(){…})`) carries NO loc on the
|
|
103
|
+
// outer declaration. Two recovery moves, in priority order:
|
|
104
|
+
//
|
|
105
|
+
// 1. descend — the synthesized wrapper still contains the user's ORIGINAL node
|
|
106
|
+
// (`function Root()` keeps its loc 59). The nearest loc-bearing descendant is the most
|
|
107
|
+
// specific real source location, so prefer it.
|
|
108
|
+
// 2. climb — only if the whole subtree is synthetic (no loc anywhere inside) do we fall back
|
|
109
|
+
// to the nearest ancestor's loc.
|
|
110
|
+
//
|
|
111
|
+
// Without this, observer-wrapped components — the ones most worth profiling — collapse onto
|
|
112
|
+
// `<file>:1` (climbing all the way to Program) or `<file>:0` (no loc at all).
|
|
113
|
+
function resolveLine(nodePath: NodePath<Statement>): number {
|
|
114
|
+
if (nodePath.node.loc) return nodePath.node.loc.start.line
|
|
115
|
+
|
|
116
|
+
const descend = firstLocLine(nodePath.node)
|
|
117
|
+
if (descend != null) return descend
|
|
118
|
+
|
|
119
|
+
let p: NodePath | null = nodePath.parentPath
|
|
120
|
+
while (p) {
|
|
121
|
+
if (p.node.loc) return p.node.loc.start.line
|
|
122
|
+
p = p.parentPath
|
|
123
|
+
}
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Pre-order DFS for the first node carrying a loc. Skips the root itself (caller already
|
|
128
|
+
// checked it). Source-order traversal so the earliest real line wins.
|
|
129
|
+
function firstLocLine(node: t.Node): number | null {
|
|
130
|
+
for (const key of t.VISITOR_KEYS[node.type] ?? []) {
|
|
131
|
+
const child = (node as any)[key]
|
|
132
|
+
const children = Array.isArray(child) ? child : [child]
|
|
133
|
+
for (const c of children) {
|
|
134
|
+
if (!c || typeof c.type !== 'string') continue
|
|
135
|
+
if (c.loc) return c.loc.start.line
|
|
136
|
+
const deeper = firstLocLine(c)
|
|
137
|
+
if (deeper != null) return deeper
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Emit: (globalThis.__line || ((_l, f) => f()))(label, () => expr)
|
|
144
|
+
// NOT a bare `__line(...)`: instrumented modules eval their top-level statements at IMPORT time,
|
|
145
|
+
// before client-patch installs window.__line — a bare identifier then throws ReferenceError and
|
|
146
|
+
// kills the app. globalThis.__line is a member access (→ undefined, not a throw); the || fallback
|
|
147
|
+
// just runs the original expr, so the instrumentation is inert until the runtime is present.
|
|
148
|
+
function wrapExpr(expr: t.Expression, label: string): t.CallExpression {
|
|
149
|
+
const runtime = t.logicalExpression(
|
|
150
|
+
'||',
|
|
151
|
+
t.memberExpression(t.identifier('globalThis'), t.identifier('__line')),
|
|
152
|
+
t.arrowFunctionExpression(
|
|
153
|
+
[t.identifier('_l'), t.identifier('f')],
|
|
154
|
+
t.callExpression(t.identifier('f'), []),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
return t.callExpression(
|
|
158
|
+
runtime,
|
|
159
|
+
[
|
|
160
|
+
t.stringLiteral(label),
|
|
161
|
+
t.arrowFunctionExpression([], expr),
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// True if expr contains an await/yield at THIS function level (not inside a nested function).
|
|
167
|
+
// Wrapping such an expr in `() => expr` would either be a syntax error (await in non-async
|
|
168
|
+
// arrow) or silently change semantics (the arrow returns a Promise instead of awaiting).
|
|
169
|
+
// Nested functions are their own scope, so we stop recursing at function boundaries.
|
|
170
|
+
function hasAwaitOrYield(node: t.Node | null | undefined): boolean {
|
|
171
|
+
if (!node || typeof node !== 'object') return false
|
|
172
|
+
if (t.isAwaitExpression(node) || t.isYieldExpression(node)) return true
|
|
173
|
+
// Don't descend into nested functions — their await/yield belongs to their own scope.
|
|
174
|
+
if (
|
|
175
|
+
t.isFunctionExpression(node)
|
|
176
|
+
|| t.isArrowFunctionExpression(node)
|
|
177
|
+
|| t.isFunctionDeclaration(node)
|
|
178
|
+
|| t.isObjectMethod(node)
|
|
179
|
+
|| t.isClassMethod(node)
|
|
180
|
+
) return false
|
|
181
|
+
|
|
182
|
+
for (const key of Object.keys(node)) {
|
|
183
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'leadingComments' || key === 'trailingComments') continue
|
|
184
|
+
const child = (node as any)[key]
|
|
185
|
+
if (Array.isArray(child)) {
|
|
186
|
+
for (const c of child) {
|
|
187
|
+
if (c && typeof c.type === 'string' && hasAwaitOrYield(c)) return true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (child && typeof child.type === 'string') {
|
|
191
|
+
if (hasAwaitOrYield(child)) return true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default lineProfilerPlugin
|