@youtyan/code-viewer 0.1.52 → 0.1.54
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 +73 -3
- package/dist/code-viewer.js +78 -19
- package/package.json +1 -1
- package/skills/code-viewer-snapshot/SKILL.md +102 -0
- package/web/app.js +29 -6
- package/web/style.css +10 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Requires Node.js 20 or newer when installed from npm. Development uses
|
|
|
14
14
|
- Preview Markdown with a table of contents, task lists, Mermaid diagrams, and Shiki code highlighting.
|
|
15
15
|
- Preview browser-safe media and show metadata for binary files that cannot be rendered.
|
|
16
16
|
- Switch the viewer UI between English and Japanese from Viewer Settings.
|
|
17
|
+
- Browse SQLite, PostgreSQL, and MySQL databases with a built-in database viewer.
|
|
17
18
|
- Read the built-in Help page for repository browsing, diffs, annotations, agent skills, and shortcuts.
|
|
18
19
|
- Open repository folders in the OS file manager from localhost-only actions.
|
|
19
20
|
- Upload files into worktree folders when upload is explicitly enabled.
|
|
@@ -111,6 +112,75 @@ Use `scope.omitDirs` for directories that should stay visible as skipped, and
|
|
|
111
112
|
`scope.excludeNames` for file or directory names that should be hidden entirely.
|
|
112
113
|
`.DS_Store` is hidden by default.
|
|
113
114
|
|
|
115
|
+
## Database Viewer
|
|
116
|
+
|
|
117
|
+
code-viewer auto-discovers databases in your repository and provides a
|
|
118
|
+
browser-based viewer for exploring their contents.
|
|
119
|
+
|
|
120
|
+
**SQLite** files (`.db`, `.sqlite`, `.sqlite3`, `.s3db`) are detected
|
|
121
|
+
automatically by scanning the repository tree. **PostgreSQL** and **MySQL**
|
|
122
|
+
databases are detected from `docker-compose.yml` and connected through
|
|
123
|
+
Docker.
|
|
124
|
+
|
|
125
|
+
### Browser UI
|
|
126
|
+
|
|
127
|
+
Open the database icon in the sidebar to access:
|
|
128
|
+
|
|
129
|
+
- **Table browser** — paginated data grid with column sorting, text
|
|
130
|
+
filtering, and CSV/JSON export.
|
|
131
|
+
- **Query editor** — execute read-only SQL with syntax highlighting.
|
|
132
|
+
Results and history are saved and shared across tabs.
|
|
133
|
+
- **ER diagram** — auto-generated entity-relationship diagram showing
|
|
134
|
+
foreign key relationships between tables.
|
|
135
|
+
- **Global search** — full-text search across all tables and text columns.
|
|
136
|
+
- **Snapshots & diffs** — take point-in-time snapshots of selected tables
|
|
137
|
+
and compare any two snapshots to see inserted, updated, and deleted rows
|
|
138
|
+
with full before/after values.
|
|
139
|
+
|
|
140
|
+
### CLI
|
|
141
|
+
|
|
142
|
+
AI agents can query databases and manage snapshots from the command line:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
code-viewer query exec --db data.db --sql "SELECT * FROM users LIMIT 10" \
|
|
146
|
+
--title "Sample users" --body "Checking user data shape."
|
|
147
|
+
|
|
148
|
+
code-viewer query search --db app.db --term "john@example.com"
|
|
149
|
+
|
|
150
|
+
code-viewer query snapshot create --db app.db --tables users,orders \
|
|
151
|
+
--note "Before migration"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`code-viewer query --help` shows all commands. `code-viewer query agent-help`
|
|
155
|
+
prints a detailed guide for AI agents covering queries, search, snapshots,
|
|
156
|
+
and diffs.
|
|
157
|
+
|
|
158
|
+
### Snapshot & Diff Workflow
|
|
159
|
+
|
|
160
|
+
Snapshots capture the state of selected tables at a point in time. Diff any
|
|
161
|
+
two snapshots to verify that a migration, test, or operation changed exactly
|
|
162
|
+
what you expected:
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
# 1. Snapshot before the operation
|
|
166
|
+
code-viewer query snapshot create --db app.db --tables users --note "Before"
|
|
167
|
+
|
|
168
|
+
# 2. (run the migration / test / script)
|
|
169
|
+
|
|
170
|
+
# 3. Snapshot after
|
|
171
|
+
code-viewer query snapshot create --db app.db --tables users --note "After"
|
|
172
|
+
|
|
173
|
+
# 4. List snapshots to get IDs
|
|
174
|
+
code-viewer query snapshot list --db app.db
|
|
175
|
+
|
|
176
|
+
# 5. Compare
|
|
177
|
+
code-viewer query diff tables --before snap-abc123 --after snap-def456
|
|
178
|
+
code-viewer query diff rows --before snap-abc123 --after snap-def456 --table users
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Diffs are computed on demand — no pre-computation needed. The browser's
|
|
182
|
+
Snapshot tab provides a visual interface for the same workflow.
|
|
183
|
+
|
|
114
184
|
## AI Code Annotations
|
|
115
185
|
|
|
116
186
|
AI coding agents (Claude Code, Codex, and similar CLI agents) can walk you
|
|
@@ -163,9 +233,9 @@ write concise Markdown explanations, and how to install the bundled agent skill.
|
|
|
163
233
|
|
|
164
234
|
### Agent Skill
|
|
165
235
|
|
|
166
|
-
The package bundles
|
|
167
|
-
open standard) that
|
|
168
|
-
`annotate`. Install
|
|
236
|
+
The package bundles [Agent Skills](https://agentskills.io) (the SKILL.md
|
|
237
|
+
open standard) that teach AI coding agents when and how to use
|
|
238
|
+
`annotate`, `query`, and `snapshot`. Install them into the current project:
|
|
169
239
|
|
|
170
240
|
```sh
|
|
171
241
|
npx -y @youtyan/code-viewer skill install # Claude Code (.claude/skills/)
|
package/dist/code-viewer.js
CHANGED
|
@@ -3337,6 +3337,15 @@ function openDockerAdapter(serviceName, kind, env, cwd, overrideDatabase) {
|
|
|
3337
3337
|
var init_docker = () => {};
|
|
3338
3338
|
|
|
3339
3339
|
// web-src/server/database/adapters/sqlite.ts
|
|
3340
|
+
function safePrepare(db, sql) {
|
|
3341
|
+
const stmt = db.prepare(sql);
|
|
3342
|
+
if (typeof stmt.safeIntegers === "function") {
|
|
3343
|
+
try {
|
|
3344
|
+
stmt.safeIntegers(true);
|
|
3345
|
+
} catch {}
|
|
3346
|
+
}
|
|
3347
|
+
return stmt;
|
|
3348
|
+
}
|
|
3340
3349
|
async function getSqliteClass() {
|
|
3341
3350
|
if (cachedDbClass)
|
|
3342
3351
|
return cachedDbClass;
|
|
@@ -3450,7 +3459,7 @@ function createSqliteAdapter(db) {
|
|
|
3450
3459
|
getTablePage(table, options) {
|
|
3451
3460
|
const order = buildOrderClause2(options.orderBy);
|
|
3452
3461
|
const sql = `SELECT * FROM ${sanitizeIdentifier2(table)}${order} LIMIT ? OFFSET ?`;
|
|
3453
|
-
const rows = db
|
|
3462
|
+
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
3454
3463
|
const cols = queryColumns(db, table);
|
|
3455
3464
|
if (rows.length === 0) {
|
|
3456
3465
|
return {
|
|
@@ -3484,11 +3493,11 @@ function createSqliteAdapter(db) {
|
|
|
3484
3493
|
const wrappedSql = `SELECT * FROM (${limited}) LIMIT ${maxRows + 1}`;
|
|
3485
3494
|
let rows;
|
|
3486
3495
|
try {
|
|
3487
|
-
rows = db
|
|
3496
|
+
rows = safePrepare(db, wrappedSql).all(...params || []);
|
|
3488
3497
|
} catch (wrapErr) {
|
|
3489
3498
|
const fallbackSql = `${limited} LIMIT ${maxRows + 1}`;
|
|
3490
3499
|
try {
|
|
3491
|
-
rows = db
|
|
3500
|
+
rows = safePrepare(db, fallbackSql).all(...params || []);
|
|
3492
3501
|
} catch {
|
|
3493
3502
|
throw wrapErr;
|
|
3494
3503
|
}
|
|
@@ -3839,6 +3848,37 @@ var init_discovery = __esm(() => {
|
|
|
3839
3848
|
];
|
|
3840
3849
|
});
|
|
3841
3850
|
|
|
3851
|
+
// web-src/server/database/serialize.ts
|
|
3852
|
+
function serializeDbValue(value) {
|
|
3853
|
+
if (value === null || value === undefined)
|
|
3854
|
+
return null;
|
|
3855
|
+
if (typeof value === "bigint") {
|
|
3856
|
+
return value >= MIN_SAFE && value <= MAX_SAFE ? Number(value) : value.toString();
|
|
3857
|
+
}
|
|
3858
|
+
if (value instanceof Uint8Array) {
|
|
3859
|
+
return `<blob ${value.byteLength} bytes>`;
|
|
3860
|
+
}
|
|
3861
|
+
if (typeof value === "object") {
|
|
3862
|
+
try {
|
|
3863
|
+
return JSON.stringify(value);
|
|
3864
|
+
} catch {
|
|
3865
|
+
return String(value);
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
return value;
|
|
3869
|
+
}
|
|
3870
|
+
function serializeDbRow(row) {
|
|
3871
|
+
return row.map(serializeDbValue);
|
|
3872
|
+
}
|
|
3873
|
+
function serializeDbRows(rows) {
|
|
3874
|
+
return rows.map(serializeDbRow);
|
|
3875
|
+
}
|
|
3876
|
+
var MIN_SAFE, MAX_SAFE;
|
|
3877
|
+
var init_serialize = __esm(() => {
|
|
3878
|
+
MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
|
3879
|
+
MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
3880
|
+
});
|
|
3881
|
+
|
|
3842
3882
|
// web-src/server/database/global-search.ts
|
|
3843
3883
|
function sanitizeIdentifier3(name, kind) {
|
|
3844
3884
|
if (kind === "mysql")
|
|
@@ -3880,7 +3920,7 @@ function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkC
|
|
|
3880
3920
|
const result = kind === "sqlite" ? adapter.executeReadonlyQuery(sql, [`%${escapedTerm}%`], remaining) : adapter.executeReadonlyQuery(sql, undefined, remaining);
|
|
3881
3921
|
for (const row of result.rows) {
|
|
3882
3922
|
const colIdx = result.columns.indexOf(col.name);
|
|
3883
|
-
const valueRaw = colIdx >= 0 ? row[colIdx] : null;
|
|
3923
|
+
const valueRaw = colIdx >= 0 ? serializeDbValue(row[colIdx]) : null;
|
|
3884
3924
|
const valueStr = valueRaw == null ? "" : String(valueRaw);
|
|
3885
3925
|
const preview = valueStr.length > 200 ? `${valueStr.slice(0, 200)}...` : valueStr;
|
|
3886
3926
|
let rowKeyJson;
|
|
@@ -3889,7 +3929,7 @@ function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkC
|
|
|
3889
3929
|
for (const pk of pkColumns) {
|
|
3890
3930
|
const pkIdx = result.columns.indexOf(pk);
|
|
3891
3931
|
if (pkIdx >= 0)
|
|
3892
|
-
keyObj[pk] = row[pkIdx];
|
|
3932
|
+
keyObj[pk] = serializeDbValue(row[pkIdx]);
|
|
3893
3933
|
}
|
|
3894
3934
|
rowKeyJson = JSON.stringify(keyObj);
|
|
3895
3935
|
}
|
|
@@ -3898,7 +3938,7 @@ function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkC
|
|
|
3898
3938
|
column: col.name,
|
|
3899
3939
|
rowKeyJson,
|
|
3900
3940
|
valuePreview: preview,
|
|
3901
|
-
rowPreview: row
|
|
3941
|
+
rowPreview: serializeDbRow(row)
|
|
3902
3942
|
});
|
|
3903
3943
|
}
|
|
3904
3944
|
} catch {}
|
|
@@ -3909,6 +3949,9 @@ function getPrimaryKeyColumns(adapter, table) {
|
|
|
3909
3949
|
const columns = adapter.getColumns(table);
|
|
3910
3950
|
return columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
3911
3951
|
}
|
|
3952
|
+
var init_global_search = __esm(() => {
|
|
3953
|
+
init_serialize();
|
|
3954
|
+
});
|
|
3912
3955
|
|
|
3913
3956
|
// web-src/server/database/query-history.ts
|
|
3914
3957
|
import {
|
|
@@ -4283,6 +4326,8 @@ import { createHash as createHash3 } from "node:crypto";
|
|
|
4283
4326
|
function normalizeValue(v) {
|
|
4284
4327
|
if (v === null)
|
|
4285
4328
|
return "\\N";
|
|
4329
|
+
if (typeof v === "bigint")
|
|
4330
|
+
return v.toString();
|
|
4286
4331
|
if (v instanceof Uint8Array) {
|
|
4287
4332
|
return `\\x${Buffer.from(v).toString("hex")}`;
|
|
4288
4333
|
}
|
|
@@ -4291,7 +4336,7 @@ function normalizeValue(v) {
|
|
|
4291
4336
|
function rowToPayloadJson(columns, row) {
|
|
4292
4337
|
const obj = {};
|
|
4293
4338
|
for (let i = 0;i < columns.length; i++) {
|
|
4294
|
-
obj[columns[i]] = row[i]
|
|
4339
|
+
obj[columns[i]] = serializeDbValue(row[i]);
|
|
4295
4340
|
}
|
|
4296
4341
|
return JSON.stringify(obj);
|
|
4297
4342
|
}
|
|
@@ -4307,7 +4352,7 @@ function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
|
|
|
4307
4352
|
for (const pk of pkColumns) {
|
|
4308
4353
|
const idx = allColumns.indexOf(pk);
|
|
4309
4354
|
if (idx >= 0)
|
|
4310
|
-
keyObj[pk] = row[idx];
|
|
4355
|
+
keyObj[pk] = serializeDbValue(row[idx]);
|
|
4311
4356
|
}
|
|
4312
4357
|
return JSON.stringify(keyObj);
|
|
4313
4358
|
}
|
|
@@ -4353,6 +4398,7 @@ async function runSnapshot(cwd, adapter, dbId, tables, note, onProgress) {
|
|
|
4353
4398
|
}
|
|
4354
4399
|
var BATCH_SIZE = 500;
|
|
4355
4400
|
var init_snapshot_runner = __esm(() => {
|
|
4401
|
+
init_serialize();
|
|
4356
4402
|
init_snapshot_store();
|
|
4357
4403
|
});
|
|
4358
4404
|
|
|
@@ -4606,7 +4652,7 @@ async function handleTable(cwd, url) {
|
|
|
4606
4652
|
dbId: r.dbId,
|
|
4607
4653
|
table,
|
|
4608
4654
|
columns,
|
|
4609
|
-
rows: dataResult.rows,
|
|
4655
|
+
rows: serializeDbRows(dataResult.rows),
|
|
4610
4656
|
totalRows: totalRows2,
|
|
4611
4657
|
offset,
|
|
4612
4658
|
limit,
|
|
@@ -4621,7 +4667,7 @@ async function handleTable(cwd, url) {
|
|
|
4621
4667
|
dbId: r.dbId,
|
|
4622
4668
|
table,
|
|
4623
4669
|
columns,
|
|
4624
|
-
rows: result.rows,
|
|
4670
|
+
rows: serializeDbRows(result.rows),
|
|
4625
4671
|
totalRows,
|
|
4626
4672
|
offset,
|
|
4627
4673
|
limit,
|
|
@@ -4656,11 +4702,12 @@ async function handleQuery(cwd, req, sendSse) {
|
|
|
4656
4702
|
const adapter = await getAdapter(r, cwd);
|
|
4657
4703
|
const result = adapter.executeReadonlyQuery(body.sql, undefined, maxRows);
|
|
4658
4704
|
const elapsed = Date.now() - start;
|
|
4705
|
+
const serializedRows = serializeDbRows(result.rows);
|
|
4659
4706
|
const response = {
|
|
4660
4707
|
dbId: body.db,
|
|
4661
4708
|
columns: result.columns,
|
|
4662
4709
|
columnTypes: result.columnTypes,
|
|
4663
|
-
rows:
|
|
4710
|
+
rows: serializedRows,
|
|
4664
4711
|
rowCount: result.rowCount,
|
|
4665
4712
|
truncated: result.rowCount >= maxRows,
|
|
4666
4713
|
elapsedMs: elapsed
|
|
@@ -4673,9 +4720,9 @@ async function handleQuery(cwd, req, sendSse) {
|
|
|
4673
4720
|
title: body.title,
|
|
4674
4721
|
body: body.body,
|
|
4675
4722
|
columns: result.columns,
|
|
4676
|
-
rowsPreview:
|
|
4723
|
+
rowsPreview: serializedRows,
|
|
4677
4724
|
rowCount: result.rowCount,
|
|
4678
|
-
savedRows:
|
|
4725
|
+
savedRows: serializedRows.length,
|
|
4679
4726
|
truncated: result.rowCount >= maxRows,
|
|
4680
4727
|
elapsedMs: elapsed,
|
|
4681
4728
|
executedAt: new Date().toISOString(),
|
|
@@ -4750,6 +4797,8 @@ async function handleHistoryClear(cwd, req, sendSse) {
|
|
|
4750
4797
|
function formatCsvField(value) {
|
|
4751
4798
|
if (value === null || value === undefined)
|
|
4752
4799
|
return "";
|
|
4800
|
+
if (typeof value === "bigint")
|
|
4801
|
+
return value.toString();
|
|
4753
4802
|
if (value instanceof Uint8Array)
|
|
4754
4803
|
return `<blob ${value.byteLength} bytes>`;
|
|
4755
4804
|
const str = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
@@ -4789,7 +4838,7 @@ async function handleExport(cwd, url) {
|
|
|
4789
4838
|
if (sortCol && !colNameSet.has(sortCol)) {
|
|
4790
4839
|
return textError(`invalid sort column: ${sortCol}`, 400);
|
|
4791
4840
|
}
|
|
4792
|
-
let
|
|
4841
|
+
let rawRows;
|
|
4793
4842
|
if (filters.length > 0) {
|
|
4794
4843
|
const validFilters = filters.filter((f) => colNameSet.has(f.column));
|
|
4795
4844
|
if (validFilters.length > 0) {
|
|
@@ -4806,14 +4855,14 @@ async function handleExport(cwd, url) {
|
|
|
4806
4855
|
const limitOffset = filter.useParams ? "LIMIT ? OFFSET ?" : `LIMIT ${EXPORT_MAX_ROWS} OFFSET 0`;
|
|
4807
4856
|
const dataSql = `SELECT * FROM ${tbl} WHERE ${filter.where}${order} ${limitOffset}`;
|
|
4808
4857
|
const result = adapter.executeReadonlyQuery(dataSql, filter.useParams ? [...filter.params, EXPORT_MAX_ROWS, 0] : undefined);
|
|
4809
|
-
|
|
4858
|
+
rawRows = result.rows;
|
|
4810
4859
|
} else {
|
|
4811
4860
|
const result = adapter.getTablePage(table, {
|
|
4812
4861
|
offset: 0,
|
|
4813
4862
|
limit: EXPORT_MAX_ROWS,
|
|
4814
4863
|
orderBy
|
|
4815
4864
|
});
|
|
4816
|
-
|
|
4865
|
+
rawRows = result.rows;
|
|
4817
4866
|
}
|
|
4818
4867
|
} else {
|
|
4819
4868
|
const result = adapter.getTablePage(table, {
|
|
@@ -4821,8 +4870,9 @@ async function handleExport(cwd, url) {
|
|
|
4821
4870
|
limit: EXPORT_MAX_ROWS,
|
|
4822
4871
|
orderBy
|
|
4823
4872
|
});
|
|
4824
|
-
|
|
4873
|
+
rawRows = result.rows;
|
|
4825
4874
|
}
|
|
4875
|
+
const rows = serializeDbRows(rawRows);
|
|
4826
4876
|
if (format === "csv") {
|
|
4827
4877
|
const lines = [colNames.map(formatCsvField).join(",")];
|
|
4828
4878
|
for (const row of rows) {
|
|
@@ -4842,8 +4892,7 @@ async function handleExport(cwd, url) {
|
|
|
4842
4892
|
const objects = rows.map((row) => {
|
|
4843
4893
|
const obj = {};
|
|
4844
4894
|
for (let i = 0;i < colNames.length; i++) {
|
|
4845
|
-
|
|
4846
|
-
obj[colNames[i]] = val instanceof Uint8Array ? `<blob ${val.byteLength} bytes>` : val;
|
|
4895
|
+
obj[colNames[i]] = row[i];
|
|
4847
4896
|
}
|
|
4848
4897
|
return obj;
|
|
4849
4898
|
});
|
|
@@ -5232,7 +5281,9 @@ var init_handle = __esm(() => {
|
|
|
5232
5281
|
init_sqlite();
|
|
5233
5282
|
init_connection_pool();
|
|
5234
5283
|
init_discovery();
|
|
5284
|
+
init_global_search();
|
|
5235
5285
|
init_query_history();
|
|
5286
|
+
init_serialize();
|
|
5236
5287
|
init_snapshot_runner();
|
|
5237
5288
|
init_snapshot_store();
|
|
5238
5289
|
dockerAdapterCache = new Map;
|
|
@@ -7194,6 +7245,14 @@ data: ok
|
|
|
7194
7245
|
});
|
|
7195
7246
|
|
|
7196
7247
|
// web-src/server/cli.ts
|
|
7248
|
+
var REQUIRED_NODE_MAJOR = 20;
|
|
7249
|
+
var nodeMajor = Number.parseInt((process.versions.node || "0").split(".")[0] || "0", 10);
|
|
7250
|
+
if (!Number.isFinite(nodeMajor) || nodeMajor < REQUIRED_NODE_MAJOR) {
|
|
7251
|
+
process.stderr.write(`code-viewer requires Node.js >= ${REQUIRED_NODE_MAJOR}.0.0, but found ${process.versions.node}.
|
|
7252
|
+
` + `Please upgrade Node.js (e.g. via nvm, volta, or your package manager) and retry.
|
|
7253
|
+
`);
|
|
7254
|
+
process.exit(1);
|
|
7255
|
+
}
|
|
7197
7256
|
if (process.argv[2] === "annotate") {
|
|
7198
7257
|
const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
|
|
7199
7258
|
await runAnnotateCli2(process.argv.slice(3));
|
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-viewer-snapshot
|
|
3
|
+
description: Use when taking database snapshots before/after an operation to verify what changed, diffing two points in time, or confirming a migration/test/feature modified the expected tables and rows. Triggers on "snapshot", "diff", "スナップショット", "差分", "before/after", "DBの変更を確認", "マイグレーション検証", "テスト前後の比較".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# code-viewer snapshot
|
|
7
|
+
|
|
8
|
+
Take point-in-time snapshots of database tables and diff them to see
|
|
9
|
+
exactly what changed — inserted, updated, and deleted rows with full
|
|
10
|
+
before/after values. Use it to verify migrations, test runs, or any
|
|
11
|
+
operation that should modify the database in a predictable way.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
- Verifying a migration added/removed the expected rows
|
|
16
|
+
- Confirming a test correctly modifies the database
|
|
17
|
+
- Debugging "what did that operation actually change?"
|
|
18
|
+
- Auditing data changes before and after a deploy or script
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- A code-viewer server must already be running for the repository
|
|
23
|
+
(the human starts it with: `code-viewer`). The CLI never starts one.
|
|
24
|
+
- Run from inside the repository, or pass `--cwd <repo>`.
|
|
25
|
+
- If `code-viewer` is not on PATH, prefix every command with
|
|
26
|
+
`npx -y @youtyan/code-viewer`.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
### 1. Take a "before" snapshot
|
|
31
|
+
|
|
32
|
+
Specify which database and tables to capture. Always use `--tables` to
|
|
33
|
+
avoid scanning unnecessary tables.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
code-viewer query snapshot create --db app.db \
|
|
37
|
+
--tables users,orders \
|
|
38
|
+
--note "Before running user registration test"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Perform the operation
|
|
42
|
+
|
|
43
|
+
The human (or a test runner, migration script, etc.) modifies the database.
|
|
44
|
+
|
|
45
|
+
### 3. Take an "after" snapshot
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
code-viewer query snapshot create --db app.db \
|
|
49
|
+
--tables users,orders \
|
|
50
|
+
--note "After running user registration test"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. Get snapshot IDs
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
code-viewer query snapshot list --db app.db
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 5. View the diff
|
|
60
|
+
|
|
61
|
+
Compare table-level summary (which tables changed and how many rows):
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
code-viewer query diff tables --before snap-abc123 --after snap-def456
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Drill into row-level changes for a specific table:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
code-viewer query diff rows --before snap-abc123 --after snap-def456 \
|
|
71
|
+
--table users
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The human can also view all snapshots and diffs in the browser's
|
|
75
|
+
Database > Snapshot tab.
|
|
76
|
+
|
|
77
|
+
## Managing snapshots
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
# Update a snapshot's note
|
|
81
|
+
code-viewer query snapshot note --id snap-abc123 --note "Updated note"
|
|
82
|
+
|
|
83
|
+
# Delete a snapshot
|
|
84
|
+
code-viewer query snapshot delete --id snap-abc123
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Guidelines
|
|
88
|
+
|
|
89
|
+
- Always specify `--tables` — snapshotting every table wastes time.
|
|
90
|
+
- Write meaningful `--note` values — the human uses them to tell
|
|
91
|
+
snapshots apart.
|
|
92
|
+
- Take both snapshots against the same `--db` and `--tables`.
|
|
93
|
+
- For large tables, the snapshot captures all rows — be mindful of
|
|
94
|
+
database size.
|
|
95
|
+
- The diff is computed on demand, not stored — you can diff any two
|
|
96
|
+
snapshots of the same database.
|
|
97
|
+
|
|
98
|
+
## Full reference
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
code-viewer query agent-help
|
|
102
|
+
```
|
package/web/app.js
CHANGED
|
@@ -9415,6 +9415,7 @@ ${frontmatter.yaml}
|
|
|
9415
9415
|
var PAGE_SIZE = 200;
|
|
9416
9416
|
var FILTER_DEBOUNCE_MS = 300;
|
|
9417
9417
|
var DEFAULT_COL_WIDTH = 180;
|
|
9418
|
+
var CELL_PREVIEW_MAX_CHARS = 4000;
|
|
9418
9419
|
function createTableGrid(callbacks) {
|
|
9419
9420
|
const el = document.createElement("div");
|
|
9420
9421
|
el.className = "db-grid";
|
|
@@ -9557,11 +9558,17 @@ ${frontmatter.yaml}
|
|
|
9557
9558
|
}
|
|
9558
9559
|
return filters;
|
|
9559
9560
|
}
|
|
9561
|
+
function resetSelectionAndDetail() {
|
|
9562
|
+
selectedRowIndex = -1;
|
|
9563
|
+
detailPanel.hidden = true;
|
|
9564
|
+
detailPanel.innerHTML = "";
|
|
9565
|
+
}
|
|
9560
9566
|
function invalidateData() {
|
|
9561
9567
|
pageCache = new Map;
|
|
9562
9568
|
pendingPages = new Set;
|
|
9563
9569
|
loadGeneration++;
|
|
9564
9570
|
viewport.scrollTop = 0;
|
|
9571
|
+
resetSelectionAndDetail();
|
|
9565
9572
|
ensurePage(0);
|
|
9566
9573
|
}
|
|
9567
9574
|
function clear() {
|
|
@@ -9600,6 +9607,22 @@ ${frontmatter.yaml}
|
|
|
9600
9607
|
const title = document.createElement("span");
|
|
9601
9608
|
title.className = "db-grid-detail-title";
|
|
9602
9609
|
title.textContent = `${colName} (${colType})`;
|
|
9610
|
+
const copyBtn = document.createElement("button");
|
|
9611
|
+
copyBtn.type = "button";
|
|
9612
|
+
copyBtn.className = "db-btn db-grid-detail-copy";
|
|
9613
|
+
copyBtn.textContent = "Copy";
|
|
9614
|
+
const copyText = formatValueForCopy(value);
|
|
9615
|
+
copyBtn.addEventListener("click", () => {
|
|
9616
|
+
navigator.clipboard.writeText(copyText).then(() => {
|
|
9617
|
+
const original = copyBtn.textContent;
|
|
9618
|
+
copyBtn.textContent = "Copied";
|
|
9619
|
+
setTimeout(() => {
|
|
9620
|
+
copyBtn.textContent = original;
|
|
9621
|
+
}, 800);
|
|
9622
|
+
}, () => {
|
|
9623
|
+
copyBtn.textContent = "Copy failed";
|
|
9624
|
+
});
|
|
9625
|
+
});
|
|
9603
9626
|
const closeBtn = document.createElement("button");
|
|
9604
9627
|
closeBtn.type = "button";
|
|
9605
9628
|
closeBtn.className = "db-btn db-btn-icon db-grid-detail-close";
|
|
@@ -9607,7 +9630,7 @@ ${frontmatter.yaml}
|
|
|
9607
9630
|
closeBtn.addEventListener("click", () => {
|
|
9608
9631
|
detailPanel.hidden = true;
|
|
9609
9632
|
});
|
|
9610
|
-
header.append(title, closeBtn);
|
|
9633
|
+
header.append(title, copyBtn, closeBtn);
|
|
9611
9634
|
const content = document.createElement("div");
|
|
9612
9635
|
content.className = "db-grid-detail-content";
|
|
9613
9636
|
if (value === null) {
|
|
@@ -9769,6 +9792,7 @@ ${frontmatter.yaml}
|
|
|
9769
9792
|
}
|
|
9770
9793
|
pageCache = new Map;
|
|
9771
9794
|
pendingPages = new Set;
|
|
9795
|
+
resetSelectionAndDetail();
|
|
9772
9796
|
renderHeader();
|
|
9773
9797
|
renderViewport();
|
|
9774
9798
|
}
|
|
@@ -9852,11 +9876,6 @@ ${frontmatter.yaml}
|
|
|
9852
9876
|
});
|
|
9853
9877
|
row.classList.add("selected");
|
|
9854
9878
|
showCellDetail(cellColIndex, cellValue);
|
|
9855
|
-
const text2 = formatValueForCopy(cellValue);
|
|
9856
|
-
navigator.clipboard.writeText(text2).then(() => {
|
|
9857
|
-
cell.classList.add("copied");
|
|
9858
|
-
setTimeout(() => cell.classList.remove("copied"), 600);
|
|
9859
|
-
}, () => {});
|
|
9860
9879
|
});
|
|
9861
9880
|
row.appendChild(cell);
|
|
9862
9881
|
}
|
|
@@ -9994,6 +10013,10 @@ ${frontmatter.yaml}
|
|
|
9994
10013
|
const s2 = String(value);
|
|
9995
10014
|
if (s2 === "")
|
|
9996
10015
|
return "<empty>";
|
|
10016
|
+
if (s2.length > CELL_PREVIEW_MAX_CHARS) {
|
|
10017
|
+
const truncated = s2.slice(0, CELL_PREVIEW_MAX_CHARS);
|
|
10018
|
+
return `${truncated} … (+${s2.length - CELL_PREVIEW_MAX_CHARS} chars)`;
|
|
10019
|
+
}
|
|
9997
10020
|
return s2;
|
|
9998
10021
|
}
|
|
9999
10022
|
function formatValueForCopy(value) {
|
package/web/style.css
CHANGED
|
@@ -6116,6 +6116,16 @@ html[data-theme="dark"] .db-query-highlight span[style] {
|
|
|
6116
6116
|
color: var(--danger);
|
|
6117
6117
|
background: none;
|
|
6118
6118
|
}
|
|
6119
|
+
.db-grid-detail-copy {
|
|
6120
|
+
font-size: 11px;
|
|
6121
|
+
padding: 2px 8px;
|
|
6122
|
+
margin-left: auto;
|
|
6123
|
+
margin-right: 8px;
|
|
6124
|
+
color: var(--fg-muted);
|
|
6125
|
+
}
|
|
6126
|
+
.db-grid-detail-copy:hover {
|
|
6127
|
+
color: var(--fg);
|
|
6128
|
+
}
|
|
6119
6129
|
|
|
6120
6130
|
.db-grid-detail-content {
|
|
6121
6131
|
padding: 8px 12px;
|