mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
chore(ci): add GitHub CI workflow (#271)
* Add GitHub CI workflow * Address CI workflow review feedback Generated-By: looper 0.3.0 (runner=fixer, agent=openai/gpt-5.5)
This commit is contained in:
parent
5a0f954297
commit
a93246d892
12 changed files with 218 additions and 24 deletions
87
.github/workflows/ci.yml
vendored
Normal file
87
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
# Release validation is owned by the release workflows rather than this CI
|
||||
# workflow: `release-stable` has a verify job before publishing, and
|
||||
# `release-beta` builds from its selected release commit. Keep this trigger
|
||||
# focused on PRs, main, and manual reruns instead of duplicating tag/release
|
||||
# events that would run after those release workflows have already selected
|
||||
# or validated their commit.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
# Prefer current-head signal over preserving superseded logs: PR authors often
|
||||
# push fixups while this workflow is still running, and stale runs can report
|
||||
# failures for commits reviewers no longer need to evaluate. Release workflows
|
||||
# use cancel-in-progress: false where preserving build evidence matters more.
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate workspace
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# `scripts/postinstall.mjs` only prebuilds package/tool entrypoints that
|
||||
# are needed immediately after install for linked bins and shared
|
||||
# sidecar/platform imports. It intentionally skips app outputs because
|
||||
# building all apps would make every install run a Next/Electron-adjacent
|
||||
# app build, even when a developer only needs packages/tools.
|
||||
#
|
||||
# Fresh CI typecheck/test still need these specific generated declarations:
|
||||
# - `apps/daemon/dist/*.d.ts` for e2e runtime-adapter tests that import
|
||||
# daemon runtime modules
|
||||
# - `apps/desktop/dist/main/index.d.ts` for `apps/packaged` imports of
|
||||
# `@open-design/desktop/main`
|
||||
# - `apps/web/dist/sidecar/index.d.ts` for `apps/packaged` imports of
|
||||
# `@open-design/web/sidecar`
|
||||
# If postinstall grows a targeted app type-generation phase covering these
|
||||
# three exports without broad app builds, this CI prebuild can be removed.
|
||||
- name: Prebuild workspace type declarations
|
||||
run: |
|
||||
pnpm --filter @open-design/daemon build
|
||||
pnpm --filter @open-design/desktop build
|
||||
pnpm --filter @open-design/web build:sidecar
|
||||
|
||||
- name: Typecheck workspaces
|
||||
run: pnpm -r --workspace-concurrency=1 --if-present run typecheck
|
||||
|
||||
- name: Check residual JS in TypeScript packages
|
||||
run: pnpm check:residual-js
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
# Keep workspace builds serialized so generated dist output and local
|
||||
# runtime artifacts are produced in a deterministic order. Parallel
|
||||
# recursive builds would surface late-package failures sooner, but the
|
||||
# current workspace is small enough that safer logs and fewer shared-FS
|
||||
# races outweigh the lost parallelism; revisit if the package count grows.
|
||||
- name: Build workspaces
|
||||
run: pnpm -r --workspace-concurrency=1 --if-present run build
|
||||
|
|
@ -62,19 +62,23 @@ function attachParentMonitor(stop: () => Promise<void>): void {
|
|||
}
|
||||
|
||||
export async function startDaemonSidecar(runtime: SidecarRuntimeContext<SidecarStamp>): Promise<DaemonSidecarHandle> {
|
||||
const started = await startServer({ port: parsePort(process.env[DAEMON_PORT_ENV]), returnServer: true }) as
|
||||
| string
|
||||
| { server: Server; url: string };
|
||||
if (typeof started === "string") {
|
||||
const serverHandle = await startServer({ port: parsePort(process.env[DAEMON_PORT_ENV]), returnServer: true }) as
|
||||
| Server
|
||||
| undefined;
|
||||
if (serverHandle == null) {
|
||||
throw new Error("daemon startServer did not return a server handle");
|
||||
}
|
||||
const serverHandle = started;
|
||||
const server = serverHandle;
|
||||
const address = server.address();
|
||||
if (address == null || typeof address === "string") {
|
||||
throw new Error("daemon startServer did not bind to a TCP port");
|
||||
}
|
||||
|
||||
const state: DaemonStatusSnapshot = {
|
||||
pid: process.pid,
|
||||
state: "running",
|
||||
updatedAt: new Date().toISOString(),
|
||||
url: serverHandle.url,
|
||||
url: `http://127.0.0.1:${address.port}`,
|
||||
};
|
||||
let ipcServer: JsonIpcServerHandle | null = null;
|
||||
let stopped = false;
|
||||
|
|
@ -89,7 +93,7 @@ export async function startDaemonSidecar(runtime: SidecarRuntimeContext<SidecarS
|
|||
state.state = "stopped";
|
||||
state.updatedAt = new Date().toISOString();
|
||||
await ipcServer?.close().catch(() => undefined);
|
||||
await closeHttpServer(serverHandle.server).catch(() => undefined);
|
||||
await closeHttpServer(server).catch(() => undefined);
|
||||
resolveStopped();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@ describe('/api/version', () => {
|
|||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const started = await startServer({ port: 0, returnServer: true }) as {
|
||||
url: string;
|
||||
server: http.Server;
|
||||
};
|
||||
baseUrl = started.url;
|
||||
server = started.server;
|
||||
const started = await startServer({ port: 0, returnServer: true }) as http.Server | undefined;
|
||||
if (started == null) {
|
||||
throw new Error('startServer did not return a server handle');
|
||||
}
|
||||
const address = started.address();
|
||||
if (address == null || typeof address === 'string') {
|
||||
throw new Error('startServer did not bind to a TCP port');
|
||||
}
|
||||
server = started;
|
||||
baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise<void>((resolve) => server.close(() => resolve())));
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const DE_SKILL_COPY: Record<string, { description?: string; examplePrompt?: stri
|
|||
examplePrompt:
|
||||
'Entwerfen Sie ‚mutuals‘ — eine Dating-Site für X-Poster. Tägliches Digest-Dashboard mit Statistiken, Balkendiagramm für gegenseitige Matches und Community-Ticker.',
|
||||
},
|
||||
'design-brief': {},
|
||||
'digital-eguide': {
|
||||
examplePrompt:
|
||||
'Entwerfen Sie ‚The Creator\'s Style & Format Guide‘ — Coverseite und eine Innenseite für eine Lifestyle-Creator-Brand.',
|
||||
|
|
@ -60,6 +61,22 @@ const DE_SKILL_COPY: Record<string, { description?: string; examplePrompt?: stri
|
|||
examplePrompt:
|
||||
'Erstellen Sie einen 30-Tage-Onboardingplan für einen neuen Product Designer in einem 40-Personen-Startup.',
|
||||
},
|
||||
'html-ppt': {},
|
||||
'html-ppt-course-module': {},
|
||||
'html-ppt-dir-key-nav-minimal': {},
|
||||
'html-ppt-graphify-dark-graph': {},
|
||||
'html-ppt-hermes-cyber-terminal': {},
|
||||
'html-ppt-knowledge-arch-blueprint': {},
|
||||
'html-ppt-obsidian-claude-gradient': {},
|
||||
'html-ppt-pitch-deck': {},
|
||||
'html-ppt-presenter-mode': {},
|
||||
'html-ppt-product-launch': {},
|
||||
'html-ppt-tech-sharing': {},
|
||||
'html-ppt-testing-safety-alert': {},
|
||||
'html-ppt-weekly-report': {},
|
||||
'html-ppt-xhs-pastel-card': {},
|
||||
'html-ppt-xhs-post': {},
|
||||
'html-ppt-xhs-white-editorial': {},
|
||||
'hyperframes': {
|
||||
examplePrompt:
|
||||
'Ein 5-Sekunden-Product-Reveal: ein minimalistisches High-End-Produkt auf einer sauberen cremefarbenen Fläche, weiches Seitenlicht, langsamer Kamera-Push-in, zurückhaltende Bewegung, keine Text-Overlays.',
|
||||
|
|
@ -337,6 +354,7 @@ const DE_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
|||
'Social / Meme': 'Social / Meme',
|
||||
Branding: 'Branding',
|
||||
Data: 'Daten',
|
||||
'Game UI': 'Game UI',
|
||||
Marketing: 'Marketing',
|
||||
Product: 'Produkt',
|
||||
'Short Form': 'Short Form',
|
||||
|
|
@ -350,49 +368,89 @@ const DE_PROMPT_TEMPLATE_TAGS: Record<string, string> = {
|
|||
anime: 'Anime',
|
||||
'app-showcase': 'App-Showcase',
|
||||
'audio-reactive': 'Audio-reaktiv',
|
||||
'ancient-china': 'Ancient China',
|
||||
archery: 'Archery',
|
||||
arpg: 'ARPG',
|
||||
'boss-fight': 'Boss Fight',
|
||||
brand: 'Brand',
|
||||
branding: 'Branding',
|
||||
captions: 'Untertitel',
|
||||
cavalry: 'Cavalry',
|
||||
chart: 'Chart',
|
||||
choreography: 'Choreography',
|
||||
cinematic: 'Filmisch',
|
||||
'cinematic-romance': 'Filmische Romanze',
|
||||
combat: 'Combat',
|
||||
combo: 'Combo',
|
||||
'companion-to-image': 'Companion to Image',
|
||||
counter: 'Counter',
|
||||
cyberpunk: 'Cyberpunk',
|
||||
dance: 'Dance',
|
||||
'data-viz': 'Data-Viz',
|
||||
editorial: 'Editorial',
|
||||
'elden-ring': 'Elden Ring',
|
||||
endcard: 'End Card',
|
||||
escort: 'Escort',
|
||||
'escort-mission': 'Escort Mission',
|
||||
fantasy: 'Fantasy',
|
||||
fashion: 'Fashion',
|
||||
'fighting-game': 'Fighting Game',
|
||||
food: 'Food',
|
||||
'game-cinematic': 'Game Cinematic',
|
||||
'game-ui': 'Game UI',
|
||||
'grid-sheet': 'Grid Sheet',
|
||||
guanyu: 'Guanyu',
|
||||
hud: 'HUD',
|
||||
'hud-safe': 'HUD Safe',
|
||||
hype: 'Hype',
|
||||
hyperframes: 'HyperFrames',
|
||||
idol: 'Idol',
|
||||
infographic: 'Infografik',
|
||||
japanese: 'Japanese',
|
||||
karaoke: 'Karaoke',
|
||||
'key-visual': 'Key Visual',
|
||||
'kinetic-typography': 'Kinetische Typografie',
|
||||
'linear-style': 'Linear-Stil',
|
||||
logo: 'Logo',
|
||||
lyubu: 'Lyu Bu',
|
||||
map: 'Karte',
|
||||
marketing: 'Marketing',
|
||||
minimal: 'Minimal',
|
||||
mmo: 'MMO',
|
||||
mobile: 'Mobile',
|
||||
money: 'Geld',
|
||||
'mounted-combat': 'Mounted Combat',
|
||||
nature: 'Natur',
|
||||
'open-world': 'Open World',
|
||||
'otaku-dance': 'Otaku Dance',
|
||||
outro: 'Outro',
|
||||
overlay: 'Overlay',
|
||||
pipeline: 'Pipeline',
|
||||
'pose-reference': 'Pose Reference',
|
||||
portrait: 'Porträt',
|
||||
product: 'Produkt',
|
||||
'product-promo': 'Produkt-Promo',
|
||||
route: 'Route',
|
||||
saas: 'SaaS',
|
||||
sequence: 'Sequence',
|
||||
sizzle: 'Sizzle',
|
||||
social: 'Social',
|
||||
storyboard: 'Storyboard',
|
||||
'street-fighter': 'Street Fighter',
|
||||
tekken: 'Tekken',
|
||||
'three-kingdoms': 'Three Kingdoms',
|
||||
tiktok: 'TikTok',
|
||||
'title-card': 'Title Card',
|
||||
travel: 'Reise',
|
||||
tts: 'TTS',
|
||||
typography: 'Typografie',
|
||||
'unreal-engine-5': 'Unreal Engine 5',
|
||||
vertical: 'Vertikal',
|
||||
'video-reference': 'Video Reference',
|
||||
'vs-screen': 'VS Screen',
|
||||
'website-to-video': 'Website-zu-Video',
|
||||
wuxia: 'Wuxia',
|
||||
zhaoyun: 'Zhaoyun',
|
||||
};
|
||||
|
||||
const DE_PROMPT_TEMPLATE_COPY: Record<string, Partial<Pick<PromptTemplateSummary, 'summary' | 'title'>>> = {
|
||||
|
|
@ -416,6 +474,7 @@ const DE_PROMPT_TEMPLATE_COPY: Record<string, Partial<Pick<PromptTemplateSummary
|
|||
summary:
|
||||
'Erzeugt eine handgezeichnete Tourist Map im Aquarellstil mit nummerierten lokalen Spezialitäten, Sehenswürdigkeiten und Legende.',
|
||||
},
|
||||
'infographic-otaku-dance-choreography-breakdown-gokurakujodo-16-panels': {},
|
||||
'momotaro-explainer-slide-in-hybrid-style': {
|
||||
title: 'Momotaro-Erklärslide im Hybrid-Stil',
|
||||
summary:
|
||||
|
|
@ -556,6 +615,7 @@ const DE_PROMPT_TEMPLATE_COPY: Record<string, Partial<Pick<PromptTemplateSummary
|
|||
summary:
|
||||
'Warme Editorial-Seite zu einem japanischen Feiertag mit Anime-Charakterkunst, nostalgischer Showa-Straßenszene und Magazinlayout.',
|
||||
},
|
||||
'social-media-post-sensational-girl-dance-storyboard-8-shots': {},
|
||||
'social-media-post-social-media-fashion-outfit-generation': {
|
||||
title: 'Social-Media-Post - Fashion-Outfit-Generierung',
|
||||
summary:
|
||||
|
|
@ -671,6 +731,11 @@ const DE_PROMPT_TEMPLATE_COPY: Record<string, Partial<Pick<PromptTemplateSummary
|
|||
summary:
|
||||
'Komplexer Dark-Comedy-Prompt für Seedance 2.0 mit einem orangefarbenen Katzenbeamten und einem Hyänenkaiser in einer satirischen Qing-Dynastie-Szene.',
|
||||
},
|
||||
'game-screenshot-anime-fighting-game-captain-ryuuga-vs-kaze-renshin': {},
|
||||
'game-screenshot-three-kingdoms-guanyu-slaying-yanliang': {},
|
||||
'game-screenshot-three-kingdoms-lyubu-yuanmen-archery': {},
|
||||
'game-screenshot-three-kingdoms-zhaoyun-cradle-escape': {},
|
||||
'game-ui-ancient-china-open-world-mmo-hud': {},
|
||||
'hollywood-haute-couture-fantasy-video-prompt': {
|
||||
title: 'Hollywood-Haute-Couture-Fantasy-Video-Prompt',
|
||||
summary:
|
||||
|
|
@ -796,6 +861,9 @@ const DE_PROMPT_TEMPLATE_COPY: Record<string, Partial<Pick<PromptTemplateSummary
|
|||
summary:
|
||||
'Umfassender Seedance-2.0-Video-Prompt für einen anmutigen traditionellen Tanz auf Basis von Choreografie- und Identitätsreferenzbildern.',
|
||||
},
|
||||
'video-seedance-three-kingdoms-guanyu-slaying-yanliang': {},
|
||||
'video-seedance-three-kingdoms-lyubu-yuanmen-archery': {},
|
||||
'video-seedance-three-kingdoms-zhaoyun-cradle-escape': {},
|
||||
'vintage-disney-style-pirate-crocodile-animation': {
|
||||
title: 'Piraten-Krokodil-Animation im Vintage-Disney-Stil',
|
||||
summary:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|||
import { en } from './locales/en';
|
||||
import { LOCALES, LOCALE_LABEL, type Dict, type Locale } from './types';
|
||||
|
||||
const EXPECTED_LOCALES = ['en', 'de', 'zh-CN', 'zh-TW', 'pt-BR', 'es-ES', 'ru', 'fa', 'ja'];
|
||||
const EXPECTED_LOCALES = ['en', 'de', 'zh-CN', 'zh-TW', 'pt-BR', 'es-ES', 'ru', 'fa', 'ja', 'ko'];
|
||||
|
||||
function placeholders(value: string): string[] {
|
||||
const names: string[] = [];
|
||||
|
|
|
|||
|
|
@ -471,6 +471,7 @@ export const de: Dict = {
|
|||
'fileViewer.download': 'Herunterladen',
|
||||
'fileViewer.open': 'Öffnen',
|
||||
'fileViewer.imageMeta': 'Bild · {size}',
|
||||
'fileViewer.reactMeta': 'React-Komponente · {size}',
|
||||
'fileViewer.sketchMeta': 'Sketch · {size}',
|
||||
'fileViewer.markdownStreamingMeta': 'Streaming-Vorschau…',
|
||||
'fileViewer.markdownErrorMeta': 'Vorschau ist möglicherweise unvollständig (Generierungsfehler).',
|
||||
|
|
@ -511,6 +512,8 @@ export const de: Dict = {
|
|||
'fileViewer.exportPptxNa': 'PPTX-Export ist hier nicht verfügbar.',
|
||||
'fileViewer.exportZip': 'Als .zip herunterladen',
|
||||
'fileViewer.exportHtml': 'Als eigenständiges HTML exportieren',
|
||||
'fileViewer.exportJsx': 'Als JSX exportieren',
|
||||
'fileViewer.exportReactHtml': 'Vorschau als HTML exportieren',
|
||||
'fileViewer.saveAsTemplate': 'Als Template speichern…',
|
||||
'fileViewer.savingTemplate': 'Template wird gespeichert…',
|
||||
'fileViewer.savedTemplate': 'Als „{name}“ gespeichert',
|
||||
|
|
|
|||
|
|
@ -472,6 +472,7 @@ export const esES: Dict = {
|
|||
'fileViewer.download': 'Descargar',
|
||||
'fileViewer.open': 'Abrir',
|
||||
'fileViewer.imageMeta': 'Imagen · {size}',
|
||||
'fileViewer.reactMeta': 'Componente React · {size}',
|
||||
'fileViewer.sketchMeta': 'Boceto · {size}',
|
||||
'fileViewer.markdownStreamingMeta': 'Vista previa en streaming…',
|
||||
'fileViewer.markdownErrorMeta': 'La vista previa puede estar incompleta (error de generación).',
|
||||
|
|
@ -512,6 +513,8 @@ export const esES: Dict = {
|
|||
'fileViewer.exportPptxNa': 'La exportación a PPTX no está disponible aquí.',
|
||||
'fileViewer.exportZip': 'Descargar como .zip',
|
||||
'fileViewer.exportHtml': 'Exportar como HTML independiente',
|
||||
'fileViewer.exportJsx': 'Exportar como JSX',
|
||||
'fileViewer.exportReactHtml': 'Exportar vista previa como HTML',
|
||||
'fileViewer.saveAsTemplate': 'Guardar como plantilla…',
|
||||
'fileViewer.savingTemplate': 'Guardando plantilla…',
|
||||
'fileViewer.savedTemplate': 'Guardado como «{name}»',
|
||||
|
|
|
|||
|
|
@ -470,6 +470,7 @@ export const ja: Dict = {
|
|||
'fileViewer.download': 'ダウンロード',
|
||||
'fileViewer.open': '開く',
|
||||
'fileViewer.imageMeta': '画像 · {size}',
|
||||
'fileViewer.reactMeta': 'React コンポーネント · {size}',
|
||||
'fileViewer.sketchMeta': 'スケッチ · {size}',
|
||||
'fileViewer.markdownStreamingMeta': 'ストリーミングプレビュー中…',
|
||||
'fileViewer.markdownErrorMeta': 'プレビューが不完全な場合があります(生成エラー)。',
|
||||
|
|
@ -510,6 +511,8 @@ export const ja: Dict = {
|
|||
'fileViewer.exportPptxNa': 'ここでは PPTX エクスポートは利用できません。',
|
||||
'fileViewer.exportZip': '.zip としてダウンロード',
|
||||
'fileViewer.exportHtml': 'スタンドアロン HTML としてエクスポート',
|
||||
'fileViewer.exportJsx': 'JSX としてエクスポート',
|
||||
'fileViewer.exportReactHtml': 'プレビューを HTML としてエクスポート',
|
||||
'fileViewer.saveAsTemplate': 'テンプレートとして保存…',
|
||||
'fileViewer.savingTemplate': 'テンプレートを保存中…',
|
||||
'fileViewer.savedTemplate': '"{name}" として保存しました',
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ export const ko: Dict = {
|
|||
'settings.show': '표시',
|
||||
'settings.hide': '숨기기',
|
||||
'settings.model': '모델',
|
||||
'settings.maxTokens': '최대 토큰 수 (선택 사항)',
|
||||
'settings.maxTokensHint':
|
||||
'응답 길이 상한입니다. 각 모델에는 기본값이 미리 조정되어 있으며(placeholder로 표시됨), 비워 두면 그 값을 사용하고 숫자를 입력하면 덮어씁니다.',
|
||||
'settings.baseUrl': 'Base URL',
|
||||
'settings.apiHint':
|
||||
'이 브라우저에서 설정한 Base URL로 직접 호출됩니다. 프록시는 사용되지 않으며, 키는 localStorage에만 보관됩니다.',
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export const tr: Dict = {
|
|||
'promptTemplates.openSource': 'Orijinali görüntüle',
|
||||
'promptTemplates.openFullscreen': 'Tam ekran ön izlemeyi aç',
|
||||
'promptTemplates.closeFullscreen': 'Tam ekran ön izlemeyi kapat',
|
||||
'promptTemplates.retry': 'Yeniden dene',
|
||||
|
||||
'newproj.tabPrototype': 'Prototip',
|
||||
'newproj.tabDeck': 'Slayt gösterisi',
|
||||
|
|
@ -209,6 +210,16 @@ export const tr: Dict = {
|
|||
'newproj.audioDurationSeconds': '{n}s',
|
||||
'newproj.voiceLabel': 'Ses',
|
||||
'newproj.voicePlaceholder': 'Sağlayıcı ses kimliği, opsiyonel',
|
||||
'newproj.promptTemplateLabel': 'Referans şablon',
|
||||
'newproj.promptTemplateNoneTitle': 'Hiçbiri — kendin yaz',
|
||||
'newproj.promptTemplateNoneSub': 'Galeriyi atla, kendi brief’ini açıkla',
|
||||
'newproj.promptTemplateRefSub': 'Referans şablon',
|
||||
'newproj.promptTemplateSearch': 'Şablon ara…',
|
||||
'newproj.promptTemplateEmpty': 'Bu yüzey için henüz şablon yüklenmedi.',
|
||||
'newproj.promptTemplateBodyLabel': 'İstem (istediğin gibi düzenleyebilirsin)',
|
||||
'newproj.promptTemplateOptimizeHint':
|
||||
'İstediğin her şeyi düzenle — değişikliklerin ajan brief’ine taşınır.',
|
||||
'newproj.promptTemplateBodyEmpty': 'Gövde boş — ajana şablon referansı gitmeyecek.',
|
||||
|
||||
'designs.subRecent': 'Yakında',
|
||||
'designs.subYours': 'Tasarımların',
|
||||
|
|
@ -386,7 +397,7 @@ export const tr: Dict = {
|
|||
'workspace.deleteFileConfirm': '"{name}"ı proje klasöründen sil?',
|
||||
'workspace.openFromDesignFiles': 'bir dosya aç',
|
||||
'workspace.designFilesLink': 'Tasarım Dosyaları',
|
||||
'workspace.loadingSketch': 'Taslak yükleniyor…',',
|
||||
'workspace.loadingSketch': 'Taslak yükleniyor…',
|
||||
'designFiles.title': 'Tasarım Dosyaları',
|
||||
'designFiles.upload': 'Dosyaları yükle',
|
||||
'designFiles.pasteText': 'Metin dosyası olarak yapıştır',
|
||||
|
|
@ -459,6 +470,7 @@ export const tr: Dict = {
|
|||
'fileViewer.download': 'İndir',
|
||||
'fileViewer.open': 'Aç',
|
||||
'fileViewer.imageMeta': 'Görsel · {size}',
|
||||
'fileViewer.reactMeta': 'React bileşeni · {size}',
|
||||
'fileViewer.sketchMeta': 'Taslak · {size}',
|
||||
'fileViewer.markdownStreamingMeta': 'Önizleme yayınlanıyor…',
|
||||
'fileViewer.markdownErrorMeta': 'Önizleme eksik olabilir (oluşturma hatası).',
|
||||
|
|
@ -499,6 +511,8 @@ export const tr: Dict = {
|
|||
'fileViewer.exportPptxNa': 'PPTX dışa aktarma burada mevcut değil.',
|
||||
'fileViewer.exportZip': 'ZIP olarak indir',
|
||||
'fileViewer.exportHtml': 'Tekil HTML olarak dışa aktar',
|
||||
'fileViewer.exportJsx': 'JSX olarak dışa aktar',
|
||||
'fileViewer.exportReactHtml': 'Önizlemeyi HTML olarak dışa aktar',
|
||||
'fileViewer.saveAsTemplate': 'Şablon olarak kaydet…',
|
||||
'fileViewer.savingTemplate': 'Şablon kaydediliyor…',
|
||||
'fileViewer.savedTemplate': '"{name}" olarak kaydedildi',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ export interface Dict {
|
|||
'settings.show': string;
|
||||
'settings.hide': string;
|
||||
'settings.model': string;
|
||||
'settings.maxTokens': string;
|
||||
'settings.maxTokensHint': string;
|
||||
'settings.baseUrl': string;
|
||||
'settings.apiHint': string;
|
||||
'settings.skipForNow': string;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type http from 'node:http';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
|
|
@ -19,17 +20,12 @@ interface AgentsResponse {
|
|||
agents: AgentInfo[];
|
||||
}
|
||||
|
||||
interface StartServerResult {
|
||||
url: string;
|
||||
server: { close: (callback: (err?: Error) => void) => void };
|
||||
}
|
||||
|
||||
interface ParsedSseEvent {
|
||||
event: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type StartServer = (options: { port: number; returnServer: true }) => Promise<StartServerResult>;
|
||||
type StartServer = (options: { port: number; returnServer: true }) => Promise<http.Server | undefined>;
|
||||
type CloseDatabase = () => void;
|
||||
|
||||
const liveTimeoutMs = Number(process.env.OD_RUNTIME_LIVE_TIMEOUT_MS || 180_000);
|
||||
|
|
@ -38,7 +34,7 @@ const maxRuntimeCount = 8;
|
|||
const marker = 'OD_RUNTIME_ADAPTER_LIVE_OK';
|
||||
|
||||
let baseUrl: string;
|
||||
let server: StartServerResult['server'] | undefined;
|
||||
let server: http.Server | undefined;
|
||||
let startServer: StartServer;
|
||||
let closeDatabase: CloseDatabase | undefined;
|
||||
let detectedAgents: AgentInfo[] | undefined;
|
||||
|
|
@ -50,8 +46,15 @@ test.before(async () => {
|
|||
({ startServer } = await import('../../apps/daemon/dist/server.js') as { startServer: StartServer });
|
||||
({ closeDatabase } = await import('../../apps/daemon/dist/db.js') as { closeDatabase: CloseDatabase });
|
||||
const started = await startServer({ port: 0, returnServer: true });
|
||||
baseUrl = started.url;
|
||||
server = started.server;
|
||||
if (started == null) {
|
||||
throw new Error('startServer did not return a server handle');
|
||||
}
|
||||
const address = started.address();
|
||||
if (address == null || typeof address === 'string') {
|
||||
throw new Error('startServer did not bind to a TCP port');
|
||||
}
|
||||
server = started;
|
||||
baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue