@leftium/gg 0.0.49 → 0.0.51
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 +108 -2
- package/dist/debug/browser.d.ts +1 -1
- package/dist/debug/browser.js +14 -6
- package/dist/debug/node.js +5 -3
- package/dist/eruda/buffer.d.ts +4 -0
- package/dist/eruda/buffer.js +16 -0
- package/dist/eruda/loader.js +58 -0
- package/dist/eruda/plugin.d.ts +5 -1
- package/dist/eruda/plugin.js +1219 -379
- package/dist/eruda/types.d.ts +20 -1
- package/dist/gg-call-sites-plugin.js +11 -4
- package/dist/gg-file-sink-plugin.d.ts +6 -0
- package/dist/gg-file-sink-plugin.js +394 -0
- package/dist/gg.d.ts +3 -0
- package/dist/gg.js +160 -62
- package/dist/open-in-editor.js +1 -1
- package/dist/pattern.d.ts +23 -0
- package/dist/pattern.js +41 -0
- package/dist/vite.d.ts +12 -2
- package/dist/vite.js +7 -1
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -273,16 +273,122 @@ import ggPlugins from '@leftium/gg/vite';
|
|
|
273
273
|
|
|
274
274
|
ggPlugins({
|
|
275
275
|
callSites: { srcRootPattern: '.*?(/src/)' },
|
|
276
|
-
openInEditor: false // disable open-in-editor middleware
|
|
276
|
+
openInEditor: false, // disable open-in-editor middleware
|
|
277
|
+
fileSink: true // write all gg() calls to .gg/logs-{port}.jsonl (for coding agents)
|
|
277
278
|
});
|
|
278
279
|
```
|
|
279
280
|
|
|
281
|
+
| Option | Type | Default | Description |
|
|
282
|
+
| -------------- | ----------------------------- | ------- | ------------------------------------------------------------- |
|
|
283
|
+
| `callSites` | `boolean \| CallSiteOptions` | `true` | Rewrites `gg()` calls with source file/line/col metadata |
|
|
284
|
+
| `openInEditor` | `boolean` | `true` | Adds dev server middleware for click-to-open source files |
|
|
285
|
+
| `fileSink` | `boolean \| { dir?: string }` | `false` | Writes all `gg()` calls to `.gg/logs-{port}.jsonl` (dev only) |
|
|
286
|
+
|
|
280
287
|
Individual plugins are also available for advanced setups:
|
|
281
288
|
|
|
282
289
|
```ts
|
|
283
|
-
import { ggCallSitesPlugin, openInEditorPlugin } from '@leftium/gg/vite';
|
|
290
|
+
import { ggCallSitesPlugin, openInEditorPlugin, ggFileSinkPlugin } from '@leftium/gg/vite';
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Coding Agent Access
|
|
294
|
+
|
|
295
|
+
With `fileSink: true`, all `gg()` calls -- both browser-side and server-side -- are written to `.gg/logs-{port}.jsonl` during development. Coding agents (Claude, Cursor, etc.) can read this file directly without clipboard, browser automation, or manual copy/paste.
|
|
296
|
+
|
|
297
|
+
Add `.gg/` to your `.gitignore` to keep log files out of version control.
|
|
298
|
+
|
|
299
|
+
**Enable in `vite.config.ts`:**
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
ggPlugins({ fileSink: true });
|
|
284
303
|
```
|
|
285
304
|
|
|
305
|
+
**Typical agent workflow:**
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# 1. Clear logs before the action under investigation
|
|
309
|
+
curl -X DELETE http://localhost:5173/__gg/logs
|
|
310
|
+
|
|
311
|
+
# 2. Trigger the action (page load, button click, etc.)
|
|
312
|
+
|
|
313
|
+
# 3. Read the logs — SSR duplicates are removed by default
|
|
314
|
+
curl -s http://localhost:5173/__gg/logs
|
|
315
|
+
|
|
316
|
+
# Or query the file directly with jq
|
|
317
|
+
jq 'select(.lvl == "error")' .gg/logs-5173.jsonl
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Each JSONL line contains:**
|
|
321
|
+
|
|
322
|
+
| Field | Description |
|
|
323
|
+
| -------- | ------------------------------------------------------------------------------------------- |
|
|
324
|
+
| `ns` | Namespace (file + function, e.g., `gg:routes/+page.svelte@click`) |
|
|
325
|
+
| `msg` | Formatted message string |
|
|
326
|
+
| `ts` | Unix epoch ms |
|
|
327
|
+
| `lvl` | `"debug"` \| `"info"` \| `"warn"` \| `"error"` (omitted if debug) |
|
|
328
|
+
| `env` | `"client"` or `"server"` -- which runtime produced this entry |
|
|
329
|
+
| `origin` | `"tauri"` \| `"browser"` (client entries only) |
|
|
330
|
+
| `file` | Source file path |
|
|
331
|
+
| `line` | Source line number |
|
|
332
|
+
| `count` | Repeat count when consecutive entries share the same `ns`+`msg` (HTTP only, omitted when 1) |
|
|
333
|
+
|
|
334
|
+
**HTTP API:**
|
|
335
|
+
|
|
336
|
+
In SSR apps, component `gg()` calls fire on both server and client. The HTTP endpoint deduplicates by default: server entries are canonical; a client entry at the same `[ns, line]` is dropped if its `msg` is identical. Client-only call sites (e.g. `onMount`) and hydration mismatches (same `[ns, line]`, different `msg`) are always kept.
|
|
337
|
+
|
|
338
|
+
Consecutive repeated messages are collapsed by default (Chrome DevTools-style `count` field on the entry). Use `?raw` to disable collapsing and get one line per entry.
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Default — deduplicated, repeated messages collapsed
|
|
342
|
+
curl -s http://localhost:5173/__gg/logs
|
|
343
|
+
|
|
344
|
+
# Prefer jq over grep for filtering NDJSON — cleaner field access
|
|
345
|
+
curl -s http://localhost:5173/__gg/logs | jq 'select(.msg | test("myFunction"))'
|
|
346
|
+
curl -s http://localhost:5173/__gg/logs | jq -r '.msg'
|
|
347
|
+
curl -s http://localhost:5173/__gg/logs | jq 'select(.lvl == "error")'
|
|
348
|
+
|
|
349
|
+
# All entries, both sides (raw file contents)
|
|
350
|
+
curl -s "http://localhost:5173/__gg/logs?all"
|
|
351
|
+
|
|
352
|
+
# Unrolled — one line per entry, no count collapsing
|
|
353
|
+
curl -s "http://localhost:5173/__gg/logs?raw"
|
|
354
|
+
|
|
355
|
+
# Only call sites where server and client produced different values (hydration mismatches)
|
|
356
|
+
curl -s "http://localhost:5173/__gg/logs?mismatch"
|
|
357
|
+
|
|
358
|
+
# Filter by namespace glob, environment, origin, or timestamp — all compose with dedup
|
|
359
|
+
curl -s "http://localhost:5173/__gg/logs?filter=api:*&env=server"
|
|
360
|
+
curl -s "http://localhost:5173/__gg/logs?origin=tauri"
|
|
361
|
+
curl -s "http://localhost:5173/__gg/logs?since=1741234567890"
|
|
362
|
+
|
|
363
|
+
# Status and endpoint list
|
|
364
|
+
curl -s http://localhost:5173/__gg/
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Querying the file directly with `jq`:**
|
|
368
|
+
|
|
369
|
+
The JSONL file always contains all entries (both sides). Use `jq` when you need aggregation, field extraction, or queries the HTTP API doesn't cover:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Check the env split (useful in SSR apps)
|
|
373
|
+
jq -s 'group_by(.env) | map({env: .[0].env, count: length})' .gg/logs-5173.jsonl
|
|
374
|
+
|
|
375
|
+
# Errors only
|
|
376
|
+
jq 'select(.lvl == "error")' .gg/logs-5173.jsonl
|
|
377
|
+
|
|
378
|
+
# Entries from a specific file
|
|
379
|
+
jq 'select(.file | contains("+page.svelte"))' .gg/logs-5173.jsonl
|
|
380
|
+
|
|
381
|
+
# Messages with source location
|
|
382
|
+
jq -r '"\(.file):\(.line) \(.msg)"' .gg/logs-5173.jsonl
|
|
383
|
+
|
|
384
|
+
# Count entries by namespace
|
|
385
|
+
jq -s 'group_by(.ns) | map({ns: .[0].ns, count: length}) | sort_by(-.count)' .gg/logs-5173.jsonl
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The file is truncated on dev server restart. Use `DELETE /__gg/logs` to clear mid-session. The `origin` field distinguishes Tauri webview (`"tauri"`) from browser tab (`"browser"`) when both are open.
|
|
389
|
+
|
|
390
|
+
**Add to your project's `AGENTS.md`** -- see [Agent Instructions Template](specs/gg-agent-file-sink.md#agent-instructions-template-for-consuming-projects-agentsmd) in the spec for a copy-paste-ready block to add to consuming projects.
|
|
391
|
+
|
|
286
392
|
## Color Support (ANSI)
|
|
287
393
|
|
|
288
394
|
Color your logs for better visual distinction using `fg()` (foreground/text) and `bg()` (background):
|
package/dist/debug/browser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Browser-specific debug implementation.
|
|
3
3
|
*
|
|
4
4
|
* Output: console.debug with %c CSS color formatting.
|
|
5
|
-
* Persistence: localStorage
|
|
5
|
+
* Persistence: localStorage['gg-show'] + localStorage['gg-console']
|
|
6
6
|
* Format (patched): +123ms namespace message
|
|
7
7
|
*/
|
|
8
8
|
import { type DebugFactory } from './common.js';
|
package/dist/debug/browser.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Browser-specific debug implementation.
|
|
3
3
|
*
|
|
4
4
|
* Output: console.debug with %c CSS color formatting.
|
|
5
|
-
* Persistence: localStorage
|
|
5
|
+
* Persistence: localStorage['gg-show'] + localStorage['gg-console']
|
|
6
6
|
* Format (patched): +123ms namespace message
|
|
7
7
|
*/
|
|
8
8
|
import { setup, humanize } from './common.js';
|
|
@@ -59,12 +59,13 @@ function formatArgs(args) {
|
|
|
59
59
|
}
|
|
60
60
|
function save(namespaces) {
|
|
61
61
|
try {
|
|
62
|
+
// Only persist non-empty patterns. Empty string means "console disabled"
|
|
63
|
+
// (gg-console=false), not a user-set filter — don't wipe gg-show in that case.
|
|
62
64
|
if (namespaces) {
|
|
63
|
-
localStorage.setItem('
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
localStorage.removeItem('debug');
|
|
65
|
+
localStorage.setItem('gg-show', namespaces);
|
|
67
66
|
}
|
|
67
|
+
// Intentionally no removeItem: gg-show is the Show filter, persisted independently.
|
|
68
|
+
// Console-disabled state is tracked via gg-console, not by clearing gg-show.
|
|
68
69
|
}
|
|
69
70
|
catch {
|
|
70
71
|
// localStorage may not be available
|
|
@@ -72,7 +73,14 @@ function save(namespaces) {
|
|
|
72
73
|
}
|
|
73
74
|
function load() {
|
|
74
75
|
try {
|
|
75
|
-
|
|
76
|
+
// gg-console controls whether native console output is enabled at all.
|
|
77
|
+
// When it is 'false', disable all console output by returning '' (nothing enabled).
|
|
78
|
+
const consoleEnabled = localStorage.getItem('gg-console');
|
|
79
|
+
if (consoleEnabled === 'false')
|
|
80
|
+
return '';
|
|
81
|
+
// Use gg-show as the namespace filter for console output.
|
|
82
|
+
// Fall back to '*' (show all) so zero-config works out of the box.
|
|
83
|
+
return localStorage.getItem('gg-show') || '*';
|
|
76
84
|
}
|
|
77
85
|
catch {
|
|
78
86
|
return '';
|
package/dist/debug/node.js
CHANGED
|
@@ -94,14 +94,16 @@ function log(...args) {
|
|
|
94
94
|
}
|
|
95
95
|
function save(namespaces) {
|
|
96
96
|
if (namespaces) {
|
|
97
|
-
process.env.
|
|
97
|
+
process.env.GG_KEEP = namespaces;
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
100
|
-
delete process.env.
|
|
100
|
+
delete process.env.GG_KEEP;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
function load() {
|
|
104
|
-
|
|
104
|
+
// GG_KEEP controls which namespaces are kept (and thus output to the server console).
|
|
105
|
+
// Fall back to '*' so gg works zero-config in dev without setting any env var.
|
|
106
|
+
return process.env.GG_KEEP || '*';
|
|
105
107
|
}
|
|
106
108
|
function init(instance) {
|
|
107
109
|
// Each instance gets its own inspectOpts copy (for per-instance color override)
|
package/dist/eruda/buffer.d.ts
CHANGED
package/dist/eruda/buffer.js
CHANGED
|
@@ -101,4 +101,20 @@ export class LogBuffer {
|
|
|
101
101
|
get capacity() {
|
|
102
102
|
return this.maxSize;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Resize the buffer. Existing entries are preserved up to newCapacity (oldest dropped if shrinking).
|
|
106
|
+
*/
|
|
107
|
+
resize(newCapacity) {
|
|
108
|
+
const entries = this.getEntries(); // oldest→newest, up to current count
|
|
109
|
+
this.maxSize = newCapacity;
|
|
110
|
+
this.buf = new Array(newCapacity);
|
|
111
|
+
this.head = 0;
|
|
112
|
+
this.count = 0;
|
|
113
|
+
this._totalPushed = 0;
|
|
114
|
+
// Re-push entries, keeping the most recent up to newCapacity
|
|
115
|
+
const start = entries.length > newCapacity ? entries.length - newCapacity : 0;
|
|
116
|
+
for (let i = start; i < entries.length; i++) {
|
|
117
|
+
this.push(entries[i]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
104
120
|
}
|
package/dist/eruda/loader.js
CHANGED
|
@@ -52,6 +52,61 @@ export function shouldLoadEruda(options) {
|
|
|
52
52
|
const prodTriggers = options.prod ?? ['url-param', 'gesture'];
|
|
53
53
|
return checkProdTriggers(prodTriggers);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Adjusts document.body padding-bottom to match the Eruda panel height so
|
|
57
|
+
* the page remains fully scrollable while the panel is open.
|
|
58
|
+
*
|
|
59
|
+
* Mirrors the TanStack Router devtools approach: inject padding when visible,
|
|
60
|
+
* removed when hidden. A ResizeObserver tracks panel resizes (e.g. the user
|
|
61
|
+
* drags it taller or shorter).
|
|
62
|
+
*
|
|
63
|
+
* Implementation notes:
|
|
64
|
+
* - Eruda uses shadow DOM by default, so #eruda.shadowRoot must be queried
|
|
65
|
+
* - The visible panel is .eruda-dev-tools inside .eruda-container
|
|
66
|
+
* - Eruda appends its DOM asynchronously, so we poll with rAF until it appears
|
|
67
|
+
*/
|
|
68
|
+
function setupBodyPadding(
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
eruda, initiallyOpen) {
|
|
71
|
+
let attempts = 0;
|
|
72
|
+
function trySetup() {
|
|
73
|
+
const host = document.getElementById('eruda');
|
|
74
|
+
const root = host?.shadowRoot ?? host;
|
|
75
|
+
const container = root?.querySelector('.eruda-container');
|
|
76
|
+
const panel = container?.querySelector('.eruda-dev-tools');
|
|
77
|
+
if (!panel) {
|
|
78
|
+
if (++attempts < 60)
|
|
79
|
+
requestAnimationFrame(trySetup);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let observer = null;
|
|
83
|
+
function applyPadding() {
|
|
84
|
+
const h = panel.offsetHeight;
|
|
85
|
+
document.body.style.paddingBottom = `${h}px`;
|
|
86
|
+
// Ensure the document is tall enough to scroll even on short pages.
|
|
87
|
+
document.documentElement.style.minHeight = `calc(100vh + ${h}px)`;
|
|
88
|
+
}
|
|
89
|
+
function clearPadding() {
|
|
90
|
+
document.body.style.paddingBottom = '';
|
|
91
|
+
document.documentElement.style.minHeight = '';
|
|
92
|
+
observer?.disconnect();
|
|
93
|
+
observer = null;
|
|
94
|
+
}
|
|
95
|
+
function startObserving() {
|
|
96
|
+
if (observer)
|
|
97
|
+
return;
|
|
98
|
+
observer = new ResizeObserver(applyPadding);
|
|
99
|
+
observer.observe(panel);
|
|
100
|
+
applyPadding();
|
|
101
|
+
}
|
|
102
|
+
const devTools = eruda.get();
|
|
103
|
+
devTools.on('show', startObserving);
|
|
104
|
+
devTools.on('hide', clearPadding);
|
|
105
|
+
if (initiallyOpen)
|
|
106
|
+
startObserving();
|
|
107
|
+
}
|
|
108
|
+
requestAnimationFrame(trySetup);
|
|
109
|
+
}
|
|
55
110
|
/**
|
|
56
111
|
* Dynamically imports and initializes Eruda
|
|
57
112
|
*/
|
|
@@ -92,6 +147,9 @@ export async function loadEruda(options) {
|
|
|
92
147
|
if (options.open) {
|
|
93
148
|
eruda.show();
|
|
94
149
|
}
|
|
150
|
+
// Adjust body padding-bottom so the page remains fully scrollable while
|
|
151
|
+
// the panel is open — mirrors the TanStack Router devtools pattern.
|
|
152
|
+
setupBodyPadding(eruda, options.open ?? false);
|
|
95
153
|
// Run diagnostics after Eruda is ready so they appear in Console tab
|
|
96
154
|
await runGgDiagnostics();
|
|
97
155
|
}
|
package/dist/eruda/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GgErudaOptions, CapturedEntry } from './types.js';
|
|
1
|
+
import type { GgErudaOptions, CapturedEntry, DroppedNamespaceInfo } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Licia jQuery-like wrapper used by Eruda
|
|
4
4
|
*/
|
|
@@ -19,11 +19,15 @@ interface LiciaElement {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function createGgPlugin(options: GgErudaOptions, gg: {
|
|
21
21
|
_onLog?: ((entry: CapturedEntry) => void) | null;
|
|
22
|
+
addLogListener?: (callback: (entry: CapturedEntry) => void) => void;
|
|
23
|
+
removeLogListener?: (callback: (entry: CapturedEntry) => void) => void;
|
|
22
24
|
}): {
|
|
23
25
|
name: string;
|
|
24
26
|
init($container: LiciaElement): void;
|
|
25
27
|
show(): void;
|
|
26
28
|
hide(): void;
|
|
27
29
|
destroy(): void;
|
|
30
|
+
/** Returns a read-only view of the dropped-namespace tracking map (Phase 2 data layer). */
|
|
31
|
+
getDroppedNamespaces(): ReadonlyMap<string, DroppedNamespaceInfo>;
|
|
28
32
|
};
|
|
29
33
|
export {};
|