chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment'
|
||||
import {
|
||||
canonicalWorkspaceFilePath,
|
||||
decodeVfsPathSegments,
|
||||
encodeVfsPathSegments,
|
||||
} from '@/lib/copilot/vfs/path-utils'
|
||||
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
|
||||
|
||||
export const WORKFLOW_CHANGELOG_ALIAS_NAME = 'changelog.md'
|
||||
export const WORKFLOW_PLANS_ALIAS_DIR = '.plans'
|
||||
export const WORKFLOW_ALIAS_LINKS_NAME = 'links.json'
|
||||
export const WORKFLOW_CHANGELOG_BACKING_FOLDER = '.changelogs'
|
||||
export const WORKFLOW_PLANS_BACKING_FOLDER = '.plans'
|
||||
export const WORKSPACE_PLANS_BACKING_FOLDER = 'workspace'
|
||||
|
||||
export type WorkflowAliasKind = 'changelog' | 'plan_file' | 'plans_dir'
|
||||
export type WorkflowAliasScope = 'workspace' | 'workflow'
|
||||
|
||||
export interface WorkflowAliasWorkflow {
|
||||
id: string
|
||||
name: string
|
||||
folderPath?: string | null
|
||||
}
|
||||
|
||||
export interface WorkflowAliasWorkflowRow {
|
||||
id: string
|
||||
name: string
|
||||
folderId?: string | null
|
||||
}
|
||||
|
||||
export interface WorkflowAliasFolderRow {
|
||||
folderId: string
|
||||
folderName: string
|
||||
parentId: string | null
|
||||
}
|
||||
|
||||
interface BaseWorkflowAliasTarget {
|
||||
kind: WorkflowAliasKind
|
||||
scope: WorkflowAliasScope
|
||||
aliasPath: string
|
||||
backingPath: string
|
||||
backingFolderPath: string
|
||||
planRelativePath?: string
|
||||
}
|
||||
|
||||
export type WorkflowAliasTarget =
|
||||
| (BaseWorkflowAliasTarget & {
|
||||
kind: 'changelog'
|
||||
scope: 'workflow'
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
workflowPath: string
|
||||
})
|
||||
| (BaseWorkflowAliasTarget & {
|
||||
kind: 'plans_dir'
|
||||
scope: 'workflow'
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
workflowPath: string
|
||||
})
|
||||
| (BaseWorkflowAliasTarget & {
|
||||
kind: 'plan_file'
|
||||
scope: 'workflow'
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
workflowPath: string
|
||||
planRelativePath: string
|
||||
})
|
||||
| (BaseWorkflowAliasTarget & {
|
||||
kind: 'plans_dir'
|
||||
scope: 'workspace'
|
||||
})
|
||||
| (BaseWorkflowAliasTarget & {
|
||||
kind: 'plan_file'
|
||||
scope: 'workspace'
|
||||
planRelativePath: string
|
||||
})
|
||||
|
||||
export interface WorkflowAliasLink {
|
||||
kind: WorkflowAliasKind
|
||||
aliasPath: string
|
||||
backingPath: string
|
||||
backingFileId?: string
|
||||
}
|
||||
|
||||
export function workflowVfsPath(workflow: WorkflowAliasWorkflow): string {
|
||||
const safeName = normalizeVfsSegment(workflow.name)
|
||||
return workflow.folderPath
|
||||
? `workflows/${workflow.folderPath}/${safeName}`
|
||||
: `workflows/${safeName}`
|
||||
}
|
||||
|
||||
export function buildWorkflowAliasWorkflowEntries(
|
||||
workflows: WorkflowAliasWorkflowRow[],
|
||||
folders: WorkflowAliasFolderRow[]
|
||||
): WorkflowAliasWorkflow[] {
|
||||
const folderMap = new Map<string, { name: string; parentId: string | null }>()
|
||||
for (const folder of folders) {
|
||||
folderMap.set(folder.folderId, { name: folder.folderName, parentId: folder.parentId })
|
||||
}
|
||||
|
||||
const folderPathCache = new Map<string, string>()
|
||||
const folderPath = (folderId: string): string => {
|
||||
const cached = folderPathCache.get(folderId)
|
||||
if (cached) return cached
|
||||
|
||||
const folder = folderMap.get(folderId)
|
||||
if (!folder) return ''
|
||||
|
||||
const safeName = normalizeVfsSegment(folder.name)
|
||||
const path = folder.parentId ? `${folderPath(folder.parentId)}/${safeName}` : safeName
|
||||
folderPathCache.set(folderId, path)
|
||||
return path
|
||||
}
|
||||
|
||||
return workflows.map((workflow) => ({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
folderPath: workflow.folderId ? folderPath(workflow.folderId) : null,
|
||||
}))
|
||||
}
|
||||
|
||||
export function workflowChangelogBackingPath(workflowId: string): string {
|
||||
return canonicalWorkspaceFilePath({
|
||||
folderPath: WORKFLOW_CHANGELOG_BACKING_FOLDER,
|
||||
name: `${workflowId}.md`,
|
||||
})
|
||||
}
|
||||
|
||||
export function workflowPlansBackingFolderPath(workflowId: string): string {
|
||||
return `files/${normalizeVfsSegment(WORKFLOW_PLANS_BACKING_FOLDER)}/${normalizeVfsSegment(workflowId)}`
|
||||
}
|
||||
|
||||
export function workspacePlansBackingFolderPath(): string {
|
||||
return `files/${normalizeVfsSegment(WORKFLOW_PLANS_BACKING_FOLDER)}/${normalizeVfsSegment(WORKSPACE_PLANS_BACKING_FOLDER)}`
|
||||
}
|
||||
|
||||
export function workspacePlanBackingPath(planRelativePath: string): string {
|
||||
const segments = decodeVfsPathSegments(planRelativePath)
|
||||
if (segments.length === 0) {
|
||||
throw new Error('Workspace plan alias must include a plan file path')
|
||||
}
|
||||
return canonicalWorkspaceFilePath({
|
||||
folderPath: [
|
||||
WORKFLOW_PLANS_BACKING_FOLDER,
|
||||
WORKSPACE_PLANS_BACKING_FOLDER,
|
||||
...segments.slice(0, -1),
|
||||
].join('/'),
|
||||
name: segments[segments.length - 1],
|
||||
})
|
||||
}
|
||||
|
||||
export function workflowPlanBackingPath(workflowId: string, planRelativePath: string): string {
|
||||
const segments = decodeVfsPathSegments(planRelativePath)
|
||||
if (segments.length === 0) {
|
||||
throw new Error('Workflow plan alias must include a plan file path')
|
||||
}
|
||||
return canonicalWorkspaceFilePath({
|
||||
folderPath: [WORKFLOW_PLANS_BACKING_FOLDER, workflowId, ...segments.slice(0, -1)].join('/'),
|
||||
name: segments[segments.length - 1],
|
||||
})
|
||||
}
|
||||
|
||||
function workflowAliasTargetForPath(workflow: WorkflowAliasWorkflow, rawPath: string) {
|
||||
const workflowPath = workflowVfsPath(workflow)
|
||||
const changelogPath = `${workflowPath}/${WORKFLOW_CHANGELOG_ALIAS_NAME}`
|
||||
if (rawPath === changelogPath) {
|
||||
return {
|
||||
kind: 'changelog' as const,
|
||||
scope: 'workflow' as const,
|
||||
workflowId: workflow.id,
|
||||
workflowName: workflow.name,
|
||||
workflowPath,
|
||||
aliasPath: changelogPath,
|
||||
backingPath: workflowChangelogBackingPath(workflow.id),
|
||||
backingFolderPath: `files/${normalizeVfsSegment(WORKFLOW_CHANGELOG_BACKING_FOLDER)}`,
|
||||
}
|
||||
}
|
||||
|
||||
const plansDirPath = `${workflowPath}/${WORKFLOW_PLANS_ALIAS_DIR}`
|
||||
if (rawPath === plansDirPath || rawPath === `${plansDirPath}/.folder`) {
|
||||
return {
|
||||
kind: 'plans_dir' as const,
|
||||
scope: 'workflow' as const,
|
||||
workflowId: workflow.id,
|
||||
workflowName: workflow.name,
|
||||
workflowPath,
|
||||
aliasPath: plansDirPath,
|
||||
backingPath: workflowPlansBackingFolderPath(workflow.id),
|
||||
backingFolderPath: workflowPlansBackingFolderPath(workflow.id),
|
||||
}
|
||||
}
|
||||
|
||||
const plansPrefix = `${plansDirPath}/`
|
||||
if (rawPath.startsWith(plansPrefix)) {
|
||||
const planRelativePath = rawPath.slice(plansPrefix.length)
|
||||
if (!planRelativePath || planRelativePath === '.folder') return null
|
||||
return {
|
||||
kind: 'plan_file' as const,
|
||||
scope: 'workflow' as const,
|
||||
workflowId: workflow.id,
|
||||
workflowName: workflow.name,
|
||||
workflowPath,
|
||||
aliasPath: rawPath,
|
||||
backingPath: workflowPlanBackingPath(workflow.id, planRelativePath),
|
||||
backingFolderPath: workflowPlansBackingFolderPath(workflow.id),
|
||||
planRelativePath,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveWorkspacePlanAliasPath(path: string): WorkflowAliasTarget | null {
|
||||
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '')
|
||||
if (
|
||||
normalizedPath === WORKFLOW_PLANS_ALIAS_DIR ||
|
||||
normalizedPath === `${WORKFLOW_PLANS_ALIAS_DIR}/.folder`
|
||||
) {
|
||||
return {
|
||||
kind: 'plans_dir',
|
||||
scope: 'workspace',
|
||||
aliasPath: WORKFLOW_PLANS_ALIAS_DIR,
|
||||
backingPath: workspacePlansBackingFolderPath(),
|
||||
backingFolderPath: workspacePlansBackingFolderPath(),
|
||||
}
|
||||
}
|
||||
|
||||
const plansPrefix = `${WORKFLOW_PLANS_ALIAS_DIR}/`
|
||||
if (!normalizedPath.startsWith(plansPrefix)) return null
|
||||
const planRelativePath = normalizedPath.slice(plansPrefix.length)
|
||||
if (
|
||||
!planRelativePath ||
|
||||
planRelativePath === '.folder' ||
|
||||
planRelativePath === WORKFLOW_ALIAS_LINKS_NAME
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
kind: 'plan_file',
|
||||
scope: 'workspace',
|
||||
aliasPath: normalizedPath,
|
||||
backingPath: workspacePlanBackingPath(planRelativePath),
|
||||
backingFolderPath: workspacePlansBackingFolderPath(),
|
||||
planRelativePath,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWorkflowAliasPath(
|
||||
path: string,
|
||||
workflows: WorkflowAliasWorkflow[]
|
||||
): WorkflowAliasTarget | null {
|
||||
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '')
|
||||
if (!normalizedPath.startsWith('workflows/')) return null
|
||||
|
||||
const bySpecificity = [...workflows].sort(
|
||||
(a, b) => workflowVfsPath(b).length - workflowVfsPath(a).length
|
||||
)
|
||||
for (const workflow of bySpecificity) {
|
||||
const target = workflowAliasTargetForPath(workflow, normalizedPath)
|
||||
if (target) return target
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function isWorkflowAliasPath(path: string): boolean {
|
||||
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '')
|
||||
return (
|
||||
normalizedPath.startsWith('workflows/') &&
|
||||
(normalizedPath.endsWith(`/${WORKFLOW_CHANGELOG_ALIAS_NAME}`) ||
|
||||
normalizedPath.includes(`/${WORKFLOW_PLANS_ALIAS_DIR}/`) ||
|
||||
normalizedPath.endsWith(`/${WORKFLOW_PLANS_ALIAS_DIR}`))
|
||||
)
|
||||
}
|
||||
|
||||
export function isWorkspacePlanAliasPath(path: string): boolean {
|
||||
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '')
|
||||
return (
|
||||
normalizedPath === WORKFLOW_PLANS_ALIAS_DIR ||
|
||||
normalizedPath.startsWith(`${WORKFLOW_PLANS_ALIAS_DIR}/`)
|
||||
)
|
||||
}
|
||||
|
||||
export function isPlanAliasPath(path: string): boolean {
|
||||
return isWorkspacePlanAliasPath(path) || isWorkflowAliasPath(path)
|
||||
}
|
||||
|
||||
export function isWorkflowAliasBackingPath(path: string): boolean {
|
||||
const trimmedPath = path.trim().replace(/^\/+|\/+$/g, '')
|
||||
let normalizedPath = trimmedPath
|
||||
if (trimmedPath.startsWith('files/')) {
|
||||
try {
|
||||
normalizedPath = `files/${decodeVfsPathSegments(trimmedPath.slice('files/'.length))
|
||||
.map((segment) => normalizeVfsSegment(segment))
|
||||
.join('/')}`
|
||||
} catch {
|
||||
normalizedPath = trimmedPath
|
||||
}
|
||||
}
|
||||
return (
|
||||
normalizedPath === `files/${normalizeVfsSegment(WORKFLOW_CHANGELOG_BACKING_FOLDER)}` ||
|
||||
normalizedPath === `files/${normalizeVfsSegment(WORKFLOW_PLANS_BACKING_FOLDER)}` ||
|
||||
normalizedPath.startsWith(`files/${normalizeVfsSegment(WORKFLOW_CHANGELOG_BACKING_FOLDER)}/`) ||
|
||||
normalizedPath.startsWith(`files/${normalizeVfsSegment(WORKFLOW_PLANS_BACKING_FOLDER)}/`)
|
||||
)
|
||||
}
|
||||
|
||||
export function isReservedWorkflowAliasBackingDisplayPath(path?: string | null): boolean {
|
||||
if (!path) return false
|
||||
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '')
|
||||
return (
|
||||
normalizedPath === WORKFLOW_CHANGELOG_BACKING_FOLDER ||
|
||||
normalizedPath === WORKFLOW_PLANS_BACKING_FOLDER ||
|
||||
normalizedPath.startsWith(`${WORKFLOW_CHANGELOG_BACKING_FOLDER}/`) ||
|
||||
normalizedPath.startsWith(`${WORKFLOW_PLANS_BACKING_FOLDER}/`)
|
||||
)
|
||||
}
|
||||
|
||||
export function workflowAliasSandboxPath(aliasPath: string): string {
|
||||
return `/home/user/${aliasPath.trim().replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
export function buildWorkflowAliasLinks(args: {
|
||||
workflowPath: string
|
||||
workflowId: string
|
||||
changelog?: WorkspaceFileRecord | null
|
||||
planFiles?: WorkspaceFileRecord[]
|
||||
}): WorkflowAliasLink[] {
|
||||
const links: WorkflowAliasLink[] = [
|
||||
{
|
||||
kind: 'changelog',
|
||||
aliasPath: `${args.workflowPath}/${WORKFLOW_CHANGELOG_ALIAS_NAME}`,
|
||||
backingPath: workflowChangelogBackingPath(args.workflowId),
|
||||
backingFileId: args.changelog?.id,
|
||||
},
|
||||
{
|
||||
kind: 'plans_dir',
|
||||
aliasPath: `${args.workflowPath}/${WORKFLOW_PLANS_ALIAS_DIR}`,
|
||||
backingPath: workflowPlansBackingFolderPath(args.workflowId),
|
||||
},
|
||||
]
|
||||
|
||||
for (const file of args.planFiles ?? []) {
|
||||
const relativePath = file.folderPath
|
||||
?.replace(`${WORKFLOW_PLANS_BACKING_FOLDER}/${args.workflowId}`, '')
|
||||
.replace(/^\/+/, '')
|
||||
const aliasRelativePath = encodeVfsPathSegments(
|
||||
[relativePath, file.name].filter(Boolean).join('/').split('/')
|
||||
)
|
||||
const aliasPath = [args.workflowPath, WORKFLOW_PLANS_ALIAS_DIR, aliasRelativePath].join('/')
|
||||
links.push({
|
||||
kind: 'plan_file',
|
||||
aliasPath,
|
||||
backingPath: canonicalWorkspaceFilePath({ folderPath: file.folderPath, name: file.name }),
|
||||
backingFileId: file.id,
|
||||
})
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
Reference in New Issue
Block a user