@insitue/capture-core 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/LICENSE +21 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +234 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rod Leviton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure DOM → data helpers. Browser APIs only — no transport/agent/fs,
|
|
3
|
+
* no dependencies. Shared by every vehicle.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Prune + sanitize a subtree: depth/breadth-capped, event handlers
|
|
7
|
+
* and secret-looking attrs stripped, text truncated. */
|
|
8
|
+
declare function serializeNode(el: Element, depth?: number, maxChildren?: number): SerializedNode;
|
|
9
|
+
declare function curateComputedStyles(el: Element): Record<string, string>;
|
|
10
|
+
/** The verbatim class list — the agent edits Tailwind classes, not
|
|
11
|
+
* inline styles, so the source-of-truth is `className`. */
|
|
12
|
+
declare function extractTailwindClasses(el: Element): string[];
|
|
13
|
+
/** A robust, reasonably-stable CSS path. Prefers #id, then a
|
|
14
|
+
* data-testid, else tag + nth-of-type up to a shortest unique path.
|
|
15
|
+
* Always present — the last-resort locator. */
|
|
16
|
+
declare function buildSelector(el: Element): string;
|
|
17
|
+
/** Tailwind-ish breakpoint label from viewport width. */
|
|
18
|
+
declare function breakpointFor(w: number): string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pure React-fiber → source resolver. No React dependency: reads the
|
|
22
|
+
* `__reactFiber$*` expando React attaches to host DOM nodes in dev,
|
|
23
|
+
* walks `_debugOwner`/`return`, and harvests `_debugSource`
|
|
24
|
+
* ({ fileName, lineNumber, columnNumber }) plus component names.
|
|
25
|
+
* Falls back to a build-injected `data-insitu-source` attribute.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
declare function resolveTarget(el: Element): CaptureTarget;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @insitue/capture-core — THE SEAM.
|
|
32
|
+
*
|
|
33
|
+
* Pure, serializable data model + interfaces shared by every InSitue
|
|
34
|
+
* vehicle (dev SDK now; production capture-only, browser extension or
|
|
35
|
+
* Electron later). This package MUST NOT import transport (ws), an
|
|
36
|
+
* agent SDK, or fs/git — that is enforced by lint (see eslint config)
|
|
37
|
+
* and by keeping this file dependency-free. The whole point: a
|
|
38
|
+
* `CaptureBundle` can cross any boundary unchanged, and swapping the
|
|
39
|
+
* `CaptureSink` is what turns "local agentic edit" into "prod
|
|
40
|
+
* capture-only" without a rewrite.
|
|
41
|
+
*/
|
|
42
|
+
/** Bump when `CaptureBundle`'s shape changes; sinks branch on it.
|
|
43
|
+
* v2: additive `screenshotUnavailable` (M5 — honest screenshots).
|
|
44
|
+
* v3: additive `screenshot.source` + `screenshot.qualityNote`
|
|
45
|
+
* (pixel-perfect layered capture — rasterise vs display-media). */
|
|
46
|
+
declare const CAPTURE_SCHEMA_VERSION: 3;
|
|
47
|
+
/** Bump when the WS envelope below changes; companion/SDK pin it.
|
|
48
|
+
* v2: agent edit-loop messages (M2). v3: session undo/commit (M3).
|
|
49
|
+
* v4: agent-activity (M6 — live "what it's doing" feedback). */
|
|
50
|
+
declare const PROTOCOL_VERSION: 4;
|
|
51
|
+
interface SourceLoc {
|
|
52
|
+
/** Repo-relative POSIX path, e.g. `components/MainBar.tsx`. */
|
|
53
|
+
file: string;
|
|
54
|
+
line: number;
|
|
55
|
+
column: number;
|
|
56
|
+
}
|
|
57
|
+
type SelectionMode = "element" | "rect" | "multi";
|
|
58
|
+
interface SelectionInput {
|
|
59
|
+
mode: SelectionMode;
|
|
60
|
+
/** `elementsFromPoint` chain at the click (outermost→innermost). */
|
|
61
|
+
pointerPath?: Element[];
|
|
62
|
+
/** Freeform region in viewport CSS px. */
|
|
63
|
+
rect?: {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
interface SerializedNode {
|
|
71
|
+
tag: string;
|
|
72
|
+
attrs: Record<string, string>;
|
|
73
|
+
/** Truncated text content for leaf-ish nodes. */
|
|
74
|
+
text?: string;
|
|
75
|
+
children: SerializedNode[];
|
|
76
|
+
}
|
|
77
|
+
interface ConsoleEntry {
|
|
78
|
+
level: "log" | "info" | "warn" | "error" | "debug";
|
|
79
|
+
args: string[];
|
|
80
|
+
ts: number;
|
|
81
|
+
}
|
|
82
|
+
interface NetworkEntry {
|
|
83
|
+
url: string;
|
|
84
|
+
method: string;
|
|
85
|
+
status?: number;
|
|
86
|
+
ok: boolean;
|
|
87
|
+
ts: number;
|
|
88
|
+
}
|
|
89
|
+
interface RuntimeError {
|
|
90
|
+
message: string;
|
|
91
|
+
stack?: string;
|
|
92
|
+
ts: number;
|
|
93
|
+
}
|
|
94
|
+
/** How confident the DOM→source resolution is. Surfaced in the UI and
|
|
95
|
+
* handed to the agent so it knows whether file:line is trustworthy. */
|
|
96
|
+
type SourceConfidence = "exact" | "approximate" | "selector-only";
|
|
97
|
+
interface CaptureTarget {
|
|
98
|
+
source?: SourceLoc;
|
|
99
|
+
confidence: SourceConfidence;
|
|
100
|
+
/** Outer→inner component ownership chain (React fiber `_debugOwner`). */
|
|
101
|
+
componentStack: Array<{
|
|
102
|
+
name: string;
|
|
103
|
+
source?: SourceLoc;
|
|
104
|
+
}>;
|
|
105
|
+
/** Robust CSS path — always present, the last-resort locator. */
|
|
106
|
+
selector: string;
|
|
107
|
+
}
|
|
108
|
+
interface CaptureBundle {
|
|
109
|
+
schemaVersion: typeof CAPTURE_SCHEMA_VERSION;
|
|
110
|
+
id: string;
|
|
111
|
+
createdAt: string;
|
|
112
|
+
target: CaptureTarget | null;
|
|
113
|
+
domSubtree: SerializedNode;
|
|
114
|
+
computedStyles: Record<string, string>;
|
|
115
|
+
tailwindClasses: string[];
|
|
116
|
+
screenshot?: {
|
|
117
|
+
mime: "image/png";
|
|
118
|
+
dataUrl: string;
|
|
119
|
+
bounds: {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
};
|
|
125
|
+
/** Which capture path produced this screenshot. `rasterise` =
|
|
126
|
+
* html-to-image full-document render + crop (no permission).
|
|
127
|
+
* `display-media` = `getDisplayMedia` OS-compositor capture (one
|
|
128
|
+
* permission per session, pixel-perfect across any content). */
|
|
129
|
+
source?: "rasterise" | "display-media";
|
|
130
|
+
/** Human-readable note when the capture is structurally complete
|
|
131
|
+
* but visually imperfect — e.g. some non-CORS images fell back
|
|
132
|
+
* to a placeholder because the user declined `getDisplayMedia`. */
|
|
133
|
+
qualityNote?: string;
|
|
134
|
+
};
|
|
135
|
+
/** Set (and `screenshot` omitted) when an in-browser rasterise was
|
|
136
|
+
* impossible — e.g. cross-origin media taints the canvas. Honest
|
|
137
|
+
* signal so the UI/sink never shows a blank box claiming success. */
|
|
138
|
+
screenshotUnavailable?: string;
|
|
139
|
+
viewport: {
|
|
140
|
+
w: number;
|
|
141
|
+
h: number;
|
|
142
|
+
dpr: number;
|
|
143
|
+
breakpoint?: string;
|
|
144
|
+
};
|
|
145
|
+
runtime: {
|
|
146
|
+
url: string;
|
|
147
|
+
route?: string;
|
|
148
|
+
console: ConsoleEntry[];
|
|
149
|
+
network: NetworkEntry[];
|
|
150
|
+
errors: RuntimeError[];
|
|
151
|
+
};
|
|
152
|
+
userNote?: string;
|
|
153
|
+
}
|
|
154
|
+
/** Builds bundles from a selection. Implemented by the SDK; the
|
|
155
|
+
* contract is what an extension/Electron vehicle reuses verbatim. */
|
|
156
|
+
interface CaptureCore {
|
|
157
|
+
beginPick(opts?: {
|
|
158
|
+
mode?: SelectionMode;
|
|
159
|
+
}): Promise<SelectionInput>;
|
|
160
|
+
buildBundle(sel: SelectionInput): Promise<CaptureBundle>;
|
|
161
|
+
}
|
|
162
|
+
/** The swap point. v1 local = WebSocketAgentSink (→ companion → agent
|
|
163
|
+
* edits). Future prod = IssueTrackerSink (same bundle → hosted task,
|
|
164
|
+
* no auto-edit). capture-core never knows which. */
|
|
165
|
+
interface CaptureSink {
|
|
166
|
+
readonly kind: string;
|
|
167
|
+
submit(bundle: CaptureBundle): Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
interface IssueDraft {
|
|
170
|
+
title: string;
|
|
171
|
+
/** Markdown summary a human/tracker can read at a glance. */
|
|
172
|
+
body: string;
|
|
173
|
+
/** Full bundle, attached verbatim for the agent-ready future. */
|
|
174
|
+
bundle: CaptureBundle;
|
|
175
|
+
}
|
|
176
|
+
/** Turn a bundle into a tracker-ready draft. Source is included when
|
|
177
|
+
* present (fiber/attribute resolved it client-side) and degrades
|
|
178
|
+
* gracefully to the always-present selector when it isn't. */
|
|
179
|
+
declare function toIssueDraft(bundle: CaptureBundle): IssueDraft;
|
|
180
|
+
/** A CaptureSink that produces an `IssueDraft` and hands it to a
|
|
181
|
+
* caller-supplied delivery fn (download/gh/HTTP — not our concern).
|
|
182
|
+
* Same bundle, different sink: this is the local→prod door. */
|
|
183
|
+
declare class IssueTrackerSink implements CaptureSink {
|
|
184
|
+
private readonly deliver;
|
|
185
|
+
readonly kind = "issue-tracker";
|
|
186
|
+
constructor(deliver: (draft: IssueDraft) => void | Promise<void>);
|
|
187
|
+
submit(bundle: CaptureBundle): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
interface HelloMsg {
|
|
190
|
+
t: "hello";
|
|
191
|
+
protocolVersion: typeof PROTOCOL_VERSION;
|
|
192
|
+
token: string;
|
|
193
|
+
}
|
|
194
|
+
interface HelloOkMsg {
|
|
195
|
+
t: "hello-ok";
|
|
196
|
+
companionVersion: string;
|
|
197
|
+
}
|
|
198
|
+
interface PingMsg {
|
|
199
|
+
t: "ping";
|
|
200
|
+
nonce: string;
|
|
201
|
+
}
|
|
202
|
+
interface PongMsg {
|
|
203
|
+
t: "pong";
|
|
204
|
+
nonce: string;
|
|
205
|
+
}
|
|
206
|
+
interface ErrorMsg {
|
|
207
|
+
t: "error";
|
|
208
|
+
code: "bad-token" | "bad-origin" | "bad-protocol" | "internal";
|
|
209
|
+
message: string;
|
|
210
|
+
}
|
|
211
|
+
interface CaptureSubmitMsg {
|
|
212
|
+
t: "capture";
|
|
213
|
+
bundle: CaptureBundle;
|
|
214
|
+
}
|
|
215
|
+
/** Companion's resolution of a submitted bundle's source target. */
|
|
216
|
+
interface ResolvedSource {
|
|
217
|
+
/** Repo-relative POSIX path the companion resolved. */
|
|
218
|
+
file: string;
|
|
219
|
+
line: number;
|
|
220
|
+
column: number;
|
|
221
|
+
/** A few lines around `line` from the real file. */
|
|
222
|
+
snippet: string;
|
|
223
|
+
/** Whole component file path, if distinct/known. */
|
|
224
|
+
componentFile?: string;
|
|
225
|
+
}
|
|
226
|
+
interface CaptureResolvedMsg {
|
|
227
|
+
t: "capture-resolved";
|
|
228
|
+
id: string;
|
|
229
|
+
resolved: ResolvedSource | null;
|
|
230
|
+
/** Human note, e.g. why resolution was selector-only. */
|
|
231
|
+
note: string;
|
|
232
|
+
}
|
|
233
|
+
type AgentErrorCode = "not-logged-in" | "api-key-set" | "claude-missing" | "transport" | "internal";
|
|
234
|
+
/** A normalized file edit the agent proposes (never auto-applied). */
|
|
235
|
+
interface ProposedEdit {
|
|
236
|
+
/** Repo-relative POSIX path. */
|
|
237
|
+
file: string;
|
|
238
|
+
/** Whole new file contents (companion diffs vs disk). */
|
|
239
|
+
contents: string;
|
|
240
|
+
/** Optional one-line rationale from the agent. */
|
|
241
|
+
why?: string;
|
|
242
|
+
}
|
|
243
|
+
type AgentEvent = {
|
|
244
|
+
t: "agent-text";
|
|
245
|
+
turnId: string;
|
|
246
|
+
delta: string;
|
|
247
|
+
} | {
|
|
248
|
+
t: "agent-thinking";
|
|
249
|
+
turnId: string;
|
|
250
|
+
note: string;
|
|
251
|
+
}
|
|
252
|
+
/** Live "what it's doing" signal (tool use / phase) — UI progress
|
|
253
|
+
* only, never part of the transcript. */
|
|
254
|
+
| {
|
|
255
|
+
t: "agent-activity";
|
|
256
|
+
turnId: string;
|
|
257
|
+
kind: "tool" | "thinking" | "start";
|
|
258
|
+
label: string;
|
|
259
|
+
} | {
|
|
260
|
+
t: "agent-tool-proposal";
|
|
261
|
+
turnId: string;
|
|
262
|
+
edit: ProposedEdit;
|
|
263
|
+
} | {
|
|
264
|
+
t: "agent-turn-complete";
|
|
265
|
+
turnId: string;
|
|
266
|
+
} | {
|
|
267
|
+
t: "agent-error";
|
|
268
|
+
turnId: string;
|
|
269
|
+
code: AgentErrorCode;
|
|
270
|
+
message: string;
|
|
271
|
+
};
|
|
272
|
+
/** Companion preflight result for the active provider/transport. */
|
|
273
|
+
interface AgentStatusMsg {
|
|
274
|
+
t: "agent-status";
|
|
275
|
+
ready: boolean;
|
|
276
|
+
transport: "cli-headless" | "mcp" | "sdk";
|
|
277
|
+
warnings: string[];
|
|
278
|
+
blockers: string[];
|
|
279
|
+
}
|
|
280
|
+
interface AgentStreamMsg {
|
|
281
|
+
t: "agent-stream";
|
|
282
|
+
event: AgentEvent;
|
|
283
|
+
}
|
|
284
|
+
interface ChangesetProposedMsg {
|
|
285
|
+
t: "changeset-proposed";
|
|
286
|
+
turnId: string;
|
|
287
|
+
files: Array<{
|
|
288
|
+
file: string;
|
|
289
|
+
diff: string;
|
|
290
|
+
bytes: number;
|
|
291
|
+
}>;
|
|
292
|
+
}
|
|
293
|
+
interface ChangesetAppliedMsg {
|
|
294
|
+
t: "changeset-applied";
|
|
295
|
+
turnId: string;
|
|
296
|
+
files: string[];
|
|
297
|
+
checkpointRef: string;
|
|
298
|
+
}
|
|
299
|
+
interface AgentUndoneMsg {
|
|
300
|
+
t: "agent-undone";
|
|
301
|
+
turnId: string;
|
|
302
|
+
restored: string[];
|
|
303
|
+
}
|
|
304
|
+
interface AgentTurnMsg {
|
|
305
|
+
t: "agent-turn";
|
|
306
|
+
turnId: string;
|
|
307
|
+
bundleId: string;
|
|
308
|
+
userMessage: string;
|
|
309
|
+
}
|
|
310
|
+
interface AgentDecisionMsg {
|
|
311
|
+
t: "agent-decision";
|
|
312
|
+
turnId: string;
|
|
313
|
+
decision: "approve" | "reject";
|
|
314
|
+
/** Subset of files to act on; omitted = whole changeset. */
|
|
315
|
+
files?: string[];
|
|
316
|
+
reason?: string;
|
|
317
|
+
}
|
|
318
|
+
interface AgentCancelMsg {
|
|
319
|
+
t: "agent-cancel";
|
|
320
|
+
turnId: string;
|
|
321
|
+
}
|
|
322
|
+
interface AgentUndoMsg {
|
|
323
|
+
t: "agent-undo";
|
|
324
|
+
turnId: string;
|
|
325
|
+
}
|
|
326
|
+
/** Undo every checkpoint applied this session (reverse order). */
|
|
327
|
+
interface AgentUndoSessionMsg {
|
|
328
|
+
t: "agent-undo-session";
|
|
329
|
+
}
|
|
330
|
+
/** Explicit, user-initiated git commit of ONLY the files InSitue
|
|
331
|
+
* applied this session. Never auto; never pushes. */
|
|
332
|
+
interface AgentCommitSessionMsg {
|
|
333
|
+
t: "agent-commit-session";
|
|
334
|
+
message?: string;
|
|
335
|
+
}
|
|
336
|
+
interface AgentSessionUndoneMsg {
|
|
337
|
+
t: "agent-session-undone";
|
|
338
|
+
restored: string[];
|
|
339
|
+
}
|
|
340
|
+
interface AgentSessionCommittedMsg {
|
|
341
|
+
t: "agent-session-committed";
|
|
342
|
+
/** Short commit sha. */
|
|
343
|
+
commit: string;
|
|
344
|
+
files: string[];
|
|
345
|
+
}
|
|
346
|
+
/** Client→server messages. */
|
|
347
|
+
type ClientMessage = HelloMsg | PingMsg | CaptureSubmitMsg | AgentTurnMsg | AgentDecisionMsg | AgentCancelMsg | AgentUndoMsg | AgentUndoSessionMsg | AgentCommitSessionMsg;
|
|
348
|
+
/** Server→client messages. */
|
|
349
|
+
type ServerMessage = HelloOkMsg | PongMsg | ErrorMsg | CaptureResolvedMsg | AgentStatusMsg | AgentStreamMsg | ChangesetProposedMsg | ChangesetAppliedMsg | AgentUndoneMsg | AgentSessionUndoneMsg | AgentSessionCommittedMsg;
|
|
350
|
+
|
|
351
|
+
export { type AgentCancelMsg, type AgentCommitSessionMsg, type AgentDecisionMsg, type AgentErrorCode, type AgentEvent, type AgentSessionCommittedMsg, type AgentSessionUndoneMsg, type AgentStatusMsg, type AgentStreamMsg, type AgentTurnMsg, type AgentUndoMsg, type AgentUndoSessionMsg, type AgentUndoneMsg, CAPTURE_SCHEMA_VERSION, type CaptureBundle, type CaptureCore, type CaptureResolvedMsg, type CaptureSink, type CaptureSubmitMsg, type CaptureTarget, type ChangesetAppliedMsg, type ChangesetProposedMsg, type ClientMessage, type ConsoleEntry, type ErrorMsg, type HelloMsg, type HelloOkMsg, type IssueDraft, IssueTrackerSink, type NetworkEntry, PROTOCOL_VERSION, type PingMsg, type PongMsg, type ProposedEdit, type ResolvedSource, type RuntimeError, type SelectionInput, type SelectionMode, type SerializedNode, type ServerMessage, type SourceConfidence, type SourceLoc, breakpointFor, buildSelector, curateComputedStyles, extractTailwindClasses, resolveTarget, serializeNode, toIssueDraft };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// src/dom.ts
|
|
2
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
|
|
3
|
+
var ATTR_DENY = /^(on|data-insitu)/i;
|
|
4
|
+
var SECRETISH = /(token|secret|key|password|authorization|bearer)/i;
|
|
5
|
+
function serializeNode(el, depth = 3, maxChildren = 12) {
|
|
6
|
+
const attrs = {};
|
|
7
|
+
for (const a of Array.from(el.attributes)) {
|
|
8
|
+
if (ATTR_DENY.test(a.name)) continue;
|
|
9
|
+
const v = SECRETISH.test(a.name) ? "[redacted]" : a.value;
|
|
10
|
+
attrs[a.name] = v.length > 300 ? v.slice(0, 300) + "\u2026" : v;
|
|
11
|
+
}
|
|
12
|
+
const node = {
|
|
13
|
+
tag: el.tagName.toLowerCase(),
|
|
14
|
+
attrs,
|
|
15
|
+
children: []
|
|
16
|
+
};
|
|
17
|
+
const directText = Array.from(el.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent ?? "").join(" ").trim();
|
|
18
|
+
if (directText) node.text = directText.slice(0, 200);
|
|
19
|
+
if (depth > 0) {
|
|
20
|
+
const kids = Array.from(el.children).filter((c) => !SKIP_TAGS.has(c.tagName)).slice(0, maxChildren);
|
|
21
|
+
node.children = kids.map((c) => serializeNode(c, depth - 1, maxChildren));
|
|
22
|
+
}
|
|
23
|
+
return node;
|
|
24
|
+
}
|
|
25
|
+
var STYLE_KEYS = [
|
|
26
|
+
"display",
|
|
27
|
+
"position",
|
|
28
|
+
"boxSizing",
|
|
29
|
+
"width",
|
|
30
|
+
"height",
|
|
31
|
+
"margin",
|
|
32
|
+
"padding",
|
|
33
|
+
"border",
|
|
34
|
+
"flexDirection",
|
|
35
|
+
"alignItems",
|
|
36
|
+
"justifyContent",
|
|
37
|
+
"gap",
|
|
38
|
+
"gridTemplateColumns",
|
|
39
|
+
"color",
|
|
40
|
+
"backgroundColor",
|
|
41
|
+
"fontFamily",
|
|
42
|
+
"fontSize",
|
|
43
|
+
"fontWeight",
|
|
44
|
+
"lineHeight",
|
|
45
|
+
"letterSpacing",
|
|
46
|
+
"textAlign",
|
|
47
|
+
"borderRadius",
|
|
48
|
+
"boxShadow",
|
|
49
|
+
"opacity",
|
|
50
|
+
"zIndex"
|
|
51
|
+
];
|
|
52
|
+
function curateComputedStyles(el) {
|
|
53
|
+
const cs = getComputedStyle(el);
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const k of STYLE_KEYS) {
|
|
56
|
+
const v = cs[k];
|
|
57
|
+
if (typeof v === "string" && v && v !== "normal" && v !== "none") {
|
|
58
|
+
out[k] = v;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function extractTailwindClasses(el) {
|
|
64
|
+
const cls = typeof el.className === "string" ? el.className : el.getAttribute("class") ?? "";
|
|
65
|
+
return cls.split(/\s+/).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
function buildSelector(el) {
|
|
68
|
+
if (el.id && document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1) {
|
|
69
|
+
return `#${CSS.escape(el.id)}`;
|
|
70
|
+
}
|
|
71
|
+
const parts = [];
|
|
72
|
+
let cur = el;
|
|
73
|
+
while (cur && cur.nodeType === Node.ELEMENT_NODE && parts.length < 6) {
|
|
74
|
+
const tid = cur.getAttribute("data-testid");
|
|
75
|
+
if (tid) {
|
|
76
|
+
parts.unshift(`[data-testid="${CSS.escape(tid)}"]`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const tag = cur.tagName.toLowerCase();
|
|
80
|
+
const parent = cur.parentElement;
|
|
81
|
+
if (!parent) {
|
|
82
|
+
parts.unshift(tag);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const sibs = Array.from(parent.children).filter(
|
|
86
|
+
(c) => c.tagName === cur.tagName
|
|
87
|
+
);
|
|
88
|
+
const idx = sibs.indexOf(cur);
|
|
89
|
+
parts.unshift(
|
|
90
|
+
sibs.length > 1 ? `${tag}:nth-of-type(${idx + 1})` : tag
|
|
91
|
+
);
|
|
92
|
+
if (cur.id) {
|
|
93
|
+
parts.unshift(`#${CSS.escape(cur.id)}`);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
cur = parent;
|
|
97
|
+
}
|
|
98
|
+
return parts.join(" > ");
|
|
99
|
+
}
|
|
100
|
+
function breakpointFor(w) {
|
|
101
|
+
if (w < 640) return "xs";
|
|
102
|
+
if (w < 768) return "sm";
|
|
103
|
+
if (w < 1024) return "md";
|
|
104
|
+
if (w < 1280) return "lg";
|
|
105
|
+
if (w < 1536) return "xl";
|
|
106
|
+
return "2xl";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/react-source.ts
|
|
110
|
+
function getFiber(el) {
|
|
111
|
+
for (const k of Object.keys(el)) {
|
|
112
|
+
if (k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")) {
|
|
113
|
+
return el[k] ?? null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function compName(type) {
|
|
119
|
+
if (typeof type === "function") {
|
|
120
|
+
const f = type;
|
|
121
|
+
return f.displayName || f.name || "Anonymous";
|
|
122
|
+
}
|
|
123
|
+
if (type && typeof type === "object") {
|
|
124
|
+
const o = type;
|
|
125
|
+
return o.displayName || o.render?.name || null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function toLoc(workspaceCwdRelative) {
|
|
130
|
+
if (!workspaceCwdRelative.fileName) return null;
|
|
131
|
+
return {
|
|
132
|
+
// React gives an absolute path in dev; the companion re-roots it.
|
|
133
|
+
file: workspaceCwdRelative.fileName,
|
|
134
|
+
line: workspaceCwdRelative.lineNumber ?? 1,
|
|
135
|
+
column: workspaceCwdRelative.columnNumber ?? 1
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function fromAttribute(el) {
|
|
139
|
+
let cur = el;
|
|
140
|
+
for (let i = 0; cur && i < 8; i++, cur = cur.parentElement) {
|
|
141
|
+
const raw = cur.getAttribute("data-insitu-source");
|
|
142
|
+
if (raw) {
|
|
143
|
+
const m = /^(.*):(\d+):(\d+)$/.exec(raw);
|
|
144
|
+
if (m) return { file: m[1], line: Number(m[2]), column: Number(m[3]) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function resolveTarget(el) {
|
|
150
|
+
const selector = buildSelector(el);
|
|
151
|
+
const fiber = getFiber(el);
|
|
152
|
+
const componentStack = [];
|
|
153
|
+
if (fiber) {
|
|
154
|
+
let f = fiber;
|
|
155
|
+
let guard = 0;
|
|
156
|
+
while (f && guard++ < 60) {
|
|
157
|
+
const name = compName(f.type);
|
|
158
|
+
if (name) {
|
|
159
|
+
const src = f._debugSource ? toLoc(f._debugSource) : null;
|
|
160
|
+
componentStack.push(
|
|
161
|
+
src ? { name, source: src } : { name }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
f = f._debugOwner ?? f.return ?? null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
let source;
|
|
168
|
+
let confidence = "selector-only";
|
|
169
|
+
const hostSrc = fiber?._debugSource ? toLoc(fiber._debugSource) : null;
|
|
170
|
+
if (hostSrc) {
|
|
171
|
+
source = hostSrc;
|
|
172
|
+
confidence = "exact";
|
|
173
|
+
} else {
|
|
174
|
+
const attrSrc = fromAttribute(el);
|
|
175
|
+
if (attrSrc) {
|
|
176
|
+
source = attrSrc;
|
|
177
|
+
confidence = "exact";
|
|
178
|
+
} else {
|
|
179
|
+
const ownerWithSrc = componentStack.find((c) => c.source);
|
|
180
|
+
if (ownerWithSrc?.source) {
|
|
181
|
+
source = ownerWithSrc.source;
|
|
182
|
+
confidence = "approximate";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return source === void 0 ? { confidence, componentStack, selector } : { source, confidence, componentStack, selector };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/index.ts
|
|
190
|
+
var CAPTURE_SCHEMA_VERSION = 3;
|
|
191
|
+
var PROTOCOL_VERSION = 4;
|
|
192
|
+
function toIssueDraft(bundle) {
|
|
193
|
+
const t = bundle.target;
|
|
194
|
+
const where = t?.source ? `\`${t.source.file}:${t.source.line}\` (${t.confidence})` : t ? `\`${t.selector}\` (selector-only \u2014 no source resolver)` : "(empty selection)";
|
|
195
|
+
const stack = t?.componentStack.map((c) => c.name).join(" < ") || "(none)";
|
|
196
|
+
const errs = bundle.runtime.errors.length;
|
|
197
|
+
const title = `[InSitue] ${t?.componentStack[0]?.name ?? t?.selector ?? "selection"} on ${bundle.runtime.route ?? new URL(bundle.runtime.url).pathname}`;
|
|
198
|
+
const body = [
|
|
199
|
+
`**Where:** ${where}`,
|
|
200
|
+
`**Components:** ${stack}`,
|
|
201
|
+
`**URL:** ${bundle.runtime.url}`,
|
|
202
|
+
`**Viewport:** ${bundle.viewport.w}\xD7${bundle.viewport.h}${bundle.viewport.breakpoint ? ` (${bundle.viewport.breakpoint})` : ""}`,
|
|
203
|
+
`**Tailwind:** ${bundle.tailwindClasses.join(" ") || "\u2014"}`,
|
|
204
|
+
`**Runtime:** ${bundle.runtime.console.length} log \xB7 ${bundle.runtime.network.length} net \xB7 ${errs} err`,
|
|
205
|
+
`**Screenshot:** ${bundle.screenshot ? `attached` + (bundle.screenshot.source ? ` (${bundle.screenshot.source})` : "") + (bundle.screenshot.qualityNote ? ` \u2014 ${bundle.screenshot.qualityNote}` : "") : bundle.screenshotUnavailable ? `unavailable \u2014 ${bundle.screenshotUnavailable}` : "\u2014"}`,
|
|
206
|
+
bundle.userNote ? `
|
|
207
|
+
> ${bundle.userNote}` : "",
|
|
208
|
+
`
|
|
209
|
+
_Captured ${bundle.createdAt} \xB7 schema v${bundle.schemaVersion}_`
|
|
210
|
+
].join("\n");
|
|
211
|
+
return { title, body, bundle };
|
|
212
|
+
}
|
|
213
|
+
var IssueTrackerSink = class {
|
|
214
|
+
constructor(deliver) {
|
|
215
|
+
this.deliver = deliver;
|
|
216
|
+
}
|
|
217
|
+
deliver;
|
|
218
|
+
kind = "issue-tracker";
|
|
219
|
+
async submit(bundle) {
|
|
220
|
+
await this.deliver(toIssueDraft(bundle));
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
export {
|
|
224
|
+
CAPTURE_SCHEMA_VERSION,
|
|
225
|
+
IssueTrackerSink,
|
|
226
|
+
PROTOCOL_VERSION,
|
|
227
|
+
breakpointFor,
|
|
228
|
+
buildSelector,
|
|
229
|
+
curateComputedStyles,
|
|
230
|
+
extractTailwindClasses,
|
|
231
|
+
resolveTarget,
|
|
232
|
+
serializeNode,
|
|
233
|
+
toIssueDraft
|
|
234
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@insitue/capture-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "The seam: pure, transport/agent/fs-free capture model shared by every InSitue vehicle.",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.3.5",
|
|
24
|
+
"typescript": "^5.6.3"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
28
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"lint": "tsc --noEmit"
|
|
31
|
+
}
|
|
32
|
+
}
|