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.
Files changed (113) hide show
  1. package/README.md +136 -2
  2. package/dist/chunk-2NTK7H4W.js +10 -0
  3. package/dist/chunk-3X4FTHLC.cjs +369 -0
  4. package/dist/chunk-BXVNR4E2.js +399 -0
  5. package/dist/chunk-C7DGE6MY.cjs +1456 -0
  6. package/dist/chunk-DUUCVLC3.cjs +254 -0
  7. package/dist/chunk-FAHI53KO.cjs +125 -0
  8. package/dist/chunk-G7KVJ7NF.js +369 -0
  9. package/dist/chunk-GNEUSRGP.js +52 -0
  10. package/dist/chunk-IGNEAOLT.cjs +130 -0
  11. package/dist/chunk-IS5XFUDB.js +125 -0
  12. package/dist/chunk-JLYC76XL.js +2448 -0
  13. package/dist/chunk-KQOABC2O.cjs +52 -0
  14. package/dist/chunk-OVMK33AC.cjs +104 -0
  15. package/dist/chunk-OWYK2IGV.js +250 -0
  16. package/dist/chunk-PQSQN4CN.js +126 -0
  17. package/dist/chunk-QF6AG3M5.cjs +410 -0
  18. package/dist/chunk-QSAMLXML.js +1456 -0
  19. package/dist/chunk-VEKYRKPF.cjs +399 -0
  20. package/dist/chunk-Y6H7W7PI.cjs +2451 -0
  21. package/dist/chunk-YKSYW77R.js +410 -0
  22. package/dist/chunk-Z2Y65YOY.cjs +7 -0
  23. package/dist/chunk-ZJQRNIK7.js +104 -0
  24. package/dist/cli-FDS2C2CZ.cjs +651 -0
  25. package/dist/cli-HHRGYPSM.js +649 -0
  26. package/dist/cli-JQEIE7RQ.js +120 -0
  27. package/dist/cli-K3OS2QQH.cjs +122 -0
  28. package/dist/cli-OSYG6LJD.cjs +89 -0
  29. package/dist/cli-TXRW5PG6.js +89 -0
  30. package/dist/cli.cjs +81 -0
  31. package/dist/cli.js +81 -0
  32. package/dist/config-5KEQLN6L.cjs +13 -0
  33. package/dist/config-PJPYKDLQ.js +13 -0
  34. package/dist/graph-IH56SCPK.js +8 -0
  35. package/dist/graph-ZUXXCJ5A.cjs +8 -0
  36. package/dist/index.cjs +481 -0
  37. package/dist/index.d.cts +461 -0
  38. package/dist/index.d.ts +461 -0
  39. package/dist/index.js +479 -0
  40. package/dist/locate-5XFSXJ5J.cjs +15 -0
  41. package/dist/locate-NKSUGL3A.js +15 -0
  42. package/dist/refactor-5FWSZIBN.cjs +19 -0
  43. package/dist/refactor-BOB3SZSA.js +19 -0
  44. package/dist/scan-4R7GQG2W.cjs +9 -0
  45. package/dist/scan-VF54GAAX.js +9 -0
  46. package/dist/ui/probe/server.cjs +505 -0
  47. package/dist/ui/probe/server.js +507 -0
  48. package/dist/vite.cjs +12 -0
  49. package/dist/vite.d.cts +12 -0
  50. package/dist/vite.d.ts +12 -0
  51. package/dist/vite.js +12 -0
  52. package/package.json +82 -9
  53. package/src/cli.ts +107 -0
  54. package/src/index.ts +54 -0
  55. package/src/read/cli.ts +650 -0
  56. package/src/read/compact.ts +286 -0
  57. package/src/read/config.ts +62 -0
  58. package/src/read/graph.ts +182 -0
  59. package/src/read/index.ts +12 -0
  60. package/src/read/inject.ts +121 -0
  61. package/src/read/locate.ts +104 -0
  62. package/src/read/panel.ts +335 -0
  63. package/src/read/pipeline.ts +78 -0
  64. package/src/read/refactor.ts +576 -0
  65. package/src/read/render.ts +1118 -0
  66. package/src/read/scan.ts +61 -0
  67. package/src/read/seam.ts +0 -0
  68. package/src/read/security.ts +171 -0
  69. package/src/read/signals.ts +333 -0
  70. package/src/read/state.ts +71 -0
  71. package/src/read/stategraph.ts +205 -0
  72. package/src/read/types.ts +162 -0
  73. package/src/read/vite.ts +77 -0
  74. package/src/ui/babel/line-profiler.ts +197 -0
  75. package/src/ui/babel/source-loc.ts +68 -0
  76. package/src/ui/bridge/cdp-bridge.ts +138 -0
  77. package/src/ui/bridge/compile-probe.ts +80 -0
  78. package/src/ui/bridge/transport.ts +26 -0
  79. package/src/ui/bridge/vite-bridge.ts +116 -0
  80. package/src/ui/client/client-patch.ts +899 -0
  81. package/src/ui/client/client.ts +2562 -0
  82. package/src/ui/core/action.ts +747 -0
  83. package/src/ui/core/candidates.ts +348 -0
  84. package/src/ui/core/canvas.ts +305 -0
  85. package/src/ui/core/check.ts +34 -0
  86. package/src/ui/core/compact.ts +314 -0
  87. package/src/ui/core/detail.ts +244 -0
  88. package/src/ui/core/diff.ts +253 -0
  89. package/src/ui/core/emit.ts +198 -0
  90. package/src/ui/core/knob-exec.ts +137 -0
  91. package/src/ui/core/perf.ts +254 -0
  92. package/src/ui/core/types.ts +164 -0
  93. package/src/ui/core/util.ts +221 -0
  94. package/src/ui/index.ts +5 -0
  95. package/src/ui/probe/cli.ts +139 -0
  96. package/src/ui/probe/server.ts +468 -0
  97. package/src/ui/self/act.ts +47 -0
  98. package/src/ui/self/discover.ts +101 -0
  99. package/src/ui/self/grow.ts +121 -0
  100. package/src/ui/self/install.ts +100 -0
  101. package/src/ui/self/probe.ts +105 -0
  102. package/src/ui/self/screen-hook.ts +44 -0
  103. package/src/ui/self/self.ts +48 -0
  104. package/src/ui/self/store-refs.ts +123 -0
  105. package/src/ui/self/store-schema.ts +65 -0
  106. package/src/ui/self/synth.ts +37 -0
  107. package/src/ui/server/cli.ts +102 -0
  108. package/src/ui/server/dispatch.ts +276 -0
  109. package/src/ui/server/help-text.ts +237 -0
  110. package/src/ui/server/knob-schema.ts +87 -0
  111. package/src/ui/server/plugin.ts +1151 -0
  112. package/src/vite.ts +39 -0
  113. 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
+ }
@@ -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