quilltap 4.8.0-dev.143 → 4.8.0-dev.147
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/lib/docs-commands.js +64 -27
- package/package.json +1 -1
package/lib/docs-commands.js
CHANGED
|
@@ -84,9 +84,9 @@ Write subcommands (server required for database-backed mounts):
|
|
|
84
84
|
write [--force] [--base64] <mount> <path> [file] Write a file from <file> or stdin
|
|
85
85
|
delete <mount> <path> Idempotent file delete
|
|
86
86
|
mkdir <mount> <path> Idempotent folder create
|
|
87
|
-
move <srcMount> <srcPath> <dstMount> <dstPath> Move file (
|
|
88
|
-
copy [--force] <srcMount> <srcPath> <dstMount> <dstPath> Copy file (
|
|
89
|
-
link <srcMount> <srcPath> <dstMount> <dstPath> Hard-link file (server-required)
|
|
87
|
+
move <srcMount> <srcPath> <dstMount> <dstPath> Move file (relocates the link; no byte copy)
|
|
88
|
+
copy [--force] <srcMount> <srcPath> <dstMount> <dstPath> Copy file (independent; shares bytes until either side is written)
|
|
89
|
+
link <srcMount> <srcPath> <dstMount> <dstPath> Hard-link file — one file, two paths; edits show at both (server-required)
|
|
90
90
|
rmdir <mount> <path> Delete an empty folder (server-required)
|
|
91
91
|
mvdir <mount> <fromPath> <toPath> Rename/move a folder (server-required)
|
|
92
92
|
|
|
@@ -117,14 +117,17 @@ Options:
|
|
|
117
117
|
file size, or hard-link count
|
|
118
118
|
-r, --reverse Reverse sort order
|
|
119
119
|
--links For 'ls' / 'dir': under each file with more than
|
|
120
|
-
one hard link, list the other mount/path entries
|
|
120
|
+
one hard link, list the other mount/path entries.
|
|
121
|
+
Counts deliberate links made with 'docs link' — not
|
|
122
|
+
unrelated files that merely share identical bytes
|
|
121
123
|
--depth N For 'tree': maximum nesting depth (default: 20)
|
|
122
124
|
--max-nodes N For 'tree': maximum nodes to render (default: 1000)
|
|
123
125
|
--long For 'tree': include text/emb columns (reserved for future)
|
|
124
126
|
--force For 'read': dump binary to TTY anyway
|
|
125
127
|
For 'write': overwrite existing destination
|
|
126
128
|
For 'copy': overwrite + force a real byte copy
|
|
127
|
-
(skips the default
|
|
129
|
+
(skips the default shared-content path;
|
|
130
|
+
the end state is the same either way)
|
|
128
131
|
--base64 For 'write': send content as base64 JSON via PUT
|
|
129
132
|
.../files/{path} (portable path used
|
|
130
133
|
by the file browser; server-required)
|
|
@@ -624,21 +627,51 @@ function formatLsDate(iso) {
|
|
|
624
627
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
625
628
|
}
|
|
626
629
|
|
|
627
|
-
|
|
630
|
+
// Cached per process: one PRAGMA per run, not one per prepared statement.
|
|
631
|
+
let linkGroupColumnPresent = null;
|
|
632
|
+
|
|
633
|
+
function hasLinkGroupColumn(db) {
|
|
634
|
+
if (linkGroupColumnPresent !== null) return linkGroupColumnPresent;
|
|
635
|
+
try {
|
|
636
|
+
const cols = db.prepare(`PRAGMA table_info("doc_mount_file_links")`).all();
|
|
637
|
+
linkGroupColumnPresent = cols.some((c) => c.name === 'linkGroupId');
|
|
638
|
+
} catch {
|
|
639
|
+
linkGroupColumnPresent = false;
|
|
640
|
+
}
|
|
641
|
+
return linkGroupColumnPresent;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The "links" column counts deliberate hard links — members of this file's
|
|
645
|
+
// linkGroupId — NOT rows sharing a fileId. Content rows are addressed by
|
|
646
|
+
// sha256, so a boilerplate or empty file collects dozens of unrelated links
|
|
647
|
+
// that share its bytes purely by coincidence; reporting those as links told
|
|
648
|
+
// the operator a file was linked into 36 stores when nothing had been linked
|
|
649
|
+
// at all. An instance that hasn't run the linkGroupId migration yet degrades
|
|
650
|
+
// to 1 rather than failing the whole listing.
|
|
651
|
+
function lsFileColumns(db) {
|
|
652
|
+
const present = hasLinkGroupColumn(db);
|
|
653
|
+
const linkCount = present
|
|
654
|
+
? `(CASE WHEN l.linkGroupId IS NULL THEN 1 ELSE
|
|
655
|
+
(SELECT COUNT(*) FROM doc_mount_file_links g WHERE g.linkGroupId = l.linkGroupId) END)`
|
|
656
|
+
: `1`;
|
|
657
|
+
const linkGroupId = present ? `l.linkGroupId` : `NULL`;
|
|
658
|
+
return `
|
|
659
|
+
${linkGroupId} AS linkGroupId,
|
|
628
660
|
l.id AS linkId, l.fileId, l.relativePath, l.fileName, l.lastModified,
|
|
629
661
|
l.extractionStatus, l.extractedTextSha256, l.chunkCount,
|
|
630
662
|
f.fileType, f.fileSizeBytes, f.source, f.sha256,
|
|
631
|
-
|
|
663
|
+
${linkCount} AS linkCount,
|
|
632
664
|
(SELECT COUNT(*) FROM doc_mount_chunks
|
|
633
665
|
WHERE linkId = l.id AND embedding IS NOT NULL) AS embeddedChunkCount
|
|
634
666
|
`;
|
|
667
|
+
}
|
|
635
668
|
|
|
636
669
|
function resolveLsTarget(db, mountId, normalizedPath) {
|
|
637
670
|
if (!normalizedPath) return { kind: 'root', path: '' };
|
|
638
671
|
|
|
639
672
|
// Exact file match wins — handles the single-file display mode.
|
|
640
673
|
const file = db.prepare(`
|
|
641
|
-
SELECT ${
|
|
674
|
+
SELECT ${lsFileColumns(db)}
|
|
642
675
|
FROM doc_mount_file_links l
|
|
643
676
|
JOIN doc_mount_files f ON f.id = l.fileId
|
|
644
677
|
WHERE l.mountPointId = ? AND l.relativePath = ?
|
|
@@ -692,7 +725,7 @@ function fetchLsRows(db, mountId, parentPath) {
|
|
|
692
725
|
|
|
693
726
|
const files = parentPath === ''
|
|
694
727
|
? db.prepare(`
|
|
695
|
-
SELECT ${
|
|
728
|
+
SELECT ${lsFileColumns(db)}
|
|
696
729
|
FROM doc_mount_file_links l
|
|
697
730
|
JOIN doc_mount_files f ON f.id = l.fileId
|
|
698
731
|
WHERE l.mountPointId = ?
|
|
@@ -700,7 +733,7 @@ function fetchLsRows(db, mountId, parentPath) {
|
|
|
700
733
|
ORDER BY l.fileName COLLATE NOCASE
|
|
701
734
|
`).all(mountId)
|
|
702
735
|
: db.prepare(`
|
|
703
|
-
SELECT ${
|
|
736
|
+
SELECT ${lsFileColumns(db)}
|
|
704
737
|
FROM doc_mount_file_links l
|
|
705
738
|
JOIN doc_mount_files f ON f.id = l.fileId
|
|
706
739
|
WHERE l.mountPointId = ?
|
|
@@ -712,26 +745,29 @@ function fetchLsRows(db, mountId, parentPath) {
|
|
|
712
745
|
return { folders, files };
|
|
713
746
|
}
|
|
714
747
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
748
|
+
// Members of each named hard-link group, keyed by linkGroupId. Grouping is by
|
|
749
|
+
// linkGroupId rather than fileId on purpose — see lsFileColumns: a shared
|
|
750
|
+
// fileId only means "identical bytes", which is not a link.
|
|
751
|
+
function fetchLinkGroupMembers(db, groupIds) {
|
|
752
|
+
if (groupIds.length === 0) return new Map();
|
|
753
|
+
const placeholders = groupIds.map(() => '?').join(',');
|
|
718
754
|
const rows = db.prepare(`
|
|
719
|
-
SELECT l.
|
|
755
|
+
SELECT l.linkGroupId, l.relativePath, l.mountPointId, m.name AS mountName
|
|
720
756
|
FROM doc_mount_file_links l
|
|
721
757
|
JOIN doc_mount_points m ON m.id = l.mountPointId
|
|
722
|
-
WHERE l.
|
|
758
|
+
WHERE l.linkGroupId IN (${placeholders})
|
|
723
759
|
ORDER BY m.name COLLATE NOCASE, l.relativePath COLLATE NOCASE
|
|
724
|
-
`).all(...
|
|
725
|
-
const
|
|
760
|
+
`).all(...groupIds);
|
|
761
|
+
const byGroup = new Map();
|
|
726
762
|
for (const r of rows) {
|
|
727
|
-
if (!
|
|
728
|
-
|
|
763
|
+
if (!byGroup.has(r.linkGroupId)) byGroup.set(r.linkGroupId, []);
|
|
764
|
+
byGroup.get(r.linkGroupId).push({
|
|
729
765
|
mountPointId: r.mountPointId,
|
|
730
766
|
mountName: r.mountName,
|
|
731
767
|
relativePath: r.relativePath,
|
|
732
768
|
});
|
|
733
769
|
}
|
|
734
|
-
return
|
|
770
|
+
return byGroup;
|
|
735
771
|
}
|
|
736
772
|
|
|
737
773
|
function sortLsFiles(files, sortType, reverse) {
|
|
@@ -780,14 +816,14 @@ async function handleLs(flags, mountSpec, rawPath) {
|
|
|
780
816
|
const prefix = normalizedPath ? normalizedPath.replace(/\/+$/, '') + '/' : '';
|
|
781
817
|
const allFiles = normalizedPath
|
|
782
818
|
? db.prepare(`
|
|
783
|
-
SELECT ${
|
|
819
|
+
SELECT ${lsFileColumns(db)}
|
|
784
820
|
FROM doc_mount_file_links l
|
|
785
821
|
JOIN doc_mount_files f ON f.id = l.fileId
|
|
786
822
|
WHERE l.mountPointId = ? AND l.relativePath LIKE ?
|
|
787
823
|
ORDER BY l.relativePath
|
|
788
824
|
`).all(mount.id, prefix + '%')
|
|
789
825
|
: db.prepare(`
|
|
790
|
-
SELECT ${
|
|
826
|
+
SELECT ${lsFileColumns(db)}
|
|
791
827
|
FROM doc_mount_file_links l
|
|
792
828
|
JOIN doc_mount_files f ON f.id = l.fileId
|
|
793
829
|
WHERE l.mountPointId = ?
|
|
@@ -824,10 +860,10 @@ async function handleLs(flags, mountSpec, rawPath) {
|
|
|
824
860
|
|
|
825
861
|
// Fetch links for JSON or --links flag
|
|
826
862
|
const wantLinks = flags.json || flags.links;
|
|
827
|
-
const
|
|
828
|
-
? files.filter((f) => f.linkCount > 1).map((f) => f.
|
|
863
|
+
const linkedGroupIds = wantLinks
|
|
864
|
+
? files.filter((f) => f.linkCount > 1 && f.linkGroupId).map((f) => f.linkGroupId)
|
|
829
865
|
: [];
|
|
830
|
-
const
|
|
866
|
+
const linksByGroup = fetchLinkGroupMembers(db, linkedGroupIds);
|
|
831
867
|
|
|
832
868
|
// JSON output
|
|
833
869
|
if (flags.json) {
|
|
@@ -846,7 +882,7 @@ async function handleLs(flags, mountSpec, rawPath) {
|
|
|
846
882
|
}
|
|
847
883
|
}
|
|
848
884
|
for (const file of files) {
|
|
849
|
-
const others =
|
|
885
|
+
const others = file.linkGroupId ? linksByGroup.get(file.linkGroupId) : undefined;
|
|
850
886
|
const links = others && others.length > 0
|
|
851
887
|
? others
|
|
852
888
|
: [{
|
|
@@ -951,6 +987,7 @@ async function handleLs(flags, mountSpec, rawPath) {
|
|
|
951
987
|
emb: embedColumnMarker(file.chunkCount, file.embeddedChunkCount),
|
|
952
988
|
name: singleFile ? file.relativePath : file.fileName,
|
|
953
989
|
fileId: file.fileId,
|
|
990
|
+
linkGroupId: file.linkGroupId,
|
|
954
991
|
relativePath: file.relativePath,
|
|
955
992
|
});
|
|
956
993
|
}
|
|
@@ -982,7 +1019,7 @@ async function handleLs(flags, mountSpec, rawPath) {
|
|
|
982
1019
|
for (const r of dataRows) {
|
|
983
1020
|
console.log(renderLine(r, false));
|
|
984
1021
|
if (flags.links && r.type === '-') {
|
|
985
|
-
const others = (
|
|
1022
|
+
const others = ((r.linkGroupId && linksByGroup.get(r.linkGroupId)) || []).filter(
|
|
986
1023
|
(l) => !(l.mountPointId === mount.id && l.relativePath === r.relativePath)
|
|
987
1024
|
);
|
|
988
1025
|
if (others.length > 0) {
|