diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 2bd5a0a98..000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2db296457..c316fa59a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,8 +14,8 @@ This guide tells you exactly where to look for each type of contribution and wha |---|---|---|---| | Make OD render a new kind of artifact (an invoice, an iOS Settings screen, a one-pager…) | a **Skill** | [`skills//`](skills/) | one folder, ~2 files | | Make OD speak a new brand's visual language | a **Design System** | [`design-systems//DESIGN.md`](design-systems/) | one Markdown file | -| Hook up a new coding-agent CLI | an **Agent adapter** | [`daemon/agents.js`](daemon/agents.js) | ~10 lines in one array | -| Add a feature, fix a bug, lift a UX pattern from [`open-codesign`][ocod] | code | `src/`, `daemon/` | normal PR | +| Hook up a new coding-agent CLI | an **Agent adapter** | [`apps/daemon/agents.js`](apps/daemon/agents.js) | ~10 lines in one array | +| Add a feature, fix a bug, lift a UX pattern from [`open-codesign`][ocod] | code | `apps/web/src/`, `apps/daemon/` | normal PR | | Improve docs, port a section to 中文, fix typos | docs | `README.md`, `README.zh-CN.md`, `docs/`, `QUICKSTART.md` | one PR | If you're not sure which bucket your idea is in, [open a discussion / issue first](https://github.com/nexu-io/open-design/issues/new) and we'll point you at the right surface. @@ -169,7 +169,7 @@ The 69 product systems we ship are imported from [`VoltAgent/awesome-design-md`] ## Adding a new coding-agent CLI -Hooking up a new agent (e.g. some new shop's `foo-coder` CLI) is one entry in [`daemon/agents.js`](daemon/agents.js): +Hooking up a new agent (e.g. some new shop's `foo-coder` CLI) is one entry in [`apps/daemon/agents.js`](apps/daemon/agents.js): ```javascript { @@ -182,7 +182,7 @@ Hooking up a new agent (e.g. some new shop's `foo-coder` CLI) is one entry in [` } ``` -That's it — daemon will detect it on `PATH`, the picker shows it, the chat path works. If the CLI emits **typed events** (like Claude Code's `--output-format stream-json`), wire a parser in [`daemon/claude-stream.js`](daemon/claude-stream.js) and set `streamFormat: 'claude-stream-json'`. +That's it — daemon will detect it on `PATH`, the picker shows it, the chat path works. If the CLI emits **typed events** (like Claude Code's `--output-format stream-json`), wire a parser in [`apps/daemon/claude-stream.js`](apps/daemon/claude-stream.js) and set `streamFormat: 'claude-stream-json'`. Bar for merging: @@ -202,7 +202,7 @@ We're not pedantic about formatting (Prettier on save is fine), but two rules ar Beyond that: - **Don't narrate.** No `// import the module`, no `// loop through items`. If the code reads obviously, the comment is noise. Save comments for non-obvious intent or constraints the code can't express. -- **TypeScript** for `src/`. The daemon (`daemon/`) is plain ESM JavaScript with JSDoc when types matter — keep it that way. +- **TypeScript** for `apps/web/src/`. The daemon (`apps/daemon/`) is plain ESM JavaScript with JSDoc when types matter — keep it that way. - **No new top-level dependencies** without a paragraph in the PR description on what we get vs. what bytes we ship. The dep list in [`package.json`](package.json) is small on purpose. - **Run `pnpm typecheck`** before pushing. CI runs it; failing it earns a "please fix" comment. diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index 07cece70a..a77e0a1a5 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -14,8 +14,8 @@ |---|---|---|---| | 让 OD 渲染一种新的 artifact(一份发票、一个 iOS 设置页、一张 one-pager……) | 一个 **Skill** | [`skills//`](skills/) | 一个文件夹,约 2 个文件 | | 让 OD 说一种新品牌的视觉语言 | 一套 **Design System** | [`design-systems//DESIGN.md`](design-systems/) | 一个 Markdown 文件 | -| 接入一个新的 coding-agent CLI | 一个 **Agent adapter** | [`daemon/agents.js`](daemon/agents.js) | 一个数组里 ~10 行 | -| 加功能、修 bug、从 [`open-codesign`][ocod] 移植一个 UX 模式 | 代码 | `src/`、`daemon/` | 普通 PR | +| 接入一个新的 coding-agent CLI | 一个 **Agent adapter** | [`apps/daemon/agents.js`](apps/daemon/agents.js) | 一个数组里 ~10 行 | +| 加功能、修 bug、从 [`open-codesign`][ocod] 移植一个 UX 模式 | 代码 | `apps/web/src/`、`apps/daemon/` | 普通 PR | | 改文档、补中文翻译、修错别字 | 文档 | `README.md`、`README.zh-CN.md`、`docs/`、`QUICKSTART.md` | 一个 PR | 不确定自己想做的属于哪一桶?[先开 issue / discussion](https://github.com/nexu-io/open-design/issues/new),我们告诉你该改哪个面。 @@ -168,7 +168,7 @@ design-systems/your-brand/ ## 接入一个新的 coding-agent CLI -接入一个新 agent(比如某个新 shop 的 `foo-coder` CLI)就是在 [`daemon/agents.js`](daemon/agents.js) 里加一项: +接入一个新 agent(比如某个新 shop 的 `foo-coder` CLI)就是在 [`apps/daemon/agents.js`](apps/daemon/agents.js) 里加一项: ```javascript { @@ -181,7 +181,7 @@ design-systems/your-brand/ } ``` -完事 —— daemon 会在 `PATH` 上检测到它、picker 显示出来、对话路径就通了。如果这个 CLI 吐 **类型化事件**(像 Claude Code 的 `--output-format stream-json`),在 [`daemon/claude-stream.js`](daemon/claude-stream.js) 里写一个 parser,并把 `streamFormat` 设成 `'claude-stream-json'`。 +完事 —— daemon 会在 `PATH` 上检测到它、picker 显示出来、对话路径就通了。如果这个 CLI 吐 **类型化事件**(像 Claude Code 的 `--output-format stream-json`),在 [`apps/daemon/claude-stream.js`](apps/daemon/claude-stream.js) 里写一个 parser,并把 `streamFormat` 设成 `'claude-stream-json'`。 合并硬线: @@ -201,7 +201,7 @@ design-systems/your-brand/ 除此之外: - **不要写废话注释。** 不要 `// 引入这个模块`、不要 `// 遍历元素`。如果代码本身一眼能读,注释就是噪音。注释只用来说明非显而易见的意图、或者代码本身表达不出来的约束。 -- **`src/` 用 TypeScript。** Daemon (`daemon/`) 是纯 ESM JavaScript,类型重要的地方用 JSDoc —— 保持这样。 +- **`apps/web/src/` 用 TypeScript。** Daemon (`apps/daemon/`) 是纯 ESM JavaScript,类型重要的地方用 JSDoc —— 保持这样。 - **不要随便加顶层依赖。** PR 描述里至少要有一段,说明引入它能换到什么、又新增了多少 bundle 字节。[`package.json`](package.json) 的依赖少是有意为之。 - **推之前跑 `pnpm typecheck`。** CI 会跑;挂了会换来一句「请修一下」。 diff --git a/QUICKSTART.md b/QUICKSTART.md index a6cab4aaa..794e0ff9e 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -114,7 +114,7 @@ open-design/ ## Troubleshooting - **"no agents found on PATH"** — install one of: `claude`, `codex`, `gemini`, `opencode`, `cursor-agent`, `qwen`, `copilot`. Or switch to "Anthropic API · BYOK" in the top bar and paste a key in **Settings**. -- **daemon 500 on /api/chat** — check the daemon terminal for the stderr tail; usually the CLI rejected its args. Different CLIs take different argv shapes; see `daemon/agents.js` `buildArgs` if you need to tweak. +- **daemon 500 on /api/chat** — check the daemon terminal for the stderr tail; usually the CLI rejected its args. Different CLIs take different argv shapes; see `apps/daemon/agents.js` `buildArgs` if you need to tweak. - **artifact never renders** — the model produced text without wrapping in ``. Confirm the system prompt is going through (check daemon log) and consider switching to a more capable model or a stricter skill. ## Mapping back to the vision @@ -122,6 +122,6 @@ open-design/ This Quickstart is the runnable seed of the spec in [`docs/`](docs/). The spec describes where this grows (see [`docs/roadmap.md`](docs/roadmap.md)). Highlights: - `docs/architecture.md` now matches the shipped stack: Next.js 16 App Router in front, local daemon behind it, and `next.config.ts` rewrites in dev to keep the browser talking to the same `/api` surface. -- `docs/skills-protocol.md` describes the full `od:` frontmatter (typed inputs, sliders, capability gating). This MVP reads `name` / `description` / `triggers` / `od.mode` / `od.design_system.requires` only — extend `daemon/skills.js` to add the rest. -- `docs/agent-adapters.md` foresees richer dispatch (capability detection, streaming tool-calls). Our `daemon/agents.js` is a minimal dispatcher — enough to prove the wiring. +- `docs/skills-protocol.md` describes the full `od:` frontmatter (typed inputs, sliders, capability gating). This MVP reads `name` / `description` / `triggers` / `od.mode` / `od.design_system.requires` only — extend `apps/daemon/skills.js` to add the rest. +- `docs/agent-adapters.md` foresees richer dispatch (capability detection, streaming tool-calls). Our `apps/daemon/agents.js` is a minimal dispatcher — enough to prove the wiring. - `docs/modes.md` lists four modes: prototype / deck / template / design-system. We ship skills for the first two; the picker already filters by `mode`. diff --git a/README.md b/README.md index db574f11f..fb1577228 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ That's not "AI tries to design something". That's an AI that has been trained, b OD stands on four open-source shoulders: -- [**`alchaincyf/huashu-design`**](https://github.com/alchaincyf/huashu-design) — the design-philosophy compass. Junior-Designer workflow, the 5-step brand-asset protocol, the anti-AI-slop checklist, the 5-dimensional self-critique, and the "5 schools × 20 design philosophies" idea behind our direction picker — all distilled into [`src/prompts/discovery.ts`](src/prompts/discovery.ts). +- [**`alchaincyf/huashu-design`**](https://github.com/alchaincyf/huashu-design) — the design-philosophy compass. Junior-Designer workflow, the 5-step brand-asset protocol, the anti-AI-slop checklist, the 5-dimensional self-critique, and the "5 schools × 20 design philosophies" idea behind our direction picker — all distilled into [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts). - [**`op7418/guizang-ppt-skill`**](https://github.com/op7418/guizang-ppt-skill) — the deck mode. Bundled verbatim under [`skills/guizang-ppt/`](skills/guizang-ppt/) with original LICENSE preserved; magazine-style layouts, WebGL hero, P0/P1/P2 checklists. - [**`OpenCoworkAI/open-codesign`**](https://github.com/OpenCoworkAI/open-codesign) — the UX north star and our closest peer. The first open-source Claude-Design alternative. We borrow its streaming-artifact loop, its sandboxed-iframe preview pattern (vendored React 18 + Babel), its live agent panel (todos + tool calls + interruptible generation), and its five-format export list (HTML / PDF / PPTX / ZIP / Markdown). We deliberately diverge on form factor — they are a desktop Electron app bundling [`pi-ai`][piai]; we are a web app + local daemon that delegates to your existing CLI. - [**`multica-ai/multica`**](https://github.com/multica-ai/multica) — the daemon-and-runtime architecture. PATH-scan agent detection, the local daemon as the only privileged process, the agent-as-teammate worldview. @@ -214,7 +214,7 @@ DISCOVERY directives (turn-1 form, turn-2 brand branch, TodoWrite, 5-dim critiq + (deck kind, no skill seed) DECK_FRAMEWORK_DIRECTIVE (nav / counter / scroll / print) ``` -Every layer is composable. Every layer is a file you can edit. Read [`src/prompts/system.ts`](src/prompts/system.ts) and [`src/prompts/discovery.ts`](src/prompts/discovery.ts) to see the actual contract. +Every layer is composable. Every layer is a file you can edit. Read [`apps/web/src/prompts/system.ts`](apps/web/src/prompts/system.ts) and [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts) to see the actual contract. ## Architecture @@ -453,11 +453,11 @@ When the user has no brand spec, the agent emits a second form with five curated | Brutalist | Raw, oversized type, no shadows, harsh accents | Bloomberg Businessweek · Achtung | | Soft warm | Generous, low contrast, peachy neutrals | Notion marketing · Apple Health | -Full spec → [`src/prompts/directions.ts`](src/prompts/directions.ts). +Full spec → [`apps/web/src/prompts/directions.ts`](apps/web/src/prompts/directions.ts). ## Anti-AI-slop machinery -The whole machinery below is the [`huashu-design`](https://github.com/alchaincyf/huashu-design) playbook, ported into OD's prompt-stack and made enforceable per-skill via the side-file pre-flight. Read [`src/prompts/discovery.ts`](src/prompts/discovery.ts) for the live wording: +The whole machinery below is the [`huashu-design`](https://github.com/alchaincyf/huashu-design) playbook, ported into OD's prompt-stack and made enforceable per-skill via the side-file pre-flight. Read [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts) for the live wording: - **Question form first.** Turn 1 is `` only — no thinking, no tools, no narration. The user chooses defaults at radio speed. - **Brand-spec extraction.** When the user attaches a screenshot or URL, the agent runs a five-step protocol (locate · download · grep hex · codify `brand-spec.md` · vocalise) before writing CSS. **Never guesses brand colors from memory.** @@ -511,7 +511,7 @@ Auto-detected from `PATH` on daemon boot. No config required. | [GitHub Copilot CLI](https://github.com/features/copilot/cli) | `copilot` | `--output-format json` (typed events) | `copilot -p --allow-all-tools --output-format json` | | Anthropic API · BYOK | n/a | SSE direct | Browser fallback when no CLI is on PATH | -Adding a new CLI is one entry in [`daemon/agents.js`](daemon/agents.js). Streaming format is one of `claude-stream-json` (typed events) or `plain` (raw text). +Adding a new CLI is one entry in [`apps/daemon/agents.js`](apps/daemon/agents.js). Streaming format is one of `claude-stream-json` (typed events) or `plain` (raw text). ## References & lineage @@ -520,7 +520,7 @@ Every external project this repo borrows from. Each link goes to the source so y | Project | Role here | |---|---| | [`Claude Design`][cd] | The closed-source product this repo is the open-source alternative to. | -| [**`alchaincyf/huashu-design`**](https://github.com/alchaincyf/huashu-design) | The design-philosophy core. Junior-Designer workflow, the 5-step brand-asset protocol, anti-AI-slop checklist, 5-dimensional self-critique, and the "5 schools × 20 design philosophies" library behind our direction picker — all distilled into [`src/prompts/discovery.ts`](src/prompts/discovery.ts) and [`src/prompts/directions.ts`](src/prompts/directions.ts). | +| [**`alchaincyf/huashu-design`**](https://github.com/alchaincyf/huashu-design) | The design-philosophy core. Junior-Designer workflow, the 5-step brand-asset protocol, anti-AI-slop checklist, 5-dimensional self-critique, and the "5 schools × 20 design philosophies" library behind our direction picker — all distilled into [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts) and [`apps/web/src/prompts/directions.ts`](apps/web/src/prompts/directions.ts). | | [**`op7418/guizang-ppt-skill`**][guizang] | Magazine-web-PPT skill bundled verbatim under [`skills/guizang-ppt/`](skills/guizang-ppt/) with original LICENSE preserved. Default for deck mode. P0/P1/P2 checklist culture borrowed for every other skill. | | [**`multica-ai/multica`**](https://github.com/multica-ai/multica) | The daemon + adapter architecture. PATH-scan agent detection, local daemon as the only privileged process, agent-as-teammate worldview. We adopt the model; we do not vendor the code. | | [**`OpenCoworkAI/open-codesign`**][ocod] | The first open-source Claude-Design alternative and our closest peer. UX patterns adopted: streaming-artifact loop, sandboxed-iframe preview (vendored React 18 + Babel), live agent panel (todos + tool calls + interruptible), five-format export list (HTML/PDF/PPTX/ZIP/Markdown), local-first storage hub, `SKILL.md` taste-injection. UX patterns on our roadmap: comment-mode surgical edits, AI-emitted tweaks panel. **We deliberately do not vendor [`pi-ai`][piai]** — open-codesign bundles it as the agent runtime; we delegate to whichever CLI the user already has. | @@ -562,7 +562,7 @@ Issues, PRs, new skills, and new design systems are all welcome. The highest-lev - **Add a skill** — drop a folder into [`skills/`](skills/) following the [`SKILL.md`][skill] convention. - **Add a design system** — drop a `DESIGN.md` into [`design-systems//`](design-systems/) using the 9-section schema. -- **Wire up a new coding-agent CLI** — one entry in [`daemon/agents.js`](daemon/agents.js). +- **Wire up a new coding-agent CLI** — one entry in [`apps/daemon/agents.js`](apps/daemon/agents.js). Full walkthrough, bar-for-merging, code style, and what we don't accept → [`CONTRIBUTING.md`](CONTRIBUTING.md) ([简体中文](CONTRIBUTING.zh-CN.md)). diff --git a/README.zh-CN.md b/README.zh-CN.md index 25a9da271..8cbe424ab 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -30,7 +30,7 @@ Anthropic 的 [Claude Design][cd](2026-04-17 发布,基于 Opus 4.7)让大 OD 站在四个开源项目的肩膀上: -- [**`alchaincyf/huashu-design`**(花叔的画术)](https://github.com/alchaincyf/huashu-design) —— 设计哲学的指南针。Junior-Designer 工作流、5 步品牌资产协议、anti-AI-slop checklist、五维自评审、以及方向选择器背后的「5 流派 × 20 种设计哲学」思路 —— 全部蒸馏进 [`src/prompts/discovery.ts`](src/prompts/discovery.ts)。 +- [**`alchaincyf/huashu-design`**(花叔的画术)](https://github.com/alchaincyf/huashu-design) —— 设计哲学的指南针。Junior-Designer 工作流、5 步品牌资产协议、anti-AI-slop checklist、五维自评审、以及方向选择器背后的「5 流派 × 20 种设计哲学」思路 —— 全部蒸馏进 [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts)。 - [**`op7418/guizang-ppt-skill`**(歸藏的杂志风 PPT skill)](https://github.com/op7418/guizang-ppt-skill) —— Deck 模式。原样捆绑在 [`skills/guizang-ppt/`](skills/guizang-ppt/) 下,原 LICENSE 保留;杂志版式、WebGL hero、P0/P1/P2 checklist。 - [**`OpenCoworkAI/open-codesign`**](https://github.com/OpenCoworkAI/open-codesign) —— UX 北极星,也是我们最接近的同类。第一个开源的 Claude-Design 替代品。我们借鉴了它的流式 artifact 循环、沙盒 iframe 预览模式(自带 React 18 + Babel)、实时 agent 面板(todos + tool calls + 可中断生成)、5 种导出格式列表(HTML / PDF / PPTX / ZIP / Markdown)。我们刻意在形态上分流 —— 它是桌面 Electron 应用,把 [`pi-ai`][piai] 打包进去做 agent;我们是 Web 应用 + 本地 daemon,把 agent 运行时**委托**给你已经装好的 CLI。 - [**`multica-ai/multica`**](https://github.com/multica-ai/multica) —— Daemon 与运行时架构。PATH 扫描式 agent 检测,本地 daemon 作为唯一的特权进程,agent-as-teammate 的世界观。 @@ -214,7 +214,7 @@ DISCOVERY 指令 (turn-1 表单、turn-2 品牌分支、TodoWrite、 + (deck kind 且无 skill 种子时) DECK_FRAMEWORK_DIRECTIVE (nav / counter / scroll / print) ``` -每一层都可组合。每一层都是一个你能改的文件。看 [`src/prompts/system.ts`](src/prompts/system.ts) 和 [`src/prompts/discovery.ts`](src/prompts/discovery.ts) 就知道真实契约长什么样。 +每一层都可组合。每一层都是一个你能改的文件。看 [`apps/web/src/prompts/system.ts`](apps/web/src/prompts/system.ts) 和 [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts) 就知道真实契约长什么样。 ## 技术架构 @@ -453,11 +453,11 @@ open-design/ | Brutalist | 粗粝、巨字、无阴影、刺眼强调 | Bloomberg Businessweek · Achtung | | Soft warm | 大方、低对比、桃色中性 | Notion 营销页 · Apple Health | -完整 spec → [`src/prompts/directions.ts`](src/prompts/directions.ts)。 +完整 spec → [`apps/web/src/prompts/directions.ts`](apps/web/src/prompts/directions.ts)。 ## 反 AI Slop 机制 -下面整套机制都是 [`huashu-design`](https://github.com/alchaincyf/huashu-design) 的 playbook,被移植进 OD 的提示词栈,并通过 skill 副文件 pre-flight 让每个 skill 都能落地执行。看 [`src/prompts/discovery.ts`](src/prompts/discovery.ts) 是真实文案: +下面整套机制都是 [`huashu-design`](https://github.com/alchaincyf/huashu-design) 的 playbook,被移植进 OD 的提示词栈,并通过 skill 副文件 pre-flight 让每个 skill 都能落地执行。看 [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts) 是真实文案: - **先表单。** Turn 1 必须是 ``,**不准** thinking、不准 tools、不准旁白。用户用 radio 速度选默认。 - **品牌资产协议。** 用户贴截图或 URL 时,agent 走 5 步流程(定位 · 下载 · grep hex · 写 `brand-spec.md` · 复述)才能开始写 CSS。**绝不从记忆里猜品牌色**。 @@ -511,7 +511,7 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。 | [GitHub Copilot CLI](https://github.com/features/copilot/cli) | `copilot` | `--output-format json`(类型化事件) | `copilot -p --allow-all-tools --output-format json` | | Anthropic API · BYOK | n/a | SSE 直连 | 没装任何 CLI 时的浏览器兜底 | -加一个新 CLI = 在 [`daemon/agents.js`](daemon/agents.js) 里加一项。流式格式从 `claude-stream-json`(类型化事件)和 `plain`(原始文本)两种里选一个。 +加一个新 CLI = 在 [`apps/daemon/agents.js`](apps/daemon/agents.js) 里加一项。流式格式从 `claude-stream-json`(类型化事件)和 `plain`(原始文本)两种里选一个。 ## 引用与师承 @@ -520,7 +520,7 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。 | 项目 | 在这里的角色 | |---|---| | [`Claude Design`][cd] | 本仓库为之提供开源替代的闭源产品。 | -| [**`alchaincyf/huashu-design`**(花叔的画术)](https://github.com/alchaincyf/huashu-design) | 设计哲学的核心。Junior-Designer 工作流、5 步品牌资产协议、anti-AI-slop checklist、五维自评审、以及方向选择器背后的「5 流派 × 20 种设计哲学」库 —— 全部蒸馏进 [`src/prompts/discovery.ts`](src/prompts/discovery.ts) 与 [`src/prompts/directions.ts`](src/prompts/directions.ts)。 | +| [**`alchaincyf/huashu-design`**(花叔的画术)](https://github.com/alchaincyf/huashu-design) | 设计哲学的核心。Junior-Designer 工作流、5 步品牌资产协议、anti-AI-slop checklist、五维自评审、以及方向选择器背后的「5 流派 × 20 种设计哲学」库 —— 全部蒸馏进 [`apps/web/src/prompts/discovery.ts`](apps/web/src/prompts/discovery.ts) 与 [`apps/web/src/prompts/directions.ts`](apps/web/src/prompts/directions.ts)。 | | [**`op7418/guizang-ppt-skill`**(歸藏)][guizang] | Magazine-web-PPT skill 原样捆绑在 [`skills/guizang-ppt/`](skills/guizang-ppt/) 下,原 LICENSE 保留。Deck 模式默认。P0/P1/P2 checklist 文化也被借给了所有其他 skill。 | | [**`multica-ai/multica`**](https://github.com/multica-ai/multica) | Daemon + adapter 架构。PATH 扫描式 agent 检测、本地 daemon 作为唯一特权进程、agent-as-teammate 世界观。我们采纳模型,不 vendor 代码。 | | [**`OpenCoworkAI/open-codesign`**][ocod] | 第一个开源的 Claude-Design 替代品,也是我们最接近的同类。已采纳的 UX 模式:流式 artifact 循环、沙盒 iframe 预览(自带 React 18 + Babel)、实时 agent 面板(todos + tool calls + 可中断)、5 种导出格式列表(HTML/PDF/PPTX/ZIP/Markdown)、本地优先的 designs hub、`SKILL.md` 品味注入。路线图上的 UX 模式:评论模式手术刀编辑、AI 自吐 tweaks 面板。**我们刻意不 vendor [`pi-ai`][piai]** —— open-codesign 把它打包成 agent 运行时;我们则委托给用户已经装好的 CLI。 | @@ -562,7 +562,7 @@ Daemon 启动时从 `PATH` 自动检测,无需配置。 - **加一个 skill** —— 往 [`skills/`](skills/) 丢一个文件夹,遵循 [`SKILL.md`][skill] 规范。 - **加一套 design system** —— 往 [`design-systems//`](design-systems/) 丢一份 `DESIGN.md`,用 9 段式 schema。 -- **接入一个新的 coding-agent CLI** —— 在 [`daemon/agents.js`](daemon/agents.js) 里加一项。 +- **接入一个新的 coding-agent CLI** —— 在 [`apps/daemon/agents.js`](apps/daemon/agents.js) 里加一项。 完整流程、合并硬线、代码风格、我们不接收的 PR 类型 → [`CONTRIBUTING.zh-CN.md`](CONTRIBUTING.zh-CN.md)([English](CONTRIBUTING.md))。 diff --git a/daemon/acp.js b/apps/daemon/acp.js similarity index 100% rename from daemon/acp.js rename to apps/daemon/acp.js diff --git a/daemon/agents.js b/apps/daemon/agents.js similarity index 100% rename from daemon/agents.js rename to apps/daemon/agents.js diff --git a/daemon/artifact-manifest.js b/apps/daemon/artifact-manifest.js similarity index 100% rename from daemon/artifact-manifest.js rename to apps/daemon/artifact-manifest.js diff --git a/daemon/artifact-manifest.test.ts b/apps/daemon/artifact-manifest.test.ts similarity index 100% rename from daemon/artifact-manifest.test.ts rename to apps/daemon/artifact-manifest.test.ts diff --git a/daemon/claude-design-import.js b/apps/daemon/claude-design-import.js similarity index 100% rename from daemon/claude-design-import.js rename to apps/daemon/claude-design-import.js diff --git a/daemon/claude-stream.js b/apps/daemon/claude-stream.js similarity index 100% rename from daemon/claude-stream.js rename to apps/daemon/claude-stream.js diff --git a/daemon/cli.js b/apps/daemon/cli.js similarity index 100% rename from daemon/cli.js rename to apps/daemon/cli.js diff --git a/daemon/copilot-stream.js b/apps/daemon/copilot-stream.js similarity index 100% rename from daemon/copilot-stream.js rename to apps/daemon/copilot-stream.js diff --git a/daemon/db.js b/apps/daemon/db.js similarity index 100% rename from daemon/db.js rename to apps/daemon/db.js diff --git a/daemon/design-system-preview.js b/apps/daemon/design-system-preview.js similarity index 100% rename from daemon/design-system-preview.js rename to apps/daemon/design-system-preview.js diff --git a/daemon/design-system-showcase.js b/apps/daemon/design-system-showcase.js similarity index 100% rename from daemon/design-system-showcase.js rename to apps/daemon/design-system-showcase.js diff --git a/daemon/design-systems.js b/apps/daemon/design-systems.js similarity index 100% rename from daemon/design-systems.js rename to apps/daemon/design-systems.js diff --git a/daemon/document-preview.js b/apps/daemon/document-preview.js similarity index 100% rename from daemon/document-preview.js rename to apps/daemon/document-preview.js diff --git a/daemon/frontmatter.js b/apps/daemon/frontmatter.js similarity index 100% rename from daemon/frontmatter.js rename to apps/daemon/frontmatter.js diff --git a/daemon/json-event-stream.js b/apps/daemon/json-event-stream.js similarity index 100% rename from daemon/json-event-stream.js rename to apps/daemon/json-event-stream.js diff --git a/daemon/json-event-stream.test.mjs b/apps/daemon/json-event-stream.test.mjs similarity index 100% rename from daemon/json-event-stream.test.mjs rename to apps/daemon/json-event-stream.test.mjs diff --git a/daemon/lint-artifact.js b/apps/daemon/lint-artifact.js similarity index 100% rename from daemon/lint-artifact.js rename to apps/daemon/lint-artifact.js diff --git a/apps/daemon/package.json b/apps/daemon/package.json new file mode 100644 index 000000000..73b70d62b --- /dev/null +++ b/apps/daemon/package.json @@ -0,0 +1,27 @@ +{ + "name": "@open-design/daemon", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "od": "./cli.js" + }, + "scripts": { + "daemon": "node cli.js --no-open", + "dev": "node cli.js --no-open", + "start": "node cli.js", + "test": "vitest run -c vitest.config.ts" + }, + "dependencies": { + "better-sqlite3": "^11.10.0", + "express": "^4.19.2", + "jszip": "^3.10.1", + "multer": "^1.4.5-lts.1" + }, + "devDependencies": { + "vitest": "^2.1.8" + }, + "engines": { + "node": "~24" + } +} diff --git a/daemon/projects.js b/apps/daemon/projects.js similarity index 100% rename from daemon/projects.js rename to apps/daemon/projects.js diff --git a/daemon/server.js b/apps/daemon/server.js similarity index 99% rename from daemon/server.js rename to apps/daemon/server.js index 8f99a9247..feda860cd 100644 --- a/daemon/server.js +++ b/apps/daemon/server.js @@ -59,16 +59,16 @@ import { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const PROJECT_ROOT = path.resolve(__dirname, '..'); +const PROJECT_ROOT = path.resolve(__dirname, '../..'); // Built web app lives in `out/` — that's where Next.js writes the static // export configured in next.config.ts. The folder name used to be `dist/` // when this project shipped with Vite; the daemon serves whatever the // frontend toolchain emits, no further config needed. -const STATIC_DIR = path.join(PROJECT_ROOT, 'out'); +const STATIC_DIR = path.join(PROJECT_ROOT, 'apps', 'web', 'out'); const SKILLS_DIR = path.join(PROJECT_ROOT, 'skills'); const DESIGN_SYSTEMS_DIR = path.join(PROJECT_ROOT, 'design-systems'); const RUNTIME_DATA_DIR = process.env.OD_DATA_DIR - ? path.resolve(process.env.OD_DATA_DIR) + ? path.resolve(PROJECT_ROOT, process.env.OD_DATA_DIR) : path.join(PROJECT_ROOT, '.od'); const ARTIFACTS_DIR = path.join(RUNTIME_DATA_DIR, 'artifacts'); const PROJECTS_DIR = path.join(RUNTIME_DATA_DIR, 'projects'); @@ -721,7 +721,7 @@ export async function startServer({ port = 7456, returnServer = false } = {}) { // Project files. Each project owns a flat folder under .od/projects// // containing every file the user has uploaded, pasted, sketched, or that // the agent has generated. Names are sanitized; paths are confined to the - // project's own folder (see daemon/projects.js). + // project's own folder (see apps/daemon/projects.js). app.get('/api/projects/:id/files', async (req, res) => { try { const files = await listFiles(PROJECTS_DIR, req.params.id); diff --git a/daemon/skills.js b/apps/daemon/skills.js similarity index 100% rename from daemon/skills.js rename to apps/daemon/skills.js diff --git a/apps/daemon/vitest.config.ts b/apps/daemon/vitest.config.ts new file mode 100644 index 000000000..bfe2db0e6 --- /dev/null +++ b/apps/daemon/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['**/*.test.{ts,tsx,js,mjs,cjs}'], + }, +}); diff --git a/app/[[...slug]]/client-app.tsx b/apps/web/app/[[...slug]]/client-app.tsx similarity index 100% rename from app/[[...slug]]/client-app.tsx rename to apps/web/app/[[...slug]]/client-app.tsx diff --git a/app/[[...slug]]/page.tsx b/apps/web/app/[[...slug]]/page.tsx similarity index 90% rename from app/[[...slug]]/page.tsx rename to apps/web/app/[[...slug]]/page.tsx index aba36da8f..e18681db6 100644 --- a/app/[[...slug]]/page.tsx +++ b/apps/web/app/[[...slug]]/page.tsx @@ -7,7 +7,7 @@ import { ClientApp } from './client-app'; // // For `output: 'export'` we return a single empty `slug` so Next.js emits // one shell HTML at out/index.html; the daemon's SPA fallback (see -// daemon/server.js) serves it for any unknown non-API path so deep links +// apps/daemon/server.js) serves it for any unknown non-API path so deep links // still hydrate to the right view. In dev we leave `dynamicParams` at its // default (true) so `next dev` happily renders /projects/ directly. export function generateStaticParams() { diff --git a/app/layout.tsx b/apps/web/app/layout.tsx similarity index 100% rename from app/layout.tsx rename to apps/web/app/layout.tsx diff --git a/next-env.d.ts b/apps/web/next-env.d.ts similarity index 100% rename from next-env.d.ts rename to apps/web/next-env.d.ts diff --git a/next.config.ts b/apps/web/next.config.ts similarity index 91% rename from next.config.ts rename to apps/web/next.config.ts index 275c6c00d..9544ec33a 100644 --- a/next.config.ts +++ b/apps/web/next.config.ts @@ -1,6 +1,6 @@ import type { NextConfig } from 'next'; -// Daemon port the local Express server binds to (see daemon/cli.js). The +// Daemon port the local Express server binds to (see apps/daemon/cli.js). The // dev-all launcher overrides OD_PORT after probing for a free port; we read // the same env so /api, /artifacts, and /frames always reach the right // daemon instance during `next dev`. @@ -17,6 +17,7 @@ const DAEMON_ORIGIN = `http://127.0.0.1:${DAEMON_PORT}`; const isProd = process.env.NODE_ENV !== 'development'; const nextConfig: NextConfig = { + allowedDevOrigins: ['127.0.0.1'], reactStrictMode: true, // Keep the bundle output predictable so the daemon's STATIC_DIR can point // at it without any glob trickery. @@ -32,7 +33,7 @@ const nextConfig: NextConfig = { } : { async rewrites() { - // In dev we run the daemon on a sibling port; mirror the old Vite + // In dev we run the daemon on a sibling port; proxy the app API // proxy so the SPA can hit /api, /artifacts, and /frames without // CORS gymnastics. SSE on /api/chat works through this rewrite // because Next.js's dev server streams responses unbuffered. diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 000000000..05e67250d --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@open-design/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "typecheck": "tsc -b --noEmit", + "test": "vitest run -c vitest.config.ts" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.32.1", + "next": "^16.2.4", + "openai": "^6.35.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.17.10", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.3", + "vitest": "^2.1.8" + }, + "engines": { + "node": "~24" + } +} diff --git a/public/avatar.png b/apps/web/public/avatar.png similarity index 100% rename from public/avatar.png rename to apps/web/public/avatar.png diff --git a/public/logo.svg b/apps/web/public/logo.svg similarity index 100% rename from public/logo.svg rename to apps/web/public/logo.svg diff --git a/src/App.tsx b/apps/web/src/App.tsx similarity index 100% rename from src/App.tsx rename to apps/web/src/App.tsx diff --git a/src/artifacts/manifest.test.ts b/apps/web/src/artifacts/manifest.test.ts similarity index 100% rename from src/artifacts/manifest.test.ts rename to apps/web/src/artifacts/manifest.test.ts diff --git a/src/artifacts/manifest.ts b/apps/web/src/artifacts/manifest.ts similarity index 100% rename from src/artifacts/manifest.ts rename to apps/web/src/artifacts/manifest.ts diff --git a/src/artifacts/parser.ts b/apps/web/src/artifacts/parser.ts similarity index 100% rename from src/artifacts/parser.ts rename to apps/web/src/artifacts/parser.ts diff --git a/src/artifacts/question-form.ts b/apps/web/src/artifacts/question-form.ts similarity index 100% rename from src/artifacts/question-form.ts rename to apps/web/src/artifacts/question-form.ts diff --git a/src/artifacts/renderer-registry.ts b/apps/web/src/artifacts/renderer-registry.ts similarity index 100% rename from src/artifacts/renderer-registry.ts rename to apps/web/src/artifacts/renderer-registry.ts diff --git a/src/artifacts/types.ts b/apps/web/src/artifacts/types.ts similarity index 100% rename from src/artifacts/types.ts rename to apps/web/src/artifacts/types.ts diff --git a/src/components/AgentIcon.tsx b/apps/web/src/components/AgentIcon.tsx similarity index 100% rename from src/components/AgentIcon.tsx rename to apps/web/src/components/AgentIcon.tsx diff --git a/src/components/AgentPicker.tsx b/apps/web/src/components/AgentPicker.tsx similarity index 100% rename from src/components/AgentPicker.tsx rename to apps/web/src/components/AgentPicker.tsx diff --git a/src/components/AssistantMessage.tsx b/apps/web/src/components/AssistantMessage.tsx similarity index 100% rename from src/components/AssistantMessage.tsx rename to apps/web/src/components/AssistantMessage.tsx diff --git a/src/components/AvatarMenu.tsx b/apps/web/src/components/AvatarMenu.tsx similarity index 100% rename from src/components/AvatarMenu.tsx rename to apps/web/src/components/AvatarMenu.tsx diff --git a/src/components/ChatComposer.tsx b/apps/web/src/components/ChatComposer.tsx similarity index 100% rename from src/components/ChatComposer.tsx rename to apps/web/src/components/ChatComposer.tsx diff --git a/src/components/ChatPane.tsx b/apps/web/src/components/ChatPane.tsx similarity index 100% rename from src/components/ChatPane.tsx rename to apps/web/src/components/ChatPane.tsx diff --git a/src/components/ConversationsMenu.tsx b/apps/web/src/components/ConversationsMenu.tsx similarity index 100% rename from src/components/ConversationsMenu.tsx rename to apps/web/src/components/ConversationsMenu.tsx diff --git a/src/components/DesignFilesPanel.tsx b/apps/web/src/components/DesignFilesPanel.tsx similarity index 100% rename from src/components/DesignFilesPanel.tsx rename to apps/web/src/components/DesignFilesPanel.tsx diff --git a/src/components/DesignSystemPreviewModal.tsx b/apps/web/src/components/DesignSystemPreviewModal.tsx similarity index 100% rename from src/components/DesignSystemPreviewModal.tsx rename to apps/web/src/components/DesignSystemPreviewModal.tsx diff --git a/src/components/DesignSystemsTab.tsx b/apps/web/src/components/DesignSystemsTab.tsx similarity index 100% rename from src/components/DesignSystemsTab.tsx rename to apps/web/src/components/DesignSystemsTab.tsx diff --git a/src/components/DesignsTab.tsx b/apps/web/src/components/DesignsTab.tsx similarity index 100% rename from src/components/DesignsTab.tsx rename to apps/web/src/components/DesignsTab.tsx diff --git a/src/components/EntryView.tsx b/apps/web/src/components/EntryView.tsx similarity index 100% rename from src/components/EntryView.tsx rename to apps/web/src/components/EntryView.tsx diff --git a/src/components/ExamplesTab.tsx b/apps/web/src/components/ExamplesTab.tsx similarity index 100% rename from src/components/ExamplesTab.tsx rename to apps/web/src/components/ExamplesTab.tsx diff --git a/src/components/FileViewer.tsx b/apps/web/src/components/FileViewer.tsx similarity index 100% rename from src/components/FileViewer.tsx rename to apps/web/src/components/FileViewer.tsx diff --git a/src/components/FileWorkspace.tsx b/apps/web/src/components/FileWorkspace.tsx similarity index 100% rename from src/components/FileWorkspace.tsx rename to apps/web/src/components/FileWorkspace.tsx diff --git a/src/components/Icon.tsx b/apps/web/src/components/Icon.tsx similarity index 100% rename from src/components/Icon.tsx rename to apps/web/src/components/Icon.tsx diff --git a/src/components/LanguageMenu.tsx b/apps/web/src/components/LanguageMenu.tsx similarity index 100% rename from src/components/LanguageMenu.tsx rename to apps/web/src/components/LanguageMenu.tsx diff --git a/src/components/Loading.tsx b/apps/web/src/components/Loading.tsx similarity index 100% rename from src/components/Loading.tsx rename to apps/web/src/components/Loading.tsx diff --git a/src/components/NewProjectPanel.tsx b/apps/web/src/components/NewProjectPanel.tsx similarity index 100% rename from src/components/NewProjectPanel.tsx rename to apps/web/src/components/NewProjectPanel.tsx diff --git a/src/components/PasteTextDialog.tsx b/apps/web/src/components/PasteTextDialog.tsx similarity index 100% rename from src/components/PasteTextDialog.tsx rename to apps/web/src/components/PasteTextDialog.tsx diff --git a/src/components/PreviewModal.tsx b/apps/web/src/components/PreviewModal.tsx similarity index 100% rename from src/components/PreviewModal.tsx rename to apps/web/src/components/PreviewModal.tsx diff --git a/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx similarity index 100% rename from src/components/ProjectView.tsx rename to apps/web/src/components/ProjectView.tsx diff --git a/src/components/QuestionForm.tsx b/apps/web/src/components/QuestionForm.tsx similarity index 100% rename from src/components/QuestionForm.tsx rename to apps/web/src/components/QuestionForm.tsx diff --git a/src/components/SettingsDialog.tsx b/apps/web/src/components/SettingsDialog.tsx similarity index 100% rename from src/components/SettingsDialog.tsx rename to apps/web/src/components/SettingsDialog.tsx diff --git a/src/components/SketchEditor.tsx b/apps/web/src/components/SketchEditor.tsx similarity index 100% rename from src/components/SketchEditor.tsx rename to apps/web/src/components/SketchEditor.tsx diff --git a/src/components/ToolCard.tsx b/apps/web/src/components/ToolCard.tsx similarity index 100% rename from src/components/ToolCard.tsx rename to apps/web/src/components/ToolCard.tsx diff --git a/src/components/modelOptions.tsx b/apps/web/src/components/modelOptions.tsx similarity index 100% rename from src/components/modelOptions.tsx rename to apps/web/src/components/modelOptions.tsx diff --git a/src/i18n/index.tsx b/apps/web/src/i18n/index.tsx similarity index 100% rename from src/i18n/index.tsx rename to apps/web/src/i18n/index.tsx diff --git a/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts similarity index 100% rename from src/i18n/locales/en.ts rename to apps/web/src/i18n/locales/en.ts diff --git a/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts similarity index 100% rename from src/i18n/locales/pt-BR.ts rename to apps/web/src/i18n/locales/pt-BR.ts diff --git a/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts similarity index 100% rename from src/i18n/locales/zh-CN.ts rename to apps/web/src/i18n/locales/zh-CN.ts diff --git a/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts similarity index 93% rename from src/i18n/locales/zh-TW.ts rename to apps/web/src/i18n/locales/zh-TW.ts index ff4525e72..f6945e449 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -82,6 +82,13 @@ export const zhTW: Dict = { 'settings.noAgentSelected': '尚未選擇代理', 'settings.language': '介面語言', 'settings.languageHint': '切換介面語言,設定僅儲存在當前瀏覽器。', + 'settings.modelPicker': '模型', + 'settings.reasoningPicker': '推理強度', + 'settings.modelPickerHint': + '當 CLI 提供 `models` 命令時會自動拉取。選擇「預設」則沿用 CLI 自身的設定;選擇「自訂」可手動輸入任何 CLI 支援的模型 id。', + 'settings.modelCustom': '自訂(在下方填寫)…', + 'settings.modelCustomLabel': '自訂模型 id', + 'settings.modelCustomPlaceholder': '例如 anthropic/claude-sonnet-4-6', 'entry.tabDesigns': '我的設計', 'entry.tabExamples': '範例', @@ -117,6 +124,9 @@ export const zhTW: Dict = { 'newproj.create': '建立', 'newproj.createFromTemplate': '基於範本建立', 'newproj.createDisabledTitle': '請先在任意專案內透過「分享」選單將其儲存為範本。', + 'newproj.importClaudeZip': '匯入 Claude Design ZIP', + 'newproj.importClaudeZipTitle': '匯入 Claude Design 匯出的 .zip 檔案', + 'newproj.importingClaudeZip': '正在匯入…', 'newproj.privacyFooter': '預設情況下只有你能看到自己的專案。', 'newproj.designSystem': '設計系統', 'newproj.dsNoneFreeform': '不指定 — 自由發揮', @@ -208,6 +218,10 @@ export const zhTW: Dict = { 'avatar.metaOffline': '未執行', 'avatar.metaSelected': '已選', 'avatar.noAgentSelected': '尚未選擇代理', + 'avatar.modelSection': '模型', + 'avatar.modelLabel': '模型', + 'avatar.reasoningLabel': '推理', + 'avatar.customSuffix': '(自訂)', 'project.backToProjects': '返回專案列表', 'project.metaFreeform': '自由設計', @@ -315,6 +329,10 @@ export const zhTW: Dict = { 'designFiles.kindSketch': '草圖', 'designFiles.kindText': '文字', 'designFiles.kindCode': '腳本', + 'designFiles.kindPdf': 'PDF', + 'designFiles.kindDocument': '文件', + 'designFiles.kindPresentation': '簡報', + 'designFiles.kindSpreadsheet': '試算表', 'designFiles.kindBinary': '二進位', 'pasteDialog.title': '貼上文字', 'pasteDialog.hint': '將儲存到專案資料夾中,名稱隨你定。', @@ -338,6 +356,11 @@ export const zhTW: Dict = { 'fileViewer.share': '分享', 'fileViewer.binaryMeta': '二進位 · {size}', 'fileViewer.binaryNote': '二進位檔案({size} 位元組)。請下載或在本機開啟檢視。', + 'fileViewer.pdfMeta': 'PDF · {size}', + 'fileViewer.documentMeta': '文件', + 'fileViewer.presentationMeta': '簡報', + 'fileViewer.spreadsheetMeta': '試算表', + 'fileViewer.previewUnavailable': '無法產生預覽,請下載或開啟檔案檢視。', 'fileViewer.download': '下載', 'fileViewer.open': '開啟', 'fileViewer.imageMeta': '圖片 · {size}', @@ -434,6 +457,10 @@ export const zhTW: Dict = { 'assistant.producedFiles': '本輪產出的檔案', 'assistant.openFile': '開啟', 'assistant.downloadFile': '下載', + 'assistant.unfinishedLabel': '已停止,仍有未完成任務', + 'assistant.unfinishedSummary': '剩餘 {n} 個任務', + 'assistant.unfinishedMore': '還有 {n} 個', + 'assistant.continueRemaining': '繼續剩餘任務', 'assistant.thinking': '思考中', 'assistant.systemReminder': '系統提示', 'assistant.waitingFirstOutput': '等待首批輸出中', diff --git a/src/i18n/types.ts b/apps/web/src/i18n/types.ts similarity index 100% rename from src/i18n/types.ts rename to apps/web/src/i18n/types.ts diff --git a/src/index.css b/apps/web/src/index.css similarity index 100% rename from src/index.css rename to apps/web/src/index.css diff --git a/src/prompts/deck-framework.ts b/apps/web/src/prompts/deck-framework.ts similarity index 100% rename from src/prompts/deck-framework.ts rename to apps/web/src/prompts/deck-framework.ts diff --git a/src/prompts/directions.ts b/apps/web/src/prompts/directions.ts similarity index 100% rename from src/prompts/directions.ts rename to apps/web/src/prompts/directions.ts diff --git a/src/prompts/discovery.ts b/apps/web/src/prompts/discovery.ts similarity index 100% rename from src/prompts/discovery.ts rename to apps/web/src/prompts/discovery.ts diff --git a/src/prompts/official-system.ts b/apps/web/src/prompts/official-system.ts similarity index 100% rename from src/prompts/official-system.ts rename to apps/web/src/prompts/official-system.ts diff --git a/src/prompts/system.ts b/apps/web/src/prompts/system.ts similarity index 100% rename from src/prompts/system.ts rename to apps/web/src/prompts/system.ts diff --git a/src/providers/anthropic.ts b/apps/web/src/providers/anthropic.ts similarity index 100% rename from src/providers/anthropic.ts rename to apps/web/src/providers/anthropic.ts diff --git a/src/providers/daemon.ts b/apps/web/src/providers/daemon.ts similarity index 98% rename from src/providers/daemon.ts rename to apps/web/src/providers/daemon.ts index 22cc262eb..f92fb918c 100644 --- a/src/providers/daemon.ts +++ b/apps/web/src/providers/daemon.ts @@ -175,7 +175,7 @@ function parseFrame(frame: string): ParsedFrame | null { } } -// Translate a raw `agent` SSE payload (what daemon/claude-stream.js emits) +// Translate a raw `agent` SSE payload (what apps/daemon/claude-stream.js emits) // into the UI's AgentEvent union. Keep this liberal — unknown types just // return null so the UI ignores them instead of rendering garbage. function translateAgentEvent(data: Record): AgentEvent | null { diff --git a/src/providers/openai-compatible.ts b/apps/web/src/providers/openai-compatible.ts similarity index 100% rename from src/providers/openai-compatible.ts rename to apps/web/src/providers/openai-compatible.ts diff --git a/src/providers/registry.ts b/apps/web/src/providers/registry.ts similarity index 100% rename from src/providers/registry.ts rename to apps/web/src/providers/registry.ts diff --git a/src/router.ts b/apps/web/src/router.ts similarity index 100% rename from src/router.ts rename to apps/web/src/router.ts diff --git a/src/runtime/exports.ts b/apps/web/src/runtime/exports.ts similarity index 100% rename from src/runtime/exports.ts rename to apps/web/src/runtime/exports.ts diff --git a/src/runtime/markdown.tsx b/apps/web/src/runtime/markdown.tsx similarity index 100% rename from src/runtime/markdown.tsx rename to apps/web/src/runtime/markdown.tsx diff --git a/src/runtime/srcdoc.ts b/apps/web/src/runtime/srcdoc.ts similarity index 100% rename from src/runtime/srcdoc.ts rename to apps/web/src/runtime/srcdoc.ts diff --git a/src/runtime/todos.ts b/apps/web/src/runtime/todos.ts similarity index 100% rename from src/runtime/todos.ts rename to apps/web/src/runtime/todos.ts diff --git a/src/runtime/zip.ts b/apps/web/src/runtime/zip.ts similarity index 100% rename from src/runtime/zip.ts rename to apps/web/src/runtime/zip.ts diff --git a/src/state/config.ts b/apps/web/src/state/config.ts similarity index 100% rename from src/state/config.ts rename to apps/web/src/state/config.ts diff --git a/src/state/projects.ts b/apps/web/src/state/projects.ts similarity index 100% rename from src/state/projects.ts rename to apps/web/src/state/projects.ts diff --git a/src/types.ts b/apps/web/src/types.ts similarity index 100% rename from src/types.ts rename to apps/web/src/types.ts diff --git a/src/utils/agentLabels.ts b/apps/web/src/utils/agentLabels.ts similarity index 100% rename from src/utils/agentLabels.ts rename to apps/web/src/utils/agentLabels.ts diff --git a/tsconfig.json b/apps/web/tsconfig.json similarity index 100% rename from tsconfig.json rename to apps/web/tsconfig.json diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 000000000..756267c29 --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.{ts,tsx,js,mjs,cjs}'], + }, +}); diff --git a/docs/agent-adapters.md b/docs/agent-adapters.md index 9a145f9e6..41efa4bb1 100644 --- a/docs/agent-adapters.md +++ b/docs/agent-adapters.md @@ -176,7 +176,7 @@ The adapter declares which strategy to use via `capabilities().nativeSkillLoadin ### 5.7 GitHub Copilot CLI - Invocation: `copilot -p "" --allow-all-tools --output-format json --add-dir --add-dir `. `--allow-all-tools` is mandatory in non-interactive mode — without it the CLI blocks waiting for human approval on every tool call. Unlike Codex (where `exec` is a dedicated headless subcommand with auto-approve baked in) or Claude Code (which inherits its permission policy from `~/.claude/settings.json`), Copilot's `-p` mode always prompts unless this flag is passed explicitly. `--add-dir` (repeatable) widens the path-level sandbox so Copilot can read skill seeds and design-system specs that live outside the project cwd. -- Streaming: `--output-format json` emits JSONL with the same expressive shape as Claude Code's stream-json (`assistant.reasoning_delta`, `assistant.message_delta`, `tool.execution_start/complete`, `result`). `daemon/copilot-stream.js` maps these onto the same UI events as `claude-stream.js`. +- Streaming: `--output-format json` emits JSONL with the same expressive shape as Claude Code's stream-json (`assistant.reasoning_delta`, `assistant.message_delta`, `tool.execution_start/complete`, `result`). `apps/daemon/copilot-stream.js` maps these onto the same UI events as `claude-stream.js`. - Skill loading: prompt injection only. Github Copilot's tool catalog includes a `skill` tool — native format worth reverse-engineering later. - Surgical edits: dedicated `edit` tool. - Detection assumes Copilot is already authenticated, via one of: `copilot login` (subcommand, OAuth device flow), the interactive `/login` slash command inside `copilot` with no args. diff --git a/e2e/cases/README.zh-CN.md b/e2e/cases/README.zh-CN.md index 444a1e36d..9cc24b769 100644 --- a/e2e/cases/README.zh-CN.md +++ b/e2e/cases/README.zh-CN.md @@ -30,7 +30,7 @@ - `title`:人可读的用例名称 - `kind`:项目类型,比如 `prototype`、`deck`、`workspace` - `flow`:Playwright 里对应的自动化流程分支 -- `automated`:当前是否会被 `npm run test:ui` 执行 +- `automated`:当前是否会被 `pnpm run test:ui` 执行 - `description`:覆盖目标和场景说明 - `create`:创建项目时要用到的输入 - `prompt`:主输入内容 @@ -91,7 +91,13 @@ ## 运行方式 ```bash -npm run test:ui +pnpm run test:ui +``` + +也可以直接在独立测试包内运行: + +```bash +pnpm --filter @open-design/e2e test:ui ``` 运行完成后会自动生成: @@ -111,5 +117,5 @@ npm run test:ui 如果要带界面调试: ```bash -npm run test:ui:headed +pnpm run test:ui:headed ``` diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 000000000..a3b9c1b4f --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,26 @@ +{ + "name": "@open-design/e2e", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run -c vitest.config.ts", + "test:ui:clean": "node scripts/reset-artifacts.mjs", + "test:ui": "pnpm run test:ui:clean && playwright test -c playwright.config.ts", + "test:ui:headed": "pnpm run test:ui:clean && playwright test -c playwright.config.ts --headed", + "test:e2e:live": "node --test scripts/runtime-adapter.e2e.live.test.mjs" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^20.17.10", + "jsdom": "^29.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.6.3", + "vitest": "^2.1.8" + }, + "engines": { + "node": "~24" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 762e250fb..1b403f40c 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -26,14 +26,14 @@ export default defineConfig({ ['html', { open: 'never', outputFolder: './reports/playwright-html-report' }], ['json', { outputFile: './reports/results.json' }], ['junit', { outputFile: './reports/junit.xml' }], - ['./reporters/markdown-reporter.cjs', { outputFile: 'e2e/reports/latest.md' }], + ['./reporters/markdown-reporter.cjs', { outputFile: './reports/latest.md' }], ] : [ ['list'], ['html', { open: 'never', outputFolder: './reports/playwright-html-report' }], ['json', { outputFile: './reports/results.json' }], ['junit', { outputFile: './reports/junit.xml' }], - ['./reporters/markdown-reporter.cjs', { outputFile: 'e2e/reports/latest.md' }], + ['./reporters/markdown-reporter.cjs', { outputFile: './reports/latest.md' }], ], use: { baseURL, @@ -44,7 +44,7 @@ export default defineConfig({ command: `OD_DATA_DIR=e2e/.od-data ` + `OD_PORT=${daemonPort} OD_PORT_STRICT=1 ` + - `NEXT_PORT=${nextPort} NEXT_PORT_STRICT=1 npm run dev:all`, + `NEXT_PORT=${nextPort} NEXT_PORT_STRICT=1 pnpm --dir .. run dev:all`, url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, diff --git a/e2e/reports/README.zh-CN.md b/e2e/reports/README.zh-CN.md index 683c03dee..02303ba12 100644 --- a/e2e/reports/README.zh-CN.md +++ b/e2e/reports/README.zh-CN.md @@ -11,7 +11,7 @@ - `junit.xml`:JUnit 格式结果,方便接 CI - `test-results/`:失败用例的截图、trace、error-context 等原始附件 -每次执行 `npm run test:ui` 前,系统会先自动清理旧的: +每次执行 `pnpm run test:ui`(或 `pnpm --filter @open-design/e2e test:ui`)前,系统会先自动清理旧的: - `e2e/.od-data/` - `e2e/reports/test-results/` diff --git a/scripts/reset-e2e-artifacts.mjs b/e2e/scripts/reset-artifacts.mjs similarity index 60% rename from scripts/reset-e2e-artifacts.mjs rename to e2e/scripts/reset-artifacts.mjs index 043f76f13..35ab4155f 100644 --- a/scripts/reset-e2e-artifacts.mjs +++ b/e2e/scripts/reset-artifacts.mjs @@ -3,36 +3,36 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const rootDir = path.resolve(__dirname, '..'); +const e2eDir = path.resolve(__dirname, '..'); const targets = [ - path.join(rootDir, 'e2e', '.od-data'), - path.join(rootDir, 'e2e', 'test-results'), - path.join(rootDir, 'e2e', 'reports', 'test-results'), - path.join(rootDir, 'e2e', 'reports', 'html'), - path.join(rootDir, 'e2e', 'reports', 'playwright-html-report'), - path.join(rootDir, 'e2e', 'reports', 'results.json'), - path.join(rootDir, 'e2e', 'reports', 'junit.xml'), - path.join(rootDir, 'e2e', 'reports', 'latest.md'), - path.join(rootDir, 'e2e', '.DS_Store'), + path.join(e2eDir, '.od-data'), + path.join(e2eDir, 'test-results'), + path.join(e2eDir, 'reports', 'test-results'), + path.join(e2eDir, 'reports', 'html'), + path.join(e2eDir, 'reports', 'playwright-html-report'), + path.join(e2eDir, 'reports', 'results.json'), + path.join(e2eDir, 'reports', 'junit.xml'), + path.join(e2eDir, 'reports', 'latest.md'), + path.join(e2eDir, '.DS_Store'), ]; for (const target of targets) { await rm(target, { recursive: true, force: true }); } -await mkdir(path.join(rootDir, 'e2e', 'reports'), { recursive: true }); +await mkdir(path.join(e2eDir, 'reports'), { recursive: true }); // Recreate runtime roots so local inspection stays predictable even before // Playwright or the daemon materializes them. -await mkdir(path.join(rootDir, 'e2e', '.od-data'), { recursive: true }); -await mkdir(path.join(rootDir, 'e2e', 'reports', 'test-results'), { +await mkdir(path.join(e2eDir, '.od-data'), { recursive: true }); +await mkdir(path.join(e2eDir, 'reports', 'test-results'), { recursive: true, }); // Best-effort removal of accidental empty directories directly under the // test data root. This keeps old project ids from piling up across runs. -const projectsRoot = path.join(rootDir, 'e2e', '.od-data', 'projects'); +const projectsRoot = path.join(e2eDir, '.od-data', 'projects'); try { const entries = await readdir(projectsRoot, { withFileTypes: true }); await Promise.all( diff --git a/scripts/runtime-adapter.e2e.live.test.mjs b/e2e/scripts/runtime-adapter.e2e.live.test.mjs similarity index 98% rename from scripts/runtime-adapter.e2e.live.test.mjs rename to e2e/scripts/runtime-adapter.e2e.live.test.mjs index ec6babc43..85727ed7b 100644 --- a/scripts/runtime-adapter.e2e.live.test.mjs +++ b/e2e/scripts/runtime-adapter.e2e.live.test.mjs @@ -22,8 +22,8 @@ let dataDir; test.before(async () => { dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'od-runtime-adapter-live-')); process.env.OD_DATA_DIR = dataDir; - ({ startServer } = await import('../daemon/server.js')); - ({ closeDatabase } = await import('../daemon/db.js')); + ({ startServer } = await import('../../apps/daemon/server.js')); + ({ closeDatabase } = await import('../../apps/daemon/db.js')); const started = await startServer({ port: 0, returnServer: true }); baseUrl = started.url; server = started.server; diff --git a/e2e/specs/app.spec.ts b/e2e/specs/app.spec.ts index cb60a3d31..e6c16fc60 100644 --- a/e2e/specs/app.spec.ts +++ b/e2e/specs/app.spec.ts @@ -313,7 +313,12 @@ async function sendPrompt( try { await expect(input).toHaveValue(prompt, { timeout: 1500 }); await expect(sendButton).toBeEnabled({ timeout: 1500 }); + const chatResponse = page.waitForResponse( + (resp) => resp.url().includes('/api/chat') && resp.request().method() === 'POST', + { timeout: 2000 }, + ); await sendButton.evaluate((button: HTMLButtonElement) => button.click()); + await chatResponse; return; } catch (error) { await input.click(); @@ -323,7 +328,12 @@ async function sendPrompt( try { await expect(input).toHaveValue(prompt, { timeout: 1500 }); await expect(sendButton).toBeEnabled({ timeout: 1500 }); + const chatResponse = page.waitForResponse( + (resp) => resp.url().includes('/api/chat') && resp.request().method() === 'POST', + { timeout: 2000 }, + ); await sendButton.evaluate((button: HTMLButtonElement) => button.click()); + await chatResponse; return; } catch (retryError) { if (attempt === 2) throw retryError; @@ -606,11 +616,16 @@ async function runFileUploadSendFlow( page: Parameters[0]['page'], entry: UICase, ) { + const uploadResponse = page.waitForResponse( + (resp) => resp.url().includes('/upload') && resp.request().method() === 'POST', + { timeout: 5000 }, + ); await page.getByTestId('chat-file-input').setInputFiles({ name: 'reference.txt', mimeType: 'text/plain', buffer: Buffer.from('Reference content for upload flow.\n', 'utf8'), }); + await expect((await uploadResponse).ok()).toBeTruthy(); await expect(page.getByTestId('staged-attachments')).toBeVisible(); await expect( @@ -637,12 +652,14 @@ async function runDesignFilesUploadFlow( await expect(page.getByRole('tab', { name: /moodboard\.png/i })).toBeVisible(); await page.getByTestId('design-files-tab').click(); - const fileRow = page.getByTestId('design-file-row-moodboard.png'); + const fileRow = page.locator('[data-testid^="design-file-row-"]', { + hasText: 'moodboard.png', + }); await expect(fileRow).toBeVisible(); await fileRow.click(); const preview = page.getByTestId('design-file-preview'); await expect(preview).toBeVisible(); - await expect(preview.getByText('moodboard.png', { exact: true })).toBeVisible(); + await expect(preview.getByText(/moodboard\.png/i)).toBeVisible(); await fileRow.dblclick(); await expect(page.getByRole('tab', { name: /moodboard\.png/i })).toBeVisible(); @@ -667,14 +684,16 @@ async function runDesignFilesDeleteFlow( await expect(page.getByRole('tab', { name: /trash-me\.png/i })).toBeVisible(); await page.getByTestId('design-files-tab').click(); - const fileRow = page.getByTestId('design-file-row-trash-me.png'); + const fileRow = page.locator('[data-testid^="design-file-row-"]', { + hasText: 'trash-me.png', + }); await expect(fileRow).toBeVisible(); await fileRow.hover(); - await page.getByTestId('design-file-menu-trash-me.png').click(); + await fileRow.locator('[data-testid^="design-file-menu-"]').click(); await expect(page.getByTestId('design-file-menu-popover')).toBeVisible(); - await page.getByTestId('design-file-delete-trash-me.png').click(); + await page.locator('[data-testid^="design-file-delete-"]').click(); - await expect(page.getByTestId('design-file-row-trash-me.png')).toHaveCount(0); + await expect(fileRow).toHaveCount(0); await expect(page.getByRole('tab', { name: /trash-me\.png/i })).toHaveCount(0); } diff --git a/tests/assistant-message.test.tsx b/e2e/tests/assistant-message.test.tsx similarity index 84% rename from tests/assistant-message.test.tsx rename to e2e/tests/assistant-message.test.tsx index 149578474..5c6c7049d 100644 --- a/tests/assistant-message.test.tsx +++ b/e2e/tests/assistant-message.test.tsx @@ -1,8 +1,7 @@ -import '@testing-library/jest-dom/vitest'; import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { AssistantMessage } from '../src/components/AssistantMessage'; -import type { AgentEvent, ChatMessage } from '../src/types'; +import { AssistantMessage } from '../../apps/web/src/components/AssistantMessage'; +import type { AgentEvent, ChatMessage } from '../../apps/web/src/types'; function messageWithEvents(events: AgentEvent[]): ChatMessage { return { @@ -35,8 +34,8 @@ describe('AssistantMessage unfinished todo state', () => { />, ); - expect(screen.getByText('Done')).toBeInTheDocument(); - expect(screen.queryByText('Stopped with unfinished work')).not.toBeInTheDocument(); + expect(screen.getByText('Done')).toBeTruthy(); + expect(screen.queryByText('Stopped with unfinished work')).toBeNull(); expect(screen.queryByRole('button', { name: 'Continue remaining tasks' })).toBeNull(); }); @@ -69,12 +68,12 @@ describe('AssistantMessage unfinished todo state', () => { />, ); - expect(screen.getByText('Stopped with unfinished work')).toBeInTheDocument(); - expect(screen.getByText('2 task(s) remain')).toBeInTheDocument(); + expect(screen.getByText('Stopped with unfinished work')).toBeTruthy(); + expect(screen.getByText('2 task(s) remain')).toBeTruthy(); const remainingList = screen.getByText('2 task(s) remain').closest('.unfinished-todos'); expect(remainingList).not.toBeNull(); - expect(within(remainingList as HTMLElement).getByText('Building components')).toBeInTheDocument(); - expect(within(remainingList as HTMLElement).getByText('Run QA')).toBeInTheDocument(); + expect(within(remainingList as HTMLElement).getByText('Building components')).toBeTruthy(); + expect(within(remainingList as HTMLElement).getByText('Run QA')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'Continue remaining tasks' })); @@ -106,8 +105,8 @@ describe('AssistantMessage unfinished todo state', () => { />, ); - expect(screen.getByText('Stopped with unfinished work')).toBeInTheDocument(); - expect(screen.getByText('1 task(s) remain')).toBeInTheDocument(); + expect(screen.getByText('Stopped with unfinished work')).toBeTruthy(); + expect(screen.getByText('1 task(s) remain')).toBeTruthy(); expect(screen.queryByRole('button', { name: 'Continue remaining tasks' })).toBeNull(); }); }); diff --git a/tests/structured-streams.test.ts b/e2e/tests/structured-streams.test.ts similarity index 90% rename from tests/structured-streams.test.ts rename to e2e/tests/structured-streams.test.ts index 7f139bf36..35c2c291a 100644 --- a/tests/structured-streams.test.ts +++ b/e2e/tests/structured-streams.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { createClaudeStreamHandler } from '../daemon/claude-stream.js'; -import { createCopilotStreamHandler } from '../daemon/copilot-stream.js'; +import { createClaudeStreamHandler } from '../../apps/daemon/claude-stream.js'; +import { createCopilotStreamHandler } from '../../apps/daemon/copilot-stream.js'; describe('structured agent stream fixtures', () => { it('emits TodoWrite tool_use from Claude Code stream JSON', () => { diff --git a/tests/todos.test.ts b/e2e/tests/todos.test.ts similarity index 96% rename from tests/todos.test.ts rename to e2e/tests/todos.test.ts index 430c9e976..c73b63f23 100644 --- a/tests/todos.test.ts +++ b/e2e/tests/todos.test.ts @@ -3,8 +3,8 @@ import { latestTodosFromEvents, parseTodoWriteInput, unfinishedTodosFromEvents, -} from '../src/runtime/todos'; -import type { AgentEvent } from '../src/types'; +} from '../../apps/web/src/runtime/todos'; +import type { AgentEvent } from '../../apps/web/src/types'; const firstTodoInput = { todos: [ diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts new file mode 100644 index 000000000..7c1d9f518 --- /dev/null +++ b/e2e/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + esbuild: { + jsx: 'automatic', + jsxImportSource: 'react', + }, + test: { + environment: 'jsdom', + include: ['tests/**/*.test.{ts,tsx}'], + }, +}); diff --git a/package.json b/package.json index b228219ae..6b483085c 100644 --- a/package.json +++ b/package.json @@ -2,53 +2,31 @@ "name": "open-design", "version": "0.1.0", "private": true, - "packageManager": "pnpm@9.15.9", + "packageManager": "pnpm@10.33.2", "type": "module", "description": "Local-first design product: detects your installed code-agent CLI, runs design skills + design systems, streams artifacts into a sandboxed preview.", "license": "Apache-2.0", "bin": { - "od": "./daemon/cli.js" + "od": "./apps/daemon/cli.js" }, "scripts": { - "daemon": "node daemon/cli.js --no-open", - "dev": "next dev", + "daemon": "pnpm --filter @open-design/daemon daemon", + "dev": "pnpm --filter @open-design/web dev", "dev:all": "node scripts/dev-all.mjs", - "build": "next build", - "preview": "pnpm build && node daemon/cli.js --no-open", - "test:e2e:live": "node --test scripts/runtime-adapter.e2e.live.test.mjs", - "test": "vitest run", - "test:ui:clean": "node scripts/reset-e2e-artifacts.mjs", - "test:ui": "npm run test:ui:clean && playwright test -c e2e/playwright.config.ts", - "test:ui:headed": "npm run test:ui:clean && playwright test -c e2e/playwright.config.ts --headed", - "typecheck": "tsc -b --noEmit", - "start": "npm run build && node daemon/cli.js", - "test:run": "vitest run" - }, - "dependencies": { - "@anthropic-ai/sdk": "^0.32.1", - "better-sqlite3": "^11.10.0", - "express": "^4.19.2", - "jszip": "^3.10.1", - "multer": "^1.4.5-lts.1", - "next": "^16.2.4", - "openai": "^6.35.0", - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@playwright/test": "^1.59.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@types/node": "^20.17.10", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "concurrently": "^9.0.1", - "jsdom": "^29.1.0", - "typescript": "^5.6.3", - "vitest": "^2.1.8" + "build": "pnpm --filter @open-design/web build", + "preview": "pnpm run build && pnpm --filter @open-design/daemon daemon", + "test:e2e:live": "pnpm --filter @open-design/e2e test:e2e:live", + "test": "pnpm --filter @open-design/web test && pnpm --filter @open-design/daemon test && pnpm --filter @open-design/e2e test", + "test:ui:clean": "pnpm --filter @open-design/e2e test:ui:clean", + "test:ui": "pnpm --filter @open-design/e2e test:ui", + "test:ui:headed": "pnpm --filter @open-design/e2e test:ui:headed", + "typecheck": "pnpm --filter @open-design/web typecheck", + "start": "pnpm run build && pnpm --filter @open-design/daemon start", + "test:run": "pnpm run test" }, "engines": { - "node": ">=20.9.0 <23" + "node": "~24", + "pnpm": ">=10.33.2 <11" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd1f9050f..2274347cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,10 @@ settings: importers: - .: + .: {} + + apps/daemon: dependencies: - '@anthropic-ai/sdk': - specifier: ^0.32.1 - version: 0.32.1 better-sqlite3: specifier: ^11.10.0 version: 11.10.0 @@ -23,9 +22,22 @@ importers: multer: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.2 + devDependencies: + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.0) + + apps/web: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.32.1 + version: 0.32.1 next: specifier: ^16.2.4 version: 16.2.4(@playwright/test@1.59.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + openai: + specifier: ^6.35.0 + version: 6.35.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -33,15 +45,6 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) devDependencies: - '@playwright/test': - specifier: ^1.59.1 - version: 1.59.1 - '@testing-library/jest-dom': - specifier: ^6.9.1 - version: 6.9.1 - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/node': specifier: ^20.17.10 version: 20.19.39 @@ -51,12 +54,33 @@ importers: '@types/react-dom': specifier: ^18.3.1 version: 18.3.7(@types/react@18.3.28) - concurrently: - specifier: ^9.0.1 - version: 9.2.1 + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@20.19.39)(jsdom@29.1.0) + + e2e: + devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/node': + specifier: ^20.17.10 + version: 20.19.39 jsdom: specifier: ^29.1.0 version: 29.1.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) typescript: specifier: ^5.6.3 version: 5.9.3 @@ -66,9 +90,6 @@ importers: packages: - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@anthropic-ai/sdk@0.32.1': resolution: {integrity: sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==} @@ -650,10 +671,6 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} @@ -740,10 +757,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} @@ -754,10 +767,6 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -825,10 +834,6 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -839,17 +844,6 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -858,11 +852,6 @@ packages: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} engines: {'0': node >= 0.8} - concurrently@9.2.1: - resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} - engines: {node: '>=18'} - hasBin: true - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -885,9 +874,6 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -950,9 +936,6 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -960,9 +943,6 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -998,10 +978,6 @@ packages: engines: {node: '>=12'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1070,10 +1046,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1089,10 +1061,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1126,10 +1094,6 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1140,10 +1104,6 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1221,10 +1181,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1312,6 +1268,18 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openai@6.35.0: + resolution: {integrity: sha512-L/skwIGnt5xQZHb0UfTu9uAUKbis3ehKypOuJKi20QvG7UStV6C8IC3myGYHcdiF4kms/bAvOJ9UqqNWqi8x/Q==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -1416,14 +1384,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1433,9 +1393,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -1475,10 +1432,6 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} - engines: {node: '>= 0.4'} - side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -1522,24 +1475,12 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -1557,14 +1498,6 @@ packages: babel-plugin-macros: optional: true - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1615,10 +1548,6 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1754,10 +1683,6 @@ packages: engines: {node: '>=8'} hasBin: true - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -1772,22 +1697,8 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - snapshots: - '@adobe/css-tools@4.4.4': {} - '@anthropic-ai/sdk@0.32.1': dependencies: '@types/node': 18.19.130 @@ -2153,15 +2064,6 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': - dependencies: - '@adobe/css-tools': 4.4.4 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 @@ -2255,10 +2157,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - ansi-styles@5.2.0: {} append-field@1.0.0: {} @@ -2267,8 +2165,6 @@ snapshots: dependencies: dequal: 2.0.3 - aria-query@5.3.2: {} - array-flatten@1.1.1: {} assertion-error@2.0.1: {} @@ -2350,29 +2246,12 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - check-error@2.1.3: {} chownr@1.1.4: {} client-only@0.0.1: {} - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -2384,15 +2263,6 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 - concurrently@9.2.1: - dependencies: - chalk: 4.1.2 - rxjs: 7.8.2 - shell-quote: 1.8.3 - supports-color: 8.1.1 - tree-kill: 1.2.2 - yargs: 17.7.2 - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -2410,8 +2280,6 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 - css.escape@1.5.1: {} - csstype@3.2.3: {} data-urls@7.0.0: @@ -2451,8 +2319,6 @@ snapshots: dom-accessibility-api@0.5.16: {} - dom-accessibility-api@0.6.3: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2461,8 +2327,6 @@ snapshots: ee-first@1.1.1: {} - emoji-regex@8.0.0: {} - encodeurl@2.0.0: {} end-of-stream@1.4.5: @@ -2514,8 +2378,6 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - escalade@3.2.0: {} - escape-html@1.0.3: {} estree-walker@3.0.3: @@ -2609,8 +2471,6 @@ snapshots: function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2633,8 +2493,6 @@ snapshots: gopd@1.2.0: {} - has-flag@4.0.0: {} - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2671,16 +2529,12 @@ snapshots: immediate@3.0.6: {} - indent-string@4.0.0: {} - inherits@2.0.4: {} ini@1.3.8: {} ipaddr.js@1.9.1: {} - is-fullwidth-code-point@3.0.0: {} - is-potential-custom-element-name@1.0.1: {} isarray@1.0.0: {} @@ -2758,8 +2612,6 @@ snapshots: mimic-response@3.1.0: {} - min-indent@1.0.1: {} - minimist@1.2.8: {} mkdirp-classic@0.5.3: {} @@ -2835,6 +2687,8 @@ snapshots: dependencies: wrappy: 1.0.2 + openai@6.35.0: {} + pako@1.0.11: {} parse5@8.0.1: @@ -2958,13 +2812,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - - require-directory@2.1.1: {} - require-from-string@2.0.2: {} rollup@4.60.2: @@ -2998,10 +2845,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -3081,8 +2924,6 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true - shell-quote@1.8.3: {} - side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -3131,12 +2972,6 @@ snapshots: streamsearch@1.1.0: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -3145,14 +2980,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - strip-json-comments@2.0.1: {} styled-jsx@5.1.6(react@18.3.1): @@ -3160,14 +2987,6 @@ snapshots: client-only: 0.0.1 react: 18.3.1 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - symbol-tree@3.2.4: {} tar-fs@2.1.4: @@ -3213,8 +3032,6 @@ snapshots: dependencies: punycode: 2.3.1 - tree-kill@1.2.2: {} - tslib@2.8.1: {} tunnel-agent@0.6.0: @@ -3337,12 +3154,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrappy@1.0.2: {} xml-name-validator@5.0.0: {} @@ -3350,17 +3161,3 @@ snapshots: xmlchars@2.2.0: {} xtend@4.0.2: {} - - y18n@5.0.8: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..611eb8811 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - apps/* + - e2e diff --git a/scripts/dev-all.mjs b/scripts/dev-all.mjs index f824f7503..82648d20a 100755 --- a/scripts/dev-all.mjs +++ b/scripts/dev-all.mjs @@ -1,16 +1,16 @@ #!/usr/bin/env node -// Launcher for `npm run dev:all`. +// Launcher for `pnpm run dev:all`. // // Probes for free ports for the daemon (OD_PORT, default 7456) and the -// Next.js dev server (NEXT_PORT, default 3000) before spawning -// `concurrently`, so a stray process holding either port doesn't kill the +// Next.js dev server (NEXT_PORT, default 3000) before spawning the workspace +// apps, so a stray process holding either port doesn't kill the // whole boot. The resolved ports are exported into the child env, which // means: // * the daemon's cli.js sees the new OD_PORT and binds to it -// * next.config.ts reads the same OD_PORT and proxies /api, /artifacts, +// * apps/web/next.config.ts reads the same OD_PORT and proxies /api, /artifacts, // /frames to the daemon's actual port -// * Next.js binds to NEXT_PORT (we pass `next dev -p $NEXT_PORT` so the -// `dev` script can stay parameter-free for the common single-process +// * Next.js binds to NEXT_PORT (we pass `-p $NEXT_PORT` to the web package +// dev script so it can stay parameter-free for the common single-process // case where the user runs just `pnpm dev`) // // If a port is busy we walk forward up to PORT_SEARCH_RANGE steps and log @@ -49,24 +49,40 @@ const env = { PORT: String(nextPort), }; -// `npm:daemon` is the shorthand for the daemon script, and `next dev -p -// ` is invoked directly so we can pass the resolved port without -// round-tripping through npm scripts. Keep the port numeric before it reaches -// the command string, and avoid shell interpretation on POSIX; Windows needs -// shell mode so the local `.cmd` shim can resolve. -const child = spawn( - 'concurrently', - ['-k', '-n', 'daemon,web', '-c', 'cyan,magenta', 'npm:daemon', `next dev -p ${nextPort}`], - { env, stdio: 'inherit', shell: process.platform === 'win32' }, -); +const packageManager = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; -child.on('exit', (code, signal) => { - if (signal) process.kill(process.pid, signal); - else process.exit(code ?? 0); -}); +const children = [ + spawn(packageManager, ['--filter', '@open-design/daemon', 'daemon'], { + env, + stdio: 'inherit', + }), + spawn(packageManager, ['--filter', '@open-design/web', 'dev', '-p', String(nextPort)], { + env, + stdio: 'inherit', + }), +]; + +let shuttingDown = false; + +function stopChildren(signal = 'SIGTERM') { + for (const child of children) { + if (!child.killed) child.kill(signal); + } +} + +for (const child of children) { + child.on('exit', (code, signal) => { + if (shuttingDown) return; + shuttingDown = true; + stopChildren(signal || 'SIGTERM'); + if (signal) process.kill(process.pid, signal); + else process.exit(code ?? 0); + }); +} for (const sig of ['SIGINT', 'SIGTERM']) { process.on(sig, () => { - if (!child.killed) child.kill(sig); + shuttingDown = true; + stopChildren(sig); }); } diff --git a/specs/current/runtime-adapter.md b/specs/current/runtime-adapter.md index 4707aecee..9aaf1450c 100644 --- a/specs/current/runtime-adapter.md +++ b/specs/current/runtime-adapter.md @@ -6,15 +6,15 @@ Runtime Adapter is the daemon layer responsible for adapting local AI agent CLIs The current implementation is concentrated in: -- `daemon/agents.js`: agent definitions, detection, model lists, argument construction, model validation. -- `daemon/server.js`: `/api/chat` request orchestration, prompt composition, `spawn()` subprocesses, SSE forwarding. -- `daemon/claude-stream.js`: parsing Claude Code structured JSONL output. -- `daemon/json-event-stream.js`: parsing structured JSON/JSONL output from Codex, Gemini, OpenCode, and Cursor Agent. -- `daemon/acp.js`: model detection and streaming session orchestration for the ACP JSON-RPC runtime. +- `apps/daemon/agents.js`: agent definitions, detection, model lists, argument construction, model validation. +- `apps/daemon/server.js`: `/api/chat` request orchestration, prompt composition, `spawn()` subprocesses, SSE forwarding. +- `apps/daemon/claude-stream.js`: parsing Claude Code structured JSONL output. +- `apps/daemon/json-event-stream.js`: parsing structured JSON/JSONL output from Codex, Gemini, OpenCode, and Cursor Agent. +- `apps/daemon/acp.js`: model detection and streaming session orchestration for the ACP JSON-RPC runtime. ## Currently Supported Runtimes -`AGENT_DEFS` in `daemon/agents.js` defines 8 local runtimes: +`AGENT_DEFS` in `apps/daemon/agents.js` defines 8 local runtimes: | id | Name | CLI | Output format | Model list source | |---|---|---|---|---| @@ -61,7 +61,7 @@ The detection result includes: ## Runtime Flow -Actual execution happens in `POST /api/chat` in `daemon/server.js`. +Actual execution happens in `POST /api/chat` in `apps/daemon/server.js`. Flow: @@ -102,7 +102,7 @@ These events are sent to the frontend through the SSE `agent` event. ### Codex / Gemini / OpenCode / Cursor Agent: Structured JSON Event Stream -These four runtimes currently use the unified `json-event-stream` output format, with stdout parsed by `daemon/json-event-stream.js`. +These four runtimes currently use the unified `json-event-stream` output format, with stdout parsed by `apps/daemon/json-event-stream.js`. #### Codex @@ -198,7 +198,7 @@ Kimi uses: kimi acp ``` -The daemon starts an ACP session over stdio through `daemon/acp.js`: +The daemon starts an ACP session over stdio through `apps/daemon/acp.js`: 1. `initialize` 2. `session/new` diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index ba539032f..000000000 --- a/vitest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - environment: 'jsdom', - include: [ - 'src/**/*.test.{ts,tsx,js,mjs,cjs}', - 'daemon/**/*.test.{ts,tsx,js,mjs,cjs}', - 'tests/**/*.test.{ts,tsx}', - ], - }, -});