fix: restore complete source code and fix launchers

- Copy complete source code packages from original CodeNomad project
- Add root package.json with npm workspace configuration
- Include electron-app, server, ui, tauri-app, and opencode-config packages
- Fix Launch-Windows.bat and Launch-Dev-Windows.bat to work with correct npm scripts
- Fix Launch-Unix.sh to work with correct npm scripts
- Launchers now correctly call npm run dev:electron which launches Electron app
This commit is contained in:
Gemini AI
2025-12-23 12:57:55 +04:00
Unverified
parent f365d64c97
commit b448d11991
249 changed files with 48776 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import assert from "node:assert/strict"
import { beforeEach, describe, it } from "node:test"
import type { FileSystemEntry } from "../../api-types"
import {
clearWorkspaceSearchCache,
getWorkspaceCandidates,
refreshWorkspaceCandidates,
WORKSPACE_CANDIDATE_CACHE_TTL_MS,
} from "../search-cache"
describe("workspace search cache", () => {
beforeEach(() => {
clearWorkspaceSearchCache()
})
it("expires cached candidates after the TTL", () => {
const workspacePath = "/tmp/workspace"
const startTime = 1_000
refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-a")], startTime)
const beforeExpiry = getWorkspaceCandidates(
workspacePath,
startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS - 1,
)
assert.ok(beforeExpiry)
assert.equal(beforeExpiry.length, 1)
assert.equal(beforeExpiry[0].name, "file-a")
const afterExpiry = getWorkspaceCandidates(
workspacePath,
startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS + 1,
)
assert.equal(afterExpiry, undefined)
})
it("replaces cached entries when manually refreshed", () => {
const workspacePath = "/tmp/workspace"
refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-a")], 5_000)
const initial = getWorkspaceCandidates(workspacePath)
assert.ok(initial)
assert.equal(initial[0].name, "file-a")
refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-b")], 6_000)
const refreshed = getWorkspaceCandidates(workspacePath)
assert.ok(refreshed)
assert.equal(refreshed[0].name, "file-b")
})
})
function createEntry(name: string): FileSystemEntry {
return {
name,
path: name,
absolutePath: `/tmp/${name}`,
type: "file",
size: 1,
modifiedAt: new Date().toISOString(),
}
}

View File

@@ -0,0 +1,295 @@
import fs from "fs"
import os from "os"
import path from "path"
import {
FileSystemEntry,
FileSystemListResponse,
FileSystemListingMetadata,
WINDOWS_DRIVES_ROOT,
} from "../api-types"
interface FileSystemBrowserOptions {
rootDir: string
unrestricted?: boolean
}
interface DirectoryReadOptions {
includeFiles: boolean
formatPath: (entryName: string) => string
formatAbsolutePath: (entryName: string) => string
}
const WINDOWS_DRIVE_LETTERS = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i))
export class FileSystemBrowser {
private readonly root: string
private readonly unrestricted: boolean
private readonly homeDir: string
private readonly isWindows: boolean
constructor(options: FileSystemBrowserOptions) {
this.root = path.resolve(options.rootDir)
this.unrestricted = Boolean(options.unrestricted)
this.homeDir = os.homedir()
this.isWindows = process.platform === "win32"
}
list(relativePath = ".", options: { includeFiles?: boolean } = {}): FileSystemEntry[] {
if (this.unrestricted) {
throw new Error("Relative listing is unavailable when running with unrestricted root")
}
const includeFiles = options.includeFiles ?? true
const normalizedPath = this.normalizeRelativePath(relativePath)
const absolutePath = this.toRestrictedAbsolute(normalizedPath)
return this.readDirectoryEntries(absolutePath, {
includeFiles,
formatPath: (entryName) => this.buildRelativePath(normalizedPath, entryName),
formatAbsolutePath: (entryName) => this.resolveRestrictedAbsoluteChild(normalizedPath, entryName),
})
}
browse(targetPath?: string, options: { includeFiles?: boolean } = {}): FileSystemListResponse {
const includeFiles = options.includeFiles ?? true
if (this.unrestricted) {
return this.listUnrestricted(targetPath, includeFiles)
}
return this.listRestrictedWithMetadata(targetPath, includeFiles)
}
readFile(relativePath: string): string {
if (this.unrestricted) {
throw new Error("readFile is not available in unrestricted mode")
}
const resolved = this.toRestrictedAbsolute(relativePath)
return fs.readFileSync(resolved, "utf-8")
}
private listRestrictedWithMetadata(relativePath: string | undefined, includeFiles: boolean): FileSystemListResponse {
const normalizedPath = this.normalizeRelativePath(relativePath)
const absolutePath = this.toRestrictedAbsolute(normalizedPath)
const entries = this.readDirectoryEntries(absolutePath, {
includeFiles,
formatPath: (entryName) => this.buildRelativePath(normalizedPath, entryName),
formatAbsolutePath: (entryName) => this.resolveRestrictedAbsoluteChild(normalizedPath, entryName),
})
const metadata: FileSystemListingMetadata = {
scope: "restricted",
currentPath: normalizedPath,
parentPath: normalizedPath === "." ? undefined : this.getRestrictedParent(normalizedPath),
rootPath: this.root,
homePath: this.homeDir,
displayPath: this.resolveRestrictedAbsolute(normalizedPath),
pathKind: "relative",
}
return { entries, metadata }
}
private listUnrestricted(targetPath: string | undefined, includeFiles: boolean): FileSystemListResponse {
const resolvedPath = this.resolveUnrestrictedPath(targetPath)
if (this.isWindows && resolvedPath === WINDOWS_DRIVES_ROOT) {
return this.listWindowsDrives()
}
const entries = this.readDirectoryEntries(resolvedPath, {
includeFiles,
formatPath: (entryName) => this.resolveAbsoluteChild(resolvedPath, entryName),
formatAbsolutePath: (entryName) => this.resolveAbsoluteChild(resolvedPath, entryName),
})
const parentPath = this.getUnrestrictedParent(resolvedPath)
const metadata: FileSystemListingMetadata = {
scope: "unrestricted",
currentPath: resolvedPath,
parentPath,
rootPath: this.homeDir,
homePath: this.homeDir,
displayPath: resolvedPath,
pathKind: "absolute",
}
return { entries, metadata }
}
private listWindowsDrives(): FileSystemListResponse {
if (!this.isWindows) {
throw new Error("Drive listing is only supported on Windows hosts")
}
const entries: FileSystemEntry[] = []
for (const letter of WINDOWS_DRIVE_LETTERS) {
const drivePath = `${letter}:\\`
try {
if (fs.existsSync(drivePath)) {
entries.push({
name: `${letter}:`,
path: drivePath,
absolutePath: drivePath,
type: "directory",
})
}
} catch {
// Ignore inaccessible drives
}
}
// Provide a generic UNC root entry so users can navigate to network shares manually.
entries.push({
name: "UNC Network",
path: "\\\\",
absolutePath: "\\\\",
type: "directory",
})
const metadata: FileSystemListingMetadata = {
scope: "unrestricted",
currentPath: WINDOWS_DRIVES_ROOT,
parentPath: undefined,
rootPath: this.homeDir,
homePath: this.homeDir,
displayPath: "Drives",
pathKind: "drives",
}
return { entries, metadata }
}
private readDirectoryEntries(directory: string, options: DirectoryReadOptions): FileSystemEntry[] {
const dirents = fs.readdirSync(directory, { withFileTypes: true })
const results: FileSystemEntry[] = []
for (const entry of dirents) {
if (!options.includeFiles && !entry.isDirectory()) {
continue
}
const absoluteEntryPath = path.join(directory, entry.name)
let stats: fs.Stats
try {
stats = fs.statSync(absoluteEntryPath)
} catch {
// Skip entries we cannot stat (insufficient permissions, etc.)
continue
}
const isDirectory = entry.isDirectory()
if (!options.includeFiles && !isDirectory) {
continue
}
results.push({
name: entry.name,
path: options.formatPath(entry.name),
absolutePath: options.formatAbsolutePath(entry.name),
type: isDirectory ? "directory" : "file",
size: isDirectory ? undefined : stats.size,
modifiedAt: stats.mtime.toISOString(),
})
}
return results.sort((a, b) => a.name.localeCompare(b.name))
}
private normalizeRelativePath(input: string | undefined) {
if (!input || input === "." || input === "./" || input === "/") {
return "."
}
let normalized = input.replace(/\\+/g, "/")
if (normalized.startsWith("./")) {
normalized = normalized.replace(/^\.\/+/, "")
}
if (normalized.startsWith("/")) {
normalized = normalized.replace(/^\/+/g, "")
}
return normalized === "" ? "." : normalized
}
private buildRelativePath(parent: string, child: string) {
if (!parent || parent === ".") {
return this.normalizeRelativePath(child)
}
return this.normalizeRelativePath(`${parent}/${child}`)
}
private resolveRestrictedAbsolute(relativePath: string) {
return this.toRestrictedAbsolute(relativePath)
}
private resolveRestrictedAbsoluteChild(parent: string, child: string) {
const normalized = this.buildRelativePath(parent, child)
return this.toRestrictedAbsolute(normalized)
}
private toRestrictedAbsolute(relativePath: string) {
const normalized = this.normalizeRelativePath(relativePath)
const target = path.resolve(this.root, normalized)
const relativeToRoot = path.relative(this.root, target)
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot) && relativeToRoot !== "") {
throw new Error("Access outside of root is not allowed")
}
return target
}
private resolveUnrestrictedPath(input: string | undefined): string {
if (!input || input === "." || input === "./") {
return this.homeDir
}
if (this.isWindows) {
if (input === WINDOWS_DRIVES_ROOT) {
return WINDOWS_DRIVES_ROOT
}
const normalized = path.win32.normalize(input)
if (/^[a-zA-Z]:/.test(normalized) || normalized.startsWith("\\\\")) {
return normalized
}
return path.win32.resolve(this.homeDir, normalized)
}
if (input.startsWith("/")) {
return path.posix.normalize(input)
}
return path.posix.resolve(this.homeDir, input)
}
private resolveAbsoluteChild(parent: string, child: string) {
if (this.isWindows) {
return path.win32.normalize(path.win32.join(parent, child))
}
return path.posix.normalize(path.posix.join(parent, child))
}
private getRestrictedParent(relativePath: string) {
const normalized = this.normalizeRelativePath(relativePath)
if (normalized === ".") {
return undefined
}
const segments = normalized.split("/")
segments.pop()
return segments.length === 0 ? "." : segments.join("/")
}
private getUnrestrictedParent(currentPath: string) {
if (this.isWindows) {
const normalized = path.win32.normalize(currentPath)
const parsed = path.win32.parse(normalized)
if (normalized === WINDOWS_DRIVES_ROOT) {
return undefined
}
if (normalized === parsed.root) {
return WINDOWS_DRIVES_ROOT
}
return path.win32.dirname(normalized)
}
const normalized = path.posix.normalize(currentPath)
if (normalized === "/") {
return undefined
}
return path.posix.dirname(normalized)
}
}

View File

@@ -0,0 +1,66 @@
import path from "path"
import type { FileSystemEntry } from "../api-types"
export const WORKSPACE_CANDIDATE_CACHE_TTL_MS = 30_000
interface WorkspaceCandidateCacheEntry {
expiresAt: number
candidates: FileSystemEntry[]
}
const workspaceCandidateCache = new Map<string, WorkspaceCandidateCacheEntry>()
export function getWorkspaceCandidates(rootDir: string, now = Date.now()): FileSystemEntry[] | undefined {
const key = normalizeKey(rootDir)
const cached = workspaceCandidateCache.get(key)
if (!cached) {
return undefined
}
if (cached.expiresAt <= now) {
workspaceCandidateCache.delete(key)
return undefined
}
return cloneEntries(cached.candidates)
}
export function refreshWorkspaceCandidates(
rootDir: string,
builder: () => FileSystemEntry[],
now = Date.now(),
): FileSystemEntry[] {
const key = normalizeKey(rootDir)
const freshCandidates = builder()
if (!freshCandidates || freshCandidates.length === 0) {
workspaceCandidateCache.delete(key)
return []
}
const storedCandidates = cloneEntries(freshCandidates)
workspaceCandidateCache.set(key, {
expiresAt: now + WORKSPACE_CANDIDATE_CACHE_TTL_MS,
candidates: storedCandidates,
})
return cloneEntries(storedCandidates)
}
export function clearWorkspaceSearchCache(rootDir?: string) {
if (typeof rootDir === "undefined") {
workspaceCandidateCache.clear()
return
}
const key = normalizeKey(rootDir)
workspaceCandidateCache.delete(key)
}
function cloneEntries(entries: FileSystemEntry[]): FileSystemEntry[] {
return entries.map((entry) => ({ ...entry }))
}
function normalizeKey(rootDir: string) {
return path.resolve(rootDir)
}

View File

@@ -0,0 +1,184 @@
import fs from "fs"
import path from "path"
import fuzzysort from "fuzzysort"
import type { FileSystemEntry } from "../api-types"
import { clearWorkspaceSearchCache, getWorkspaceCandidates, refreshWorkspaceCandidates } from "./search-cache"
const DEFAULT_LIMIT = 100
const MAX_LIMIT = 200
const MAX_CANDIDATES = 8000
const IGNORED_DIRECTORIES = new Set(
[".git", ".hg", ".svn", "node_modules", "dist", "build", ".next", ".nuxt", ".turbo", ".cache", "coverage"].map(
(name) => name.toLowerCase(),
),
)
export type WorkspaceFileSearchType = "all" | "file" | "directory"
export interface WorkspaceFileSearchOptions {
limit?: number
type?: WorkspaceFileSearchType
refresh?: boolean
}
interface CandidateEntry {
entry: FileSystemEntry
key: string
}
export function searchWorkspaceFiles(
rootDir: string,
query: string,
options: WorkspaceFileSearchOptions = {},
): FileSystemEntry[] {
const trimmedQuery = query.trim()
if (!trimmedQuery) {
throw new Error("Search query is required")
}
const normalizedRoot = path.resolve(rootDir)
const limit = normalizeLimit(options.limit)
const typeFilter: WorkspaceFileSearchType = options.type ?? "all"
const refreshRequested = options.refresh === true
let entries: FileSystemEntry[] | undefined
try {
if (!refreshRequested) {
entries = getWorkspaceCandidates(normalizedRoot)
}
if (!entries) {
entries = refreshWorkspaceCandidates(normalizedRoot, () => collectCandidates(normalizedRoot))
}
} catch (error) {
clearWorkspaceSearchCache(normalizedRoot)
throw error
}
if (!entries || entries.length === 0) {
clearWorkspaceSearchCache(normalizedRoot)
return []
}
const candidates = buildCandidateEntries(entries, typeFilter)
if (candidates.length === 0) {
return []
}
const matches = fuzzysort.go<CandidateEntry>(trimmedQuery, candidates, {
key: "key",
limit,
})
if (!matches || matches.length === 0) {
return []
}
return matches.map((match) => match.obj.entry)
}
function collectCandidates(rootDir: string): FileSystemEntry[] {
const queue: string[] = [""]
const entries: FileSystemEntry[] = []
while (queue.length > 0 && entries.length < MAX_CANDIDATES) {
const relativeDir = queue.pop() || ""
const absoluteDir = relativeDir ? path.join(rootDir, relativeDir) : rootDir
let dirents: fs.Dirent[]
try {
dirents = fs.readdirSync(absoluteDir, { withFileTypes: true })
} catch {
continue
}
for (const dirent of dirents) {
const entryName = dirent.name
const lowerName = entryName.toLowerCase()
const relativePath = relativeDir ? `${relativeDir}/${entryName}` : entryName
const absolutePath = path.join(absoluteDir, entryName)
if (dirent.isDirectory() && IGNORED_DIRECTORIES.has(lowerName)) {
continue
}
let stats: fs.Stats
try {
stats = fs.statSync(absolutePath)
} catch {
continue
}
const isDirectory = stats.isDirectory()
if (isDirectory && !IGNORED_DIRECTORIES.has(lowerName)) {
if (entries.length < MAX_CANDIDATES) {
queue.push(relativePath)
}
}
const entryType: FileSystemEntry["type"] = isDirectory ? "directory" : "file"
const normalizedPath = normalizeRelativeEntryPath(relativePath)
const entry: FileSystemEntry = {
name: entryName,
path: normalizedPath,
absolutePath: path.resolve(rootDir, normalizedPath === "." ? "" : normalizedPath),
type: entryType,
size: entryType === "file" ? stats.size : undefined,
modifiedAt: stats.mtime.toISOString(),
}
entries.push(entry)
if (entries.length >= MAX_CANDIDATES) {
break
}
}
}
return entries
}
function buildCandidateEntries(entries: FileSystemEntry[], filter: WorkspaceFileSearchType): CandidateEntry[] {
const filtered: CandidateEntry[] = []
for (const entry of entries) {
if (!shouldInclude(entry.type, filter)) {
continue
}
filtered.push({ entry, key: buildSearchKey(entry) })
}
return filtered
}
function normalizeLimit(limit?: number) {
if (!limit || Number.isNaN(limit)) {
return DEFAULT_LIMIT
}
const clamped = Math.min(Math.max(limit, 1), MAX_LIMIT)
return clamped
}
function shouldInclude(entryType: FileSystemEntry["type"], filter: WorkspaceFileSearchType) {
return filter === "all" || entryType === filter
}
function normalizeRelativeEntryPath(relativePath: string): string {
if (!relativePath) {
return "."
}
let normalized = relativePath.replace(/\\+/g, "/")
if (normalized.startsWith("./")) {
normalized = normalized.replace(/^\.\/+/, "")
}
if (normalized.startsWith("/")) {
normalized = normalized.replace(/^\/+/g, "")
}
return normalized || "."
}
function buildSearchKey(entry: FileSystemEntry) {
return entry.path.toLowerCase()
}