@youtyan/code-viewer 0.2.0 → 0.2.1
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/dist/code-viewer.js +294 -72
- package/package.json +1 -1
- package/web/app.js +296 -87
- package/web/index.html +1 -0
- package/web/style.css +26 -14
package/dist/code-viewer.js
CHANGED
|
@@ -589,12 +589,28 @@ function startServer(options) {
|
|
|
589
589
|
resolve({
|
|
590
590
|
port,
|
|
591
591
|
close: () => new Promise((resolveClose, rejectClose) => {
|
|
592
|
-
|
|
593
|
-
|
|
592
|
+
let settled = false;
|
|
593
|
+
let forceTimer = null;
|
|
594
|
+
const settle = (error) => {
|
|
595
|
+
if (settled)
|
|
596
|
+
return;
|
|
597
|
+
settled = true;
|
|
598
|
+
if (forceTimer)
|
|
599
|
+
clearTimeout(forceTimer);
|
|
600
|
+
const code = error && "code" in error ? String(error.code) : "";
|
|
601
|
+
if (error && code !== "ERR_SERVER_NOT_RUNNING") {
|
|
594
602
|
rejectClose(error);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
resolveClose();
|
|
606
|
+
};
|
|
607
|
+
forceTimer = setTimeout(() => {
|
|
608
|
+
server.closeAllConnections?.();
|
|
609
|
+
settle();
|
|
610
|
+
}, 2000);
|
|
611
|
+
forceTimer.unref?.();
|
|
612
|
+
server.close(settle);
|
|
613
|
+
server.closeIdleConnections?.();
|
|
598
614
|
server.closeAllConnections?.();
|
|
599
615
|
})
|
|
600
616
|
});
|
|
@@ -3500,31 +3516,85 @@ var init_sql_snapshot = __esm(() => {
|
|
|
3500
3516
|
|
|
3501
3517
|
// web-src/server/database/adapters/docker-utils.ts
|
|
3502
3518
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3519
|
+
function isDockerComposeServiceUnavailableError(err) {
|
|
3520
|
+
return err instanceof DockerComposeServiceUnavailableError;
|
|
3521
|
+
}
|
|
3522
|
+
function composeContainerNameCacheKey(serviceName, cwd) {
|
|
3523
|
+
return `${cwd}\x00${serviceName}`;
|
|
3524
|
+
}
|
|
3503
3525
|
function resolveRunningComposeContainerName(serviceName, cwd) {
|
|
3504
|
-
const
|
|
3505
|
-
|
|
3526
|
+
const cacheKey = composeContainerNameCacheKey(serviceName, cwd);
|
|
3527
|
+
const now = Date.now();
|
|
3528
|
+
const cached = composeContainerNameCache.get(cacheKey);
|
|
3529
|
+
if (cached && cached.expiresAt > now)
|
|
3530
|
+
return cached.value;
|
|
3531
|
+
const proc = spawnSyncImpl("docker", ["compose", "ps", "--format", "json", "--status", "running"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], cwd });
|
|
3532
|
+
if (proc.status !== 0) {
|
|
3533
|
+
composeContainerNameCache.set(cacheKey, {
|
|
3534
|
+
value: null,
|
|
3535
|
+
expiresAt: now + COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS
|
|
3536
|
+
});
|
|
3506
3537
|
return null;
|
|
3538
|
+
}
|
|
3507
3539
|
try {
|
|
3508
3540
|
const output = proc.stdout.trim();
|
|
3509
3541
|
const containers = output.startsWith("[") ? JSON.parse(output) : output.split(`
|
|
3510
3542
|
`).filter(Boolean).map((line) => JSON.parse(line));
|
|
3511
3543
|
const match = containers.find((c) => c.Service === serviceName && c.State === "running");
|
|
3512
|
-
|
|
3544
|
+
const value = match?.Name || null;
|
|
3545
|
+
composeContainerNameCache.set(cacheKey, {
|
|
3546
|
+
value,
|
|
3547
|
+
expiresAt: now + (value ? COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS : COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS)
|
|
3548
|
+
});
|
|
3549
|
+
return value;
|
|
3513
3550
|
} catch {
|
|
3551
|
+
composeContainerNameCache.set(cacheKey, {
|
|
3552
|
+
value: null,
|
|
3553
|
+
expiresAt: now + COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS
|
|
3554
|
+
});
|
|
3514
3555
|
return null;
|
|
3515
3556
|
}
|
|
3516
3557
|
}
|
|
3517
3558
|
function resolveRunningComposeContainerNameOrThrow(serviceName, cwd) {
|
|
3518
3559
|
const containerName = resolveRunningComposeContainerName(serviceName, cwd);
|
|
3519
3560
|
if (!containerName) {
|
|
3520
|
-
throw new
|
|
3561
|
+
throw new DockerComposeServiceUnavailableError(serviceName, cwd);
|
|
3521
3562
|
}
|
|
3522
3563
|
return containerName;
|
|
3523
3564
|
}
|
|
3524
|
-
var
|
|
3565
|
+
var COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS = 30000, COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS = 3000, composeContainerNameCache, spawnSyncImpl, DockerComposeServiceUnavailableError;
|
|
3566
|
+
var init_docker_utils = __esm(() => {
|
|
3567
|
+
composeContainerNameCache = new Map;
|
|
3568
|
+
spawnSyncImpl = spawnSync2;
|
|
3569
|
+
DockerComposeServiceUnavailableError = class DockerComposeServiceUnavailableError extends Error {
|
|
3570
|
+
serviceName;
|
|
3571
|
+
cwd;
|
|
3572
|
+
status = 503;
|
|
3573
|
+
constructor(serviceName, cwd) {
|
|
3574
|
+
super(`Container for service "${serviceName}" is not running. Start it with: docker compose up -d ${serviceName}`);
|
|
3575
|
+
this.name = "DockerComposeServiceUnavailableError";
|
|
3576
|
+
this.serviceName = serviceName;
|
|
3577
|
+
this.cwd = cwd;
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
});
|
|
3525
3581
|
|
|
3526
3582
|
// web-src/server/database/adapters/docker.ts
|
|
3527
3583
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
3584
|
+
function dockerDatabasesCacheKey(serviceName, kind, cwd) {
|
|
3585
|
+
return `${serviceName}\x00${kind}\x00${cwd}`;
|
|
3586
|
+
}
|
|
3587
|
+
function setDockerDatabasesCache(key, value, ttlMs, now = Date.now()) {
|
|
3588
|
+
const cachedValue = [...value];
|
|
3589
|
+
dockerDatabasesCache.set(key, {
|
|
3590
|
+
value: cachedValue,
|
|
3591
|
+
expiresAt: now + ttlMs
|
|
3592
|
+
});
|
|
3593
|
+
return [...cachedValue];
|
|
3594
|
+
}
|
|
3595
|
+
function fallbackDockerDatabases(defaultDb) {
|
|
3596
|
+
return defaultDb ? [defaultDb] : [];
|
|
3597
|
+
}
|
|
3528
3598
|
function buildExecArgs(config, sql) {
|
|
3529
3599
|
if (config.kind === "postgresql") {
|
|
3530
3600
|
return [
|
|
@@ -3563,7 +3633,6 @@ function buildExecArgs(config, sql) {
|
|
|
3563
3633
|
config.user,
|
|
3564
3634
|
config.database,
|
|
3565
3635
|
"--batch",
|
|
3566
|
-
"--raw",
|
|
3567
3636
|
"--default-character-set=utf8mb4",
|
|
3568
3637
|
"-e",
|
|
3569
3638
|
sql
|
|
@@ -3571,7 +3640,7 @@ function buildExecArgs(config, sql) {
|
|
|
3571
3640
|
}
|
|
3572
3641
|
function execInContainer(config, sql, timeoutMs = 1e4) {
|
|
3573
3642
|
const args = buildExecArgs(config, sql);
|
|
3574
|
-
const proc =
|
|
3643
|
+
const proc = spawnSyncImpl2(args[0], args.slice(1), {
|
|
3575
3644
|
encoding: "utf8",
|
|
3576
3645
|
timeout: timeoutMs,
|
|
3577
3646
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -3670,17 +3739,71 @@ async function execInContainerAsync(config, sql, timeoutMs = 1e4) {
|
|
|
3670
3739
|
return execWithBunSpawn(bunSpawn, args, timeoutMs);
|
|
3671
3740
|
return execWithNodeSpawn(args, timeoutMs);
|
|
3672
3741
|
}
|
|
3742
|
+
function stripFinalLineBreak(text) {
|
|
3743
|
+
if (text.endsWith(`\r
|
|
3744
|
+
`))
|
|
3745
|
+
return text.slice(0, -2);
|
|
3746
|
+
if (text.endsWith(`
|
|
3747
|
+
`) || text.endsWith("\r"))
|
|
3748
|
+
return text.slice(0, -1);
|
|
3749
|
+
return text;
|
|
3750
|
+
}
|
|
3751
|
+
function decodeMysqlBatchField(value) {
|
|
3752
|
+
let out = "";
|
|
3753
|
+
for (let i = 0;i < value.length; i++) {
|
|
3754
|
+
const ch = value[i];
|
|
3755
|
+
if (ch !== "\\" || i + 1 >= value.length) {
|
|
3756
|
+
out += ch;
|
|
3757
|
+
continue;
|
|
3758
|
+
}
|
|
3759
|
+
const next = value[++i];
|
|
3760
|
+
switch (next) {
|
|
3761
|
+
case "0":
|
|
3762
|
+
out += "\x00";
|
|
3763
|
+
break;
|
|
3764
|
+
case "b":
|
|
3765
|
+
out += "\b";
|
|
3766
|
+
break;
|
|
3767
|
+
case "n":
|
|
3768
|
+
out += `
|
|
3769
|
+
`;
|
|
3770
|
+
break;
|
|
3771
|
+
case "r":
|
|
3772
|
+
out += "\r";
|
|
3773
|
+
break;
|
|
3774
|
+
case "t":
|
|
3775
|
+
out += "\t";
|
|
3776
|
+
break;
|
|
3777
|
+
case "Z":
|
|
3778
|
+
out += "\x1A";
|
|
3779
|
+
break;
|
|
3780
|
+
case "\\":
|
|
3781
|
+
out += "\\";
|
|
3782
|
+
break;
|
|
3783
|
+
default:
|
|
3784
|
+
out += `\\${next}`;
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
return out;
|
|
3789
|
+
}
|
|
3790
|
+
function splitTsvLine(line, decodeFields) {
|
|
3791
|
+
const fields = line.split("\t");
|
|
3792
|
+
return decodeFields ? fields.map(decodeMysqlBatchField) : fields;
|
|
3793
|
+
}
|
|
3673
3794
|
function parseTsvOutput(stdout, hasHeader) {
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3795
|
+
const text = stripFinalLineBreak(stdout);
|
|
3796
|
+
if (text.length === 0)
|
|
3797
|
+
return { columns: [], rows: [] };
|
|
3798
|
+
const lines = text.split(/\r?\n/);
|
|
3676
3799
|
if (lines.length === 0)
|
|
3677
3800
|
return { columns: [], rows: [] };
|
|
3678
3801
|
if (hasHeader) {
|
|
3679
|
-
const columns = lines[0]
|
|
3680
|
-
const rows2 = lines.slice(1).map((line) => line
|
|
3802
|
+
const columns = splitTsvLine(lines[0], true);
|
|
3803
|
+
const rows2 = lines.slice(1).map((line) => splitTsvLine(line, true));
|
|
3681
3804
|
return { columns, rows: rows2 };
|
|
3682
3805
|
}
|
|
3683
|
-
const rows = lines.map((line) => line
|
|
3806
|
+
const rows = lines.map((line) => splitTsvLine(line, false));
|
|
3684
3807
|
return { columns: [], rows };
|
|
3685
3808
|
}
|
|
3686
3809
|
function sanitizeIdentifier(name, kind) {
|
|
@@ -3694,6 +3817,23 @@ function buildOrderClause(orderBy, kind) {
|
|
|
3694
3817
|
const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
|
|
3695
3818
|
return ` ORDER BY ${parts.join(", ")}`;
|
|
3696
3819
|
}
|
|
3820
|
+
function isMysqlSpatialType(type) {
|
|
3821
|
+
const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
|
|
3822
|
+
return MYSQL_SPATIAL_TYPES.has(baseType);
|
|
3823
|
+
}
|
|
3824
|
+
function buildTableSelectList(columns, kind) {
|
|
3825
|
+
if (kind !== "mysql")
|
|
3826
|
+
return "*";
|
|
3827
|
+
let hasSpatialColumn = false;
|
|
3828
|
+
const parts = columns.map((column) => {
|
|
3829
|
+
const columnId = sanitizeIdentifier(column.name, kind);
|
|
3830
|
+
if (!isMysqlSpatialType(column.type))
|
|
3831
|
+
return columnId;
|
|
3832
|
+
hasSpatialColumn = true;
|
|
3833
|
+
return `ST_AsText(${columnId}) AS ${columnId}`;
|
|
3834
|
+
});
|
|
3835
|
+
return hasSpatialColumn ? parts.join(", ") : "*";
|
|
3836
|
+
}
|
|
3697
3837
|
function escapeSqlString(value) {
|
|
3698
3838
|
return `'${value.replace(/'/g, "''")}'`;
|
|
3699
3839
|
}
|
|
@@ -4018,12 +4158,15 @@ function createDockerAdapter(config) {
|
|
|
4018
4158
|
async getTablePageWithMeta(table, options) {
|
|
4019
4159
|
const id = sanitizeIdentifier(table, config.kind);
|
|
4020
4160
|
const order = buildOrderClause(options.orderBy, config.kind);
|
|
4021
|
-
const dataSql = `SELECT * FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
|
|
4022
4161
|
const countSql = `SELECT COUNT(*) AS cnt FROM ${id}`;
|
|
4023
|
-
const
|
|
4024
|
-
|
|
4162
|
+
const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table));
|
|
4163
|
+
const totalRowsPromise = tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql)));
|
|
4164
|
+
const columns = await columnsPromise;
|
|
4165
|
+
const selectList = buildTableSelectList(columns, config.kind);
|
|
4166
|
+
const dataSql = `SELECT ${selectList} FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
|
|
4167
|
+
const [dataResult, totalRows] = await Promise.all([
|
|
4025
4168
|
execAsync(dataSql),
|
|
4026
|
-
|
|
4169
|
+
totalRowsPromise
|
|
4027
4170
|
]);
|
|
4028
4171
|
return tablePageMetaFromResults(columns, dataResult, totalRows);
|
|
4029
4172
|
},
|
|
@@ -4032,21 +4175,25 @@ function createDockerAdapter(config) {
|
|
|
4032
4175
|
const order = buildOrderClause(options.orderBy, config.kind);
|
|
4033
4176
|
const where = buildDockerFilterWhere(options.grouped, config.kind);
|
|
4034
4177
|
const whereClause = where ? ` WHERE ${where}` : "";
|
|
4035
|
-
const dataSql = `SELECT * FROM ${id}${whereClause}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
|
|
4036
4178
|
const countSql = `SELECT COUNT(*) AS cnt FROM ${id}${whereClause}`;
|
|
4037
|
-
const
|
|
4038
|
-
|
|
4179
|
+
const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table));
|
|
4180
|
+
const countResultPromise = execAsync(countSql);
|
|
4181
|
+
const columns = await columnsPromise;
|
|
4182
|
+
const selectList = buildTableSelectList(columns, config.kind);
|
|
4183
|
+
const dataSql = `SELECT ${selectList} FROM ${id}${whereClause}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
|
|
4184
|
+
const [dataResult, countResult] = await Promise.all([
|
|
4039
4185
|
execAsync(dataSql),
|
|
4040
|
-
|
|
4186
|
+
countResultPromise
|
|
4041
4187
|
]);
|
|
4042
4188
|
return tablePageMetaFromResults(columns, dataResult, rowCountFromResult(countResult));
|
|
4043
4189
|
},
|
|
4044
4190
|
getTablePage(table, options) {
|
|
4045
4191
|
const id = sanitizeIdentifier(table, config.kind);
|
|
4046
4192
|
const order = buildOrderClause(options.orderBy, config.kind);
|
|
4047
|
-
const sql = `SELECT * FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
|
|
4048
|
-
const result = exec(sql);
|
|
4049
4193
|
const cols = this.getColumns(table);
|
|
4194
|
+
const selectList = buildTableSelectList(cols, config.kind);
|
|
4195
|
+
const sql = `SELECT ${selectList} FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
|
|
4196
|
+
const result = exec(sql);
|
|
4050
4197
|
if (result.rows.length === 0) {
|
|
4051
4198
|
return {
|
|
4052
4199
|
columns: cols.map((c) => c.name),
|
|
@@ -4163,34 +4310,48 @@ function createDockerAdapter(config) {
|
|
|
4163
4310
|
return adapter;
|
|
4164
4311
|
}
|
|
4165
4312
|
function listDockerDatabases(serviceName, kind, env, cwd) {
|
|
4313
|
+
const cacheKey = dockerDatabasesCacheKey(serviceName, kind, cwd);
|
|
4314
|
+
const now = Date.now();
|
|
4315
|
+
const cached = dockerDatabasesCache.get(cacheKey);
|
|
4316
|
+
if (cached && cached.expiresAt > now)
|
|
4317
|
+
return [...cached.value];
|
|
4166
4318
|
const containerName = resolveRunningComposeContainerName(serviceName, cwd);
|
|
4167
|
-
if (!containerName)
|
|
4168
|
-
return [];
|
|
4169
|
-
|
|
4170
|
-
const
|
|
4171
|
-
const
|
|
4319
|
+
if (!containerName) {
|
|
4320
|
+
return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
|
|
4321
|
+
}
|
|
4322
|
+
const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || env.POSTGRES_USERNAME || env.MYSQL_USERNAME || env.USER || "root";
|
|
4323
|
+
const password = env.POSTGRES_PASSWORD || env.MYSQL_PASSWORD || env.MARIADB_PASSWORD || "";
|
|
4324
|
+
const defaultDb = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || env.DATABASE_NAME || "";
|
|
4172
4325
|
const config = {
|
|
4173
4326
|
kind,
|
|
4174
4327
|
containerName,
|
|
4175
4328
|
user,
|
|
4176
4329
|
password,
|
|
4177
|
-
database: defaultDb || (kind === "postgresql" ? "postgres" : "mysql")
|
|
4330
|
+
database: defaultDb || (kind === "postgresql" ? user || "postgres" : "mysql")
|
|
4178
4331
|
};
|
|
4179
4332
|
try {
|
|
4180
4333
|
let sql;
|
|
4181
4334
|
if (kind === "postgresql") {
|
|
4182
|
-
sql = `SELECT datname FROM pg_database WHERE datistemplate = false
|
|
4335
|
+
sql = `SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname`;
|
|
4183
4336
|
} else {
|
|
4184
4337
|
sql = `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','performance_schema','mysql','sys') ORDER BY schema_name`;
|
|
4185
4338
|
}
|
|
4186
4339
|
const result = execInContainer(config, sql);
|
|
4187
|
-
if (result.code !== 0)
|
|
4188
|
-
|
|
4340
|
+
if (result.code !== 0) {
|
|
4341
|
+
const fallback = fallbackDockerDatabases(defaultDb);
|
|
4342
|
+
if (fallback.length > 0)
|
|
4343
|
+
return fallback;
|
|
4344
|
+
return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
|
|
4345
|
+
}
|
|
4189
4346
|
const parsed = parseTsvOutput(result.stdout, kind === "mysql");
|
|
4190
4347
|
const dbs = parsed.rows.map((r) => r[0]).filter(Boolean);
|
|
4191
|
-
|
|
4348
|
+
const value = dbs.length > 0 ? dbs : fallbackDockerDatabases(defaultDb);
|
|
4349
|
+
return setDockerDatabasesCache(cacheKey, value, value.length > 0 ? DOCKER_DATABASES_POSITIVE_TTL_MS : DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
|
|
4192
4350
|
} catch {
|
|
4193
|
-
|
|
4351
|
+
const fallback = fallbackDockerDatabases(defaultDb);
|
|
4352
|
+
if (fallback.length > 0)
|
|
4353
|
+
return fallback;
|
|
4354
|
+
return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
|
|
4194
4355
|
}
|
|
4195
4356
|
}
|
|
4196
4357
|
function openDockerAdapter(serviceName, kind, env, cwd, overrideDatabase) {
|
|
@@ -4206,10 +4367,23 @@ function openDockerAdapter(serviceName, kind, env, cwd, overrideDatabase) {
|
|
|
4206
4367
|
database
|
|
4207
4368
|
});
|
|
4208
4369
|
}
|
|
4209
|
-
var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000;
|
|
4370
|
+
var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, spawnSyncImpl2, MYSQL_SPATIAL_TYPES;
|
|
4210
4371
|
var init_docker = __esm(() => {
|
|
4211
4372
|
init_sql_snapshot();
|
|
4212
4373
|
init_docker_utils();
|
|
4374
|
+
dockerDatabasesCache = new Map;
|
|
4375
|
+
spawnSyncImpl2 = spawnSync3;
|
|
4376
|
+
MYSQL_SPATIAL_TYPES = new Set([
|
|
4377
|
+
"geometry",
|
|
4378
|
+
"point",
|
|
4379
|
+
"linestring",
|
|
4380
|
+
"polygon",
|
|
4381
|
+
"multipoint",
|
|
4382
|
+
"multilinestring",
|
|
4383
|
+
"multipolygon",
|
|
4384
|
+
"geometrycollection",
|
|
4385
|
+
"geomcollection"
|
|
4386
|
+
]);
|
|
4213
4387
|
});
|
|
4214
4388
|
|
|
4215
4389
|
// web-src/server/database/adapters/sqlite.ts
|
|
@@ -4561,6 +4735,7 @@ function isSqliteFile(fullPath) {
|
|
|
4561
4735
|
function discoverSqliteFiles(cwd, omitDirNames) {
|
|
4562
4736
|
const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
|
|
4563
4737
|
omitSet.add(".git");
|
|
4738
|
+
omitSet.add(".code-viewer");
|
|
4564
4739
|
const results = [];
|
|
4565
4740
|
function scan(dir, depth) {
|
|
4566
4741
|
if (depth > MAX_SCAN_DEPTH || results.length >= MAX_ENTRIES)
|
|
@@ -4605,20 +4780,14 @@ function discoverSqliteFiles(cwd, omitDirNames) {
|
|
|
4605
4780
|
}
|
|
4606
4781
|
}
|
|
4607
4782
|
scan(cwd, 0);
|
|
4608
|
-
results.sort((a, b) =>
|
|
4609
|
-
const aInternal = a.path.startsWith(".code-viewer/") ? 1 : 0;
|
|
4610
|
-
const bInternal = b.path.startsWith(".code-viewer/") ? 1 : 0;
|
|
4611
|
-
if (aInternal !== bInternal)
|
|
4612
|
-
return aInternal - bInternal;
|
|
4613
|
-
return a.path.localeCompare(b.path);
|
|
4614
|
-
});
|
|
4783
|
+
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4615
4784
|
return results;
|
|
4616
4785
|
}
|
|
4617
4786
|
function validateDbPath(cwd, dbPath) {
|
|
4618
4787
|
if (!dbPath || dbPath.includes("\x00") || dbPath.startsWith("/") || dbPath.startsWith("\\"))
|
|
4619
4788
|
return null;
|
|
4620
4789
|
const parts = dbPath.split(/[\\/]+/);
|
|
4621
|
-
if (parts.some((p) => p === ".." || p.toLowerCase() === ".git"))
|
|
4790
|
+
if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
|
|
4622
4791
|
return null;
|
|
4623
4792
|
const full = join8(cwd, dbPath);
|
|
4624
4793
|
if (!existsSync6(full))
|
|
@@ -5045,7 +5214,7 @@ function isTextLikeType(type) {
|
|
|
5045
5214
|
return upper.includes("CHAR") || upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CLOB") || upper.includes("STRING") || upper === "JSON" || upper === "JSONB" || upper === "XML" || upper === "UUID";
|
|
5046
5215
|
}
|
|
5047
5216
|
function escapeLikeTerm(term) {
|
|
5048
|
-
return term.replace(
|
|
5217
|
+
return term.replace(/=/g, "==").replace(/%/g, "=%").replace(/_/g, "=_");
|
|
5049
5218
|
}
|
|
5050
5219
|
function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkColumns) {
|
|
5051
5220
|
const kind = adapter.kind;
|
|
@@ -5063,10 +5232,10 @@ function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkC
|
|
|
5063
5232
|
let sql;
|
|
5064
5233
|
const remaining = maxHits - hits.length;
|
|
5065
5234
|
if (kind === "sqlite") {
|
|
5066
|
-
sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '
|
|
5235
|
+
sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '='`;
|
|
5067
5236
|
} else {
|
|
5068
5237
|
const likeVal = escapeSqlString2(`%${escapedTerm}%`);
|
|
5069
|
-
sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '
|
|
5238
|
+
sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '='`;
|
|
5070
5239
|
}
|
|
5071
5240
|
try {
|
|
5072
5241
|
const result = kind === "sqlite" ? adapter.executeReadonlyQuery(sql, [`%${escapedTerm}%`], remaining) : adapter.executeReadonlyQuery(sql, undefined, remaining);
|
|
@@ -5534,7 +5703,7 @@ function resolveDockerExplorer(cwd, dbParam, kind, cache, openFn, omitDirNames)
|
|
|
5534
5703
|
const explorer = cache.getOrOpen(dbParam, () => openFn(info));
|
|
5535
5704
|
return { dbId: dbParam, explorer };
|
|
5536
5705
|
}
|
|
5537
|
-
async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res) {
|
|
5706
|
+
async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
|
|
5538
5707
|
if (!Object.prototype.hasOwnProperty.call(routes, url.pathname))
|
|
5539
5708
|
return null;
|
|
5540
5709
|
const route = routes[url.pathname];
|
|
@@ -5545,7 +5714,13 @@ async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res)
|
|
|
5545
5714
|
if (requiresSideEffect && sideEffectAllowed && !sideEffectAllowed(req)) {
|
|
5546
5715
|
return wrap(textError("forbidden", 403));
|
|
5547
5716
|
}
|
|
5548
|
-
|
|
5717
|
+
try {
|
|
5718
|
+
return wrap(await route.handler());
|
|
5719
|
+
} catch (err) {
|
|
5720
|
+
if (!handleRouteError)
|
|
5721
|
+
throw err;
|
|
5722
|
+
return wrap(handleRouteError(err));
|
|
5723
|
+
}
|
|
5549
5724
|
}
|
|
5550
5725
|
async function parsePostJsonBody(req) {
|
|
5551
5726
|
if (req.method !== "POST") {
|
|
@@ -5560,10 +5735,14 @@ async function parsePostJsonBody(req) {
|
|
|
5560
5735
|
function handleError(prefix, action, err) {
|
|
5561
5736
|
const message = err instanceof Error ? err.message : String(err);
|
|
5562
5737
|
console.error(`[code-viewer] ${prefix} error:`, message);
|
|
5738
|
+
if (isDockerComposeServiceUnavailableError(err)) {
|
|
5739
|
+
return textError(message, err.status);
|
|
5740
|
+
}
|
|
5563
5741
|
return textError(`failed to ${action}: ${message}`, 500);
|
|
5564
5742
|
}
|
|
5565
5743
|
var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS;
|
|
5566
5744
|
var init_handle_shared = __esm(() => {
|
|
5745
|
+
init_docker_utils();
|
|
5567
5746
|
init_discovery();
|
|
5568
5747
|
DEFAULT_DOCKER_ADAPTER_IDLE_MS = 5 * 60 * 1000;
|
|
5569
5748
|
});
|
|
@@ -5741,7 +5920,7 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
|
|
|
5741
5920
|
sideEffect: (m) => m === "POST",
|
|
5742
5921
|
handler: () => handleSearch(cwd, req, url, omitDirNames)
|
|
5743
5922
|
}
|
|
5744
|
-
}, sideEffectAllowed, wrap);
|
|
5923
|
+
}, sideEffectAllowed, wrap, (err) => handleError("elasticsearch", "handle elasticsearch request", err));
|
|
5745
5924
|
}
|
|
5746
5925
|
var esAdapterCache;
|
|
5747
5926
|
var init_handle_elasticsearch = __esm(() => {
|
|
@@ -6472,7 +6651,7 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
|
|
|
6472
6651
|
methods: ["GET"],
|
|
6473
6652
|
handler: () => handleValue(cwd, url, omitDirNames)
|
|
6474
6653
|
}
|
|
6475
|
-
}, sideEffectAllowed, wrap);
|
|
6654
|
+
}, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
|
|
6476
6655
|
}
|
|
6477
6656
|
function handleValue(cwd, url, omitDirNames) {
|
|
6478
6657
|
const r = resolveRedis(cwd, url.searchParams.get("db"), omitDirNames);
|
|
@@ -6968,6 +7147,11 @@ function sanitizeCssSize(v) {
|
|
|
6968
7147
|
return;
|
|
6969
7148
|
return isValidCssSize(v) ? v : undefined;
|
|
6970
7149
|
}
|
|
7150
|
+
function isToolInternalDbId(dbId) {
|
|
7151
|
+
if (!dbId || dbId.startsWith("docker:"))
|
|
7152
|
+
return false;
|
|
7153
|
+
return dbId.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
|
|
7154
|
+
}
|
|
6971
7155
|
function sanitizeRedis(v) {
|
|
6972
7156
|
if (!v || typeof v !== "object")
|
|
6973
7157
|
return;
|
|
@@ -7023,6 +7207,8 @@ function sanitize(input) {
|
|
|
7023
7207
|
continue;
|
|
7024
7208
|
seenIds.add(id);
|
|
7025
7209
|
const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN) ?? null;
|
|
7210
|
+
if (isToolInternalDbId(dbId))
|
|
7211
|
+
continue;
|
|
7026
7212
|
const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
|
|
7027
7213
|
const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
|
|
7028
7214
|
const out = { id, dbId, table, view };
|
|
@@ -7497,6 +7683,9 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
7497
7683
|
}
|
|
7498
7684
|
return json(response);
|
|
7499
7685
|
} catch (err) {
|
|
7686
|
+
if (isDockerComposeServiceUnavailableError(err)) {
|
|
7687
|
+
return handleError("database", "execute query", err);
|
|
7688
|
+
}
|
|
7500
7689
|
console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
|
|
7501
7690
|
const elapsed = Date.now() - start;
|
|
7502
7691
|
const response = {
|
|
@@ -7723,7 +7912,7 @@ async function handleSearchStart(cwd, req, omitDirNames) {
|
|
|
7723
7912
|
const maxHitsPerTable = body.maxHitsPerTable ?? 50;
|
|
7724
7913
|
const includeNonText = body.includeNonText ?? false;
|
|
7725
7914
|
const filterTables = body.tables;
|
|
7726
|
-
|
|
7915
|
+
const runJob = async () => {
|
|
7727
7916
|
try {
|
|
7728
7917
|
const adapter = await getAdapter(r, cwd);
|
|
7729
7918
|
let tables = adapter.getTables().filter((t) => t.type === "table").map((t) => t.name);
|
|
@@ -7757,7 +7946,9 @@ async function handleSearchStart(cwd, req, omitDirNames) {
|
|
|
7757
7946
|
job.error = err instanceof Error ? err.message : String(err);
|
|
7758
7947
|
job.done = true;
|
|
7759
7948
|
}
|
|
7760
|
-
}
|
|
7949
|
+
};
|
|
7950
|
+
const timer = setTimeout(() => void runJob(), 0);
|
|
7951
|
+
timer.unref?.();
|
|
7761
7952
|
return json({ jobId });
|
|
7762
7953
|
}
|
|
7763
7954
|
function handleSearchStatus(url) {
|
|
@@ -8171,11 +8362,12 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
|
|
|
8171
8362
|
sideEffect: (m) => m !== "GET",
|
|
8172
8363
|
handler: () => method === "GET" ? handleTabsGet(cwd) : handleTabsPut(cwd, req)
|
|
8173
8364
|
}
|
|
8174
|
-
}, sideEffectAllowed, wrapResponse);
|
|
8365
|
+
}, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
|
|
8175
8366
|
}
|
|
8176
8367
|
var initialized = false, dockerAdapterCache, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
|
|
8177
8368
|
var init_handle = __esm(() => {
|
|
8178
8369
|
init_docker();
|
|
8370
|
+
init_docker_utils();
|
|
8179
8371
|
init_sqlite();
|
|
8180
8372
|
init_connection_pool();
|
|
8181
8373
|
init_discovery();
|
|
@@ -9986,15 +10178,45 @@ data: ${data}
|
|
|
9986
10178
|
try {
|
|
9987
10179
|
client.enqueue(payload);
|
|
9988
10180
|
} catch {
|
|
9989
|
-
|
|
10181
|
+
removeSseClient(client);
|
|
9990
10182
|
}
|
|
9991
10183
|
}
|
|
9992
10184
|
}
|
|
10185
|
+
function removeSseClient(ctrl) {
|
|
10186
|
+
sseClients.delete(ctrl);
|
|
10187
|
+
const keepalive = sseKeepalives.get(ctrl);
|
|
10188
|
+
if (keepalive)
|
|
10189
|
+
clearInterval(keepalive);
|
|
10190
|
+
sseKeepalives.delete(ctrl);
|
|
10191
|
+
}
|
|
10192
|
+
function closeSseClients() {
|
|
10193
|
+
for (const client of [...sseClients]) {
|
|
10194
|
+
removeSseClient(client);
|
|
10195
|
+
try {
|
|
10196
|
+
client.close();
|
|
10197
|
+
} catch {}
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
9993
10200
|
function openBrowser(url) {
|
|
9994
10201
|
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd.exe", "/c", "start", "", url] : ["xdg-open", url];
|
|
9995
10202
|
spawnDetached(cmd);
|
|
9996
10203
|
}
|
|
9997
|
-
|
|
10204
|
+
async function shutdown(exitCode = 0) {
|
|
10205
|
+
if (shuttingDown) {
|
|
10206
|
+
process.exit(1);
|
|
10207
|
+
}
|
|
10208
|
+
shuttingDown = true;
|
|
10209
|
+
removeServerRegistry(cwd, process.pid);
|
|
10210
|
+
closeSseClients();
|
|
10211
|
+
worktreeWatch?.close();
|
|
10212
|
+
try {
|
|
10213
|
+
await server.close();
|
|
10214
|
+
} catch (error) {
|
|
10215
|
+
console.warn(`code-viewer server close skipped: ${String(error)}`);
|
|
10216
|
+
}
|
|
10217
|
+
process.exit(exitCode);
|
|
10218
|
+
}
|
|
10219
|
+
var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, uploadDisabledByConfig = false, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, server, worktreeWatch = null, shuttingDown = false;
|
|
9998
10220
|
var init_preview = __esm(async () => {
|
|
9999
10221
|
init_routes();
|
|
10000
10222
|
init_annotations();
|
|
@@ -10057,6 +10279,7 @@ var init_preview = __esm(async () => {
|
|
|
10057
10279
|
scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
10058
10280
|
enc = new TextEncoder;
|
|
10059
10281
|
sseClients = new Set;
|
|
10282
|
+
sseKeepalives = new Map;
|
|
10060
10283
|
fileCache = new Map;
|
|
10061
10284
|
metaCache = new Map;
|
|
10062
10285
|
fileListCache = new Map;
|
|
@@ -10137,16 +10360,15 @@ data: ok
|
|
|
10137
10360
|
|
|
10138
10361
|
`));
|
|
10139
10362
|
} catch {
|
|
10140
|
-
|
|
10141
|
-
clearInterval(keepalive);
|
|
10363
|
+
removeSseClient(controller);
|
|
10142
10364
|
}
|
|
10143
10365
|
}, 15000);
|
|
10366
|
+
keepalive.unref?.();
|
|
10367
|
+
sseKeepalives.set(controller, keepalive);
|
|
10144
10368
|
},
|
|
10145
10369
|
cancel() {
|
|
10146
10370
|
if (ctrl)
|
|
10147
|
-
|
|
10148
|
-
if (keepalive)
|
|
10149
|
-
clearInterval(keepalive);
|
|
10371
|
+
removeSseClient(ctrl);
|
|
10150
10372
|
}
|
|
10151
10373
|
}), {
|
|
10152
10374
|
headers: {
|
|
@@ -10167,12 +10389,13 @@ data: ok
|
|
|
10167
10389
|
root: cwd,
|
|
10168
10390
|
started_at: new Date().toISOString()
|
|
10169
10391
|
});
|
|
10170
|
-
process.on("exit", () =>
|
|
10392
|
+
process.on("exit", () => {
|
|
10393
|
+
removeServerRegistry(cwd, process.pid);
|
|
10394
|
+
closeSseClients();
|
|
10395
|
+
worktreeWatch?.close();
|
|
10396
|
+
});
|
|
10171
10397
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
10172
|
-
process.on(signal, () =>
|
|
10173
|
-
removeServerRegistry(cwd, process.pid);
|
|
10174
|
-
process.exit(0);
|
|
10175
|
-
});
|
|
10398
|
+
process.on(signal, () => void shutdown(0));
|
|
10176
10399
|
}
|
|
10177
10400
|
if (process.env.CODE_VIEWER_DEV === "1") {
|
|
10178
10401
|
const parentPid = process.ppid;
|
|
@@ -10181,8 +10404,7 @@ data: ok
|
|
|
10181
10404
|
process.kill(parentPid, 0);
|
|
10182
10405
|
} catch {
|
|
10183
10406
|
console.log("dev wrapper exited; shutting down preview server");
|
|
10184
|
-
|
|
10185
|
-
process.exit(0);
|
|
10407
|
+
shutdown(0);
|
|
10186
10408
|
}
|
|
10187
10409
|
}, 1000).unref();
|
|
10188
10410
|
}
|
|
@@ -10193,7 +10415,7 @@ data: ok
|
|
|
10193
10415
|
watch,
|
|
10194
10416
|
sendReload: () => sendSse("reload")
|
|
10195
10417
|
});
|
|
10196
|
-
startWorktreeUpdateWatch({
|
|
10418
|
+
worktreeWatch = startWorktreeUpdateWatch({
|
|
10197
10419
|
root: cwd,
|
|
10198
10420
|
omitDirNames: scopeOmitDirNames,
|
|
10199
10421
|
excludeNames: scopeExcludeNames,
|