Files
simstudioai--sim/apps/sim/lib/copilot/vfs/path-utils.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

126 lines
4.4 KiB
TypeScript

const CONTROL_CHARS = /[\x00-\x1f\x7f]/g
const WHITESPACE = /\s+/g
export class VfsPathError extends Error {
constructor(message: string) {
super(message)
this.name = 'VfsPathError'
}
}
function normalizeDisplaySegment(segment: string): string {
return segment.normalize('NFC').trim().replace(CONTROL_CHARS, '').replace(WHITESPACE, ' ')
}
export function encodeVfsSegment(segment: string): string {
const normalized = normalizeDisplaySegment(segment)
if (!normalized || normalized === '.' || normalized === '..') {
throw new VfsPathError('VFS path segment cannot be empty or a dot segment')
}
return encodeURIComponent(normalized)
}
export function decodeVfsSegment(segment: string): string {
try {
const decoded = decodeURIComponent(segment)
const normalized = normalizeDisplaySegment(decoded)
if (!normalized || normalized === '.' || normalized === '..') {
throw new VfsPathError('VFS path segment cannot be empty or a dot segment')
}
return normalized
} catch (error) {
if (error instanceof VfsPathError) throw error
throw new VfsPathError(`Invalid encoded VFS path segment: ${segment}`)
}
}
export function encodeVfsPathSegments(segments: string[]): string {
return segments.map(encodeVfsSegment).join('/')
}
export function decodeVfsPathSegments(path: string): string[] {
const trimmed = path.trim().replace(/^\/+|\/+$/g, '')
if (!trimmed) return []
return trimmed.split('/').map(decodeVfsSegment)
}
export function canonicalizeVfsPath(path: string): string {
return encodeVfsPathSegments(decodeVfsPathSegments(path))
}
export function canonicalWorkspaceFilePath(parts: {
folderPath?: string | null
name: string
prefix?: 'files' | 'recently-deleted/files'
}): string {
const prefix = parts.prefix ?? 'files'
const folderSegments = parts.folderPath ? parts.folderPath.split('/').filter(Boolean) : []
const encoded = encodeVfsPathSegments([...folderSegments, parts.name])
return `${prefix}/${encoded}`
}
/**
* Build a map from folderId to its canonical, per-segment-encoded VFS folder
* path (e.g. `My%20Folder/Sub`), resolving nested folders via `parentId`.
*
* Shared by the workspace VFS materializer (`workspace-vfs.ts`) and the chat
* context resolver (`process-contents.ts`) so workflow/folder pointer paths
* cannot drift from what the VFS actually serves. Works for any folder
* hierarchy that exposes `{ folderId, folderName, parentId }` rows (workflow
* folders and file folders both qualify).
*/
export function buildVfsFolderPathMap(
folders: Array<{ folderId: string; folderName: string; parentId: string | null }>
): Map<string, string> {
const folderMap = new Map<string, { name: string; parentId: string | null }>()
for (const f of folders) {
folderMap.set(f.folderId, { name: f.folderName, parentId: f.parentId })
}
const cache = new Map<string, string>()
const resolve = (id: string): string => {
const cached = cache.get(id)
if (cached !== undefined) return cached
const folder = folderMap.get(id)
if (!folder) return ''
const parentPath = folder.parentId ? resolve(folder.parentId) : ''
const path = parentPath
? `${parentPath}/${encodeVfsSegment(folder.name)}`
: encodeVfsSegment(folder.name)
cache.set(id, path)
return path
}
for (const id of folderMap.keys()) resolve(id)
return cache
}
/**
* Canonical VFS directory for a workflow. `folderPath` is the already
* per-segment-encoded folder path (from {@link buildVfsFolderPathMap}) or
* null/empty for a root-level workflow. Mirrors the prefix built by
* `workspace-vfs.ts` (`workflows/{folder}/{name}` or `workflows/{name}`).
*/
export function canonicalWorkflowVfsDir(parts: {
name: string
folderPath?: string | null
}): string {
const safeName = encodeVfsSegment(parts.name)
return parts.folderPath ? `workflows/${parts.folderPath}/${safeName}` : `workflows/${safeName}`
}
/** Canonical VFS path for a table's metadata file (`tables/{name}/meta.json`). */
export function canonicalTableVfsPath(name: string): string {
return `tables/${encodeVfsSegment(name)}/meta.json`
}
/** Canonical VFS directory for a knowledge base (`knowledgebases/{name}`). */
export function canonicalKnowledgeBaseVfsDir(name: string): string {
return `knowledgebases/${encodeVfsSegment(name)}`
}
/** Canonical VFS path for a block catalog entry (`components/blocks/{type}.json`). */
export function canonicalBlockVfsPath(blockType: string): string {
return `components/blocks/${blockType}.json`
}