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
126 lines
4.4 KiB
TypeScript
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`
|
|
}
|