mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
history
This commit is contained in:
parent
9df1bb6765
commit
3cdcafdf97
12 changed files with 796 additions and 40 deletions
|
|
@ -31,6 +31,7 @@ import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
|
||||||
import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
|
import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
|
||||||
import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
|
import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
|
||||||
import { search } from '@x/core/dist/search/search.js';
|
import { search } from '@x/core/dist/search/search.js';
|
||||||
|
import { versionHistory } from '@x/core';
|
||||||
|
|
||||||
type InvokeChannels = ipc.InvokeChannels;
|
type InvokeChannels = ipc.InvokeChannels;
|
||||||
type IPCChannels = ipc.IPCChannels;
|
type IPCChannels = ipc.IPCChannels;
|
||||||
|
|
@ -498,6 +499,19 @@ export function setupIpcHandlers() {
|
||||||
const mimeType = mimeMap[ext] || 'application/octet-stream';
|
const mimeType = mimeMap[ext] || 'application/octet-stream';
|
||||||
return { data: buffer.toString('base64'), mimeType, size: stat.size };
|
return { data: buffer.toString('base64'), mimeType, size: stat.size };
|
||||||
},
|
},
|
||||||
|
// Knowledge version history handlers
|
||||||
|
'knowledge:history': async (_event, args) => {
|
||||||
|
const commits = await versionHistory.getFileHistory(args.path);
|
||||||
|
return { commits };
|
||||||
|
},
|
||||||
|
'knowledge:fileAtCommit': async (_event, args) => {
|
||||||
|
const content = await versionHistory.getFileAtCommit(args.path, args.oid);
|
||||||
|
return { content };
|
||||||
|
},
|
||||||
|
'knowledge:restore': async (_event, args) => {
|
||||||
|
await versionHistory.restoreFile(args.path, args.oid);
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
// Search handler
|
// Search handler
|
||||||
'search:query': async (_event, args) => {
|
'search:query': async (_event, args) => {
|
||||||
return search(args.query, args.limit, args.types);
|
return search(args.query, args.limit, args.types);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
|
||||||
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon } from 'lucide-react';
|
import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon, HistoryIcon } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { MarkdownEditor } from './components/markdown-editor';
|
import { MarkdownEditor } from './components/markdown-editor';
|
||||||
import { ChatSidebar } from './components/chat-sidebar';
|
import { ChatSidebar } from './components/chat-sidebar';
|
||||||
|
|
@ -49,6 +49,7 @@ import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-lin
|
||||||
import { OnboardingModal } from '@/components/onboarding-modal'
|
import { OnboardingModal } from '@/components/onboarding-modal'
|
||||||
import { SearchDialog } from '@/components/search-dialog'
|
import { SearchDialog } from '@/components/search-dialog'
|
||||||
import { BackgroundTaskDetail } from '@/components/background-task-detail'
|
import { BackgroundTaskDetail } from '@/components/background-task-detail'
|
||||||
|
import { VersionHistoryPanel } from '@/components/version-history-panel'
|
||||||
import { FileCardProvider } from '@/contexts/file-card-context'
|
import { FileCardProvider } from '@/contexts/file-card-context'
|
||||||
import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override'
|
import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override'
|
||||||
import { TabBar, type ChatTab, type FileTab } from '@/components/tab-bar'
|
import { TabBar, type ChatTab, type FileTab } from '@/components/tab-bar'
|
||||||
|
|
@ -506,6 +507,13 @@ function App() {
|
||||||
const initialContentRef = useRef<string>('')
|
const initialContentRef = useRef<string>('')
|
||||||
const renameInProgressRef = useRef(false)
|
const renameInProgressRef = useRef(false)
|
||||||
|
|
||||||
|
// Version history state
|
||||||
|
const [versionHistoryPath, setVersionHistoryPath] = useState<string | null>(null)
|
||||||
|
const [viewingHistoricalVersion, setViewingHistoricalVersion] = useState<{
|
||||||
|
oid: string
|
||||||
|
content: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// Chat state
|
// Chat state
|
||||||
const [, setMessage] = useState<string>('')
|
const [, setMessage] = useState<string>('')
|
||||||
const [conversation, setConversation] = useState<ConversationItem[]>([])
|
const [conversation, setConversation] = useState<ConversationItem[]>([])
|
||||||
|
|
@ -1072,6 +1080,14 @@ function App() {
|
||||||
saveFile()
|
saveFile()
|
||||||
}, [debouncedContent, setHistory])
|
}, [debouncedContent, setHistory])
|
||||||
|
|
||||||
|
// Close version history panel when switching files
|
||||||
|
useEffect(() => {
|
||||||
|
if (versionHistoryPath && selectedPath !== versionHistoryPath) {
|
||||||
|
setVersionHistoryPath(null)
|
||||||
|
setViewingHistoricalVersion(null)
|
||||||
|
}
|
||||||
|
}, [selectedPath, versionHistoryPath])
|
||||||
|
|
||||||
// Load runs list (all pages)
|
// Load runs list (all pages)
|
||||||
const loadRuns = useCallback(async () => {
|
const loadRuns = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3213,6 +3229,31 @@ function App() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{selectedPath && selectedPath.startsWith('knowledge/') && selectedPath.endsWith('.md') && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (versionHistoryPath) {
|
||||||
|
setVersionHistoryPath(null)
|
||||||
|
setViewingHistoricalVersion(null)
|
||||||
|
} else {
|
||||||
|
setVersionHistoryPath(selectedPath)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors self-center shrink-0",
|
||||||
|
versionHistoryPath && "bg-accent text-foreground"
|
||||||
|
)}
|
||||||
|
aria-label="Version history"
|
||||||
|
>
|
||||||
|
<HistoryIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">Version history</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{!selectedPath && !isGraphOpen && !selectedTask && (
|
{!selectedPath && !isGraphOpen && !selectedTask && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -3276,12 +3317,16 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
) : selectedPath ? (
|
) : selectedPath ? (
|
||||||
selectedPath.endsWith('.md') ? (
|
selectedPath.endsWith('.md') ? (
|
||||||
|
<div className="flex-1 min-h-0 flex flex-row overflow-hidden">
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
{openMarkdownTabs.map((tab) => {
|
{openMarkdownTabs.map((tab) => {
|
||||||
const isActive = activeFileTabId
|
const isActive = activeFileTabId
|
||||||
? tab.id === activeFileTabId || tab.path === selectedPath
|
? tab.id === activeFileTabId || tab.path === selectedPath
|
||||||
: tab.path === selectedPath
|
: tab.path === selectedPath
|
||||||
const tabContent = editorContentByPath[tab.path]
|
const isViewingHistory = viewingHistoricalVersion && isActive && versionHistoryPath === tab.path
|
||||||
|
const tabContent = isViewingHistory
|
||||||
|
? viewingHistoricalVersion.content
|
||||||
|
: editorContentByPath[tab.path]
|
||||||
?? (isActive && editorPathRef.current === tab.path ? editorContent : '')
|
?? (isActive && editorPathRef.current === tab.path ? editorContent : '')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -3307,11 +3352,46 @@ function App() {
|
||||||
fileHistoryHandlersRef.current.delete(tab.id)
|
fileHistoryHandlersRef.current.delete(tab.id)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
editable={!isViewingHistory}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{versionHistoryPath && (
|
||||||
|
<VersionHistoryPanel
|
||||||
|
path={versionHistoryPath}
|
||||||
|
onClose={() => {
|
||||||
|
setVersionHistoryPath(null)
|
||||||
|
setViewingHistoricalVersion(null)
|
||||||
|
}}
|
||||||
|
onSelectVersion={(oid, content) => {
|
||||||
|
if (oid === null) {
|
||||||
|
setViewingHistoricalVersion(null)
|
||||||
|
} else {
|
||||||
|
setViewingHistoricalVersion({ oid, content })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRestore={async (oid) => {
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('knowledge:restore', {
|
||||||
|
path: versionHistoryPath.startsWith('knowledge/')
|
||||||
|
? versionHistoryPath.slice('knowledge/'.length)
|
||||||
|
: versionHistoryPath,
|
||||||
|
oid,
|
||||||
|
})
|
||||||
|
// Reload file content
|
||||||
|
const result = await window.ipc.invoke('workspace:readFile', { path: versionHistoryPath })
|
||||||
|
handleEditorChange(versionHistoryPath, result.data)
|
||||||
|
setViewingHistoricalVersion(null)
|
||||||
|
setVersionHistoryPath(null)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to restore version:', err)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 overflow-auto p-4">
|
<div className="flex-1 overflow-auto p-4">
|
||||||
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap">
|
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap">
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ interface MarkdownEditorProps {
|
||||||
onImageUpload?: (file: File) => Promise<string | null>
|
onImageUpload?: (file: File) => Promise<string | null>
|
||||||
editorSessionKey?: number
|
editorSessionKey?: number
|
||||||
onHistoryHandlersChange?: (handlers: { undo: () => boolean; redo: () => boolean } | null) => void
|
onHistoryHandlersChange?: (handlers: { undo: () => boolean; redo: () => boolean } | null) => void
|
||||||
|
editable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type WikiLinkMatch = {
|
type WikiLinkMatch = {
|
||||||
|
|
@ -282,6 +283,7 @@ export function MarkdownEditor({
|
||||||
onImageUpload,
|
onImageUpload,
|
||||||
editorSessionKey = 0,
|
editorSessionKey = 0,
|
||||||
onHistoryHandlersChange,
|
onHistoryHandlersChange,
|
||||||
|
editable = true,
|
||||||
}: MarkdownEditorProps) {
|
}: MarkdownEditorProps) {
|
||||||
const isInternalUpdate = useRef(false)
|
const isInternalUpdate = useRef(false)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -303,6 +305,7 @@ export function MarkdownEditor({
|
||||||
)
|
)
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
|
editable,
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
heading: {
|
heading: {
|
||||||
|
|
@ -517,6 +520,13 @@ export function MarkdownEditor({
|
||||||
}
|
}
|
||||||
}, [editor, onHistoryHandlersChange])
|
}, [editor, onHistoryHandlersChange])
|
||||||
|
|
||||||
|
// Update editable state when prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.setEditable(editable)
|
||||||
|
}
|
||||||
|
}, [editor, editable])
|
||||||
|
|
||||||
// Force re-render decorations when selection highlight changes
|
// Force re-render decorations when selection highlight changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
|
|
|
||||||
170
apps/x/apps/renderer/src/components/version-history-panel.tsx
Normal file
170
apps/x/apps/renderer/src/components/version-history-panel.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
|
import { X, Lock } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
interface CommitInfo {
|
||||||
|
oid: string
|
||||||
|
message: string
|
||||||
|
timestamp: number
|
||||||
|
author: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionHistoryPanelProps {
|
||||||
|
path: string // knowledge-relative file path (e.g. "knowledge/People/John.md")
|
||||||
|
onClose: () => void
|
||||||
|
onSelectVersion: (oid: string | null, content: string) => void // null = current
|
||||||
|
onRestore: (oid: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(unixSeconds: number): { date: string; time: string } {
|
||||||
|
const d = new Date(unixSeconds * 1000)
|
||||||
|
const date = d.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
|
||||||
|
const time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
|
||||||
|
return { date, time }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VersionHistoryPanel({
|
||||||
|
path,
|
||||||
|
onClose,
|
||||||
|
onSelectVersion,
|
||||||
|
onRestore,
|
||||||
|
}: VersionHistoryPanelProps) {
|
||||||
|
const [commits, setCommits] = useState<CommitInfo[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [selectedOid, setSelectedOid] = useState<string | null>(null) // null = current/latest
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Strip "knowledge/" prefix for IPC calls
|
||||||
|
const relPath = path.startsWith('knowledge/') ? path.slice('knowledge/'.length) : path
|
||||||
|
|
||||||
|
const loadHistory = useCallback(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('knowledge:history', { path: relPath })
|
||||||
|
setCommits(result.commits)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load version history:', err)
|
||||||
|
setError('Failed to load history')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [relPath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadHistory()
|
||||||
|
}, [loadHistory])
|
||||||
|
|
||||||
|
const handleSelectCommit = useCallback(async (oid: string, isLatest: boolean) => {
|
||||||
|
if (isLatest) {
|
||||||
|
setSelectedOid(null)
|
||||||
|
// Read current file content
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('workspace:readFile', { path })
|
||||||
|
onSelectVersion(null, result.data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to read current file:', err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedOid(oid)
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('knowledge:fileAtCommit', { path: relPath, oid })
|
||||||
|
onSelectVersion(oid, result.content)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load file at commit:', err)
|
||||||
|
}
|
||||||
|
}, [path, relPath, onSelectVersion])
|
||||||
|
|
||||||
|
const handleRestore = useCallback(() => {
|
||||||
|
if (selectedOid) {
|
||||||
|
onRestore(selectedOid)
|
||||||
|
}
|
||||||
|
}, [selectedOid, onRestore])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-[280px] shrink-0 border-l border-border bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-3 py-2 border-b border-border shrink-0">
|
||||||
|
<span className="text-sm font-medium text-foreground">Version history</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||||
|
aria-label="Close version history"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Commit list */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : commits.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||||
|
No history available
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-1">
|
||||||
|
{commits.map((commit, index) => {
|
||||||
|
const isLatest = index === 0
|
||||||
|
const isSelected = isLatest ? selectedOid === null : selectedOid === commit.oid
|
||||||
|
const { date, time } = formatTimestamp(commit.timestamp)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={commit.oid}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectCommit(commit.oid, isLatest)}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-left px-3 py-2 transition-colors',
|
||||||
|
isSelected
|
||||||
|
? 'bg-accent'
|
||||||
|
: 'hover:bg-accent/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{!isLatest && (
|
||||||
|
<Lock className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-foreground">
|
||||||
|
{date} · {time}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{commit.author}
|
||||||
|
{isLatest && ' (current)'}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
{selectedOid && (
|
||||||
|
<div className="shrink-0 border-t border-border p-3">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleRestore}
|
||||||
|
>
|
||||||
|
Restore this version
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"google-auth-library": "^10.5.0",
|
"google-auth-library": "^10.5.0",
|
||||||
|
"isomorphic-git": "^1.29.0",
|
||||||
"googleapis": "^169.0.0",
|
"googleapis": "^169.0.0",
|
||||||
"mammoth": "^1.11.0",
|
"mammoth": "^1.11.0",
|
||||||
"node-html-markdown": "^2.0.0",
|
"node-html-markdown": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,8 @@ function ensureWelcomeFile() {
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
ensureDefaultConfigs();
|
ensureDefaultConfigs();
|
||||||
ensureWelcomeFile();
|
ensureWelcomeFile();
|
||||||
|
|
||||||
|
// Initialize version history repo (async, fire-and-forget on startup)
|
||||||
|
import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => {
|
||||||
|
console.error('[VersionHistory] Failed to init repo:', err);
|
||||||
|
});
|
||||||
|
|
@ -6,3 +6,6 @@ export * as watcher from './workspace/watcher.js';
|
||||||
|
|
||||||
// Config initialization
|
// Config initialization
|
||||||
export { initConfigs } from './config/initConfigs.js';
|
export { initConfigs } from './config/initConfigs.js';
|
||||||
|
|
||||||
|
// Knowledge version history
|
||||||
|
export * as versionHistory from './knowledge/version_history.js';
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from './graph_state.js';
|
} from './graph_state.js';
|
||||||
import { buildKnowledgeIndex, formatIndexForPrompt } from './knowledge_index.js';
|
import { buildKnowledgeIndex, formatIndexForPrompt } from './knowledge_index.js';
|
||||||
import { limitEventItems } from './limit_event_items.js';
|
import { limitEventItems } from './limit_event_items.js';
|
||||||
|
import { commitAll } from './version_history.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build obsidian-style knowledge graph by running topic extraction
|
* Build obsidian-style knowledge graph by running topic extraction
|
||||||
|
|
@ -320,6 +321,13 @@ async function buildGraphWithFiles(
|
||||||
// Save state after each successful batch
|
// Save state after each successful batch
|
||||||
// This ensures partial progress is saved even if later batches fail
|
// This ensures partial progress is saved even if later batches fail
|
||||||
saveState(state);
|
saveState(state);
|
||||||
|
|
||||||
|
// Commit knowledge changes to version history
|
||||||
|
try {
|
||||||
|
await commitAll('Knowledge update', 'Rowboat');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[GraphBuilder] Failed to commit version history:`, err);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
console.error(`Error processing batch ${batchNumber}:`, error);
|
console.error(`Error processing batch ${batchNumber}:`, error);
|
||||||
|
|
@ -467,6 +475,13 @@ async function processVoiceMemosForKnowledge(): Promise<boolean> {
|
||||||
|
|
||||||
// Save state after each batch
|
// Save state after each batch
|
||||||
saveState(state);
|
saveState(state);
|
||||||
|
|
||||||
|
// Commit knowledge changes to version history
|
||||||
|
try {
|
||||||
|
await commitAll('Knowledge update', 'Rowboat');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[GraphBuilder] Failed to commit version history:`, err);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
console.error(`[GraphBuilder] Error processing batch ${batchNumber}:`, error);
|
console.error(`[GraphBuilder] Error processing batch ${batchNumber}:`, error);
|
||||||
|
|
|
||||||
205
apps/x/packages/core/src/knowledge/version_history.ts
Normal file
205
apps/x/packages/core/src/knowledge/version_history.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import git from 'isomorphic-git';
|
||||||
|
import { WorkDir } from '../config/config.js';
|
||||||
|
|
||||||
|
const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a git repo in the knowledge directory if one doesn't exist.
|
||||||
|
* Stages all existing .md files and makes an initial commit.
|
||||||
|
*/
|
||||||
|
export async function initRepo(): Promise<void> {
|
||||||
|
const gitDir = path.join(KNOWLEDGE_DIR, '.git');
|
||||||
|
if (fs.existsSync(gitDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure knowledge dir exists
|
||||||
|
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
||||||
|
fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await git.init({ fs, dir: KNOWLEDGE_DIR });
|
||||||
|
|
||||||
|
// Stage all existing .md files
|
||||||
|
const files = getAllMdFiles(KNOWLEDGE_DIR, '');
|
||||||
|
for (const file of files) {
|
||||||
|
await git.add({ fs, dir: KNOWLEDGE_DIR, filepath: file });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
await git.commit({
|
||||||
|
fs,
|
||||||
|
dir: KNOWLEDGE_DIR,
|
||||||
|
message: 'Initial snapshot',
|
||||||
|
author: { name: 'Rowboat', email: 'local' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find all .md files relative to the knowledge dir.
|
||||||
|
*/
|
||||||
|
function getAllMdFiles(baseDir: string, relDir: string): string[] {
|
||||||
|
const results: string[] = [];
|
||||||
|
const absDir = relDir ? path.join(baseDir, relDir) : baseDir;
|
||||||
|
let entries: string[];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(absDir);
|
||||||
|
} catch {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry === '.git' || entry.startsWith('.')) continue;
|
||||||
|
const fullPath = path.join(absDir, entry);
|
||||||
|
const relPath = relDir ? `${relDir}/${entry}` : entry;
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
results.push(...getAllMdFiles(baseDir, relPath));
|
||||||
|
} else if (entry.endsWith('.md')) {
|
||||||
|
results.push(relPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage all changes to .md files and commit. No-op if nothing changed.
|
||||||
|
*/
|
||||||
|
export async function commitAll(message: string, authorName: string): Promise<void> {
|
||||||
|
const matrix = await git.statusMatrix({ fs, dir: KNOWLEDGE_DIR });
|
||||||
|
|
||||||
|
let hasChanges = false;
|
||||||
|
for (const [filepath, head, workdir, stage] of matrix) {
|
||||||
|
// Skip non-md files
|
||||||
|
if (!filepath.endsWith('.md')) continue;
|
||||||
|
|
||||||
|
// [filepath, HEAD, WORKDIR, STAGE]
|
||||||
|
// Unchanged: [f, 1, 1, 1]
|
||||||
|
if (head === 1 && workdir === 1 && stage === 1) continue;
|
||||||
|
|
||||||
|
hasChanges = true;
|
||||||
|
|
||||||
|
if (workdir === 0) {
|
||||||
|
// File deleted from workdir
|
||||||
|
await git.remove({ fs, dir: KNOWLEDGE_DIR, filepath });
|
||||||
|
} else {
|
||||||
|
// File added or modified
|
||||||
|
await git.add({ fs, dir: KNOWLEDGE_DIR, filepath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChanges) return;
|
||||||
|
|
||||||
|
await git.commit({
|
||||||
|
fs,
|
||||||
|
dir: KNOWLEDGE_DIR,
|
||||||
|
message,
|
||||||
|
author: { name: authorName, email: 'local' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommitInfo {
|
||||||
|
oid: string;
|
||||||
|
message: string;
|
||||||
|
timestamp: number;
|
||||||
|
author: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit history for a specific file.
|
||||||
|
* Returns commits where the file content changed, most recent first.
|
||||||
|
*/
|
||||||
|
export async function getFileHistory(knowledgeRelPath: string): Promise<CommitInfo[]> {
|
||||||
|
// Normalize path separators for git (always forward slashes)
|
||||||
|
const filepath = knowledgeRelPath.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
let commits: Awaited<ReturnType<typeof git.log>>;
|
||||||
|
try {
|
||||||
|
commits = await git.log({ fs, dir: KNOWLEDGE_DIR });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commits.length === 0) return [];
|
||||||
|
|
||||||
|
const result: CommitInfo[] = [];
|
||||||
|
|
||||||
|
// Walk through commits and check if file changed between consecutive commits
|
||||||
|
for (let i = 0; i < commits.length; i++) {
|
||||||
|
const commit = commits[i]!;
|
||||||
|
const parentCommit = commits[i + 1]; // undefined for the first (oldest) commit
|
||||||
|
|
||||||
|
const currentOid = await getBlobOidAtCommit(commit.oid, filepath);
|
||||||
|
const parentOid = parentCommit
|
||||||
|
? await getBlobOidAtCommit(parentCommit.oid, filepath)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Include this commit if:
|
||||||
|
// - The file existed and changed from parent
|
||||||
|
// - The file was added (parentOid is null but currentOid exists)
|
||||||
|
// - The file was deleted (currentOid is null but parentOid exists)
|
||||||
|
if (currentOid !== parentOid) {
|
||||||
|
result.push({
|
||||||
|
oid: commit.oid,
|
||||||
|
message: commit.commit.message.trim(),
|
||||||
|
timestamp: commit.commit.author.timestamp,
|
||||||
|
author: commit.commit.author.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the blob OID for a file at a specific commit, or null if not found.
|
||||||
|
*/
|
||||||
|
async function getBlobOidAtCommit(commitOid: string, filepath: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const result = await git.readBlob({
|
||||||
|
fs,
|
||||||
|
dir: KNOWLEDGE_DIR,
|
||||||
|
oid: commitOid,
|
||||||
|
filepath,
|
||||||
|
});
|
||||||
|
// Compute a content hash from the blob to compare
|
||||||
|
return result.oid;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read file content at a specific commit.
|
||||||
|
*/
|
||||||
|
export async function getFileAtCommit(knowledgeRelPath: string, oid: string): Promise<string> {
|
||||||
|
const filepath = knowledgeRelPath.replace(/\\/g, '/');
|
||||||
|
const result = await git.readBlob({
|
||||||
|
fs,
|
||||||
|
dir: KNOWLEDGE_DIR,
|
||||||
|
oid,
|
||||||
|
filepath,
|
||||||
|
});
|
||||||
|
return Buffer.from(result.blob).toString('utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore a file to its content at a given commit, then commit the restoration.
|
||||||
|
*/
|
||||||
|
export async function restoreFile(knowledgeRelPath: string, oid: string): Promise<void> {
|
||||||
|
const content = await getFileAtCommit(knowledgeRelPath, oid);
|
||||||
|
const absPath = path.join(KNOWLEDGE_DIR, knowledgeRelPath);
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const dir = path.dirname(absPath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(absPath, content, 'utf-8');
|
||||||
|
|
||||||
|
const filename = path.basename(knowledgeRelPath);
|
||||||
|
await commitAll(`Restored ${filename}`, 'You');
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import { z } from 'zod';
|
||||||
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
|
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { rewriteWikiLinksForRenamedKnowledgeFile } from './wiki-link-rewrite.js';
|
import { rewriteWikiLinksForRenamedKnowledgeFile } from './wiki-link-rewrite.js';
|
||||||
|
import { commitAll } from '../knowledge/version_history.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Path Utilities
|
// Path Utilities
|
||||||
|
|
@ -218,6 +219,21 @@ export async function readFile(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debounced commit for knowledge file edits
|
||||||
|
let knowledgeCommitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function scheduleKnowledgeCommit(filename: string): void {
|
||||||
|
if (knowledgeCommitTimer) {
|
||||||
|
clearTimeout(knowledgeCommitTimer);
|
||||||
|
}
|
||||||
|
knowledgeCommitTimer = setTimeout(() => {
|
||||||
|
knowledgeCommitTimer = null;
|
||||||
|
commitAll(`Edit ${filename}`, 'You').catch(err => {
|
||||||
|
console.error('[VersionHistory] Failed to commit after edit:', err);
|
||||||
|
});
|
||||||
|
}, 3 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
export async function writeFile(
|
export async function writeFile(
|
||||||
relPath: string,
|
relPath: string,
|
||||||
data: string,
|
data: string,
|
||||||
|
|
@ -266,6 +282,11 @@ export async function writeFile(
|
||||||
const stat = statToSchema(stats, 'file');
|
const stat = statToSchema(stats, 'file');
|
||||||
const etag = computeEtag(stats.size, stats.mtimeMs);
|
const etag = computeEtag(stats.size, stats.mtimeMs);
|
||||||
|
|
||||||
|
// Schedule a debounced version history commit for knowledge files
|
||||||
|
if (relPath.startsWith('knowledge/') && relPath.endsWith('.md')) {
|
||||||
|
scheduleKnowledgeCommit(path.basename(relPath));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: relPath,
|
path: relPath,
|
||||||
stat,
|
stat,
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,26 @@ const ipcSchemas = {
|
||||||
req: z.object({ path: z.string() }),
|
req: z.object({ path: z.string() }),
|
||||||
res: z.object({ data: z.string(), mimeType: z.string(), size: z.number() }),
|
res: z.object({ data: z.string(), mimeType: z.string(), size: z.number() }),
|
||||||
},
|
},
|
||||||
|
// Knowledge version history channels
|
||||||
|
'knowledge:history': {
|
||||||
|
req: z.object({ path: RelPath }),
|
||||||
|
res: z.object({
|
||||||
|
commits: z.array(z.object({
|
||||||
|
oid: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
timestamp: z.number(),
|
||||||
|
author: z.string(),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'knowledge:fileAtCommit': {
|
||||||
|
req: z.object({ path: RelPath, oid: z.string() }),
|
||||||
|
res: z.object({ content: z.string() }),
|
||||||
|
},
|
||||||
|
'knowledge:restore': {
|
||||||
|
req: z.object({ path: RelPath, oid: z.string() }),
|
||||||
|
res: z.object({ ok: z.literal(true) }),
|
||||||
|
},
|
||||||
// Search channels
|
// Search channels
|
||||||
'search:query': {
|
'search:query': {
|
||||||
req: z.object({
|
req: z.object({
|
||||||
|
|
|
||||||
216
apps/x/pnpm-lock.yaml
generated
216
apps/x/pnpm-lock.yaml
generated
|
|
@ -359,6 +359,9 @@ importers:
|
||||||
googleapis:
|
googleapis:
|
||||||
specifier: ^169.0.0
|
specifier: ^169.0.0
|
||||||
version: 169.0.0
|
version: 169.0.0
|
||||||
|
isomorphic-git:
|
||||||
|
specifier: ^1.29.0
|
||||||
|
version: 1.37.2
|
||||||
mammoth:
|
mammoth:
|
||||||
specifier: ^1.11.0
|
specifier: ^1.11.0
|
||||||
version: 1.11.0
|
version: 1.11.0
|
||||||
|
|
@ -3501,6 +3504,10 @@ packages:
|
||||||
abbrev@1.1.1:
|
abbrev@1.1.1:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
|
||||||
abs-svg-path@0.1.1:
|
abs-svg-path@0.1.1:
|
||||||
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
|
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
|
||||||
|
|
||||||
|
|
@ -3627,6 +3634,9 @@ packages:
|
||||||
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
|
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
async-lock@1.4.1:
|
||||||
|
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
|
||||||
|
|
||||||
async@1.5.2:
|
async@1.5.2:
|
||||||
resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==}
|
resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==}
|
||||||
|
|
||||||
|
|
@ -3641,6 +3651,10 @@ packages:
|
||||||
resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==}
|
resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
available-typed-arrays@1.0.7:
|
||||||
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
awilix@12.0.5:
|
awilix@12.0.5:
|
||||||
resolution: {integrity: sha512-Qf/V/hRo6DK0FoBKJ9QiObasRxHAhcNi0mV6kW2JMawxS3zq6Un+VsZmVAZDUfvB+MjTEiJ2tUJUl4cr0JiUAw==}
|
resolution: {integrity: sha512-Qf/V/hRo6DK0FoBKJ9QiObasRxHAhcNi0mV6kW2JMawxS3zq6Un+VsZmVAZDUfvB+MjTEiJ2tUJUl4cr0JiUAw==}
|
||||||
engines: {node: '>=16.3.0'}
|
engines: {node: '>=16.3.0'}
|
||||||
|
|
@ -3742,6 +3756,9 @@ packages:
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -3762,6 +3779,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
call-bind@1.0.8:
|
||||||
|
resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
call-bound@1.0.4:
|
call-bound@1.0.4:
|
||||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -3825,6 +3846,9 @@ packages:
|
||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
clean-git-ref@2.0.1:
|
||||||
|
resolution: {integrity: sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==}
|
||||||
|
|
||||||
clean-stack@2.2.0:
|
clean-stack@2.2.0:
|
||||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -4256,6 +4280,9 @@ packages:
|
||||||
dfa@1.2.0:
|
dfa@1.2.0:
|
||||||
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
||||||
|
|
||||||
|
diff3@0.0.3:
|
||||||
|
resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==}
|
||||||
|
|
||||||
dingbat-to-unicode@1.0.1:
|
dingbat-to-unicode@1.0.1:
|
||||||
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
|
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
|
||||||
|
|
||||||
|
|
@ -4496,6 +4523,10 @@ packages:
|
||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
eventemitter3@5.0.1:
|
eventemitter3@5.0.1:
|
||||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||||
|
|
||||||
|
|
@ -4640,6 +4671,10 @@ packages:
|
||||||
fontkit@2.0.4:
|
fontkit@2.0.4:
|
||||||
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
||||||
|
|
||||||
|
for-each@0.3.5:
|
||||||
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
@ -5104,6 +5139,10 @@ packages:
|
||||||
is-arrayish@0.3.4:
|
is-arrayish@0.3.4:
|
||||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||||
|
|
||||||
|
is-callable@1.2.7:
|
||||||
|
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
is-core-module@2.16.1:
|
is-core-module@2.16.1:
|
||||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -5170,6 +5209,10 @@ packages:
|
||||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-typed-array@1.1.15:
|
||||||
|
resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
is-unicode-supported@0.1.0:
|
is-unicode-supported@0.1.0:
|
||||||
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -5184,6 +5227,9 @@ packages:
|
||||||
isarray@1.0.0:
|
isarray@1.0.0:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
|
isarray@2.0.5:
|
||||||
|
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||||
|
|
||||||
isbinaryfile@4.0.10:
|
isbinaryfile@4.0.10:
|
||||||
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
|
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
|
|
@ -5191,6 +5237,11 @@ packages:
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
isomorphic-git@1.37.2:
|
||||||
|
resolution: {integrity: sha512-HCQBBKmXIMPdHgYGstSBNp6MNmVcMQBbUqJF8xfywFmlpNseO4KKex59YlXqNxhRxmv3fUZwvNWvMyOdc1VvhA==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
|
|
@ -5762,6 +5813,9 @@ packages:
|
||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
minimisted@2.0.1:
|
||||||
|
resolution: {integrity: sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==}
|
||||||
|
|
||||||
minipass-collect@1.0.2:
|
minipass-collect@1.0.2:
|
||||||
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -6169,6 +6223,10 @@ packages:
|
||||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
pify@4.0.1:
|
||||||
|
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
pkce-challenge@5.0.1:
|
pkce-challenge@5.0.1:
|
||||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||||
engines: {node: '>=16.20.0'}
|
engines: {node: '>=16.20.0'}
|
||||||
|
|
@ -6186,6 +6244,10 @@ packages:
|
||||||
points-on-path@0.2.1:
|
points-on-path@0.2.1:
|
||||||
resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
|
resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
|
||||||
|
|
||||||
|
possible-typed-array-names@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
postcss-value-parser@4.2.0:
|
postcss-value-parser@4.2.0:
|
||||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||||
|
|
||||||
|
|
@ -6220,6 +6282,10 @@ packages:
|
||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
|
process@0.11.10:
|
||||||
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
progress@2.0.3:
|
progress@2.0.3:
|
||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
@ -6434,6 +6500,10 @@ packages:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
readable-stream@4.7.0:
|
||||||
|
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
@ -6649,12 +6719,21 @@ packages:
|
||||||
server-destroy@1.0.1:
|
server-destroy@1.0.1:
|
||||||
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
|
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
|
||||||
|
|
||||||
|
set-function-length@1.2.2:
|
||||||
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
setimmediate@1.0.5:
|
setimmediate@1.0.5:
|
||||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
|
||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
|
sha.js@2.4.12:
|
||||||
|
resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
shebang-command@1.2.0:
|
shebang-command@1.2.0:
|
||||||
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -6701,6 +6780,12 @@ packages:
|
||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
simple-concat@1.0.1:
|
||||||
|
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
simple-swizzle@0.2.4:
|
||||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||||
|
|
||||||
|
|
@ -6928,6 +7013,10 @@ packages:
|
||||||
resolution: {integrity: sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==}
|
resolution: {integrity: sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
to-buffer@1.2.2:
|
||||||
|
resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
to-data-view@1.1.0:
|
to-data-view@1.1.0:
|
||||||
resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
|
resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
|
||||||
|
|
||||||
|
|
@ -6998,6 +7087,10 @@ packages:
|
||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
typed-array-buffer@1.0.3:
|
||||||
|
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
typescript-eslint@8.50.1:
|
typescript-eslint@8.50.1:
|
||||||
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
|
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -7272,6 +7365,10 @@ packages:
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
|
which-typed-array@1.1.20:
|
||||||
|
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
which@1.3.1:
|
which@1.3.1:
|
||||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -11316,6 +11413,10 @@ snapshots:
|
||||||
|
|
||||||
abbrev@1.1.1: {}
|
abbrev@1.1.1: {}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
|
||||||
abs-svg-path@0.1.1: {}
|
abs-svg-path@0.1.1: {}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
|
|
@ -11440,6 +11541,8 @@ snapshots:
|
||||||
|
|
||||||
arrify@2.0.1: {}
|
arrify@2.0.1: {}
|
||||||
|
|
||||||
|
async-lock@1.4.1: {}
|
||||||
|
|
||||||
async@1.5.2:
|
async@1.5.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -11449,6 +11552,10 @@ snapshots:
|
||||||
|
|
||||||
author-regex@1.0.0: {}
|
author-regex@1.0.0: {}
|
||||||
|
|
||||||
|
available-typed-arrays@1.0.7:
|
||||||
|
dependencies:
|
||||||
|
possible-typed-array-names: 1.1.0
|
||||||
|
|
||||||
awilix@12.0.5:
|
awilix@12.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
camel-case: 4.1.2
|
camel-case: 4.1.2
|
||||||
|
|
@ -11568,6 +11675,11 @@ snapshots:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
cacache@16.1.3:
|
cacache@16.1.3:
|
||||||
|
|
@ -11610,6 +11722,13 @@ snapshots:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
call-bind@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
set-function-length: 1.2.2
|
||||||
|
|
||||||
call-bound@1.0.4:
|
call-bound@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
|
@ -11672,6 +11791,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
clean-git-ref@2.0.1: {}
|
||||||
|
|
||||||
clean-stack@2.2.0: {}
|
clean-stack@2.2.0: {}
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
|
|
@ -12061,7 +12182,6 @@ snapshots:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
optional: true
|
|
||||||
|
|
||||||
define-properties@1.2.1:
|
define-properties@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -12095,6 +12215,8 @@ snapshots:
|
||||||
|
|
||||||
dfa@1.2.0: {}
|
dfa@1.2.0: {}
|
||||||
|
|
||||||
|
diff3@0.0.3: {}
|
||||||
|
|
||||||
dingbat-to-unicode@1.0.1: {}
|
dingbat-to-unicode@1.0.1: {}
|
||||||
|
|
||||||
dir-compare@4.2.0:
|
dir-compare@4.2.0:
|
||||||
|
|
@ -12451,6 +12573,8 @@ snapshots:
|
||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
eventemitter3@5.0.1: {}
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
@ -12638,6 +12762,10 @@ snapshots:
|
||||||
unicode-properties: 1.4.1
|
unicode-properties: 1.4.1
|
||||||
unicode-trie: 2.0.0
|
unicode-trie: 2.0.0
|
||||||
|
|
||||||
|
for-each@0.3.5:
|
||||||
|
dependencies:
|
||||||
|
is-callable: 1.2.7
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
|
|
@ -12983,7 +13111,6 @@ snapshots:
|
||||||
has-property-descriptors@1.0.2:
|
has-property-descriptors@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
has-symbols@1.1.0: {}
|
has-symbols@1.1.0: {}
|
||||||
|
|
||||||
|
|
@ -13251,6 +13378,8 @@ snapshots:
|
||||||
|
|
||||||
is-arrayish@0.3.4: {}
|
is-arrayish@0.3.4: {}
|
||||||
|
|
||||||
|
is-callable@1.2.7: {}
|
||||||
|
|
||||||
is-core-module@2.16.1:
|
is-core-module@2.16.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
|
|
@ -13300,6 +13429,10 @@ snapshots:
|
||||||
|
|
||||||
is-stream@2.0.1: {}
|
is-stream@2.0.1: {}
|
||||||
|
|
||||||
|
is-typed-array@1.1.15:
|
||||||
|
dependencies:
|
||||||
|
which-typed-array: 1.1.20
|
||||||
|
|
||||||
is-unicode-supported@0.1.0: {}
|
is-unicode-supported@0.1.0: {}
|
||||||
|
|
||||||
is-url@1.2.4: {}
|
is-url@1.2.4: {}
|
||||||
|
|
@ -13310,10 +13443,26 @@ snapshots:
|
||||||
|
|
||||||
isarray@1.0.0: {}
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
|
isarray@2.0.5: {}
|
||||||
|
|
||||||
isbinaryfile@4.0.10: {}
|
isbinaryfile@4.0.10: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
isomorphic-git@1.37.2:
|
||||||
|
dependencies:
|
||||||
|
async-lock: 1.4.1
|
||||||
|
clean-git-ref: 2.0.1
|
||||||
|
crc-32: 1.2.2
|
||||||
|
diff3: 0.0.3
|
||||||
|
ignore: 5.3.2
|
||||||
|
minimisted: 2.0.1
|
||||||
|
pako: 1.0.11
|
||||||
|
pify: 4.0.1
|
||||||
|
readable-stream: 4.7.0
|
||||||
|
sha.js: 2.4.12
|
||||||
|
simple-get: 4.0.1
|
||||||
|
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
|
|
@ -14139,6 +14288,10 @@ snapshots:
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
minimisted@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.8
|
||||||
|
|
||||||
minipass-collect@1.0.2:
|
minipass-collect@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 3.3.6
|
minipass: 3.3.6
|
||||||
|
|
@ -14506,6 +14659,8 @@ snapshots:
|
||||||
|
|
||||||
pify@2.3.0: {}
|
pify@2.3.0: {}
|
||||||
|
|
||||||
|
pify@4.0.1: {}
|
||||||
|
|
||||||
pkce-challenge@5.0.1: {}
|
pkce-challenge@5.0.1: {}
|
||||||
|
|
||||||
pkg-types@1.3.1:
|
pkg-types@1.3.1:
|
||||||
|
|
@ -14527,6 +14682,8 @@ snapshots:
|
||||||
path-data-parser: 0.1.0
|
path-data-parser: 0.1.0
|
||||||
points-on-curve: 0.2.0
|
points-on-curve: 0.2.0
|
||||||
|
|
||||||
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss-value-parser@4.2.0: {}
|
postcss-value-parser@4.2.0: {}
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.6:
|
||||||
|
|
@ -14565,6 +14722,8 @@ snapshots:
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
|
process@0.11.10: {}
|
||||||
|
|
||||||
progress@2.0.3: {}
|
progress@2.0.3: {}
|
||||||
|
|
||||||
promise-inflight@1.0.1: {}
|
promise-inflight@1.0.1: {}
|
||||||
|
|
@ -14887,6 +15046,14 @@ snapshots:
|
||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
|
readable-stream@4.7.0:
|
||||||
|
dependencies:
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
buffer: 6.0.3
|
||||||
|
events: 3.3.0
|
||||||
|
process: 0.11.10
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
rechoir@0.8.0:
|
rechoir@0.8.0:
|
||||||
|
|
@ -15175,10 +15342,25 @@ snapshots:
|
||||||
|
|
||||||
server-destroy@1.0.1: {}
|
server-destroy@1.0.1: {}
|
||||||
|
|
||||||
|
set-function-length@1.2.2:
|
||||||
|
dependencies:
|
||||||
|
define-data-property: 1.1.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-property-descriptors: 1.0.2
|
||||||
|
|
||||||
setimmediate@1.0.5: {}
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
|
sha.js@2.4.12:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
to-buffer: 1.2.2
|
||||||
|
|
||||||
shebang-command@1.2.0:
|
shebang-command@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 1.0.0
|
shebang-regex: 1.0.0
|
||||||
|
|
@ -15236,6 +15418,14 @@ snapshots:
|
||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
simple-concat@1.0.1: {}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
decompress-response: 6.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
simple-swizzle@0.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.3.4
|
is-arrayish: 0.3.4
|
||||||
|
|
@ -15493,6 +15683,12 @@ snapshots:
|
||||||
unorm: 1.6.0
|
unorm: 1.6.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
to-buffer@1.2.2:
|
||||||
|
dependencies:
|
||||||
|
isarray: 2.0.5
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
typed-array-buffer: 1.0.3
|
||||||
|
|
||||||
to-data-view@1.1.0:
|
to-data-view@1.1.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -15550,6 +15746,12 @@ snapshots:
|
||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
|
|
||||||
|
typed-array-buffer@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
is-typed-array: 1.1.15
|
||||||
|
|
||||||
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
|
@ -15827,6 +16029,16 @@ snapshots:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
webidl-conversions: 3.0.1
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
|
which-typed-array@1.1.20:
|
||||||
|
dependencies:
|
||||||
|
available-typed-arrays: 1.0.7
|
||||||
|
call-bind: 1.0.8
|
||||||
|
call-bound: 1.0.4
|
||||||
|
for-each: 0.3.5
|
||||||
|
get-proto: 1.0.1
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
which@1.3.1:
|
which@1.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue