flavor-code 1.2.9 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/README.zh-CN.md +48 -36
- package/dist/desktop/main.js +81 -9
- package/dist/desktop-renderer/assets/{index-D-s27J39.js → index-BVxdOZFW.js} +16 -16
- package/dist/desktop-renderer/assets/{index-DMZBSGrp.css → index-CPxt3vFY.css} +1 -1
- package/dist/desktop-renderer/index.html +2 -2
- package/package.json +1 -1
- package//346/212/200/346/234/257/346/226/271/346/241/210/346/212/245/345/221/212.md +22 -22
package/README.md
CHANGED
|
@@ -38,6 +38,18 @@ Flavor Code connects to OpenAI, Anthropic, or compatible services and works with
|
|
|
38
38
|
| 🎨 | **D2C design-to-code** | Import Pixso exports; the agent generates Vue/React implementations with automatic pixel-level visual evaluation (Electron only) |
|
|
39
39
|
| 🛡️ | **Clear permission boundaries** | Independent control over read, write, Shell, network, and destructive actions; Docker supported |
|
|
40
40
|
|
|
41
|
+
## 1.2.10 D2C Acceptance & Delivery
|
|
42
|
+
|
|
43
|
+
1.2.10 hardens the D2C acceptance loop: failed authentication prerequisites block protected scenarios immediately, request recording survives navigation, repair prompts accept extra instructions, and the backend process restarts automatically when its source changes.
|
|
44
|
+
|
|
45
|
+
| Feature | How to use |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| **Auth-prerequisite fail-fast** | When a login / sign-in scenario (e.g. `POST /api/v1/auth/login`) fails during interactive acceptance, later protected scenarios are reported as blocked by that failed prerequisite instead of being executed one by one, so the root cause is visible immediately. |
|
|
48
|
+
| **Navigation-safe request recording** | Request recording now persists across in-app navigation through `sessionStorage` (up to 500 entries). Requests fired after a click that navigates to another page are captured too, so post-navigation behavior can still be asserted. |
|
|
49
|
+
| **Extra repair instructions** | Before repairing failed interaction scenarios, type extra requirements in the “补充修复要求” box; they are injected into the repair prompt as user-supplied constraints together with the failure details. |
|
|
50
|
+
| **Acceptance as a separate stage** | The D2C workbench now splits “API Integration” and “Acceptance & Delivery” into two explicit stages; after an automated repair run finishes, the workbench switches to the acceptance tab automatically. |
|
|
51
|
+
| **Backend source fingerprinting** | The mock/server backend is fingerprinted when it starts. If its source files change, the runtime detects the fingerprint difference and restarts the backend before acceptance, so tests always run against the code currently on disk. |
|
|
52
|
+
|
|
41
53
|
## 1.2.9 Runtime Productivity
|
|
42
54
|
|
|
43
55
|
1.2.9 adds layered project instructions, safe writes, background jobs, persistent terminals, and native web tools. Usually you can just describe the goal in natural language and the agent picks the right tool; when you need precise control, name the tool and its parameters explicitly in the prompt.
|
|
@@ -45,11 +57,11 @@ Flavor Code connects to OpenAI, Anthropic, or compatible services and works with
|
|
|
45
57
|
| Feature | How to use |
|
|
46
58
|
| --- | --- |
|
|
47
59
|
| **Layered project instructions** | Put `AGENTS.md` / `CLAUDE.md` in the project root or subdirectories; use `AGENTS.local.md` / `CLAUDE.local.md` for local additions in the same directory. Root rules load at startup; subdirectory rules load automatically when the agent touches files there. |
|
|
48
|
-
| **Per-turn change summary** | No configuration needed. After a successful `Write`, `Edit`, or `ApplyPatch`, the turn shows a color-coded `CHANGESET` receipt with workspace-relative paths, `CREATE` / `UPDATE` / `DELETE` operations, per-file line counts, and a total. At most 8 files are shown, with an explicit shown/total footer when more changed. |
|
|
60
|
+
| **Per-turn change summary** | No configuration needed. After a successful `Write`, `Edit`, or `ApplyPatch`, the turn shows a color-coded `CHANGESET` receipt with workspace-relative paths, `CREATE` / `UPDATE` / `DELETE` operations, per-file line counts, and a total. At most 8 files are shown, with an explicit shown/total footer when more changed. |
|
|
49
61
|
| **File version protection** | No configuration needed. If the IDE, a formatter, or another process modifies a file after the agent read it, the next write fails with `Stale file`; ask the agent to re-read before editing. |
|
|
50
|
-
| **Standard tool presentation protocol** | Tool authors can declare `outputSchema`, `renderForModel`, `presentCall`, and `presentResult`, so the same result can use an appropriate form in the model context, CLI, and desktop. The CLI visually separates file diffs, web evidence, job receipts, foreground `COMMAND` output, and persistent `TERMINAL` output from the final answer. |
|
|
51
|
-
| **Background Shell / Jobs** | Say "start the dev server in the background" and the agent calls `Shell` with `background: true`. Use `JobList` to view jobs, `JobRead` for incremental output, `JobWait` to wait, and `JobKill` to stop. The CLI shows a color-bordered `JOB` receipt separating job metadata, logs, and the final answer; logs show at most the latest 12 lines and lists at most 8 items. Windows prefers UTF-8 and falls back automatically on GBK/GB18030 system diagnostics. |
|
|
52
|
-
| **Foreground command results** | Foreground `Shell` calls render as state-colored `COMMAND` receipts with separate command, stdout, stderr, and exit regions. Long output keeps the first and last 8 lines and explicitly folds the middle; persistent PTY output uses the distinct `TERMINAL` label. |
|
|
62
|
+
| **Standard tool presentation protocol** | Tool authors can declare `outputSchema`, `renderForModel`, `presentCall`, and `presentResult`, so the same result can use an appropriate form in the model context, CLI, and desktop. The CLI visually separates file diffs, web evidence, job receipts, foreground `COMMAND` output, and persistent `TERMINAL` output from the final answer. |
|
|
63
|
+
| **Background Shell / Jobs** | Say "start the dev server in the background" and the agent calls `Shell` with `background: true`. Use `JobList` to view jobs, `JobRead` for incremental output, `JobWait` to wait, and `JobKill` to stop. The CLI shows a color-bordered `JOB` receipt separating job metadata, logs, and the final answer; logs show at most the latest 12 lines and lists at most 8 items. Windows prefers UTF-8 and falls back automatically on GBK/GB18030 system diagnostics. |
|
|
64
|
+
| **Foreground command results** | Foreground `Shell` calls render as state-colored `COMMAND` receipts with separate command, stdout, stderr, and exit regions. Long output keeps the first and last 8 lines and explicitly folds the middle; persistent PTY output uses the distinct `TERMINAL` label. |
|
|
53
65
|
| **Desktop background status** | Electron automatically shows the number of running jobs in the session title bar, updated live on start, output, exit, or cancel. |
|
|
54
66
|
| **Persistent PTY** | Say "open a persistent terminal and keep interacting". The agent uses `TerminalOpen` to create a terminal, `TerminalWrite` for input, `TerminalRead` for incremental output, and `TerminalClose` to close it. |
|
|
55
67
|
| **Unified D2C/E2E process lifecycle** | No usage change. Preview and backend services still start/stop from the E2E/D2C workbench, but the underlying layer unifies output limits, process-tree termination, and idempotent cleanup. |
|
package/README.zh-CN.md
CHANGED
|
@@ -32,42 +32,54 @@ Flavor Code 接入 OpenAI、Anthropic 或兼容服务,在受控工作区内使
|
|
|
32
32
|
| | 能力 | 你得到什么 |
|
|
33
33
|
| --- | --- | --- |
|
|
34
34
|
| 🖥️ | **一个运行时,三个入口** | CLI、Electron 与 VS Code 共享模型配置、会话和工具能力 |
|
|
35
|
-
| 🧭 | **复杂任务可控推进** | 任务计划、子 Agent、steering、follow-up、`/loop` 和 `/goal` |
|
|
36
|
-
| ⏪ | **结果可追溯、可恢复** | 完整时间线、checkpoint、rewind、trace、Diff 和失败审计 |
|
|
37
|
-
| 🧠 | **本地长期上下文** | 记忆、Skill、插件和项目指南均保存在本机 |
|
|
38
|
-
| 🎨 | **D2C 设计转代码** | 导入 Pixso 导出结果,由 Agent 生成 Vue/React 实现并自动进行像素级视觉评估(仅 Electron) |
|
|
39
|
-
| 🛡️ | **明确的权限边界** | 分别控制读、写、Shell、网络和破坏性操作,也可使用 Docker |
|
|
40
|
-
|
|
41
|
-
## 1.2.
|
|
42
|
-
|
|
43
|
-
1.2.
|
|
44
|
-
|
|
45
|
-
| 功能 | 使用方式 |
|
|
46
|
-
| --- | --- |
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
35
|
+
| 🧭 | **复杂任务可控推进** | 任务计划、子 Agent、steering、follow-up、`/loop` 和 `/goal` |
|
|
36
|
+
| ⏪ | **结果可追溯、可恢复** | 完整时间线、checkpoint、rewind、trace、Diff 和失败审计 |
|
|
37
|
+
| 🧠 | **本地长期上下文** | 记忆、Skill、插件和项目指南均保存在本机 |
|
|
38
|
+
| 🎨 | **D2C 设计转代码** | 导入 Pixso 导出结果,由 Agent 生成 Vue/React 实现并自动进行像素级视觉评估(仅 Electron) |
|
|
39
|
+
| 🛡️ | **明确的权限边界** | 分别控制读、写、Shell、网络和破坏性操作,也可使用 Docker |
|
|
40
|
+
|
|
41
|
+
## 1.2.10 D2C 验收与交付
|
|
42
|
+
|
|
43
|
+
1.2.10 强化 D2C 验收闭环:认证前置失败快速阻断、请求记录跨导航保留、修复提示支持补充要求,后端源码变化时自动重启。
|
|
44
|
+
|
|
45
|
+
| 功能 | 使用方式 |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| **认证前置失败快速阻断** | 交互验收中,登录/认证类场景(如 `POST /api/v1/auth/login`)失败后,后续受保护场景不再逐个执行,直接标记为被该失败前置场景阻断,根因一目了然。 |
|
|
48
|
+
| **跨导航请求记录** | 请求记录通过 `sessionStorage` 在页面导航之间保留(最多 500 条),点击跳转等导航之后的请求同样会被捕获,可继续参与断言。 |
|
|
49
|
+
| **修复补充要求** | 修复失败的交互场景前,可在“补充修复要求”输入框填写额外约束,它们会与失败详情一起作为“用户补充要求”注入修复提示词。 |
|
|
50
|
+
| **验收与交付独立阶段** | D2C 工作台将“接口联调”与“验收与交付”拆分为两个明确阶段;自动修复完成后自动切换到验收页签。 |
|
|
51
|
+
| **后端源码指纹检测** | 启动时为 mock/server 后端源码计算指纹;源码文件变化后,运行时检测到指纹差异会自动重启后端,确保验收针对磁盘上的最新代码。 |
|
|
52
|
+
|
|
53
|
+
## 1.2.9 运行时生产力
|
|
54
|
+
|
|
55
|
+
1.2.9 新增分层项目指令、安全写入、后台任务、持久终端和原生 Web 工具。通常只需用自然语言描述目标,Agent 会选择合适的工具;需要精确控制时,也可以在提示词中明确指定工具和参数。
|
|
56
|
+
|
|
57
|
+
| 功能 | 使用方式 |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| **分层项目指令** | 在项目根目录或子目录放置 `AGENTS.md` / `CLAUDE.md`;同目录需要本地补充时使用 `AGENTS.local.md` / `CLAUDE.local.md`。根规则启动时加载,子目录规则在 Agent 访问该目录文件时自动加载。 |
|
|
60
|
+
| **每轮成果物汇总** | 无需配置。`Write`、`Edit` 或 `ApplyPatch` 成功后,回合结束会显示带语义色的 `CHANGESET` 收据,使用工作区相对路径列出 `CREATE` / `UPDATE` / `DELETE` 操作、各文件行数和总计。最多展示 8 个文件,超出时明确显示已展示数与总数。 |
|
|
61
|
+
| **文件版本保护** | 无需配置。如果 IDE、格式化器或其他进程在 Agent 读取后修改了文件,后续写入会报 `Stale file`;让 Agent 重新读取后再修改即可。 |
|
|
62
|
+
| **标准工具展示协议** | 工具作者可声明 `outputSchema`、`renderForModel`、`presentCall` 和 `presentResult`,让同一结果在模型上下文、CLI 与桌面端分别使用合适的形式。CLI 会把文件 Diff、Web 证据、Job 运行收据、前台 `COMMAND` 和持久 `TERMINAL` 与最终回答明确分开。 |
|
|
63
|
+
| **后台 Shell / Job** | 提示“在后台启动开发服务器”,Agent 会调用 `Shell` 并设置 `background: true`。使用 `JobList` 查看任务、`JobRead` 增量读取输出、`JobWait` 等待结束、`JobKill` 停止任务。CLI 使用带状态色边界的 `JOB` 收据区分任务元数据、日志与最终回答;日志最多显示最近 12 行,列表最多显示 8 项。Windows 优先使用 UTF-8,遇到 GBK/GB18030 系统诊断时自动回退。 |
|
|
64
|
+
| **前台命令结果** | 前台 `Shell` 显示为带状态色的 `COMMAND` 收据,分别展示命令、stdout、stderr 和退出状态。长输出保留开头 8 行与结尾 8 行,并明确折叠中间部分;持久 PTY 使用独立的 `TERMINAL` 标签。 |
|
|
65
|
+
| **桌面后台状态** | Electron 会在会话标题栏自动显示运行中的 Job 数量,并在任务启动、输出、退出或取消时实时更新。 |
|
|
66
|
+
| **持久 PTY** | 提示“打开一个持久终端并继续交互”。Agent 使用 `TerminalOpen` 创建终端、`TerminalWrite` 输入、`TerminalRead` 增量读取输出,并用 `TerminalClose` 关闭。 |
|
|
67
|
+
| **D2C/E2E 统一进程生命周期** | 使用方式不变。预览和后端服务仍从 E2E/D2C 工作台启动或停止,底层统一处理输出限制、进程树终止和幂等清理。 |
|
|
68
|
+
| **原生 WebSearch** | 提示“搜索 Web 上的……”,或明确要求使用 `WebSearch`。默认使用无需密钥的 DuckDuckGo Lite;连接失败、HTTP 拒绝或没有可解析结果时自动降级到 Bing。单次最多返回 20 条;CLI 将前 5 条放入带边界的 `WEB SEARCH` 证据块,按搜索排名显示标题和紧凑来源。 |
|
|
69
|
+
| **原生 WebFetch** | 提示“读取这个网页:`https://...`”,或明确要求使用 `WebFetch`。支持 HTTP(S)、重定向、HTML 转文本、超时和响应大小限制,并兼容 Clash/TUN Fake-IP DNS。直接访问 Fake-IP、内网或云元数据地址仍会被拦截;网络操作继续遵守 Flavor 权限审批。 |
|
|
70
|
+
|
|
71
|
+
常见的精确用法:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
使用 Shell 后台模式启动 npm run dev,然后通过 JobRead 检查启动日志。
|
|
75
|
+
打开持久终端,在其中运行 Python REPL,连续执行两段代码后关闭终端。
|
|
76
|
+
使用 WebSearch 搜索 TypeScript 7 官方迁移说明,再用 WebFetch 读取最相关的官方页面。
|
|
77
|
+
这个目录有独立约定,请先遵守 src/payments/AGENTS.md 再修改代码。
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
原生工具的参数、状态机、安全边界和扩展接口详见[技术方案报告第 38 节](./技术方案报告.md#38-129-运行时生产力与原生-web-能力);验收规格见[运行时生产力规范](./docs/specs/2026-08-13-runtime-productivity-waves.md)。
|
|
81
|
+
|
|
82
|
+
## 快速开始
|
|
71
83
|
|
|
72
84
|
> [!IMPORTANT]
|
|
73
85
|
> CLI 需要 Node.js 20 或更高版本。Windows 桌面端也可以直接从 [Releases](https://github.com/YachuanWzh/flavor-code/releases) 下载。
|
package/dist/desktop/main.js
CHANGED
|
@@ -1331,14 +1331,29 @@ function expectationDescription(step) {
|
|
|
1331
1331
|
if (step.expect === "class") return `${step.selector} to have class ${step.value}`;
|
|
1332
1332
|
return `${step.selector} ${step.expect} to equal ${String(step.value)}`;
|
|
1333
1333
|
}
|
|
1334
|
+
function isAuthenticationPrerequisite(scenario) {
|
|
1335
|
+
return scenario.steps.some((step) => "expect" in step && step.expect === "request" && step.method === "POST" && step.path !== void 0 && /(?:auth|login|sign-in|session|token)/i.test(step.path) && (step.status === void 0 || step.status >= 200 && step.status < 400));
|
|
1336
|
+
}
|
|
1334
1337
|
async function runInteractionManifest(manifest, baseUrl, createDriver) {
|
|
1335
1338
|
if (!isLoopbackPreviewUrl(baseUrl)) throw new Error("D2C interaction preview must use a loopback HTTP URL");
|
|
1336
1339
|
const origin = new URL(baseUrl).origin;
|
|
1337
1340
|
const scenarios = [];
|
|
1341
|
+
let blockedByAuthentication;
|
|
1338
1342
|
for (const page of manifest.pages) {
|
|
1339
1343
|
const pageUrl = new URL(page.url, baseUrl).toString();
|
|
1340
1344
|
if (new URL(pageUrl).origin !== origin) throw new Error(`D2C interaction page escaped preview origin: ${page.url}`);
|
|
1341
1345
|
for (const scenario of page.scenarios) {
|
|
1346
|
+
if (blockedByAuthentication !== void 0) {
|
|
1347
|
+
scenarios.push({
|
|
1348
|
+
id: scenario.id,
|
|
1349
|
+
pageUrl,
|
|
1350
|
+
passed: false,
|
|
1351
|
+
durationMs: 0,
|
|
1352
|
+
apiRequestCount: 0,
|
|
1353
|
+
failure: `Blocked by failed authentication prerequisite ${blockedByAuthentication.id}: ${blockedByAuthentication.failure}`
|
|
1354
|
+
});
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1342
1357
|
const started = Date.now();
|
|
1343
1358
|
let driver;
|
|
1344
1359
|
let failure;
|
|
@@ -1377,6 +1392,9 @@ async function runInteractionManifest(manifest, baseUrl, createDriver) {
|
|
|
1377
1392
|
...failure === void 0 ? {} : { failure },
|
|
1378
1393
|
...capturedRequests === void 0 ? {} : { requests: capturedRequests }
|
|
1379
1394
|
});
|
|
1395
|
+
if (failure !== void 0 && isAuthenticationPrerequisite(scenario)) {
|
|
1396
|
+
blockedByAuthentication = { id: scenario.id, failure };
|
|
1397
|
+
}
|
|
1380
1398
|
}
|
|
1381
1399
|
}
|
|
1382
1400
|
const failures = scenarios.filter((item) => !item.passed).length;
|
|
@@ -1742,15 +1760,20 @@ function assertionScript(step) {
|
|
|
1742
1760
|
const actual = (element.textContent || "").trim(); return { passed: step.expect === "text" ? actual === step.value : actual.includes(step.value), actual };
|
|
1743
1761
|
})()`;
|
|
1744
1762
|
}
|
|
1745
|
-
function requestRecorderScript(mockOrigin) {
|
|
1763
|
+
function requestRecorderScript(mockOrigin, reset = false) {
|
|
1746
1764
|
return `(() => {
|
|
1765
|
+
const storageKey = "__flavorD2cRequests";
|
|
1766
|
+
if (${JSON.stringify(reset)}) { sessionStorage.removeItem(storageKey); window.__flavorD2cRequests = undefined; }
|
|
1747
1767
|
if (window.__flavorD2cRequests) return;
|
|
1748
|
-
window.__flavorD2cRequests = [];
|
|
1768
|
+
try { window.__flavorD2cRequests = JSON.parse(sessionStorage.getItem(storageKey) || "[]"); }
|
|
1769
|
+
catch { window.__flavorD2cRequests = []; }
|
|
1749
1770
|
const record = (method, url, status) => {
|
|
1750
1771
|
try {
|
|
1751
1772
|
const target = new URL(url, location.href);
|
|
1752
1773
|
if (target.origin !== ${JSON.stringify(mockOrigin)}) return;
|
|
1753
1774
|
window.__flavorD2cRequests.push({ method: String(method || "GET").toUpperCase(), path: target.pathname, status: Number(status) || 0 });
|
|
1775
|
+
window.__flavorD2cRequests = window.__flavorD2cRequests.slice(-500);
|
|
1776
|
+
sessionStorage.setItem(storageKey, JSON.stringify(window.__flavorD2cRequests));
|
|
1754
1777
|
} catch { }
|
|
1755
1778
|
};
|
|
1756
1779
|
const originalFetch = window.fetch;
|
|
@@ -1934,7 +1957,8 @@ function createEmbeddedD2cAutomation(getHost, options = {}) {
|
|
|
1934
1957
|
return runInteractionManifest(manifest, baseUrl, async ({ id: scenarioId }) => {
|
|
1935
1958
|
const reset = await fetch(new URL("/_e2e/reset", mockOrigin), {
|
|
1936
1959
|
method: "POST",
|
|
1937
|
-
headers: { "X-Flavor-E2E": "reset" }
|
|
1960
|
+
headers: { "X-Flavor-E2E": "reset" },
|
|
1961
|
+
signal: AbortSignal.timeout(Math.min(navigationTimeoutMs, 5e3))
|
|
1938
1962
|
}).catch(() => void 0);
|
|
1939
1963
|
if (reset !== void 0 && !reset.ok && reset.status !== 404 && reset.status !== 405) {
|
|
1940
1964
|
throw new Error(`E2E backend reset failed with status ${reset.status}`);
|
|
@@ -1947,7 +1971,7 @@ function createEmbeddedD2cAutomation(getHost, options = {}) {
|
|
|
1947
1971
|
load: async (url) => {
|
|
1948
1972
|
const loaded = await navigate(url);
|
|
1949
1973
|
activeUrl = loaded.url;
|
|
1950
|
-
await frame(activeUrl).executeJavaScript(requestRecorderScript(mockOrigin), true).catch(() => void 0);
|
|
1974
|
+
await frame(activeUrl).executeJavaScript(requestRecorderScript(mockOrigin, true), true).catch(() => void 0);
|
|
1951
1975
|
await frame(activeUrl).executeJavaScript(scenarioScript(scenarioId, visualDelayMs), true);
|
|
1952
1976
|
},
|
|
1953
1977
|
action: async (step) => {
|
|
@@ -1957,16 +1981,19 @@ function createEmbeddedD2cAutomation(getHost, options = {}) {
|
|
|
1957
1981
|
const target = new URL(step.url, baseUrl).toString();
|
|
1958
1982
|
const loaded = await navigate(target);
|
|
1959
1983
|
activeUrl = loaded.url;
|
|
1984
|
+
await loaded.executeJavaScript(requestRecorderScript(mockOrigin), true).catch(() => void 0);
|
|
1960
1985
|
await loaded.executeJavaScript(scenarioScript(scenarioId, visualDelayMs), true);
|
|
1961
1986
|
return;
|
|
1962
1987
|
}
|
|
1963
1988
|
await frame(activeUrl).executeJavaScript(actionScript(step, scenarioId, stepNumber, visualDelayMs), true);
|
|
1964
1989
|
activeUrl = (await settleAfterAction(activeUrl)).url;
|
|
1990
|
+
await frame(activeUrl).executeJavaScript(requestRecorderScript(mockOrigin), true).catch(() => void 0);
|
|
1965
1991
|
} catch (error) {
|
|
1966
1992
|
if (step.action === "click") {
|
|
1967
1993
|
const recovered = await settleAfterAction(activeUrl).catch(() => void 0);
|
|
1968
1994
|
if (recovered !== void 0 && recovered.url !== activeUrl) {
|
|
1969
1995
|
activeUrl = recovered.url;
|
|
1996
|
+
await frame(activeUrl).executeJavaScript(requestRecorderScript(mockOrigin), true).catch(() => void 0);
|
|
1970
1997
|
return;
|
|
1971
1998
|
}
|
|
1972
1999
|
}
|
|
@@ -21643,7 +21670,7 @@ function createMemoryTaskId() {
|
|
|
21643
21670
|
|
|
21644
21671
|
// src/desktop/runtime-controller.ts
|
|
21645
21672
|
import { createHash as createHash18 } from "crypto";
|
|
21646
|
-
import { mkdir as mkdir22, readFile as readFile30, rm as rm13, stat as stat12, writeFile as writeFile14 } from "fs/promises";
|
|
21673
|
+
import { mkdir as mkdir22, readFile as readFile30, readdir as readdir12, rm as rm13, stat as stat12, writeFile as writeFile14 } from "fs/promises";
|
|
21647
21674
|
import { homedir as homedir3 } from "os";
|
|
21648
21675
|
import { basename as basename8, join as join34, resolve as resolve32 } from "path";
|
|
21649
21676
|
|
|
@@ -23815,6 +23842,36 @@ async function detectD2cFramework(workspace, task) {
|
|
|
23815
23842
|
function integrationDirectory(workspace, task) {
|
|
23816
23843
|
return join34(workspace, ".flavor", "d2c", task, "integration");
|
|
23817
23844
|
}
|
|
23845
|
+
var D2C_BACKEND_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".cfg", ".env", ".ini", ".js", ".json", ".lock", ".mjs", ".py", ".toml", ".txt", ".yaml", ".yml"]);
|
|
23846
|
+
var D2C_BACKEND_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".venv", "__pycache__", "data", "node_modules", "venv"]);
|
|
23847
|
+
async function d2cBackendSourceFingerprint(project) {
|
|
23848
|
+
const runtimeManifest = join34(project, "server", "flavor-runtime.json");
|
|
23849
|
+
const realBackend = (await stat12(runtimeManifest).catch(() => void 0))?.isFile() === true;
|
|
23850
|
+
const root = realBackend ? join34(project, "server") : join34(project, "mock");
|
|
23851
|
+
if ((await stat12(root).catch(() => void 0))?.isDirectory() !== true) return void 0;
|
|
23852
|
+
const files = [];
|
|
23853
|
+
const visit = async (directory) => {
|
|
23854
|
+
const entries = await readdir12(directory, { withFileTypes: true });
|
|
23855
|
+
for (const entry2 of entries) {
|
|
23856
|
+
if (entry2.isDirectory()) {
|
|
23857
|
+
if (!D2C_BACKEND_IGNORED_DIRECTORIES.has(entry2.name)) await visit(join34(directory, entry2.name));
|
|
23858
|
+
continue;
|
|
23859
|
+
}
|
|
23860
|
+
if (!entry2.isFile()) continue;
|
|
23861
|
+
const extension2 = entry2.name.includes(".") ? `.${entry2.name.split(".").pop().toLowerCase()}` : "";
|
|
23862
|
+
if (D2C_BACKEND_SOURCE_EXTENSIONS.has(extension2)) files.push(join34(directory, entry2.name));
|
|
23863
|
+
}
|
|
23864
|
+
};
|
|
23865
|
+
await visit(root);
|
|
23866
|
+
const hash = createHash18("sha256");
|
|
23867
|
+
for (const path of files.sort()) {
|
|
23868
|
+
hash.update(path.slice(root.length));
|
|
23869
|
+
hash.update("\0");
|
|
23870
|
+
hash.update(await readFile30(path));
|
|
23871
|
+
hash.update("\0");
|
|
23872
|
+
}
|
|
23873
|
+
return hash.digest("hex");
|
|
23874
|
+
}
|
|
23818
23875
|
var DesktopRuntimeController = class {
|
|
23819
23876
|
#home;
|
|
23820
23877
|
#createRuntime;
|
|
@@ -23848,6 +23905,8 @@ var DesktopRuntimeController = class {
|
|
|
23848
23905
|
#d2cProductPreviews = /* @__PURE__ */ new Map();
|
|
23849
23906
|
/** Mock URL each preview observed at startup; Vite only reads .env.local on boot. */
|
|
23850
23907
|
#d2cPreviewMockUrls = /* @__PURE__ */ new Map();
|
|
23908
|
+
/** Backend source digest observed when the managed process was started. */
|
|
23909
|
+
#d2cMockSourceFingerprints = /* @__PURE__ */ new Map();
|
|
23851
23910
|
constructor(options) {
|
|
23852
23911
|
this.#home = resolve32(options.home ?? homedir3());
|
|
23853
23912
|
this.#createRuntime = options.createRuntime ?? (async (runtimeOptions) => createProductionRuntime(runtimeOptions));
|
|
@@ -24417,6 +24476,8 @@ var DesktopRuntimeController = class {
|
|
|
24417
24476
|
const running = await this.#runD2cMockServer(project);
|
|
24418
24477
|
this.#d2cMocks.set(task, running);
|
|
24419
24478
|
try {
|
|
24479
|
+
const fingerprint = await d2cBackendSourceFingerprint(project);
|
|
24480
|
+
if (fingerprint !== void 0) this.#d2cMockSourceFingerprints.set(task, fingerprint);
|
|
24420
24481
|
const environmentPath = join34(project, ".env.local");
|
|
24421
24482
|
const existingEnvironment = await readFile30(environmentPath, "utf8").catch(() => "");
|
|
24422
24483
|
const environment = `${existingEnvironment.split(/\r?\n/).filter((line) => !/^\s*VITE_API_BASE_URL\s*=/.test(line)).join("\n").trim()}
|
|
@@ -24426,6 +24487,7 @@ VITE_API_BASE_URL=${running.url}
|
|
|
24426
24487
|
return { running: true, url: running.url, output: running.output() };
|
|
24427
24488
|
} catch (error) {
|
|
24428
24489
|
this.#d2cMocks.delete(task);
|
|
24490
|
+
this.#d2cMockSourceFingerprints.delete(task);
|
|
24429
24491
|
await running.stop();
|
|
24430
24492
|
throw error;
|
|
24431
24493
|
}
|
|
@@ -24435,6 +24497,7 @@ VITE_API_BASE_URL=${running.url}
|
|
|
24435
24497
|
const running = this.#d2cMocks.get(task);
|
|
24436
24498
|
if (running !== void 0) {
|
|
24437
24499
|
this.#d2cMocks.delete(task);
|
|
24500
|
+
this.#d2cMockSourceFingerprints.delete(task);
|
|
24438
24501
|
await running.stop();
|
|
24439
24502
|
}
|
|
24440
24503
|
return { running: false };
|
|
@@ -24574,14 +24637,22 @@ VITE_API_BASE_URL=${running.url}
|
|
|
24574
24637
|
scenarios: result.scenarios.map((scenario) => scenario.passed ? scenario : { ...scenario, failure: `${scenario.failure ?? "Scenario failed"} | ${note}` })
|
|
24575
24638
|
};
|
|
24576
24639
|
}
|
|
24577
|
-
/** Restarts a dead
|
|
24640
|
+
/** Restarts a dead or source-stale backend and any preview whose baked-in API URL no longer matches it. */
|
|
24578
24641
|
async #syncD2cRuntimeForAcceptance(task) {
|
|
24579
24642
|
const mock = this.#d2cMocks.get(task);
|
|
24580
24643
|
if (mock === void 0) return;
|
|
24581
|
-
const
|
|
24582
|
-
if (
|
|
24583
|
-
|
|
24644
|
+
const workspace = this.#workspace;
|
|
24645
|
+
if (workspace === void 0) return;
|
|
24646
|
+
const [healthy, currentFingerprint] = await Promise.all([
|
|
24647
|
+
this.#probeD2cMock(mock.url),
|
|
24648
|
+
d2cBackendSourceFingerprint(d2cOutputDirectory(workspace, task))
|
|
24649
|
+
]);
|
|
24650
|
+
const startedFingerprint = this.#d2cMockSourceFingerprints.get(task);
|
|
24651
|
+
const sourceChanged = currentFingerprint !== void 0 && currentFingerprint !== startedFingerprint;
|
|
24652
|
+
if (healthy && !sourceChanged && this.#d2cPreviewMockUrls.get(task) === mock.url) return;
|
|
24653
|
+
if (!healthy || sourceChanged) {
|
|
24584
24654
|
this.#d2cMocks.delete(task);
|
|
24655
|
+
this.#d2cMockSourceFingerprints.delete(task);
|
|
24585
24656
|
await mock.stop().catch(() => void 0);
|
|
24586
24657
|
await this.startD2cMock(task);
|
|
24587
24658
|
}
|
|
@@ -24922,6 +24993,7 @@ VITE_API_BASE_URL=${running.url}
|
|
|
24922
24993
|
async #stopAllD2cMocks() {
|
|
24923
24994
|
const running = [...this.#d2cMocks.values()];
|
|
24924
24995
|
this.#d2cMocks.clear();
|
|
24996
|
+
this.#d2cMockSourceFingerprints.clear();
|
|
24925
24997
|
await Promise.all(running.map((item) => item.stop().catch(() => void 0)));
|
|
24926
24998
|
}
|
|
24927
24999
|
#mockUrl(task) {
|