@youtyan/code-viewer 0.8.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -16
- package/dist/code-viewer.js +714 -137
- package/package.json +1 -1
- package/skills/code-viewer-query/SKILL.md +3 -1
- package/web/app.js +239 -75
- package/web/style.css +10 -4
package/package.json
CHANGED
|
@@ -41,7 +41,9 @@ browser's Database > Search tab, so the human can review the same workflow.
|
|
|
41
41
|
the sidebar — it lists every SQLite file plus PostgreSQL / MySQL / Redis
|
|
42
42
|
/ Elasticsearch / S3 service that any nearby `docker-compose` exposes, as
|
|
43
43
|
well as any local Supabase CLI (`supabase start`) project found via a
|
|
44
|
-
`supabase/config.toml` (id form `supabase:<project_id>`)
|
|
44
|
+
`supabase/config.toml` (id form `supabase:<project_id>`) and any saved
|
|
45
|
+
connection added from the browser UI (id form `connection:<uuid>`,
|
|
46
|
+
including Cloudflare D1 and R2).
|
|
45
47
|
Use the printed `id` as `--db` on every other command. Credentials and
|
|
46
48
|
internal config are stripped server-side.
|
|
47
49
|
|
package/web/app.js
CHANGED
|
@@ -7669,15 +7669,24 @@ ${lines.join(`
|
|
|
7669
7669
|
function markdownSlugify(text2) {
|
|
7670
7670
|
return text2.trim().toLowerCase().replace(/[\s ]+/g, "-").replace(/[^\p{L}\p{N}\-_]/gu, "").slice(0, 80) || "section";
|
|
7671
7671
|
}
|
|
7672
|
-
function
|
|
7672
|
+
function resolveMarkdownLinkTarget(currentPath, href) {
|
|
7673
7673
|
if (!href || href.startsWith("#"))
|
|
7674
7674
|
return null;
|
|
7675
7675
|
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(href))
|
|
7676
7676
|
return null;
|
|
7677
|
-
const
|
|
7678
|
-
|
|
7677
|
+
const hashAt = href.indexOf("#");
|
|
7678
|
+
const hash = hashAt < 0 ? "" : href.slice(hashAt + 1);
|
|
7679
|
+
const cleanHref = (hashAt < 0 ? href : href.slice(0, hashAt)).replace(/\?.*$/, "");
|
|
7680
|
+
if (!cleanHref)
|
|
7679
7681
|
return null;
|
|
7680
|
-
|
|
7682
|
+
const path = resolveRepoRelative(currentPath, decodeUriComponentSafe(cleanHref));
|
|
7683
|
+
if (path == null)
|
|
7684
|
+
return null;
|
|
7685
|
+
return {
|
|
7686
|
+
path,
|
|
7687
|
+
hash: decodeUriComponentSafe(hash),
|
|
7688
|
+
directory: cleanHref.endsWith("/")
|
|
7689
|
+
};
|
|
7681
7690
|
}
|
|
7682
7691
|
function resolveMarkdownAssetPath(currentPath, src) {
|
|
7683
7692
|
if (!src || src.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src))
|
|
@@ -7685,6 +7694,13 @@ ${lines.join(`
|
|
|
7685
7694
|
const cleanSrc = src.split(/[?#]/, 1)[0];
|
|
7686
7695
|
return resolveRepoRelative(currentPath, cleanSrc);
|
|
7687
7696
|
}
|
|
7697
|
+
function decodeUriComponentSafe(value) {
|
|
7698
|
+
try {
|
|
7699
|
+
return decodeURIComponent(value);
|
|
7700
|
+
} catch {
|
|
7701
|
+
return value;
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7688
7704
|
function resolveRepoRelative(currentPath, requestedPath) {
|
|
7689
7705
|
const base2 = currentPath.split("/").slice(0, -1);
|
|
7690
7706
|
const parts = [
|
|
@@ -7780,11 +7796,15 @@ ${lines.join(`
|
|
|
7780
7796
|
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
|
7781
7797
|
const token = tokens[idx];
|
|
7782
7798
|
const href = token.attrGet("href") || "";
|
|
7783
|
-
const
|
|
7784
|
-
if (
|
|
7799
|
+
const link2 = resolveMarkdownLinkTarget(target.path, href);
|
|
7800
|
+
if (link2) {
|
|
7785
7801
|
token.attrSet("href", "#");
|
|
7786
|
-
token.attrSet("data-gdp-md-link",
|
|
7802
|
+
token.attrSet("data-gdp-md-link", link2.path);
|
|
7787
7803
|
token.attrSet("data-gdp-md-ref", target.ref || "worktree");
|
|
7804
|
+
if (link2.hash)
|
|
7805
|
+
token.attrSet("data-gdp-md-hash", link2.hash);
|
|
7806
|
+
if (link2.directory)
|
|
7807
|
+
token.attrSet("data-gdp-md-dir", "1");
|
|
7788
7808
|
} else if (/^(?:https?:)?\/\//i.test(href)) {
|
|
7789
7809
|
token.attrSet("target", "_blank");
|
|
7790
7810
|
token.attrSet("rel", "noopener noreferrer");
|
|
@@ -7899,11 +7919,15 @@ ${frontmatter.yaml}
|
|
|
7899
7919
|
if (!link2)
|
|
7900
7920
|
return;
|
|
7901
7921
|
const path = link2.dataset.gdpMdLink;
|
|
7902
|
-
|
|
7903
|
-
if (!path)
|
|
7922
|
+
if (path == null)
|
|
7904
7923
|
return;
|
|
7905
7924
|
e2.preventDefault();
|
|
7906
|
-
options.onNavigateMarkdown?.(
|
|
7925
|
+
options.onNavigateMarkdown?.({
|
|
7926
|
+
path,
|
|
7927
|
+
ref: link2.dataset.gdpMdRef || target.ref,
|
|
7928
|
+
hash: link2.dataset.gdpMdHash || "",
|
|
7929
|
+
directory: link2.dataset.gdpMdDir === "1"
|
|
7930
|
+
});
|
|
7907
7931
|
});
|
|
7908
7932
|
setupMarkdownScrollSpy(root);
|
|
7909
7933
|
setupMermaidLightbox(root);
|
|
@@ -7986,12 +8010,7 @@ ${frontmatter.yaml}
|
|
|
7986
8010
|
scrollMarkdownSectionIntoView(section, "auto");
|
|
7987
8011
|
}
|
|
7988
8012
|
function decodeHashFragment(hash) {
|
|
7989
|
-
|
|
7990
|
-
try {
|
|
7991
|
-
return decodeURIComponent(value);
|
|
7992
|
-
} catch {
|
|
7993
|
-
return value;
|
|
7994
|
-
}
|
|
8013
|
+
return decodeUriComponentSafe(hash.startsWith("#") ? hash.slice(1) : hash);
|
|
7995
8014
|
}
|
|
7996
8015
|
function scrollMarkdownSectionIntoView(section, behavior) {
|
|
7997
8016
|
const top = section.getBoundingClientRect().top + window.scrollY - markdownAnchorOffset() - 12;
|
|
@@ -10309,11 +10328,21 @@ ${frontmatter.yaml}
|
|
|
10309
10328
|
var KINDS = [
|
|
10310
10329
|
"postgresql",
|
|
10311
10330
|
"mysql",
|
|
10331
|
+
"d1",
|
|
10312
10332
|
"redis",
|
|
10313
10333
|
"elasticsearch",
|
|
10314
10334
|
"s3",
|
|
10315
10335
|
"dynamodb"
|
|
10316
10336
|
];
|
|
10337
|
+
var S3_PROVIDERS = ["custom", "r2"];
|
|
10338
|
+
var R2_ENDPOINT_RE = /^https:\/\/([a-z0-9-]+)\.r2\.cloudflarestorage\.com$/i;
|
|
10339
|
+
var R2_REGION = "auto";
|
|
10340
|
+
function r2EndpointFor(accountId) {
|
|
10341
|
+
return `https://${accountId}.r2.cloudflarestorage.com`;
|
|
10342
|
+
}
|
|
10343
|
+
function r2AccountIdFromEndpoint(endpoint) {
|
|
10344
|
+
return endpoint ? R2_ENDPOINT_RE.exec(endpoint)?.[1] ?? null : null;
|
|
10345
|
+
}
|
|
10317
10346
|
function text2(language) {
|
|
10318
10347
|
return language === "ja" ? {
|
|
10319
10348
|
titleAdd: "データストア接続を追加",
|
|
@@ -10327,6 +10356,12 @@ ${frontmatter.yaml}
|
|
|
10327
10356
|
password: "パスワード",
|
|
10328
10357
|
database: "データベース",
|
|
10329
10358
|
schema: "既定スキーマ(任意)",
|
|
10359
|
+
provider: "プロバイダ",
|
|
10360
|
+
providerCustom: "S3 互換(AWS / MinIO / LocalStack)",
|
|
10361
|
+
providerR2: "Cloudflare R2",
|
|
10362
|
+
accountId: "Cloudflare アカウント ID",
|
|
10363
|
+
databaseId: "データベース ID",
|
|
10364
|
+
apiToken: "API トークン",
|
|
10330
10365
|
endpoint: "エンドポイント URL",
|
|
10331
10366
|
region: "リージョン",
|
|
10332
10367
|
accessKeyId: "アクセスキー ID",
|
|
@@ -10345,7 +10380,9 @@ ${frontmatter.yaml}
|
|
|
10345
10380
|
requestFailed: "接続情報を保存できませんでした",
|
|
10346
10381
|
deleteTitle: "保存済み接続を削除",
|
|
10347
10382
|
deleteBody: "この接続情報を削除します。タブやスナップショットのデータは削除されません。",
|
|
10348
|
-
delete: "削除"
|
|
10383
|
+
delete: "削除",
|
|
10384
|
+
secretsLeftTitle: "資格情報がキーチェーンに残りました",
|
|
10385
|
+
secretsLeftBody: "接続は削除しましたが、キーチェーンから資格情報を削除できませんでした。キーチェーンがロックされている可能性があります。ロックを解除して「キーチェーンアクセス」から code-viewer の項目を削除してください。"
|
|
10349
10386
|
} : {
|
|
10350
10387
|
titleAdd: "Add datastore connection",
|
|
10351
10388
|
titleEdit: "Edit datastore connection",
|
|
@@ -10358,6 +10395,12 @@ ${frontmatter.yaml}
|
|
|
10358
10395
|
password: "Password",
|
|
10359
10396
|
database: "Database",
|
|
10360
10397
|
schema: "Default schema (optional)",
|
|
10398
|
+
provider: "Provider",
|
|
10399
|
+
providerCustom: "S3-compatible (AWS / MinIO / LocalStack)",
|
|
10400
|
+
providerR2: "Cloudflare R2",
|
|
10401
|
+
accountId: "Cloudflare account ID",
|
|
10402
|
+
databaseId: "Database ID",
|
|
10403
|
+
apiToken: "API token",
|
|
10361
10404
|
endpoint: "Endpoint URL",
|
|
10362
10405
|
region: "Region",
|
|
10363
10406
|
accessKeyId: "Access key ID",
|
|
@@ -10376,7 +10419,9 @@ ${frontmatter.yaml}
|
|
|
10376
10419
|
requestFailed: "Failed to save the connection",
|
|
10377
10420
|
deleteTitle: "Delete saved connection",
|
|
10378
10421
|
deleteBody: "This removes the saved connection. Tabs and snapshot data are not deleted.",
|
|
10379
|
-
delete: "Delete"
|
|
10422
|
+
delete: "Delete",
|
|
10423
|
+
secretsLeftTitle: "Credentials left in the keychain",
|
|
10424
|
+
secretsLeftBody: "The connection was removed, but its credentials could not be deleted from the keychain — it may be locked. Unlock it and remove the code-viewer item from Keychain Access."
|
|
10380
10425
|
};
|
|
10381
10426
|
}
|
|
10382
10427
|
function field(labelText, input, required = false, requiredLabel = "Required") {
|
|
@@ -10440,6 +10485,21 @@ ${frontmatter.yaml}
|
|
|
10440
10485
|
database.value = current?.database ?? "";
|
|
10441
10486
|
const schema = input();
|
|
10442
10487
|
schema.value = current?.schema ?? "";
|
|
10488
|
+
const provider = document.createElement("select");
|
|
10489
|
+
for (const value of S3_PROVIDERS) {
|
|
10490
|
+
const option = document.createElement("option");
|
|
10491
|
+
option.value = value;
|
|
10492
|
+
option.textContent = value === "r2" ? labels.providerR2 : labels.providerCustom;
|
|
10493
|
+
provider.appendChild(option);
|
|
10494
|
+
}
|
|
10495
|
+
const currentR2AccountId = r2AccountIdFromEndpoint(current?.endpoint);
|
|
10496
|
+
provider.value = currentR2AccountId ? "r2" : "custom";
|
|
10497
|
+
const accountId = input();
|
|
10498
|
+
accountId.value = current?.accountId ?? currentR2AccountId ?? "";
|
|
10499
|
+
const databaseId = input();
|
|
10500
|
+
databaseId.value = current?.databaseId ?? "";
|
|
10501
|
+
const apiToken = input("password");
|
|
10502
|
+
apiToken.autocomplete = "new-password";
|
|
10443
10503
|
const endpoint = input("url");
|
|
10444
10504
|
endpoint.value = current?.endpoint ?? "http://127.0.0.1:9200";
|
|
10445
10505
|
const region = input();
|
|
@@ -10461,6 +10521,10 @@ ${frontmatter.yaml}
|
|
|
10461
10521
|
password: field(labels.password, password),
|
|
10462
10522
|
database: field(labels.database, database, true, labels.requiredField),
|
|
10463
10523
|
schema: field(labels.schema, schema),
|
|
10524
|
+
provider: field(labels.provider, provider),
|
|
10525
|
+
accountId: field(labels.accountId, accountId, true, labels.requiredField),
|
|
10526
|
+
databaseId: field(labels.databaseId, databaseId, true, labels.requiredField),
|
|
10527
|
+
apiToken: field(labels.apiToken, apiToken, true, labels.requiredField),
|
|
10464
10528
|
endpoint: field(labels.endpoint, endpoint, true, labels.requiredField),
|
|
10465
10529
|
region: field(labels.region, region, true, labels.requiredField),
|
|
10466
10530
|
accessKeyId: field(labels.accessKeyId, accessKeyId, true, labels.requiredField),
|
|
@@ -10469,21 +10533,28 @@ ${frontmatter.yaml}
|
|
|
10469
10533
|
tls: field(labels.tls, tls)
|
|
10470
10534
|
};
|
|
10471
10535
|
form.append(...Object.values(rows));
|
|
10536
|
+
const isR2 = () => kind.value === "s3" && provider.value === "r2";
|
|
10472
10537
|
const syncKind = () => {
|
|
10473
10538
|
const value = kind.value;
|
|
10474
10539
|
const sql = value === "postgresql" || value === "mysql";
|
|
10475
10540
|
const redis = value === "redis";
|
|
10476
10541
|
const http = value === "elasticsearch";
|
|
10477
10542
|
const aws = value === "s3" || value === "dynamodb";
|
|
10543
|
+
const d1 = value === "d1";
|
|
10544
|
+
const r2 = isR2();
|
|
10478
10545
|
rows.host.hidden = !sql && !redis;
|
|
10479
10546
|
rows.port.hidden = !sql && !redis;
|
|
10480
10547
|
rows.user.hidden = !sql;
|
|
10481
10548
|
rows.username.hidden = !redis && !http;
|
|
10482
|
-
rows.password.hidden = aws;
|
|
10549
|
+
rows.password.hidden = aws || d1;
|
|
10483
10550
|
rows.database.hidden = !sql;
|
|
10484
10551
|
rows.schema.hidden = value !== "postgresql";
|
|
10485
|
-
rows.
|
|
10486
|
-
rows.
|
|
10552
|
+
rows.provider.hidden = value !== "s3";
|
|
10553
|
+
rows.accountId.hidden = !d1 && !r2;
|
|
10554
|
+
rows.databaseId.hidden = !d1;
|
|
10555
|
+
rows.apiToken.hidden = !d1;
|
|
10556
|
+
rows.endpoint.hidden = !http && !aws || r2;
|
|
10557
|
+
rows.region.hidden = !aws || r2;
|
|
10487
10558
|
rows.accessKeyId.hidden = !aws;
|
|
10488
10559
|
rows.secretAccessKey.hidden = !aws;
|
|
10489
10560
|
rows.sessionToken.hidden = !aws;
|
|
@@ -10498,8 +10569,11 @@ ${frontmatter.yaml}
|
|
|
10498
10569
|
if (!current && (http || aws)) {
|
|
10499
10570
|
endpoint.value = value === "elasticsearch" ? "http://127.0.0.1:9200" : "http://127.0.0.1:4566";
|
|
10500
10571
|
}
|
|
10572
|
+
if (!current && aws)
|
|
10573
|
+
region.value = r2 ? R2_REGION : "us-east-1";
|
|
10501
10574
|
};
|
|
10502
10575
|
kind.addEventListener("change", syncKind);
|
|
10576
|
+
provider.addEventListener("change", syncKind);
|
|
10503
10577
|
syncKind();
|
|
10504
10578
|
const validateConnection = () => {
|
|
10505
10579
|
const value = kind.value;
|
|
@@ -10516,10 +10590,16 @@ ${frontmatter.yaml}
|
|
|
10516
10590
|
if ((value === "postgresql" || value === "mysql") && (!user.value.trim() || !database.value.trim())) {
|
|
10517
10591
|
return labels.required;
|
|
10518
10592
|
}
|
|
10519
|
-
if (
|
|
10593
|
+
if (value === "d1" && (!accountId.value.trim() || !databaseId.value.trim() || !apiToken.value)) {
|
|
10594
|
+
return labels.required;
|
|
10595
|
+
}
|
|
10596
|
+
if (isR2() && !accountId.value.trim()) {
|
|
10597
|
+
return labels.required;
|
|
10598
|
+
}
|
|
10599
|
+
if ((value === "elasticsearch" || value === "s3" && !isR2() || value === "dynamodb") && !endpoint.value.trim()) {
|
|
10520
10600
|
return labels.required;
|
|
10521
10601
|
}
|
|
10522
|
-
if ((value === "s3" || value === "dynamodb") && (!region.value.trim() || !accessKeyId.value.trim())) {
|
|
10602
|
+
if ((value === "s3" || value === "dynamodb") && (!isR2() && !region.value.trim() || !accessKeyId.value.trim())) {
|
|
10523
10603
|
return labels.required;
|
|
10524
10604
|
}
|
|
10525
10605
|
return null;
|
|
@@ -10555,10 +10635,16 @@ ${frontmatter.yaml}
|
|
|
10555
10635
|
...username.value.trim() || !current ? { username: username.value.trim() || undefined } : {},
|
|
10556
10636
|
...password.value || !current ? { password: password.value } : {}
|
|
10557
10637
|
});
|
|
10638
|
+
} else if (value === "d1") {
|
|
10639
|
+
Object.assign(payload, {
|
|
10640
|
+
accountId: accountId.value.trim(),
|
|
10641
|
+
databaseId: databaseId.value.trim(),
|
|
10642
|
+
apiToken: apiToken.value
|
|
10643
|
+
});
|
|
10558
10644
|
} else {
|
|
10559
10645
|
Object.assign(payload, {
|
|
10560
|
-
endpoint: endpoint.value.trim(),
|
|
10561
|
-
region: region.value.trim(),
|
|
10646
|
+
endpoint: isR2() ? r2EndpointFor(accountId.value.trim()) : endpoint.value.trim(),
|
|
10647
|
+
region: isR2() ? R2_REGION : region.value.trim(),
|
|
10562
10648
|
...accessKeyId.value.trim() || !current ? { accessKeyId: accessKeyId.value.trim() } : {},
|
|
10563
10649
|
...secretAccessKey.value || !current ? { secretAccessKey: secretAccessKey.value } : {},
|
|
10564
10650
|
...sessionToken.value ? { sessionToken: sessionToken.value } : {}
|
|
@@ -10668,6 +10754,13 @@ ${frontmatter.yaml}
|
|
|
10668
10754
|
}));
|
|
10669
10755
|
if (!response.ok)
|
|
10670
10756
|
throw new Error(await response.text() || labels.requestFailed);
|
|
10757
|
+
const body = await response.json().catch(() => ({}));
|
|
10758
|
+
if (body.secretsRemoved === false) {
|
|
10759
|
+
await showAlertDialog({
|
|
10760
|
+
title: labels.secretsLeftTitle,
|
|
10761
|
+
body: labels.secretsLeftBody
|
|
10762
|
+
});
|
|
10763
|
+
}
|
|
10671
10764
|
return true;
|
|
10672
10765
|
}
|
|
10673
10766
|
|
|
@@ -20343,8 +20436,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20343
20436
|
function isAbortError2(err) {
|
|
20344
20437
|
return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
|
|
20345
20438
|
}
|
|
20439
|
+
var SQL_KINDS = new Set([
|
|
20440
|
+
"sqlite",
|
|
20441
|
+
"postgresql",
|
|
20442
|
+
"mysql",
|
|
20443
|
+
"d1"
|
|
20444
|
+
]);
|
|
20445
|
+
var ROW_EDITABLE_KINDS = new Set([
|
|
20446
|
+
"sqlite",
|
|
20447
|
+
"postgresql",
|
|
20448
|
+
"mysql"
|
|
20449
|
+
]);
|
|
20346
20450
|
function isSqlKind(kind) {
|
|
20347
|
-
return kind
|
|
20451
|
+
return !!kind && SQL_KINDS.has(kind);
|
|
20452
|
+
}
|
|
20453
|
+
function isRowEditableKind(kind) {
|
|
20454
|
+
return !!kind && ROW_EDITABLE_KINDS.has(kind);
|
|
20348
20455
|
}
|
|
20349
20456
|
function isPostgresKind(kind) {
|
|
20350
20457
|
return kind === "postgresql";
|
|
@@ -20700,7 +20807,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20700
20807
|
cb.onStateChange();
|
|
20701
20808
|
},
|
|
20702
20809
|
getText: () => paneText(),
|
|
20703
|
-
getEditable: () =>
|
|
20810
|
+
getEditable: () => isRowEditableKind(currentDbInfo?.kind),
|
|
20704
20811
|
applyMutations: (mutations) => applyRowMutations(mutations),
|
|
20705
20812
|
onRefreshComplete: ({ table: table2, filters }) => {
|
|
20706
20813
|
if (filters.length === 0) {
|
|
@@ -26304,6 +26411,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
26304
26411
|
kind: "paragraph",
|
|
26305
26412
|
text: "A file detail page exposes up to four tabs — Preview, Code, Blame, History. For text files Code is the default and ?preview=1 opts in to the Markdown / HTML preview; media files (images, video, audio, PDF) show a Preview tab only, with no Code tab. Blame and History have their own canonical URLs (view=blame, view=history) so deep links and the browser back/forward stay in sync, and they keep the Repository sidebar visible. Opening another file from the repository tree keeps the active tab (a file that cannot be previewed falls back to Code)."
|
|
26306
26413
|
},
|
|
26414
|
+
{
|
|
26415
|
+
kind: "paragraph",
|
|
26416
|
+
text: "Relative links inside a Markdown preview lead to the same destinations as they do on GitHub: another Markdown file opens its file page, an #anchor opens the preview and scrolls to that heading, a non-Markdown file opens in the Code view, and a link to a directory opens that folder in the repository tree."
|
|
26417
|
+
},
|
|
26307
26418
|
{
|
|
26308
26419
|
kind: "paragraph",
|
|
26309
26420
|
text: "When the repository remote is on GitHub, repository and file headers can open the current path there. Selecting source lines also exposes separate actions to copy the AI reference, open the exact GitHub line range, or copy its URL. Markdown and HTML diff cards include a direct Preview shortcut."
|
|
@@ -26555,7 +26666,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
26555
26666
|
database: {
|
|
26556
26667
|
nav: "Datastores",
|
|
26557
26668
|
title: "Datastore Viewer",
|
|
26558
|
-
intro: "Browse SQLite files, Docker-hosted databases, Redis, Elasticsearch, DynamoDB, and S3-compatible object stores from one local viewer.",
|
|
26669
|
+
intro: "Browse SQLite files, Docker-hosted databases, Cloudflare D1, Redis, Elasticsearch, DynamoDB, and S3-compatible object stores (including Cloudflare R2) from one local viewer.",
|
|
26559
26670
|
groups: [
|
|
26560
26671
|
{
|
|
26561
26672
|
title: "Supported datastores",
|
|
@@ -26565,7 +26676,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
26565
26676
|
rows: [
|
|
26566
26677
|
[
|
|
26567
26678
|
"Saved connections",
|
|
26568
|
-
"Use + beside the datastore selector to add an arbitrary PostgreSQL, MySQL, Redis, Elasticsearch, S3-compatible, or DynamoDB endpoint. Required fields are marked, and Test connection verifies the current values before saving. Drivers are included, so no database CLI or curl is required. Non-secret settings are saved locally;
|
|
26679
|
+
"Use + beside the datastore selector to add an arbitrary PostgreSQL, MySQL, Cloudflare D1, Redis, Elasticsearch, S3-compatible (including a Cloudflare R2 preset), or DynamoDB endpoint. Required fields are marked, and Test connection verifies the current values before saving. Drivers are included, so no database CLI or curl is required. Non-secret settings are saved locally; credentials are never written into the repository — on macOS they are kept in the Keychain so they survive a restart, and elsewhere they stay in server memory and must be entered again."
|
|
26569
26680
|
],
|
|
26570
26681
|
[
|
|
26571
26682
|
"SQLite",
|
|
@@ -26579,6 +26690,10 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
26579
26690
|
"PostgreSQL",
|
|
26580
26691
|
"Detected from compose files. Multiple databases per server, plus a schema selector for switching schemas without reopening. Same inline row edit / insert / delete as SQLite. Local Supabase CLI (`supabase start`) projects are also auto-discovered from `supabase/config.toml`, without needing a docker-compose file."
|
|
26581
26692
|
],
|
|
26693
|
+
[
|
|
26694
|
+
"Cloudflare D1",
|
|
26695
|
+
"Added as a saved connection with an account ID, database ID, and API token (needs D1:Read). Browsed over the D1 REST API and reuses the SQL screens — table list, row grid, query editor, schema, ER diagram, snapshots and diffs. Read-only: the query editor accepts SELECT / PRAGMA / EXPLAIN / WITH only, and grid Edit mode is not offered."
|
|
26696
|
+
],
|
|
26582
26697
|
[
|
|
26583
26698
|
"Redis",
|
|
26584
26699
|
"Detected from compose files. Browse DB 0-15, SCAN keys, dedicated string/hash/list panes, JSON view for set/zset/stream. Edit values, delete keys, and create new keys (all types) via in-pane editors. Participates in snapshots and diffs."
|
|
@@ -26592,8 +26707,8 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
26592
26707
|
"Detected when DynamoDB is enabled on a LocalStack compose service. List tables, browse a Structure tab (key schema, GSI/LSI, and non-key attribute types inferred from loaded items), scan or query items, follow pagination tokens, and open item details with a copyable key. Browsing is read-only."
|
|
26593
26708
|
],
|
|
26594
26709
|
[
|
|
26595
|
-
"S3 / MinIO / LocalStack",
|
|
26596
|
-
"Detected from compose files. Folder-tree browse, prefix/filename search, updated-time sort, and previews for images, video, audio, PDF, Markdown, HTML, and text. Edit text/markdown/JSON object bodies inline, upload new objects, and delete existing ones. LocalStack falls back to `docker exec curl` when no host port is published; MinIO requires a published host port."
|
|
26710
|
+
"S3 / MinIO / LocalStack / Cloudflare R2",
|
|
26711
|
+
"Detected from compose files, or added as a saved connection. R2 uses the Cloudflare R2 provider preset: enter the account ID and an R2 access key pair, and the endpoint and the required auto signing region are filled in. Folder-tree browse, prefix/filename search, updated-time sort, and previews for images, video, audio, PDF, Markdown, HTML, and text. Edit text/markdown/JSON object bodies inline, upload new objects, and delete existing ones. LocalStack falls back to `docker exec curl` when no host port is published; MinIO requires a published host port."
|
|
26597
26712
|
]
|
|
26598
26713
|
]
|
|
26599
26714
|
}
|
|
@@ -26987,6 +27102,10 @@ code-viewer query agent-help`
|
|
|
26987
27102
|
kind: "paragraph",
|
|
26988
27103
|
text: "ファイル詳細ページには最大 4 つのタブ (Preview / Code / Blame / History) があります。テキストファイルは Code がデフォルトで、?preview=1 を付けると Markdown / HTML プレビューに切り替わります。画像・動画・音声・PDF などのメディアファイルは Preview タブのみ表示され、Code タブは表示されません。Blame と History はそれぞれ専用 URL (view=blame, view=history) を持つため、ディープリンクとブラウザの戻る / 進むが同期し、いずれも Repository サイドバーは表示されたままです。ツリーから別のファイルを開いても選択中のタブは維持されます (プレビューできないファイルでは Code に戻ります)。"
|
|
26989
27104
|
},
|
|
27105
|
+
{
|
|
27106
|
+
kind: "paragraph",
|
|
27107
|
+
text: "Markdown プレビュー内の相対リンクは GitHub と同じ行き先に解決されます。別の Markdown ファイルはそのファイルページを開き、#見出し 付きのリンクはプレビューを開いて該当見出しまでスクロールし、Markdown 以外のファイルは Code ビュー、ディレクトリへのリンクはリポジトリツリーのそのフォルダを開きます。"
|
|
27108
|
+
},
|
|
26990
27109
|
{
|
|
26991
27110
|
kind: "paragraph",
|
|
26992
27111
|
text: "リポジトリの remote が GitHub の場合、リポジトリとファイルのヘッダーから現在のパスを GitHub で開けます。ソース行を選択すると、AI 参照のコピー、選択行範囲を GitHub で開く、GitHub URL のコピーを個別に選べます。Markdown / HTML の diff カードには直接 Preview を開く導線も表示されます。"
|
|
@@ -27238,7 +27357,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
27238
27357
|
database: {
|
|
27239
27358
|
nav: "データストア",
|
|
27240
27359
|
title: "データストアビューア",
|
|
27241
|
-
intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、DynamoDB、S3
|
|
27360
|
+
intro: "SQLite ファイル、Docker 上のデータベース、Cloudflare D1、Redis、Elasticsearch、DynamoDB、S3 互換オブジェクトストア (Cloudflare R2 を含む) をローカルビューアで閲覧できます。",
|
|
27242
27361
|
groups: [
|
|
27243
27362
|
{
|
|
27244
27363
|
title: "対応データストア",
|
|
@@ -27248,7 +27367,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
27248
27367
|
rows: [
|
|
27249
27368
|
[
|
|
27250
27369
|
"保存済み接続",
|
|
27251
|
-
"データストア選択の横にある + から、任意の PostgreSQL、MySQL、Redis、Elasticsearch、S3
|
|
27370
|
+
"データストア選択の横にある + から、任意の PostgreSQL、MySQL、Cloudflare D1、Redis、Elasticsearch、S3 互換 (Cloudflare R2 プリセットあり)、DynamoDB エンドポイントを追加できます。必須項目にはマークが付き、保存前に「接続テスト」で入力内容を確認できます。ドライバーは同梱されているため、データベース CLI や curl は不要です。非機密設定はローカルに保存されます。資格情報はリポジトリ配下には一切書かれず、macOS ではキーチェーンに保存されるため再起動をまたいで保持されます (それ以外の OS ではサーバーメモリのみで、再起動後は再入力が必要です)。"
|
|
27252
27371
|
],
|
|
27253
27372
|
[
|
|
27254
27373
|
"SQLite",
|
|
@@ -27262,6 +27381,10 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
27262
27381
|
"PostgreSQL",
|
|
27263
27382
|
"compose ファイルから検出。同一サーバー上の複数データベースに対応し、スキーマ切替セレクターで再オープンせずスキーマを切り替えられます。SQLite と同じく行のインライン編集 / 追加 / 削除に対応。ローカルの Supabase CLI (`supabase start`) プロジェクトも `supabase/config.toml` から自動検出され、docker-compose ファイルは不要です。"
|
|
27264
27383
|
],
|
|
27384
|
+
[
|
|
27385
|
+
"Cloudflare D1",
|
|
27386
|
+
"アカウント ID・データベース ID・API トークン (D1:Read 権限が必要) を入力して保存済み接続として追加します。D1 REST API 経由で閲覧し、SQL 系の画面 (テーブル一覧・行グリッド・クエリエディタ・スキーマ・ER 図・スナップショット/差分) をそのまま使えます。閲覧専用で、クエリエディタは SELECT / PRAGMA / EXPLAIN / WITH のみ受け付け、グリッドの Edit モードは表示されません。"
|
|
27387
|
+
],
|
|
27265
27388
|
[
|
|
27266
27389
|
"Redis",
|
|
27267
27390
|
"compose ファイルから検出。DB 0-15 を SCAN し、string/hash/list は専用ペイン、set/zset/stream は JSON ビュー。値の編集 / キー削除 / 新規キー作成 (全タイプ) に対応。スナップショット/差分にも参加します。"
|
|
@@ -27275,8 +27398,8 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
27275
27398
|
"LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、構造タブ(キースキーマ・GSI/LSI・読み込み済みアイテムから推測した非キー属性の型)、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
|
|
27276
27399
|
],
|
|
27277
27400
|
[
|
|
27278
|
-
"S3 / MinIO / LocalStack",
|
|
27279
|
-
"compose
|
|
27401
|
+
"S3 / MinIO / LocalStack / Cloudflare R2",
|
|
27402
|
+
"compose ファイルから検出するほか、保存済み接続としても追加できます。R2 は「Cloudflare R2」プロバイダプリセットを選び、アカウント ID と R2 のアクセスキーを入力すればエンドポイントと必須リージョン auto が自動で入ります。フォルダツリー型ブラウザ、prefix/ファイル名検索、更新日時順表示、画像/動画/音声/PDF/Markdown/HTML/テキストのプレビューに対応。テキスト/Markdown/JSON のインライン編集、新規オブジェクトアップロード、オブジェクト削除も可能。LocalStack はホストポート未公開時 `docker exec curl` にフォールバックしますが、MinIO はホストポート公開が必須です。"
|
|
27280
27403
|
]
|
|
27281
27404
|
]
|
|
27282
27405
|
}
|
|
@@ -30831,6 +30954,59 @@ code-viewer query agent-help`
|
|
|
30831
30954
|
return trimmed;
|
|
30832
30955
|
}
|
|
30833
30956
|
|
|
30957
|
+
// web-src/views/markdown-link-navigation.ts
|
|
30958
|
+
async function openMarkdownLink(target, deps) {
|
|
30959
|
+
const ref = target.ref || "worktree";
|
|
30960
|
+
const routeAtClick = currentRouteKey();
|
|
30961
|
+
const directory = await isMarkdownDirectoryLink(target, deps);
|
|
30962
|
+
if (directory === null || currentRouteKey() !== routeAtClick)
|
|
30963
|
+
return;
|
|
30964
|
+
if (directory) {
|
|
30965
|
+
deps.setRoute(deps.repoRoute(ref, target.path));
|
|
30966
|
+
await deps.loadRepo();
|
|
30967
|
+
return;
|
|
30968
|
+
}
|
|
30969
|
+
const anchorInMarkdown = !!target.hash && sourcePreviewKind(target.path) === "markdown";
|
|
30970
|
+
deps.setRoute({
|
|
30971
|
+
screen: "file",
|
|
30972
|
+
path: target.path,
|
|
30973
|
+
ref,
|
|
30974
|
+
view: "blob",
|
|
30975
|
+
...anchorInMarkdown ? { preview: true } : {},
|
|
30976
|
+
range: deps.currentRange()
|
|
30977
|
+
});
|
|
30978
|
+
applyMarkdownLinkHash(target.hash);
|
|
30979
|
+
await deps.renderStandaloneSource({ path: target.path, ref });
|
|
30980
|
+
}
|
|
30981
|
+
function currentRouteKey() {
|
|
30982
|
+
return window.location.pathname + window.location.search;
|
|
30983
|
+
}
|
|
30984
|
+
async function isMarkdownDirectoryLink(target, deps) {
|
|
30985
|
+
if (target.directory)
|
|
30986
|
+
return true;
|
|
30987
|
+
if (!target.path)
|
|
30988
|
+
return true;
|
|
30989
|
+
const name = target.path.split("/").pop() || "";
|
|
30990
|
+
if (/.\.[^.]+$/.test(name))
|
|
30991
|
+
return false;
|
|
30992
|
+
try {
|
|
30993
|
+
const res = await deps.trackLoad(fetch(buildRawFileUrl({ path: target.path, ref: target.ref }), {
|
|
30994
|
+
method: "HEAD"
|
|
30995
|
+
}));
|
|
30996
|
+
return res.status === 404;
|
|
30997
|
+
} catch (err) {
|
|
30998
|
+
if (deps.isAbortError(err))
|
|
30999
|
+
return null;
|
|
31000
|
+
return false;
|
|
31001
|
+
}
|
|
31002
|
+
}
|
|
31003
|
+
function applyMarkdownLinkHash(hash) {
|
|
31004
|
+
if (!hash)
|
|
31005
|
+
return;
|
|
31006
|
+
const base2 = window.location.pathname + window.location.search;
|
|
31007
|
+
history.replaceState(history.state, "", `${base2}#${encodeURIComponent(hash)}`);
|
|
31008
|
+
}
|
|
31009
|
+
|
|
30834
31010
|
// web-src/views/repository-web-link.ts
|
|
30835
31011
|
function createRepositoryWebLink(target, label) {
|
|
30836
31012
|
const link2 = document.createElement("a");
|
|
@@ -32533,6 +32709,7 @@ code-viewer query agent-help`
|
|
|
32533
32709
|
renderStandaloneSource,
|
|
32534
32710
|
repoFileTargetFromRoute,
|
|
32535
32711
|
trackLoad,
|
|
32712
|
+
isAbortError: isAbortError3,
|
|
32536
32713
|
syncSidebarHeaderHeight,
|
|
32537
32714
|
clearLoadQueue,
|
|
32538
32715
|
getProjectName,
|
|
@@ -32894,6 +33071,17 @@ code-viewer query agent-help`
|
|
|
32894
33071
|
range: currentRange()
|
|
32895
33072
|
};
|
|
32896
33073
|
}
|
|
33074
|
+
function markdownLinkNavigationDeps() {
|
|
33075
|
+
return {
|
|
33076
|
+
setRoute,
|
|
33077
|
+
currentRange,
|
|
33078
|
+
loadRepo,
|
|
33079
|
+
repoRoute,
|
|
33080
|
+
renderStandaloneSource,
|
|
33081
|
+
trackLoad,
|
|
33082
|
+
isAbortError: isAbortError3
|
|
33083
|
+
};
|
|
33084
|
+
}
|
|
32897
33085
|
function createRepoBreadcrumb(target, path) {
|
|
32898
33086
|
const nav = document.createElement("nav");
|
|
32899
33087
|
nav.className = "gdp-file-breadcrumb gdp-repo-breadcrumb";
|
|
@@ -33087,16 +33275,7 @@ code-viewer query agent-help`
|
|
|
33087
33275
|
try {
|
|
33088
33276
|
wrapper.appendChild(await renderMarkdownPreview(meta.readme.text, { path: meta.readme.path, ref: meta.ref }, {
|
|
33089
33277
|
syntaxHighlight: STATE.syntaxHighlight,
|
|
33090
|
-
onNavigateMarkdown: (
|
|
33091
|
-
setRoute({
|
|
33092
|
-
screen: "file",
|
|
33093
|
-
path,
|
|
33094
|
-
ref,
|
|
33095
|
-
view: "blob",
|
|
33096
|
-
range: currentRange()
|
|
33097
|
-
});
|
|
33098
|
-
renderStandaloneSource({ path, ref });
|
|
33099
|
-
}
|
|
33278
|
+
onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
|
|
33100
33279
|
}));
|
|
33101
33280
|
} catch {
|
|
33102
33281
|
const fallback = document.createElement("pre");
|
|
@@ -34321,6 +34500,17 @@ code-viewer query agent-help`
|
|
|
34321
34500
|
focusMainSurface,
|
|
34322
34501
|
isPaletteOpen
|
|
34323
34502
|
} = deps;
|
|
34503
|
+
function markdownLinkNavigationDeps() {
|
|
34504
|
+
return {
|
|
34505
|
+
setRoute,
|
|
34506
|
+
currentRange,
|
|
34507
|
+
loadRepo,
|
|
34508
|
+
repoRoute,
|
|
34509
|
+
renderStandaloneSource,
|
|
34510
|
+
trackLoad,
|
|
34511
|
+
isAbortError: isAbortError3
|
|
34512
|
+
};
|
|
34513
|
+
}
|
|
34324
34514
|
const VIRTUAL_SOURCE_LINE_THRESHOLD = 3000;
|
|
34325
34515
|
const VIRTUAL_SOURCE_SIZE_THRESHOLD = 1024 * 1024;
|
|
34326
34516
|
const VIRTUAL_SOURCE_PAGE_SIZE = 2000;
|
|
@@ -34562,16 +34752,7 @@ code-viewer query agent-help`
|
|
|
34562
34752
|
(deps.renderMarkdownPreview ?? renderMarkdownPreview)(textValue, target, {
|
|
34563
34753
|
syntaxHighlight: true,
|
|
34564
34754
|
signal,
|
|
34565
|
-
onNavigateMarkdown: (
|
|
34566
|
-
setRoute({
|
|
34567
|
-
screen: "file",
|
|
34568
|
-
path,
|
|
34569
|
-
ref,
|
|
34570
|
-
view: "blob",
|
|
34571
|
-
range: currentRange()
|
|
34572
|
-
});
|
|
34573
|
-
renderStandaloneSource({ path, ref });
|
|
34574
|
-
}
|
|
34755
|
+
onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
|
|
34575
34756
|
}).then((next) => {
|
|
34576
34757
|
if (signal?.aborted || !preview.isConnected || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
34577
34758
|
return;
|
|
@@ -34856,16 +35037,7 @@ code-viewer query agent-help`
|
|
|
34856
35037
|
let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(textValue, target, {
|
|
34857
35038
|
syntaxHighlight: false,
|
|
34858
35039
|
signal,
|
|
34859
|
-
onNavigateMarkdown: (
|
|
34860
|
-
setRoute({
|
|
34861
|
-
screen: "file",
|
|
34862
|
-
path,
|
|
34863
|
-
ref,
|
|
34864
|
-
view: "blob",
|
|
34865
|
-
range: currentRange()
|
|
34866
|
-
});
|
|
34867
|
-
renderStandaloneSource({ path, ref });
|
|
34868
|
-
}
|
|
35040
|
+
onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
|
|
34869
35041
|
});
|
|
34870
35042
|
if (signal?.aborted)
|
|
34871
35043
|
return false;
|
|
@@ -34956,16 +35128,7 @@ code-viewer query agent-help`
|
|
|
34956
35128
|
let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(textValue, target, {
|
|
34957
35129
|
syntaxHighlight: false,
|
|
34958
35130
|
signal,
|
|
34959
|
-
onNavigateMarkdown: (
|
|
34960
|
-
setRoute({
|
|
34961
|
-
screen: "file",
|
|
34962
|
-
path,
|
|
34963
|
-
ref,
|
|
34964
|
-
view: "blob",
|
|
34965
|
-
range: currentRange()
|
|
34966
|
-
});
|
|
34967
|
-
renderStandaloneSource({ path, ref });
|
|
34968
|
-
}
|
|
35131
|
+
onNavigateMarkdown: (link2) => void openMarkdownLink(link2, markdownLinkNavigationDeps())
|
|
34969
35132
|
});
|
|
34970
35133
|
if (signal?.aborted)
|
|
34971
35134
|
return false;
|
|
@@ -36754,6 +36917,7 @@ code-viewer query agent-help`
|
|
|
36754
36917
|
renderStandaloneSource,
|
|
36755
36918
|
repoFileTargetFromRoute,
|
|
36756
36919
|
trackLoad,
|
|
36920
|
+
isAbortError: isAbortError3,
|
|
36757
36921
|
syncSidebarHeaderHeight,
|
|
36758
36922
|
clearLoadQueue: () => DIFF_VIEW.clearLoadQueue(),
|
|
36759
36923
|
getProjectName: () => PROJECT_NAME,
|
package/web/style.css
CHANGED
|
@@ -3766,10 +3766,12 @@ table.d2h-diff-table td.d2h-code-side-linenumber {
|
|
|
3766
3766
|
border-radius: 6px;
|
|
3767
3767
|
}
|
|
3768
3768
|
.gdp-upload-panel {
|
|
3769
|
-
|
|
3769
|
+
/* Grid, not space-between flex: the always-present error node would
|
|
3770
|
+
otherwise take the right edge and strand the button in the middle. */
|
|
3771
|
+
display: grid;
|
|
3772
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
3770
3773
|
align-items: center;
|
|
3771
|
-
|
|
3772
|
-
gap: 12px;
|
|
3774
|
+
gap: 6px 12px;
|
|
3773
3775
|
margin: 12px 14px;
|
|
3774
3776
|
padding: 10px 14px;
|
|
3775
3777
|
border: 1px dashed var(--border);
|
|
@@ -3789,11 +3791,15 @@ table.d2h-diff-table td.d2h-code-side-linenumber {
|
|
|
3789
3791
|
border-color: var(--danger);
|
|
3790
3792
|
}
|
|
3791
3793
|
.gdp-upload-error {
|
|
3792
|
-
|
|
3794
|
+
/* Second row, full width, so showing an error never shifts the button. */
|
|
3795
|
+
grid-column: 1 / -1;
|
|
3793
3796
|
color: var(--danger);
|
|
3794
3797
|
font-size: var(--ui-font-base);
|
|
3795
3798
|
line-height: 18px;
|
|
3796
3799
|
}
|
|
3800
|
+
.gdp-upload-error:empty {
|
|
3801
|
+
display: none;
|
|
3802
|
+
}
|
|
3797
3803
|
.gdp-upload-copy {
|
|
3798
3804
|
min-width: 0;
|
|
3799
3805
|
color: var(--fg-muted);
|