mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
feat: implement validation utilities for user and page entities; add email, username, slug, title, level validation functions
This commit is contained in:
@@ -1,153 +1,10 @@
|
||||
import type { User, PageView } from './types'
|
||||
|
||||
/**
|
||||
* Validation utilities for DBAL operations
|
||||
* Mirrors the validation logic from C++ implementation
|
||||
*/
|
||||
|
||||
// Email validation using regex (RFC-compliant)
|
||||
// TODO: add tests for isValidEmail (valid/invalid formats).
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return emailPattern.test(email)
|
||||
}
|
||||
|
||||
// Username validation: alphanumeric, underscore, hyphen only (1-50 chars)
|
||||
// TODO: add tests for isValidUsername (length, allowed chars).
|
||||
export function isValidUsername(username: string): boolean {
|
||||
if (!username || username.length === 0 || username.length > 50) {
|
||||
return false
|
||||
}
|
||||
const usernamePattern = /^[a-zA-Z0-9_-]+$/
|
||||
return usernamePattern.test(username)
|
||||
}
|
||||
|
||||
// Slug validation: lowercase alphanumeric with hyphens (1-100 chars)
|
||||
// TODO: add tests for isValidSlug (length, lowercase, invalid chars).
|
||||
export function isValidSlug(slug: string): boolean {
|
||||
if (!slug || slug.length === 0 || slug.length > 100) {
|
||||
return false
|
||||
}
|
||||
const slugPattern = /^[a-z0-9-]+$/
|
||||
return slugPattern.test(slug)
|
||||
}
|
||||
|
||||
// Title validation: 1-200 characters
|
||||
// TODO: add tests for isValidTitle (min/max bounds).
|
||||
export function isValidTitle(title: string): boolean {
|
||||
return title.length > 0 && title.length <= 200
|
||||
}
|
||||
|
||||
// Level validation: 0-5 range
|
||||
// TODO: add tests for isValidLevel (min/max bounds).
|
||||
export function isValidLevel(level: number): boolean {
|
||||
return level >= 0 && level <= 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation rules for User entity
|
||||
*/
|
||||
// TODO: add tests for validateUserCreate (required fields, invalid formats, role validation).
|
||||
export function validateUserCreate(data: Partial<User>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.username) {
|
||||
errors.push('Username is required')
|
||||
} else if (!isValidUsername(data.username)) {
|
||||
errors.push('Invalid username format (alphanumeric, underscore, hyphen only, 1-50 chars)')
|
||||
}
|
||||
|
||||
if (!data.email) {
|
||||
errors.push('Email is required')
|
||||
} else if (!isValidEmail(data.email)) {
|
||||
errors.push('Invalid email format')
|
||||
}
|
||||
|
||||
if (!data.role) {
|
||||
errors.push('Role is required')
|
||||
} else if (!['user', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
errors.push('Invalid role')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// TODO: add tests for validateUserUpdate (optional field validation).
|
||||
export function validateUserUpdate(data: Partial<User>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.username !== undefined && !isValidUsername(data.username)) {
|
||||
errors.push('Invalid username format')
|
||||
}
|
||||
|
||||
if (data.email !== undefined && !isValidEmail(data.email)) {
|
||||
errors.push('Invalid email format')
|
||||
}
|
||||
|
||||
if (data.role !== undefined && !['user', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
errors.push('Invalid role')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation rules for PageView entity
|
||||
*/
|
||||
// TODO: add tests for validatePageCreate (required fields, invalid formats, bounds).
|
||||
export function validatePageCreate(data: Partial<PageView>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.slug) {
|
||||
errors.push('Slug is required')
|
||||
} else if (!isValidSlug(data.slug)) {
|
||||
errors.push('Invalid slug format (lowercase alphanumeric with hyphens, 1-100 chars)')
|
||||
}
|
||||
|
||||
if (!data.title) {
|
||||
errors.push('Title is required')
|
||||
} else if (!isValidTitle(data.title)) {
|
||||
errors.push('Invalid title (must be 1-200 characters)')
|
||||
}
|
||||
|
||||
if (data.level === undefined) {
|
||||
errors.push('Level is required')
|
||||
} else if (!isValidLevel(data.level)) {
|
||||
errors.push('Invalid level (must be 0-5)')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// TODO: add tests for validatePageUpdate (optional field validation).
|
||||
export function validatePageUpdate(data: Partial<PageView>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.slug !== undefined && !isValidSlug(data.slug)) {
|
||||
errors.push('Invalid slug format')
|
||||
}
|
||||
|
||||
if (data.title !== undefined && !isValidTitle(data.title)) {
|
||||
errors.push('Invalid title (must be 1-200 characters)')
|
||||
}
|
||||
|
||||
if (data.level !== undefined && !isValidLevel(data.level)) {
|
||||
errors.push('Invalid level (must be 0-5)')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic ID validation
|
||||
*/
|
||||
// TODO: add tests for validateId (empty/whitespace).
|
||||
export function validateId(id: string): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!id || id.trim().length === 0) {
|
||||
errors.push('ID cannot be empty')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
export { isValidEmail } from './validation/is-valid-email'
|
||||
export { isValidUsername } from './validation/is-valid-username'
|
||||
export { isValidSlug } from './validation/is-valid-slug'
|
||||
export { isValidTitle } from './validation/is-valid-title'
|
||||
export { isValidLevel } from './validation/is-valid-level'
|
||||
export { validateUserCreate } from './validation/validate-user-create'
|
||||
export { validateUserUpdate } from './validation/validate-user-update'
|
||||
export { validatePageCreate } from './validation/validate-page-create'
|
||||
export { validatePageUpdate } from './validation/validate-page-update'
|
||||
export { validateId } from './validation/validate-id'
|
||||
|
||||
6
dbal/ts/src/core/validation/is-valid-email.ts
Normal file
6
dbal/ts/src/core/validation/is-valid-email.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Email validation using regex (RFC-compliant)
|
||||
// TODO: add tests for isValidEmail (valid/invalid formats).
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return emailPattern.test(email)
|
||||
}
|
||||
5
dbal/ts/src/core/validation/is-valid-level.ts
Normal file
5
dbal/ts/src/core/validation/is-valid-level.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Level validation: 0-5 range
|
||||
// TODO: add tests for isValidLevel (min/max bounds).
|
||||
export function isValidLevel(level: number): boolean {
|
||||
return level >= 0 && level <= 5
|
||||
}
|
||||
9
dbal/ts/src/core/validation/is-valid-slug.ts
Normal file
9
dbal/ts/src/core/validation/is-valid-slug.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Slug validation: lowercase alphanumeric with hyphens (1-100 chars)
|
||||
// TODO: add tests for isValidSlug (length, lowercase, invalid chars).
|
||||
export function isValidSlug(slug: string): boolean {
|
||||
if (!slug || slug.length === 0 || slug.length > 100) {
|
||||
return false
|
||||
}
|
||||
const slugPattern = /^[a-z0-9-]+$/
|
||||
return slugPattern.test(slug)
|
||||
}
|
||||
5
dbal/ts/src/core/validation/is-valid-title.ts
Normal file
5
dbal/ts/src/core/validation/is-valid-title.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Title validation: 1-200 characters
|
||||
// TODO: add tests for isValidTitle (min/max bounds).
|
||||
export function isValidTitle(title: string): boolean {
|
||||
return title.length > 0 && title.length <= 200
|
||||
}
|
||||
9
dbal/ts/src/core/validation/is-valid-username.ts
Normal file
9
dbal/ts/src/core/validation/is-valid-username.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Username validation: alphanumeric, underscore, hyphen only (1-50 chars)
|
||||
// TODO: add tests for isValidUsername (length, allowed chars).
|
||||
export function isValidUsername(username: string): boolean {
|
||||
if (!username || username.length === 0 || username.length > 50) {
|
||||
return false
|
||||
}
|
||||
const usernamePattern = /^[a-zA-Z0-9_-]+$/
|
||||
return usernamePattern.test(username)
|
||||
}
|
||||
10
dbal/ts/src/core/validation/validate-id.ts
Normal file
10
dbal/ts/src/core/validation/validate-id.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// TODO: add tests for validateId (empty/whitespace).
|
||||
export function validateId(id: string): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!id || id.trim().length === 0) {
|
||||
errors.push('ID cannot be empty')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
32
dbal/ts/src/core/validation/validate-page-create.ts
Normal file
32
dbal/ts/src/core/validation/validate-page-create.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { PageView } from '../types'
|
||||
import { isValidLevel } from './is-valid-level'
|
||||
import { isValidSlug } from './is-valid-slug'
|
||||
import { isValidTitle } from './is-valid-title'
|
||||
|
||||
/**
|
||||
* Validation rules for PageView entity
|
||||
*/
|
||||
// TODO: add tests for validatePageCreate (required fields, invalid formats, bounds).
|
||||
export function validatePageCreate(data: Partial<PageView>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.slug) {
|
||||
errors.push('Slug is required')
|
||||
} else if (!isValidSlug(data.slug)) {
|
||||
errors.push('Invalid slug format (lowercase alphanumeric with hyphens, 1-100 chars)')
|
||||
}
|
||||
|
||||
if (!data.title) {
|
||||
errors.push('Title is required')
|
||||
} else if (!isValidTitle(data.title)) {
|
||||
errors.push('Invalid title (must be 1-200 characters)')
|
||||
}
|
||||
|
||||
if (data.level === undefined) {
|
||||
errors.push('Level is required')
|
||||
} else if (!isValidLevel(data.level)) {
|
||||
errors.push('Invalid level (must be 0-5)')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
23
dbal/ts/src/core/validation/validate-page-update.ts
Normal file
23
dbal/ts/src/core/validation/validate-page-update.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { PageView } from '../types'
|
||||
import { isValidLevel } from './is-valid-level'
|
||||
import { isValidSlug } from './is-valid-slug'
|
||||
import { isValidTitle } from './is-valid-title'
|
||||
|
||||
// TODO: add tests for validatePageUpdate (optional field validation).
|
||||
export function validatePageUpdate(data: Partial<PageView>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.slug !== undefined && !isValidSlug(data.slug)) {
|
||||
errors.push('Invalid slug format')
|
||||
}
|
||||
|
||||
if (data.title !== undefined && !isValidTitle(data.title)) {
|
||||
errors.push('Invalid title (must be 1-200 characters)')
|
||||
}
|
||||
|
||||
if (data.level !== undefined && !isValidLevel(data.level)) {
|
||||
errors.push('Invalid level (must be 0-5)')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
31
dbal/ts/src/core/validation/validate-user-create.ts
Normal file
31
dbal/ts/src/core/validation/validate-user-create.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { User } from '../types'
|
||||
import { isValidEmail } from './is-valid-email'
|
||||
import { isValidUsername } from './is-valid-username'
|
||||
|
||||
/**
|
||||
* Validation rules for User entity
|
||||
*/
|
||||
// TODO: add tests for validateUserCreate (required fields, invalid formats, role validation).
|
||||
export function validateUserCreate(data: Partial<User>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.username) {
|
||||
errors.push('Username is required')
|
||||
} else if (!isValidUsername(data.username)) {
|
||||
errors.push('Invalid username format (alphanumeric, underscore, hyphen only, 1-50 chars)')
|
||||
}
|
||||
|
||||
if (!data.email) {
|
||||
errors.push('Email is required')
|
||||
} else if (!isValidEmail(data.email)) {
|
||||
errors.push('Invalid email format')
|
||||
}
|
||||
|
||||
if (!data.role) {
|
||||
errors.push('Role is required')
|
||||
} else if (!['user', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
errors.push('Invalid role')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
22
dbal/ts/src/core/validation/validate-user-update.ts
Normal file
22
dbal/ts/src/core/validation/validate-user-update.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { User } from '../types'
|
||||
import { isValidEmail } from './is-valid-email'
|
||||
import { isValidUsername } from './is-valid-username'
|
||||
|
||||
// TODO: add tests for validateUserUpdate (optional field validation).
|
||||
export function validateUserUpdate(data: Partial<User>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.username !== undefined && !isValidUsername(data.username)) {
|
||||
errors.push('Invalid username format')
|
||||
}
|
||||
|
||||
if (data.email !== undefined && !isValidEmail(data.email)) {
|
||||
errors.push('Invalid email format')
|
||||
}
|
||||
|
||||
if (data.role !== undefined && !['user', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
errors.push('Invalid role')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
@@ -22,7 +22,7 @@ interface CssCategory {
|
||||
classes: string[]
|
||||
}
|
||||
|
||||
const CLASS_TOKEN_PATTERN = /^[A-Za-z0-9:_/.[\]()%#!,=+-]+$/
|
||||
const CLASS_TOKEN_PATTERN = /^[A-Za-z0-9:_/.\[\]()%#!,=+-]+$/
|
||||
const parseClassList = (value: string) => Array.from(new Set(value.split(/\s+/).filter(Boolean)))
|
||||
|
||||
export function CssClassBuilder({ open, onClose, initialValue = '', onSave }: CssClassBuilderProps) {
|
||||
@@ -80,6 +80,10 @@ export function CssClassBuilder({ open, onClose, initialValue = '', onSave }: Cs
|
||||
return
|
||||
}
|
||||
|
||||
if (activeTab === 'custom') {
|
||||
return
|
||||
}
|
||||
|
||||
const hasActiveTab = filteredCategories.some((category) => category.name === activeTab)
|
||||
if (!hasActiveTab) {
|
||||
setActiveTab(filteredCategories[0]?.name ?? 'custom')
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Database, CssCategory } from '@/lib/database'
|
||||
import { Plus, X, Pencil, Trash, FloppyDisk } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const CLASS_TOKEN_PATTERN = /^[A-Za-z0-9:_/.[\]()%#!,=+-]+$/
|
||||
const CLASS_TOKEN_PATTERN = /^[A-Za-z0-9:_/.\[\]()%#!,=+-]+$/
|
||||
const uniqueClasses = (classes: string[]) => Array.from(new Set(classes))
|
||||
|
||||
export function CssClassManager() {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { LuaEngine, LuaExecutionContext, LuaExecutionResult } from './lua-engine'
|
||||
import { securityScanner, type SecurityScanResult } from '../security'
|
||||
import * as fengari from 'fengari-web'
|
||||
|
||||
const lua = fengari.lua
|
||||
const lauxlib = fengari.lauxlib
|
||||
const lualib = fengari.lualib
|
||||
import { LuaEngine } from './lua-engine'
|
||||
import type { LuaExecutionResult } from './lua-engine'
|
||||
import type { SecurityScanResult } from '../security-scanner'
|
||||
import { executeWithSandbox } from './functions/sandbox/execute-with-sandbox'
|
||||
import { disableDangerousFunctions } from './functions/sandbox/disable-dangerous-functions'
|
||||
import { setupSandboxedEnvironment } from './functions/sandbox/setup-sandboxed-environment'
|
||||
import { executeWithTimeout } from './functions/sandbox/execute-with-timeout'
|
||||
import { setExecutionTimeout } from './functions/sandbox/set-execution-timeout'
|
||||
import { destroy } from './functions/sandbox/destroy'
|
||||
|
||||
export interface SandboxedLuaResult {
|
||||
execution: LuaExecutionResult
|
||||
@@ -12,170 +14,21 @@ export interface SandboxedLuaResult {
|
||||
}
|
||||
|
||||
export class SandboxedLuaEngine {
|
||||
private engine: LuaEngine | null = null
|
||||
private executionTimeout: number = 5000
|
||||
private maxMemory: number = 10 * 1024 * 1024
|
||||
engine: LuaEngine | null = null
|
||||
executionTimeout = 5000
|
||||
// TODO: Enforce maxMemory limit in sandbox execution.
|
||||
maxMemory = 10 * 1024 * 1024
|
||||
|
||||
constructor(timeout: number = 5000) {
|
||||
this.executionTimeout = timeout
|
||||
}
|
||||
|
||||
async executeWithSandbox(code: string, context: LuaExecutionContext = {}): Promise<SandboxedLuaResult> {
|
||||
const securityResult = securityScanner.scanLua(code)
|
||||
|
||||
if (securityResult.severity === 'critical') {
|
||||
return {
|
||||
execution: {
|
||||
success: false,
|
||||
error: 'Code blocked due to critical security issues. Please review the security warnings.',
|
||||
logs: []
|
||||
},
|
||||
security: securityResult
|
||||
}
|
||||
}
|
||||
|
||||
this.engine = new LuaEngine()
|
||||
|
||||
this.disableDangerousFunctions()
|
||||
this.setupSandboxedEnvironment()
|
||||
|
||||
const executionPromise = this.executeWithTimeout(code, context)
|
||||
|
||||
try {
|
||||
const result = await executionPromise
|
||||
|
||||
return {
|
||||
execution: result,
|
||||
security: securityResult
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
execution: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Execution failed',
|
||||
logs: []
|
||||
},
|
||||
security: securityResult
|
||||
}
|
||||
} finally {
|
||||
if (this.engine) {
|
||||
this.engine.destroy()
|
||||
this.engine = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private disableDangerousFunctions() {
|
||||
if (!this.engine) return
|
||||
|
||||
const L = (this.engine as any).L
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('os'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('io'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('loadfile'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setglobal(L, fengari.to_luastring('dofile'))
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('package'))
|
||||
if (!lua.lua_isnil(L, -1)) {
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('loadlib'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('searchpath'))
|
||||
|
||||
lua.lua_pushstring(L, fengari.to_luastring(''))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('cpath'))
|
||||
}
|
||||
lua.lua_pop(L, 1)
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('debug'))
|
||||
if (!lua.lua_isnil(L, -1)) {
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('getfenv'))
|
||||
|
||||
lua.lua_pushnil(L)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('setfenv'))
|
||||
}
|
||||
lua.lua_pop(L, 1)
|
||||
}
|
||||
|
||||
private setupSandboxedEnvironment() {
|
||||
if (!this.engine) return
|
||||
|
||||
const L = (this.engine as any).L
|
||||
|
||||
lua.lua_newtable(L)
|
||||
|
||||
const safeFunctions = [
|
||||
'assert', 'error', 'ipairs', 'next', 'pairs', 'pcall', 'select',
|
||||
'tonumber', 'tostring', 'type', 'unpack', 'xpcall',
|
||||
'string', 'table', 'math', 'bit32'
|
||||
]
|
||||
|
||||
for (const funcName of safeFunctions) {
|
||||
lua.lua_getglobal(L, fengari.to_luastring(funcName))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring(funcName))
|
||||
}
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('print'))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('print'))
|
||||
|
||||
lua.lua_getglobal(L, fengari.to_luastring('log'))
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('log'))
|
||||
|
||||
lua.lua_pushvalue(L, -1)
|
||||
lua.lua_setfield(L, -2, fengari.to_luastring('_G'))
|
||||
|
||||
lua.lua_setglobal(L, fengari.to_luastring('_ENV'))
|
||||
}
|
||||
|
||||
private async executeWithTimeout(code: string, context: LuaExecutionContext): Promise<LuaExecutionResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.engine) {
|
||||
this.engine.destroy()
|
||||
this.engine = null
|
||||
}
|
||||
reject(new Error(`Execution timeout: exceeded ${this.executionTimeout}ms limit`))
|
||||
}, this.executionTimeout)
|
||||
|
||||
if (!this.engine) {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error('Engine not initialized'))
|
||||
return
|
||||
}
|
||||
|
||||
this.engine.execute(code, context)
|
||||
.then(result => {
|
||||
clearTimeout(timeout)
|
||||
resolve(result)
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setExecutionTimeout(timeout: number) {
|
||||
this.executionTimeout = timeout
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.engine) {
|
||||
this.engine.destroy()
|
||||
this.engine = null
|
||||
}
|
||||
}
|
||||
executeWithSandbox = executeWithSandbox
|
||||
disableDangerousFunctions = disableDangerousFunctions
|
||||
setupSandboxedEnvironment = setupSandboxedEnvironment
|
||||
executeWithTimeout = executeWithTimeout
|
||||
setExecutionTimeout = setExecutionTimeout
|
||||
destroy = destroy
|
||||
}
|
||||
|
||||
export function createSandboxedLuaEngine(timeout: number = 5000): SandboxedLuaEngine {
|
||||
return new SandboxedLuaEngine(timeout)
|
||||
}
|
||||
export { createSandboxedLuaEngine } from './create-sandboxed-lua-engine'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LuaEngine } from '../lua-engine'
|
||||
import { LuaEngine } from '../../lua-engine'
|
||||
import type { DeclarativeComponentConfig, LuaScriptDefinition } from './types'
|
||||
|
||||
export interface DeclarativeRendererState {
|
||||
|
||||
@@ -3,4 +3,6 @@ import { WorkflowEngine } from './workflow-engine-class'
|
||||
/**
|
||||
* @deprecated Use WorkflowEngine.execute() directly
|
||||
*/
|
||||
export const createWorkflowEngine = (): WorkflowEngine => new WorkflowEngine()
|
||||
export function createWorkflowEngine(): WorkflowEngine {
|
||||
return new WorkflowEngine()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import type { Workflow } from '../types/level-types'
|
||||
import type { WorkflowExecutionContext } from './workflow-execution-context'
|
||||
import type { WorkflowExecutionResult } from './workflow-execution-result'
|
||||
import type { WorkflowEngine } from './workflow-engine-class'
|
||||
import { executeWorkflow } from './execute-workflow'
|
||||
|
||||
export const executeWorkflowInstance = (
|
||||
/**
|
||||
* Convenience instance method for legacy workflow execution
|
||||
*/
|
||||
export async function executeWorkflowInstance(
|
||||
this: WorkflowEngine,
|
||||
workflow: Workflow,
|
||||
context: WorkflowExecutionContext
|
||||
): Promise<WorkflowExecutionResult> => executeWorkflow(workflow, context)
|
||||
): Promise<WorkflowExecutionResult> {
|
||||
return executeWorkflow(workflow, context)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ export {
|
||||
logToWorkflow,
|
||||
WorkflowEngine,
|
||||
createWorkflowEngine,
|
||||
} from './workflow'
|
||||
} from './index'
|
||||
|
||||
export type {
|
||||
WorkflowExecutionContext,
|
||||
WorkflowExecutionResult,
|
||||
WorkflowState,
|
||||
} from './workflow'
|
||||
} from './index'
|
||||
|
||||
212
tools/autopusher.sh
Executable file
212
tools/autopusher.sh
Executable file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SLEEP_SECONDS="${SLEEP_SECONDS:-15}"
|
||||
REMOTE="${REMOTE:-origin}"
|
||||
NO_VERIFY="${NO_VERIFY:-1}" # set to 0 to enable hooks
|
||||
PUSH_RETRIES="${PUSH_RETRIES:-1}" # per loop; keep low since loop repeats
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
in_git_repo() {
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1
|
||||
}
|
||||
|
||||
current_branch() {
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
}
|
||||
|
||||
has_unpushed_commits() {
|
||||
# Returns 0 if ahead of upstream, else 1
|
||||
local upstream
|
||||
if ! upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)"; then
|
||||
return 1
|
||||
fi
|
||||
[ "$(git rev-list --count "${upstream}..HEAD" 2>/dev/null || echo 0)" -gt 0 ]
|
||||
}
|
||||
|
||||
ensure_upstream() {
|
||||
local branch upstream
|
||||
branch="$(current_branch)"
|
||||
if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
echo "No upstream set for '$branch'; setting to ${REMOTE}/${branch}"
|
||||
git push -u "$REMOTE" "$branch"
|
||||
}
|
||||
|
||||
safe_git_commit() {
|
||||
local msg="$1"
|
||||
if [ "$NO_VERIFY" = "1" ]; then
|
||||
git commit --no-verify -m "$msg"
|
||||
else
|
||||
git commit -m "$msg"
|
||||
fi
|
||||
}
|
||||
|
||||
safe_git_push() {
|
||||
local branch tries=0
|
||||
branch="$(current_branch)"
|
||||
ensure_upstream
|
||||
|
||||
while :; do
|
||||
if git push "$REMOTE" "$branch"; then
|
||||
return 0
|
||||
fi
|
||||
tries=$((tries + 1))
|
||||
if [ "$tries" -ge "$PUSH_RETRIES" ]; then
|
||||
return 1
|
||||
fi
|
||||
sleep "$SLEEP_SECONDS"
|
||||
done
|
||||
}
|
||||
|
||||
sanitize_token() {
|
||||
local s="$1"
|
||||
s="${s//_/ }"
|
||||
s="${s//-/ }"
|
||||
s="${s//./ }"
|
||||
s="$(echo "$s" | tr -s ' ' | sed -e 's/^ *//' -e 's/ *$//')"
|
||||
echo "$s"
|
||||
}
|
||||
|
||||
top_tokens_from_paths() {
|
||||
# Prints up to 3 best-guess tokens (dirs/files/exts) from a newline list of paths.
|
||||
local paths="$1"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
printf "%s\n" "$paths" | awk '
|
||||
function emit(tok) {
|
||||
gsub(/[^A-Za-z0-9]+/, " ", tok)
|
||||
gsub(/^ +| +$/, "", tok)
|
||||
if (length(tok) > 0) print tok
|
||||
}
|
||||
{
|
||||
path=$0
|
||||
n=split(path, parts, "/")
|
||||
if (n >= 2) emit(parts[1])
|
||||
if (n >= 3) emit(parts[2])
|
||||
|
||||
file=parts[n]
|
||||
# extension
|
||||
m=split(file, fp, ".")
|
||||
if (m >= 2) emit(fp[m])
|
||||
# basename (no ext)
|
||||
base=file
|
||||
sub(/\.[^.]+$/, "", base)
|
||||
emit(base)
|
||||
}
|
||||
' | tr '[:upper:]' '[:lower:]' | awk 'length($0) >= 3' >"$tmp"
|
||||
|
||||
# Collapse to frequency, prefer non-generic words.
|
||||
awk '
|
||||
BEGIN {
|
||||
split("test tests spec specs docs doc readme license changelog vendor thirdparty third_party build dist out tmp temp cache node_modules .git", bad, " ")
|
||||
for (i in bad) isbad[bad[i]]=1
|
||||
}
|
||||
{
|
||||
for (i=1; i<=NF; i++) {
|
||||
w=$i
|
||||
if (!isbad[w]) freq[w]++
|
||||
}
|
||||
}
|
||||
END {
|
||||
for (w in freq) print freq[w], w
|
||||
}
|
||||
' "$tmp" | sort -rn | awk 'NR<=3 {print $2}' | paste -sd',' - || true
|
||||
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
detect_verb() {
|
||||
# Simple verb heuristic from changed file types
|
||||
local paths="$1"
|
||||
if echo "$paths" | grep -qiE '\.(md|rst|txt)$'; then
|
||||
echo "docs"
|
||||
return
|
||||
fi
|
||||
if echo "$paths" | grep -qiE '\.(yml|yaml|json|toml|ini|cfg)$'; then
|
||||
echo "config"
|
||||
return
|
||||
fi
|
||||
if echo "$paths" | grep -qiE '\.(sh|ps1|bat|cmd)$'; then
|
||||
echo "scripts"
|
||||
return
|
||||
fi
|
||||
if echo "$paths" | grep -qiE '\.(py|js|ts|tsx|java|kt|c|cc|cpp|h|hpp|rs|go)$'; then
|
||||
echo "code"
|
||||
return
|
||||
fi
|
||||
echo "update"
|
||||
}
|
||||
|
||||
generate_message() {
|
||||
local name_status paths changed_count verb tokens
|
||||
name_status="$(git diff --cached --name-status --diff-filter=ACDMRT || true)"
|
||||
if [ -z "$name_status" ]; then
|
||||
echo "chore: update"
|
||||
return
|
||||
fi
|
||||
|
||||
paths="$(echo "$name_status" | awk '{print $NF}' | sed '/^$/d')"
|
||||
changed_count="$(echo "$paths" | sed '/^$/d' | wc -l | tr -d ' ')"
|
||||
verb="$(detect_verb "$paths")"
|
||||
tokens="$(top_tokens_from_paths "$paths")"
|
||||
|
||||
if [ -n "$tokens" ]; then
|
||||
echo "${verb}: ${tokens} (${changed_count} files)"
|
||||
else
|
||||
echo "${verb}: ${changed_count} files"
|
||||
fi
|
||||
}
|
||||
|
||||
stage_if_needed() {
|
||||
# Stage all changes; returns 0 if anything staged, else 1
|
||||
git add -A
|
||||
git diff --cached --quiet && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
in_git_repo || die "Not inside a git repository."
|
||||
|
||||
while :; do
|
||||
if has_unpushed_commits; then
|
||||
echo "Unpushed commits detected; attempting push..."
|
||||
if ! safe_git_push; then
|
||||
echo "Push failed; retrying after ${SLEEP_SECONDS}s..."
|
||||
sleep "$SLEEP_SECONDS"
|
||||
continue
|
||||
fi
|
||||
echo "Push succeeded."
|
||||
sleep "$SLEEP_SECONDS"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! stage_if_needed; then
|
||||
sleep "$SLEEP_SECONDS"
|
||||
continue
|
||||
fi
|
||||
|
||||
local msg
|
||||
msg="$(generate_message)"
|
||||
echo "Committing: $msg"
|
||||
safe_git_commit "$msg" || {
|
||||
echo "Commit failed; retrying after ${SLEEP_SECONDS}s..."
|
||||
sleep "$SLEEP_SECONDS"
|
||||
continue
|
||||
}
|
||||
|
||||
echo "Pushing..."
|
||||
if ! safe_git_push; then
|
||||
echo "Push failed; will retry after ${SLEEP_SECONDS}s (commit preserved)."
|
||||
sleep "$SLEEP_SECONDS"
|
||||
continue
|
||||
fi
|
||||
|
||||
sleep "$SLEEP_SECONDS"
|
||||
done
|
||||
}
|
||||
|
||||
main_loop
|
||||
|
||||
Reference in New Issue
Block a user