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
package/README.md
CHANGED
|
@@ -1,5 +1,139 @@
|
|
|
1
1
|
# aihand
|
|
2
2
|
|
|
3
|
-
**The hand of AI
|
|
3
|
+
**The hand of AI.** One CLI that lets an agent *read* your code, *refactor* it, and *drive your running app* — the three things a human does with their hands when working on software, given to the model directly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Today an agent works half-blind: it reads files but can't see the running page, it edits text but can't safely move a symbol, it guesses at the UI instead of looking. `aihand` closes that loop. Add one Vite plugin and the agent gets a code map, AST-safe refactors, and a live probe into the browser — all over plain HTTP, all from one command.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
aihand read panel | source <file:line> understand code (read-only)
|
|
9
|
+
aihand refactor move-file | rename | move-symbol change source (AST, writes disk)
|
|
10
|
+
aihand runtime screen | click | fill | wait … inspect & drive the running app
|
|
11
|
+
aihand probe open <url> | screen | click … drive ANY website (out-of-app, own chromium)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
The bottleneck in agentic coding isn't the model — it's the **round-trips**. Every time the agent has to guess, then check, then correct, that's a wasted turn. `aihand` is built to collapse those turns:
|
|
17
|
+
|
|
18
|
+
- **See the app, don't guess it.** `runtime screen` returns a semantic, spatial sketch of the live UI — view, modals, focus, every clickable knob — so the agent reads the page directly instead of asking you to describe it.
|
|
19
|
+
- **One round-trip per action.** Click-and-observe is fused: an action returns the *delta* it caused (and, for async flows, the trajectory until it settles), so the agent doesn't re-screen to find out what happened.
|
|
20
|
+
- **Refactors that can't silently break.** `move-symbol` / `rename` go through ts-morph (never regex), rewrite every importer, and *refuse* unsafe edits with a clear reason instead of producing a broken tree.
|
|
21
|
+
- **A control-panel view of the whole app.** `read panel` projects thousands of components down to the dozen knobs that actually drive state — the agent learns "what this app can do and which store each control writes" in one read.
|
|
22
|
+
|
|
23
|
+
It's fully self-contained — a static code-analysis engine (tree-sitter) and a runtime browser probe + AST refactorer (ts-morph), both vendored in. No peer tools to install.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm i -D aihand
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add the plugin once to your `vite.config.ts`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { aihand } from 'aihand/vite'
|
|
35
|
+
|
|
36
|
+
export default defineConfig({
|
|
37
|
+
plugins: [aihand()],
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
One registration provides both the runtime UI probe (serving `/__aihand/*` endpoints in dev) and the watch-mode codemap injection into your AI context files.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
First run detects existing AI context files (`CLAUDE.md`, `AGENTS.md`, etc.), adds injection markers, and writes `aihand.config.ts`.
|
|
46
|
+
|
|
47
|
+
The config body is the three capability modules — that's the decision you actually make (which capabilities this repo wants). Tuning knobs live inside a module and have smart defaults.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// aihand.config.ts
|
|
51
|
+
import { defineConfig } from 'aihand'
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
// Three capability modules. Each: true (on, defaults) · false (off) · { ...overrides }.
|
|
55
|
+
// Most repos only need the three switches below — tuning knobs have smart defaults.
|
|
56
|
+
|
|
57
|
+
// Codemap injection into CLAUDE.md / AGENTS.md
|
|
58
|
+
read: true,
|
|
59
|
+
// AST refactor commands (move-file / rename / move-symbol)
|
|
60
|
+
refactor: true,
|
|
61
|
+
// Inspect & drive the running app (runtime browser probe — needs the vite plugin);
|
|
62
|
+
// set false on repos without a dev server.
|
|
63
|
+
// Non-MobX store factory? runtime: { storeMarker: 'getState' } — the reactive
|
|
64
|
+
// fingerprint field on the store's return type (zustand 'getState', default '_loading').
|
|
65
|
+
runtime: true,
|
|
66
|
+
|
|
67
|
+
// Need to tune read? Open it up (all optional, shown with defaults):
|
|
68
|
+
// read: {
|
|
69
|
+
// include: ['src/**'], // what to scan (root config files always included)
|
|
70
|
+
// ignore: [], // extra skips (.gitignore already applied)
|
|
71
|
+
// maxTokens: 5_000, // budget — auto-downgrades less important files
|
|
72
|
+
// injectCodemap: true, // false → Overview + a pointer to `aihand read --stdout`
|
|
73
|
+
// },
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Smart allocation
|
|
78
|
+
|
|
79
|
+
When `maxTokens` is set, `aihand` scores every file by **git recency** and **import centrality**, then auto-downgrades low-signal files (`compact` → `tree` → drop) to fit the budget. High-traffic, recently-changed files keep full signatures; the long tail collapses to names.
|
|
80
|
+
|
|
81
|
+
## read — understand code
|
|
82
|
+
|
|
83
|
+
- `aihand read panel` — the app's control-theoretic model: every knob (UI control + the store state it writes), one line each. `--live=<port>` joins it with the running app, marking which knobs are rendered right now (● live).
|
|
84
|
+
- `aihand read source <file:line>` — reverse morphism: a runtime coordinate (or a `/dom` data-insp-path) → the static symbol covering it, plus its callers/callees.
|
|
85
|
+
- `aihand read tree` — where files live and how the repo is laid out.
|
|
86
|
+
- `aihand read --stdout` — the full code map on demand, instead of resident in your context.
|
|
87
|
+
|
|
88
|
+
All support `--json` for structured output.
|
|
89
|
+
|
|
90
|
+
## refactor — change source structure
|
|
91
|
+
|
|
92
|
+
AST-driven via ts-morph (never regex). Rewrites every importer; verify with `tsc --noEmit` after.
|
|
93
|
+
|
|
94
|
+
- `aihand refactor move-file <src> <dest> [--dry]` — move a file, rewrite all importers.
|
|
95
|
+
- `aihand refactor rename <file> <oldName> <newName> [--dry]` — rename a top-level symbol across the project.
|
|
96
|
+
- `aihand refactor move-symbol <fromFile> <name> <toFile> [--dry]` — move one symbol, carry its deps.
|
|
97
|
+
|
|
98
|
+
Unsafe forms (default exports, alias-exported symbols, namespace-accessed symbols, barrel re-exports, existing destination) are rejected with a clear message — never a silent broken edit. Use `--dry` to preview.
|
|
99
|
+
|
|
100
|
+
## runtime — inspect & drive the running app
|
|
101
|
+
|
|
102
|
+
Reads and drives the live app over HTTP (needs the dev server running with `aihand()`). Start with `screen`.
|
|
103
|
+
|
|
104
|
+
**Inspect:**
|
|
105
|
+
|
|
106
|
+
- `aihand runtime screen` — state-machine view of the UI (start here); `ui` / `dom` for detail.
|
|
107
|
+
- `aihand runtime check` — health check (exits non-zero on failure); `console` / `network` / `errors` for diagnostics.
|
|
108
|
+
|
|
109
|
+
**Drive** — params are flags, forwarded as the query string the endpoint expects:
|
|
110
|
+
|
|
111
|
+
- `aihand runtime click --text=New` — click by visible text (or `--sel=<css>` to target by selector).
|
|
112
|
+
- `aihand runtime fill --sel=textarea --value=hello` — set an input/textarea value.
|
|
113
|
+
- `aihand runtime wait --text=Done --timeout=8000` — poll until text/selector appears.
|
|
114
|
+
- also `hover` / `press` / `scrollIntoView`.
|
|
115
|
+
|
|
116
|
+
Flags use `--key=value` (not `--key value`); values may contain `=` and non-ASCII (CSS selectors, CJK text) and are URL-encoded for you. `--port=<port>` selects the dev server port (default 5173); `--full` requests untruncated output.
|
|
117
|
+
|
|
118
|
+
These are a thin façade over the `/__aihand/*` HTTP endpoints the plugin serves — `curl` them directly for anything the CLI doesn't wrap (e.g. `POST /chain` for scripted interactions).
|
|
119
|
+
|
|
120
|
+
## probe — drive ANY external website (out-of-app)
|
|
121
|
+
|
|
122
|
+
The same probe, parachuted into a website you don't own. No vite plugin, no app cooperation: `aihand probe` launches its own chromium (playwright-core), injects the probe over CDP, and serves the same kernel from a resident server.
|
|
123
|
+
|
|
124
|
+
- `aihand probe open <url>` — launch chromium, inject the probe, navigate; the reply *is* the page (semantic tree + addressable knobs + view/modal/focus), so `open → see` is one roundtrip. `--headed` pops a real window.
|
|
125
|
+
- `aihand probe screen` / `click --text=…` (also `fill` / `press` / `hover` / `wait`) — same verbs as runtime, CDP transport.
|
|
126
|
+
- `aihand probe eval --code=…` — run JS in the real page with auto-return; reads the exact text/numbers the spatial sketch clips (a stats table, a price), queries closures, installs listeners. The escape hatch when `screen` isn't enough.
|
|
127
|
+
- `aihand probe screenshot` — save a PNG (pixels, not the sketch) and return its path; `--sel=…` shoots one element, `--fullPage=1` the whole scroll height. For visual verification a character sketch can't give (a chart, a captcha).
|
|
128
|
+
|
|
129
|
+
The page handle lives in the resident server (`:5179`) — repeated commands hit the **same page**, so login / scroll / open menus persist across calls. Full-page navigations are back-filled in one roundtrip: a `click` that follows a link returns `navigated: from → to` plus the new page's full view, no extra round.
|
|
130
|
+
|
|
131
|
+
For known multi-step sequences (login `fill→fill→click`, search `fill→press`), `POST /chain` with a JSON array of steps runs the whole interaction in **one roundtrip** — fail-fast, each step rendered with its delta, skipped steps announced:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
curl -s :5179/chain -d '[{"type":"fill","sel":"input[type=search]","value":"hello"},{"type":"press","key":"Enter"}]'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__dirname
|
|
10
|
+
};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkDUUCVLC3cjs = require('./chunk-DUUCVLC3.cjs');
|
|
4
|
+
|
|
5
|
+
// src/read/panel.ts
|
|
6
|
+
var _fs = require('fs');
|
|
7
|
+
var _webtreesitter = require('web-tree-sitter');
|
|
8
|
+
var ATTR_QUERY = `
|
|
9
|
+
(jsx_attribute
|
|
10
|
+
(property_identifier) @name
|
|
11
|
+
(jsx_expression) @val) @attr
|
|
12
|
+
`;
|
|
13
|
+
function collapseWs(s) {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let cur = "";
|
|
16
|
+
for (const ch of s) {
|
|
17
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
18
|
+
if (cur) {
|
|
19
|
+
tokens.push(cur);
|
|
20
|
+
cur = "";
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
cur += ch;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (cur) tokens.push(cur);
|
|
27
|
+
return tokens.join(" ");
|
|
28
|
+
}
|
|
29
|
+
function storeWritesIn(node) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const walk = (n) => {
|
|
32
|
+
if (n.type === "assignment_expression") {
|
|
33
|
+
const left = n.childForFieldName("left");
|
|
34
|
+
const right = n.childForFieldName("right");
|
|
35
|
+
if (_optionalChain([left, 'optionalAccess', _ => _.type]) === "member_expression") {
|
|
36
|
+
const obj = left.childForFieldName("object");
|
|
37
|
+
const prop = left.childForFieldName("property");
|
|
38
|
+
if (_optionalChain([obj, 'optionalAccess', _2 => _2.type]) === "identifier" && obj.text.endsWith("Store") && prop)
|
|
39
|
+
out.push({ store: obj.text, field: prop.text, to: collapseWs(_nullishCoalesce(_optionalChain([right, 'optionalAccess', _3 => _3.text]), () => ( ""))) });
|
|
40
|
+
}
|
|
41
|
+
} else if (n.type === "call_expression") {
|
|
42
|
+
const fn = n.childForFieldName("function");
|
|
43
|
+
if (_optionalChain([fn, 'optionalAccess', _4 => _4.type]) === "member_expression") {
|
|
44
|
+
const obj = fn.childForFieldName("object");
|
|
45
|
+
const method = fn.childForFieldName("property");
|
|
46
|
+
if (_optionalChain([obj, 'optionalAccess', _5 => _5.type]) === "identifier" && obj.text.endsWith("Store") && method) {
|
|
47
|
+
const argsNode = n.childForFieldName("arguments");
|
|
48
|
+
const inner = argsNode ? collapseWs(argsNode.text).slice(1, -1).trim() : "";
|
|
49
|
+
out.push({ store: obj.text, field: `${method.text}()`, to: "", args: inner });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i));
|
|
54
|
+
};
|
|
55
|
+
walk(node);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function labelOf(opening) {
|
|
59
|
+
for (let i = 0; i < opening.namedChildCount; i++) {
|
|
60
|
+
const a = opening.namedChild(i);
|
|
61
|
+
if (a.type !== "jsx_attribute") continue;
|
|
62
|
+
if (_optionalChain([a, 'access', _6 => _6.namedChild, 'call', _7 => _7(0), 'optionalAccess', _8 => _8.text]) !== "label" && _optionalChain([a, 'access', _9 => _9.namedChild, 'call', _10 => _10(0), 'optionalAccess', _11 => _11.text]) !== "title") continue;
|
|
63
|
+
const val = a.namedChild(1);
|
|
64
|
+
if (!val) continue;
|
|
65
|
+
if (val.type === "string") return val.text.slice(1, -1);
|
|
66
|
+
let found = null;
|
|
67
|
+
const dig = (n) => {
|
|
68
|
+
if (found) return;
|
|
69
|
+
if (n.type === "string") {
|
|
70
|
+
found = n.text.slice(1, -1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (let j = 0; j < n.namedChildCount; j++) dig(n.namedChild(j));
|
|
74
|
+
};
|
|
75
|
+
dig(val);
|
|
76
|
+
if (found) return found;
|
|
77
|
+
}
|
|
78
|
+
const element = opening.parent;
|
|
79
|
+
if (_optionalChain([element, 'optionalAccess', _12 => _12.type]) === "jsx_element") {
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (let i = 0; i < element.namedChildCount; i++) {
|
|
82
|
+
const child = element.namedChild(i);
|
|
83
|
+
if (child.type === "jsx_text") {
|
|
84
|
+
const t = collapseWs(child.text).trim();
|
|
85
|
+
if (t) parts.push(t);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (parts.length) return parts.join(" ");
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function extractKnobs(filePath, code, lang) {
|
|
93
|
+
const parser = new (0, _webtreesitter.Parser)();
|
|
94
|
+
parser.setLanguage(lang);
|
|
95
|
+
const tree = parser.parse(code);
|
|
96
|
+
if (!tree) return [];
|
|
97
|
+
const knobs = [];
|
|
98
|
+
for (const c of new (0, _webtreesitter.Query)(lang, ATTR_QUERY).captures(tree.rootNode)) {
|
|
99
|
+
if (c.name !== "attr") continue;
|
|
100
|
+
const event = _optionalChain([c, 'access', _13 => _13.node, 'access', _14 => _14.namedChild, 'call', _15 => _15(0), 'optionalAccess', _16 => _16.text]);
|
|
101
|
+
if (!event || !event.startsWith("on")) continue;
|
|
102
|
+
const writes = storeWritesIn(c.node);
|
|
103
|
+
if (!writes.length) continue;
|
|
104
|
+
const opening = c.node.parent;
|
|
105
|
+
const byField = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const w of writes) byField.set(w.field, { to: w.to, args: w.args });
|
|
107
|
+
const transitions = [...byField].map(([field, v]) => v.args !== void 0 ? { field, to: v.to, args: v.args } : { field, to: v.to });
|
|
108
|
+
knobs.push({
|
|
109
|
+
label: labelOf(opening),
|
|
110
|
+
tag: _nullishCoalesce(_optionalChain([opening, 'access', _17 => _17.childForFieldName, 'call', _18 => _18("name"), 'optionalAccess', _19 => _19.text]), () => ( "?")),
|
|
111
|
+
event,
|
|
112
|
+
transitions,
|
|
113
|
+
store: writes[0].store,
|
|
114
|
+
filePath,
|
|
115
|
+
line: c.node.startPosition.row + 1
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return knobs;
|
|
119
|
+
}
|
|
120
|
+
function fmtTransitions(ts) {
|
|
121
|
+
return ts.map((t) => t.to ? `${t.field}=${t.to}` : t.field).join(", ");
|
|
122
|
+
}
|
|
123
|
+
function formatPanel(knobs) {
|
|
124
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
125
|
+
for (const k of knobs) {
|
|
126
|
+
const arr = _nullishCoalesce(byFile.get(k.filePath), () => ( []));
|
|
127
|
+
arr.push(k);
|
|
128
|
+
byFile.set(k.filePath, arr);
|
|
129
|
+
}
|
|
130
|
+
const lines = [];
|
|
131
|
+
for (const [file, ks] of byFile) {
|
|
132
|
+
lines.push(file);
|
|
133
|
+
for (const k of ks) {
|
|
134
|
+
const name = k.label ? `\u300C${k.label}\u300D` : `<${k.tag}>`;
|
|
135
|
+
lines.push(` ${name} ${k.event} \u2192 ${k.store}.{ ${fmtTransitions(k.transitions)} } :${k.line}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
function isIdentChar(ch) {
|
|
141
|
+
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "$";
|
|
142
|
+
}
|
|
143
|
+
function isLiteral(to) {
|
|
144
|
+
if (to === "true" || to === "false" || to === "null") return true;
|
|
145
|
+
if (to.length >= 2) {
|
|
146
|
+
const q = to[0];
|
|
147
|
+
if ((q === "'" || q === '"' || q === "`") && to[to.length - 1] === q) {
|
|
148
|
+
for (let i = 1; i < to.length - 1; i++) {
|
|
149
|
+
if (to[i] === q && to[i - 1] !== "\\") return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let dot = 0;
|
|
155
|
+
for (let i = 0; i < to.length; i++) {
|
|
156
|
+
const ch = to[i];
|
|
157
|
+
if (ch === "-" && i === 0) continue;
|
|
158
|
+
if (ch === ".") {
|
|
159
|
+
dot++;
|
|
160
|
+
if (dot > 1) return false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch < "0" || ch > "9") return false;
|
|
164
|
+
}
|
|
165
|
+
return to.length > 0 && to !== "-" && to !== ".";
|
|
166
|
+
}
|
|
167
|
+
function isMemberPath(s) {
|
|
168
|
+
if (!s || s[0] === "." || s[s.length - 1] === ".") return false;
|
|
169
|
+
let prevDot = false;
|
|
170
|
+
for (const ch of s) {
|
|
171
|
+
if (ch === ".") {
|
|
172
|
+
if (prevDot) return false;
|
|
173
|
+
prevDot = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!isIdentChar(ch)) return false;
|
|
177
|
+
prevDot = false;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function isPlaceholder(to) {
|
|
182
|
+
if (!isMemberPath(to)) return false;
|
|
183
|
+
const segs = to.split(".");
|
|
184
|
+
if (segs.length > 2) return false;
|
|
185
|
+
if (segs[0].endsWith("Store")) return false;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
function lastSeg(path) {
|
|
189
|
+
const i = path.lastIndexOf(".");
|
|
190
|
+
return i < 0 ? path : path.slice(i + 1);
|
|
191
|
+
}
|
|
192
|
+
function classifyTransition(t) {
|
|
193
|
+
const field = t.field;
|
|
194
|
+
const to = t.to;
|
|
195
|
+
if (field.endsWith("()")) {
|
|
196
|
+
if (t.args && t.args.length > 0)
|
|
197
|
+
return { field, kind: "context", arity: "nullary", reason: `\u65B9\u6CD5\u5E26\u53C2 ${field.slice(0, -2)}(${t.args})\uFF0C\u5B9E\u53C2\u4F9D\u8D56\u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587` };
|
|
198
|
+
return { field, kind: "call", arity: "nullary" };
|
|
199
|
+
}
|
|
200
|
+
if (isLiteral(to)) return { field, kind: "assign", arity: "nullary", to };
|
|
201
|
+
if (to.startsWith("!")) {
|
|
202
|
+
const inner = to.slice(1);
|
|
203
|
+
if (isMemberPath(inner)) return { field: lastSeg(inner), kind: "toggle", arity: "nullary" };
|
|
204
|
+
}
|
|
205
|
+
if (isPlaceholder(to)) return { field, kind: "assign", arity: "param" };
|
|
206
|
+
let reason = "\u4F9D\u8D56\u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587";
|
|
207
|
+
if (to.includes("e.target") || to.includes(".target.")) reason = "\u4F9D\u8D56\u4E8B\u4EF6\u5BF9\u8C61 e.target";
|
|
208
|
+
else if (to.includes(".filter(") || to.includes(".map(") || to.includes(".find(")) reason = "\u6570\u7EC4\u8FD0\u7B97,\u4F9D\u8D56\u5F53\u524D\u96C6\u5408";
|
|
209
|
+
else if (to.includes("...")) reason = "\u5C55\u5F00\u8FD0\u7B97\u7B26,\u4F9D\u8D56\u5F53\u524D\u5FEB\u7167";
|
|
210
|
+
else if (to.includes("=>")) reason = "\u503C\u662F\u7BAD\u5934\u51FD\u6570/\u95ED\u5305";
|
|
211
|
+
else if (to.includes("?") && to.includes(":")) reason = "\u4E09\u5143\u8868\u8FBE\u5F0F,\u4F9D\u8D56\u8FD0\u884C\u65F6\u5206\u652F";
|
|
212
|
+
else if (to.includes(".message") || to.includes(".id") || to.includes(".name")) reason = "\u8BFB\u8FD0\u884C\u65F6\u5BF9\u8C61\u5C5E\u6027";
|
|
213
|
+
return { field, kind: "context", arity: "nullary", reason };
|
|
214
|
+
}
|
|
215
|
+
function classifyKnob(transitions) {
|
|
216
|
+
const ops = transitions.map(classifyTransition);
|
|
217
|
+
const executable = ops.length > 0 && ops.every((o) => o.kind !== "context");
|
|
218
|
+
const arity = ops.some((o) => o.arity === "param") ? "param" : "nullary";
|
|
219
|
+
return { ops, arity, executable };
|
|
220
|
+
}
|
|
221
|
+
async function buildPanel(filePaths, contents) {
|
|
222
|
+
await _webtreesitter.Parser.init();
|
|
223
|
+
const tsx = await _chunkDUUCVLC3cjs.getLang.call(void 0, ".tsx");
|
|
224
|
+
if (!tsx) return [];
|
|
225
|
+
const panel = [];
|
|
226
|
+
for (const fp of filePaths) {
|
|
227
|
+
if (!fp.endsWith(".tsx") && !fp.endsWith(".jsx")) continue;
|
|
228
|
+
let code;
|
|
229
|
+
if (_optionalChain([contents, 'optionalAccess', _20 => _20.has, 'call', _21 => _21(fp)])) code = contents.get(fp);
|
|
230
|
+
else {
|
|
231
|
+
try {
|
|
232
|
+
code = _fs.readFileSync.call(void 0, fp, "utf-8");
|
|
233
|
+
} catch (e) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
panel.push(...extractKnobs(fp, code, tsx));
|
|
238
|
+
}
|
|
239
|
+
return panel;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/read/stategraph.ts
|
|
243
|
+
|
|
244
|
+
var _path = require('path');
|
|
245
|
+
var _process = require('process'); var _process2 = _interopRequireDefault(_process);
|
|
246
|
+
var _tsmorph = require('ts-morph');
|
|
247
|
+
async function openProject(cwd = _process2.default.cwd()) {
|
|
248
|
+
const { Project } = await Promise.resolve().then(() => _interopRequireWildcard(require("ts-morph")));
|
|
249
|
+
const tsConfigFilePath = ["tsconfig.app.json", "tsconfig.json"].map((f) => _path.resolve.call(void 0, cwd, f)).find(_fs.existsSync);
|
|
250
|
+
if (!tsConfigFilePath)
|
|
251
|
+
throw new Error(`stategraph: no tsconfig.json (or tsconfig.app.json) found under ${cwd}`);
|
|
252
|
+
return new Project({ tsConfigFilePath });
|
|
253
|
+
}
|
|
254
|
+
function isModuleLevel(v) {
|
|
255
|
+
const stmt = v.getVariableStatement();
|
|
256
|
+
return !!stmt && _optionalChain([stmt, 'access', _22 => _22.getParent, 'call', _23 => _23(), 'optionalAccess', _24 => _24.getKind, 'call', _25 => _25()]) === _tsmorph.SyntaxKind.SourceFile;
|
|
257
|
+
}
|
|
258
|
+
var DEFAULT_STORE_MARKER = "_loading";
|
|
259
|
+
function isStateFactory(v, marker) {
|
|
260
|
+
const init = v.getInitializer();
|
|
261
|
+
if (!init || !_tsmorph.Node.isCallExpression(init))
|
|
262
|
+
return false;
|
|
263
|
+
return !!v.getType().getProperty(marker);
|
|
264
|
+
}
|
|
265
|
+
function classify(v, marker = DEFAULT_STORE_MARKER) {
|
|
266
|
+
const init = v.getInitializer();
|
|
267
|
+
if (!init)
|
|
268
|
+
return null;
|
|
269
|
+
const k = init.getKind();
|
|
270
|
+
if (k === _tsmorph.SyntaxKind.ArrowFunction || k === _tsmorph.SyntaxKind.FunctionExpression || k === _tsmorph.SyntaxKind.ClassExpression)
|
|
271
|
+
return "view";
|
|
272
|
+
if (isStateFactory(v, marker))
|
|
273
|
+
return "state";
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
function fieldOf(pa) {
|
|
277
|
+
if (!_tsmorph.Node.isPropertyAssignment(pa))
|
|
278
|
+
return null;
|
|
279
|
+
const valueInit = pa.getInitializer();
|
|
280
|
+
if (!valueInit)
|
|
281
|
+
return null;
|
|
282
|
+
const init = _tsmorph.Node.isAsExpression(valueInit) ? valueInit.getExpression().getText() : valueInit.getText();
|
|
283
|
+
const t = valueInit.getType();
|
|
284
|
+
const domain = t.isUnion() ? t.getUnionTypes().filter((u) => u.isStringLiteral()).map((u) => u.getLiteralValue()) : [];
|
|
285
|
+
return { name: pa.getName(), init, type: t.getText(valueInit), domain, line: pa.getStartLineNumber() };
|
|
286
|
+
}
|
|
287
|
+
function resolveStateObject(arg) {
|
|
288
|
+
if (_tsmorph.Node.isObjectLiteralExpression(arg))
|
|
289
|
+
return arg;
|
|
290
|
+
if (_tsmorph.Node.isIdentifier(arg)) {
|
|
291
|
+
const decl = _optionalChain([arg, 'access', _26 => _26.getSymbol, 'call', _27 => _27(), 'optionalAccess', _28 => _28.getDeclarations, 'call', _29 => _29(), 'optionalAccess', _30 => _30[0]]);
|
|
292
|
+
if (decl && _tsmorph.Node.isVariableDeclaration(decl)) {
|
|
293
|
+
const di = decl.getInitializer();
|
|
294
|
+
if (di && _tsmorph.Node.isObjectLiteralExpression(di))
|
|
295
|
+
return di;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
function extractStoreFields(v, marker = DEFAULT_STORE_MARKER) {
|
|
301
|
+
if (!isStateFactory(v, marker))
|
|
302
|
+
return [];
|
|
303
|
+
const init = v.getInitializer();
|
|
304
|
+
if (!_tsmorph.Node.isCallExpression(init))
|
|
305
|
+
return [];
|
|
306
|
+
const arg = init.getArguments()[0];
|
|
307
|
+
const stateObj = arg && resolveStateObject(arg);
|
|
308
|
+
if (!stateObj || !_tsmorph.Node.isObjectLiteralExpression(stateObj))
|
|
309
|
+
return [];
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const pa of stateObj.getProperties()) {
|
|
312
|
+
const f = fieldOf(pa);
|
|
313
|
+
if (f)
|
|
314
|
+
out.push(f);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
function growFromDecl(v, file, marker = DEFAULT_STORE_MARKER) {
|
|
319
|
+
const fields = extractStoreFields(v, marker).map((f) => ({
|
|
320
|
+
name: f.name,
|
|
321
|
+
type: f.type,
|
|
322
|
+
domain: f.domain,
|
|
323
|
+
line: f.line
|
|
324
|
+
}));
|
|
325
|
+
return {
|
|
326
|
+
store: v.getName(),
|
|
327
|
+
anchor: `${file}:${v.getStartLineNumber()}`,
|
|
328
|
+
fields
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function buildStateGraph(project, fileFor = (p) => p.split("/").slice(-2).join("/"), marker = DEFAULT_STORE_MARKER) {
|
|
332
|
+
const nodes = [];
|
|
333
|
+
for (const sf of project.getSourceFiles()) {
|
|
334
|
+
const fp = sf.getFilePath();
|
|
335
|
+
if (fp.endsWith(".d.ts") || fp.includes("/node_modules/"))
|
|
336
|
+
continue;
|
|
337
|
+
const file = fileFor(fp);
|
|
338
|
+
for (const v of sf.getVariableDeclarations()) {
|
|
339
|
+
if (!isModuleLevel(v) || classify(v, marker) !== "state")
|
|
340
|
+
continue;
|
|
341
|
+
const grown = growFromDecl(v, file, marker);
|
|
342
|
+
const seen = /* @__PURE__ */ new Set();
|
|
343
|
+
let refs = 0;
|
|
344
|
+
try {
|
|
345
|
+
for (const ref of v.findReferencesAsNodes()) {
|
|
346
|
+
refs++;
|
|
347
|
+
seen.add(ref.getSourceFile().getFilePath());
|
|
348
|
+
}
|
|
349
|
+
} catch (e2) {
|
|
350
|
+
}
|
|
351
|
+
nodes.push({ ...grown, refs, files: seen.size, rank: refs + seen.size * 3 });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
nodes.sort((a, b) => b.rank - a.rank);
|
|
355
|
+
return { nodes };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
exports.extractKnobs = extractKnobs; exports.fmtTransitions = fmtTransitions; exports.formatPanel = formatPanel; exports.classifyKnob = classifyKnob; exports.buildPanel = buildPanel; exports.openProject = openProject; exports.classify = classify; exports.extractStoreFields = extractStoreFields; exports.growFromDecl = growFromDecl; exports.buildStateGraph = buildStateGraph;
|