chore(deps): bump dependencies
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
StageViewSnapshotPayload,
|
||||
} from '@proj-airi/stage-shared/godot-stage'
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import type { WebSocketMessage, WebSocketPeer } from 'h3'
|
||||
import type { InferOutput } from 'valibot'
|
||||
|
||||
import type {
|
||||
@@ -54,29 +55,15 @@ import {
|
||||
import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle'
|
||||
import { getElectronMainDirname } from '../../../libs/electron/location'
|
||||
|
||||
type MainContext = ReturnType<typeof createContext>['context']
|
||||
type GodotStageWebSocketHooks = Exclude<Parameters<typeof defineWebSocketHandler>[0], (...args: never[]) => unknown>
|
||||
type GodotStagePeer = Parameters<NonNullable<GodotStageWebSocketHooks['open']>>[0]
|
||||
type GodotStageMessage = Parameters<NonNullable<GodotStageWebSocketHooks['message']>>[1]
|
||||
type GodotStageProcess = ChildProcessByStdio<null, Readable, Readable>
|
||||
type MainContext = ReturnType<typeof createContext>['context']
|
||||
|
||||
const DEFAULT_GODOT_REMOTE_DEBUG_URI = 'tcp://127.0.0.1:6007'
|
||||
|
||||
interface Deferred<T> {
|
||||
promise: Promise<T>
|
||||
reject: (error?: unknown) => void
|
||||
resolve: (value: T | PromiseLike<T>) => void
|
||||
}
|
||||
|
||||
interface ListenerChannel<T> {
|
||||
publish: (payload: T) => void
|
||||
subscribe: (callback: (payload: T) => void) => () => void
|
||||
}
|
||||
|
||||
interface GodotStageSocketRuntime {
|
||||
port: number
|
||||
server: ReturnType<typeof serve>
|
||||
token: string
|
||||
resolve: (value: PromiseLike<T> | T) => void
|
||||
}
|
||||
|
||||
interface GodotStageSceneApplyPayload {
|
||||
@@ -86,12 +73,23 @@ interface GodotStageSceneApplyPayload {
|
||||
path: string
|
||||
}
|
||||
|
||||
interface GodotStageSocketRuntime {
|
||||
port: number
|
||||
server: ReturnType<typeof serve>
|
||||
token: string
|
||||
}
|
||||
|
||||
interface ListenerChannel<T> {
|
||||
publish: (payload: T) => void
|
||||
subscribe: (callback: (payload: T) => void) => () => void
|
||||
}
|
||||
|
||||
const godotStageSceneInputPayloadSchema = object({
|
||||
modelId: string(),
|
||||
format: literal('vrm'),
|
||||
name: string(),
|
||||
fileName: string(),
|
||||
data: instance(Uint8Array),
|
||||
fileName: string(),
|
||||
format: literal('vrm'),
|
||||
modelId: string(),
|
||||
name: string(),
|
||||
})
|
||||
|
||||
const godotStageSocketEnvelopeSchema = object({
|
||||
@@ -103,8 +101,6 @@ const godotStagePayloadMessageSchema = object({
|
||||
message: string(),
|
||||
})
|
||||
|
||||
type GodotStageSocketEnvelope = InferOutput<typeof godotStageSocketEnvelopeSchema>
|
||||
|
||||
/**
|
||||
* Godot sidecar lifecycle controller owned by Electron main.
|
||||
*
|
||||
@@ -124,7 +120,7 @@ export interface GodotStageManager {
|
||||
applySceneInput: (payload: ElectronGodotStageSceneInputPayload) => Promise<void>
|
||||
applyViewPatch: (payload: StageViewPatch) => Promise<StageViewRequestAckPayload>
|
||||
getStatus: () => ElectronGodotStageStatus
|
||||
getViewSnapshot: () => StageViewSnapshotPayload | null
|
||||
getViewSnapshot: () => null | StageViewSnapshotPayload
|
||||
requestViewSnapshot: () => Promise<StageViewRequestAckPayload>
|
||||
start: () => Promise<ElectronGodotStageStatus>
|
||||
stop: () => Promise<ElectronGodotStageStatus>
|
||||
@@ -133,242 +129,12 @@ export interface GodotStageManager {
|
||||
subscribeViewSnapshot: (callback: (snapshot: StageViewSnapshotPayload) => void) => () => void
|
||||
}
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve!: Deferred<T>['resolve']
|
||||
let reject!: Deferred<T>['reject']
|
||||
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
|
||||
return {
|
||||
promise,
|
||||
reject,
|
||||
resolve,
|
||||
}
|
||||
}
|
||||
|
||||
function createListenerChannel<T>(onListenerError: (error: unknown) => void): ListenerChannel<T> {
|
||||
const listeners = new Set<(payload: T) => void>()
|
||||
|
||||
return {
|
||||
publish(payload) {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(payload)
|
||||
}
|
||||
catch (error) {
|
||||
onListenerError(error)
|
||||
}
|
||||
}
|
||||
},
|
||||
subscribe(callback) {
|
||||
listeners.add(callback)
|
||||
|
||||
return () => {
|
||||
listeners.delete(callback)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createInitialStatus(): ElectronGodotStageStatus {
|
||||
return {
|
||||
state: 'stopped',
|
||||
pid: null,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
function createSocketEnvelope(type: string, payload?: unknown) {
|
||||
return JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
function waitForProcessExit(exitPromise: Promise<void>, timeoutMs: number) {
|
||||
return Promise.race([
|
||||
exitPromise.then(() => true, () => false),
|
||||
new Promise<boolean>(resolve => setTimeout(resolve, timeoutMs, false)),
|
||||
])
|
||||
}
|
||||
|
||||
function pipeProcessLog(stream: Readable, write: (message: string) => void) {
|
||||
stream.on('data', (data) => {
|
||||
const message = data.toString('utf-8').trim()
|
||||
if (message) {
|
||||
write(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeFileName(fileName: string) {
|
||||
const normalized = basename(fileName.trim())
|
||||
return normalized || 'model.bin'
|
||||
}
|
||||
|
||||
function parseSocketMessage(message: GodotStageMessage): GodotStageSocketEnvelope {
|
||||
const parsed = safeDestr<unknown>(message.text(), { strict: true })
|
||||
const result = safeParse(godotStageSocketEnvelopeSchema, parsed)
|
||||
if (!result.success)
|
||||
throw new Error('Invalid Godot stage WebSocket envelope.')
|
||||
|
||||
return result.output
|
||||
}
|
||||
|
||||
function getPayloadMessage(payload: unknown) {
|
||||
const result = safeParse(godotStagePayloadMessageSchema, payload)
|
||||
if (!result.success) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result.output.message
|
||||
}
|
||||
|
||||
function parseSceneInputPayload(payload: unknown): ElectronGodotStageSceneInputPayload {
|
||||
const result = safeParse(godotStageSceneInputPayloadSchema, payload)
|
||||
if (!result.success)
|
||||
throw new Error('Invalid Godot stage scene input payload.')
|
||||
|
||||
return result.output
|
||||
}
|
||||
|
||||
function resolveGodotStageStorageRoot() {
|
||||
return join(app.getPath('userData'), 'godot-stage')
|
||||
}
|
||||
|
||||
function resolveGodotStageDebugLaunchOptions() {
|
||||
const remoteDebugEnabled = ['1', 'true', 'yes', 'on'].includes(
|
||||
(process.env.GODOT_STAGE_REMOTE_DEBUG ?? '').trim().toLowerCase(),
|
||||
)
|
||||
const remoteDebugUri = remoteDebugEnabled
|
||||
? process.env.GODOT_STAGE_REMOTE_DEBUG_URI?.trim() || DEFAULT_GODOT_REMOTE_DEBUG_URI
|
||||
: undefined
|
||||
|
||||
// Godot engine/debugger flags must stay before `--`; StageRoot arguments stay
|
||||
// after it and are assembled next to the WebSocket URL.
|
||||
return {
|
||||
engineArgs: remoteDebugUri ? ['--remote-debug', remoteDebugUri] : [],
|
||||
remoteDebugUri,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGodotStageProcessEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
AIRI_GODOT_STAGE_DEV_MODE: app.isPackaged
|
||||
? process.env.AIRI_GODOT_STAGE_DEV_MODE ?? '0'
|
||||
: process.env.AIRI_GODOT_STAGE_DEV_MODE ?? '1',
|
||||
}
|
||||
}
|
||||
|
||||
interface GodotBinaryResolution {
|
||||
executable: string
|
||||
mode: 'engine' | 'exported'
|
||||
}
|
||||
|
||||
// Dev builds run the Godot engine against the workspace project.godot.
|
||||
async function resolveGodotProjectPath() {
|
||||
let currentDirectory = getElectronMainDirname()
|
||||
|
||||
while (true) {
|
||||
const projectPath = resolve(currentDirectory, 'engines', 'stage-tamagotchi-godot')
|
||||
|
||||
try {
|
||||
await access(join(projectPath, 'project.godot'))
|
||||
return projectPath
|
||||
}
|
||||
catch {}
|
||||
|
||||
const parentDirectory = dirname(currentDirectory)
|
||||
if (parentDirectory === currentDirectory) {
|
||||
break
|
||||
}
|
||||
|
||||
currentDirectory = parentDirectory
|
||||
}
|
||||
|
||||
throw new Error(`Unable to locate engines/stage-tamagotchi-godot/project.godot from ${getElectronMainDirname()}.`)
|
||||
}
|
||||
|
||||
// Packaged builds ship a pre-exported sidecar under Electron resources.
|
||||
async function resolveExportedGodotBinary(): Promise<string | undefined> {
|
||||
const platform = process.platform
|
||||
let binaryName: string
|
||||
|
||||
if (platform === 'win32') {
|
||||
binaryName = 'godot-stage.exe'
|
||||
}
|
||||
else if (platform === 'darwin') {
|
||||
binaryName = join('godot-stage.app', 'Contents', 'MacOS', 'godot-stage')
|
||||
}
|
||||
else {
|
||||
binaryName = 'godot-stage'
|
||||
}
|
||||
|
||||
const binaryPath = join(process.resourcesPath, 'godot-stage', binaryName)
|
||||
|
||||
try {
|
||||
await access(binaryPath)
|
||||
return binaryPath
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function validateConfiguredGodotEnginePath(executable: string) {
|
||||
let executableStats
|
||||
try {
|
||||
executableStats = await stat(executable)
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(
|
||||
'GODOT4 points to a missing Godot executable.\n'
|
||||
+ `Configured path: ${executable}\n`
|
||||
+ 'Set GODOT4 to the absolute path of your Godot 4.x .NET/Mono executable before starting dev mode.\n'
|
||||
+ `Original error: ${errorMessageFrom(error) ?? 'unknown error'}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!executableStats.isFile()) {
|
||||
throw new Error(
|
||||
'GODOT4 must point to the Godot executable file, not a directory or app bundle.\n'
|
||||
+ `Configured path: ${executable}\n`
|
||||
+ 'Examples:\n'
|
||||
+ ' Windows: C:\\Path\\To\\Godot_v4.x-stable_mono_win64.exe\n'
|
||||
+ ' macOS: /Applications/Godot_mono.app/Contents/MacOS/Godot\n'
|
||||
+ ' Linux: /path/to/Godot_v4.x-stable_mono_linux.x86_64',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveGodotBinary(): Promise<GodotBinaryResolution> {
|
||||
if (app.isPackaged) {
|
||||
const exported = await resolveExportedGodotBinary()
|
||||
if (exported) {
|
||||
return { executable: exported, mode: 'exported' }
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Godot stage exported binary not found. '
|
||||
+ `Expected at: ${join(process.resourcesPath, 'godot-stage')}`,
|
||||
)
|
||||
}
|
||||
|
||||
const envPath = process.env.GODOT4?.trim()
|
||||
if (!envPath) {
|
||||
throw new Error(
|
||||
'GODOT4 is required to start Godot Stage in development mode.\n'
|
||||
+ 'Set GODOT4 to the absolute path of your Godot 4.x .NET/Mono executable, then restart the Electron dev app.\n'
|
||||
+ 'Examples:\n'
|
||||
+ ' PowerShell: $env:GODOT4 = "C:\\Path\\To\\Godot_v4.x-stable_mono_win64.exe"\n'
|
||||
+ ' Bash: export GODOT4="/path/to/godot"',
|
||||
)
|
||||
}
|
||||
|
||||
await validateConfiguredGodotEnginePath(envPath)
|
||||
return { executable: envPath, mode: 'engine' }
|
||||
}
|
||||
type GodotStageSocketEnvelope = InferOutput<typeof godotStageSocketEnvelopeSchema>
|
||||
|
||||
/**
|
||||
* Creates the shared Godot stage manager.
|
||||
@@ -397,8 +163,8 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
let currentProcessExit = createDeferred<void>()
|
||||
let currentReady: Deferred<void> | undefined
|
||||
let currentSocketRuntime: GodotStageSocketRuntime | undefined
|
||||
let currentSocketPeer: GodotStagePeer | undefined
|
||||
let currentViewSnapshot: StageViewSnapshotPayload | null = null
|
||||
let currentSocketPeer: undefined | WebSocketPeer
|
||||
let currentViewSnapshot: null | StageViewSnapshotPayload = null
|
||||
let expectedProcessExit = false
|
||||
|
||||
function setStatus(next: Partial<ElectronGodotStageStatus> & Pick<ElectronGodotStageStatus, 'state'>) {
|
||||
@@ -492,35 +258,12 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
|
||||
function handleSocketMessage(message: GodotStageSocketEnvelope) {
|
||||
switch (message.type) {
|
||||
case 'stage.ready': {
|
||||
setStatus({
|
||||
state: 'running',
|
||||
pid: currentProcess?.pid ?? null,
|
||||
lastError: undefined,
|
||||
})
|
||||
currentReady?.resolve()
|
||||
currentReady = undefined
|
||||
|
||||
return
|
||||
}
|
||||
case 'stage.fatal': {
|
||||
const error = getPayloadMessage(message.payload) ?? 'Godot stage reported a fatal startup error.'
|
||||
setStatus({
|
||||
state: 'error',
|
||||
pid: currentProcess?.pid ?? null,
|
||||
lastError: error,
|
||||
})
|
||||
currentReady?.reject(new Error(error))
|
||||
currentReady = undefined
|
||||
currentProcess?.kill()
|
||||
return
|
||||
}
|
||||
case 'scene.applied': {
|
||||
if (currentStatus.state === 'running' && currentStatus.lastError) {
|
||||
setStatus({
|
||||
state: 'running',
|
||||
pid: currentProcess?.pid ?? null,
|
||||
lastError: undefined,
|
||||
pid: currentProcess?.pid ?? null,
|
||||
state: 'running',
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -528,24 +271,47 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
case 'scene.error': {
|
||||
const error = getPayloadMessage(message.payload) ?? 'Godot stage failed to apply scene input.'
|
||||
setStatus({
|
||||
state: currentStatus.state,
|
||||
pid: currentProcess?.pid ?? null,
|
||||
lastError: error,
|
||||
pid: currentProcess?.pid ?? null,
|
||||
state: currentStatus.state,
|
||||
})
|
||||
return
|
||||
}
|
||||
case 'stage.view.snapshot': {
|
||||
case 'stage.fatal': {
|
||||
const error = getPayloadMessage(message.payload) ?? 'Godot stage reported a fatal startup error.'
|
||||
setStatus({
|
||||
lastError: error,
|
||||
pid: currentProcess?.pid ?? null,
|
||||
state: 'error',
|
||||
})
|
||||
currentReady?.reject(new Error(error))
|
||||
currentReady = undefined
|
||||
currentProcess?.kill()
|
||||
return
|
||||
}
|
||||
case 'stage.ready': {
|
||||
setStatus({
|
||||
lastError: undefined,
|
||||
pid: currentProcess?.pid ?? null,
|
||||
state: 'running',
|
||||
})
|
||||
currentReady?.resolve()
|
||||
currentReady = undefined
|
||||
|
||||
return
|
||||
}
|
||||
case 'stage.view.error': {
|
||||
try {
|
||||
broadcastViewSnapshot(parseStageViewSnapshotPayload(message.payload))
|
||||
broadcastViewError(parseStageViewErrorPayload(message.payload))
|
||||
}
|
||||
catch (error) {
|
||||
broadcastInvalidViewPayloadError(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'stage.view.error': {
|
||||
case 'stage.view.snapshot': {
|
||||
try {
|
||||
broadcastViewError(parseStageViewErrorPayload(message.payload))
|
||||
broadcastViewSnapshot(parseStageViewSnapshotPayload(message.payload))
|
||||
}
|
||||
catch (error) {
|
||||
broadcastInvalidViewPayloadError(error)
|
||||
@@ -569,6 +335,19 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
const appServer = new H3()
|
||||
|
||||
appServer.get('/ws', defineWebSocketHandler({
|
||||
close: (peer) => {
|
||||
if (currentSocketPeer?.id === peer.id) {
|
||||
currentSocketPeer = undefined
|
||||
}
|
||||
},
|
||||
message: (_peer, message) => {
|
||||
try {
|
||||
handleSocketMessage(parseSocketMessage(message))
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).warn('failed to parse Godot websocket message')
|
||||
}
|
||||
},
|
||||
open: (peer) => {
|
||||
const requestUrl = peer.request.url ?? ''
|
||||
const url = new URL(requestUrl, `ws://${host}:${port}`)
|
||||
@@ -580,33 +359,20 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
currentSocketPeer = peer
|
||||
log.withFields({ peer: peer.id }).debug('Godot websocket connected')
|
||||
},
|
||||
message: (_peer, message) => {
|
||||
try {
|
||||
handleSocketMessage(parseSocketMessage(message))
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).warn('failed to parse Godot websocket message')
|
||||
}
|
||||
},
|
||||
close: (peer) => {
|
||||
if (currentSocketPeer?.id === peer.id) {
|
||||
currentSocketPeer = undefined
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const server = serve(appServer, {
|
||||
// @ts-expect-error - h3 does not extend the crossws response type.
|
||||
plugins: [ws({ resolve: async req => (await appServer.fetch(req)).crossws })],
|
||||
port,
|
||||
hostname: host,
|
||||
manual: true,
|
||||
reusePort: false,
|
||||
silent: true,
|
||||
gracefulShutdown: {
|
||||
forceTimeout: 0.25,
|
||||
gracefulTimeout: 0.25,
|
||||
},
|
||||
hostname: host,
|
||||
manual: true,
|
||||
// @ts-expect-error - h3 does not extend the crossws response type.
|
||||
plugins: [ws({ resolve: async req => (await appServer.fetch(req)).crossws })],
|
||||
port,
|
||||
reusePort: false,
|
||||
silent: true,
|
||||
})
|
||||
|
||||
await server.serve()
|
||||
@@ -632,9 +398,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
|
||||
const message = errorMessageFrom(error) ?? 'Failed to spawn Godot stage process.'
|
||||
setStatus({
|
||||
state: 'error',
|
||||
pid: processHandle.pid ?? null,
|
||||
lastError: message,
|
||||
pid: processHandle.pid ?? null,
|
||||
state: 'error',
|
||||
})
|
||||
currentReady?.reject(error)
|
||||
currentReady = undefined
|
||||
@@ -659,16 +425,16 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
|
||||
if (expectedProcessExit) {
|
||||
setStatus({
|
||||
state: 'stopped',
|
||||
pid: null,
|
||||
lastError: undefined,
|
||||
pid: null,
|
||||
state: 'stopped',
|
||||
})
|
||||
}
|
||||
else {
|
||||
setStatus({
|
||||
state: 'error',
|
||||
pid: null,
|
||||
lastError: exitMessage,
|
||||
pid: null,
|
||||
state: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -679,10 +445,34 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe(callback) {
|
||||
const unsubscribe = statusListeners.subscribe(callback)
|
||||
callback(currentStatus)
|
||||
return unsubscribe
|
||||
async applySceneInput(payload) {
|
||||
await lifecycleMutex.runExclusive(async () => {
|
||||
if (currentStatus.state !== 'running') {
|
||||
throw new Error('Godot stage is not running.')
|
||||
}
|
||||
|
||||
const sceneInputPayload = parseSceneInputPayload(payload)
|
||||
|
||||
const fileName = normalizeFileName(sceneInputPayload.fileName)
|
||||
const modelDirectory = join(resolveGodotStageStorageRoot(), 'models', sceneInputPayload.modelId)
|
||||
const materializedPath = join(modelDirectory, fileName)
|
||||
|
||||
await mkdir(modelDirectory, { recursive: true })
|
||||
await writeFile(materializedPath, sceneInputPayload.data)
|
||||
|
||||
sendSceneInputToGodot({
|
||||
format: sceneInputPayload.format,
|
||||
modelId: sceneInputPayload.modelId,
|
||||
name: sceneInputPayload.name,
|
||||
path: materializedPath,
|
||||
})
|
||||
})
|
||||
},
|
||||
async applyViewPatch(payload) {
|
||||
return await lifecycleMutex.runExclusive(async () => {
|
||||
const patch = parseStageViewPatchPayload(payload)
|
||||
return sendViewRequest('host.view.patch', { patch })
|
||||
})
|
||||
},
|
||||
getStatus() {
|
||||
return currentStatus
|
||||
@@ -690,11 +480,10 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
getViewSnapshot() {
|
||||
return currentViewSnapshot
|
||||
},
|
||||
subscribeViewSnapshot(callback) {
|
||||
return viewSnapshotListeners.subscribe(callback)
|
||||
},
|
||||
subscribeViewError(callback) {
|
||||
return viewErrorListeners.subscribe(callback)
|
||||
async requestViewSnapshot() {
|
||||
return await lifecycleMutex.runExclusive(async () => {
|
||||
return sendViewRequest('host.view.request_snapshot')
|
||||
})
|
||||
},
|
||||
async start() {
|
||||
return await lifecycleMutex.runExclusive(async () => {
|
||||
@@ -732,9 +521,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
currentReady = readyDeferred
|
||||
expectedProcessExit = false
|
||||
setStatus({
|
||||
state: 'starting',
|
||||
pid: null,
|
||||
lastError: undefined,
|
||||
pid: null,
|
||||
state: 'starting',
|
||||
})
|
||||
|
||||
let spawnArgs: string[]
|
||||
@@ -778,9 +567,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
attachProcessListeners(processHandle)
|
||||
|
||||
setStatus({
|
||||
state: 'starting',
|
||||
pid: processHandle.pid ?? null,
|
||||
lastError: undefined,
|
||||
pid: processHandle.pid ?? null,
|
||||
state: 'starting',
|
||||
})
|
||||
|
||||
try {
|
||||
@@ -800,9 +589,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
}
|
||||
await stopSocketRuntime()
|
||||
setStatus({
|
||||
state: 'error',
|
||||
pid: null,
|
||||
lastError: errorMessageFrom(error) ?? 'Failed to start Godot stage.',
|
||||
pid: null,
|
||||
state: 'error',
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -813,9 +602,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
if (!currentProcess) {
|
||||
await stopSocketRuntime()
|
||||
setStatus({
|
||||
state: 'stopped',
|
||||
pid: null,
|
||||
lastError: undefined,
|
||||
pid: null,
|
||||
state: 'stopped',
|
||||
})
|
||||
return currentStatus
|
||||
}
|
||||
@@ -825,9 +614,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
|
||||
expectedProcessExit = true
|
||||
setStatus({
|
||||
state: 'stopping',
|
||||
pid: activeProcess.pid ?? null,
|
||||
lastError: undefined,
|
||||
pid: activeProcess.pid ?? null,
|
||||
state: 'stopping',
|
||||
})
|
||||
|
||||
try {
|
||||
@@ -842,9 +631,9 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
}
|
||||
catch (error) {
|
||||
setStatus({
|
||||
state: 'error',
|
||||
pid: activeProcess.pid ?? null,
|
||||
lastError: errorMessageFrom(error) ?? 'Failed to stop Godot stage.',
|
||||
pid: activeProcess.pid ?? null,
|
||||
state: 'error',
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -853,73 +642,28 @@ export function createGodotStageManager(): GodotStageManager {
|
||||
}
|
||||
|
||||
setStatus({
|
||||
state: 'stopped',
|
||||
pid: null,
|
||||
lastError: undefined,
|
||||
pid: null,
|
||||
state: 'stopped',
|
||||
})
|
||||
|
||||
return currentStatus
|
||||
})
|
||||
},
|
||||
async applySceneInput(payload) {
|
||||
await lifecycleMutex.runExclusive(async () => {
|
||||
if (currentStatus.state !== 'running') {
|
||||
throw new Error('Godot stage is not running.')
|
||||
}
|
||||
|
||||
const sceneInputPayload = parseSceneInputPayload(payload)
|
||||
|
||||
const fileName = normalizeFileName(sceneInputPayload.fileName)
|
||||
const modelDirectory = join(resolveGodotStageStorageRoot(), 'models', sceneInputPayload.modelId)
|
||||
const materializedPath = join(modelDirectory, fileName)
|
||||
|
||||
await mkdir(modelDirectory, { recursive: true })
|
||||
await writeFile(materializedPath, sceneInputPayload.data)
|
||||
|
||||
sendSceneInputToGodot({
|
||||
modelId: sceneInputPayload.modelId,
|
||||
format: sceneInputPayload.format,
|
||||
name: sceneInputPayload.name,
|
||||
path: materializedPath,
|
||||
})
|
||||
})
|
||||
subscribe(callback) {
|
||||
const unsubscribe = statusListeners.subscribe(callback)
|
||||
callback(currentStatus)
|
||||
return unsubscribe
|
||||
},
|
||||
async applyViewPatch(payload) {
|
||||
return await lifecycleMutex.runExclusive(async () => {
|
||||
const patch = parseStageViewPatchPayload(payload)
|
||||
return sendViewRequest('host.view.patch', { patch })
|
||||
})
|
||||
subscribeViewError(callback) {
|
||||
return viewErrorListeners.subscribe(callback)
|
||||
},
|
||||
async requestViewSnapshot() {
|
||||
return await lifecycleMutex.runExclusive(async () => {
|
||||
return sendViewRequest('host.view.request_snapshot')
|
||||
})
|
||||
subscribeViewSnapshot(callback) {
|
||||
return viewSnapshotListeners.subscribe(callback)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and wires the shared Godot stage manager into app lifecycle hooks.
|
||||
*
|
||||
* Use when:
|
||||
* - Electron main needs one app-wide Godot sidecar lifecycle owner
|
||||
*
|
||||
* Expects:
|
||||
* - App shutdown to call the registered `onAppBeforeQuit` hook
|
||||
*
|
||||
* Returns:
|
||||
* - The ready-to-use Godot stage manager
|
||||
*/
|
||||
export function setupGodotStageManager() {
|
||||
const manager = createGodotStageManager()
|
||||
|
||||
onAppBeforeQuit(async () => {
|
||||
await manager.stop()
|
||||
})
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers Godot stage invoke handlers for one Electron window context.
|
||||
*
|
||||
@@ -972,3 +716,257 @@ export function createGodotStageService(params: {
|
||||
params.window.on('closed', cleanup)
|
||||
return cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and wires the shared Godot stage manager into app lifecycle hooks.
|
||||
*
|
||||
* Use when:
|
||||
* - Electron main needs one app-wide Godot sidecar lifecycle owner
|
||||
*
|
||||
* Expects:
|
||||
* - App shutdown to call the registered `onAppBeforeQuit` hook
|
||||
*
|
||||
* Returns:
|
||||
* - The ready-to-use Godot stage manager
|
||||
*/
|
||||
export function setupGodotStageManager() {
|
||||
const manager = createGodotStageManager()
|
||||
|
||||
onAppBeforeQuit(async () => {
|
||||
await manager.stop()
|
||||
})
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve!: Deferred<T>['resolve']
|
||||
let reject!: Deferred<T>['reject']
|
||||
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
|
||||
return {
|
||||
promise,
|
||||
reject,
|
||||
resolve,
|
||||
}
|
||||
}
|
||||
|
||||
function createInitialStatus(): ElectronGodotStageStatus {
|
||||
return {
|
||||
pid: null,
|
||||
state: 'stopped',
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
function createListenerChannel<T>(onListenerError: (error: unknown) => void): ListenerChannel<T> {
|
||||
const listeners = new Set<(payload: T) => void>()
|
||||
|
||||
return {
|
||||
publish(payload) {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(payload)
|
||||
}
|
||||
catch (error) {
|
||||
onListenerError(error)
|
||||
}
|
||||
}
|
||||
},
|
||||
subscribe(callback) {
|
||||
listeners.add(callback)
|
||||
|
||||
return () => {
|
||||
listeners.delete(callback)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createSocketEnvelope(type: string, payload?: unknown) {
|
||||
return JSON.stringify({ payload, type })
|
||||
}
|
||||
|
||||
function getPayloadMessage(payload: unknown) {
|
||||
const result = safeParse(godotStagePayloadMessageSchema, payload)
|
||||
if (!result.success) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result.output.message
|
||||
}
|
||||
|
||||
function normalizeFileName(fileName: string) {
|
||||
const normalized = basename(fileName.trim())
|
||||
return normalized || 'model.bin'
|
||||
}
|
||||
|
||||
function parseSceneInputPayload(payload: unknown): ElectronGodotStageSceneInputPayload {
|
||||
const result = safeParse(godotStageSceneInputPayloadSchema, payload)
|
||||
if (!result.success)
|
||||
throw new Error('Invalid Godot stage scene input payload.')
|
||||
|
||||
return result.output
|
||||
}
|
||||
|
||||
function parseSocketMessage(message: WebSocketMessage): GodotStageSocketEnvelope {
|
||||
const parsed = safeDestr<unknown>(message.text(), { strict: true })
|
||||
const result = safeParse(godotStageSocketEnvelopeSchema, parsed)
|
||||
if (!result.success)
|
||||
throw new Error('Invalid Godot stage WebSocket envelope.')
|
||||
|
||||
return result.output
|
||||
}
|
||||
|
||||
function pipeProcessLog(stream: Readable, write: (message: string) => void) {
|
||||
stream.on('data', (data) => {
|
||||
const message = data.toString('utf-8').trim()
|
||||
if (message) {
|
||||
write(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Packaged builds ship a pre-exported sidecar under Electron resources.
|
||||
async function resolveExportedGodotBinary(): Promise<string | undefined> {
|
||||
const platform = process.platform
|
||||
let binaryName: string
|
||||
|
||||
if (platform === 'win32') {
|
||||
binaryName = 'godot-stage.exe'
|
||||
}
|
||||
else if (platform === 'darwin') {
|
||||
binaryName = join('godot-stage.app', 'Contents', 'MacOS', 'godot-stage')
|
||||
}
|
||||
else {
|
||||
binaryName = 'godot-stage'
|
||||
}
|
||||
|
||||
const binaryPath = join(process.resourcesPath, 'godot-stage', binaryName)
|
||||
|
||||
try {
|
||||
await access(binaryPath)
|
||||
return binaryPath
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveGodotBinary(): Promise<GodotBinaryResolution> {
|
||||
if (app.isPackaged) {
|
||||
const exported = await resolveExportedGodotBinary()
|
||||
if (exported) {
|
||||
return { executable: exported, mode: 'exported' }
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Godot stage exported binary not found. '
|
||||
+ `Expected at: ${join(process.resourcesPath, 'godot-stage')}`,
|
||||
)
|
||||
}
|
||||
|
||||
const envPath = process.env.GODOT4?.trim()
|
||||
if (!envPath) {
|
||||
throw new Error(
|
||||
'GODOT4 is required to start Godot Stage in development mode.\n'
|
||||
+ 'Set GODOT4 to the absolute path of your Godot 4.x .NET/Mono executable, then restart the Electron dev app.\n'
|
||||
+ 'Examples:\n'
|
||||
+ ' PowerShell: $env:GODOT4 = "C:\\Path\\To\\Godot_v4.x-stable_mono_win64.exe"\n'
|
||||
+ ' Bash: export GODOT4="/path/to/godot"',
|
||||
)
|
||||
}
|
||||
|
||||
await validateConfiguredGodotEnginePath(envPath)
|
||||
return { executable: envPath, mode: 'engine' }
|
||||
}
|
||||
|
||||
// Dev builds run the Godot engine against the workspace project.godot.
|
||||
async function resolveGodotProjectPath() {
|
||||
let currentDirectory = getElectronMainDirname()
|
||||
|
||||
while (true) {
|
||||
const projectPath = resolve(currentDirectory, 'engines', 'stage-tamagotchi-godot')
|
||||
|
||||
try {
|
||||
await access(join(projectPath, 'project.godot'))
|
||||
return projectPath
|
||||
}
|
||||
catch {}
|
||||
|
||||
const parentDirectory = dirname(currentDirectory)
|
||||
if (parentDirectory === currentDirectory) {
|
||||
break
|
||||
}
|
||||
|
||||
currentDirectory = parentDirectory
|
||||
}
|
||||
|
||||
throw new Error(`Unable to locate engines/stage-tamagotchi-godot/project.godot from ${getElectronMainDirname()}.`)
|
||||
}
|
||||
|
||||
function resolveGodotStageDebugLaunchOptions() {
|
||||
const remoteDebugEnabled = ['1', 'on', 'true', 'yes'].includes(
|
||||
(process.env.GODOT_STAGE_REMOTE_DEBUG ?? '').trim().toLowerCase(),
|
||||
)
|
||||
const remoteDebugUri = remoteDebugEnabled
|
||||
? process.env.GODOT_STAGE_REMOTE_DEBUG_URI?.trim() || DEFAULT_GODOT_REMOTE_DEBUG_URI
|
||||
: undefined
|
||||
|
||||
// Godot engine/debugger flags must stay before `--`; StageRoot arguments stay
|
||||
// after it and are assembled next to the WebSocket URL.
|
||||
return {
|
||||
engineArgs: remoteDebugUri ? ['--remote-debug', remoteDebugUri] : [],
|
||||
remoteDebugUri,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGodotStageProcessEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
AIRI_GODOT_STAGE_DEV_MODE: app.isPackaged
|
||||
? process.env.AIRI_GODOT_STAGE_DEV_MODE ?? '0'
|
||||
: process.env.AIRI_GODOT_STAGE_DEV_MODE ?? '1',
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGodotStageStorageRoot() {
|
||||
return join(app.getPath('userData'), 'godot-stage')
|
||||
}
|
||||
|
||||
async function validateConfiguredGodotEnginePath(executable: string) {
|
||||
let executableStats
|
||||
try {
|
||||
executableStats = await stat(executable)
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(
|
||||
'GODOT4 points to a missing Godot executable.\n'
|
||||
+ `Configured path: ${executable}\n`
|
||||
+ 'Set GODOT4 to the absolute path of your Godot 4.x .NET/Mono executable before starting dev mode.\n'
|
||||
+ `Original error: ${errorMessageFrom(error) ?? 'unknown error'}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!executableStats.isFile()) {
|
||||
throw new Error(
|
||||
'GODOT4 must point to the Godot executable file, not a directory or app bundle.\n'
|
||||
+ `Configured path: ${executable}\n`
|
||||
+ 'Examples:\n'
|
||||
+ ' Windows: C:\\Path\\To\\Godot_v4.x-stable_mono_win64.exe\n'
|
||||
+ ' macOS: /Applications/Godot_mono.app/Contents/MacOS/Godot\n'
|
||||
+ ' Linux: /path/to/Godot_v4.x-stable_mono_linux.x86_64',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function waitForProcessExit(exitPromise: Promise<void>, timeoutMs: number) {
|
||||
return Promise.race([
|
||||
exitPromise.then(() => true, () => false),
|
||||
new Promise<boolean>(resolve => setTimeout(resolve, timeoutMs, false)),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import posthog from 'posthog-js'
|
||||
import {
|
||||
DEFAULT_POSTHOG_CONFIG,
|
||||
POSTHOG_PROJECT_KEY,
|
||||
} from '../../../../../posthog.config'
|
||||
} from '@proj-airi/stage-shared/analytics/posthog'
|
||||
|
||||
/** Creates the auth analytics adapter and initializes its provider SDK. */
|
||||
export function createPosthogAdapter(): AnalyticsAdapter {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
import { DEFAULT_POSTHOG_CONFIG, POSTHOG_PROJECT_KEY } from '../../../posthog.config'
|
||||
import { DEFAULT_POSTHOG_CONFIG, POSTHOG_PROJECT_KEY } from '@proj-airi/stage-shared/analytics/posthog'
|
||||
|
||||
if (!import.meta.env.DEV) {
|
||||
posthog.init(POSTHOG_PROJECT_KEY, {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@moeru/std": "catalog:",
|
||||
"@proj-airi/chromatic": "catalog:",
|
||||
"@proj-airi/i18n": "workspace:^",
|
||||
"@proj-airi/stage-shared": "workspace:^",
|
||||
"@vueuse/core": "catalog:",
|
||||
"colorjs.io": "catalog:",
|
||||
"date-fns": "catalog:",
|
||||
|
||||
+274
-267
@@ -1,280 +1,16 @@
|
||||
import type { Preset } from 'unocss'
|
||||
|
||||
import { presetChromatic } from '@proj-airi/unocss-preset-chromatic'
|
||||
import { blackA, cyan, grass, green, indigo, mauve, purple, red, slate, teal, violet } from '@radix-ui/colors'
|
||||
import { defineConfig, presetAttributify, presetIcons, presetTypography, presetWebFonts, presetWind3, transformerDirectives, transformerVariantGroup } from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetAttributify(),
|
||||
presetTypography({
|
||||
cssExtend: {
|
||||
'h1': {
|
||||
'margin-bottom': '1rem',
|
||||
},
|
||||
'a': {
|
||||
'color': '#223f5dff',
|
||||
'text-decoration': 'underline',
|
||||
'text-decoration-style': 'dotted',
|
||||
'text-decoration-color': '#9fa4b1ff',
|
||||
'transition': 'color 0.2s ease-in-out',
|
||||
},
|
||||
'a:hover': {
|
||||
'--primary': '207 62% 59%',
|
||||
'color': 'hsl(var(--primary))',
|
||||
},
|
||||
'.dark a': {
|
||||
'color': '#9ca0a4',
|
||||
'text-decoration-color': '#4b5056',
|
||||
},
|
||||
'code::before': {
|
||||
content: 'normal',
|
||||
},
|
||||
'code::after': {
|
||||
content: 'normal',
|
||||
},
|
||||
'pre': {
|
||||
'margin-top': '0.5rem',
|
||||
'margin-bottom': '0',
|
||||
},
|
||||
'p': {
|
||||
'margin-top': '0.5rem',
|
||||
'margin-bottom': '0.5rem',
|
||||
},
|
||||
'details': {
|
||||
'margin-top': '0.5rem',
|
||||
'margin-bottom': '0.5rem',
|
||||
'padding': '0.5rem 1rem',
|
||||
'background-color': '#a6ceef1a',
|
||||
},
|
||||
'.dark details': {
|
||||
'background-color': '#191c1e',
|
||||
},
|
||||
'ol': {
|
||||
'margin-top': '0.25rem',
|
||||
'margin-bottom': '0.25rem',
|
||||
'padding-inline-start': '1.25rem',
|
||||
},
|
||||
'ol li': {
|
||||
'padding-inline-start': '0.25rem',
|
||||
},
|
||||
'ul': {
|
||||
'margin-top': '0.25rem',
|
||||
'margin-bottom': '0.25rem',
|
||||
'padding-inline-start': '1.25rem',
|
||||
},
|
||||
'ul li': {
|
||||
'padding-inline-start': '0.25rem',
|
||||
},
|
||||
'li': {
|
||||
'margin-top': '0',
|
||||
'margin-bottom': '0',
|
||||
},
|
||||
'li p': {
|
||||
'margin-top': '0.25rem',
|
||||
'margin-bottom': '0.25rem',
|
||||
},
|
||||
'li blockquote': {
|
||||
'margin-top': '1rem',
|
||||
'margin-bottom': '1rem',
|
||||
},
|
||||
},
|
||||
}),
|
||||
presetWind3(),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
'sans': {
|
||||
name: 'DM Sans Variable',
|
||||
provider: 'none',
|
||||
},
|
||||
'serif': {
|
||||
name: 'DM Serif Display',
|
||||
provider: 'none',
|
||||
},
|
||||
'mono': {
|
||||
name: 'DM Mono',
|
||||
provider: 'none',
|
||||
},
|
||||
'sans-rounded': {
|
||||
name: 'Comfortaa Variable',
|
||||
provider: 'none',
|
||||
},
|
||||
'mystery-quest': {
|
||||
name: 'Mystery Quest',
|
||||
},
|
||||
'grandstander': {
|
||||
name: 'Grandstander',
|
||||
},
|
||||
},
|
||||
}),
|
||||
presetIcons(),
|
||||
presetChromatic({
|
||||
baseHue: 220.44,
|
||||
colors: {
|
||||
primary: 0,
|
||||
complementary: 180,
|
||||
},
|
||||
}),
|
||||
],
|
||||
export default defineConfig<object>({
|
||||
content: {
|
||||
filesystem: [
|
||||
'.vitepress/**/*.{js,ts,vue}',
|
||||
'content/**/*.md',
|
||||
],
|
||||
},
|
||||
safelist: [
|
||||
'-ml-8',
|
||||
'top-0',
|
||||
'hidden',
|
||||
'border-0',
|
||||
'opacity-0',
|
||||
'group-hover:opacity-100',
|
||||
'focus:opacity-100',
|
||||
'lg:flex',
|
||||
'transition-opacity',
|
||||
'duration-200',
|
||||
'ease-in-out',
|
||||
'[&_span]:focus:opacity-100',
|
||||
'[&_span_>_span]:focus:outline',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
'sans': `ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-rounded': `"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-serif-halloween-secondary': `"Grandstander", "DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-serif-halloween': `"Mystery Quest", "DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
code: 'hsl(var(--code))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
...blackA,
|
||||
...mauve,
|
||||
...violet,
|
||||
...green,
|
||||
...red,
|
||||
...grass,
|
||||
...teal,
|
||||
...cyan,
|
||||
...indigo,
|
||||
...purple,
|
||||
...slate,
|
||||
},
|
||||
/**
|
||||
* https://github.com/unocss/unocss/blob/1031312057a3bea1082b7d938eb2ad640f57613a/packages-presets/preset-wind4/src/theme/animate.ts
|
||||
* https://unocss.dev/presets/wind4#transformdirectives
|
||||
*/
|
||||
animation: {
|
||||
keyframes: {
|
||||
overlayShow: '{from{opacity:0}to{opacity:1}}',
|
||||
contentShow: '{from{opacity:0;transform:translate(-50%, -48%) scale(0.96)}to{opacity:1;transform:translate(-50%, -50%) scale(1)}}',
|
||||
slideDownAndFade: '{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}',
|
||||
slideLeftAndFade: '{from{opacity:0;transform:translateX(2px)}to{opacity:1;transform:translateX(0)}}',
|
||||
slideUpAndFade: '{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}',
|
||||
slideRightAndFade: '{from{opacity:0;transform:translateX(-2px)}to{opacity:1;transform:translateX(0)}}',
|
||||
slideDown: '{from{height:0}to{height:var(--reka-collapsible-content-height)}}',
|
||||
slideUp: '{from{height:var(--reka-collapsible-content-height)}to{height:0}}',
|
||||
enterFromRight: '{from{opacity:0;transform:translateX(200px)}to{opacity:1;transform:translateX(0)}}',
|
||||
enterFromLeft: '{from{opacity:0;transform:translateX(-200px)}to{opacity:1;transform:translateX(0)}}',
|
||||
exitToRight: '{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(200px)}}',
|
||||
exitToLeft: '{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-200px)}}',
|
||||
scaleIn: '{from{opacity:0;transform:rotateX(-10deg) scale(0.9)}to{opacity:1;transform:rotateX(0deg) scale(1)}}',
|
||||
scaleOut: '{from{opacity:1;transform:rotateX(0deg) scale(1)}to{opacity:0;transform:rotateX(-10deg) scale(0.95)}}',
|
||||
fadeIn: '{from{opacity:0}to{opacity:1}}',
|
||||
fadeOut: '{from{opacity:1}to{opacity:0}}',
|
||||
hide: '{from{opacity:1}to{opacity:0}}',
|
||||
slideIn: '{from{transform:translateX(calc(100% + var(--viewport-padding)))}to{transform:translateX(0)}}',
|
||||
swipeOut: '{from{transform:translateX(var(--reka-toast-swipe-end-x))}to{transform:translateX(calc(100% + var(--viewport-padding)))}}',
|
||||
text: '{0%,100%{background-size:200% 200%;background-position:left center}50%{background-size:200% 200%;background-position:right center}}',
|
||||
progress: '{0%{background-position:0 0}100%{background-position:30px 30px}}',
|
||||
},
|
||||
durations: {
|
||||
overlayShow: '150ms',
|
||||
contentShow: '150ms',
|
||||
slideDownAndFade: '400ms',
|
||||
slideLeftAndFade: '400ms',
|
||||
slideUpAndFade: '400ms',
|
||||
slideRightAndFade: '400ms',
|
||||
slideDown: '300ms',
|
||||
slideUp: '300ms',
|
||||
scaleIn: '200ms',
|
||||
scaleOut: '200ms',
|
||||
fadeIn: '200ms',
|
||||
fadeOut: '200ms',
|
||||
enterFromLeft: '250ms',
|
||||
enterFromRight: '250ms',
|
||||
exitToLeft: '250ms',
|
||||
exitToRight: '250ms',
|
||||
hide: '100ms',
|
||||
slideIn: '150ms',
|
||||
swipeOut: '100ms',
|
||||
text: '5s',
|
||||
progress: '1s',
|
||||
},
|
||||
timingFns: {
|
||||
overlayShow: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
contentShow: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideDownAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideLeftAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideUpAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideRightAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideDown: 'cubic-bezier(0.87, 0, 0.13, 1)',
|
||||
slideUp: 'cubic-bezier(0.87, 0, 0.13, 1)',
|
||||
scaleIn: 'ease',
|
||||
scaleOut: 'ease',
|
||||
fadeIn: 'ease',
|
||||
fadeOut: 'ease',
|
||||
enterFromLeft: 'ease',
|
||||
enterFromRight: 'ease',
|
||||
exitToLeft: 'ease',
|
||||
exitToRight: 'ease',
|
||||
hide: 'ease-in',
|
||||
slideIn: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
swipeOut: 'ease-out',
|
||||
text: 'ease',
|
||||
progress: 'linear',
|
||||
},
|
||||
counts: {
|
||||
text: 'infinite',
|
||||
progress: 'infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
'bg-gradient-radial': 'bg-gradient-radial-[var(--tw-gradient-stops)]',
|
||||
},
|
||||
rules: [
|
||||
[/^bg-gradient-radial-\[(.+)\]$/, ([, d]) => ({ 'background-image': `radial-gradient(${d})` })],
|
||||
],
|
||||
preflights: [
|
||||
{
|
||||
getCSS: () => {
|
||||
@@ -301,6 +37,277 @@ code,kbd,samp,pre {
|
||||
},
|
||||
},
|
||||
],
|
||||
presets: [
|
||||
presetAttributify(),
|
||||
presetTypography({
|
||||
cssExtend: {
|
||||
'.dark a': {
|
||||
'color': '#9ca0a4',
|
||||
'text-decoration-color': '#4b5056',
|
||||
},
|
||||
'.dark details': {
|
||||
'background-color': '#191c1e',
|
||||
},
|
||||
'a': {
|
||||
'color': '#223f5dff',
|
||||
'text-decoration': 'underline',
|
||||
'text-decoration-color': '#9fa4b1ff',
|
||||
'text-decoration-style': 'dotted',
|
||||
'transition': 'color 0.2s ease-in-out',
|
||||
},
|
||||
'a:hover': {
|
||||
'--primary': '207 62% 59%',
|
||||
'color': 'hsl(var(--primary))',
|
||||
},
|
||||
'code::after': {
|
||||
content: 'normal',
|
||||
},
|
||||
'code::before': {
|
||||
content: 'normal',
|
||||
},
|
||||
'details': {
|
||||
'background-color': '#a6ceef1a',
|
||||
'margin-bottom': '0.5rem',
|
||||
'margin-top': '0.5rem',
|
||||
'padding': '0.5rem 1rem',
|
||||
},
|
||||
'h1': {
|
||||
'margin-bottom': '1rem',
|
||||
},
|
||||
'li': {
|
||||
'margin-bottom': '0',
|
||||
'margin-top': '0',
|
||||
},
|
||||
'li blockquote': {
|
||||
'margin-bottom': '1rem',
|
||||
'margin-top': '1rem',
|
||||
},
|
||||
'li p': {
|
||||
'margin-bottom': '0.25rem',
|
||||
'margin-top': '0.25rem',
|
||||
},
|
||||
'ol': {
|
||||
'margin-bottom': '0.25rem',
|
||||
'margin-top': '0.25rem',
|
||||
'padding-inline-start': '1.25rem',
|
||||
},
|
||||
'ol li': {
|
||||
'padding-inline-start': '0.25rem',
|
||||
},
|
||||
'p': {
|
||||
'margin-bottom': '0.5rem',
|
||||
'margin-top': '0.5rem',
|
||||
},
|
||||
'pre': {
|
||||
'margin-bottom': '0',
|
||||
'margin-top': '0.5rem',
|
||||
},
|
||||
'ul': {
|
||||
'margin-bottom': '0.25rem',
|
||||
'margin-top': '0.25rem',
|
||||
'padding-inline-start': '1.25rem',
|
||||
},
|
||||
'ul li': {
|
||||
'padding-inline-start': '0.25rem',
|
||||
},
|
||||
},
|
||||
}),
|
||||
presetWind3(),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
'grandstander': {
|
||||
name: 'Grandstander',
|
||||
},
|
||||
'mono': {
|
||||
name: 'DM Mono',
|
||||
provider: 'none',
|
||||
},
|
||||
'mystery-quest': {
|
||||
name: 'Mystery Quest',
|
||||
},
|
||||
'sans': {
|
||||
name: 'DM Sans Variable',
|
||||
provider: 'none',
|
||||
},
|
||||
'sans-rounded': {
|
||||
name: 'Comfortaa Variable',
|
||||
provider: 'none',
|
||||
},
|
||||
'serif': {
|
||||
name: 'DM Serif Display',
|
||||
provider: 'none',
|
||||
},
|
||||
},
|
||||
}),
|
||||
presetIcons(),
|
||||
// NOTICE:
|
||||
// This cast bridges the preset's bundled UnoCSS declarations to the workspace declarations.
|
||||
// Version 1.1.4 embeds @unocss/core types in its generated shared declaration file.
|
||||
// Source: node_modules/@proj-airi/unocss-preset-chromatic/dist/shared-3Rd5ey9i.d.mts.
|
||||
// Remove this when the package emits imports for UnoCSS types instead of bundling them.
|
||||
presetChromatic({
|
||||
baseHue: 220.44,
|
||||
colors: {
|
||||
complementary: 180,
|
||||
primary: 0,
|
||||
},
|
||||
}) as unknown as Preset<object>,
|
||||
],
|
||||
rules: [
|
||||
[/^bg-gradient-radial-\[(.+)\]$/, ([, d]) => ({ 'background-image': `radial-gradient(${d})` })],
|
||||
],
|
||||
safelist: [
|
||||
'-ml-8',
|
||||
'top-0',
|
||||
'hidden',
|
||||
'border-0',
|
||||
'opacity-0',
|
||||
'group-hover:opacity-100',
|
||||
'focus:opacity-100',
|
||||
'lg:flex',
|
||||
'transition-opacity',
|
||||
'duration-200',
|
||||
'ease-in-out',
|
||||
'[&_span]:focus:opacity-100',
|
||||
'[&_span_>_span]:focus:outline',
|
||||
],
|
||||
shortcuts: {
|
||||
'bg-gradient-radial': 'bg-gradient-radial-[var(--tw-gradient-stops)]',
|
||||
},
|
||||
theme: {
|
||||
/**
|
||||
* https://github.com/unocss/unocss/blob/1031312057a3bea1082b7d938eb2ad640f57613a/packages-presets/preset-wind4/src/theme/animate.ts
|
||||
* https://unocss.dev/presets/wind4#transformdirectives
|
||||
*/
|
||||
animation: {
|
||||
counts: {
|
||||
progress: 'infinite',
|
||||
text: 'infinite',
|
||||
},
|
||||
durations: {
|
||||
contentShow: '150ms',
|
||||
enterFromLeft: '250ms',
|
||||
enterFromRight: '250ms',
|
||||
exitToLeft: '250ms',
|
||||
exitToRight: '250ms',
|
||||
fadeIn: '200ms',
|
||||
fadeOut: '200ms',
|
||||
hide: '100ms',
|
||||
overlayShow: '150ms',
|
||||
progress: '1s',
|
||||
scaleIn: '200ms',
|
||||
scaleOut: '200ms',
|
||||
slideDown: '300ms',
|
||||
slideDownAndFade: '400ms',
|
||||
slideIn: '150ms',
|
||||
slideLeftAndFade: '400ms',
|
||||
slideRightAndFade: '400ms',
|
||||
slideUp: '300ms',
|
||||
slideUpAndFade: '400ms',
|
||||
swipeOut: '100ms',
|
||||
text: '5s',
|
||||
},
|
||||
keyframes: {
|
||||
contentShow: '{from{opacity:0;transform:translate(-50%, -48%) scale(0.96)}to{opacity:1;transform:translate(-50%, -50%) scale(1)}}',
|
||||
enterFromLeft: '{from{opacity:0;transform:translateX(-200px)}to{opacity:1;transform:translateX(0)}}',
|
||||
enterFromRight: '{from{opacity:0;transform:translateX(200px)}to{opacity:1;transform:translateX(0)}}',
|
||||
exitToLeft: '{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-200px)}}',
|
||||
exitToRight: '{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(200px)}}',
|
||||
fadeIn: '{from{opacity:0}to{opacity:1}}',
|
||||
fadeOut: '{from{opacity:1}to{opacity:0}}',
|
||||
hide: '{from{opacity:1}to{opacity:0}}',
|
||||
overlayShow: '{from{opacity:0}to{opacity:1}}',
|
||||
progress: '{0%{background-position:0 0}100%{background-position:30px 30px}}',
|
||||
scaleIn: '{from{opacity:0;transform:rotateX(-10deg) scale(0.9)}to{opacity:1;transform:rotateX(0deg) scale(1)}}',
|
||||
scaleOut: '{from{opacity:1;transform:rotateX(0deg) scale(1)}to{opacity:0;transform:rotateX(-10deg) scale(0.95)}}',
|
||||
slideDown: '{from{height:0}to{height:var(--reka-collapsible-content-height)}}',
|
||||
slideDownAndFade: '{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}',
|
||||
slideIn: '{from{transform:translateX(calc(100% + var(--viewport-padding)))}to{transform:translateX(0)}}',
|
||||
slideLeftAndFade: '{from{opacity:0;transform:translateX(2px)}to{opacity:1;transform:translateX(0)}}',
|
||||
slideRightAndFade: '{from{opacity:0;transform:translateX(-2px)}to{opacity:1;transform:translateX(0)}}',
|
||||
slideUp: '{from{height:var(--reka-collapsible-content-height)}to{height:0}}',
|
||||
slideUpAndFade: '{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}',
|
||||
swipeOut: '{from{transform:translateX(var(--reka-toast-swipe-end-x))}to{transform:translateX(calc(100% + var(--viewport-padding)))}}',
|
||||
text: '{0%,100%{background-size:200% 200%;background-position:left center}50%{background-size:200% 200%;background-position:right center}}',
|
||||
},
|
||||
timingFns: {
|
||||
contentShow: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
enterFromLeft: 'ease',
|
||||
enterFromRight: 'ease',
|
||||
exitToLeft: 'ease',
|
||||
exitToRight: 'ease',
|
||||
fadeIn: 'ease',
|
||||
fadeOut: 'ease',
|
||||
hide: 'ease-in',
|
||||
overlayShow: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
progress: 'linear',
|
||||
scaleIn: 'ease',
|
||||
scaleOut: 'ease',
|
||||
slideDown: 'cubic-bezier(0.87, 0, 0.13, 1)',
|
||||
slideDownAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideIn: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideLeftAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideRightAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideUp: 'cubic-bezier(0.87, 0, 0.13, 1)',
|
||||
slideUpAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
swipeOut: 'ease-out',
|
||||
text: 'ease',
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
background: 'hsl(var(--background))',
|
||||
border: 'hsl(var(--border))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
code: 'hsl(var(--code))',
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
input: 'hsl(var(--input))',
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
ring: 'hsl(var(--ring))',
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
...blackA,
|
||||
...mauve,
|
||||
...violet,
|
||||
...green,
|
||||
...red,
|
||||
...grass,
|
||||
...teal,
|
||||
...cyan,
|
||||
...indigo,
|
||||
...purple,
|
||||
...slate,
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': `ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-rounded': `"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-serif-halloween': `"Mystery Quest", "DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-serif-halloween-secondary': `"Grandstander", "DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
},
|
||||
},
|
||||
transformers: [
|
||||
transformerDirectives(),
|
||||
transformerVariantGroup(),
|
||||
|
||||
+11
-11
@@ -2,8 +2,8 @@ import { defineConfig } from '@moeru/eslint-config'
|
||||
|
||||
export default defineConfig({
|
||||
masknet: false,
|
||||
perfectionist: true,
|
||||
preferArrow: false,
|
||||
perfectionist: false,
|
||||
sonarjs: false,
|
||||
sortPackageJsonScripts: false,
|
||||
typescript: true,
|
||||
@@ -32,15 +32,12 @@ export default defineConfig({
|
||||
],
|
||||
}, {
|
||||
rules: {
|
||||
'pnpm/json-valid-catalog': 'off',
|
||||
'pnpm/json-enforce-catalog': 'off',
|
||||
'pnpm/yaml-enforce-settings': 'off',
|
||||
'antfu/import-dedupe': 'error',
|
||||
// TODO: remove this
|
||||
'depend/ban-dependencies': 'warn',
|
||||
'import/order': 'off',
|
||||
'markdown/require-alt-text': 'off',
|
||||
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
|
||||
|
||||
// Catches the manual `error instanceof Error ? error.message : ...`
|
||||
// pattern AGENTS.md forbids. The selector matches a ConditionalExpression
|
||||
// whose test is `<x> instanceof Error` and whose consequent is `<x>.message`,
|
||||
@@ -50,29 +47,32 @@ export default defineConfig({
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'ConditionalExpression[test.type=\'BinaryExpression\'][test.operator=\'instanceof\'][test.right.name=\'Error\'][consequent.type=\'MemberExpression\'][consequent.property.name=\'message\']',
|
||||
message: 'Avoid `error instanceof Error ? error.message : ...`. Use `errorMessageFrom(error)` from \'@moeru/std\' (or `errorMessageFromUnknown(error, fallback)` from \'@proj-airi/stage-shared\'). Pair with `?? \'fallback\'` when a default is needed.',
|
||||
selector: 'ConditionalExpression[test.type=\'BinaryExpression\'][test.operator=\'instanceof\'][test.right.name=\'Error\'][consequent.type=\'MemberExpression\'][consequent.property.name=\'message\']',
|
||||
},
|
||||
{
|
||||
message: 'Omit TypeScript and JavaScript source extensions from relative imports, dynamic imports, and re-exports.',
|
||||
selector: [
|
||||
'ImportDeclaration[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
'ExportNamedDeclaration[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
'ExportAllDeclaration[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
'ImportExpression[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
].join(', '),
|
||||
message: 'Omit TypeScript and JavaScript source extensions from relative imports, dynamic imports, and re-exports.',
|
||||
},
|
||||
'TSEnumDeclaration[const=true]',
|
||||
'TSExportAssignment',
|
||||
],
|
||||
'pnpm/json-enforce-catalog': 'off',
|
||||
|
||||
'pnpm/json-valid-catalog': 'off',
|
||||
|
||||
'pnpm/yaml-enforce-settings': 'off',
|
||||
// 'sonarjs/cognitive-complexity': 'off',
|
||||
// 'sonarjs/no-commented-code': 'off',
|
||||
// 'sonarjs/pseudo-random': 'off',
|
||||
'style/padding-line-between-statements': 'error',
|
||||
'vue/prefer-separate-static-class': 'off',
|
||||
'yaml/plain-scalar': 'off',
|
||||
'markdown/require-alt-text': 'off',
|
||||
},
|
||||
}, {
|
||||
files: ['server/apps/api/**/*.ts'],
|
||||
@@ -80,21 +80,21 @@ export default defineConfig({
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'CallExpression[callee.type=\'MemberExpression\'][callee.object.name=\'vi\'][callee.property.name=/^(mock|doMock)$/][arguments.0.type=\'Literal\'][arguments.0.value=/^(\\.|@proj-airi\\/|~)/]',
|
||||
message: 'Do not mock internal project modules with vi.mock or vi.doMock. Inject the collaborator through the route, service, or factory boundary and pass a fake or spy in tests.',
|
||||
selector: 'CallExpression[callee.type=\'MemberExpression\'][callee.object.name=\'vi\'][callee.property.name=/^(mock|doMock)$/][arguments.0.type=\'Literal\'][arguments.0.value=/^(\\.|@proj-airi\\/|~)/]',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.type=\'MemberExpression\'][callee.object.name=\'vi\'][callee.property.name=\'hoisted\']',
|
||||
message: 'Do not use vi.hoisted. If a test needs a collaborator spy, expose an explicit dependency injection point instead of hoisting module mocks.',
|
||||
selector: 'CallExpression[callee.type=\'MemberExpression\'][callee.object.name=\'vi\'][callee.property.name=\'hoisted\']',
|
||||
},
|
||||
{
|
||||
message: 'Omit TypeScript and JavaScript source extensions from relative imports, dynamic imports, and re-exports.',
|
||||
selector: [
|
||||
'ImportDeclaration[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
'ExportNamedDeclaration[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
'ExportAllDeclaration[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
'ImportExpression[source.value=/^\\.{1,2}\\/.*\\.[cm]?[jt]sx?$/]',
|
||||
].join(', '),
|
||||
message: 'Omit TypeScript and JavaScript source extensions from relative imports, dynamic imports, and re-exports.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"version": "0.12.0-beta.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"packageManager": "pnpm@11.24.0",
|
||||
"description": "LLM powered virtual character",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
@@ -72,6 +72,7 @@
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-format": "catalog:",
|
||||
"eslint-plugin-oxlint": "catalog:",
|
||||
"eslint-plugin-perfectionist": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"nano-staged": "catalog:",
|
||||
"oxc-minify": "catalog:",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"@moeru/std": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/audioworklet": "catalog:"
|
||||
"@types/audioworklet": "catalog:",
|
||||
"vue": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,49 @@
|
||||
import type { CrossWsConstructor } from '.'
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createCrossWsConnector } from '.'
|
||||
import { createClient } from '../..'
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The mock narrowed message data to two types, but CrossWS exposes unknown data.
|
||||
// This made the mock constructor incompatible with the production connector contract.
|
||||
//
|
||||
// The mock now derives its message event from the public constructor contract.
|
||||
type CrossWsMessageEvent = Parameters<NonNullable<InstanceType<CrossWsConstructor>['onmessage']>>[0]
|
||||
|
||||
const { MockWebSocket } = vi.hoisted(() => {
|
||||
class MockWebSocket {
|
||||
static readonly CONNECTING = 0
|
||||
static readonly OPEN = 1
|
||||
static readonly CLOSING = 2
|
||||
static readonly CLOSED = 3
|
||||
static readonly CLOSING = 2
|
||||
static readonly CONNECTING = 0
|
||||
static readonly instances: MockWebSocket[] = []
|
||||
static readonly OPEN = 1
|
||||
|
||||
onclose?: (event: { code?: number, reason?: string, wasClean?: boolean }) => void
|
||||
onerror?: (event: unknown | { error?: Error }) => void
|
||||
onmessage?: (event: CrossWsMessageEvent) => void
|
||||
onopen?: () => void
|
||||
ping = vi.fn()
|
||||
pong = vi.fn()
|
||||
|
||||
readyState = MockWebSocket.CONNECTING
|
||||
|
||||
readonly sent: string[] = []
|
||||
readyState = MockWebSocket.CONNECTING
|
||||
onclose?: (event: { code?: number, reason?: string, wasClean?: boolean }) => void
|
||||
onerror?: (event: { error?: Error } | unknown) => void
|
||||
onmessage?: (event: { data: string | ArrayBuffer }) => void
|
||||
onopen?: () => void
|
||||
|
||||
constructor(readonly url: string | URL, readonly protocols?: string | string[]) {
|
||||
MockWebSocket.instances.push(this)
|
||||
}
|
||||
|
||||
send(message: string) {
|
||||
this.sent.push(message)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = MockWebSocket.CLOSED
|
||||
this.onclose?.({ code: 1000, reason: 'closed', wasClean: true })
|
||||
}
|
||||
|
||||
ping = vi.fn()
|
||||
pong = vi.fn()
|
||||
send(message: string) {
|
||||
this.sent.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
return { MockWebSocket }
|
||||
@@ -84,8 +95,8 @@ describe('createCrossWsConnector', () => {
|
||||
wsConstructor: MockWebSocket,
|
||||
}),
|
||||
reconnect: {
|
||||
retries: 0,
|
||||
onFailed,
|
||||
retries: 0,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -18,89 +18,6 @@ export interface CapVitePluginOptions {
|
||||
capArgs: string[]
|
||||
}
|
||||
|
||||
async function stopCapProcess(current: Result | undefined) {
|
||||
if (!current) {
|
||||
return
|
||||
}
|
||||
|
||||
current.kill('SIGINT')
|
||||
|
||||
try {
|
||||
await current
|
||||
}
|
||||
catch {
|
||||
// tinyexec rejects when a process is stopped during a restart.
|
||||
}
|
||||
}
|
||||
|
||||
function startCapProcess(cwd: string, capArgs: string[], url: URL) {
|
||||
return x('cap', ['run', ...capArgs], {
|
||||
throwOnError: false,
|
||||
nodeOptions: {
|
||||
cwd,
|
||||
env: {
|
||||
CAPACITOR_DEV_SERVER_URL: url.toString(),
|
||||
},
|
||||
// NOTICE: cap-vite owns the terminal shortcuts, so cap run should not
|
||||
// consume stdin while still mirroring its stdout/stderr to the console.
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function bindCapViteShortcuts(
|
||||
onRestart: () => void,
|
||||
onShutdown: () => Promise<void>,
|
||||
) {
|
||||
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
process.stdin.setEncoding('utf8')
|
||||
readline.emitKeypressEvents(process.stdin)
|
||||
|
||||
const shouldRestoreRawMode = !process.stdin.isRaw
|
||||
if (shouldRestoreRawMode) {
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
|
||||
async function shutdownFromShortcut() {
|
||||
try {
|
||||
await onShutdown()
|
||||
}
|
||||
finally {
|
||||
if (shouldRestoreRawMode) {
|
||||
process.stdin.setRawMode(false)
|
||||
}
|
||||
|
||||
process.kill(process.pid, 'SIGINT')
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyPress = (input: string, key: readline.Key) => {
|
||||
if (key.ctrl && key.name === 'c') {
|
||||
void shutdownFromShortcut()
|
||||
return
|
||||
}
|
||||
|
||||
const keyName = key.name?.toLowerCase() ?? input.toLowerCase()
|
||||
if (!key.ctrl && !key.meta && keyName === 'r') {
|
||||
onRestart()
|
||||
}
|
||||
}
|
||||
|
||||
process.stdin.on('keypress', onKeyPress)
|
||||
|
||||
return () => {
|
||||
process.stdin.off('keypress', onKeyPress)
|
||||
|
||||
if (shouldRestoreRawMode) {
|
||||
process.stdin.setRawMode(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function capVitePlugin(options: CapVitePluginOptions): Plugin {
|
||||
const platform = parseCapacitorPlatform(options.capArgs[0])
|
||||
if (!platform) {
|
||||
@@ -110,7 +27,6 @@ export function capVitePlugin(options: CapVitePluginOptions): Plugin {
|
||||
|
||||
return {
|
||||
apply: 'serve',
|
||||
name: 'cap-vite:run-capacitor',
|
||||
async configureServer(server) {
|
||||
const resolvedCapArgs = await resolveCapRunArgs(options.capArgs)
|
||||
const cwd = resolve(server.config.root)
|
||||
@@ -175,7 +91,23 @@ export function capVitePlugin(options: CapVitePluginOptions): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
function onWatcherEvent(_event, file) {
|
||||
/**
|
||||
* Requests a Capacitor restart after a native project file changes.
|
||||
*
|
||||
* Triggering workflow:
|
||||
*
|
||||
* `server.watcher`
|
||||
* -> `all`
|
||||
* -> `onWatcherEvent`
|
||||
* -> `requestRestart`
|
||||
*
|
||||
* Upstream:
|
||||
* - `server.watcher`
|
||||
*
|
||||
* Downstream:
|
||||
* - `requestRestart`
|
||||
*/
|
||||
function onWatcherEvent(_event: string, file: string) {
|
||||
if (!shouldRestartForNativeChange(file, resolvedPlatform, cwd)) {
|
||||
return
|
||||
}
|
||||
@@ -219,5 +151,89 @@ export function capVitePlugin(options: CapVitePluginOptions): Plugin {
|
||||
process.once('SIGINT', handleShutdownRequest)
|
||||
process.once('SIGTERM', handleShutdownRequest)
|
||||
},
|
||||
name: 'cap-vite:run-capacitor',
|
||||
}
|
||||
}
|
||||
|
||||
function bindCapViteShortcuts(
|
||||
onRestart: () => void,
|
||||
onShutdown: () => Promise<void>,
|
||||
) {
|
||||
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
process.stdin.setEncoding('utf8')
|
||||
readline.emitKeypressEvents(process.stdin)
|
||||
|
||||
const shouldRestoreRawMode = !process.stdin.isRaw
|
||||
if (shouldRestoreRawMode) {
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
|
||||
async function shutdownFromShortcut() {
|
||||
try {
|
||||
await onShutdown()
|
||||
}
|
||||
finally {
|
||||
if (shouldRestoreRawMode) {
|
||||
process.stdin.setRawMode(false)
|
||||
}
|
||||
|
||||
process.kill(process.pid, 'SIGINT')
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyPress = (input: string, key: readline.Key) => {
|
||||
if (key.ctrl && key.name === 'c') {
|
||||
void shutdownFromShortcut()
|
||||
return
|
||||
}
|
||||
|
||||
const keyName = key.name?.toLowerCase() ?? input.toLowerCase()
|
||||
if (!key.ctrl && !key.meta && keyName === 'r') {
|
||||
onRestart()
|
||||
}
|
||||
}
|
||||
|
||||
process.stdin.on('keypress', onKeyPress)
|
||||
|
||||
return () => {
|
||||
process.stdin.off('keypress', onKeyPress)
|
||||
|
||||
if (shouldRestoreRawMode) {
|
||||
process.stdin.setRawMode(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startCapProcess(cwd: string, capArgs: string[], url: URL) {
|
||||
return x('cap', ['run', ...capArgs], {
|
||||
nodeOptions: {
|
||||
cwd,
|
||||
env: {
|
||||
CAPACITOR_DEV_SERVER_URL: url.toString(),
|
||||
},
|
||||
// NOTICE: cap-vite owns the terminal shortcuts, so cap run should not
|
||||
// consume stdin while still mirroring its stdout/stderr to the console.
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
}
|
||||
|
||||
async function stopCapProcess(current: Result | undefined) {
|
||||
if (!current) {
|
||||
return
|
||||
}
|
||||
|
||||
current.kill('SIGINT')
|
||||
|
||||
try {
|
||||
await current
|
||||
}
|
||||
catch {
|
||||
// tinyexec rejects when a process is stopped during a restart.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/hast": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/unist": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"],
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
@@ -47,16 +47,14 @@
|
||||
"@moeru/std": "catalog:",
|
||||
"async-mutex": "catalog:",
|
||||
"nanoid": "catalog:",
|
||||
"vite": "catalog:"
|
||||
"vite": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"inlinedDependencies": {
|
||||
"@electron-toolkit/preload": "3.0.2",
|
||||
"@moeru/eventa": "1.0.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"nanoid": [
|
||||
"5.1.11",
|
||||
"6.0.1"
|
||||
],
|
||||
"picomatch": "4.0.4"
|
||||
"nanoid": "6.0.1",
|
||||
"picomatch": "4.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"std-env": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@proj-airi/electron-eventa": "workspace:^"
|
||||
"@proj-airi/electron-eventa": "workspace:^",
|
||||
"vue": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
LlmStreamingControlSignal,
|
||||
LlmStreamingControlSignalContext,
|
||||
LlmStreamingControlSignalHandler,
|
||||
LlmStreamingControlTurn,
|
||||
LlmStreamingControlTurnDone,
|
||||
} from './types'
|
||||
|
||||
@@ -14,148 +15,10 @@ import { tokenAct, tokenCall, tokenDelay } from './parsers'
|
||||
import { renderCallManifestPrompt } from './parsers/call'
|
||||
|
||||
interface StreamingControlTurnState {
|
||||
handlers: Map<string, Set<LlmStreamingControlCallHandler>>
|
||||
callManifests: Map<string, LlmStreamingControlCallManifest>
|
||||
settle: (result: LlmStreamingControlTurnDone) => void
|
||||
done: Promise<LlmStreamingControlTurnDone>
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts parsed signal payload into observer-friendly text.
|
||||
*
|
||||
* Use when:
|
||||
* - Observer logs need a compact human-readable parameter
|
||||
*
|
||||
* Notice:
|
||||
* - Intentionally serializes payloads once
|
||||
* - Returns undefined for empty CALL payload
|
||||
*/
|
||||
function parsedParameter(signal: LlmStreamingControlSignal): string | undefined {
|
||||
switch (signal.type) {
|
||||
case 'act':
|
||||
return JSON.stringify(signal.payload)
|
||||
|
||||
case 'call':
|
||||
return signal.payload != null
|
||||
? JSON.stringify(signal.payload)
|
||||
: undefined
|
||||
|
||||
case 'delay':
|
||||
return `${signal.seconds}s`
|
||||
}
|
||||
}
|
||||
|
||||
function createTurnId() {
|
||||
return `turn:${
|
||||
globalThis.crypto?.randomUUID?.()
|
||||
?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes manifest values before registration.
|
||||
*
|
||||
* Notice:
|
||||
* - Empty names/prompts are rejected
|
||||
* - Prevents duplicated trim logic
|
||||
*/
|
||||
function normalizeManifest(
|
||||
manifest: LlmStreamingControlCallManifest,
|
||||
): LlmStreamingControlCallManifest | undefined {
|
||||
const name = manifest.name.trim()
|
||||
const prompt = manifest.prompt.trim()
|
||||
|
||||
if (!name || !prompt)
|
||||
return
|
||||
|
||||
return {
|
||||
...manifest,
|
||||
name,
|
||||
prompt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits observer events safely.
|
||||
*
|
||||
* Notice:
|
||||
* - Observer failures must never break dispatch
|
||||
*/
|
||||
function emit(
|
||||
context: Pick<LlmStreamingControlCallContext, 'observer'> | undefined,
|
||||
payload: Parameters<NonNullable<LlmStreamingControlCallContext['observer']>>[0],
|
||||
) {
|
||||
try {
|
||||
context?.observer?.(payload)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates isolated turn state.
|
||||
*
|
||||
* Notice:
|
||||
* - Promise resolves once only
|
||||
* - Prevents accidental double completion
|
||||
*/
|
||||
function createTurnState(): StreamingControlTurnState {
|
||||
let settled = false
|
||||
let settle!: (result: LlmStreamingControlTurnDone) => void
|
||||
|
||||
const done = new Promise<LlmStreamingControlTurnDone>((resolve) => {
|
||||
settle = (result) => {
|
||||
if (settled)
|
||||
return
|
||||
|
||||
settled = true
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
handlers: new Map(),
|
||||
callManifests: new Map(),
|
||||
settle,
|
||||
done,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers handler and keeps cleanup centralized.
|
||||
*
|
||||
* Notice:
|
||||
* - Avoid duplicated Map allocations
|
||||
* - Removes manifest automatically once empty
|
||||
*/
|
||||
function registerHandler<TPayload extends Record<string, unknown>>(
|
||||
container: Pick<StreamingControlTurnState, 'handlers' | 'callManifests'>,
|
||||
manifest: LlmStreamingControlCallManifest,
|
||||
handler: LlmStreamingControlCallHandler<TPayload>,
|
||||
) {
|
||||
const normalized = normalizeManifest(manifest)
|
||||
|
||||
if (!normalized)
|
||||
return () => undefined
|
||||
|
||||
let set = container.handlers.get(normalized.name)
|
||||
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
container.handlers.set(normalized.name, set)
|
||||
}
|
||||
|
||||
container.callManifests.set(normalized.name, normalized)
|
||||
|
||||
set.add(handler as LlmStreamingControlCallHandler)
|
||||
|
||||
return () => {
|
||||
set!.delete(handler)
|
||||
|
||||
if (set!.size === 0) {
|
||||
container.handlers.delete(normalized.name)
|
||||
container.callManifests.delete(normalized.name)
|
||||
}
|
||||
}
|
||||
handlers: Map<string, Set<LlmStreamingControlCallHandler>>
|
||||
settle: (result: LlmStreamingControlTurnDone) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,11 +86,22 @@ export function createStreamingControlParser(
|
||||
function createTurnApi(
|
||||
turnId: string,
|
||||
turn: StreamingControlTurnState,
|
||||
) {
|
||||
): LlmStreamingControlTurn {
|
||||
return {
|
||||
turnId,
|
||||
cancel() {
|
||||
finalizeTurn(turnId, 'cancelled')
|
||||
},
|
||||
|
||||
on<TPayload extends Record<string, unknown> = Record<string, unknown>>(manifest, handler) {
|
||||
complete() {
|
||||
finalizeTurn(turnId, 'completed')
|
||||
},
|
||||
|
||||
done: turn.done,
|
||||
|
||||
on<TPayload extends Record<string, unknown> = Record<string, unknown>>(
|
||||
manifest: LlmStreamingControlCallManifest,
|
||||
handler: LlmStreamingControlCallHandler<TPayload>,
|
||||
) {
|
||||
return registerHandler<TPayload>(turn, manifest, handler)
|
||||
},
|
||||
|
||||
@@ -237,21 +111,35 @@ export function createStreamingControlParser(
|
||||
)
|
||||
},
|
||||
|
||||
complete() {
|
||||
finalizeTurn(turnId, 'completed')
|
||||
},
|
||||
|
||||
cancel() {
|
||||
finalizeTurn(turnId, 'cancelled')
|
||||
},
|
||||
|
||||
done: turn.done,
|
||||
turnId,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
match(input) {
|
||||
return !!findParser(input)
|
||||
beginTurn(options) {
|
||||
// crypto UUID avoids collision under concurrency
|
||||
const turnId
|
||||
= options?.turnId?.trim()
|
||||
|| createTurnId()
|
||||
|
||||
const existing = turns.get(turnId)
|
||||
|
||||
if (existing)
|
||||
return createTurnApi(turnId, existing)
|
||||
|
||||
const turn = createTurnState()
|
||||
|
||||
turns.set(turnId, turn)
|
||||
|
||||
return createTurnApi(turnId, turn)
|
||||
},
|
||||
|
||||
cancelTurn(turnId) {
|
||||
finalizeTurn(turnId, 'cancelled')
|
||||
},
|
||||
|
||||
completeTurn(turnId) {
|
||||
finalizeTurn(turnId, 'completed')
|
||||
},
|
||||
|
||||
async dispatchWith(special, context) {
|
||||
@@ -259,8 +147,8 @@ export function createStreamingControlParser(
|
||||
|
||||
if (!parser) {
|
||||
emit(context, {
|
||||
type: 'rejected',
|
||||
reason: 'no-matching-parser',
|
||||
type: 'rejected',
|
||||
})
|
||||
|
||||
return false
|
||||
@@ -270,23 +158,23 @@ export function createStreamingControlParser(
|
||||
|
||||
if (!parsed) {
|
||||
emit(context, {
|
||||
type: 'rejected',
|
||||
reason: 'parse-failed',
|
||||
parserName: parser.name,
|
||||
reason: 'parse-failed',
|
||||
type: 'rejected',
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
emit(context, {
|
||||
type: 'parsed',
|
||||
parserName: parser.name,
|
||||
tokenType: parsed.type,
|
||||
callName:
|
||||
parsed.type === 'call'
|
||||
? parsed.name
|
||||
: undefined,
|
||||
parameter: parsedParameter(parsed),
|
||||
parserName: parser.name,
|
||||
tokenType: parsed.type,
|
||||
type: 'parsed',
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -306,9 +194,9 @@ export function createStreamingControlParser(
|
||||
}
|
||||
catch (error) {
|
||||
emit(context, {
|
||||
type: 'signal-handler-error',
|
||||
tokenType: parsed.type,
|
||||
error,
|
||||
tokenType: parsed.type,
|
||||
type: 'signal-handler-error',
|
||||
})
|
||||
|
||||
console.warn(
|
||||
@@ -337,15 +225,15 @@ export function createStreamingControlParser(
|
||||
]
|
||||
|
||||
emit(context, {
|
||||
type: 'call-handler-count',
|
||||
count: registeredHandlers.length,
|
||||
type: 'call-handler-count',
|
||||
})
|
||||
|
||||
if (!registeredHandlers.length) {
|
||||
emit(context, {
|
||||
type: 'call-handler-missing',
|
||||
callName: parsed.name,
|
||||
payload: parsed.payload,
|
||||
type: 'call-handler-missing',
|
||||
})
|
||||
|
||||
return true
|
||||
@@ -356,8 +244,8 @@ export function createStreamingControlParser(
|
||||
for (const handler of registeredHandlers) {
|
||||
try {
|
||||
emit(context, {
|
||||
type: 'call-handler-start',
|
||||
callName: parsed.name,
|
||||
type: 'call-handler-start',
|
||||
})
|
||||
|
||||
await handler(
|
||||
@@ -366,15 +254,15 @@ export function createStreamingControlParser(
|
||||
)
|
||||
|
||||
emit(context, {
|
||||
type: 'call-handler-end',
|
||||
callName: parsed.name,
|
||||
type: 'call-handler-end',
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
emit(context, {
|
||||
type: 'call-handler-error',
|
||||
callName: parsed.name,
|
||||
error,
|
||||
type: 'call-handler-error',
|
||||
})
|
||||
|
||||
console.warn(
|
||||
@@ -387,23 +275,21 @@ export function createStreamingControlParser(
|
||||
return true
|
||||
},
|
||||
|
||||
match(input) {
|
||||
return !!findParser(input)
|
||||
},
|
||||
|
||||
on(manifest, handler) {
|
||||
return registerHandler(
|
||||
{
|
||||
handlers,
|
||||
callManifests,
|
||||
handlers,
|
||||
},
|
||||
manifest,
|
||||
handler,
|
||||
)
|
||||
},
|
||||
|
||||
renderManifestPrompt() {
|
||||
return renderCallManifestPrompt(
|
||||
[...callManifests.values()],
|
||||
)
|
||||
},
|
||||
|
||||
onSignal(handler) {
|
||||
signalHandlers.add(handler)
|
||||
|
||||
@@ -412,30 +298,160 @@ export function createStreamingControlParser(
|
||||
}
|
||||
},
|
||||
|
||||
beginTurn(options) {
|
||||
// crypto UUID avoids collision under concurrency
|
||||
const turnId
|
||||
= options?.turnId?.trim()
|
||||
|| createTurnId()
|
||||
|
||||
const existing = turns.get(turnId)
|
||||
|
||||
if (existing)
|
||||
return createTurnApi(turnId, existing)
|
||||
|
||||
const turn = createTurnState()
|
||||
|
||||
turns.set(turnId, turn)
|
||||
|
||||
return createTurnApi(turnId, turn)
|
||||
},
|
||||
|
||||
completeTurn(turnId) {
|
||||
finalizeTurn(turnId, 'completed')
|
||||
},
|
||||
|
||||
cancelTurn(turnId) {
|
||||
finalizeTurn(turnId, 'cancelled')
|
||||
renderManifestPrompt() {
|
||||
return renderCallManifestPrompt(
|
||||
[...callManifests.values()],
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createTurnId() {
|
||||
return `turn:${
|
||||
globalThis.crypto?.randomUUID?.()
|
||||
?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates isolated turn state.
|
||||
*
|
||||
* Notice:
|
||||
* - Promise resolves once only
|
||||
* - Prevents accidental double completion
|
||||
*/
|
||||
function createTurnState(): StreamingControlTurnState {
|
||||
let settled = false
|
||||
let settle!: (result: LlmStreamingControlTurnDone) => void
|
||||
|
||||
const done = new Promise<LlmStreamingControlTurnDone>((resolve) => {
|
||||
settle = (result) => {
|
||||
if (settled)
|
||||
return
|
||||
|
||||
settled = true
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
callManifests: new Map(),
|
||||
done,
|
||||
handlers: new Map(),
|
||||
settle,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits observer events safely.
|
||||
*
|
||||
* Notice:
|
||||
* - Observer failures must never break dispatch
|
||||
*/
|
||||
function emit(
|
||||
context: Pick<LlmStreamingControlCallContext, 'observer'> | undefined,
|
||||
payload: Parameters<NonNullable<LlmStreamingControlCallContext['observer']>>[0],
|
||||
) {
|
||||
try {
|
||||
context?.observer?.(payload)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes manifest values before registration.
|
||||
*
|
||||
* Notice:
|
||||
* - Empty names/prompts are rejected
|
||||
* - Prevents duplicated trim logic
|
||||
*/
|
||||
function normalizeManifest(
|
||||
manifest: LlmStreamingControlCallManifest,
|
||||
): LlmStreamingControlCallManifest | undefined {
|
||||
const name = manifest.name.trim()
|
||||
const prompt = manifest.prompt.trim()
|
||||
|
||||
if (!name || !prompt)
|
||||
return
|
||||
|
||||
return {
|
||||
...manifest,
|
||||
name,
|
||||
prompt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts parsed signal payload into observer-friendly text.
|
||||
*
|
||||
* Use when:
|
||||
* - Observer logs need a compact human-readable parameter
|
||||
*
|
||||
* Notice:
|
||||
* - Intentionally serializes payloads once
|
||||
* - Returns undefined for empty CALL payload
|
||||
*/
|
||||
function parsedParameter(signal: LlmStreamingControlSignal): string | undefined {
|
||||
switch (signal.type) {
|
||||
case 'act':
|
||||
return JSON.stringify(signal.payload)
|
||||
|
||||
case 'call':
|
||||
return signal.payload != null
|
||||
? JSON.stringify(signal.payload)
|
||||
: undefined
|
||||
|
||||
case 'delay':
|
||||
return `${signal.seconds}s`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers one CALL handler and returns its disposer.
|
||||
*
|
||||
* Triggering workflow:
|
||||
*
|
||||
* {@link createStreamingControlParser}
|
||||
* -> `LlmStreamingControl.on` or `LlmStreamingControlTurn.on`
|
||||
* -> {@link registerHandler}
|
||||
*
|
||||
* Upstream:
|
||||
* - {@link LlmStreamingControl}
|
||||
* - {@link LlmStreamingControlTurn}
|
||||
*
|
||||
* Downstream:
|
||||
* - {@link StreamingControlTurnState} handler and manifest registries
|
||||
*/
|
||||
function registerHandler<TPayload extends Record<string, unknown>>(
|
||||
container: Pick<StreamingControlTurnState, 'callManifests' | 'handlers'>,
|
||||
manifest: LlmStreamingControlCallManifest,
|
||||
handler: LlmStreamingControlCallHandler<TPayload>,
|
||||
) {
|
||||
const normalized = normalizeManifest(manifest)
|
||||
|
||||
if (!normalized)
|
||||
return () => undefined
|
||||
|
||||
let registeredHandlers = container.handlers.get(normalized.name)
|
||||
|
||||
if (!registeredHandlers) {
|
||||
registeredHandlers = new Set()
|
||||
container.handlers.set(normalized.name, registeredHandlers)
|
||||
}
|
||||
|
||||
container.callManifests.set(normalized.name, normalized)
|
||||
|
||||
// The registration API associates each payload type with one opaque CALL name.
|
||||
// The runtime registry erases that relation before it looks up the name.
|
||||
const registeredHandler = handler as LlmStreamingControlCallHandler
|
||||
registeredHandlers.add(registeredHandler)
|
||||
|
||||
return () => {
|
||||
registeredHandlers.delete(registeredHandler)
|
||||
|
||||
if (registeredHandlers.size === 0) {
|
||||
container.handlers.delete(normalized.name)
|
||||
container.callManifests.delete(normalized.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ModulePermissionDeclaration as ProtocolModulePermissionDeclaration,
|
||||
ModulePermissionGrant as ProtocolModulePermissionGrant,
|
||||
} from '@proj-airi/plugin-protocol/types'
|
||||
import type { GenericSchema } from 'valibot'
|
||||
|
||||
import type { KitDescriptor } from './kits'
|
||||
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
lazy,
|
||||
literal,
|
||||
minValue,
|
||||
null_,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
@@ -110,12 +112,12 @@ export interface HostDataRecord {
|
||||
* - The recursive union used across shared host data structures
|
||||
*/
|
||||
export type HostDataValue
|
||||
= | null
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
= | boolean
|
||||
| HostDataArray
|
||||
| HostDataRecord
|
||||
| null
|
||||
| number
|
||||
| string
|
||||
|
||||
/**
|
||||
* Creates the recursive Valibot schema used for one {@link HostDataValue}.
|
||||
@@ -129,14 +131,14 @@ export type HostDataValue
|
||||
* Returns:
|
||||
* - A Valibot schema covering the full `HostDataValue` recursion
|
||||
*/
|
||||
export function createHostDataValueSchema() {
|
||||
export function createHostDataValueSchema(): GenericSchema<HostDataValue> {
|
||||
return union([
|
||||
literal(null),
|
||||
null_(),
|
||||
string(),
|
||||
boolean(),
|
||||
pipe(number(), finite()),
|
||||
array(lazy(createHostDataValueSchema)),
|
||||
pipe(record(string(), lazy(createHostDataValueSchema)), check(isPlainObject)),
|
||||
pipe(record(string(), lazy(createHostDataValueSchema)), check<HostDataRecord>(isPlainObject)),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -166,7 +168,10 @@ export const hostDataValueSchema = createHostDataValueSchema()
|
||||
* Returns:
|
||||
* - A Valibot schema for one host-safe record
|
||||
*/
|
||||
export const hostDataRecordSchema = pipe(record(string(), lazy(createHostDataValueSchema)), check(isPlainObject))
|
||||
export const hostDataRecordSchema = pipe(
|
||||
record(string(), lazy(createHostDataValueSchema)),
|
||||
check<HostDataRecord>(isPlainObject),
|
||||
)
|
||||
|
||||
/**
|
||||
* Validates one non-negative safe integer used for timestamps and revisions.
|
||||
@@ -196,6 +201,34 @@ export const nonNegativeIntegerSchema = pipe(number(), safeInteger(), minValue(0
|
||||
*/
|
||||
export type ExtensionIdentity = ProtocolExtensionIdentity
|
||||
|
||||
/**
|
||||
* Describes a version-1 extension manifest consumed by `ExtensionHost`.
|
||||
*
|
||||
* Extension manifests are the install/session-level package description. Module
|
||||
* registration happens later during `defineExtension({ setup })`.
|
||||
*/
|
||||
export interface ExtensionManifestV1 {
|
||||
/** Manifest schema version expected by the current host implementation. */
|
||||
apiVersion: 'v1'
|
||||
/** Runtime-specific extension entrypoints that the host can resolve and import. */
|
||||
entrypoints: {
|
||||
/** Fallback entrypoint used when no runtime-specific path is provided. */
|
||||
default?: string
|
||||
/** Electron-specific entrypoint path. */
|
||||
electron?: string
|
||||
/** Node-specific entrypoint path. */
|
||||
node?: string
|
||||
/** Web-specific entrypoint path. */
|
||||
web?: string
|
||||
}
|
||||
/** Stable extension id used for identity generation and display. */
|
||||
id: string
|
||||
/** Manifest kind discriminator used to identify AIRI extension manifests. */
|
||||
kind: 'manifest.extension.airi.moeru.ai'
|
||||
/** Package/session permission ceiling that module permissions are capped by. */
|
||||
permissions: ModulePermissionDeclaration
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-exports the protocol permission declaration model used by manifests and runtime permission flow.
|
||||
*
|
||||
@@ -224,77 +257,49 @@ export type ModulePermissionDeclaration = ProtocolModulePermissionDeclaration
|
||||
*/
|
||||
export type ModulePermissionGrant = ProtocolModulePermissionGrant
|
||||
|
||||
/**
|
||||
* Describes a version-1 extension manifest consumed by `ExtensionHost`.
|
||||
*
|
||||
* Extension manifests are the install/session-level package description. Module
|
||||
* registration happens later during `defineExtension({ setup })`.
|
||||
*/
|
||||
export interface ExtensionManifestV1 {
|
||||
/** Manifest schema version expected by the current host implementation. */
|
||||
apiVersion: 'v1'
|
||||
/** Manifest kind discriminator used to identify AIRI extension manifests. */
|
||||
kind: 'manifest.extension.airi.moeru.ai'
|
||||
/** Stable extension id used for identity generation and display. */
|
||||
id: string
|
||||
/** Package/session permission ceiling that module permissions are capped by. */
|
||||
permissions: ModulePermissionDeclaration
|
||||
/** Runtime-specific extension entrypoints that the host can resolve and import. */
|
||||
entrypoints: {
|
||||
/** Fallback entrypoint used when no runtime-specific path is provided. */
|
||||
default?: string
|
||||
/** Electron-specific entrypoint path. */
|
||||
electron?: string
|
||||
/** Node-specific entrypoint path. */
|
||||
node?: string
|
||||
/** Web-specific entrypoint path. */
|
||||
web?: string
|
||||
}
|
||||
}
|
||||
|
||||
const localizableSchema = union([
|
||||
string(),
|
||||
object({
|
||||
key: string(),
|
||||
fallback: optional(string()),
|
||||
key: string(),
|
||||
params: optional(record(string(), union([string(), number(), boolean()]))),
|
||||
}),
|
||||
])
|
||||
|
||||
const permissionDeclarationSchema = object({
|
||||
apis: optional(array(object({
|
||||
key: string(),
|
||||
actions: array(picklist(['invoke', 'emit'])),
|
||||
reason: optional(localizableSchema),
|
||||
label: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
resources: optional(array(object({
|
||||
key: string(),
|
||||
actions: array(picklist(['read', 'write', 'subscribe'])),
|
||||
reason: optional(localizableSchema),
|
||||
label: optional(localizableSchema),
|
||||
reason: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
capabilities: optional(array(object({
|
||||
key: string(),
|
||||
actions: array(picklist(['wait', 'snapshot'])),
|
||||
reason: optional(localizableSchema),
|
||||
label: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
processors: optional(array(object({
|
||||
key: string(),
|
||||
actions: array(picklist(['register', 'execute', 'manage'])),
|
||||
reason: optional(localizableSchema),
|
||||
label: optional(localizableSchema),
|
||||
reason: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
pipelines: optional(array(object({
|
||||
key: string(),
|
||||
actions: array(picklist(['hook', 'process', 'emit', 'manage'])),
|
||||
reason: optional(localizableSchema),
|
||||
key: string(),
|
||||
label: optional(localizableSchema),
|
||||
reason: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
processors: optional(array(object({
|
||||
actions: array(picklist(['register', 'execute', 'manage'])),
|
||||
key: string(),
|
||||
label: optional(localizableSchema),
|
||||
reason: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
resources: optional(array(object({
|
||||
actions: array(picklist(['read', 'write', 'subscribe'])),
|
||||
key: string(),
|
||||
label: optional(localizableSchema),
|
||||
reason: optional(localizableSchema),
|
||||
required: optional(boolean()),
|
||||
}))),
|
||||
})
|
||||
@@ -321,12 +326,98 @@ const manifestEntrypointsSchema = object({
|
||||
*/
|
||||
export const extensionManifestV1Schema = object({
|
||||
apiVersion: literal('v1'),
|
||||
kind: literal('manifest.extension.airi.moeru.ai'),
|
||||
id: string(),
|
||||
permissions: permissionDeclarationSchema,
|
||||
entrypoints: manifestEntrypointsSchema,
|
||||
id: string(),
|
||||
kind: literal('manifest.extension.airi.moeru.ai'),
|
||||
permissions: permissionDeclarationSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Installs one generic host feature into `ExtensionHost`.
|
||||
*
|
||||
* Use when:
|
||||
* - The host should register kits, resources, capabilities, or runtime-specific behavior
|
||||
*
|
||||
* Expects:
|
||||
* - Installation is idempotent for one host instance
|
||||
* - Contributions keep domain-specific behavior out of the low-level host core
|
||||
*
|
||||
* Returns:
|
||||
* - No value; the contribution mutates the provided install context
|
||||
*/
|
||||
export interface ExtensionHostContribution {
|
||||
install: (context: ExtensionHostInstallContext) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the host-owned registration surface that contributions can use during installation.
|
||||
*
|
||||
* Use when:
|
||||
* - Installing a host feature into `ExtensionHost`
|
||||
* - Registering kits, resources, or capabilities
|
||||
*
|
||||
* Expects:
|
||||
* - Installation happens during `ExtensionHost` construction
|
||||
*
|
||||
* Returns:
|
||||
* - Registration helpers that keep `ExtensionHost` generic while allowing host-specific features
|
||||
*/
|
||||
export interface ExtensionHostInstallContext {
|
||||
announceCapability: (key: string, metadata?: Record<string, unknown>) => void
|
||||
markCapabilityDegraded: (key: string, metadata?: Record<string, unknown>) => void
|
||||
markCapabilityReady: (key: string, metadata?: Record<string, unknown>) => void
|
||||
registerKit: (kit: KitDescriptor) => KitDescriptor
|
||||
setResourceResolver: <T>(key: string, resolver: () => Promise<T> | T) => void
|
||||
setResourceValue: <T>(key: string, value: T) => void
|
||||
unregisterKit: (kitId: string) => KitDescriptor | undefined
|
||||
withdrawCapability: (key: string, metadata?: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures one `ExtensionHost` instance.
|
||||
*
|
||||
* Use when:
|
||||
* - Constructing a host with specific runtime, permission, or contribution behavior
|
||||
*
|
||||
* Expects:
|
||||
* - Omitted fields fall back to the host defaults documented below
|
||||
*
|
||||
* Returns:
|
||||
* - The host bootstrap options consumed by {@link import('../core').ExtensionHost}
|
||||
*/
|
||||
export interface ExtensionHostOptions {
|
||||
/** Installable host features that can register kits, resources, and capabilities. @default [] */
|
||||
contributions?: ExtensionHostContribution[]
|
||||
/** Callback that decides the granted permission set for one extension session. */
|
||||
permissionResolver?: (payload: {
|
||||
identity: ExtensionIdentity
|
||||
manifest: ExtensionManifestV1
|
||||
persisted?: ModulePermissionGrant
|
||||
requested: ModulePermissionDeclaration
|
||||
}) => ModulePermissionGrant | Promise<ModulePermissionGrant>
|
||||
/** Runtime used when callers do not override it per load/start call. @default 'electron' */
|
||||
runtime?: PluginRuntime
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes one permission gate that a contribution-owned session API can enforce.
|
||||
*
|
||||
* Use when:
|
||||
* - A contribution method must check host-granted API, resource, or capability access
|
||||
*
|
||||
* Expects:
|
||||
* - The permission key/action pair matches the manifest permission contract
|
||||
*
|
||||
* Returns:
|
||||
* - The permission request consumed by `ExtensionHost.assertPermission(...)`
|
||||
*/
|
||||
export interface ExtensionHostPermissionRequest {
|
||||
action: string
|
||||
area: 'apis' | 'capabilities' | 'pipelines' | 'processors' | 'resources'
|
||||
key: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures how the host resolves and loads an extension entrypoint.
|
||||
*
|
||||
@@ -346,92 +437,6 @@ export interface ExtensionLoadOptions {
|
||||
runtime?: PluginRuntime
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures one `ExtensionHost` instance.
|
||||
*
|
||||
* Use when:
|
||||
* - Constructing a host with specific runtime, permission, or contribution behavior
|
||||
*
|
||||
* Expects:
|
||||
* - Omitted fields fall back to the host defaults documented below
|
||||
*
|
||||
* Returns:
|
||||
* - The host bootstrap options consumed by {@link import('../core').ExtensionHost}
|
||||
*/
|
||||
export interface ExtensionHostOptions {
|
||||
/** Runtime used when callers do not override it per load/start call. @default 'electron' */
|
||||
runtime?: PluginRuntime
|
||||
/** Callback that decides the granted permission set for one extension session. */
|
||||
permissionResolver?: (payload: {
|
||||
identity: ExtensionIdentity
|
||||
manifest: ExtensionManifestV1
|
||||
requested: ModulePermissionDeclaration
|
||||
persisted?: ModulePermissionGrant
|
||||
}) => ModulePermissionGrant | Promise<ModulePermissionGrant>
|
||||
/** Installable host features that can register kits, resources, and capabilities. @default [] */
|
||||
contributions?: ExtensionHostContribution[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes one permission gate that a contribution-owned session API can enforce.
|
||||
*
|
||||
* Use when:
|
||||
* - A contribution method must check host-granted API, resource, or capability access
|
||||
*
|
||||
* Expects:
|
||||
* - The permission key/action pair matches the manifest permission contract
|
||||
*
|
||||
* Returns:
|
||||
* - The permission request consumed by `ExtensionHost.assertPermission(...)`
|
||||
*/
|
||||
export interface ExtensionHostPermissionRequest {
|
||||
area: 'apis' | 'resources' | 'capabilities' | 'processors' | 'pipelines'
|
||||
action: string
|
||||
key: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the host-owned registration surface that contributions can use during installation.
|
||||
*
|
||||
* Use when:
|
||||
* - Installing a host feature into `ExtensionHost`
|
||||
* - Registering kits, resources, or capabilities
|
||||
*
|
||||
* Expects:
|
||||
* - Installation happens during `ExtensionHost` construction
|
||||
*
|
||||
* Returns:
|
||||
* - Registration helpers that keep `ExtensionHost` generic while allowing host-specific features
|
||||
*/
|
||||
export interface ExtensionHostInstallContext {
|
||||
registerKit: (kit: KitDescriptor) => KitDescriptor
|
||||
unregisterKit: (kitId: string) => KitDescriptor | undefined
|
||||
setResourceResolver: <T>(key: string, resolver: () => Promise<T> | T) => void
|
||||
setResourceValue: <T>(key: string, value: T) => void
|
||||
announceCapability: (key: string, metadata?: Record<string, unknown>) => void
|
||||
markCapabilityReady: (key: string, metadata?: Record<string, unknown>) => void
|
||||
markCapabilityDegraded: (key: string, metadata?: Record<string, unknown>) => void
|
||||
withdrawCapability: (key: string, metadata?: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs one generic host feature into `ExtensionHost`.
|
||||
*
|
||||
* Use when:
|
||||
* - The host should register kits, resources, capabilities, or runtime-specific behavior
|
||||
*
|
||||
* Expects:
|
||||
* - Installation is idempotent for one host instance
|
||||
* - Contributions keep domain-specific behavior out of the low-level host core
|
||||
*
|
||||
* Returns:
|
||||
* - No value; the contribution mutates the provided install context
|
||||
*/
|
||||
export interface ExtensionHostContribution {
|
||||
install: (context: ExtensionHostInstallContext) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures one `ExtensionHost.start(...)` call.
|
||||
*
|
||||
|
||||
+6
-1
@@ -5,9 +5,14 @@ export const disposedSessionIds: string[] = []
|
||||
export default defineExtension({
|
||||
id: 'test-stoppable-extension-entrypoint',
|
||||
async setup(ctx) {
|
||||
const sessionId = ctx.extension.sessionId
|
||||
if (!sessionId) {
|
||||
throw new Error('The plugin host did not provide an extension session ID.')
|
||||
}
|
||||
|
||||
ctx.subscriptions.add({
|
||||
dispose() {
|
||||
disposedSessionIds.push(ctx.extension.sessionId)
|
||||
disposedSessionIds.push(sessionId)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./auth": "./src/auth/index.ts",
|
||||
"./analytics/posthog": "./src/analytics/posthog.ts",
|
||||
"./beat-sync": "./src/beat-sync/index.ts",
|
||||
"./global-shortcut": "./src/global-shortcut/index.ts",
|
||||
"./godot-stage": "./src/godot-stage/index.ts",
|
||||
@@ -36,6 +37,7 @@
|
||||
"@vueuse/core": "catalog:",
|
||||
"gpuu": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"posthog-js": "catalog:",
|
||||
"valibot": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { PostHogConfig } from 'posthog-js'
|
||||
|
||||
function isEnvFlagEnabled(value: string | undefined): boolean {
|
||||
if (value == null)
|
||||
return false
|
||||
|
||||
return /^(?:1|true|t|yes|y|on)$/i.test(value.trim())
|
||||
}
|
||||
|
||||
/** Whether client analytics is enabled for the current Vite build. */
|
||||
export const POSTHOG_ENABLED = isEnvFlagEnabled(import.meta.env.VITE_ENABLE_POSTHOG)
|
||||
|
||||
/** The shared PostHog project key used by every AIRI client surface. */
|
||||
export const POSTHOG_PROJECT_KEY
|
||||
= import.meta.env.VITE_POSTHOG_PROJECT_KEY
|
||||
?? 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9' // cspell:disable-line
|
||||
|
||||
/** Shared PostHog defaults for AIRI single-page applications. */
|
||||
export const DEFAULT_POSTHOG_CONFIG = {
|
||||
api_host: 'https://t.airi.build',
|
||||
// This preset captures page views on history changes. PostHog then also
|
||||
// captures page leaves, which keeps route-level dwell time measurable.
|
||||
defaults: '2025-05-24',
|
||||
person_profiles: 'identified_only',
|
||||
} as const satisfies Partial<PostHogConfig>
|
||||
@@ -245,6 +245,9 @@ function toVector3(value: Vec3) {
|
||||
return new Vector3(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
const hemisphereLightPosition = new Vector3(0, 1, 0)
|
||||
const directionalLightPositionVector = computed(() => toVector3(directionalLightPosition.value))
|
||||
|
||||
function toVec3(value: Vector3): Vec3 {
|
||||
return { x: value.x, y: value.y, z: value.z }
|
||||
}
|
||||
@@ -817,7 +820,7 @@ defineExpose({
|
||||
v-else
|
||||
:color="formatHex(hemisphereSkyColor)"
|
||||
:ground-color="formatHex(hemisphereGroundColor)"
|
||||
:position="[0, 1, 0]"
|
||||
:position="hemisphereLightPosition"
|
||||
:intensity="hemisphereLightIntensity"
|
||||
cast-shadow
|
||||
/>
|
||||
@@ -829,7 +832,7 @@ defineExpose({
|
||||
<TresDirectionalLight
|
||||
ref="dirLightRef"
|
||||
:color="formatHex(directionalLightColor)"
|
||||
:position="[directionalLightPosition.x, directionalLightPosition.y, directionalLightPosition.z]"
|
||||
:position="directionalLightPositionVector"
|
||||
:intensity="directionalLightIntensity"
|
||||
cast-shadow
|
||||
/>
|
||||
|
||||
@@ -18,9 +18,9 @@ const props = defineProps<{
|
||||
zConfig?: AxisConfig
|
||||
}>()
|
||||
|
||||
const x = defineModel('x', { required: false, default: 0 })
|
||||
const y = defineModel('y', { required: false, default: 0 })
|
||||
const z = defineModel('z', { required: false, default: 0 })
|
||||
const x = defineModel<number>('x', { required: false, default: 0 })
|
||||
const y = defineModel<number>('y', { required: false, default: 0 })
|
||||
const z = defineModel<number>('z', { required: false, default: 0 })
|
||||
|
||||
// Dragging state
|
||||
const isDragging = ref<'x' | 'y' | 'z' | undefined>()
|
||||
|
||||
@@ -16,7 +16,7 @@ export function createFadeAnimator(options: CreateAnimatorOptions): Animator {
|
||||
.add(elements, {
|
||||
opacity: [0, 1],
|
||||
...options,
|
||||
delay: (_, i) => options.duration / elements.length * (i + 1),
|
||||
delay: (_: unknown, i = 0) => options.duration / elements.length * (i + 1),
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -12,19 +12,19 @@ export function createFloatAnimator(options: CreateAnimatorOptions): Animator {
|
||||
const timeline = createTimeline({ loop: options.loop })
|
||||
.set(elements, {
|
||||
opacity: 0,
|
||||
rotateZ: 180,
|
||||
translateX: '0.55em',
|
||||
translateY: '1.1em',
|
||||
rotateZ: 180,
|
||||
translateZ: 0,
|
||||
})
|
||||
.add(elements, {
|
||||
opacity: [0, 1],
|
||||
translateY: ['1.1em', 0],
|
||||
translateX: ['0.55em', 0],
|
||||
translateZ: 0,
|
||||
rotateZ: [180, 0],
|
||||
translateX: ['0.55em', 0],
|
||||
translateY: ['1.1em', 0],
|
||||
translateZ: 0,
|
||||
...options,
|
||||
delay: (_, i) => options.duration / elements.length * i,
|
||||
delay: (_: unknown, i = 0) => options.duration / elements.length * i,
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ export function createPopupAnimator(options: CreateAnimatorOptions): Animator {
|
||||
translateY: ['1.1em', 0],
|
||||
translateZ: 0,
|
||||
...options,
|
||||
delay: (_, i) => options.duration / elements.length * i,
|
||||
delay: (_: unknown, i = 0) => options.duration / elements.length * i,
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export function createCutePopupAnimator(options: CreateAnimatorOptions): Animato
|
||||
translateY: ['1.1em', 0],
|
||||
translateZ: 0,
|
||||
...options,
|
||||
delay: (_, i) => options.duration / elements.length * i,
|
||||
delay: (_: unknown, i = 0) => options.duration / elements.length * i,
|
||||
ease: createSpring(),
|
||||
})
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ export function createStackAnimator(options: CreateAnimatorOptions): Animator {
|
||||
translateZ: 0,
|
||||
})
|
||||
.add(elements, {
|
||||
opacity: [0, 1],
|
||||
translateX: [40, 0],
|
||||
translateZ: 0,
|
||||
opacity: [0, 1],
|
||||
...options,
|
||||
delay: (_, i) => options.duration / elements.length * (i + 1),
|
||||
delay: (_: unknown, i = 0) => options.duration / elements.length * (i + 1),
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Span, SpanContext, SpanStatusCode } from '@opentelemetry/api'
|
||||
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||
import type { TimedEvent } from '@opentelemetry/sdk-trace-base/build/esm/TimedEvent'
|
||||
import type { SerializedIOSpan } from '@proj-airi/stage-shared/types/io-trace'
|
||||
|
||||
import { context, trace } from '@opentelemetry/api'
|
||||
@@ -13,27 +12,7 @@ export type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
const TRACER_NAME = 'ai.moeru.airi.io-tracer'
|
||||
const BROADCAST_CHANNEL = 'io-tracer-channel' // TODO: Use simple BroadcastChannel for now
|
||||
|
||||
function serializeSpan(span: ReadableSpan): SerializedIOSpan {
|
||||
const ctx = span.spanContext()
|
||||
const parentCtx = span.parentSpanContext
|
||||
return {
|
||||
traceId: ctx.traceId,
|
||||
spanId: ctx.spanId,
|
||||
parentSpanId: parentCtx?.spanId ?? '',
|
||||
name: span.name,
|
||||
kind: span.kind,
|
||||
startTimeNano: String(hrTimeToNanoseconds(span.startTime)),
|
||||
endTimeNano: span.ended ? String(hrTimeToNanoseconds(span.endTime)) : '0',
|
||||
attributes: { ...span.attributes },
|
||||
events: span.events.map((e: TimedEvent) => ({
|
||||
name: e.name,
|
||||
timeNano: String(hrTimeToNanoseconds(e.time)),
|
||||
attributes: { ...e.attributes },
|
||||
})),
|
||||
status: { code: span.status.code, message: span.status.message ?? '' },
|
||||
ended: span.ended,
|
||||
}
|
||||
}
|
||||
type SpanCallback = (span: ReadableSpan) => void
|
||||
|
||||
export function deserializeSpan(s: SerializedIOSpan): ReadableSpan {
|
||||
const nanoToHr = (nano: string): [number, number] => {
|
||||
@@ -41,42 +20,62 @@ export function deserializeSpan(s: SerializedIOSpan): ReadableSpan {
|
||||
return [Math.floor(n / 1e9), n % 1e9]
|
||||
}
|
||||
const spanCtx: SpanContext = {
|
||||
traceId: s.traceId,
|
||||
isRemote: false,
|
||||
spanId: s.spanId,
|
||||
traceFlags: 1,
|
||||
isRemote: false,
|
||||
traceId: s.traceId,
|
||||
}
|
||||
const parentCtx: SpanContext | undefined = s.parentSpanId
|
||||
? { traceId: s.traceId, spanId: s.parentSpanId, traceFlags: 1, isRemote: false }
|
||||
? { isRemote: false, spanId: s.parentSpanId, traceFlags: 1, traceId: s.traceId }
|
||||
: undefined
|
||||
|
||||
return {
|
||||
name: s.name,
|
||||
kind: s.kind,
|
||||
spanContext: () => spanCtx,
|
||||
parentSpanContext: parentCtx,
|
||||
startTime: nanoToHr(s.startTimeNano),
|
||||
endTime: nanoToHr(s.endTimeNano),
|
||||
status: { code: s.status.code as SpanStatusCode, message: s.status.message },
|
||||
attributes: s.attributes as Record<string, string | number | boolean>,
|
||||
links: [],
|
||||
events: s.events.map(e => ({
|
||||
name: e.name,
|
||||
time: nanoToHr(e.timeNano),
|
||||
attributes: e.attributes as Record<string, string | number | boolean>,
|
||||
droppedAttributesCount: 0,
|
||||
})),
|
||||
duration: nanoToHr(String(Number(s.endTimeNano) - Number(s.startTimeNano))),
|
||||
ended: s.ended,
|
||||
resource: { attributes: {}, merge: () => ({ attributes: {} }) } as any,
|
||||
instrumentationScope: { name: TRACER_NAME },
|
||||
attributes: s.attributes as Record<string, boolean | number | string>,
|
||||
droppedAttributesCount: 0,
|
||||
droppedEventsCount: 0,
|
||||
droppedLinksCount: 0,
|
||||
duration: nanoToHr(String(Number(s.endTimeNano) - Number(s.startTimeNano))),
|
||||
ended: s.ended,
|
||||
endTime: nanoToHr(s.endTimeNano),
|
||||
events: s.events.map(e => ({
|
||||
attributes: e.attributes as Record<string, boolean | number | string>,
|
||||
droppedAttributesCount: 0,
|
||||
name: e.name,
|
||||
time: nanoToHr(e.timeNano),
|
||||
})),
|
||||
instrumentationScope: { name: TRACER_NAME },
|
||||
kind: s.kind,
|
||||
links: [],
|
||||
name: s.name,
|
||||
parentSpanContext: parentCtx,
|
||||
resource: { attributes: {}, merge: () => ({ attributes: {} }) } as any,
|
||||
spanContext: () => spanCtx,
|
||||
startTime: nanoToHr(s.startTimeNano),
|
||||
status: { code: s.status.code as SpanStatusCode, message: s.status.message },
|
||||
}
|
||||
}
|
||||
|
||||
type SpanCallback = (span: ReadableSpan) => void
|
||||
function serializeSpan(span: ReadableSpan): SerializedIOSpan {
|
||||
const ctx = span.spanContext()
|
||||
const parentCtx = span.parentSpanContext
|
||||
return {
|
||||
attributes: { ...span.attributes },
|
||||
ended: span.ended,
|
||||
endTimeNano: span.ended ? String(hrTimeToNanoseconds(span.endTime)) : '0',
|
||||
events: span.events.map(e => ({
|
||||
attributes: { ...e.attributes },
|
||||
name: e.name,
|
||||
timeNano: String(hrTimeToNanoseconds(e.time)),
|
||||
})),
|
||||
kind: span.kind,
|
||||
name: span.name,
|
||||
parentSpanId: parentCtx?.spanId ?? '',
|
||||
spanId: ctx.spanId,
|
||||
startTimeNano: String(hrTimeToNanoseconds(span.startTime)),
|
||||
status: { code: span.status.code, message: span.status.message ?? '' },
|
||||
traceId: ctx.traceId,
|
||||
}
|
||||
}
|
||||
|
||||
let provider: BasicTracerProvider | undefined
|
||||
let spanCallback: SpanCallback | undefined
|
||||
@@ -89,17 +88,23 @@ export function createCallbackSpanExporter(): SpanExporter {
|
||||
spanCallback?.(span)
|
||||
|
||||
broadcastChannel?.postMessage({
|
||||
type: 'span',
|
||||
span: serializeSpan(span),
|
||||
type: 'span',
|
||||
})
|
||||
}
|
||||
resultCallback({ code: 0 /* SUCCESS */ })
|
||||
},
|
||||
shutdown: () => Promise.resolve(),
|
||||
forceFlush: () => Promise.resolve(),
|
||||
shutdown: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
export function getIOTracer() {
|
||||
if (provider)
|
||||
return provider.getTracer(TRACER_NAME)
|
||||
return trace.getTracer(TRACER_NAME)
|
||||
}
|
||||
|
||||
export function initIOTracer() {
|
||||
if (!broadcastChannel)
|
||||
broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL)
|
||||
@@ -113,12 +118,6 @@ export function initIOTracer() {
|
||||
trace.setGlobalTracerProvider(provider)
|
||||
}
|
||||
|
||||
export function getIOTracer() {
|
||||
if (provider)
|
||||
return provider.getTracer(TRACER_NAME)
|
||||
return trace.getTracer(TRACER_NAME)
|
||||
}
|
||||
|
||||
export function onIOSpan(cb: SpanCallback | undefined) {
|
||||
spanCallback = cb
|
||||
}
|
||||
@@ -137,7 +136,7 @@ export function onRemoteIOSpan(cb: SpanCallback): () => void {
|
||||
}
|
||||
}
|
||||
|
||||
export function startSpan(name: string, parent?: Span, attrs?: Record<string, string | number | boolean>): Span {
|
||||
export function startSpan(name: string, parent?: Span, attrs?: Record<string, boolean | number | string>): Span {
|
||||
initIOTracer()
|
||||
|
||||
const tracer = getIOTracer()
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('@proj-airi/stage-shared', () => ({
|
||||
isStageTamagotchi: () => false,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../../posthog.config', () => ({
|
||||
vi.mock('@proj-airi/stage-shared/analytics/posthog', () => ({
|
||||
DEFAULT_POSTHOG_CONFIG: {},
|
||||
POSTHOG_PROJECT_KEY: 'test-project-key',
|
||||
}))
|
||||
|
||||
@@ -4,19 +4,10 @@ import type { AnalyticsAdapter, AnalyticsAdapterOptions } from './client'
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
import { isStageCapacitor, isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
|
||||
import {
|
||||
DEFAULT_POSTHOG_CONFIG,
|
||||
POSTHOG_PROJECT_KEY,
|
||||
} from '../../../../../posthog.config'
|
||||
|
||||
function currentSurface(): 'web' | 'mobile' | 'electron' {
|
||||
if (isStageTamagotchi())
|
||||
return 'electron'
|
||||
if (isStageCapacitor())
|
||||
return 'mobile'
|
||||
return 'web'
|
||||
}
|
||||
} from '@proj-airi/stage-shared/analytics/posthog'
|
||||
|
||||
/** Creates and initializes the default PostHog adapter. */
|
||||
export function createPosthogAdapter(options: AnalyticsAdapterOptions): AnalyticsAdapter {
|
||||
@@ -56,10 +47,10 @@ export function createPosthogAdapter(options: AnalyticsAdapterOptions): Analytic
|
||||
},
|
||||
registerBuildInfo(buildInfo: AboutBuildInfo) {
|
||||
posthog.register({
|
||||
app_version: (buildInfo.version && buildInfo.version !== '0.0.0') ? buildInfo.version : 'dev',
|
||||
app_commit: buildInfo.commit,
|
||||
app_branch: buildInfo.branch,
|
||||
app_build_time: buildInfo.builtOn,
|
||||
app_commit: buildInfo.commit,
|
||||
app_version: (buildInfo.version && buildInfo.version !== '0.0.0') ? buildInfo.version : 'dev',
|
||||
})
|
||||
},
|
||||
resetIdentity() {
|
||||
@@ -78,3 +69,11 @@ export function createPosthogAdapter(options: AnalyticsAdapterOptions): Analytic
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function currentSurface(): 'electron' | 'mobile' | 'web' {
|
||||
if (isStageTamagotchi())
|
||||
return 'electron'
|
||||
if (isStageCapacitor())
|
||||
return 'mobile'
|
||||
return 'web'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Ref } from 'vue'
|
||||
|
||||
import { Rive } from '@rive-app/canvas-lite'
|
||||
import { breakpointsTailwind, useBreakpoints, useDark } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import CircleFadeInAnimation from './assets/circle_blink_in_-_loading_(@proj-airi).riv'
|
||||
import CRT from './CRT.vue'
|
||||
@@ -243,9 +243,9 @@ const bootMessages = computed<BootMessage[]>(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const riveCanvas = ref<HTMLCanvasElement>()
|
||||
const riveCanvas = useTemplateRef<HTMLCanvasElement>('riveCanvas')
|
||||
const rive = ref<Rive>()
|
||||
const crtRef = ref<InstanceType<typeof CRT>>()
|
||||
const crtRef = useTemplateRef<InstanceType<typeof CRT>>('crtRef')
|
||||
|
||||
const isDark = useDark()
|
||||
|
||||
@@ -271,8 +271,9 @@ function handleUpdateDone(value: boolean) {
|
||||
|
||||
// Method to update typing speed for a specific message
|
||||
function setMessageTypingSpeed(index: number, speed: number) {
|
||||
if (index >= 0 && index < bootMessages.value.length && speed > 0) {
|
||||
bootMessages[index].typingSpeed = speed
|
||||
const message = bootMessages.value[index]
|
||||
if (message && speed > 0) {
|
||||
message.typingSpeed = speed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,12 +377,16 @@ async function writeLine<T extends any[]>(
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
riveCanvas.value.width = Math.max(window.innerWidth, 500) * 2
|
||||
riveCanvas.value.height = Math.max(window.innerWidth, 500) * 2
|
||||
const canvas = riveCanvas.value
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
canvas.width = Math.max(window.innerWidth, 500) * 2
|
||||
canvas.height = Math.max(window.innerWidth, 500) * 2
|
||||
|
||||
rive.value = new Rive({
|
||||
src: CircleFadeInAnimation,
|
||||
canvas: riveCanvas.value,
|
||||
canvas,
|
||||
autoplay: true,
|
||||
artboard: isDark.value ? 'Bold' : 'Bold (Light)',
|
||||
})
|
||||
@@ -398,10 +403,14 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
const canvas = riveCanvas.value
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
rive.value?.cleanup()
|
||||
rive.value = new Rive({
|
||||
src: CircleFadeInAnimation,
|
||||
canvas: riveCanvas.value,
|
||||
canvas,
|
||||
autoplay: true,
|
||||
artboard: isDark.value ? 'Bold' : 'Bold (Light)',
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
import StageTransitionGroup from '../../src/components/StageTransitionGroup.vue'
|
||||
|
||||
Generated
+9335
-11118
File diff suppressed because it is too large
Load Diff
+246
-241
@@ -1,9 +1,11 @@
|
||||
catalogMode: prefer
|
||||
minimumReleaseAge: 4320
|
||||
minimumReleaseAgeExclude:
|
||||
- '@moeru/*'
|
||||
- '@proj-airi/*'
|
||||
- '@vishot/*'
|
||||
- '@xsai/*'
|
||||
- '@xsai-apple-speech/*'
|
||||
- '@xsai-ext/*'
|
||||
- '@xsai-transformers/*'
|
||||
|
||||
@@ -21,12 +23,14 @@ packages:
|
||||
- '!**/dist/**'
|
||||
|
||||
overrides:
|
||||
'@types/hast': 'catalog:'
|
||||
array-flatten: npm:@nolyfill/array-flatten@^1.0.44
|
||||
axios: npm:feaxios@^0.0.23
|
||||
hono: 4.13.4
|
||||
'eslint-plugin-sonarjs>typescript': 'catalog:'
|
||||
hono: 4.13.3
|
||||
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
|
||||
isarray: npm:@nolyfill/isarray@^1.0.44
|
||||
onnxruntime-web: npm:onnxruntime-web@^1.24.3
|
||||
onnxruntime-web: npm:onnxruntime-web@^1.27.0
|
||||
safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44
|
||||
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
|
||||
side-channel: npm:@nolyfill/side-channel@^1.0.44
|
||||
@@ -39,27 +43,28 @@ patchedDependencies:
|
||||
uiohook-napi@1.5.5: patches/uiohook-napi@1.5.5.patch
|
||||
catalog:
|
||||
'@alexanderolsen/libsamplerate-js': ^2.1.2
|
||||
'@alint-js/cli': ^0.4.0
|
||||
'@alint-js/plugin-js': ^0.4.0
|
||||
'@antfu/eslint-config': ^8.2.0
|
||||
'@anthropic-ai/claude-code': ^2.1.204
|
||||
'@arethetypeswrong/core': ^0.18.2
|
||||
'@ax-llm/ax': ^19.0.43
|
||||
'@alint-js/cli': ^0.6.0
|
||||
'@alint-js/plugin-js': ^0.6.0
|
||||
'@antfu/eslint-config': 8.2.0
|
||||
'@anthropic-ai/claude-code': ^2.1.241
|
||||
'@arethetypeswrong/core': ^0.18.5
|
||||
'@ax-llm/ax': ^24.0.5
|
||||
'@better-auth/cli': ^1.4.22
|
||||
'@better-auth/drizzle-adapter': ^1.6.25
|
||||
'@better-auth/drizzle-adapter': 1.6.25
|
||||
'@better-auth/oauth-provider': 1.6.25
|
||||
'@better-fetch/fetch': ^1.3.1
|
||||
'@capacitor/android': ^8.3.1
|
||||
'@capacitor/app': ^8.1.0
|
||||
'@capacitor/barcode-scanner': ^3.0.2
|
||||
'@capacitor/cli': ^8.3.1
|
||||
'@capacitor/core': ^8.3.1
|
||||
'@capacitor/ios': ^8.3.1
|
||||
'@capacitor/local-notifications': ^8.0.2
|
||||
'@capacitor/android': ^8.5.0
|
||||
'@capacitor/app': ^8.1.1
|
||||
'@capacitor/barcode-scanner': ^3.1.1
|
||||
'@capacitor/cli': ^8.5.0
|
||||
'@capacitor/core': ^8.5.0
|
||||
'@capacitor/ios': ^8.5.0
|
||||
'@capacitor/local-notifications': ^8.3.1
|
||||
'@date-fns/utc': ^2.1.1
|
||||
'@discordjs/voice': ^0.19.2
|
||||
'@dotenvx/dotenvx': ^1.61.1
|
||||
'@electric-sql/pglite': ^0.4.4
|
||||
'@dotenvx/dotenvx': ^2.21.0
|
||||
'@electric-sql/pglite': ^0.5.6
|
||||
'@electric-sql/pglite-pgvector': 0.0.7
|
||||
'@electron-toolkit/eslint-config-ts': ^3.1.0
|
||||
'@electron-toolkit/preload': ^3.0.2
|
||||
'@electron-toolkit/tsconfig': ^2.0.0
|
||||
@@ -69,82 +74,82 @@ catalog:
|
||||
'@esotericsoftware/spine-webgl-4-0': npm:@esotericsoftware/spine-webgl@~4.0.31
|
||||
'@esotericsoftware/spine-webgl-4-1': npm:@esotericsoftware/spine-webgl@~4.1.56
|
||||
'@ffmpeg-installer/ffmpeg': ^1.1.0
|
||||
'@fontsource-variable/comfortaa': ^5.2.8
|
||||
'@fontsource-variable/dm-sans': ^5.2.8
|
||||
'@fontsource-variable/jura': ^5.2.7
|
||||
'@fontsource-variable/nunito': ^5.2.7
|
||||
'@fontsource-variable/quicksand': ^5.2.10
|
||||
'@fontsource-variable/urbanist': ^5.2.7
|
||||
'@fontsource/dm-mono': ^5.2.7
|
||||
'@fontsource/dm-serif-display': ^5.2.8
|
||||
'@fontsource/gugi': ^5.2.7
|
||||
'@fontsource/kiwi-maru': ^5.2.8
|
||||
'@fontsource/m-plus-rounded-1c': ^5.2.10
|
||||
'@formkit/auto-animate': ^0.9.0
|
||||
'@fontsource-variable/comfortaa': ^5.3.0
|
||||
'@fontsource-variable/dm-sans': ^5.3.0
|
||||
'@fontsource-variable/jura': ^5.3.0
|
||||
'@fontsource-variable/nunito': ^5.3.0
|
||||
'@fontsource-variable/quicksand': ^5.3.0
|
||||
'@fontsource-variable/urbanist': ^5.3.0
|
||||
'@fontsource/dm-mono': ^5.3.0
|
||||
'@fontsource/dm-serif-display': ^5.3.0
|
||||
'@fontsource/gugi': ^5.3.0
|
||||
'@fontsource/kiwi-maru': ^5.3.0
|
||||
'@fontsource/m-plus-rounded-1c': ^5.3.0
|
||||
'@formkit/auto-animate': ^0.10.0
|
||||
'@grammyjs/files': ^1.2.0
|
||||
'@guiiai/logg': ^1.2.11
|
||||
'@histoire/plugin-vue': 1.0.0-beta.1
|
||||
'@hono/node-server': ^1.19.14
|
||||
'@hono/node-ws': ^1.3.0
|
||||
'@hono/node-server': ^2.1.1
|
||||
'@hono/node-ws': ^1.3.1
|
||||
'@hono/otel': ^1.1.2
|
||||
'@huggingface/transformers': ^3.8.1
|
||||
'@iconify-json/carbon': ^1.2.20
|
||||
'@huggingface/transformers': 3.8.1
|
||||
'@iconify-json/carbon': ^1.2.25
|
||||
'@iconify-json/eos-icons': ^1.2.4
|
||||
'@iconify-json/line-md': ^1.2.16
|
||||
'@iconify-json/logos': ^1.2.11
|
||||
'@iconify-json/lucide': ^1.2.102
|
||||
'@iconify-json/material-symbols': ^1.2.67
|
||||
'@iconify-json/logos': ^1.2.13
|
||||
'@iconify-json/lucide': ^1.2.125
|
||||
'@iconify-json/material-symbols': ^1.2.89
|
||||
'@iconify-json/mdi': ^1.2.3
|
||||
'@iconify-json/mingcute': ^1.2.7
|
||||
'@iconify-json/mingcute': ^1.2.8
|
||||
'@iconify-json/ph': ^1.2.2
|
||||
'@iconify-json/simple-icons': ^1.2.78
|
||||
'@iconify-json/solar': ^1.2.5
|
||||
'@iconify-json/simple-icons': ^1.2.93
|
||||
'@iconify-json/solar': ^1.2.9
|
||||
'@iconify-json/svg-spinners': ^1.2.4
|
||||
'@iconify-json/tabler': ^1.2.33
|
||||
'@iconify-json/tabler': ^1.2.38
|
||||
'@iconify-json/twemoji': ^1.2.5
|
||||
'@iconify-json/vscode-icons': ^1.2.45
|
||||
'@iconify/utils': ^3.1.0
|
||||
'@iconify/vue': ^5.0.0
|
||||
'@intlify/core': ^11.3.2
|
||||
'@intlify/unplugin-vue-i18n': ^11.0.7
|
||||
'@langfuse/otel': ^5.4.0
|
||||
'@langfuse/tracing': ^5.4.0
|
||||
'@laplace.live/event-bridge-sdk': ^1.0.22
|
||||
'@laplace.live/event-types': ^2.0.14
|
||||
'@iconify-json/vscode-icons': ^1.2.74
|
||||
'@iconify/utils': ^3.1.4
|
||||
'@iconify/vue': ^5.0.1
|
||||
'@intlify/core': ^11.4.9
|
||||
'@intlify/unplugin-vue-i18n': ^11.2.5
|
||||
'@langfuse/otel': ^5.10.1
|
||||
'@langfuse/tracing': ^5.10.1
|
||||
'@laplace.live/event-bridge-sdk': ^1.1.0
|
||||
'@laplace.live/event-types': ^2.0.21
|
||||
'@lemonneko/crop-empty-pixels': 0.1.1
|
||||
'@mdit/plugin-footnote': ^0.23.2
|
||||
'@mdit/plugin-tasklist': ^0.23.2
|
||||
'@mediapipe/tasks-vision': ^0.10.34
|
||||
'@modelcontextprotocol/sdk': ^1.29.0
|
||||
'@mdit/plugin-footnote': ^1.1.0
|
||||
'@mdit/plugin-tasklist': ^1.1.0
|
||||
'@mediapipe/tasks-vision': ^1.0.1
|
||||
'@modelcontextprotocol/sdk': ^1.30.0
|
||||
'@moeru/eslint-config': 0.1.0-beta.19
|
||||
'@moeru/eventa': 1.0.0
|
||||
'@moeru/std': 0.1.0-beta.17
|
||||
'@moeru/three-mmd': 0.1.0-beta.7
|
||||
'@moeru/three-mmd-physics-ammo': 0.1.0-beta.7
|
||||
'@napi-rs/image': ^1.12.0
|
||||
'@napi-rs/image': ^1.14.0
|
||||
'@nekopaw/tempora': 0.4.0-alpha.1
|
||||
'@opentelemetry/api': ^1.9.1
|
||||
'@opentelemetry/api-logs': ^0.215.0
|
||||
'@opentelemetry/auto-instrumentations-node': ^0.73.0
|
||||
'@opentelemetry/core': ^2.7.0
|
||||
'@opentelemetry/exporter-logs-otlp-proto': ^0.215.0
|
||||
'@opentelemetry/exporter-metrics-otlp-proto': ^0.215.0
|
||||
'@opentelemetry/exporter-trace-otlp-proto': ^0.215.0
|
||||
'@opentelemetry/instrumentation': ^0.215.0
|
||||
'@opentelemetry/instrumentation-http': ^0.215.0
|
||||
'@opentelemetry/instrumentation-ioredis': ^0.63.0
|
||||
'@opentelemetry/instrumentation-pg': ^0.67.0
|
||||
'@opentelemetry/instrumentation-runtime-node': ^0.28.0
|
||||
'@opentelemetry/instrumentation-undici': ^0.25.0
|
||||
'@opentelemetry/resources': ^2.7.0
|
||||
'@opentelemetry/sdk-logs': ^0.215.0
|
||||
'@opentelemetry/sdk-metrics': ^2.7.0
|
||||
'@opentelemetry/sdk-node': ^0.215.0
|
||||
'@opentelemetry/sdk-trace-base': ^2.7.0
|
||||
'@opentelemetry/sdk-trace-node': ^2.7.0
|
||||
'@opentelemetry/semantic-conventions': ^1.40.0
|
||||
'@pinia/colada': ^1.2.1
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@opentelemetry/api-logs': ^0.221.0
|
||||
'@opentelemetry/auto-instrumentations-node': ^0.79.0
|
||||
'@opentelemetry/core': ^2.10.0
|
||||
'@opentelemetry/exporter-logs-otlp-proto': ^0.221.0
|
||||
'@opentelemetry/exporter-metrics-otlp-proto': ^0.221.0
|
||||
'@opentelemetry/exporter-trace-otlp-proto': ^0.221.0
|
||||
'@opentelemetry/instrumentation': ^0.221.0
|
||||
'@opentelemetry/instrumentation-http': ^0.221.0
|
||||
'@opentelemetry/instrumentation-ioredis': ^0.69.0
|
||||
'@opentelemetry/instrumentation-pg': ^0.73.0
|
||||
'@opentelemetry/instrumentation-runtime-node': ^0.34.0
|
||||
'@opentelemetry/instrumentation-undici': ^0.31.0
|
||||
'@opentelemetry/resources': ^2.10.0
|
||||
'@opentelemetry/sdk-logs': ^0.221.0
|
||||
'@opentelemetry/sdk-metrics': ^2.10.0
|
||||
'@opentelemetry/sdk-node': ^0.221.0
|
||||
'@opentelemetry/sdk-trace-base': ^2.10.0
|
||||
'@opentelemetry/sdk-trace-node': ^2.10.0
|
||||
'@opentelemetry/semantic-conventions': ^1.43.0
|
||||
'@pinia/colada': ^1.4.2
|
||||
'@pinia/testing': ^2.0.1
|
||||
'@pixi/app': ^6.5.10
|
||||
'@pixi/constants': ^6.5.10
|
||||
'@pixi/core': ^6.5.10
|
||||
@@ -158,66 +163,66 @@ catalog:
|
||||
'@pixi/sprite': ^6.5.10
|
||||
'@pixi/ticker': ^6.5.10
|
||||
'@pixi/utils': ^6.5.10
|
||||
'@pixiv/three-vrm': ^3.5.2
|
||||
'@pixiv/three-vrm-animation': ^3.5.2
|
||||
'@pixiv/three-vrm-core': ^3.5.2
|
||||
'@pixiv/three-vrm': ^3.5.5
|
||||
'@pixiv/three-vrm-animation': ^3.5.5
|
||||
'@pixiv/three-vrm-core': ^3.5.5
|
||||
'@pnpm/find-workspace-dir': ^1000.1.5
|
||||
'@proj-airi/chromatic': ^1.1.1
|
||||
'@proj-airi/drizzle-duckdb-wasm': ^0.5.0
|
||||
'@proj-airi/chromatic': ^1.1.4
|
||||
'@proj-airi/drizzle-duckdb-wasm': ^0.6.0
|
||||
'@proj-airi/iconify-meteocons': ^0.1.5
|
||||
'@proj-airi/lobe-icons': ^1.0.19
|
||||
'@proj-airi/unocss-preset-chromatic': ^1.1.1
|
||||
'@proj-airi/unocss-preset-chromatic': ^1.1.4
|
||||
'@proj-airi/unplugin-fetch': ^0.2.3
|
||||
'@proj-airi/unplugin-live2d-sdk': ^0.1.7
|
||||
'@radix-ui/colors': ^3.0.0
|
||||
'@ricky0123/vad-web': ^0.0.30
|
||||
'@rive-app/canvas-lite': ^2.37.2
|
||||
'@shikijs/markdown-it': ^4.0.2
|
||||
'@shikijs/rehype': ^4.0.2
|
||||
'@rive-app/canvas-lite': ^2.40.1
|
||||
'@shikijs/markdown-it': ^4.4.3
|
||||
'@shikijs/rehype': ^4.4.3
|
||||
'@shopify/draggable': ^1.2.1
|
||||
'@snazzah/davey': ^0.1.11
|
||||
'@snazzah/davey': ^0.1.12
|
||||
'@takumi-rs/image-response': 1.0.0-beta.20
|
||||
'@tresjs/cientos': ^5.7.0
|
||||
'@tresjs/core': ^5.8.0
|
||||
'@tresjs/post-processing': ^3.7.1
|
||||
'@types/audioworklet': ^0.0.97
|
||||
'@tresjs/cientos': ^5.8.1
|
||||
'@tresjs/core': ^5.8.3
|
||||
'@tresjs/post-processing': ^3.7.4
|
||||
'@types/audioworklet': ^0.0.100
|
||||
'@types/culori': ^4.0.1
|
||||
'@types/d3': ^7.4.3
|
||||
'@types/hast': ^3.0.4
|
||||
'@types/hast': ^3.0.5
|
||||
'@types/ioredis-mock': ^8.2.8
|
||||
'@types/markdown-it': ^14.1.2
|
||||
'@types/markdown-it': ^14.2.0
|
||||
'@types/mdast': ^4.0.4
|
||||
'@types/node': ^24.12.2
|
||||
'@types/node': ^26.2.0
|
||||
'@types/nprogress': ^0.2.3
|
||||
'@types/pg': ^8.20.0
|
||||
'@types/semver': ^7.7.1
|
||||
'@types/pg': ^8.23.1
|
||||
'@types/semver': ^7.8.0
|
||||
'@types/splitpanes': ^2.2.6
|
||||
'@types/three': ^0.184.0
|
||||
'@types/three': ^0.185.4
|
||||
'@types/unist': ^3.0.3
|
||||
'@types/vscode': ^1.116.0
|
||||
'@types/vscode': ^1.134.0
|
||||
'@types/whatwg-mimetype': ^5.0.0
|
||||
'@types/ws': ^8.18.1
|
||||
'@types/xast': ^2.0.4
|
||||
'@types/yauzl': ^2.10.3
|
||||
'@unocss/eslint-config': ^66.6.8
|
||||
'@unocss/eslint-plugin': ^66.6.8
|
||||
'@unocss/preset-mini': ^66.6.8
|
||||
'@unocss/preset-web-fonts': ^66.6.8
|
||||
'@unocss/reset': ^66.6.8
|
||||
'@types/yauzl': ^3.4.0
|
||||
'@unocss/eslint-config': 66.7.5
|
||||
'@unocss/eslint-plugin': 66.7.5
|
||||
'@unocss/preset-mini': 66.7.5
|
||||
'@unocss/preset-web-fonts': 66.7.5
|
||||
'@unocss/reset': 66.7.5
|
||||
'@valibot/to-json-schema': 1.0.0-rc.0
|
||||
'@velin-dev/core': ^0.4.0
|
||||
'@vishot/cli': ^0.2.0
|
||||
'@vishot/mockup-desktop-vue': ^0.2.0
|
||||
'@vishot/renderer-browser': ^0.2.0
|
||||
'@vishot/source-electron': ^0.2.0
|
||||
'@vitejs/plugin-vue': ^6.0.6
|
||||
'@vue-macros/volar': ^3.1.2
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vitejs/plugin-vue': ^6.0.8
|
||||
'@vue-macros/volar': ^3.1.4
|
||||
'@vue/test-utils': ^2.4.11
|
||||
'@vue/tsconfig': ^0.9.1
|
||||
'@vueuse/core': ^14.2.1
|
||||
'@vueuse/core': ^14.4.0
|
||||
'@vueuse/motion': ^3.0.3
|
||||
'@vueuse/shared': ^14.2.1
|
||||
'@webgpu/types': ^0.1.69
|
||||
'@vueuse/shared': ^14.4.0
|
||||
'@webgpu/types': ^0.1.72
|
||||
'@wxt-dev/module-vue': ^1.0.3
|
||||
'@xsai-apple-speech/transcription': 0.1.3
|
||||
'@xsai-apple-speech/transcription-electron-plugin': 0.1.3
|
||||
@@ -239,80 +244,82 @@ catalog:
|
||||
'@xsai/stream-transcription': 0.5.0-beta.8
|
||||
'@xsai/tool': 0.5.0-beta.8
|
||||
'@xsai/utils-chat': 0.5.0-beta.8
|
||||
alien-signals: ^3.1.2
|
||||
animejs: ^4.3.6
|
||||
alien-signals: ^3.2.1
|
||||
animejs: ^4.5.0
|
||||
async-mutex: 0.5.0
|
||||
awilix: ^13.0.3
|
||||
best-effort-json-parser: ^1.4.0
|
||||
better-auth: ^1.6.25
|
||||
builder-util-runtime: ^9.5.1
|
||||
bumpp: ^11.0.1
|
||||
awilix: ^13.0.5
|
||||
best-effort-json-parser: ^1.5.1
|
||||
better-auth: 1.6.25
|
||||
builder-util-runtime: ^9.7.0
|
||||
bumpp: ^12.2.1
|
||||
cac: ^7.0.0
|
||||
canvas: ^3.2.3
|
||||
capacitor-native-settings: ^8.1.0
|
||||
capacitor-native-settings: ^8.2.0
|
||||
chess.js: ^1.4.0
|
||||
clustr: ^1.0.2
|
||||
colorjs.io: ^0.6.1
|
||||
crossws: ^0.4.5
|
||||
colorjs.io: ^0.7.1
|
||||
crossws: ^0.4.12
|
||||
csstype: ^3.2.3
|
||||
culori: ^4.0.2
|
||||
d3: 7.9.0
|
||||
date-fns: ^4.1.0
|
||||
date-fns: ^4.4.0
|
||||
debug: ^4.4.3
|
||||
defu: ^6.1.7
|
||||
destr: ^2.0.5
|
||||
discord.js: ^14.26.3
|
||||
dompurify: ^3.4.0
|
||||
driver.js: ^1.4.0
|
||||
discord.js: ^14.27.0
|
||||
dompurify: ^3.4.14
|
||||
driver.js: ^1.8.0
|
||||
drizzle-kit: ^0.31.10
|
||||
drizzle-orm: ^0.45.2
|
||||
drizzle-valibot: ^0.4.2
|
||||
electron: ^41.2.1
|
||||
electron-builder: ^26.8.1
|
||||
electron: ^41.10.6
|
||||
electron-builder: ^26.15.3
|
||||
electron-click-drag-plugin: ^2.0.2
|
||||
electron-updater: ^6.8.3
|
||||
electron-updater: ^6.8.9
|
||||
electron-vite: ^5.0.0
|
||||
embla-carousel-autoplay: 9.0.0-rc01
|
||||
embla-carousel-vue: 9.0.0-rc02
|
||||
es-toolkit: ^1.45.1
|
||||
eslint: ^10.2.1
|
||||
es-toolkit: ^1.51.0
|
||||
eslint: ^10.9.0
|
||||
eslint-plugin-format: ^2.0.1
|
||||
eslint-plugin-oxlint: ^1.60.0
|
||||
eslint-plugin-oxlint: ^1.79.0
|
||||
eslint-plugin-perfectionist: ^5.10.1
|
||||
eventemitter3: ^5.0.4
|
||||
fflate: ^0.8.3
|
||||
floating-vue: ^5.2.2
|
||||
fluent-ffmpeg: ^2.1.3
|
||||
get-port-please: ^3.2.0
|
||||
gpuu: ^1.0.7
|
||||
grammy: ^1.42.0
|
||||
grammy: ^1.45.1
|
||||
gray-matter: ^4.0.3
|
||||
h3: 2.0.1-rc.20
|
||||
h3: 2.0.1-rc.29
|
||||
hfup: ^1.0.4
|
||||
histoire: 1.0.0-beta.1
|
||||
hono: 4.13.4
|
||||
hono: 4.13.3
|
||||
hono-rate-limiter: ^0.5.3
|
||||
html2canvas: ^1.4.1
|
||||
idb-keyval: ^6.2.2
|
||||
idb-keyval: ^6.3.0
|
||||
injeca: ^0.2.0
|
||||
ioredis: ^5.10.1
|
||||
ioredis: ^6.0.0
|
||||
ioredis-mock: ^8.13.1
|
||||
is-network-error: ^1.3.1
|
||||
isolated-vm: ^7.0.0
|
||||
jose: ^6.2.2
|
||||
jsdom: ^29.0.2
|
||||
is-network-error: ^1.3.2
|
||||
isolated-vm: ^7.0.1
|
||||
jose: ^6.2.10
|
||||
jsdom: ^30.0.1
|
||||
jszip: ^3.10.1
|
||||
knip: ^6.4.1
|
||||
knip: ^6.32.2
|
||||
kokoro-js: ^1.2.1
|
||||
less: ^4.6.4
|
||||
libsodium-wrappers: ^0.8.3
|
||||
kysely: 0.29.4
|
||||
less: ^4.9.0
|
||||
libsodium-wrappers: ^0.8.4
|
||||
localforage: ^1.10.0
|
||||
mark.js: ^8.11.1
|
||||
markdown-it: ^14.1.1
|
||||
markdown-it-anchor: ^9.2.0
|
||||
mediabunny: ^1.40.1
|
||||
markdown-it: 14.1.1
|
||||
markdown-it-anchor: ^9.2.1
|
||||
mediabunny: ^1.55.2
|
||||
meta-png: ^1.0.6
|
||||
minecraft-data: 3.102.3
|
||||
mineflayer: ^4.33.0
|
||||
mineflayer: ^4.37.1
|
||||
mineflayer-armor-manager: ^2.0.1
|
||||
mineflayer-auto-eat: ^5.0.3
|
||||
mineflayer-collectblock: ^1.6.0
|
||||
@@ -321,164 +328,162 @@ catalog:
|
||||
mineflayer-tool: ^1.2.0
|
||||
minisearch: ^7.2.0
|
||||
mkcert: ^3.2.0
|
||||
motion-v: ^2.2.1
|
||||
motion-v: ^2.4.0
|
||||
nano-staged: ^1.0.2
|
||||
nanoid: ^5.1.9
|
||||
node-pty: npm:@lydell/node-pty@^1.2.0-beta.12
|
||||
nanoid: ^6.0.1
|
||||
node-pty: npm:@lydell/node-pty@^1.2.0-beta.15
|
||||
node-vibrant: ^4.0.4
|
||||
nprogress: ^0.2.0
|
||||
ofetch: ^1.5.1
|
||||
onnxruntime-web: ^1.24.3
|
||||
onnxruntime-web: ^1.27.0
|
||||
opusscript: ^0.1.1
|
||||
oxc-minify: ^0.126.0
|
||||
oxlint: ^1.60.0
|
||||
p-limit: ^7.3.0
|
||||
oxc-minify: ^0.146.0
|
||||
oxlint: ^1.79.0
|
||||
p-limit: ^7.3.1
|
||||
pathe: ^2.0.3
|
||||
pg: ^8.20.0
|
||||
pinia: ^3.0.4
|
||||
pinia-plugin-synced: ^0.1.3
|
||||
pg: ^8.23.0
|
||||
pinia: ^4.0.3
|
||||
pinia-plugin-synced: ^0.1.4
|
||||
pixi-filters: ^4.2.0
|
||||
pixi-live2d-display: ^0.4.0
|
||||
playwright: ^1.60.0
|
||||
playwright: ^1.62.1
|
||||
popmotion: ^11.0.5
|
||||
postcss: ^8.5.10
|
||||
postcss: ^8.5.26
|
||||
postgres: ^3.4.9
|
||||
posthog-js: 1.306.1
|
||||
posthog-node: ^5.39.4
|
||||
postprocessing: ^6.39.1
|
||||
posthog-node: ^5.50.0
|
||||
postprocessing: ^6.39.4
|
||||
prismarine-block: ^1.23.0
|
||||
prismarine-entity: ^2.6.0
|
||||
prismarine-item: ^1.18.0
|
||||
prismarine-recipe: ^1.5.0
|
||||
prismarine-viewer: ^1.33.0
|
||||
prismarine-windows: ^2.10.0
|
||||
publint: ^0.3.18
|
||||
publint: ^0.3.24
|
||||
rehype-katex: ^7.0.1
|
||||
rehype-parse: ^9.0.1
|
||||
rehype-stringify: ^10.0.1
|
||||
reka-ui: ^2.10.1
|
||||
reka-ui: ^2.10.3
|
||||
remark-math: ^6.0.0
|
||||
remark-parse: ^11.0.0
|
||||
remark-rehype: ^11.1.2
|
||||
replicate: ^1.4.0
|
||||
resend: ^6.12.2
|
||||
rollup: ^4.60.1
|
||||
semver: ^7.7.4
|
||||
shiki: ^4.0.2
|
||||
resend: ^6.22.0
|
||||
rollup: ^4.62.5
|
||||
semver: ^7.8.5
|
||||
shiki: ^4.4.3
|
||||
simple-git-hooks: ^2.13.1
|
||||
splitpanes: ^4.0.4
|
||||
sponsorkit: ^17.1.0
|
||||
splitpanes: ^4.1.2
|
||||
sponsorkit: 17.1.0
|
||||
sponsors-svg: ^0.3.0
|
||||
srvx: ^0.11.15
|
||||
std-env: ^4.1.0
|
||||
stockfish: ^18.0.7
|
||||
stripe: ^22.0.2
|
||||
srvx: ^0.12.7
|
||||
std-env: ^4.2.0
|
||||
stockfish: ^18.0.8
|
||||
stripe: ^22.5.0
|
||||
superjson: ^2.2.6
|
||||
taze: ^19.11.0
|
||||
taze: ^21.1.0
|
||||
telegram: ^2.26.22
|
||||
three: ^0.184.0
|
||||
three: ^0.185.1
|
||||
three-stdlib: ^2.36.1
|
||||
tinyexec: ^1.1.1
|
||||
tinyglobby: ^0.2.16
|
||||
tsdown: ^0.21.9
|
||||
tsx: ^4.21.0
|
||||
turbo: ^2.9.6
|
||||
typescript: ^5.9.3
|
||||
tinyexec: ^1.3.0
|
||||
tinyglobby: ^0.2.17
|
||||
tsdown: ^0.22.14
|
||||
tsx: ^4.23.12
|
||||
turbo: ^2.10.11
|
||||
typescript: ^6.0.3
|
||||
uiohook-napi: ^1.5.5
|
||||
uncrypto: ^0.1.3
|
||||
unified: ^11.0.5
|
||||
unist-builder: ^4.0.0
|
||||
unist-util-visit: ^5.1.0
|
||||
unocss: ^66.6.8
|
||||
unocss: 66.7.5
|
||||
unocss-preset-scrollbar: ^4.0.0
|
||||
unplugin-basemove: 0.0.1
|
||||
unplugin-info: ^1.3.2
|
||||
unplugin-lightningcss: ^0.4.5
|
||||
unplugin-lightningcss: ^0.5.0
|
||||
unplugin-raw: ^0.7.0
|
||||
unplugin-unused: ^0.5.7
|
||||
unplugin-unused: ^0.6.0
|
||||
unplugin-vue-router: ^0.19.2
|
||||
unplugin-yaml: ^4.1.0
|
||||
unplugin-yaml: ^4.2.1
|
||||
unstorage: ^1.17.5
|
||||
uqr: ^0.1.3
|
||||
uuid: ^13.0.0
|
||||
valibot: ^1.3.1
|
||||
uuid: ^14.0.2
|
||||
valibot: ^1.4.2
|
||||
vaul-vue: ^0.4.1
|
||||
vec3: ^0.2.0
|
||||
vieval: ^0.0.5
|
||||
virtua: ^0.50.3
|
||||
vite: ^8.0.8
|
||||
vieval: ^0.0.12
|
||||
virtua: ^0.50.4
|
||||
vite: ^8.2.2
|
||||
vite-bundle-visualizer: ^1.2.1
|
||||
vite-plugin-inspect: 12.0.0-beta.1
|
||||
vite-plugin-mkcert: ^2.0.0
|
||||
vite-plugin-pwa: ^1.2.0
|
||||
vite-plugin-vue-devtools: ^8.1.1
|
||||
vite-plugin-mkcert: ^2.1.0
|
||||
vite-plugin-pwa: ^1.3.0
|
||||
vite-plugin-vue-devtools: ^8.2.1
|
||||
vite-plugin-vue-layouts: ^0.11.0
|
||||
vitepress: 2.0.0-alpha.17
|
||||
vitest-browser-vue: ^2.1.0
|
||||
vscode-ext-gen: ^1.6.0
|
||||
vue: ^3.5.32
|
||||
vue: ^3.5.41
|
||||
vue-demi: ^0.14.10
|
||||
vue-i18n: ^11.3.2
|
||||
vue-macros: ^3.1.2
|
||||
vue-router: ^5.0.4
|
||||
vue-i18n: ^11.4.9
|
||||
vue-macros: ^3.1.4
|
||||
vue-router: ^5.2.0
|
||||
vue-sonner: ^2.0.9
|
||||
vue-tsc: ^3.2.6
|
||||
vue-tsc: ^3.3.11
|
||||
wavefile: ^11.0.0
|
||||
web-haptics: ^0.0.6
|
||||
whatwg-mimetype: ^5.0.0
|
||||
wlipsync: ^1.3.0
|
||||
workbox-window: ^7.4.0
|
||||
ws: ^8.20.0
|
||||
wxt: ^0.20.24
|
||||
wlipsync: ^1.3.1
|
||||
workbox-window: ^7.4.1
|
||||
ws: ^8.21.3
|
||||
wxt: ^0.21.4
|
||||
xast-util-to-xml: ^4.0.0
|
||||
xastscript: ^4.0.0
|
||||
xsschema: 0.5.0-beta.8
|
||||
yaml: ^2.8.3
|
||||
yauzl: ^3.3.0
|
||||
zod: ^4.3.6
|
||||
yaml: ^2.9.0
|
||||
yauzl: ^3.4.0
|
||||
zod: ^4.4.3
|
||||
zod-to-json-schema: ^3.25.2
|
||||
catalogs:
|
||||
vitest:
|
||||
'@vitest/browser-playwright': ^4.1.4
|
||||
'@vitest/coverage-v8': ^4.1.4
|
||||
vitest: ^4.1.4
|
||||
'@vitest/browser-playwright': ^4.1.11
|
||||
'@vitest/coverage-v8': ^4.1.11
|
||||
vitest: ^4.1.11
|
||||
xsai:
|
||||
unspeech: ^0.1.14
|
||||
unspeech: ^0.1.16
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@ax-llm/ax'
|
||||
- '@prisma/client'
|
||||
- better-sqlite3
|
||||
- simple-git-hooks # [workaround] With `shellEmulator: true`, simple-git-hooks install may fail when no .git dir exists.
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@anthropic-ai/claude-code'
|
||||
- '@discordjs/opus'
|
||||
- '@ffmpeg-installer/darwin-arm64'
|
||||
- '@ffmpeg-installer/linux-x64'
|
||||
- '@napi-rs/image'
|
||||
- '@parcel/watcher'
|
||||
- bufferutil
|
||||
- canvas
|
||||
- core-js
|
||||
- electron
|
||||
- electron-click-drag-plugin
|
||||
- electron-winstaller
|
||||
- es5-ext
|
||||
- esbuild
|
||||
- ffmpeg-static
|
||||
- isolated-vm
|
||||
- less
|
||||
- msw
|
||||
- node-pty
|
||||
- onnxruntime-node
|
||||
- protobufjs
|
||||
- sharp
|
||||
- spawn-sync
|
||||
- stockfish
|
||||
- uiohook-napi
|
||||
- utf-8-validate
|
||||
- vue-demi
|
||||
allowBuilds:
|
||||
'@anthropic-ai/claude-code': true
|
||||
'@ax-llm/ax': false
|
||||
'@discordjs/opus': true
|
||||
'@ffmpeg-installer/darwin-arm64': true
|
||||
'@ffmpeg-installer/linux-x64': true
|
||||
'@napi-rs/image': true
|
||||
'@parcel/watcher': true
|
||||
'@prisma/client': false
|
||||
better-sqlite3: false
|
||||
bufferutil: true
|
||||
canvas: true
|
||||
core-js: true
|
||||
electron: true
|
||||
electron-click-drag-plugin: true
|
||||
electron-winstaller: true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
ffmpeg-static: true
|
||||
isolated-vm: true
|
||||
less: true
|
||||
msw: true
|
||||
node-pty: true
|
||||
onnxruntime-node: true
|
||||
protobufjs: true
|
||||
sharp: true
|
||||
simple-git-hooks: false # [workaround] With `shellEmulator: true`, simple-git-hooks install may fail when no .git dir exists.
|
||||
spawn-sync: true
|
||||
stockfish: true
|
||||
uiohook-napi: true
|
||||
utf-8-validate: true
|
||||
vue-demi: true
|
||||
packageExtensions:
|
||||
'@formkit/auto-animate':
|
||||
peerDependencies:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PostHogConfig } from 'posthog-js'
|
||||
|
||||
function isEnvFlagEnabled(value: string | undefined): boolean {
|
||||
if (value == null)
|
||||
return false
|
||||
|
||||
return /^(?:1|true|t|yes|y|on)$/i.test(value.trim())
|
||||
}
|
||||
|
||||
// For Release workflows set `VITE_ENABLE_POSTHOG=true`.
|
||||
export const POSTHOG_ENABLED = isEnvFlagEnabled(import.meta.env.VITE_ENABLE_POSTHOG)
|
||||
|
||||
// Single PostHog project for every AIRI surface (web / desktop / mobile).
|
||||
// Platforms are told apart by the `app_surface` super property set at init, not
|
||||
// by routing to separate per-platform projects.
|
||||
export const POSTHOG_PROJECT_KEY
|
||||
= import.meta.env.VITE_POSTHOG_PROJECT_KEY
|
||||
?? 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9' // cspell:disable-line
|
||||
|
||||
export const DEFAULT_POSTHOG_CONFIG = {
|
||||
api_host: 'https://t.airi.build',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
// Without this, posthog-js only fires `$pageview` on the initial page load.
|
||||
// Every AIRI surface is an SPA (vue-router / VitePress client routing), so
|
||||
// route changes would be invisible in PostHog. The '2025-05-24' defaults
|
||||
// preset switches `capture_pageview` to 'history_change' — and because
|
||||
// `capture_pageleave` defaults to 'if_capture_pageview', every surface
|
||||
// that spreads this config also starts emitting `$pageleave`. That is
|
||||
// intentional: pageleave is what makes route-level dwell time queryable.
|
||||
defaults: '2025-05-24',
|
||||
} as const satisfies Partial<PostHogConfig>
|
||||
@@ -119,9 +119,9 @@ else {
|
||||
const resource = resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: serviceName,
|
||||
[ATTR_SERVICE_VERSION]: env.npm_package_version || '0.0.0',
|
||||
'service.namespace': serviceNamespace,
|
||||
'service.instance.id': instanceId,
|
||||
'deployment.environment': env.NODE_ENV || 'development',
|
||||
'service.instance.id': instanceId,
|
||||
'service.namespace': serviceNamespace,
|
||||
})
|
||||
|
||||
// Traces fan out to independent processors. OTLP (Grafana Tempo) sees every
|
||||
@@ -131,8 +131,8 @@ else {
|
||||
const spanProcessors = []
|
||||
if (otlpEndpoint) {
|
||||
spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
})))
|
||||
}
|
||||
// Langfuse runs on its OWN TracerProvider, isolated from the global NodeSDK
|
||||
@@ -160,12 +160,12 @@ else {
|
||||
resource,
|
||||
sampler: new AlwaysOnSampler(),
|
||||
spanProcessors: [new LangfuseSpanProcessor({
|
||||
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
||||
secretKey: env.LANGFUSE_SECRET_KEY,
|
||||
baseUrl: env.LANGFUSE_BASE_URL,
|
||||
// Railway is long-running → batched. Flushed via
|
||||
// langfuseProvider.shutdown() in the SIGTERM handler below.
|
||||
exportMode: 'batched',
|
||||
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
||||
secretKey: env.LANGFUSE_SECRET_KEY,
|
||||
// Full override of Langfuse's default filter. Export ONLY spans the
|
||||
// @langfuse/tracing SDK created (they carry `langfuse.*` attributes
|
||||
// such as `langfuse.observation.type`). This provider should only ever
|
||||
@@ -196,18 +196,20 @@ else {
|
||||
// pointed at an empty URL.
|
||||
...(otlpEndpoint
|
||||
? {
|
||||
logRecordProcessors: [new BatchLogRecordProcessor({
|
||||
exporter: new OTLPLogExporter({
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
}),
|
||||
})],
|
||||
metricReaders: [new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({
|
||||
url: `${otlpEndpoint}/v1/metrics`,
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/metrics`,
|
||||
}),
|
||||
exportIntervalMillis: 15_000,
|
||||
exportTimeoutMillis: 10_000,
|
||||
})],
|
||||
logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter({
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
headers,
|
||||
}))],
|
||||
}
|
||||
: {}),
|
||||
instrumentations: [
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "catalog:",
|
||||
"@electric-sql/pglite": "catalog:",
|
||||
"@electric-sql/pglite-pgvector": "catalog:",
|
||||
"@guiiai/logg": "catalog:",
|
||||
"@hono/node-server": "catalog:",
|
||||
"@hono/node-ws": "catalog:",
|
||||
|
||||
@@ -45,47 +45,27 @@ function optionalNumberFromString(defaultValue: number, envKey: string, minimum:
|
||||
}
|
||||
|
||||
const EnvSchema = object({
|
||||
HOST: optional(string(), '0.0.0.0'),
|
||||
PORT: optionalIntegerFromString(3000, 'PORT', 1),
|
||||
|
||||
API_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
AUTH_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
AUTH_SERVER_INTERNAL_URL: optional(string()),
|
||||
|
||||
// Canonical user-facing web app origin. Used as the Stripe redirect base
|
||||
// (success_url / cancel_url / portal return_url) when a request has no trusted
|
||||
// browser origin — notably the Electron desktop renderer, which loads from
|
||||
// file:// and sends no usable web origin. Web/mobile requests keep returning to
|
||||
// their own origin; only origin-less clients fall back to this.
|
||||
WEB_APP_URL: optional(string(), 'https://airi.moeru.ai'),
|
||||
|
||||
// Comma-separated exact origins (e.g. Capacitor dev server `https://10.x:5273`).
|
||||
// Prefer this over broad private-IP regex heuristics in production-like configs.
|
||||
ADDITIONAL_TRUSTED_ORIGINS: optional(
|
||||
AdditionalTrustedOriginsSchema,
|
||||
'',
|
||||
),
|
||||
API_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
|
||||
AUTH_SERVER_INTERNAL_URL: optional(string()),
|
||||
AUTH_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
DATABASE_URL: pipe(string(), nonEmpty('DATABASE_URL is required')),
|
||||
REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')),
|
||||
|
||||
// Testing-only bearer token bypass. Keep unset in production. When set,
|
||||
// Authorization: Bearer $TEST_AUTH_TOKEN resolves to the virtual user below
|
||||
// through resolveRequestAuth without creating an Auth session row.
|
||||
TEST_AUTH_TOKEN: optional(string(), ''),
|
||||
TEST_AUTH_USER_ID: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_ID must not be empty when set')), 'test-user'),
|
||||
TEST_AUTH_USER_EMAIL: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_EMAIL must not be empty when set')), 'test@example.com'),
|
||||
TEST_AUTH_USER_NAME: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_NAME must not be empty when set')), 'Test User'),
|
||||
DB_POOL_CONNECTION_TIMEOUT_MS: optionalIntegerFromString(5000, 'DB_POOL_CONNECTION_TIMEOUT_MS', 1),
|
||||
|
||||
STRIPE_SECRET_KEY: optional(string()),
|
||||
STRIPE_WEBHOOK_SECRET: optional(string()),
|
||||
DB_POOL_IDLE_TIMEOUT_MS: optionalIntegerFromString(30000, 'DB_POOL_IDLE_TIMEOUT_MS', 1),
|
||||
|
||||
// LLM/TTS gateway is fully internalised by the in-process router; provider
|
||||
// baseURLs live per-upstream inside LLM_ROUTER_CONFIG, and the default chat /
|
||||
// tts model aliases moved to configKV (DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL)
|
||||
// so they're hot-swappable via Pub/Sub invalidation. No env entries needed
|
||||
// here.
|
||||
DB_POOL_KEEPALIVE_INITIAL_DELAY_MS: optionalIntegerFromString(10000, 'DB_POOL_KEEPALIVE_INITIAL_DELAY_MS', 1),
|
||||
// Database pool
|
||||
DB_POOL_MAX: optionalIntegerFromString(20, 'DB_POOL_MAX', 1),
|
||||
|
||||
HOST: optional(string(), '0.0.0.0'),
|
||||
// Envelope-encryption master key for in-process LLM/TTS router (KTD-5).
|
||||
// Stored as base64-encoded 32 random bytes. Validator decodes + asserts the
|
||||
// 32-byte length at parse time so a misconfigured key fails the deploy
|
||||
@@ -107,27 +87,47 @@ const EnvSchema = object({
|
||||
transform(b64 => Buffer.from(b64, 'base64')),
|
||||
check(buf => buf.length === 32, 'LLM_ROUTER_MASTER_KEY_PREVIOUS must decode to exactly 32 bytes when set'),
|
||||
)),
|
||||
OTEL_DEBUG: optional(string()),
|
||||
|
||||
// Database pool
|
||||
DB_POOL_MAX: optionalIntegerFromString(20, 'DB_POOL_MAX', 1),
|
||||
DB_POOL_IDLE_TIMEOUT_MS: optionalIntegerFromString(30000, 'DB_POOL_IDLE_TIMEOUT_MS', 1),
|
||||
DB_POOL_CONNECTION_TIMEOUT_MS: optionalIntegerFromString(5000, 'DB_POOL_CONNECTION_TIMEOUT_MS', 1),
|
||||
DB_POOL_KEEPALIVE_INITIAL_DELAY_MS: optionalIntegerFromString(10000, 'DB_POOL_KEEPALIVE_INITIAL_DELAY_MS', 1),
|
||||
|
||||
// PostHog product-event forwarding for server-confirmed funnel facts.
|
||||
// Defaults to the shared AIRI project key (same browser-safe phc_* key the
|
||||
// client surfaces embed in posthog.config.ts), so forwarding is on out of
|
||||
// the box. Set to an empty string to disable server-side product analytics.
|
||||
POSTHOG_PROJECT_KEY: optional(string(), 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9'), // cspell:disable-line
|
||||
POSTHOG_API_HOST: optional(string(), 'https://t.airi.build'),
|
||||
|
||||
// OpenTelemetry
|
||||
OTEL_SERVICE_NAMESPACE: optional(string(), 'airi'),
|
||||
OTEL_SERVICE_NAME: optional(string(), 'server'),
|
||||
OTEL_TRACES_SAMPLING_RATIO: optionalNumberFromString(1, 'OTEL_TRACES_SAMPLING_RATIO', 0, 1),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: optional(string()),
|
||||
OTEL_EXPORTER_OTLP_HEADERS: optional(string()),
|
||||
OTEL_DEBUG: optional(string()),
|
||||
|
||||
// LLM/TTS gateway is fully internalised by the in-process router; provider
|
||||
// baseURLs live per-upstream inside LLM_ROUTER_CONFIG, and the default chat /
|
||||
// tts model aliases moved to configKV (DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL)
|
||||
// so they're hot-swappable via Pub/Sub invalidation. No env entries needed
|
||||
// here.
|
||||
|
||||
OTEL_SERVICE_NAME: optional(string(), 'server'),
|
||||
// OpenTelemetry
|
||||
OTEL_SERVICE_NAMESPACE: optional(string(), 'airi'),
|
||||
|
||||
OTEL_TRACES_SAMPLING_RATIO: optionalNumberFromString(1, 'OTEL_TRACES_SAMPLING_RATIO', 0, 1),
|
||||
PORT: optionalIntegerFromString(3000, 'PORT', 1),
|
||||
POSTHOG_API_HOST: optional(string(), 'https://t.airi.build'),
|
||||
// PostHog product-event forwarding for server-confirmed funnel facts.
|
||||
// Defaults to the shared AIRI project key (same browser-safe phc_* key the
|
||||
// client surfaces embed in stage-shared/analytics/posthog), so forwarding is on out of
|
||||
// the box. Set to an empty string to disable server-side product analytics.
|
||||
POSTHOG_PROJECT_KEY: optional(string(), 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9'), // cspell:disable-line
|
||||
|
||||
REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')),
|
||||
STRIPE_SECRET_KEY: optional(string()),
|
||||
|
||||
STRIPE_WEBHOOK_SECRET: optional(string()),
|
||||
// Testing-only bearer token bypass. Keep unset in production. When set,
|
||||
// Authorization: Bearer $TEST_AUTH_TOKEN resolves to the virtual user below
|
||||
// through resolveRequestAuth without creating an Auth session row.
|
||||
TEST_AUTH_TOKEN: optional(string(), ''),
|
||||
TEST_AUTH_USER_EMAIL: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_EMAIL must not be empty when set')), 'test@example.com'),
|
||||
TEST_AUTH_USER_ID: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_ID must not be empty when set')), 'test-user'),
|
||||
TEST_AUTH_USER_NAME: optional(pipe(string(), nonEmpty('TEST_AUTH_USER_NAME must not be empty when set')), 'Test User'),
|
||||
// Canonical user-facing web app origin. Used as the Stripe redirect base
|
||||
// (success_url / cancel_url / portal return_url) when a request has no trusted
|
||||
// browser origin — notably the Electron desktop renderer, which loads from
|
||||
// file:// and sends no usable web origin. Web/mobile requests keep returning to
|
||||
// their own origin; only origin-less clients fall back to this.
|
||||
WEB_APP_URL: optional(string(), 'https://airi.moeru.ai'),
|
||||
})
|
||||
|
||||
export type Env = InferOutput<typeof EnvSchema>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { mockDB } from './mock-db'
|
||||
|
||||
describe('mockDB', () => {
|
||||
it('registers the pgvector extension', async () => {
|
||||
const db = await mockDB({})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// PGlite 0.5 moved pgvector from the main package to a separate package.
|
||||
// The old subpath import prevents this test database from loading.
|
||||
//
|
||||
// The helper now registers the separate extension before it creates the schema.
|
||||
const result = await db.execute<{ distance: number }>(sql`
|
||||
SELECT '[1, 2, 3]'::vector <-> '[1, 2, 4]'::vector AS distance
|
||||
`)
|
||||
|
||||
expect(result.rows).toEqual([{ distance: 1 }])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Database } from './db'
|
||||
|
||||
import { PGlite } from '@electric-sql/pglite'
|
||||
import { vector } from '@electric-sql/pglite/vector'
|
||||
import { vector } from '@electric-sql/pglite-pgvector'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/pglite'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
@@ -51,27 +51,12 @@ else {
|
||||
const resource = resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: env.OTEL_SERVICE_NAME || 'auth-server',
|
||||
[ATTR_SERVICE_VERSION]: env.npm_package_version || '0.0.0',
|
||||
'service.namespace': env.OTEL_SERVICE_NAMESPACE || 'airi',
|
||||
'service.instance.id': instanceId,
|
||||
'deployment.environment': env.NODE_ENV || 'development',
|
||||
'service.instance.id': instanceId,
|
||||
'service.namespace': env.OTEL_SERVICE_NAMESPACE || 'airi',
|
||||
})
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource,
|
||||
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(samplingRatio) }),
|
||||
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
headers,
|
||||
}))],
|
||||
metricReaders: [new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({ url: `${otlpEndpoint}/v1/metrics`, headers }),
|
||||
exportIntervalMillis: 15_000,
|
||||
exportTimeoutMillis: 10_000,
|
||||
})],
|
||||
logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter({
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
headers,
|
||||
}))],
|
||||
instrumentations: [
|
||||
new HttpInstrumentation({ disableIncomingRequestInstrumentation: true }),
|
||||
new PgInstrumentation({ enhancedDatabaseReporting: true }),
|
||||
@@ -79,6 +64,23 @@ else {
|
||||
new RuntimeNodeInstrumentation(),
|
||||
new UndiciInstrumentation(),
|
||||
],
|
||||
logRecordProcessors: [new BatchLogRecordProcessor({
|
||||
exporter: new OTLPLogExporter({
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/logs`,
|
||||
}),
|
||||
})],
|
||||
metricReaders: [new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({ headers, url: `${otlpEndpoint}/v1/metrics` }),
|
||||
exportIntervalMillis: 15_000,
|
||||
exportTimeoutMillis: 10_000,
|
||||
})],
|
||||
resource,
|
||||
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(samplingRatio) }),
|
||||
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
headers,
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
}))],
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@electric-sql/pglite": "catalog:",
|
||||
"@types/pg": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"kysely": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user