diff --git a/.github/workflows/cli-package.yml b/.github/workflows/cli-package.yml index c662c80ba1..5494a1ce18 100644 --- a/.github/workflows/cli-package.yml +++ b/.github/workflows/cli-package.yml @@ -26,6 +26,7 @@ concurrency: jobs: # ── Resolve version info (mirrors desktop-package.yml) ───────────── prepare: + if: github.event_name != 'release' || !startsWith(github.event.release.tag_name, 'data-migrator-v') name: Prepare runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/data-migrator-package.yml b/.github/workflows/data-migrator-package.yml new file mode 100644 index 0000000000..be681e5930 --- /dev/null +++ b/.github/workflows/data-migrator-package.yml @@ -0,0 +1,112 @@ +name: Data Migrator Package + +on: + push: + tags: ['data-migrator-v*'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: data-migrator-package-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: Migrator (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - platform: windows-x64 + os: windows-latest + target: x86_64-pc-windows-msvc + bundles: --no-bundle + - platform: macos-arm64 + os: macos-15 + target: aarch64-apple-darwin + bundles: --bundles dmg + - platform: macos-x64 + os: macos-15-intel + target: x86_64-apple-darwin + bundles: --bundles dmg + - platform: linux-x64 + os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + bundles: --bundles appimage + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v5 + with: + node-version: '22.18.0' + cache: pnpm + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: swatinem/rust-cache@v2 + with: + shared-key: data-migrator-${{ matrix.platform }} + cache-bin: false + - name: Install Linux WebView build dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + - run: pnpm install --frozen-lockfile --ignore-scripts + - name: Verify independent version and packaging contract + run: | + node scripts/data-migrator-release.mjs --check-version + node --test scripts/data-migrator-tauri-build.test.mjs + - name: Check offline data compatibility + run: cargo test --locked -p openbitfun-data-migrator -p openbitfun-legacy-migration-adapters -p openbitfun-legacy-migration -p openbitfun-config-contracts --lib --test migration_engine_contracts + - name: Build independent package + run: pnpm run data-migrator:build -- --target ${{ matrix.target }} ${{ matrix.bundles }} + - name: Stage package and checksum + run: node scripts/data-migrator-release.mjs ${{ matrix.platform }} + - uses: actions/upload-artifact@v6 + with: + name: data-migrator-${{ matrix.platform }} + path: target/data-migrator-release/${{ matrix.platform }}/* + if-no-files-found: error + + draft-release: + if: startsWith(github.ref, 'refs/tags/data-migrator-v') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: '22.18.0' + - uses: actions/download-artifact@v7 + with: + pattern: data-migrator-* + path: migrator-release-assets + merge-multiple: true + - name: Sign and verify with the independent migrator release key + shell: bash + env: + OPENBITFUN_SIGNING_KEY: ${{ secrets.DATA_MIGRATOR_SIGNING_PRIVATE_KEY }} + OPENBITFUN_SIGNING_PASSWORD: ${{ secrets.DATA_MIGRATOR_SIGNING_PRIVATE_KEY_PASSWORD }} + OPENBITFUN_SIGNING_PUBKEY: ${{ secrets.DATA_MIGRATOR_SIGNING_PUBKEY }} + run: | + set -euo pipefail + test -n "$OPENBITFUN_SIGNING_KEY" + test -n "$OPENBITFUN_SIGNING_PUBKEY" + cat migrator-release-assets/*.sha256 | sort -k2 > migrator-release-assets/SHA256SUMS + (cd migrator-release-assets && sha256sum --check SHA256SUMS) + bash scripts/sign-release-assets.sh migrator-release-assets/* + node scripts/write-minisign-public-key.mjs --out migrator-release-assets/data-migrator.minisign.pub + - name: Create a separately reviewed release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ github.ref_name }} + name: OpenBitFun Data Migrator ${{ github.ref_name }} + draft: true + make_latest: false + files: migrator-release-assets/* + body_path: src/apps/data-migrator/RELEASE.md diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index b5f2480557..44e90b0601 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -44,6 +44,7 @@ concurrency: jobs: # ── Resolve version info ─────────────────────────────────────────── prepare: + if: github.event_name != 'release' || !startsWith(github.event.release.tag_name, 'data-migrator-v') name: Prepare runs-on: ubuntu-latest outputs: diff --git a/Cargo.lock b/Cargo.lock index 7d9fa167ab..5b599783f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5863,6 +5863,21 @@ dependencies = [ "url", ] +[[package]] +name = "openbitfun-config-contracts" +version = "1.0.0" +dependencies = [ + "chrono", + "hex", + "log", + "openbitfun-core-types", + "openbitfun-product-domains", + "serde", + "serde_json", + "sha2", + "ts-rs", +] + [[package]] name = "openbitfun-core" version = "1.0.0" @@ -5897,11 +5912,12 @@ dependencies = [ "openbitfun-ai-adapters", "openbitfun-claude-code-adapter", "openbitfun-codex-adapter", + "openbitfun-config-contracts", "openbitfun-core-types", "openbitfun-dsh-adapter", "openbitfun-events", "openbitfun-external-sources", - "openbitfun-legacy-migration", + "openbitfun-legacy-migration-adapters", "openbitfun-opencode-adapter", "openbitfun-opencode-plugin-host", "openbitfun-plugin-runtime-client", @@ -5945,12 +5961,11 @@ dependencies = [ [[package]] name = "openbitfun-data-migrator" -version = "1.0.0" +version = "0.1.0" dependencies = [ - "openbitfun-core", "openbitfun-core-types", "openbitfun-legacy-migration", - "openbitfun-product-capabilities", + "openbitfun-legacy-migration-adapters", "openbitfun-product-domains", "serde", "serde_json", @@ -6003,7 +6018,6 @@ dependencies = [ "openbitfun-core", "openbitfun-core-types", "openbitfun-events", - "openbitfun-legacy-migration", "openbitfun-product-domains", "openbitfun-relay-service", "openbitfun-runtime-ports", @@ -6111,6 +6125,30 @@ dependencies = [ "windows 0.61.3", ] +[[package]] +name = "openbitfun-legacy-migration-adapters" +version = "1.0.0" +dependencies = [ + "anyhow", + "chrono", + "dunce", + "hex", + "openbitfun-agent-runtime", + "openbitfun-config-contracts", + "openbitfun-core-types", + "openbitfun-legacy-migration", + "openbitfun-product-domains", + "openbitfun-services-core", + "openbitfun-services-integrations", + "rusqlite", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "uuid", +] + [[package]] name = "openbitfun-miniapp-market-server" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index b4f13d7fe5..7460f7acf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "src/crates/services/services-core", "src/crates/services/services-integrations", "src/crates/services/legacy-migration", + "src/crates/services/legacy-migration-adapters", "src/crates/services/miniapp-market-service", "src/crates/services/skin-market-service", "src/crates/services/relay-service", @@ -46,6 +47,7 @@ members = [ "src/crates/execution/tool-provider-groups", "src/crates/execution/tool-execution", "src/crates/contracts/core-types", + "src/crates/contracts/config-contracts", "src/crates/contracts/events", "src/crates/contracts/runtime-ports", ] diff --git a/README.md b/README.md index cbd9d9d93b..524d80a94d 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,12 @@ OpenBitFun is still evolving. Here are three directions we want to explore: Getting there means making long-running tasks more reliable, the runtime more efficient, and the desktop experience smoother. +## Optional legacy data migration + +Data migration is optional and uses a **separately downloaded OpenBitFun Data +Migrator**. It runs independently and is not bundled with or launched by the main +application. See the [download, compatibility and recovery guide](src/apps/data-migrator/README.md). + ## Build with it. Help shape it. **Star OpenBitFun to follow along. Share what you build, offer feedback, or contribute code to help shape what comes next.** diff --git a/README.zh-CN.md b/README.zh-CN.md index af66b1db9e..7c500d8543 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -105,6 +105,12 @@ OpenBitFun 仍在演进,我们希望继续探索三个方向: 更可靠的长任务、更高效的 Runtime、更从容的桌面体验,是这些探索共同的基础。 +## 按需迁移旧版数据 + +数据迁移是可选操作,请按需**单独下载 OpenBitFun 数据迁移器**。工具独立运行, +不随主应用打包,也不会由主应用自动启动。下载、兼容范围、操作步骤与中断恢复见 +[迁移器使用说明](src/apps/data-migrator/README.zh-CN.md)。 + ## 用它创造,也一起创造它 **欢迎用 Star 关注 OpenBitFun,用作品、反馈和代码参与它的未来。** diff --git a/docs/architecture/product-architecture.md b/docs/architecture/product-architecture.md index b4bbea2ad4..b8a0bd1be3 100644 --- a/docs/architecture/product-architecture.md +++ b/docs/architecture/product-architecture.md @@ -895,3 +895,19 @@ Shared Agent Runtime 是第一方多实例的目标部署,不是上表新增 启停顺序和失败回滚。这项宿主接入不构成 CLI、Server、ACP 或 HarmonyOS 本地产品支持。 - HarmonyOS PC 的完整目标同时包含本地 CLI/TUI 与 GUI,当前均不能标记可用;两种宿主分别验收,具体支持证据和禁止替代项以平台规约及各自专题为准。 - 文档、边界脚本和 focused 测试能说明本次变更保护了哪个稳定接口边界,或删除/降级了哪个过宽接口。 + +## 独立数据迁移工具的依赖边界 + +Data Migrator 是独立发布的本地离线工具,不依赖 Core、Product Assembly、Desktop 或 Web UI。 +主应用不检测、启动或捆绑迁移器。两者在同一源码工作区复用稳定的数据格式与存储实现: + +- contracts/config-contracts:配置 DTO、默认值、版本校验及到共享模型 DTO 的纯转换;Core 原路径保留转发,ConfigProvider 仍在 Core。 +- services-core 的 workspace-persistence、coordination-store、session-event-format:工作区记录、注册表校验、SQLite 物理 schema 和会话日志格式。 +- services/legacy-migration-adapters:旧版读取、转换、引用修复;只调用共享存储 owner。 +- services/legacy-migration:快照、锁、暂存、备份、原子写入、日志恢复和无时效交接依赖的任务存储。 + +本次只移动数据/存储 owner,不移动 WorkspaceManager、会话生命周期、权限、事件或远程执行。 +WorkspaceInfo/WorkspaceIdentity 的运行操作由 Core 的 runtime extension traits 保留,稳定记录无需导入这些能力。 +原 Core 存储入口保留错误映射;可选 legacy-migration facade 保留旧导入路径,但不再由 product-full 启用。 +远程四种场景不提供迁移工具的执行入口;仅转换本机保存的连接记录,不连接远端。 +使用与发行契约以 [独立迁移器说明](../../src/apps/data-migrator/README.zh-CN.md) 为准。 diff --git a/docs/architecture/product-customization-blueprint.md b/docs/architecture/product-customization-blueprint.md index 22ff0d80bd..4e864d4a26 100644 --- a/docs/architecture/product-customization-blueprint.md +++ b/docs/architecture/product-customization-blueprint.md @@ -17,15 +17,13 @@ C0a 实现一个构建期 JSONC 产品定义、严格解析器和确定性解析 当前真实消费者只有: - Desktop build adapter:从解析结果覆盖 Tauri `productName`、`mainBinaryName` 与 bundle identifier; -- Data Migrator build adapter:从解析结果覆盖独立 Tauri 身份,并把 Desktop 与 Migrator 的 sibling binary name - 编译进交接边界; - CLI dev/build wrapper:从同一解析结果设置命令名、隔离定制构建缓存,并按成员 `binaryName` 暂存构建产物; -- First-party Rust artifacts:Desktop/Data Migrator/CLI build adapter 通过编译期环境注入 `productId`、 +- First-party Rust artifacts:Desktop/CLI build adapter 通过编译期环境注入 `productId`、 `dataNamespace` 与由其派生的隐藏目录名,`openbitfun-core-types::product_identity` 作为最小事实 owner,供数据路径、Runtime ownership、Remote Connect 与 Detached Dispatch 复用; `product:check` / `product:explain` 只是构建作者的校验与解释工具,不计作产品字段的生产消费者。C0a 不生成无人读取的 -通用产品 manifest 或 locale projection;三个 build adapter 直接消费同一次内存解析结果,Rust consumer +通用产品 manifest 或 locale projection;两个 build adapter 直接消费同一次内存解析结果,Rust consumer 只读取随对应产品 artifact 编译进去的不可变事实,不在运行时重新选产品。 产品定义 v1 仅包含已被这些消费者读取的字段,未知字段一律拒绝。localized 名称独立于技术 ID,并按共享 locale contract @@ -37,7 +35,7 @@ GUI/TUI 布局、插件/内置扩展选择、Installer/Store target、更新与 ### 0.1 当前定义与解析契约 -产品定义描述一个 family,其中 Desktop、Data Migrator 与 CLI 是分别命名、分别消费的成员;Installer 与 Store 是可能的 Desktop +产品定义描述一个 family,其中 Desktop 与 CLI 是分别命名、分别消费的成员;Installer 与 Store 是可能的 Desktop 交付目标,不是独立成员,当前也没有对应实现。schema v1 只接受以下已消费字段: ```jsonc @@ -51,11 +49,6 @@ GUI/TUI 布局、插件/内置扩展选择、Installer/Store target、更新与 "binaryName": "acme-desktop", "bundleId": "com.acme.desktop" }, - "dataMigrator": { - "displayNameKey": "product.dataMigrator.name", - "binaryName": "acme-data-migrator", - "bundleId": "com.acme.data-migrator" - }, "cli": { "displayNameKey": "product.cli.name", "binaryName": "acme" @@ -65,7 +58,7 @@ GUI/TUI 布局、插件/内置扩展选择、Installer/Store target、更新与 ``` 解析器先校验完整 family、双方 locale key、owned path 与技术 ID,再选择命令对应成员;digest-bearing `assembly` -只携带 schema/source digest、成员、display-name key、binary/bundle identity、交接所需 sibling binary names、 +只携带 schema/source digest、成员、display-name key、binary/bundle identity、 locale contract facts 与 assembly digest。 构建 adapter 所需的源路径、localized 名称、输出目录和 default-product 标记保留在外围 build context,不扩展成通用 manifest。相同输入必须产生相同摘要;非默认产品使用 digest-scoped Cargo target 目录,避免复用其他产品的编译期身份。 @@ -79,9 +72,13 @@ i18n locale 集合和 key parity。 - 只修改默认产品定义或资源引用时运行 `pnpm run product:check`;非默认定义运行 `pnpm run product:check -- --product-config `,确保校验实际改动的产品; -- 修改 schema、resolver 或 Desktop/Data Migrator/CLI build adapter 行为时,再运行 `pnpm run product:test`; +- 修改 schema、resolver 或 Desktop/CLI build adapter 行为时,再运行 `pnpm run product:test`; - 打包和平台矩阵只在变更触及对应交付路径时运行,不作为产品定义的默认本地预检。 +Data Migrator 已从产品 family 与 sibling binary 投影中移除,使用自己的版本、Tauri 身份、构建与发布工作流。 +它固定面向 OpenBitFun 数据格式;工具身份与目标数据身份分开。旧定制产品定义应移除 `members.dataMigrator`。 +详见 [迁移器 README](../../src/apps/data-migrator/README.zh-CN.md)。 + ## 1. 设计结论 产品定制只需要四类对象: diff --git a/docs/architecture/rust-build-dependency-boundaries.md b/docs/architecture/rust-build-dependency-boundaries.md index c0a7ddc46c..1873ee621a 100644 --- a/docs/architecture/rust-build-dependency-boundaries.md +++ b/docs/architecture/rust-build-dependency-boundaries.md @@ -246,3 +246,19 @@ cargo check -p --timings 当前硬边界由 `scripts/check-core-boundaries.mjs` 统一执行。不要为同一 Cargo 架构事实增加第二个 checker;新增规则先证明当前树满足、fixture 能捕获回归,并保持错误消息可直接定位到 owner manifest。 检查器必须保持工作树只读;读取独立 manifest 的声明事实时不得生成新的 lockfile、target artifact 或格式化改动。 + +## 独立数据迁移工具的依赖边界 + +Data Migrator 是独立发布的本地离线工具,不依赖 Core、Product Assembly、Desktop 或 Web UI。 +主应用不检测、启动或捆绑迁移器。两者在同一源码工作区复用稳定的数据格式与存储实现: + +- contracts/config-contracts:配置 DTO、默认值、版本校验及到共享模型 DTO 的纯转换;Core 原路径保留转发,ConfigProvider 仍在 Core。 +- services-core 的 workspace-persistence、coordination-store、session-event-format:工作区记录、注册表校验、SQLite 物理 schema 和会话日志格式。 +- services/legacy-migration-adapters:旧版读取、转换、引用修复;只调用共享存储 owner。 +- services/legacy-migration:快照、锁、暂存、备份、原子写入、日志恢复和无时效交接依赖的任务存储。 + +本次只移动数据/存储 owner,不移动 WorkspaceManager、会话生命周期、权限、事件或远程执行。 +WorkspaceInfo/WorkspaceIdentity 的运行操作由 Core 的 runtime extension traits 保留,稳定记录无需导入这些能力。 +原 Core 存储入口保留错误映射;可选 legacy-migration facade 保留旧导入路径,但不再由 product-full 启用。 +远程四种场景不提供迁移工具的执行入口;仅转换本机保存的连接记录,不连接远端。 +使用与发行契约以 [独立迁移器说明](../../src/apps/data-migrator/README.zh-CN.md) 为准。 diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 2008504d70..fb00602c13 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -200,6 +200,13 @@ Mobile Web 直接消费 `@openbitfun/theme-openbitfun`。`ThemeProvider` 与首 ### Desktop bootstrap 与 Native Mobile 预览 +Data Migrator 的独立静态界面直接消费设计系统公开的字体、间距、控件和语义颜色 Token。 +`generate-data-migrator-theme.mjs` 构建设计系统的 Token/主题包,并将公开 CSS 入口及其依赖打包为 +`src/apps/data-migrator/ui/generated/design-system.css`,随迁移程序离线交付。原生控件只使用 canonical +Token;`theme.js` 在首屏绘制前选择系统浅色/深色和高对比模式,并监听系统设置变化。 +仅独立迁移器打包刷新该资源,开发者也可显式运行生成命令;Desktop 开发和打包不依赖迁移器。 +统一颜色审计同时检查源码和生成物漂移。 + Desktop 的更新确认页和启动页只消费 `src/apps/desktop/src/generated/bootstrap_theme.css` 发布的 canonical `--openbitfun-*`,不得内联另一套启动色。该 CSS 和两个 Appearance manifest 一起由 `generate-startup-appearance-bootstrap.mjs` 从正式主题/Appearance 源生成;统一颜色审计执行 `--check`, diff --git a/docs/interactive-capabilities/README.md b/docs/interactive-capabilities/README.md index 3bcdd2c880..09eac55a30 100644 --- a/docs/interactive-capabilities/README.md +++ b/docs/interactive-capabilities/README.md @@ -1,9 +1,9 @@ # OpenBitFun 功能与设置目录 / OpenBitFun Features & Settings -OpenBitFun Playbook 当前包含 **22 个功能**和 **22 个设置页**,共 **44 个**用户可理解的条目、**326 项**有源码证据的子能力。每个条目有独立 Markdown,并直接服务于说明书网站、OpenBitFun 全局搜索和 `OpenBitFunControl` Agent 工具。 +OpenBitFun Playbook 当前包含 **22 个功能**和 **21 个设置页**,共 **43 个**用户可理解的条目、**321 项**有源码证据的子能力。每个条目有独立 Markdown,并直接服务于说明书网站、OpenBitFun 全局搜索和 `OpenBitFunControl` Agent 工具。 -OpenBitFun Playbook currently contains **22 features**, **22 settings pages**, and **326** source-backed sub-capabilities across **44** user-facing entries. Every entry has its own Markdown page and directly powers the website, in-app global search, and the `OpenBitFunControl` agent tool. +OpenBitFun Playbook currently contains **22 features**, **21 settings pages**, and **321** source-backed sub-capabilities across **43** user-facing entries. Every entry has its own Markdown page and directly powers the website, in-app global search, and the `OpenBitFunControl` agent tool. ## 唯一事实源 / Single source of truth @@ -27,20 +27,20 @@ OpenBitFun Playbook currently contains **22 features**, **22 settings pages**, a - Generated per-item interaction audit: `docs/interactive-capabilities/technical/product-control-open-audit.json` - Generated low-level audit map: `docs/interactive-capabilities/technical/tauri-command-map.json` -说明书、网站、搜索和 Agent 只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **671** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。 +说明书、网站、搜索和 Agent 只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **666** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。 -Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **671** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes. +Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **666** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes. ## 控制边界 / Control boundary -- 每个子能力都明确标记为直接控制、委托给专用 Agent 工具、需交互打开或不支持;“打开页面”不会再被统计成“Agent 已控制”。当前覆盖:直接 **48**、委托 **61**、需交互 **217**、不支持 **0**。 +- 每个子能力都明确标记为直接控制、委托给专用 Agent 工具、需交互打开或不支持;“打开页面”不会再被统计成“Agent 已控制”。当前覆盖:直接 **48**、委托 **61**、需交互 **212**、不支持 **0**。 - 稳定行为声明为带 JSON 输入契约的 `operations` 或 `options`,并绑定原生产品控制 Provider;Agent 不接触原始 Tauri Command。 -- `OpenBitFunControl list` 和 `search` 都返回带 `nextCursor` 的精简分页结果;目录可持续增长,不靠固定总量上限。完整目录和 326 项子能力都不会写入 system prompt。 +- `OpenBitFunControl list` 和 `search` 都返回带 `nextCursor` 的精简分页结果;目录可持续增长,不靠固定总量上限。完整目录和 321 项子能力都不会写入 system prompt。 - 目录发现与契约读取不依赖 React 或可见窗口。普通配置型 option 统一由 Product Assembly 的共享 ConfigService 执行器读、写并回读,因此 Desktop、CLI 与 Headless 表面走同一份实现;只有宿主原生 operation/provider option 和界面导航按表面注册适配器,缺失时必须明确返回不可用,禁止静默回退本机。只读 Agent 只能发现和读取目录。 -- Every documented item is classified as direct control, delegated Agent control, interactive opening, or unsupported; opening a page is never counted as direct control. Current coverage is **48 direct**, **61 delegated**, **217 interactive**, and **0 unsupported**. +- Every documented item is classified as direct control, delegated Agent control, interactive opening, or unsupported; opening a page is never counted as direct control. Current coverage is **48 direct**, **61 delegated**, **212 interactive**, and **0 unsupported**. - Stable behavior becomes a typed `operation` or `option` with a JSON input contract and a native product-control provider. Agents never receive raw Tauri commands. -- `OpenBitFunControl list` and `search` return compact pages with a `nextCursor`; the catalog can grow without a fixed total-size ceiling. Neither the full catalog nor its 326 documented items enters the system prompt. +- `OpenBitFunControl list` and `search` return compact pages with a `nextCursor`; the catalog can grow without a fixed total-size ceiling. Neither the full catalog nor its 321 documented items enters the system prompt. - Discovery and contract lookup do not depend on React or a visible window. Ordinary config-backed options are read, written, and read back by one Product Assembly ConfigService executor shared by Desktop, CLI, and headless surfaces. Only host-native operations/provider options and presentation routes install surface adapters; missing adapters return explicit unavailability without local fallback. Read-only agents may only discover and inspect entries. ## 防腐化门禁 / Anti-drift gates diff --git a/docs/interactive-capabilities/capabilities.json b/docs/interactive-capabilities/capabilities.json index 653efc45a6..a8c4149de3 100644 --- a/docs/interactive-capabilities/capabilities.json +++ b/docs/interactive-capabilities/capabilities.json @@ -4,7 +4,7 @@ "title": "OpenBitFun Playbook", "origin": "https://playbook.openbitfun.com", "source": "src/shared/interactive-capabilities/catalog.json", - "digest": "c77c16c414ce9fe50929fe249d900489fdceedb8e646aaaca03893aa33d4fbc8", + "digest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", "ownerDigest": "c0e5c187cf62bc6ed06196ce8520b3eb427bf268cf24659b72d2552fb1d99c54", "searchAcceptance": [ { @@ -136,13 +136,13 @@ ], "counts": { "features": 22, - "settings": 22, - "userFacing": 44, - "documentedItems": 326, + "settings": 21, + "userFacing": 43, + "documentedItems": 321, "controlCoverage": { "direct": 48, "delegated": 61, - "interactive": 217, + "interactive": 212, "unsupported": 0 } }, @@ -18765,285 +18765,6 @@ "pageId": "data.archived" } }, - { - "id": "setting.data.migration:query", - "capabilityId": "setting.data.migration", - "itemIds": [ - "scan", - "scope", - "launch", - "report", - "reminder" - ], - "kind": "query", - "risk": "read", - "executionHost": "productHost", - "availability": { - "desktop": { - "available": true - }, - "cli": { - "available": true - }, - "peer": { - "available": true, - "requiredCapabilities": [ - "product_control_v1" - ] - }, - "remoteControl": { - "available": true - }, - "detachedDispatch": { - "available": true - } - }, - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": { - "type": "object", - "additionalProperties": true - }, - "valueSource": { - "kind": "static" - }, - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - } - }, - { - "id": "setting.data.migration:open:scan", - "capabilityId": "setting.data.migration", - "itemIds": [ - "scan" - ], - "kind": "open", - "risk": "ui", - "executionHost": "presentationSurface", - "availability": { - "desktop": { - "available": true - }, - "cli": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - }, - "peer": { - "available": true, - "requiredCapabilities": [ - "product_control_v1", - "product_control_presentation_v1" - ] - }, - "remoteControl": { - "available": true - }, - "detachedDispatch": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - } - }, - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": { - "type": "object", - "additionalProperties": true - }, - "openReason": "unstructuredInteraction", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - } - }, - { - "id": "setting.data.migration:open:scope", - "capabilityId": "setting.data.migration", - "itemIds": [ - "scope" - ], - "kind": "open", - "risk": "ui", - "executionHost": "presentationSurface", - "availability": { - "desktop": { - "available": true - }, - "cli": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - }, - "peer": { - "available": true, - "requiredCapabilities": [ - "product_control_v1", - "product_control_presentation_v1" - ] - }, - "remoteControl": { - "available": true - }, - "detachedDispatch": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - } - }, - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": { - "type": "object", - "additionalProperties": true - }, - "openReason": "visualSelection", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - } - }, - { - "id": "setting.data.migration:open:launch", - "capabilityId": "setting.data.migration", - "itemIds": [ - "launch" - ], - "kind": "open", - "risk": "ui", - "executionHost": "presentationSurface", - "availability": { - "desktop": { - "available": true - }, - "cli": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - }, - "peer": { - "available": true, - "requiredCapabilities": [ - "product_control_v1", - "product_control_presentation_v1" - ] - }, - "remoteControl": { - "available": true - }, - "detachedDispatch": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - } - }, - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": { - "type": "object", - "additionalProperties": true - }, - "openReason": "unstructuredInteraction", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - } - }, - { - "id": "setting.data.migration:open:report", - "capabilityId": "setting.data.migration", - "itemIds": [ - "report" - ], - "kind": "open", - "risk": "ui", - "executionHost": "presentationSurface", - "availability": { - "desktop": { - "available": true - }, - "cli": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - }, - "peer": { - "available": true, - "requiredCapabilities": [ - "product_control_v1", - "product_control_presentation_v1" - ] - }, - "remoteControl": { - "available": true - }, - "detachedDispatch": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - } - }, - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": { - "type": "object", - "additionalProperties": true - }, - "openReason": "unstructuredInteraction", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - } - }, - { - "id": "setting.data.migration:open:reminder", - "capabilityId": "setting.data.migration", - "itemIds": [ - "reminder" - ], - "kind": "open", - "risk": "ui", - "executionHost": "presentationSurface", - "availability": { - "desktop": { - "available": true - }, - "cli": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - }, - "peer": { - "available": true, - "requiredCapabilities": [ - "product_control_v1", - "product_control_presentation_v1" - ] - }, - "remoteControl": { - "available": true - }, - "detachedDispatch": { - "available": false, - "reason": "This delivery profile has no live presentation surface" - } - }, - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": { - "type": "object", - "additionalProperties": true - }, - "openReason": "unstructuredInteraction", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - } - }, { "id": "setting.data.diagnostics:query", "capabilityId": "setting.data.diagnostics", @@ -29844,156 +29565,6 @@ ], "docsUrl": "https://playbook.openbitfun.com/capabilities/setting.data.archived/" }, - { - "id": "setting.data.migration", - "kind": "setting", - "categoryId": "data", - "titleZh": "旧版数据迁移", - "titleEn": "Legacy data migration", - "summaryZh": "从本机旧版 安装扫描并导入受支持的数据,查看去敏报告,同时保持旧来源不变。", - "summaryEn": "Scan and import supported data from a local legacy installation, inspect redacted reports, and leave the legacy source unchanged.", - "keywordsZh": [ - "旧版数据迁移", - "旧版数据", - "迁移报告", - "导入旧数据", - "Data Migrator" - ], - "keywordsEn": [ - "legacy data migration", - "legacy data", - "migration report", - "import old data", - "Data Migrator" - ], - "highlightsZh": [ - "只读扫描本机旧版数据", - "按五个高层数据组选择迁移范围", - "通过独立 Data Migrator 导入并查看去敏报告" - ], - "highlightsEn": [ - "Read-only scan of local legacy data", - "Choose migration scope across five high-level data groups", - "Import through the standalone Data Migrator and inspect redacted reports" - ], - "items": [ - { - "id": "scan", - "titleZh": "只读扫描本机旧版数据来源及所选数据组", - "titleEn": "Read-only scan the local legacy source and selected data groups", - "control": { - "kind": "open", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“只读扫描本机旧版数据来源及所选数据组”:扫描依赖当前设备上的旧版数据、实时范围选择和结果状态;Agent 会打开精确入口,并把选择与扫描保留在用户可见界面。", - "reasonEn": "Read-only scan the local legacy source and selected data groups: Scanning depends on legacy data on the current device, live scope selection, and result state; the Agent opens the exact entry and keeps selection and scanning visible to the user." - } - }, - { - "id": "scope", - "titleZh": "选择设置、扩展、会话、记忆和远程连接迁移范围", - "titleEn": "Choose settings, extensions, sessions, memory, and remote-connection migration scope", - "control": { - "kind": "open", - "reasonCode": "visualSelection", - "reasonZh": "迁移范围是影响本机持久数据的五组可见选择;Agent 会打开精确入口,由用户确认所需范围。", - "reasonEn": "Migration scope is a visible five-group selection affecting local persisted data; the Agent opens the exact entry so the user can confirm the intended scope." - } - }, - { - "id": "launch", - "titleZh": "确认关闭影响后启动独立 Data Migrator", - "titleEn": "Launch the standalone Data Migrator after confirming shutdown impact", - "control": { - "kind": "open", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“确认关闭影响后启动独立 Data Migrator”:启动迁移器会停止正在运行的 Agent 和终端任务、关闭 Desktop,并交接到独立本机进程;Agent 只打开入口,确认和启动保留在用户可见界面。", - "reasonEn": "Launch the standalone Data Migrator after confirming shutdown impact: Launching the migrator can stop running agents and terminal tasks, close Desktop, and hand off to a separate local process; the Agent only opens the entry while confirmation and launch remain visible to the user." - } - }, - { - "id": "report", - "titleZh": "查看最近运行结果和各领域去敏状态", - "titleEn": "Inspect the latest run result and redacted per-domain status", - "control": { - "kind": "open", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“查看最近运行结果和各领域去敏状态”:报告取决于本机最近一次迁移运行和各领域实时状态;Agent 会打开精确入口,并把报告查看与失败组重试保留在用户可见界面。", - "reasonEn": "Inspect the latest run result and redacted per-domain status: Reports depend on the most recent local migration run and live per-domain state; the Agent opens the exact entry and keeps report review and failed-group retry visible to the user." - } - }, - { - "id": "reminder", - "titleZh": "恢复已关闭的首次启动迁移提醒", - "titleEn": "Restore the first-start migration reminder after it was disabled", - "control": { - "kind": "open", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“恢复已关闭的首次启动迁移提醒”:提醒偏好与当前本机旧数据来源绑定;Agent 会打开精确入口,由用户在可见界面决定是否恢复提醒。", - "reasonEn": "Restore the first-start migration reminder after it was disabled: The reminder preference is bound to the current local legacy source; the Agent opens the exact entry so the user can decide visibly whether to restore it." - } - } - ], - "stepsZh": [ - "打开设置", - "进入“数据 > 旧版数据迁移”", - "扫描来源、选择范围并在确认关闭影响后启动迁移器" - ], - "stepsEn": [ - "Open Settings", - "Go to Data > Legacy data migration", - "Scan the source, choose scope, and launch the migrator after confirming shutdown impact" - ], - "agentExamplesZh": [ - "打开旧版数据迁移", - "带我查看数据迁移报告" - ], - "agentExamplesEn": [ - "Open legacy data migration", - "Show me the data migration report" - ], - "destination": { - "kind": "settings", - "pageId": "data.migration" - }, - "operations": [], - "options": [], - "searchTerms": [ - "setting.data.migration", - "旧版数据迁移", - "Legacy data migration", - "数据与诊断", - "Data & diagnostics", - "旧版数据", - "迁移报告", - "导入旧数据", - "Data Migrator", - "legacy data migration", - "legacy data", - "migration report", - "import old data", - "只读扫描本机旧版数据", - "按五个高层数据组选择迁移范围", - "通过独立 Data Migrator 导入并查看去敏报告", - "Read-only scan of local legacy data", - "Choose migration scope across five high-level data groups", - "Import through the standalone Data Migrator and inspect redacted reports", - "只读扫描本机旧版数据来源及所选数据组", - "Read-only scan the local legacy source and selected data groups", - "选择设置、扩展、会话、记忆和远程连接迁移范围", - "Choose settings, extensions, sessions, memory, and remote-connection migration scope", - "确认关闭影响后启动独立 Data Migrator", - "Launch the standalone Data Migrator after confirming shutdown impact", - "查看最近运行结果和各领域去敏状态", - "Inspect the latest run result and redacted per-domain status", - "恢复已关闭的首次启动迁移提醒", - "Restore the first-start migration reminder after it was disabled", - "打开旧版数据迁移", - "带我查看数据迁移报告", - "Open legacy data migration", - "Show me the data migration report" - ], - "docsUrl": "https://playbook.openbitfun.com/capabilities/setting.data.migration/" - }, { "id": "setting.data.diagnostics", "kind": "setting", diff --git a/docs/interactive-capabilities/capabilities/setting.data.migration.md b/docs/interactive-capabilities/capabilities/setting.data.migration.md deleted file mode 100644 index a8039fde4c..0000000000 --- a/docs/interactive-capabilities/capabilities/setting.data.migration.md +++ /dev/null @@ -1,63 +0,0 @@ - ---- -id: setting.data.migration -kind: setting -category: data -title_zh: "旧版数据迁移" -title_en: "Legacy data migration" ---- - -# 旧版数据迁移 / Legacy data migration - -> 设置 / Setting - -从本机旧版 安装扫描并导入受支持的数据,查看去敏报告,同时保持旧来源不变。 - -Scan and import supported data from a local legacy installation, inspect redacted reports, and leave the legacy source unchanged. - -## 完整功能清单 / Everything included - -- **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 只读扫描本机旧版数据来源及所选数据组 - - Read-only scan the local legacy source and selected data groups -- **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 选择设置、扩展、会话、记忆和远程连接迁移范围 - - Choose settings, extensions, sessions, memory, and remote-connection migration scope -- **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 确认关闭影响后启动独立 Data Migrator - - Launch the standalone Data Migrator after confirming shutdown impact -- **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 查看最近运行结果和各领域去敏状态 - - Inspect the latest run result and redacted per-domain status -- **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 恢复已关闭的首次启动迁移提醒 - - Restore the first-start migration reminder after it was disabled - -## 怎么用 / How to use it - -1. 打开设置 - Open Settings -2. 进入“数据 > 旧版数据迁移” - Go to Data > Legacy data migration -3. 扫描来源、选择范围并在确认关闭影响后启动迁移器 - Scan the source, choose scope, and launch the migrator after confirming shutdown impact - -入口 / Entry: OpenBitFun 设置 - -## Agent 可替你做什么 / What an agent can do for you - -| 操作 / Action | 中文说明 | English description | -| --- | --- | --- | -| 打开对应界面 / Open the UI | 进入 OpenBitFun 中对应的功能界面。 | Open the matching feature in OpenBitFun. | - -## 可配置选项 / Configurable options - -| 选项 / Option | 可用值 / Values | 中文说明 | English description | -| --- | --- | --- | --- | -| 在界面中配置 / Configure in the UI | — | 此页面的设置在对应界面中完成。 | Configure this page in its matching UI. | - -## 可以直接对 Agent 说 / Try saying - -- “打开旧版数据迁移” - - “Open legacy data migration” -- “带我查看数据迁移报告” - - “Show me the data migration report” - -Agent 会先查找相关功能或设置,确认目标后再替你打开、执行或修改。完整能力目录不会预先塞进对话上下文。 - -The agent first finds the relevant feature or setting, confirms the target, and then opens, runs, or changes it for you. The full catalog is never embedded in the conversation context. diff --git a/docs/interactive-capabilities/technical/product-control-open-audit.json b/docs/interactive-capabilities/technical/product-control-open-audit.json index 1839909c20..b2f434a198 100644 --- a/docs/interactive-capabilities/technical/product-control-open-audit.json +++ b/docs/interactive-capabilities/technical/product-control-open-audit.json @@ -1,13 +1,13 @@ { "schemaVersion": 1, "generatedFrom": "src/shared/interactive-capabilities/catalog.json", - "catalogDigest": "c77c16c414ce9fe50929fe249d900489fdceedb8e646aaaca03893aa33d4fbc8", - "count": 217, + "catalogDigest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", + "count": 212, "reasonCounts": { "externalAuth": 4, "secretEntry": 5, - "unstructuredInteraction": 189, - "visualSelection": 19 + "unstructuredInteraction": 185, + "visualSelection": 18 }, "entries": [ { @@ -3749,91 +3749,6 @@ "command:delete_all_archived_sessions" ] }, - { - "capabilityId": "setting.data.migration", - "itemId": "scan", - "titleZh": "只读扫描本机旧版数据来源及所选数据组", - "titleEn": "Read-only scan the local legacy source and selected data groups", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“只读扫描本机旧版数据来源及所选数据组”:扫描依赖当前设备上的旧版数据、实时范围选择和结果状态;Agent 会打开精确入口,并把选择与扫描保留在用户可见界面。", - "reasonEn": "Read-only scan the local legacy source and selected data groups: Scanning depends on legacy data on the current device, live scope selection, and result state; the Agent opens the exact entry and keeps selection and scanning visible to the user.", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - }, - "evidence": [ - "command:get_legacy_migration_status", - "command:scan_legacy_migration", - "source:src/web-ui/src/locales/zh-CN/settings/legacy-migration.json#actions.scan" - ] - }, - { - "capabilityId": "setting.data.migration", - "itemId": "scope", - "titleZh": "选择设置、扩展、会话、记忆和远程连接迁移范围", - "titleEn": "Choose settings, extensions, sessions, memory, and remote-connection migration scope", - "reasonCode": "visualSelection", - "reasonZh": "迁移范围是影响本机持久数据的五组可见选择;Agent 会打开精确入口,由用户确认所需范围。", - "reasonEn": "Migration scope is a visible five-group selection affecting local persisted data; the Agent opens the exact entry so the user can confirm the intended scope.", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - }, - "evidence": [ - "source:src/web-ui/src/locales/zh-CN/settings/legacy-migration.json#sections.scope.title" - ] - }, - { - "capabilityId": "setting.data.migration", - "itemId": "launch", - "titleZh": "确认关闭影响后启动独立 Data Migrator", - "titleEn": "Launch the standalone Data Migrator after confirming shutdown impact", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“确认关闭影响后启动独立 Data Migrator”:启动迁移器会停止正在运行的 Agent 和终端任务、关闭 Desktop,并交接到独立本机进程;Agent 只打开入口,确认和启动保留在用户可见界面。", - "reasonEn": "Launch the standalone Data Migrator after confirming shutdown impact: Launching the migrator can stop running agents and terminal tasks, close Desktop, and hand off to a separate local process; the Agent only opens the entry while confirmation and launch remain visible to the user.", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - }, - "evidence": [ - "command:prepare_legacy_migration", - "source:src/web-ui/src/locales/zh-CN/settings/legacy-migration.json#confirm.title" - ] - }, - { - "capabilityId": "setting.data.migration", - "itemId": "report", - "titleZh": "查看最近运行结果和各领域去敏状态", - "titleEn": "Inspect the latest run result and redacted per-domain status", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“查看最近运行结果和各领域去敏状态”:报告取决于本机最近一次迁移运行和各领域实时状态;Agent 会打开精确入口,并把报告查看与失败组重试保留在用户可见界面。", - "reasonEn": "Inspect the latest run result and redacted per-domain status: Reports depend on the most recent local migration run and live per-domain state; the Agent opens the exact entry and keeps report review and failed-group retry visible to the user.", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - }, - "evidence": [ - "command:get_legacy_migration_report", - "source:src/web-ui/src/locales/zh-CN/settings/legacy-migration.json#sections.report.title" - ] - }, - { - "capabilityId": "setting.data.migration", - "itemId": "reminder", - "titleZh": "恢复已关闭的首次启动迁移提醒", - "titleEn": "Restore the first-start migration reminder after it was disabled", - "reasonCode": "unstructuredInteraction", - "reasonZh": "“恢复已关闭的首次启动迁移提醒”:提醒偏好与当前本机旧数据来源绑定;Agent 会打开精确入口,由用户在可见界面决定是否恢复提醒。", - "reasonEn": "Restore the first-start migration reminder after it was disabled: The reminder preference is bound to the current local legacy source; the Agent opens the exact entry so the user can decide visibly whether to restore it.", - "presentationTarget": { - "kind": "settings", - "pageId": "data.migration" - }, - "evidence": [ - "command:set_legacy_migration_prompt_preference", - "source:src/web-ui/src/locales/zh-CN/settings/legacy-migration.json#actions.restoreReminder" - ] - }, { "capabilityId": "setting.data.diagnostics", "itemId": "log-path", diff --git a/docs/interactive-capabilities/technical/tauri-command-map.json b/docs/interactive-capabilities/technical/tauri-command-map.json index c33322462f..04b6121d30 100644 --- a/docs/interactive-capabilities/technical/tauri-command-map.json +++ b/docs/interactive-capabilities/technical/tauri-command-map.json @@ -1,11 +1,11 @@ { "schemaVersion": 2, "generatedFrom": "src/shared/interactive-capabilities/catalog.json", - "catalogDigest": "c77c16c414ce9fe50929fe249d900489fdceedb8e646aaaca03893aa33d4fbc8", - "commandCount": 671, + "catalogDigest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", + "commandCount": 666, "coverage": { - "commandCount": 671, - "documentedCommandCount": 638, + "commandCount": 666, + "documentedCommandCount": 633, "implementationCommandCount": 33, "implementationDigest": "35539d9c1510287cb47f4a68fe35859b78f93bb06cd66b86e58d878a48d8c509" }, @@ -3230,38 +3230,6 @@ "signature": "fn get_latest_insights() -> Result, String>", "remoteWorkspacePolicy": "LocalOnly" }, - { - "id": "get_legacy_migration_report", - "moduleId": "legacy_migration", - "capabilityId": "setting.data.migration", - "capabilityIds": [ - "setting.data.migration" - ], - "documentedItemIds": [ - "setting.data.migration:report" - ], - "visibility": "documented", - "rustPath": "api::legacy_migration_api::get_legacy_migration_report", - "sourceFile": "src/apps/desktop/src/api/legacy_migration_api.rs", - "signature": "fn get_legacy_migration_report( request: GetLegacyMigrationReportRequest, ) -> Result, LegacyMigrationCommandError>", - "remoteWorkspacePolicy": "LocalOnly" - }, - { - "id": "get_legacy_migration_status", - "moduleId": "legacy_migration", - "capabilityId": "setting.data.migration", - "capabilityIds": [ - "setting.data.migration" - ], - "documentedItemIds": [ - "setting.data.migration:scan" - ], - "visibility": "documented", - "rustPath": "api::legacy_migration_api::get_legacy_migration_status", - "sourceFile": "src/apps/desktop/src/api/legacy_migration_api.rs", - "signature": "fn get_legacy_migration_status( request: EmptyLegacyMigrationRequest, ) -> Result", - "remoteWorkspacePolicy": "LocalOnly" - }, { "id": "get_mcp_prompt", "moduleId": "mcp", @@ -6646,22 +6614,6 @@ "signature": "fn predownload_acp_client_adapter( state: State<'_, AppState>, request: AcpClientIdRequest, ) -> Result<(), String>", "remoteWorkspacePolicy": "LegacyUnaudited" }, - { - "id": "prepare_legacy_migration", - "moduleId": "legacy_migration", - "capabilityId": "setting.data.migration", - "capabilityIds": [ - "setting.data.migration" - ], - "documentedItemIds": [ - "setting.data.migration:launch" - ], - "visibility": "documented", - "rustPath": "api::legacy_migration_api::prepare_legacy_migration", - "sourceFile": "src/apps/desktop/src/api/legacy_migration_api.rs", - "signature": "fn prepare_legacy_migration( app: AppHandle, request: PrepareLegacyMigrationRequest, ) -> Result", - "remoteWorkspacePolicy": "LocalOnly" - }, { "id": "preview_commit_message", "moduleId": "git_agent", @@ -8417,22 +8369,6 @@ "signature": "fn save_web_search_credential( _state: State<'_, AppState>, request: openbitfun_core::service::web_search::SaveWebSearchCredentialRequest, ) -> Result", "remoteWorkspacePolicy": "WorkspaceAgnostic" }, - { - "id": "scan_legacy_migration", - "moduleId": "legacy_migration", - "capabilityId": "setting.data.migration", - "capabilityIds": [ - "setting.data.migration" - ], - "documentedItemIds": [ - "setting.data.migration:scan" - ], - "visibility": "documented", - "rustPath": "api::legacy_migration_api::scan_legacy_migration", - "sourceFile": "src/apps/desktop/src/api/legacy_migration_api.rs", - "signature": "fn scan_legacy_migration( request: ScanLegacyMigrationRequest, ) -> Result", - "remoteWorkspacePolicy": "LocalOnly" - }, { "id": "scan_workspace_info", "moduleId": "commands", @@ -8901,22 +8837,6 @@ "signature": "fn set_global_skill_disabled( request: SetGlobalSkillDisabledRequest, ) -> Result", "remoteWorkspacePolicy": "WorkspaceAgnostic" }, - { - "id": "set_legacy_migration_prompt_preference", - "moduleId": "legacy_migration", - "capabilityId": "setting.data.migration", - "capabilityIds": [ - "setting.data.migration" - ], - "documentedItemIds": [ - "setting.data.migration:reminder" - ], - "visibility": "documented", - "rustPath": "api::legacy_migration_api::set_legacy_migration_prompt_preference", - "sourceFile": "src/apps/desktop/src/api/legacy_migration_api.rs", - "signature": "fn set_legacy_migration_prompt_preference( request: SetLegacyMigrationPromptPreferenceRequest, ) -> Result", - "remoteWorkspacePolicy": "LocalOnly" - }, { "id": "set_macos_edit_menu_mode", "moduleId": "system", diff --git a/package.json b/package.json index 2748bf7658..111c68a395 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,10 @@ "product:check": "node scripts/product-customization/cli.mjs check", "product:explain": "node scripts/product-customization/cli.mjs explain", "product:test": "node --test --test-concurrency=1 scripts/product-customization/*.test.mjs scripts/cli-product.test.mjs scripts/desktop-tauri-build.test.mjs scripts/data-migrator-tauri-build.test.mjs", + "data-migrator:dev": "cargo run -p openbitfun-data-migrator --bin openbitfun-data-migrator", "data-migrator:check": "cargo check -p openbitfun-data-migrator", + "data-migrator:theme:generate": "node scripts/generate-data-migrator-theme.mjs", + "data-migrator:theme:check": "node scripts/generate-data-migrator-theme.mjs --check", "data-migrator:build": "node scripts/data-migrator-tauri-build.mjs", "desktop:build": "node scripts/desktop-tauri-build.mjs", "desktop:build:fast": "node scripts/desktop-tauri-build.mjs --debug --no-bundle", diff --git a/products/fixtures/acme/locales/en-US.json b/products/fixtures/acme/locales/en-US.json index fa286b519c..a8d066b01c 100644 --- a/products/fixtures/acme/locales/en-US.json +++ b/products/fixtures/acme/locales/en-US.json @@ -1,5 +1,4 @@ { "product.cli.name": "Acme CLI", - "product.dataMigrator.name": "Acme Data Migrator", "product.desktop.name": "Acme Desktop" } diff --git a/products/fixtures/acme/locales/zh-CN.json b/products/fixtures/acme/locales/zh-CN.json index ed492ddd53..140f3111ef 100644 --- a/products/fixtures/acme/locales/zh-CN.json +++ b/products/fixtures/acme/locales/zh-CN.json @@ -1,5 +1,4 @@ { "product.cli.name": "Acme 命令行", - "product.dataMigrator.name": "Acme 数据迁移器", "product.desktop.name": "Acme 桌面版" } diff --git a/products/fixtures/acme/locales/zh-TW.json b/products/fixtures/acme/locales/zh-TW.json index a9e7eb4ac9..770e9abd60 100644 --- a/products/fixtures/acme/locales/zh-TW.json +++ b/products/fixtures/acme/locales/zh-TW.json @@ -1,5 +1,4 @@ { "product.cli.name": "Acme 命令列", - "product.dataMigrator.name": "Acme 資料遷移器", "product.desktop.name": "Acme 桌面版" } diff --git a/products/fixtures/acme/product.jsonc b/products/fixtures/acme/product.jsonc index 6b2ab8af27..2af9a0dfa4 100644 --- a/products/fixtures/acme/product.jsonc +++ b/products/fixtures/acme/product.jsonc @@ -10,11 +10,6 @@ "binaryName": "acme-desktop", "bundleId": "com.acme.desktop" }, - "dataMigrator": { - "displayNameKey": "product.dataMigrator.name", - "binaryName": "acme-data-migrator", - "bundleId": "com.acme.data-migrator" - }, "cli": { "displayNameKey": "product.cli.name", "binaryName": "acme" diff --git a/products/openbitfun/locales/en-US.json b/products/openbitfun/locales/en-US.json index e2ef5aa960..92463c4799 100644 --- a/products/openbitfun/locales/en-US.json +++ b/products/openbitfun/locales/en-US.json @@ -1,5 +1,4 @@ { "product.cli.name": "OpenBitFun CLI", - "product.dataMigrator.name": "OpenBitFun Data Migrator", "product.desktop.name": "OpenBitFun" } diff --git a/products/openbitfun/locales/zh-CN.json b/products/openbitfun/locales/zh-CN.json index 223b6b308e..92463c4799 100644 --- a/products/openbitfun/locales/zh-CN.json +++ b/products/openbitfun/locales/zh-CN.json @@ -1,5 +1,4 @@ { "product.cli.name": "OpenBitFun CLI", - "product.dataMigrator.name": "OpenBitFun 数据迁移器", "product.desktop.name": "OpenBitFun" } diff --git a/products/openbitfun/locales/zh-TW.json b/products/openbitfun/locales/zh-TW.json index 61b90a2bfb..92463c4799 100644 --- a/products/openbitfun/locales/zh-TW.json +++ b/products/openbitfun/locales/zh-TW.json @@ -1,5 +1,4 @@ { "product.cli.name": "OpenBitFun CLI", - "product.dataMigrator.name": "OpenBitFun 資料遷移器", "product.desktop.name": "OpenBitFun" } diff --git a/products/openbitfun/product.jsonc b/products/openbitfun/product.jsonc index 29c10f9d8a..b6f199e067 100644 --- a/products/openbitfun/product.jsonc +++ b/products/openbitfun/product.jsonc @@ -10,11 +10,6 @@ "binaryName": "openbitfun-desktop", "bundleId": "com.openbitfun.desktop" }, - "dataMigrator": { - "displayNameKey": "product.dataMigrator.name", - "binaryName": "openbitfun-data-migrator", - "bundleId": "com.openbitfun.data-migrator" - }, "cli": { "displayNameKey": "product.cli.name", "binaryName": "openbitfun" diff --git a/products/schemas/product-definition.schema.json b/products/schemas/product-definition.schema.json index 8ec650671e..a5e530f87e 100644 --- a/products/schemas/product-definition.schema.json +++ b/products/schemas/product-definition.schema.json @@ -14,10 +14,9 @@ "members": { "type": "object", "additionalProperties": false, - "required": ["desktop", "dataMigrator", "cli"], + "required": ["desktop", "cli"], "properties": { "desktop": { "$ref": "#/$defs/desktopMember" }, - "dataMigrator": { "$ref": "#/$defs/desktopMember" }, "cli": { "$ref": "#/$defs/commonMember" } } } diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index bdb2450a10..7057a221e8 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -198,7 +198,8 @@ test('Agent Runtime leaf capabilities have one managed feature and source contra 'native-hook-runtime', 'native-hook-settings', ]); - assert.equal(rule.consumers.size, 10); + assert.equal(rule.consumers.size, 11); + assert.ok(rule.consumers.has('openbitfun-legacy-migration-adapters')); assert.ok( guardedEmptyInternalDefaultManifestPaths.includes( 'src/crates/execution/agent-runtime/Cargo.toml', @@ -4156,7 +4157,7 @@ test('Core Tokio capabilities cannot hide behind an unreviewed owner feature', ( ], features: { 'agent-runtime': ['tokio/io-util', 'tokio/macros', 'tokio/rt', 'tokio/time'], - 'legacy-migration': ['tokio/rt'], + 'legacy-migration': [], 'mcp-runtime': ['agent-runtime', 'tokio/rt-multi-thread'], 'browser-control': ['tokio/net', 'tokio/rt', 'tokio/time'], sneaky: ['agent-runtime', 'browser-control'], @@ -4178,7 +4179,7 @@ test('reviewed Tokio aggregates cannot declare runtime capabilities directly', ( dependencies: [{ name: 'tokio', kind: null, optional: false, features: ['fs', 'sync'] }], features: { 'agent-runtime': ['tokio/io-util', 'tokio/macros', 'tokio/rt', 'tokio/time'], - 'legacy-migration': ['tokio/rt'], + 'legacy-migration': [], 'mcp-runtime': ['agent-runtime', 'tokio/rt-multi-thread'], 'browser-control': ['tokio/net', 'tokio/rt', 'tokio/time'], 'product-full': ['agent-runtime', 'tokio/net'], diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index 57e9fdebb0..dd3ca537d7 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -174,7 +174,7 @@ const SERVICES_INTEGRATIONS_TOKIO_AGGREGATES = new Set(['product-full']); const SERVICES_CORE_TOKIO_AGGREGATES = new Set(['session-git', 'token-usage-statistics']); const CORE_TOKIO_FEATURES = new Map([ ['agent-runtime', ['io-util', 'macros', 'rt', 'time']], - ['legacy-migration', ['rt']], + ['legacy-migration', []], ['mcp-runtime', ['io-util', 'macros', 'rt', 'rt-multi-thread', 'time']], ['browser-control', ['net', 'rt', 'time']], ]); diff --git a/scripts/core-boundaries/rules/crate-layout.mjs b/scripts/core-boundaries/rules/crate-layout.mjs index 1f550325e3..5642062585 100644 --- a/scripts/core-boundaries/rules/crate-layout.mjs +++ b/scripts/core-boundaries/rules/crate-layout.mjs @@ -2,6 +2,7 @@ // owns where workspace crates live under src/crates. export const crateLayoutRules = [ + { crateName: 'config-contracts', layer: 'contracts', path: 'src/crates/contracts/config-contracts' }, { crateName: 'core-types', layer: 'contracts', path: 'src/crates/contracts/core-types' }, { crateName: 'events', layer: 'contracts', path: 'src/crates/contracts/events' }, { crateName: 'product-domains', layer: 'contracts', path: 'src/crates/contracts/product-domains' }, @@ -23,6 +24,7 @@ export const crateLayoutRules = [ { crateName: 'services-core', layer: 'services', path: 'src/crates/services/services-core' }, { crateName: 'services-integrations', layer: 'services', path: 'src/crates/services/services-integrations' }, + { crateName: 'legacy-migration-adapters', layer: 'services', path: 'src/crates/services/legacy-migration-adapters' }, { crateName: 'legacy-migration', layer: 'services', path: 'src/crates/services/legacy-migration' }, { crateName: 'miniapp-market-service', layer: 'services', path: 'src/crates/services/miniapp-market-service' }, { crateName: 'skin-market-service', layer: 'services', path: 'src/crates/services/skin-market-service' }, diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index c576e7ead8..0b2c575fff 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -38,6 +38,7 @@ export const optionalDependencyFeatureOwnerRules = [ }, { crateName: 'services-core', + reviewedAggregateFeatures: ['workspace-persistence'], reason: 'services-core optional implementation dependencies must stay behind their exact owner capability', dependencies: [ @@ -47,11 +48,11 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'base64', ownerFeatures: ['credential-vault', 'filesystem'] }, { depName: 'openbitfun-core-types', - ownerFeatures: ['filesystem', 'local-storage', 'product-identity'], + ownerFeatures: ['filesystem', 'local-storage', 'product-identity', 'workspace-persistence'], }, - { depName: 'openbitfun-events', ownerFeatures: ['local-storage'] }, - { depName: 'openbitfun-runtime-ports', ownerFeatures: ['permission', 'workspace-runtime'] }, - { depName: 'chrono', ownerFeatures: ['filesystem', 'local-storage'] }, + { depName: 'openbitfun-events', ownerFeatures: ['local-storage', 'session-event-format'] }, + { depName: 'openbitfun-runtime-ports', ownerFeatures: ['permission', 'workspace-runtime', 'workspace-persistence'] }, + { depName: 'chrono', ownerFeatures: ['filesystem', 'local-storage', 'workspace-persistence'] }, { depName: 'chrono-tz', ownerFeatures: ['token-usage-statistics'] }, { depName: 'dunce', ownerFeatures: ['runtime-ownership', 'workspace-identity', 'workspace-runtime'] }, { depName: 'fs2', ownerFeatures: ['credential-vault', 'json-io', 'local-storage', 'runtime-ownership'] }, @@ -70,7 +71,7 @@ export const optionalDependencyFeatureOwnerRules = [ 'workspace-instructions', ], }, - { depName: 'rusqlite', ownerFeatures: ['memory-store', 'permission', 'session-search'] }, + { depName: 'rusqlite', ownerFeatures: ['memory-store', 'permission', 'session-search', 'coordination-store'] }, { depName: 'rustls', ownerFeatures: ['tls-provider'] }, { depName: 'serde_yaml', ownerFeatures: ['markdown', 'workspace-instructions'] }, { depName: 'similar', ownerFeatures: ['diff', 'local-storage'] }, @@ -157,14 +158,14 @@ export const optionalDependencyFeatureOwnerRules = [ depName: 'openbitfun-ai-adapters', ownerFeatures: ['ai-adapter-runtime', 'subscription-auth'], }, - { depName: 'openbitfun-agent-runtime', ownerFeatures: ['agent-runtime', 'legacy-migration'] }, + { depName: 'openbitfun-agent-runtime', ownerFeatures: ['agent-runtime'] }, { depName: 'openbitfun-agent-workflows', ownerFeatures: ['deep-research'] }, { depName: 'openbitfun-agent-stream', ownerFeatures: ['agent-runtime'] }, { depName: 'openbitfun-agent-tools', ownerFeatures: ['agent-runtime', 'local-storage', 'mcp-runtime'] }, { depName: 'openbitfun-claude-code-adapter', ownerFeatures: ['external-sources'] }, { depName: 'openbitfun-codex-adapter', ownerFeatures: ['external-sources'] }, { depName: 'openbitfun-external-sources', ownerFeatures: ['external-sources'] }, - { depName: 'openbitfun-legacy-migration', ownerFeatures: ['legacy-migration'] }, + { depName: 'openbitfun-legacy-migration-adapters', ownerFeatures: ['legacy-migration'] }, { depName: 'openbitfun-opencode-adapter', ownerFeatures: ['external-sources'] }, { depName: 'openbitfun-dsh-adapter', ownerFeatures: ['external-sources'] }, { depName: 'openbitfun-plugin-runtime-client', ownerFeatures: ['plugin-runtime'] }, @@ -175,7 +176,6 @@ export const optionalDependencyFeatureOwnerRules = [ 'agent-runtime', 'canvas-runtime', 'function-agents', - 'legacy-migration', 'plugin-source', 'product-search', 'tools-miniapp', @@ -193,7 +193,6 @@ export const optionalDependencyFeatureOwnerRules = [ 'external-sources', 'file-watch', 'function-agents', - 'legacy-migration', 'git', 'mcp-runtime', 'model-catalog', @@ -239,7 +238,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'md5', ownerFeatures: ['agent-runtime'] }, { depName: 'reqwest', ownerFeatures: ['mcp-runtime', 'tools-miniapp'] }, { depName: 'regex', ownerFeatures: ['agent-runtime'] }, - { depName: 'rusqlite', ownerFeatures: ['agent-runtime', 'legacy-migration'] }, + { depName: 'rusqlite', ownerFeatures: ['agent-runtime'] }, { depName: 'semver', ownerFeatures: ['tools-miniapp'] }, { depName: 'serde_yaml', ownerFeatures: ['workspace-runtime'] }, { depName: 'similar', ownerFeatures: ['agent-runtime'] }, @@ -498,7 +497,7 @@ export const capabilityContractDependencyRules = [ capabilityForwarder('workspace-runtime', 'runtime-event-port'), capabilityForwarder('workspace-runtime', 'workspace-ports'), ], - ['permission', 'workspace-runtime'], + ['permission', 'workspace-runtime', 'workspace-persistence'], )], ['openbitfun-services-integrations', capabilityConsumer( [capabilityEdge([], { optional: true })], @@ -639,6 +638,7 @@ export const capabilityContractDependencyRules = [ ], }, consumers: new Map([ + ['openbitfun-legacy-migration-adapters', capabilityConsumer([capabilityEdge(['definition-contracts'])])], ['openbitfun-acp', capabilityConsumer( [capabilityEdge([], { optional: true })], [capabilityForwarder('server', 'agent-runtime')], @@ -655,9 +655,8 @@ export const capabilityContractDependencyRules = [ [capabilityEdge([], { optional: true })], [ capabilityForwarder('agent-runtime', 'agent-runtime'), - capabilityForwarder('legacy-migration', 'definition-contracts'), ], - ['agent-runtime', 'legacy-migration'], + ['agent-runtime'], ['external-sources', 'mcp-runtime', 'opencode-plugin-host', 'plugin-runtime', 'product-search', 'product-full', 'remote-connect', 'tools-mcp'], )], ['openbitfun-desktop', capabilityConsumer([ @@ -883,6 +882,8 @@ export const coreClosedFeatureProfileRules = [ // Complete ExecCommand constraint syntax facts live in the tool owner. 'tool-runtime/shell-analysis', 'openbitfun-services-core/memory-store', + 'openbitfun-services-core/coordination-store', + 'openbitfun-services-core/session-event-format', 'openbitfun-services-core/permission', 'openbitfun-services-core/runtime-ownership', 'openbitfun-services-core/session-git', @@ -1633,6 +1634,7 @@ export const coreClosedFeatureProfileRules = [ 'openbitfun-services-core/workspace-identity', 'openbitfun-services-core/workspace-instructions', 'openbitfun-services-core/workspace-runtime', + 'openbitfun-services-core/workspace-persistence', ], exact: true, reason: 'openbitfun-core workspace-runtime must select only local workspace and runtime layout owners', diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 0667091eae..a6d5c193f5 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -544,6 +544,7 @@ export function runManifestParserSelfTest({ 'openbitfun-services-core/workspace-identity', 'openbitfun-services-core/workspace-instructions', 'openbitfun-services-core/workspace-runtime', + 'openbitfun-services-core/workspace-persistence', ], ], [coreManifest, 'workspace-watch', ['workspace-runtime', 'dep:notify']], diff --git a/scripts/data-migrator-release.mjs b/scripts/data-migrator-release.mjs new file mode 100644 index 0000000000..ae45a91e1e --- /dev/null +++ b/scripts/data-migrator-release.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import { createHash } from 'node:crypto'; +import { copyFileSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const ROOT = resolve(import.meta.dirname, '..'); +export const PLATFORMS = { + 'windows-x64': { target: 'x86_64-pc-windows-msvc', extension: 'zip' }, + 'macos-arm64': { target: 'aarch64-apple-darwin', extension: 'dmg', folder: 'dmg' }, + 'macos-x64': { target: 'x86_64-apple-darwin', extension: 'dmg', folder: 'dmg' }, + 'linux-x64': { target: 'x86_64-unknown-linux-gnu', extension: 'AppImage', folder: 'appimage' }, +}; +export function releaseVersion(root = ROOT) { + const manifest = readFileSync(join(root, 'src/apps/data-migrator/Cargo.toml'), 'utf8'); + const version = manifest.match(/^version = "([^"]+)"/m)?.[1]; + if (!version || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(version)) throw new Error('Invalid independent migrator version'); + const config = JSON.parse(readFileSync(join(root, 'src/apps/data-migrator/tauri.conf.json'), 'utf8')); + if (config.version !== version) throw new Error('Migrator version differs between Cargo and Tauri'); + return version; +} + +export function validateReleaseTag(tag, version) { + if (tag !== `data-migrator-v${version}`) throw new Error('Tag must match the independent migrator version'); +} + +function stage(platform) { + const metadata = PLATFORMS[platform]; + if (!metadata) throw new Error('Unsupported migrator release platform'); + const version = releaseVersion(); + if (process.env.GITHUB_REF_TYPE === 'tag') validateReleaseTag(process.env.GITHUB_REF_NAME, version); + const target = resolve(ROOT, process.env.CARGO_TARGET_DIR || 'target', metadata.target, 'release'); + const output = join(ROOT, 'target/data-migrator-release', platform); + mkdirSync(output, { recursive: true }); + const asset = `openbitfun-data-migrator-v${version}-${platform}.${metadata.extension}`; + const destination = join(output, asset); + if (platform === 'windows-x64') { + const portable = join(ROOT, 'target/data-migrator-portable', version); + mkdirSync(portable, { recursive: true }); + copyFileSync(join(target, 'openbitfun-data-migrator.exe'), join(portable, 'openbitfun-data-migrator.exe')); + for (const file of ['README.md', 'README.zh-CN.md']) copyFileSync(join(ROOT, 'src/apps/data-migrator', file), join(portable, file)); + copyFileSync(join(ROOT, 'THIRD_PARTY_NOTICES.md'), join(portable, 'THIRD_PARTY_NOTICES.md')); + const result = spawnSync('tar', ['-a', '-cf', destination, '-C', portable, 'openbitfun-data-migrator.exe', 'README.md', 'README.zh-CN.md', 'THIRD_PARTY_NOTICES.md'], { stdio: 'inherit', windowsHide: true }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error('Could not archive the portable migrator'); + } else { + const folder = join(target, 'bundle', metadata.folder); + const files = readdirSync(folder).filter((name) => name.startsWith(`OpenBitFun Data Migrator_${version}_`) && name.endsWith(`.${metadata.extension}`)); + if (files.length !== 1) throw new Error('Expected exactly one migrator package in the bundle output'); + copyFileSync(join(folder, files[0]), destination); + } + const sha256 = createHash('sha256').update(readFileSync(destination)).digest('hex'); + writeFileSync(`${destination}.sha256`, `${sha256} ${asset}\n`); + console.log(destination); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + if (process.argv[2] === '--check-version') { + const version = releaseVersion(); + if (process.env.GITHUB_REF_TYPE === 'tag') validateReleaseTag(process.env.GITHUB_REF_NAME, version); + console.log(version); + } else stage(process.argv[2]); +} diff --git a/scripts/data-migrator-tauri-build.mjs b/scripts/data-migrator-tauri-build.mjs index e52219ac7f..9c2a7a4cff 100644 --- a/scripts/data-migrator-tauri-build.mjs +++ b/scripts/data-migrator-tauri-build.mjs @@ -5,27 +5,19 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { extractProductConfigArg } from './product-customization/cli.mjs'; -import { productBuildEnvironment } from './product-customization/projections.mjs'; -import { resolveProductDefinition } from './product-customization/resolver.mjs'; +import { generateDataMigratorTheme } from './generate-data-migrator-theme.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const APP_DIR = join(ROOT, 'src', 'apps', 'data-migrator'); export function prepareDataMigratorTauriConfig( baseConfigPath, - resolution, outputDirectory = join(APP_DIR, 'gen'), ) { - if (resolution.assembly.member !== 'dataMigrator') { - throw new Error('Data Migrator packaging requires the dataMigrator product member.'); - } const config = JSON.parse(readFileSync(baseConfigPath, 'utf8')); - const productName = resolution.productNames[resolution.assembly.fallbackLocale] - ?? resolution.productNames[resolution.assembly.defaultLocale]; - config.productName = productName; - config.mainBinaryName = resolution.assembly.binaryName; - config.identifier = resolution.assembly.bundleId; + const manifest = readFileSync(join(APP_DIR, 'Cargo.toml'), 'utf8'); + const version = manifest.match(/^version = "([^"]+)"/m)?.[1]; + if (!version || config.version !== version) throw new Error('Migrator Cargo and Tauri versions must match.'); config.build = { frontendDist: config.build?.frontendDist || 'ui', }; @@ -38,12 +30,28 @@ export function prepareDataMigratorTauriConfig( mkdirSync(outputDirectory, { recursive: true }); const output = join( outputDirectory, - `tauri.${resolution.assembly.assemblyDigest}.generated.conf.json`, + 'tauri.generated.conf.json', ); writeFileSync(output, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); return output; } +// The tool identity and the format destination are separate. Never inherit a +// branded Desktop build's data namespace or sibling-executable projections. +export function dataMigratorEnvironment(environment = process.env) { + const result = { ...environment }; + for (const key of ['OPENBITFUN_DESKTOP_BINARY_NAME', 'OPENBITFUN_DATA_MIGRATOR_BINARY_NAME']) delete result[key]; + return { + ...result, + CI: 'true', + OPENBITFUN_PRODUCT_ID: 'openbitfun', + OPENBITFUN_DATA_NAMESPACE: 'openbitfun', + OPENBITFUN_HIDDEN_DATA_DIRECTORY: '.openbitfun', + OPENBITFUN_PRODUCT_BINARY_NAME: 'openbitfun-data-migrator', + OPENBITFUN_PRODUCT_DISPLAY_NAME: 'OpenBitFun Data Migrator', + }; +} + function tauriArguments(raw) { let offset = 0; while (raw[offset] === '--') offset += 1; @@ -51,26 +59,19 @@ function tauriArguments(raw) { } async function main() { - const { productConfig, forwardArgs } = extractProductConfigArg( - tauriArguments(process.argv.slice(2)), - ); - const resolution = resolveProductDefinition({ - rootDir: ROOT, - productConfig, - member: 'dataMigrator', - }); - Object.assign(process.env, productBuildEnvironment(resolution)); - process.env.CI = 'true'; + await generateDataMigratorTheme(); + const forwardArgs = tauriArguments(process.argv.slice(2)); + if (forwardArgs.some((arg) => arg.startsWith('--product-config'))) { + throw new Error('The standalone migrator has its own identity and supports OpenBitFun data only.'); + } const generated = prepareDataMigratorTauriConfig( join(APP_DIR, 'tauri.conf.json'), - resolution, ); - console.log(`[product] dataMigrator ${resolution.assembly.assemblyDigest}`); const tauriBin = join(ROOT, 'node_modules', '.bin', 'tauri'); const result = spawnSync(tauriBin, ['build', '--config', generated, ...forwardArgs], { cwd: APP_DIR, - env: process.env, + env: dataMigratorEnvironment(), stdio: 'inherit', shell: true, windowsHide: true, diff --git a/scripts/data-migrator-tauri-build.test.mjs b/scripts/data-migrator-tauri-build.test.mjs index 8d2cad1615..b045752d87 100644 --- a/scripts/data-migrator-tauri-build.test.mjs +++ b/scripts/data-migrator-tauri-build.test.mjs @@ -1,92 +1,67 @@ import assert from 'node:assert/strict'; -import { mkdtempSync, readFileSync } from 'node:fs'; +import { mkdtempSync, readFileSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import test from 'node:test'; - -import { prepareDataMigratorTauriConfig } from './data-migrator-tauri-build.mjs'; -import { productBuildEnvironment } from './product-customization/projections.mjs'; -import { resolveProductDefinition } from './product-customization/resolver.mjs'; - +import { prepareDataMigratorTauriConfig, dataMigratorEnvironment } from './data-migrator-tauri-build.mjs'; +import { releaseVersion, validateReleaseTag } from './data-migrator-release.mjs'; +import { prepareTauriConfig } from './desktop-tauri-build.mjs'; const ROOT = resolve(import.meta.dirname, '..'); -const APP = join(ROOT, 'src', 'apps', 'data-migrator'); -const ACME = join(ROOT, 'products', 'fixtures', 'acme', 'product.jsonc'); +const APP = join(ROOT, 'src/apps/data-migrator'); -test('Data Migrator has an independent product and non-updating bundle identity', () => { - const resolution = resolveProductDefinition({ - rootDir: ROOT, - productConfig: ACME, - member: 'dataMigrator', - }); - const output = prepareDataMigratorTauriConfig( - join(APP, 'tauri.conf.json'), - resolution, - mkdtempSync(join(tmpdir(), 'openbitfun-data-migrator-config-')), - ); +test('independent bundle owns version, offline assets, icons and identity', () => { + const output = prepareDataMigratorTauriConfig(join(APP, 'tauri.conf.json'), mkdtempSync(join(tmpdir(), 'migrator-config-'))); const config = JSON.parse(readFileSync(output, 'utf8')); - - assert.equal(config.productName, 'Acme Data Migrator'); - assert.equal(config.mainBinaryName, 'acme-data-migrator'); - assert.equal(config.identifier, 'com.acme.data-migrator'); - assert.notEqual(config.identifier, 'com.acme.desktop'); + assert.equal(config.productName, 'OpenBitFun Data Migrator'); + assert.equal(config.identifier, 'com.openbitfun.data-migrator'); + assert.equal(config.version, releaseVersion()); assert.deepEqual(config.build, { frontendDist: 'ui' }); assert.equal(config.plugins?.updater, undefined); - assert.equal(config.bundle.createUpdaterArtifacts, undefined); - assert.deepEqual(Object.values(config.bundle.resources), ['THIRD_PARTY_NOTICES.md']); - assert.deepEqual(config.app.windows.map(({ label }) => label), ['migrator']); + assert.equal(config.bundle.externalBin, undefined); + assert.ok(config.bundle.icon.every((icon) => !icon.includes('desktop') && existsSync(join(APP, icon)))); + assert.ok(config.app.security.csp.includes("connect-src 'self'")); }); -test('Data Migrator projection provides both trusted sibling binary names', () => { - const resolution = resolveProductDefinition({ - rootDir: ROOT, - productConfig: ACME, - member: 'dataMigrator', - }); - const environment = productBuildEnvironment(resolution); - - assert.equal(environment.OPENBITFUN_PRODUCT_BINARY_NAME, 'acme-data-migrator'); - assert.equal(environment.OPENBITFUN_DATA_MIGRATOR_BINARY_NAME, 'acme-data-migrator'); - assert.equal(environment.OPENBITFUN_DESKTOP_BINARY_NAME, 'acme-desktop'); +test('a Desktop product environment cannot change the migration destination identity', () => { + const env = dataMigratorEnvironment({ OPENBITFUN_PRODUCT_ID: 'acme', OPENBITFUN_DATA_NAMESPACE: 'acme', OPENBITFUN_DESKTOP_BINARY_NAME: 'acme', CI: '1' }); + assert.equal(env.OPENBITFUN_PRODUCT_ID, 'openbitfun'); + assert.equal(env.OPENBITFUN_DATA_NAMESPACE, 'openbitfun'); + assert.equal(env.OPENBITFUN_DESKTOP_BINARY_NAME, undefined); + assert.equal(env.CI, 'true'); }); -test('Data Migrator dependency and command closure stays migration-only', () => { - const manifest = readFileSync(join(APP, 'Cargo.toml'), 'utf8'); - const source = readFileSync(join(APP, 'src', 'app_state.rs'), 'utf8'); - const registration = readFileSync(join(APP, 'src', 'lib.rs'), 'utf8'); - const capability = readFileSync(join(APP, 'capabilities', 'migrator.json'), 'utf8'); - - assert.match(manifest, /openbitfun-core[^\n]+features = \["legacy-migration"\]/); - for (const forbidden of ['product-full', 'openbitfun-agent-runtime', 'plugin-runtime']) { - assert.equal(manifest.includes(forbidden), false, `manifest must not include ${forbidden}`); - } - assert.match(source, /product_assembly_plan_for_profile\(DeliveryProfile::DataMigrator\)/); - assert.match(registration, /tauri::generate_handler!/); - assert.match(registration, /commands::export_migration_diagnostics/); - assert.match( - readFileSync(join(ROOT, 'scripts', 'data-migrator-tauri-build.mjs'), 'utf8'), - /windowsHide:\s*true/, - ); - for (const forbidden of ['fs:', 'shell:', 'updater:', 'dialog:']) { - assert.equal(capability.includes(forbidden), false, `capability must not include ${forbidden}`); +test('Desktop package generation has no migrator payload or build hook', () => { + const desktop = join(ROOT, 'src/apps/desktop'); + const output = prepareTauriConfig(join(desktop, 'tauri.conf.json'), { desktopDir: desktop }); + const config = JSON.parse(readFileSync(output, 'utf8')); + assert.ok(!(config.bundle.externalBin || []).some((file) => file.includes('migrator'))); + for (const file of ['scripts/dev.cjs', 'scripts/desktop-tauri-build.mjs', 'src/apps/desktop/src/lib.rs']) { + assert.doesNotMatch(readFileSync(join(ROOT, file), 'utf8'), /data-migrator|legacy_migration_api/); } }); -test('Data Migrator gates onboarding actions on authenticated bootstrap', () => { - const html = readFileSync(join(APP, 'ui', 'index.html'), 'utf8'); - const source = readFileSync(join(APP, 'ui', 'app.js'), 'utf8'); - - assert.match(html, /
]+hidden>/); - assert.match(source, /function requireBootstrap\(\)/); - assert.match(source, /if \(!requireBootstrap\(\)\) return;/); - assert.match(source, /catch \(error\) \{\s+notice\(/); +test('migrator dependency and command closure exclude the main app and restart handshake', () => { + const manifest = readFileSync(join(APP, 'Cargo.toml'), 'utf8'); + assert.doesNotMatch(manifest, /^openbitfun-core\s*=|product-full|product-capabilities|plugin-runtime/m); + const source = readFileSync(join(APP, 'src/app_state.rs'), 'utf8'); + assert.doesNotMatch(source, /HandoffStore|MigrationOnboardingStore|restart_desktop|TrustedInstallationResolver/); + const capability = readFileSync(join(APP, 'capabilities/migrator.json'), 'utf8'); + assert.doesNotMatch(capability, /fs:|shell:|updater:|dialog:/); }); -test('Data Migrator labels unverified report counts as staged', () => { - const source = readFileSync(join(APP, 'ui', 'app.js'), 'utf8'); +test('independent release tags cannot be confused with main app versions', () => { + const version = releaseVersion(); + assert.doesNotThrow(() => validateReleaseTag('data-migrator-v' + version, version)); + assert.throws(() => validateReleaseTag('v' + version, version)); + assert.throws(() => validateReleaseTag('data-migrator-v9.0.0', version)); +}); - assert.match( - source, - /result\.state === 'verified' \? text\.imported : text\.staged/, - ); - assert.match(source, /\$\{result\.imported\} \$\{transferLabel\(result\)\}/); +test('all static UI labels and locale keys have complete translations', () => { + const html = readFileSync(join(APP, 'ui/index.html'), 'utf8'); + const locales = JSON.parse(readFileSync(join(APP, 'ui/locales.json'), 'utf8')); + const keys = Object.keys(locales.en).sort(); + for (const labels of Object.values(locales)) { + assert.deepEqual(Object.keys(labels).sort(), keys); + for (const match of html.matchAll(/data-i18n="([^"]+)"/g)) assert.ok(labels[match[1]], match[1]); + } }); diff --git a/scripts/desktop-dev-migration.mjs b/scripts/desktop-dev-migration.mjs deleted file mode 100644 index 99aa2455ae..0000000000 --- a/scripts/desktop-dev-migration.mjs +++ /dev/null @@ -1,65 +0,0 @@ -import { mkdtemp, readFile, rm } from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { setTimeout as delay } from 'node:timers/promises'; - -const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -async function readOptionalJson(file) { - try { - return JSON.parse(await readFile(file, 'utf8')); - } catch (error) { - if (error.code === 'ENOENT') return null; - throw error; - } -} - -function isProcessAlive(pid) { - try { - process.kill(pid, 0); - return true; - } catch (error) { - if (error.code === 'ESRCH') return false; - throw error; - } -} - -// Tauri stops its frontend server when Desktop hands off to Data Migrator. -// Keep the development supervisor alive and re-enter Tauri after the migrator -// requests a restart, restoring both Vite and the Rust watcher. -export async function runDesktopWithMigrationRestart(run, { - info = () => {}, - isAlive = isProcessAlive, - wait = () => delay(250), -} = {}) { - let restartArgs = []; - for (;;) { - const directory = await mkdtemp(path.join(os.tmpdir(), 'openbitfun-dev-migration-')); - try { - let failure; - try { - await run(restartArgs, { OPENBITFUN_DEV_MIGRATION_DIR: directory }); - } catch (error) { - failure = error; - } - const handoff = await readOptionalJson(path.join(directory, 'handoff.json')); - if (!handoff) { - if (failure) throw failure; - return; - } - if (!UUID.test(handoff.runId) || !Number.isSafeInteger(handoff.pid) || handoff.pid <= 0) { - throw new Error('Invalid development migration handoff'); - } - info('Waiting for Data Migrator; Desktop will reopen automatically when it finishes'); - while (isAlive(handoff.pid)) await wait(); - const restart = await readOptionalJson(path.join(directory, 'restart.json')); - if (restart?.runId !== handoff.runId) { - throw new Error('Data Migrator exited without completing its restart handoff; run desktop:dev to retry'); - } - restartArgs = ['--legacy-migration-run-id', handoff.runId]; - info('Restarting Desktop through the development launcher'); - } finally { - await rm(directory, { recursive: true, force: true }); - } - } -} diff --git a/scripts/desktop-dev-migration.test.mjs b/scripts/desktop-dev-migration.test.mjs deleted file mode 100644 index 6f064f228c..0000000000 --- a/scripts/desktop-dev-migration.test.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import assert from 'node:assert/strict'; -import { access, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import test from 'node:test'; -import { runDesktopWithMigrationRestart } from './desktop-dev-migration.mjs'; - -const runId = '01234567-89ab-4cde-8fab-0123456789ab'; -const write = (directory, file, value) => writeFile(path.join(directory, file), JSON.stringify(value)); - -test('normal exit does not restart and removes its temporary channel', async () => { - let directory; - let calls = 0; - await runDesktopWithMigrationRestart(async (args, env) => { - calls++; - assert.deepEqual(args, []); - directory = env.OPENBITFUN_DEV_MIGRATION_DIR; - }); - assert.equal(calls, 1); - await assert.rejects(access(directory), { code: 'ENOENT' }); -}); - -test('build failures remain failures when no migration was launched', async () => { - const failure = new Error('build failed'); - await assert.rejects(runDesktopWithMigrationRestart(async () => { throw failure; }), failure); -}); - -test('migration completion waits for child exit then restores the development host with the run id', async () => { - let calls = 0; - let directory; - let running = true; - await runDesktopWithMigrationRestart(async (args, env) => { - calls++; - if (calls === 1) { - directory = env.OPENBITFUN_DEV_MIGRATION_DIR; - await write(directory, 'handoff.json', { runId, pid: 123 }); - // A handoff may also make Tauri report its stopped frontend as a failure. - throw new Error('frontend stopped'); - } - assert.equal(running, false); - assert.deepEqual(args, ['--legacy-migration-run-id', runId]); - assert.notEqual(env.OPENBITFUN_DEV_MIGRATION_DIR, directory); - }, { - isAlive: (pid) => { assert.equal(pid, 123); return running; }, - wait: async () => { - await write(directory, 'restart.json', { runId }); - running = false; - }, - }); - assert.equal(calls, 2); - await assert.rejects(access(directory), { code: 'ENOENT' }); -}); - -test('crashed or mismatched migrators cannot silently restart Desktop', async () => { - for (const restart of [null, { runId: 'wrong-run' }]) { - await assert.rejects(runDesktopWithMigrationRestart(async (_, env) => { - await write(env.OPENBITFUN_DEV_MIGRATION_DIR, 'handoff.json', { runId, pid: 123 }); - if (restart) await write(env.OPENBITFUN_DEV_MIGRATION_DIR, 'restart.json', restart); - }, { isAlive: () => false }), /without completing its restart handoff/); - } -}); diff --git a/scripts/desktop-tauri-build.mjs b/scripts/desktop-tauri-build.mjs index e362c16503..34ca961ce6 100644 --- a/scripts/desktop-tauri-build.mjs +++ b/scripts/desktop-tauri-build.mjs @@ -30,7 +30,6 @@ const LINUX_FLASHGREP_BINARIES = [ 'flashgrep-aarch64-unknown-linux-musl', 'flashgrep-aarch64-unknown-linux-gnu', ]; -const DATA_MIGRATOR_CARGO_BINARY = 'openbitfun-data-migrator'; function tauriBuildArgsFromArgv() { const args = process.argv.slice(2); @@ -54,7 +53,6 @@ async function main() { const desktopDir = join(ROOT, 'src', 'apps', 'desktop'); preparePluginHost(); - const dataMigratorSidecar = prepareDataMigratorSidecar(forward, resolution, desktopDir); // Flashgrep distribution is temporarily suspended. const flashgrepBinary = null; // Tauri CLI reads CI and rejects numeric "1" (common in CI providers). @@ -68,7 +66,6 @@ async function main() { const tauriConfig = prepareTauriConfig(join(desktopDir, 'tauri.conf.json'), { desktopDir, flashgrepBinary, - dataMigratorSidecar, resolution, releaseChannel, }); @@ -99,10 +96,6 @@ async function main() { process.exit(1); } - if (r.status === 0 && forward.includes('--no-bundle')) { - stageNoBundleDataMigrator(dataMigratorSidecar); - } - // Keep only the latest useful Cargo caches for this build profile after tauri build ends. try { const { profileFromTauriBuildArgs, runGcBestEffort, targetFromTauriBuildArgs } = await import( @@ -142,84 +135,6 @@ function rustHostTargetTriple() { return host; } -export function planDataMigratorSidecar( - args, - resolution, - desktopDir, - runtime = {}, -) { - const explicitTarget = optionValue(args, '--target'); - const targetTriple = explicitTarget || runtime.hostTarget || rustHostTargetTriple(); - const profile = args.includes('--debug') ? 'debug' : optionValue(args, '--profile') || 'release'; - const targetDirValue = runtime.cargoTargetDir ?? process.env.CARGO_TARGET_DIR; - const targetDir = targetDirValue - ? isAbsolute(targetDirValue) - ? targetDirValue - : resolve(ROOT, targetDirValue) - : join(ROOT, 'target'); - const windowsTarget = targetTriple.includes('windows'); - const suffix = windowsTarget ? '.exe' : ''; - const artifactDirectory = join(targetDir, ...(explicitTarget ? [explicitTarget] : []), profile); - const cargoArgs = ['build', '-p', 'openbitfun-data-migrator', '--bin', DATA_MIGRATOR_CARGO_BINARY]; - if (explicitTarget) cargoArgs.push('--target', explicitTarget); - if (args.includes('--debug')) { - // Cargo's default profile is the Tauri CLI's debug profile. - } else if (optionValue(args, '--profile')) { - cargoArgs.push('--profile', profile); - } else { - cargoArgs.push('--release'); - } - - const siblingBinaryName = resolution.assembly.memberBinaryNames.dataMigrator; - const externalBinBase = join(desktopDir, 'gen', 'sidecars', siblingBinaryName); - return { - artifactDirectory, - cargoArgs, - externalBinBase, - externalBinInput: `${externalBinBase}-${targetTriple}${suffix}`, - sourceArtifact: join(artifactDirectory, `${DATA_MIGRATOR_CARGO_BINARY}${suffix}`), - siblingArtifact: join(artifactDirectory, `${siblingBinaryName}${suffix}`), - siblingBinaryName, - targetTriple, - }; -} - -function prepareDataMigratorSidecar(args, resolution, desktopDir) { - const plan = planDataMigratorSidecar(args, resolution, desktopDir); - console.log( - `[tauri-build] Building Data Migrator sidecar (${plan.targetTriple}, ${plan.artifactDirectory})` - ); - const result = spawnSync('cargo', plan.cargoArgs, { - cwd: ROOT, - env: process.env, - stdio: 'inherit', - shell: false, - windowsHide: true, - }); - if (result.error) throw result.error; - if (result.status !== 0) { - throw new Error(`Data Migrator sidecar build failed with exit code ${result.status}`); - } - if (!existsSync(plan.sourceArtifact)) { - throw new Error(`Data Migrator build did not produce ${plan.sourceArtifact}`); - } - mkdirSync(dirname(plan.externalBinInput), { recursive: true }); - copyFileSync(plan.sourceArtifact, plan.externalBinInput); - if (!plan.externalBinInput.endsWith('.exe')) { - chmodSync(plan.externalBinInput, statSync(plan.externalBinInput).mode | 0o111); - } - return plan; -} - -export function stageNoBundleDataMigrator(plan) { - if (resolve(plan.sourceArtifact) === resolve(plan.siblingArtifact)) return plan.siblingArtifact; - copyFileSync(plan.sourceArtifact, plan.siblingArtifact); - if (!plan.siblingArtifact.endsWith('.exe')) { - chmodSync(plan.siblingArtifact, statSync(plan.siblingArtifact).mode | 0o111); - } - return plan.siblingArtifact; -} - function preparePluginHost() { const result = spawnSync('pnpm', ['run', 'plugin-host:prepare'], { cwd: ROOT, @@ -378,7 +293,7 @@ export function prepareMacOSFlashgrepForSigning( export function prepareTauriConfig( baseConfigPath, - { desktopDir, flashgrepBinary, dataMigratorSidecar, resolution, releaseChannel } + { desktopDir, flashgrepBinary, resolution, releaseChannel } ) { const config = JSON.parse(readFileSync(baseConfigPath, 'utf8')); if (resolution) { @@ -390,7 +305,6 @@ export function prepareTauriConfig( config.identifier = resolution.assembly.bundleId; } injectTargetFlashgrepResource(config, desktopDir, flashgrepBinary); - injectDataMigratorSidecar(config, desktopDir, dataMigratorSidecar); // The DeepSeek bridge is not a compile-time resource: cargo check and // desktop:dev must not require packages/dsh-acp/dist-profile. Official // packaging injects it here; frontend:build-all (beforeBuildCommand) @@ -457,16 +371,6 @@ export function prepareTauriConfig( return generatedConfig; } -function injectDataMigratorSidecar(config, desktopDir, sidecar) { - if (!sidecar) return; - const externalBin = new Set(config.bundle?.externalBin || []); - externalBin.add(toTauriPath(relative(desktopDir, sidecar.externalBinBase))); - config.bundle = { - ...(config.bundle || {}), - externalBin: [...externalBin], - }; -} - const DSH_PROFILE_RESOURCE_SOURCE = '../../../packages/dsh-acp/dist-profile'; const DSH_PROFILE_RESOURCE_TARGET = 'resources/dsh-profile'; const EXTERNAL_FRONTEND_RESOURCE_SOURCE = '../../../dist'; diff --git a/scripts/desktop-tauri-build.test.mjs b/scripts/desktop-tauri-build.test.mjs index 5c27bcda53..2399ecc306 100644 --- a/scripts/desktop-tauri-build.test.mjs +++ b/scripts/desktop-tauri-build.test.mjs @@ -5,10 +5,8 @@ import { join } from 'node:path'; import test from 'node:test'; import { configureDesktopWebFontProfile, - planDataMigratorSidecar, prepareMacOSFlashgrepForSigning, prepareTauriConfig, - stageNoBundleDataMigrator, shouldRetryMacDmgBuild, } from './desktop-tauri-build.mjs'; import { resolveProductDefinition } from './product-customization/resolver.mjs'; @@ -345,58 +343,6 @@ test('Desktop packaging works without the suspended Flashgrep resource', () => { } }); -test('Desktop packaging builds and projects the matching Data Migrator sidecar', () => { - const fixture = join(tmpdir(), `openbitfun-migrator-sidecar-${process.pid}-${Date.now()}`); - const desktopDir = join(fixture, 'src', 'apps', 'desktop'); - const targetDir = join(fixture, 'target'); - mkdirSync(desktopDir, { recursive: true }); - const baseConfig = join(fixture, 'tauri.conf.json'); - writeFileSync(baseConfig, JSON.stringify({ bundle: { resources: {} } })); - try { - const resolution = resolveProductDefinition({ - rootDir: ROOT, - productConfig: join(ROOT, 'products', 'fixtures', 'acme', 'product.jsonc'), - member: 'desktop', - }); - const plan = planDataMigratorSidecar( - ['--target', 'x86_64-pc-windows-msvc', '--profile', 'release-fast'], - resolution, - desktopDir, - { cargoTargetDir: targetDir }, - ); - assert.deepEqual(plan.cargoArgs, [ - 'build', - '-p', - 'openbitfun-data-migrator', - '--bin', - 'openbitfun-data-migrator', - '--target', - 'x86_64-pc-windows-msvc', - '--profile', - 'release-fast', - ]); - assert.equal( - plan.externalBinInput, - join(desktopDir, 'gen', 'sidecars', 'acme-data-migrator-x86_64-pc-windows-msvc.exe'), - ); - const generated = prepareTauriConfig(baseConfig, { - desktopDir, - flashgrepBinary: join(fixture, 'flashgrep'), - dataMigratorSidecar: plan, - resolution, - }); - const config = JSON.parse(readFileSync(generated, 'utf8')); - assert.deepEqual(config.bundle.externalBin, ['gen/sidecars/acme-data-migrator']); - - mkdirSync(plan.artifactDirectory, { recursive: true }); - writeFileSync(plan.sourceArtifact, 'migrator'); - assert.equal(stageNoBundleDataMigrator(plan), plan.siblingArtifact); - assert.equal(readFileSync(plan.siblingArtifact, 'utf8'), 'migrator'); - } finally { - rmSync(fixture, { force: true, recursive: true }); - } -}); - test('Windows updater installs NSIS packages without showing its progress window', () => { const fixture = join(tmpdir(), `openbitfun-tauri-updater-${process.pid}-${Date.now()}`); const baseConfig = join(fixture, 'tauri.conf.json'); diff --git a/scripts/dev.cjs b/scripts/dev.cjs index e3672f494c..bcfd15567b 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -557,31 +557,30 @@ async function startDesktopPreview() { printInfo(`Launching debug desktop binary: ${desktopBinary}`); - const { runDesktopWithMigrationRestart } = await import( - pathToFileURL(path.join(__dirname, 'desktop-dev-migration.mjs')).href - ); - try { - await runDesktopWithMigrationRestart((restartArgs, migrationEnv) => new Promise((resolve, reject) => { - appProcess = spawnBackgroundCommand(desktopBinary, restartArgs, ROOT_DIR, { - ...process.env, - ...migrationEnv, - // Upload the current workspace mobile bundle instead of the staged copy. - OPENBITFUN_MOBILE_WEB_DIR: path.join(ROOT_DIR, 'src/mobile-web/dist'), - }); - appProcess.on('error', reject); - appProcess.on('close', (code, signal) => { - appProcess = null; - if (code === 0) resolve(); - else reject(new Error(`Desktop preview exited (code=${code}, signal=${signal})`)); - }); - printSuccess('Desktop preview is running'); - printInfo('Front-end edits continue to use Vite HMR; rebuild Rust only when desktop-side code changes'); - }), { info: printInfo }); - await shutdown(0); - } catch (error) { - printError(error.message || String(error)); - await shutdown(1); - } + appProcess = spawnBackgroundCommand(desktopBinary, [], ROOT_DIR, { + ...process.env, + // Debug previews must upload the current workspace build. The adjacent + // target/debug resource tree is only a build-time copy and can lag behind + // mobile-web edits made while the desktop binary is being reused. + OPENBITFUN_MOBILE_WEB_DIR: path.join(ROOT_DIR, 'src/mobile-web/dist'), + }); + + appProcess.on('error', (error) => { + printError(`Desktop preview failed to start: ${error.message || String(error)}`); + void shutdown(1); + }); + + appProcess.on('exit', (code, signal) => { + if (!shuttingDown) { + printInfo(`Desktop preview exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})`); + } + void shutdown(code ?? 0); + }); + + printSuccess('Desktop preview is running'); + printInfo('Front-end edits continue to use Vite HMR; rebuild Rust only when desktop-side code changes'); + + await new Promise(() => {}); } /** @@ -677,22 +676,6 @@ async function main() { process.exit(1); } - // Build after version generation and before Desktop starts. Cargo checks - // freshness so an existing but stale Migrator is rebuilt as well. - if (desktopMode) { - printInfo('Preparing Data Migrator (incremental Debug build)'); - const migratorBuild = await runCommandPrefixed( - 'data-migrator', - 'cargo', - ['build', '-p', 'openbitfun-data-migrator', '--bin', 'openbitfun-data-migrator'], - ); - if (!migratorBuild.ok) { - printError('Data Migrator build failed; Desktop was not started'); - if (migratorBuild.error?.message) printError(migratorBuild.error.message); - process.exit(1); - } - } - if (desktopMode) { const baselineHelperUrl = pathToFileURL( path.join(__dirname, 'frontend-workbench-dev-baseline.mjs') @@ -751,26 +734,19 @@ async function main() { OPENBITFUN_MOBILE_WEB_DIR: path.join(ROOT_DIR, 'src/mobile-web/dist'), }; try { - const { runDesktopWithMigrationRestart } = await import( - pathToFileURL(path.join(__dirname, 'desktop-dev-migration.mjs')).href - ); - await runDesktopWithMigrationRestart(async (restartArgs, migrationEnv) => { - const args = ['dev', '--config', tauriConfig, ...(restartArgs.length ? ['--', '--', ...restartArgs] : [])]; - if (process.platform === 'win32') { - // Running the generated .cmd shim directly via spawn is flaky on Windows. - // Use cmd.exe with an explicit args array so the desktop app directory - // stays the Tauri project root without pnpm workspace path rewriting. - const tauriBin = path.join(ROOT_DIR, 'node_modules', '.bin', 'tauri.cmd'); - await runWindowsCommandArgs(tauriBin, args, desktopDir, { ...tauriDevEnv, ...migrationEnv }); - } else { - const tauriBin = path.join(ROOT_DIR, 'node_modules', '.bin', 'tauri'); - await spawnCommand(tauriBin, args, desktopDir, { - CARGO_PROFILE_DEV_CODEGEN_UNITS: tauriDevEnv.CARGO_PROFILE_DEV_CODEGEN_UNITS, - OPENBITFUN_MOBILE_WEB_DIR: tauriDevEnv.OPENBITFUN_MOBILE_WEB_DIR, - ...migrationEnv, - }); - } - }, { info: printInfo }); + if (process.platform === 'win32') { + // Running the generated .cmd shim directly via spawn is flaky on Windows. + // Use cmd.exe with an explicit args array so the desktop app directory + // stays the Tauri project root without pnpm workspace path rewriting. + const tauriBin = path.join(ROOT_DIR, 'node_modules', '.bin', 'tauri.cmd'); + await runWindowsCommandArgs(tauriBin, ['dev', '--config', tauriConfig], desktopDir, tauriDevEnv); + } else { + const tauriBin = path.join(ROOT_DIR, 'node_modules', '.bin', 'tauri'); + await spawnCommand(tauriBin, ['dev', '--config', tauriConfig], desktopDir, { + CARGO_PROFILE_DEV_CODEGEN_UNITS: tauriDevEnv.CARGO_PROFILE_DEV_CODEGEN_UNITS, + OPENBITFUN_MOBILE_WEB_DIR: tauriDevEnv.OPENBITFUN_MOBILE_WEB_DIR, + }); + } } finally { // Option B: prune only when the desktop:dev session ends, not on each rebuild. await runDesktopTargetGc('debug'); diff --git a/scripts/frontend-color-surface-registry.json b/scripts/frontend-color-surface-registry.json index 5693d6497e..dd7bf656a6 100644 --- a/scripts/frontend-color-surface-registry.json +++ b/scripts/frontend-color-surface-registry.json @@ -114,6 +114,19 @@ "baseline": "scripts/theme-color-governance-baseline.installer.json" } }, + { + "id": "data-migrator", + "label": "Offline Data Migrator", + "kind": "canonical-web", + "owner": "@openbitfun/design-tokens and @openbitfun/theme-openbitfun public CSS exports", + "root": "src/apps/data-migrator/ui", + "audit": { + "engine": "theme", + "policy": "canonical-ui-zero", + "packageContracts": ["@openbitfun/design-tokens", "@openbitfun/theme-openbitfun"], + "excludePaths": ["generated"] + } + }, { "id": "desktop-bootstrap", "label": "Desktop pre-JavaScript bootstrap pages", @@ -362,6 +375,11 @@ } ], "generatedChecks": [ + { + "id": "data-migrator-design-system", + "surfaceIds": ["data-migrator"], + "command": ["node", "scripts/generate-data-migrator-theme.mjs", "--check"] + }, { "id": "desktop-appearance-projection", "surfaceIds": ["web-ui", "desktop-bootstrap"], @@ -463,6 +481,13 @@ "owner": "scripts/mobile-ui-design-system.mjs", "reason": "Preview data is generated from the native mobile token contract and checked for drift." }, + { + "id": "data-migrator-generated", + "path": "src/apps/data-migrator/ui/generated", + "kind": "generated-output", + "owner": "scripts/generate-data-migrator-theme.mjs", + "reason": "Offline CSS is bundled from public design-system exports and checked for drift." + }, { "id": "desktop-bootstrap-generated", "path": "src/apps/desktop/src/generated", diff --git a/scripts/generate-data-migrator-theme.mjs b/scripts/generate-data-migrator-theme.mjs new file mode 100644 index 0000000000..1631e45c24 --- /dev/null +++ b/scripts/generate-data-migrator-theme.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** Bundle the public design-system CSS for the offline, static migrator UI. */ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(import.meta.dirname, '..'); +const OUTPUT = join(ROOT, 'src/apps/data-migrator/ui/generated/design-system.css'); +const themeRequire = createRequire(join(ROOT, 'design-system/packages/theme-openbitfun/package.json')); + +function bundleStylesheet(file, ancestors = new Set()) { + if (ancestors.has(file)) throw new Error(`Circular design-system stylesheet import: ${file}`); + const imports = new Set([...ancestors, file]); + const requireFromFile = createRequire(file); + return readFileSync(file, 'utf8').replace(/\r\n?/g, '\n').replace( + /@import\s+["']([^"']+)["'];/g, + (_, specifier) => bundleStylesheet(requireFromFile.resolve(specifier), imports).trimEnd(), + ); +} + +export async function generateDataMigratorTheme({ check = false } = {}) { + // Build the owning packages before reading their exports; an older dist must + // never make a source change appear current in the generated-output check. + await import('../design-system/packages/design-tokens/scripts/build.mjs'); + await import('../design-system/packages/theme-openbitfun/scripts/build.mjs'); + const css = bundleStylesheet(themeRequire.resolve('@openbitfun/theme-openbitfun/default.css')); + if (/@import\b|url\(/i.test(css)) { + throw new Error('Data Migrator design-system CSS must have no external asset dependencies.'); + } + const content = '/* Generated by scripts/generate-data-migrator-theme.mjs. Do not edit. */\n' + + `${css.trimEnd()}\n`; + const current = existsSync(OUTPUT) ? readFileSync(OUTPUT, 'utf8').replace(/\r\n?/g, '\n') : null; + if (current === content) return; + if (check) { + throw new Error('Data Migrator theme is stale. Run `pnpm run data-migrator:theme:generate`.'); + } + mkdirSync(dirname(OUTPUT), { recursive: true }); + writeFileSync(OUTPUT, content, 'utf8'); + console.log('[data-migrator] Generated offline design-system CSS'); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + generateDataMigratorTheme({ check: process.argv.includes('--check') }).catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/scripts/product-customization/projections.mjs b/scripts/product-customization/projections.mjs index 8918e16777..8bf06c09a9 100644 --- a/scripts/product-customization/projections.mjs +++ b/scripts/product-customization/projections.mjs @@ -46,9 +46,6 @@ export function productBuildEnvironment(resolution) { OPENBITFUN_HIDDEN_DATA_DIRECTORY: `.${resolution.assembly.dataNamespace}`, OPENBITFUN_PRODUCT_BINARY_NAME: resolution.assembly.binaryName, OPENBITFUN_PRODUCT_DISPLAY_NAME: fallbackName, - OPENBITFUN_DESKTOP_BINARY_NAME: resolution.assembly.memberBinaryNames.desktop, - OPENBITFUN_DATA_MIGRATOR_BINARY_NAME: - resolution.assembly.memberBinaryNames.dataMigrator, }; if (!resolution.isDefaultProduct) { const cargoTargetRoot = process.env.CARGO_TARGET_DIR diff --git a/scripts/product-customization/projections.test.mjs b/scripts/product-customization/projections.test.mjs index 25334218db..6f6ffeb472 100644 --- a/scripts/product-customization/projections.test.mjs +++ b/scripts/product-customization/projections.test.mjs @@ -32,9 +32,4 @@ test('build environment isolates custom Cargo output without overriding the defa assert.equal(customEnvironment.OPENBITFUN_HIDDEN_DATA_DIRECTORY, '.acme'); assert.equal(customEnvironment.OPENBITFUN_PRODUCT_BINARY_NAME, 'acme'); assert.equal(customEnvironment.OPENBITFUN_PRODUCT_DISPLAY_NAME, 'Acme CLI'); - assert.equal(customEnvironment.OPENBITFUN_DESKTOP_BINARY_NAME, 'acme-desktop'); - assert.equal( - customEnvironment.OPENBITFUN_DATA_MIGRATOR_BINARY_NAME, - 'acme-data-migrator', - ); }); diff --git a/scripts/product-customization/resolver.mjs b/scripts/product-customization/resolver.mjs index c522f00ad4..8a6487ec9b 100644 --- a/scripts/product-customization/resolver.mjs +++ b/scripts/product-customization/resolver.mjs @@ -15,7 +15,7 @@ const ROOT_FIELDS = new Set([ 'localeRoot', 'members', ]); -const MEMBERS_FIELDS = new Set(['desktop', 'dataMigrator', 'cli']); +const MEMBERS_FIELDS = new Set(['desktop', 'cli']); const COMMON_MEMBER_FIELDS = new Set(['displayNameKey', 'binaryName']); const BUNDLED_MEMBER_FIELDS = new Set([...COMMON_MEMBER_FIELDS, 'bundleId']); @@ -192,7 +192,7 @@ function ownedLocaleFile(localeRoot, locale) { function validateMember(raw, member) { const owner = `members.${member}`; const value = requireObject(raw, owner); - const bundled = member === 'desktop' || member === 'dataMigrator'; + const bundled = member === 'desktop'; rejectUnknownFields(value, bundled ? BUNDLED_MEMBER_FIELDS : COMMON_MEMBER_FIELDS, owner); const result = { displayNameKey: requiredString(value.displayNameKey, `${owner}.displayNameKey`), @@ -235,8 +235,8 @@ function loadProductNames(rootDir, localeRoot, displayNameKeys) { } export function resolveProductDefinition({ rootDir, productConfig, member }) { - if (!['desktop', 'dataMigrator', 'cli'].includes(member)) { - fail('invalid_member', `Unsupported product member: ${member}`, 'Use desktop, dataMigrator, or cli.'); + if (!['desktop', 'cli'].includes(member)) { + fail('invalid_member', `Unsupported product member: ${member}`, 'Use desktop or cli.'); } const canonicalRoot = realpathSync.native(resolve(rootDir)); const defaultPath = realpathSync.native(join(canonicalRoot, 'products', 'openbitfun', 'product.jsonc')); @@ -262,7 +262,6 @@ export function resolveProductDefinition({ rootDir, productConfig, member }) { rejectUnknownFields(members, MEMBERS_FIELDS, 'members'); const normalizedMembers = { desktop: validateMember(members.desktop, 'desktop'), - dataMigrator: validateMember(members.dataMigrator, 'dataMigrator'), cli: validateMember(members.cli, 'cli'), }; const locales = loadProductNames( @@ -270,7 +269,6 @@ export function resolveProductDefinition({ rootDir, productConfig, member }) { localeRoot, [ normalizedMembers.desktop.displayNameKey, - normalizedMembers.dataMigrator.displayNameKey, normalizedMembers.cli.displayNameKey, ], ); @@ -281,10 +279,6 @@ export function resolveProductDefinition({ rootDir, productConfig, member }) { productId, dataNamespace, member, - memberBinaryNames: { - desktop: normalizedMembers.desktop.binaryName, - dataMigrator: normalizedMembers.dataMigrator.binaryName, - }, displayNameKey: selected.displayNameKey, binaryName: selected.binaryName, localeDigest: locales.digest, diff --git a/scripts/product-customization/resolver.test.mjs b/scripts/product-customization/resolver.test.mjs index 79eae41705..4c2283601c 100644 --- a/scripts/product-customization/resolver.test.mjs +++ b/scripts/product-customization/resolver.test.mjs @@ -12,7 +12,6 @@ const ACME = join(ROOT, 'products', 'fixtures', 'acme', 'product.jsonc'); test('default and custom members resolve through one deterministic contract', () => { const openbitfun = resolveProductDefinition({ rootDir: ROOT, member: 'desktop' }); const desktop = resolveProductDefinition({ rootDir: ROOT, productConfig: ACME, member: 'desktop' }); - const dataMigrator = resolveProductDefinition({ rootDir: ROOT, productConfig: ACME, member: 'dataMigrator' }); const cli = resolveProductDefinition({ rootDir: ROOT, productConfig: ACME, member: 'cli' }); assert.equal(openbitfun.assembly.productId, 'openbitfun'); @@ -20,16 +19,9 @@ test('default and custom members resolve through one deterministic contract', () assert.equal(openbitfun.assembly.binaryName, 'openbitfun-desktop'); assert.equal(openbitfun.assembly.bundleId, 'com.openbitfun.desktop'); assert.equal(desktop.assembly.bundleId, 'com.acme.desktop'); - assert.equal(dataMigrator.assembly.binaryName, 'acme-data-migrator'); - assert.equal(dataMigrator.assembly.bundleId, 'com.acme.data-migrator'); - assert.deepEqual(dataMigrator.assembly.memberBinaryNames, { - desktop: 'acme-desktop', - dataMigrator: 'acme-data-migrator', - }); assert.equal(cli.assembly.binaryName, 'acme'); assert.equal(cli.assembly.bundleId, undefined); assert.notEqual(desktop.assembly.assemblyDigest, cli.assembly.assemblyDigest); - assert.notEqual(desktop.assembly.assemblyDigest, dataMigrator.assembly.assemblyDigest); assert.equal( resolveProductDefinition({ rootDir: ROOT, productConfig: ACME, member: 'desktop' }) .assembly.assemblyDigest, diff --git a/scripts/product-identity-audit.mjs b/scripts/product-identity-audit.mjs index 84c2253364..ec8d0873aa 100644 --- a/scripts/product-identity-audit.mjs +++ b/scripts/product-identity-audit.mjs @@ -21,10 +21,6 @@ const productIdentityOwner = 'src/crates/contracts/core-types/src/product_identi const retiredIdentityDataBoundaryFiles = new Set([ 'OPENBITFUN_LEGACY_DATA_MIGRATION_IMPLEMENTATION_PLAN.md', 'OPENBITFUN_LEGACY_DATA_MIGRATION_INVENTORY.md', - 'src/apps/desktop/src/api/legacy_migration_api.rs', - 'src/web-ui/src/locales/en-US/settings/legacy-migration.json', - 'src/web-ui/src/locales/zh-CN/settings/legacy-migration.json', - 'src/web-ui/src/locales/zh-TW/settings/legacy-migration.json', 'deploy/openbitfun-host/README.md', 'deploy/openbitfun-host/migrate-market-data-v1.py', 'src/apps/relay-server/README.md', @@ -39,6 +35,7 @@ const retiredIdentityDataBoundaryPrefixes = Object.freeze([ 'src/apps/data-migrator/', 'src/crates/assembly/core/src/legacy_migration/', 'src/crates/services/legacy-migration/', + 'src/crates/services/legacy-migration-adapters/', ]); const noncanonicalIdentityDataBoundaryFiles = new Set([ 'OPENBITFUN_LEGACY_DATA_MIGRATION_INVENTORY.md', diff --git a/scripts/product-identity-audit.test.mjs b/scripts/product-identity-audit.test.mjs index 6d11658599..0a32a89540 100644 --- a/scripts/product-identity-audit.test.mjs +++ b/scripts/product-identity-audit.test.mjs @@ -106,10 +106,6 @@ test('allows only the exact legacy data-directory ignore entry', () => { test('limits retired identity data to the one-time production migration boundary', () => { for (const file of [ - 'src/apps/desktop/src/api/legacy_migration_api.rs', - 'src/web-ui/src/locales/en-US/settings/legacy-migration.json', - 'src/web-ui/src/locales/zh-CN/settings/legacy-migration.json', - 'src/web-ui/src/locales/zh-TW/settings/legacy-migration.json', ]) { assert.deepEqual(violationsFor(retiredName, file), []); } diff --git a/src/apps/data-migrator/AGENTS.md b/src/apps/data-migrator/AGENTS.md index 55f856171d..2128eca3de 100644 --- a/src/apps/data-migrator/AGENTS.md +++ b/src/apps/data-migrator/AGENTS.md @@ -1,44 +1,42 @@ # Data Migrator Agent Guide -Scope: this guide applies to `src/apps/data-migrator`. - -This app is the offline, local-only host for importing legacy BitFun data. It -must remain a separate executable and WebView identity from Desktop. - -## Guardrails - -- Select `DeliveryProfile::DataMigrator` and only the Core - `legacy-migration` feature. Do not add `product-full`, Agent Runtime, - plugin runtime, normal session startup, updater, shell, or frontend - filesystem capabilities. -- Accept only the handoff `run_id` on the command line. Derive the request path - from `MigrationRoots`; never accept a request or executable path from UI or - command-line input. -- Keep all filesystem, process, credential, and restart work in Rust. The UI - may call only the typed commands registered in `src/lib.rs`. -- Report domain, phase, and counts. Do not invent progress percentages or emit - secrets, user content, credential values, or absolute paths in errors. -- Cancellation is advisory and may be honored only at engine-declared safe - boundaries. Closing during execution requests cancellation and keeps the - window open until a safe boundary. -- The migrator never updates itself. Resolve Desktop as a fixed-name sibling - binary using product-definition projections and the trusted installation - resolver; do not accept executable paths from handoff input. - -## Verification +Scope: src/apps/data-migrator. This is a separately versioned, offline local tool. +Read README.md for its user-facing contract. + +## Boundaries + +- Launch with no arguments. Own directory selection, discovery, durable task + recovery and completion; never depend on a Desktop request or restart Desktop. +- Keep preferences under the tool's Tauri app-config identity. Migration run + journals/backups remain under the destination data directory for recovery. + Do not write Desktop onboarding/reminder state. +- Consume config-contracts, shared storage services and legacy-migration-adapters. + Do not depend on Core, product assembly, Agent execution, Web UI, updater, + plugin lifecycle or a main-application sibling executable. +- Use typed Rust commands for filesystem/process operations. Cancellation is + advisory and is honored only at engine-declared safe boundaries. +- Reject unsupported data formats and unsafe directory overlaps. Preserve source + data, old plans/reports, snapshots, journals, backups and owner conflict policies. +- The UI uses public @openbitfun/design-tokens and @openbitfun/theme-openbitfun + exports bundled in ui/generated/design-system.css. Regenerate through + pnpm run data-migrator:theme:generate. Direct Cargo builds are offline; + Desktop dev/build must never generate or build migrator assets. +- ui/theme.js selects system scheme/contrast before paint. Workflow translations + are app-owned; do not import Web UI catalogs. No mocks or browser automation + for visual verification. +- Tool version lives in Cargo.toml and tauri.conf.json. Packaging/signing uses + the independent Data Migrator workflow and data-migrator-v tags. + +## Focused verification ```bash -cargo test -p openbitfun-data-migrator +cargo test -p openbitfun-data-migrator -p openbitfun-legacy-migration-adapters -p openbitfun-legacy-migration --lib +cargo test -p openbitfun-legacy-migration --test migration_engine_contracts node --test scripts/data-migrator-tauri-build.test.mjs -node --test scripts/desktop-dev-migration.test.mjs +node --check src/apps/data-migrator/ui/app.js +pnpm run theme:color-audit:all ``` -Completion always returns to Desktop. Debug builds launched through `desktop:dev` -or `desktop:preview:debug` ask that launcher to restart via its private temporary -handoff directory, preserving the frontend server and development lifecycle. -Builds without that channel restart the trusted sibling Desktop executable. -Keep this developer-only channel out of persisted migration and remote protocols. - -Run `pnpm run check:core-boundaries` when dependencies or delivery-profile -selection change. Packaging, signing, and UI interaction are separate explicit -verification steps. +Run pnpm run check:core-boundaries for dependency ownership changes and +pnpm run check:github-config for release workflow changes. Platform packaging +and native visual checks are separate from these contract checks. diff --git a/src/apps/data-migrator/Cargo.toml b/src/apps/data-migrator/Cargo.toml index 8c112cad8d..52b1c3fd20 100644 --- a/src/apps/data-migrator/Cargo.toml +++ b/src/apps/data-migrator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openbitfun-data-migrator" -version.workspace = true +version = "0.1.0" authors.workspace = true edition.workspace = true description = "OpenBitFun offline legacy data migrator" @@ -17,18 +17,17 @@ path = "src/main.rs" tauri-build = { workspace = true } [dependencies] -openbitfun-core = { path = "../../crates/assembly/core", features = ["legacy-migration"] } +openbitfun-legacy-migration-adapters = { path = "../../crates/services/legacy-migration-adapters" } openbitfun-core-types = { path = "../../crates/contracts/core-types" } openbitfun-legacy-migration = { path = "../../crates/services/legacy-migration" } -openbitfun-product-capabilities = { path = "../../crates/assembly/product-capabilities" } openbitfun-product-domains = { path = "../../crates/contracts/product-domains", features = ["legacy-migration"] } serde = { workspace = true } serde_json = { workspace = true } tauri = { workspace = true } +uuid = { workspace = true } [dev-dependencies] tempfile = { workspace = true } -uuid = { workspace = true } [lints] workspace = true diff --git a/src/apps/data-migrator/README.md b/src/apps/data-migrator/README.md new file mode 100644 index 0000000000..b821573866 --- /dev/null +++ b/src/apps/data-migrator/README.md @@ -0,0 +1,130 @@ +# OpenBitFun Data Migrator + +[中文](README.zh-CN.md) + +A separate, optional desktop utility for importing old **BitFun** data into +**OpenBitFun**. It runs without installing or opening the main application, has +its own window and settings, and never starts or restarts Desktop. OpenBitFun +does not bundle, download, or launch it automatically. + +## Download and run + +Look for **OpenBitFun Data Migrator** releases with a `data-migrator-v*` tag on +the [release page](https://github.com/GCWing/OpenBitFun/releases?q=data-migrator-v&expanded=true). +These releases have their own version and assets; the main application's +installer does not contain the tool. If no migrator release is listed, build +from source using the commands below. + +| Platform | Download | Launch | +| --- | --- | --- | +| Windows x64 | `openbitfun-data-migrator-v-windows-x64.zip` | Extract, then double-click `openbitfun-data-migrator.exe` | +| macOS Apple Silicon | `openbitfun-data-migrator-v-macos-arm64.dmg` | Open the DMG and its Data Migrator app | +| macOS Intel | `openbitfun-data-migrator-v-macos-x64.dmg` | Open the DMG and its Data Migrator app | +| Linux x64 | `openbitfun-data-migrator-v-linux-x64.AppImage` | Make executable and launch in a desktop session | + +Windows needs the Microsoft Edge WebView2 runtime. macOS uses the system WebView; +Linux packages are built on Ubuntu 22.04. No login or network connection is +needed for migration. ARM Windows/Linux packages are not currently produced. + +1. Close BitFun, OpenBitFun, their CLI instances, and background data writers. +2. Open Data Migrator. Check the **source and destination** directories. All + four locations on each side can be edited; apply changes before scanning. +3. Select the data groups, scan, then run the preflight plan. +4. Review the destination and conflicts, then start migration. If known writers + remain open, the tool waits for them to stop; it does not terminate them. +5. Read the report. Sign in again or repair paths where indicated, close the + tool, and open OpenBitFun yourself. + +The UI uses the shared design-system tokens bundled offline, follows the system +light/dark/high-contrast setting, and offers English, Simplified Chinese, and +Traditional Chinese. + +## Data and compatibility + +The declared source range is BitFun `>=0.2.0,<1.0.0`; the archived integration +fixture is **0.2.19**. This is format-based support, not a claim that every old +release has been tested. The source must pass the probe and selected domain +validators. Unsupported or corrupt data is kept and reported. + +The destination is OpenBitFun: configuration schema **1**, workspace registry +format **1**, coordination database schema **2**, and the session, memory, +extension, and connection formats accepted by the shared storage owners in +this source revision. Unknown product/configuration schemas and newer SQLite +or session schemas fail validation. A future storage format requires a new +migrator release; matching application and tool version numbers is unnecessary. +Custom branded products are not supported by this tool. + +Migration covers settings and credentials; user Agents, Skills and MiniApps; +workspaces, sessions and task records; memories; and local connection/device +records. Existing destination values take priority or conflicts are preserved +under a new identity according to the domain policy. Runtime caches, locks, +process discovery files, built-in executable content and request traces are +excluded. Credentials that cannot be decrypted on the destination require +sign-in again. + +Source data is never automatically deleted. Writes use consistent snapshots, +staging, validation, backups, a migration lock and atomic replacement. Keep +both applications closed until the run finishes. Cancellation and window close +requests wait for an engine-declared safe boundary; already verified domains +may remain imported. + +## Resume and diagnose + +Plans, journals, reports, backups and staging live under: + +```text +/data/migrations/bitfun-to-openbitfun/runs// +``` + +Reopen the tool, select the original directories, and use **Saved migration +tasks → Review / resume task**. Recovery requires a valid plan and unchanged +source fingerprint and resumes through the journal; it does not expire after +ten minutes. Completed reports can be reopened. New scans create new tasks and +never replace earlier journals. Old handoff-based plans remain readable even +if their `request.json` has expired; select the original locations before +resuming them. Unreadable files are not deleted or reset. + +The tool remembers selected locations in its own `com.openbitfun.data-migrator` +application configuration directory. It does not write main-app onboarding or +reminder preferences. **Export failure diagnostics** writes a sanitized file +containing result codes and journal phases; full local reports and backups +can contain sensitive data and should stay private. + +This tool only operates on files accessible on the computer where it runs. +Remote workspace execution, remote control, Peer Device Mode and Detached +Dispatch are not execution surfaces for it. Run it on the data-owning computer; +importing stored connection records does not connect to or migrate a remote host. + +## Build and release + +From the repository root with Rust, Node, pnpm and the platform's Tauri build prerequisites: + +```bash +pnpm install +pnpm run data-migrator:dev # independent window; no Desktop or dev server +pnpm run data-migrator:build # independent release bundle +cargo build -p openbitfun-data-migrator --bin openbitfun-data-migrator +``` + +Direct Cargo builds embed the committed UI and design-system CSS. After changing +the token/theme owners run `pnpm run data-migrator:theme:generate`; the packaging +entry does this automatically. Desktop development/build commands do not build +the migrator. Shared Rust crates remain in the same source workspace to preserve +storage compatibility; there is no dependency on the main application's Core, +runtime assembly, Web UI, installer or updater. + +The tool version is maintained in its own `Cargo.toml` and `tauri.conf.json`. +The **Data Migrator Package** workflow builds four platform artifacts manually +or on `data-migrator-v` tags. Tag builds require the separate +`DATA_MIGRATOR_SIGNING_PRIVATE_KEY`, `DATA_MIGRATOR_SIGNING_PRIVATE_KEY_PASSWORD` +and `DATA_MIGRATOR_SIGNING_PUBKEY` secrets, verify checksums/signatures, and create +a **draft** release for review. Manual workflow runs only upload CI artifacts. +Publishing migrator releases does not start main-app packaging or update feeds. + +Each asset has a SHA-256 sidecar and a base64-encoded minisign `.sig`; the +release also carries `SHA256SUMS` and `data-migrator.minisign.pub`. Verify the key +against the maintainer's trusted key before checking signatures. Detached +signatures are distinct from Apple/Authenticode platform signing; the workflow +does not currently configure those certificates or macOS notarization. + +Focused checks and architecture rules are in [AGENTS.md](AGENTS.md). diff --git a/src/apps/data-migrator/README.zh-CN.md b/src/apps/data-migrator/README.zh-CN.md new file mode 100644 index 0000000000..4ba91993b9 --- /dev/null +++ b/src/apps/data-migrator/README.zh-CN.md @@ -0,0 +1,100 @@ +# OpenBitFun 独立数据迁移器 + +[English](README.md) + +这是一个可选的独立桌面工具,用于将旧版 **BitFun** 数据导入 **OpenBitFun**。 +无需安装或启动主应用;工具有自己的窗口、版本和配置,完成后只关闭自身。 +主应用不会捆绑、自动下载、自动启动迁移器,也不会因为旧版数据而阻止正常启动。 + +## 下载和使用 + +在 [GitHub Releases](https://github.com/GCWing/OpenBitFun/releases?q=data-migrator-v&expanded=true) +寻找 **OpenBitFun Data Migrator**、标签以 `data-migrator-v` 开头的独立发布。 +迁移器不在主应用安装包内;如果尚未列出独立发布,请按下文从源码构建。 + +| 系统 | 下载文件 | 启动方式 | +| --- | --- | --- | +| Windows x64 | `openbitfun-data-migrator-v<版本>-windows-x64.zip` | 解压后双击 `openbitfun-data-migrator.exe` | +| macOS Apple Silicon | `openbitfun-data-migrator-v<版本>-macos-arm64.dmg` | 打开 DMG 中的迁移器 | +| macOS Intel | `openbitfun-data-migrator-v<版本>-macos-x64.dmg` | 打开 DMG 中的迁移器 | +| Linux x64 | `openbitfun-data-migrator-v<版本>-linux-x64.AppImage` | 添加可执行权限,在桌面会话中启动 | + +Windows 需要 Microsoft Edge WebView2;macOS 使用系统 WebView,Linux 包以 Ubuntu 22.04 +为构建基线。迁移不需要登录或联网,目前不产出 Windows/Linux ARM 安装包。 + +1. 关闭 BitFun、OpenBitFun、CLI 实例及其后台数据写入进程。 +2. 启动迁移器,检查**来源和目标目录**。两侧各有设置与数据、主目录数据、Skills、SSH + 四个位置;如需修改,先点击“使用这些目录”。 +3. 选择迁移范围,扫描数据,再运行预检。 +4. 确认目标、范围和冲突后开始迁移。发现已知写入进程时会等待其退出,不会强制终止进程。 +5. 查看结果,根据提示重新登录或修复路径,关闭迁移器,再自行打开 OpenBitFun。 + +界面使用离线打包的共享设计系统,跟随系统深浅色和高对比度设置,并提供中英文切换。 + +## 支持范围与数据保护 + +声明支持的来源是 BitFun `>=0.2.0,<1.0.0`,仓库保存的集成验证样本是 **0.2.19**。 +支持以实际数据格式和各领域校验为准,不代表已验证范围内的每个旧版本。 +未知格式、损坏数据会明确报告并保留。 + +目标是 OpenBitFun:配置 schema **1**、工作区格式 **1**、任务协调数据库 schema **2**, +以及此源码版本共享存储模块支持的会话、记忆、扩展和连接格式。未知产品/配置格式以及 +超出支持范围的数据库、会话版本会被拒绝。未来数据格式变更需要发布新的迁移器, +工具版本无需与主应用版本相同;此工具不支持定制品牌产品的数据。 + +可迁移设置与凭据、用户 Agents/Skills/MiniApps、工作区与会话及任务记录、记忆、 +本机保存的远程连接与设备记录。已有目标值优先,部分冲突会按领域规则保留双方。 +缓存、锁、进程发现文件、内置可执行内容和请求追踪不迁移;不可解密的凭据需重新登录。 + +来源不会被自动删除。写入使用一致性快照、暂存、校验、备份、迁移锁和原子替换。 +迁移期间请保持相关应用关闭;取消或关闭窗口会等到安全边界,已验证完成的领域可能已导入。 + +## 中断恢复与诊断 + +任务保存在: + +```text +<目标设置与数据目录>/data/migrations/bitfun-to-openbitfun/runs/<任务 ID>/ +``` + +重新打开迁移器,选择原来的目录,在“历史迁移任务”中查看或恢复。恢复会校验计划、 +来源指纹和原目录,并沿用日志,不受原来十分钟交接请求有效期限制。 +已完成任务仍可查看报告;新扫描会创建新任务,不会覆盖旧日志。 +旧版交接流程留下的计划与日志仍可读取,即使 `request.json` 已过期;恢复前须选择原目录。 +无法读取的任务文件不会被删除或重置。 + +迁移器只在自己的 `com.openbitfun.data-migrator` 配置目录中记住所选位置,不写主应用的 +引导或提醒状态。“导出失败诊断”生成包含结果码和执行阶段的去敏文件。 +完整本地报告与备份可能含敏感信息,请保留在本机。 + +迁移器只操作运行电脑可访问的文件,不接入远程工作区执行、远程控制、Peer Device Mode +或 Detached Dispatch。请在数据所在电脑上运行;迁移连接记录并不连接或迁移远端主机。 + +## 开发与独立发布 + +安装 Rust、Node、pnpm 和对应系统的 Tauri 构建依赖后,在仓库根目录执行: + +```bash +pnpm install +pnpm run data-migrator:dev # 独立窗口,无需主应用或开发服务器 +pnpm run data-migrator:build # 独立发行包 +cargo build -p openbitfun-data-migrator --bin openbitfun-data-migrator +``` + +直接 Cargo 构建使用已提交的静态 UI 和设计系统 CSS。修改主题源后运行 +`pnpm run data-migrator:theme:generate`;独立打包入口会自动生成。 +主应用开发和构建不再构建迁移器。两者仍在同一源码工作区共享稳定的数据契约和存储模块, +以保证格式一致,但迁移器不依赖主应用 Core、运行时组装、Web UI、安装器或更新器。 + +版本由迁移器自己的 `Cargo.toml` 和 `tauri.conf.json` 维护。**Data Migrator Package** +工作流支持手动构建,或通过 `data-migrator-v<版本>` 标签生成独立发布草稿。 +标签发布使用专用的 `DATA_MIGRATOR_SIGNING_PRIVATE_KEY`、 +`DATA_MIGRATOR_SIGNING_PRIVATE_KEY_PASSWORD`、`DATA_MIGRATOR_SIGNING_PUBKEY`, +完成签名和校验后创建草稿,审核后再发布。手动运行只生成 CI 构建产物。 +迁移器发布不会触发主应用打包或更新源。 + +每个产物带 SHA-256 校验文件和 base64 编码的 minisign `.sig`,同时提供 +`SHA256SUMS` 与 `data-migrator.minisign.pub`。验证签名前应通过可信渠道确认公钥。 +独立文件签名不等于 Apple/Authenticode 系统代码签名;当前工作流尚未配置这些证书及 macOS 公证。 + +开发约束和针对性检查见 [AGENTS.md](AGENTS.md)。 diff --git a/src/apps/data-migrator/RELEASE.md b/src/apps/data-migrator/RELEASE.md new file mode 100644 index 0000000000..68f4fc1fc1 --- /dev/null +++ b/src/apps/data-migrator/RELEASE.md @@ -0,0 +1,16 @@ +Optional, independently downloaded BitFun → OpenBitFun data migrator. + +Extract the Windows ZIP and double-click the migrator, or open the macOS/Linux +package. Close all BitFun/OpenBitFun data writers first. Review source, +destination and the preflight plan before importing. Source data is retained; +interrupted runs can be resumed from Saved migration tasks. + +Declared source range: BitFun >=0.2.0,<1.0.0. Archived integration fixture: 0.2.19. +The tool uses explicit storage-format validation and rejects unsupported data. +No main application installation, login, network or restart handoff is required. + +See the README files at this release tag for compatibility, recovery, build and +signature verification details. Every package carries a SHA-256 sidecar and a +verified detached minisign signature. Platform code signing/notarization is not +configured by this workflow. This draft requires maintainer review and platform +launch checks before publication. diff --git a/src/apps/data-migrator/icons/openbitfun-app-icon.icns b/src/apps/data-migrator/icons/openbitfun-app-icon.icns new file mode 100644 index 0000000000..976eec4c27 Binary files /dev/null and b/src/apps/data-migrator/icons/openbitfun-app-icon.icns differ diff --git a/src/apps/data-migrator/icons/openbitfun-app-icon.ico b/src/apps/data-migrator/icons/openbitfun-app-icon.ico new file mode 100644 index 0000000000..2d88a5a677 Binary files /dev/null and b/src/apps/data-migrator/icons/openbitfun-app-icon.ico differ diff --git a/src/apps/data-migrator/icons/openbitfun-app-icon.png b/src/apps/data-migrator/icons/openbitfun-app-icon.png new file mode 100644 index 0000000000..2cfdd17e94 Binary files /dev/null and b/src/apps/data-migrator/icons/openbitfun-app-icon.png differ diff --git a/src/apps/data-migrator/src/app_state.rs b/src/apps/data-migrator/src/app_state.rs index c4d6a04d3f..1b2eb172b0 100644 --- a/src/apps/data-migrator/src/app_state.rs +++ b/src/apps/data-migrator/src/app_state.rs @@ -1,36 +1,23 @@ -use openbitfun_core::legacy_migration::adapters_for_groups; -use openbitfun_core_types::product_identity::product_id; use openbitfun_legacy_migration::{ - blocking_writer_processes_for_product, export_failure_diagnostics, launch_trusted_executable, - probe_legacy_source, CancellationToken, HandoffDisposition, HandoffStore, LegacyMigrationError, - LegacyMigrationResult, MigrationEngine, MigrationLayout, MigrationOnboardingStore, - MigrationRoots, NoCrashInjection, ProbeLimits, TrustedInstallationResolver, WriterProcess, + atomic_write_json, blocking_writer_processes, export_failure_diagnostics, list_tasks, + load_task, probe_legacy_source, save_task, CancellationToken, LegacyMigrationError, + LegacyMigrationResult, MigrationEngine, MigrationLayout, MigrationRoots, NoCrashInjection, + ProbeLimits, SavedMigrationTask, WriterProcess, }; -use openbitfun_product_capabilities::{product_assembly_plan_for_profile, DeliveryProfile}; +use openbitfun_legacy_migration_adapters::adapters_for_groups; use openbitfun_product_domains::legacy_migration::{ FindingSeverity, LegacySourceDescriptor, MigrationPhase, MigrationPlan, MigrationProgressEvent, - MigrationPromptChoice, MigrationRunReport, MigrationRunStatus, MigrationSelection, - MigratorHandoffRequest, MigratorProtocolCapabilities, MigratorRequestMode, ScanFinding, - CURRENT_MIGRATION_FORMAT_VERSION, + MigrationRunReport, MigrationRunStatus, MigrationSelection, ScanFinding, }; use serde::Serialize; -use std::ffi::OsStr; -use std::path::Path; +use std::path::PathBuf; use std::sync::{Arc, Mutex, MutexGuard}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; -const RELEASE_CHANNEL: &str = match option_env!("OPENBITFUN_RELEASE_CHANNEL") { - Some(value) => value, - None => "stable", -}; -const DESKTOP_BINARY_NAME: &str = match option_env!("OPENBITFUN_DESKTOP_BINARY_NAME") { - Some(value) => value, - None => "openbitfun-desktop", -}; -const DATA_MIGRATOR_BINARY_NAME: &str = match option_env!("OPENBITFUN_DATA_MIGRATOR_BINARY_NAME") { - Some(value) => value, - None => "openbitfun-data-migrator", -}; +#[derive(Debug, Clone)] +struct TaskRequest { + run_id: String, +} #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] @@ -82,9 +69,14 @@ impl CommandError { "This legacy BitFun data format is not supported by this migrator.", false, ), + LegacyMigrationError::UnsupportedTarget(_) => Self::new( + "unsupported_target", + "The destination data format is not supported. Use a compatible migrator or an empty destination.", + true, + ), LegacyMigrationError::InvalidRequest(_) => Self::new( - "invalid_handoff", - "The migration handoff could not be authenticated or has expired.", + "invalid_task", + "The selected migration task or data locations are invalid.", false, ), LegacyMigrationError::InvalidPlan(_) => Self::new( @@ -167,10 +159,9 @@ impl CommandError { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MigratorView { - pub delivery_profile: String, - pub restart_desktop_on_finish: bool, - pub protocol: MigratorProtocolCapabilities, - pub mode: MigratorRequestMode, + pub tool_version: String, + pub locations: MigrationRoots, + pub saved_tasks: Vec, pub source: Option, pub selection: MigrationSelection, pub findings: Vec, @@ -194,8 +185,10 @@ pub(crate) struct DiagnosticsExportView { #[derive(Debug)] struct MigratorSession { roots: MigrationRoots, - request: MigratorHandoffRequest, - disposition: HandoffDisposition, + request: TaskRequest, + recovery: bool, + saved_tasks: Vec, + settings_path: PathBuf, source: Option, selection: MigrationSelection, findings: Vec, @@ -215,80 +208,111 @@ pub(crate) struct MigratorCoordinator { } impl MigratorCoordinator { - pub(crate) fn bootstrap(run_id: &str) -> LegacyMigrationResult { - Self::bootstrap_with( - run_id, - MigrationRoots::resolve_current_user()?, - product_id(), - RELEASE_CHANNEL, - ) - } - - fn bootstrap_with( - run_id: &str, - roots: MigrationRoots, - expected_product_id: &str, - expected_release_channel: &str, - ) -> LegacyMigrationResult { - let product_plan = product_assembly_plan_for_profile(DeliveryProfile::DataMigrator); - if !product_plan.capability_set().ids().is_empty() - || !product_plan.capability_assembly().agent_ids().is_empty() - || !product_plan.feature_groups().is_empty() - { - return Err(LegacyMigrationError::InvalidRequest( - "data migrator delivery profile unexpectedly selected runtime capabilities" - .to_string(), - )); + pub(crate) fn bootstrap(settings_path: PathBuf) -> LegacyMigrationResult { + let defaults = MigrationRoots::current_user_locations()?; + let layout = MigrationLayout::new(&defaults, "preferences"); + let saved = layout.read_json::(&settings_path); + let (roots, error) = match saved { + Ok(Some(roots)) => (roots, None), + Ok(None) => (defaults, None), + Err(error) => (defaults, Some(CommandError::from_legacy(&error))), + }; + let coordinator = Self::bootstrap_with(roots, settings_path); + if let Some(error) = error { + coordinator.lock().error = Some(error); } + Ok(coordinator) + } - let store = HandoffStore::new(roots.clone(), expected_product_id, expected_release_channel); - let handoff = store.load_request(run_id, now_ms())?; - let request = handoff.request().clone(); - let source = probe_bound_source(&roots, &request)?; - let plan = store.load_authorized_plan(&handoff)?; - let report = handoff - .layout() - .read_json::(&handoff.layout().report_path())?; - let selection = plan - .as_ref() - .map(|plan| plan.selection.clone()) - .unwrap_or_else(|| request.selection.clone()); - let findings = plan - .as_ref() - .map(|plan| plan.findings.clone()) - .unwrap_or_default(); - let status = report - .as_ref() - .map(|report| report.status) - .unwrap_or_else(|| { - if plan.is_some() { - MigrationRunStatus::Planned - } else if source.is_some() { + fn bootstrap_with(roots: MigrationRoots, settings_path: PathBuf) -> Self { + let probe = crate::locations::validate(&roots) + .and_then(|_| probe_legacy_source(&roots, ProbeLimits::default())); + let (source, mut error) = match probe { + Ok(source) => (source, None), + Err(error) => (None, Some(CommandError::from_legacy(&error))), + }; + let saved_tasks = match list_tasks(&roots) { + Ok(tasks) => tasks, + Err(failure) => { + error = Some(CommandError::from_legacy(&failure)); + Vec::new() + } + }; + Self { + session: Arc::new(Mutex::new(MigratorSession { + roots, + settings_path, + saved_tasks, + request: TaskRequest { + run_id: uuid::Uuid::new_v4().to_string(), + }, + recovery: false, + status: if source.is_some() { MigrationRunStatus::Discovered } else { MigrationRunStatus::default() - } - }); - let blockers = writer_processes(request.caller_process_id)?; - - Ok(Self { - session: Arc::new(Mutex::new(MigratorSession { - roots, - request, - disposition: handoff.disposition(), + }, source, - selection, - findings, - plan, - report, + error, + selection: MigrationSelection::all(), + findings: Vec::new(), + plan: None, + report: None, progress: None, - blockers, - status, + blockers: Vec::new(), running: false, - error: None, cancellation: CancellationToken::default(), })), - }) + } + } + + pub(crate) fn set_locations( + &self, + roots: MigrationRoots, + ) -> Result { + let mut session = self.lock(); + if session.running { + return Err(CommandError::operation_in_progress()); + } + crate::locations::validate(&roots).map_err(|error| CommandError::from_legacy(&error))?; + atomic_write_json(&session.settings_path, &roots) + .map_err(|error| CommandError::from_legacy(&error))?; + let replacement = Self::bootstrap_with(roots, session.settings_path.clone()); + std::mem::swap(&mut *session, &mut *replacement.lock()); + Ok(snapshot(&session)) + } + + pub(crate) fn new_task(&self) -> Result { + let mut session = self.lock(); + if session.running { + return Err(CommandError::operation_in_progress()); + } + let replacement = + Self::bootstrap_with(session.roots.clone(), session.settings_path.clone()); + std::mem::swap(&mut *session, &mut *replacement.lock()); + Ok(snapshot(&session)) + } + + pub(crate) fn resume_task(&self, run_id: &str) -> Result { + let mut session = self.lock(); + if session.running { + return Err(CommandError::operation_in_progress()); + } + let (plan, report) = + load_task(&session.roots, run_id).map_err(|error| CommandError::from_legacy(&error))?; + session.request.run_id = run_id.to_string(); + session.selection = plan.selection.clone(); + session.findings = plan.findings.clone(); + session.status = report + .as_ref() + .map(|report| report.status) + .unwrap_or(MigrationRunStatus::Planned); + session.plan = Some(plan); + session.report = report; + session.progress = None; + session.recovery = true; + session.error = None; + Ok(snapshot(&session)) } pub(crate) fn snapshot(&self) -> MigratorView { @@ -333,12 +357,12 @@ impl MigratorCoordinator { fn scan_background( &self, roots: MigrationRoots, - request: MigratorHandoffRequest, + _request: TaskRequest, selection: MigrationSelection, cancellation: CancellationToken, ) { let result = (|| { - let source = probe_bound_source(&roots, &request)?.ok_or_else(|| { + let source = probe_legacy_source(&roots, ProbeLimits::default())?.ok_or_else(|| { LegacyMigrationError::UnsupportedSource( "no supported legacy BitFun data was discovered".to_string(), ) @@ -395,24 +419,25 @@ impl MigratorCoordinator { fn prepare_background( &self, roots: MigrationRoots, - request: MigratorHandoffRequest, + request: TaskRequest, selection: MigrationSelection, cancellation: CancellationToken, ) { let result = (|| { - let source = probe_bound_source(&roots, &request)?.ok_or_else(|| { + let source = probe_legacy_source(&roots, ProbeLimits::default())?.ok_or_else(|| { LegacyMigrationError::UnsupportedSource( "no supported legacy BitFun data was discovered".to_string(), ) })?; - let engine = migration_engine(roots, &selection)?; + let engine = migration_engine(roots.clone(), &selection)?; let plan = engine.plan_with_run_id( &source, selection.clone(), request.run_id.clone(), &cancellation, )?; - let blockers = writer_processes(request.caller_process_id)?; + save_task(&roots, &plan)?; + let blockers = writer_processes()?; Ok::<_, LegacyMigrationError>((source, plan, blockers)) })(); @@ -424,6 +449,8 @@ impl MigratorCoordinator { session.selection = selection; session.findings = plan.findings.clone(); session.plan = Some(plan); + session.saved_tasks = + list_tasks(&session.roots).unwrap_or_else(|_| session.saved_tasks.clone()); session.report = None; session.blockers = blockers; session.status = MigrationRunStatus::Planned; @@ -445,8 +472,10 @@ impl MigratorCoordinator { } pub(crate) fn refresh_blockers(&self) -> Result { - let caller_process_id = self.lock().request.caller_process_id; - match writer_processes(caller_process_id) { + if self.is_running() { + return Err(CommandError::operation_in_progress()); + } + match writer_processes() { Ok(blockers) => { let mut session = self.lock(); session.blockers = blockers; @@ -480,13 +509,25 @@ impl MigratorCoordinator { true, )); } - let store = HandoffStore::new(session.roots.clone(), product_id(), RELEASE_CHANNEL); - let handoff = store - .load_request(&session.request.run_id, now_ms()) - .map_err(|error| CommandError::from_legacy(&error))?; - store - .authorize_plan(&handoff, &plan, now_ms()) + let (saved, _) = load_task(&session.roots, &session.request.run_id) .map_err(|error| CommandError::from_legacy(&error))?; + if saved != plan { + return Err(CommandError::new( + "stale_plan", + "Review the saved plan again before continuing.", + true, + )); + } + if matches!( + session.status, + MigrationRunStatus::Completed | MigrationRunStatus::CompletedWithWarnings + ) { + return Err(CommandError::new( + "task_completed", + "This task is already complete. Start a new scan to import other data.", + true, + )); + } session.cancellation = CancellationToken::default(); session.running = true; @@ -532,66 +573,29 @@ impl MigratorCoordinator { self.lock().running } - pub(crate) fn finish_and_restart( - &self, - choice: MigrationPromptChoice, - ) -> Result<(), CommandError> { - self.finish_with_restart(choice, || self.restart_desktop()) - } - - fn finish_with_restart( - &self, - choice: MigrationPromptChoice, - restart: impl FnOnce() -> LegacyMigrationResult<()>, - ) -> Result<(), CommandError> { + pub(crate) fn finish(&self) -> Result<(), CommandError> { if self.is_running() { return Err(CommandError::operation_in_progress()); } - if choice == MigrationPromptChoice::Unset { - return Err(CommandError::new( - "invalid_prompt_choice", - "Choose whether to migrate now, be reminded later, or stop reminders.", - true, - )); - } - if choice == MigrationPromptChoice::MigrateNow && self.lock().report.is_none() { - return Err(CommandError::new( - "migration_result_required", - "A completed or recoverable migration report is required before finishing.", - true, - )); - } - - let result = (|| { - self.persist_prompt_choice(choice)?; - restart() - })(); - if let Err(error) = result { - let mut session = self.lock(); - let command_error = self.finish_error_locked(&mut session, &error); - return Err(command_error); - } Ok(()) } - pub(crate) fn close_and_restart(&self) -> Result<(), CommandError> { - let choice = if self.lock().report.is_some() { - MigrationPromptChoice::MigrateNow - } else { - MigrationPromptChoice::RemindLater - }; - self.finish_and_restart(choice) - } - fn begin_operation( &self, selection: &MigrationSelection, - ) -> Result<(MigrationRoots, MigratorHandoffRequest, CancellationToken), CommandError> { + ) -> Result<(MigrationRoots, TaskRequest, CancellationToken), CommandError> { let mut session = self.lock(); if session.running { return Err(CommandError::operation_in_progress()); } - validate_selection(&session.request, selection)?; + validate_selection(selection)?; + // A changed scan/selection is a new task. Never overwrite an old journal. + session.request.run_id = uuid::Uuid::new_v4().to_string(); + session.recovery = false; + session.selection = selection.clone(); + session.plan = None; + session.report = None; + session.findings.clear(); session.cancellation = CancellationToken::default(); session.running = true; session.error = None; @@ -634,7 +638,7 @@ impl MigratorCoordinator { fn execute_background( &self, roots: MigrationRoots, - request: MigratorHandoffRequest, + _request: TaskRequest, plan: MigrationPlan, cancellation: CancellationToken, ) { @@ -643,7 +647,7 @@ impl MigratorCoordinator { self.finish_cancelled_before_execution(&plan); return; } - match writer_processes(request.caller_process_id) { + match writer_processes() { Ok(blockers) => { let done = blockers.is_empty(); let mut session = self.lock(); @@ -694,8 +698,8 @@ impl MigratorCoordinator { session.status = report.status; session.report = Some(report); session.error = None; - drop(session); - let _ = self.persist_prompt_choice(MigrationPromptChoice::MigrateNow); + session.saved_tasks = + list_tasks(&session.roots).unwrap_or_else(|_| session.saved_tasks.clone()); } Err(error) => { let layout = MigrationLayout::new(&roots, &plan.run_id); @@ -734,49 +738,6 @@ impl MigratorCoordinator { session.progress = Some(progress); } - fn persist_prompt_choice(&self, choice: MigrationPromptChoice) -> LegacyMigrationResult<()> { - let session = self.lock(); - let store = MigrationOnboardingStore::new(session.roots.clone()); - let request = &session.request; - let source = session.source.as_ref(); - let has_report = session.report.is_some(); - store.update(|state| { - state.format_version = CURRENT_MIGRATION_FORMAT_VERSION; - if let Some(source) = source { - state.source_fingerprint = source.source_fingerprint.clone(); - state.detected_at_ms.get_or_insert_with(now_ms); - } - state.choice = choice; - state.last_prompted_version = Some(env!("CARGO_PKG_VERSION").to_string()); - state.run_id = Some(request.run_id.clone()); - state.handled_run_id = Some(request.run_id.clone()); - if has_report { - state.last_report_run_id = Some(request.run_id.clone()); - } - })?; - Ok(()) - } - - fn restart_desktop(&self) -> LegacyMigrationResult<()> { - let run_id = self.lock().request.run_id.clone(); - #[cfg(debug_assertions)] - if let Some(directory) = std::env::var_os("OPENBITFUN_DEV_MIGRATION_DIR") { - return request_dev_restart(Path::new(&directory), &run_id); - } - let current = std::env::current_exe().map_err(|error| LegacyMigrationError::Io { - path: Path::new(DATA_MIGRATOR_BINARY_NAME).to_path_buf(), - source: error, - })?; - let executable = TrustedInstallationResolver::resolve_sibling( - ¤t, - DATA_MIGRATOR_BINARY_NAME, - DESKTOP_BINARY_NAME, - )?; - let arguments = [OsStr::new("--legacy-migration-run-id"), OsStr::new(&run_id)]; - launch_trusted_executable(&executable, &arguments)?; - Ok(()) - } - fn finish_error_locked( &self, session: &mut MigratorSession, @@ -806,46 +767,20 @@ impl MigratorCoordinator { } } -#[cfg(debug_assertions)] -fn request_dev_restart(directory: &Path, run_id: &str) -> LegacyMigrationResult<()> { - // Only the development supervisor supplies this private, per-launch channel. - // The migration request never supplies paths or executable names. - let handoff_path = directory.join("handoff.json"); - let bytes = std::fs::read(&handoff_path).map_err(|source| LegacyMigrationError::Io { - path: handoff_path, - source, - })?; - let handoff: serde_json::Value = serde_json::from_slice(&bytes).map_err(|_| { - LegacyMigrationError::InvalidRequest("invalid development restart handoff".to_string()) - })?; - if handoff["runId"].as_str() != Some(run_id) - || handoff["pid"].as_u64() != Some(u64::from(std::process::id())) - { - return Err(LegacyMigrationError::InvalidRequest( - "development restart handoff does not match this migrator".to_string(), - )); - } - openbitfun_legacy_migration::atomic_write_json( - &directory.join("restart.json"), - &serde_json::json!({ "runId": run_id }), - ) -} - fn migration_engine( roots: MigrationRoots, selection: &MigrationSelection, ) -> LegacyMigrationResult { + crate::locations::validate(&roots)?; + openbitfun_legacy_migration_adapters::validate_target(&roots)?; MigrationEngine::new(roots, adapters_for_groups(selection)) } -fn writer_processes(caller_process_id: u32) -> LegacyMigrationResult> { - blocking_writer_processes_for_product(caller_process_id, &[DESKTOP_BINARY_NAME]) +fn writer_processes() -> LegacyMigrationResult> { + blocking_writer_processes(0) } -fn validate_selection( - request: &MigratorHandoffRequest, - selection: &MigrationSelection, -) -> Result<(), CommandError> { +fn validate_selection(selection: &MigrationSelection) -> Result<(), CommandError> { if selection.groups.is_empty() { return Err(CommandError::new( "empty_selection", @@ -853,51 +788,16 @@ fn validate_selection( true, )); } - if request.mode == MigratorRequestMode::Execute && request.selection != *selection { - return Err(CommandError::new( - "selection_mismatch", - "The selected groups differ from the scope confirmed in OpenBitFun.", - false, - )); - } Ok(()) } -fn probe_bound_source( - roots: &MigrationRoots, - request: &MigratorHandoffRequest, -) -> LegacyMigrationResult> { - let source = probe_legacy_source(roots, ProbeLimits::default())?; - if let Some(source) = &source { - if request - .source_id - .as_deref() - .is_some_and(|source_id| source_id != source.source_id) - || request - .source_fingerprint - .as_deref() - .is_some_and(|fingerprint| fingerprint != source.source_fingerprint) - { - return Err(LegacyMigrationError::InvalidRequest( - "discovered source does not match the authenticated handoff".to_string(), - )); - } - } else if request.source_id.is_some() || request.source_fingerprint.is_some() { - return Err(LegacyMigrationError::InvalidRequest( - "authenticated handoff source is no longer present".to_string(), - )); - } - Ok(source) -} - fn snapshot(session: &MigratorSession) -> MigratorView { let plan = session.plan.as_ref().map(redact_plan_for_ui); let report = session.report.as_ref().map(redact_report_for_ui); MigratorView { - delivery_profile: DeliveryProfile::DataMigrator.id().to_string(), - restart_desktop_on_finish: true, - protocol: MigratorProtocolCapabilities::current(), - mode: session.request.mode, + tool_version: env!("CARGO_PKG_VERSION").to_string(), + locations: session.roots.clone(), + saved_tasks: session.saved_tasks.clone(), source: session.source.clone(), selection: session.selection.clone(), findings: session @@ -911,14 +811,18 @@ fn snapshot(session: &MigratorSession) -> MigratorView { .source .as_ref() .is_some_and(|source| source.supported) - && !session.running, + && !session.running + && !matches!( + session.status, + MigrationRunStatus::Completed | MigrationRunStatus::CompletedWithWarnings + ), plan, report, progress: session.progress.clone(), blockers: session.blockers.clone(), status: session.status, running: session.running, - recovery: session.disposition == HandoffDisposition::Recovery, + recovery: session.recovery, error: session.error.clone(), } } @@ -973,24 +877,11 @@ fn status_for_phase(phase: MigrationPhase) -> MigrationRunStatus { } } -fn now_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() - .try_into() - .unwrap_or(i64::MAX) -} - #[cfg(test)] mod tests { use super::*; - use openbitfun_product_domains::legacy_migration::{ - MigratorProtocolCapability, MigratorRequestOrigin, CURRENT_MIGRATOR_PROTOCOL_VERSION, - }; - use std::collections::BTreeSet; use std::fs; - + use std::path::Path; fn fixture_roots(root: &Path) -> MigrationRoots { MigrationRoots { legacy_user_root: root.join("legacy/user"), @@ -1004,103 +895,78 @@ mod tests { } } - fn handoff_request() -> MigratorHandoffRequest { - let current = now_ms(); - MigratorHandoffRequest { - protocol_version: CURRENT_MIGRATOR_PROTOCOL_VERSION, - mode: MigratorRequestMode::Onboarding, - origin: MigratorRequestOrigin::FirstLaunch, - run_id: uuid::Uuid::new_v4().to_string(), - nonce: uuid::Uuid::new_v4().to_string(), - selection: MigrationSelection::all(), - caller_process_id: u32::MAX, - product_id: "openbitfun".to_string(), - release_channel: "stable".to_string(), - created_at_ms: current, - expires_at_ms: current + 60_000, - required_capabilities: BTreeSet::from([ - MigratorProtocolCapability::ReadOnlyScan, - MigratorProtocolCapability::JournalRecovery, - ]), - ..MigratorHandoffRequest::default() - } - } - - fn write_probe_fixture(roots: &MigrationRoots) { - let config = roots.legacy_user_root.join("config"); - fs::create_dir_all(&config).unwrap(); - fs::write(config.join("app.json"), br#"{"version":"0.2.19"}"#).unwrap(); - } - #[test] - fn bootstrap_consumes_the_real_non_agent_delivery_profile() { - let temporary = tempfile::tempdir().unwrap(); - let roots = fixture_roots(temporary.path()); - write_probe_fixture(&roots); - let request = handoff_request(); - HandoffStore::new(roots.clone(), "openbitfun", "stable") - .write_request(&request, now_ms()) - .unwrap(); - - let coordinator = - MigratorCoordinator::bootstrap_with(&request.run_id, roots, "openbitfun", "stable") - .unwrap(); + fn starts_without_desktop_request_or_data() { + let temp = tempfile::tempdir().unwrap(); + let roots = fixture_roots(temp.path()); + let coordinator = MigratorCoordinator::bootstrap_with( + roots.clone(), + temp.path().join("tool/locations.json"), + ); let view = coordinator.snapshot(); - - assert_eq!(view.delivery_profile, "data-migrator"); - assert!(view.restart_desktop_on_finish); - assert_eq!(view.mode, MigratorRequestMode::Onboarding); - assert!(view.source.is_some()); - assert!(!view.recovery); - } - - #[test] - fn execute_handoff_rejects_a_scope_change() { - let mut request = handoff_request(); - request.mode = MigratorRequestMode::Execute; - let mut changed = request.selection.clone(); - changed - .groups - .remove(&openbitfun_product_domains::legacy_migration::MigrationGroupId::Memory); - - let error = validate_selection(&request, &changed).unwrap_err(); - assert_eq!(error.code, "selection_mismatch"); + assert!(view.source.is_none()); + assert!(view.error.is_none()); + assert!(!view.running); + coordinator.finish().unwrap(); + assert!(!roots.target_user_root.exists()); } #[test] - fn cancelled_scan_finishes_in_an_explicit_cancelled_state() { - let temporary = tempfile::tempdir().unwrap(); - let roots = fixture_roots(temporary.path()); - write_probe_fixture(&roots); - let request = handoff_request(); - HandoffStore::new(roots.clone(), "openbitfun", "stable") - .write_request(&request, now_ms()) - .unwrap(); - let coordinator = MigratorCoordinator::bootstrap_with( - &request.run_id, - roots.clone(), - "openbitfun", - "stable", + fn cancelled_scan_preserves_safe_boundary() { + let temp = tempfile::tempdir().unwrap(); + let roots = fixture_roots(temp.path()); + fs::create_dir_all(roots.legacy_user_root.join("config")).unwrap(); + fs::write( + roots.legacy_user_root.join("config/app.json"), + br#"{"version":"0.2.19"}"#, ) .unwrap(); + let coordinator = + MigratorCoordinator::bootstrap_with(roots, temp.path().join("tool/locations.json")); let selection = MigrationSelection::all(); let (roots, request, cancellation) = coordinator.begin_operation(&selection).unwrap(); + assert!(coordinator.finish().is_err()); + assert!(coordinator.new_task().is_err()); cancellation.cancel(); coordinator.scan_background(roots, request, selection, cancellation); + assert_eq!(coordinator.snapshot().status, MigrationRunStatus::Cancelled); + assert!(!coordinator.is_running()); + } - let view = coordinator.snapshot(); - assert!(!view.running); - assert_eq!(view.status, MigrationRunStatus::Cancelled); - assert_eq!( - view.error.map(|error| error.code).as_deref(), - Some("cancelled") - ); - assert_eq!( - view.progress.map(|progress| progress.code).as_deref(), - Some("migration_cancelled") - ); + #[test] + fn rejects_nested_locations_without_writing_preferences() { + let temp = tempfile::tempdir().unwrap(); + let mut roots = fixture_roots(temp.path()); + let settings = temp.path().join("tool/locations.json"); + let coordinator = MigratorCoordinator::bootstrap_with(roots.clone(), settings.clone()); + roots.target_home_root = roots.legacy_user_root.join("nested"); + assert!(coordinator.set_locations(roots).is_err()); + assert!(!settings.exists()); } + #[test] + fn unsupported_target_is_rejected_before_scan_or_any_writes() { + let temp = tempfile::tempdir().unwrap(); + let roots = fixture_roots(temp.path()); + assert!(migration_engine(roots.clone(), &MigrationSelection::all()).is_ok()); + assert!(!roots.target_user_root.exists()); + let target = roots.target_user_root.join("config/app.json"); + for value in [ + serde_json::json!({"product_id":"openbitfun", "schema_version":999, "version":"9.0.0"}), + serde_json::json!({"product_id":"other-product", "schema_version":1, "version":"1.0.0"}), + serde_json::json!({"version":"0.2.19"}), + ] { + atomic_write_json(&target, &value).unwrap(); + let before = fs::read(&target).unwrap(); + assert!(matches!( + migration_engine(roots.clone(), &MigrationSelection::all()), + Err(LegacyMigrationError::UnsupportedTarget(_)) + )); + assert_eq!(fs::read(&target).unwrap(), before); + assert!(!roots.migration_root().exists()); + assert!(!roots.legacy_user_root.exists()); + } + } #[test] fn command_errors_do_not_expose_storage_paths() { let error = LegacyMigrationError::Io { @@ -1141,93 +1007,4 @@ mod tests { assert!(!serialized.contains("private")); assert!(!serialized.contains("session-state")); } - - #[test] - fn dismissing_onboarding_persists_choice_and_restarts_desktop() { - for choice in [ - MigrationPromptChoice::DoNotRemind, - MigrationPromptChoice::RemindLater, - ] { - let temporary = tempfile::tempdir().unwrap(); - let roots = fixture_roots(temporary.path()); - write_probe_fixture(&roots); - let request = handoff_request(); - HandoffStore::new(roots.clone(), "openbitfun", "stable") - .write_request(&request, now_ms()) - .unwrap(); - let coordinator = MigratorCoordinator::bootstrap_with( - &request.run_id, - roots.clone(), - "openbitfun", - "stable", - ) - .unwrap(); - let mut restarted = false; - coordinator - .finish_with_restart(choice, || { - // Restart must observe the saved choice and one-time restart receipt. - let state = MigrationOnboardingStore::new(roots.clone()).load().unwrap(); - assert_eq!(state.choice, choice); - assert_eq!( - state.handled_run_id.as_deref(), - Some(request.run_id.as_str()) - ); - restarted = true; - Ok(()) - }) - .unwrap(); - assert!(restarted); - assert!(coordinator.snapshot().restart_desktop_on_finish); - } - } - - #[test] - fn failed_restart_stays_visible_and_keeps_the_saved_choice() { - let temporary = tempfile::tempdir().unwrap(); - let roots = fixture_roots(temporary.path()); - write_probe_fixture(&roots); - let request = handoff_request(); - HandoffStore::new(roots.clone(), "openbitfun", "stable") - .write_request(&request, now_ms()) - .unwrap(); - let coordinator = MigratorCoordinator::bootstrap_with( - &request.run_id, - roots.clone(), - "openbitfun", - "stable", - ) - .unwrap(); - assert!(coordinator - .finish_with_restart(MigrationPromptChoice::DoNotRemind, || { - Err(LegacyMigrationError::TrustedInstallationUnavailable( - "missing desktop".into(), - )) - }) - .is_err()); - assert!(coordinator.snapshot().error.is_some()); - assert_eq!( - MigrationOnboardingStore::new(roots).load().unwrap().choice, - MigrationPromptChoice::DoNotRemind - ); - } - - #[test] - #[cfg(debug_assertions)] - fn development_restart_requires_matching_handoff() { - let temporary = tempfile::tempdir().unwrap(); - let run_id = uuid::Uuid::new_v4().to_string(); - let handoff = temporary.path().join("handoff.json"); - openbitfun_legacy_migration::atomic_write_json( - &handoff, - &serde_json::json!({ "runId": run_id, "pid": std::process::id() }), - ) - .unwrap(); - assert!(request_dev_restart(temporary.path(), "wrong-run").is_err()); - assert!(!temporary.path().join("restart.json").exists()); - request_dev_restart(temporary.path(), &run_id).unwrap(); - let restart: serde_json::Value = - serde_json::from_slice(&std::fs::read(temporary.path().join("restart.json")).unwrap()) - .unwrap(); - assert_eq!(restart["runId"], run_id); - } } diff --git a/src/apps/data-migrator/src/commands.rs b/src/apps/data-migrator/src/commands.rs index be2dd043da..e9bf9de2e9 100644 --- a/src/apps/data-migrator/src/commands.rs +++ b/src/apps/data-migrator/src/commands.rs @@ -1,5 +1,6 @@ use crate::app_state::{CommandError, DiagnosticsExportView, MigratorCoordinator, MigratorView}; -use openbitfun_product_domains::legacy_migration::{MigrationPromptChoice, MigrationSelection}; +use openbitfun_legacy_migration::MigrationRoots; +use openbitfun_product_domains::legacy_migration::MigrationSelection; use serde::Deserialize; use tauri::{AppHandle, State}; @@ -21,8 +22,39 @@ pub(crate) struct ExecuteRequest { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] -pub(crate) struct PromptChoiceRequest { - pub choice: MigrationPromptChoice, +pub(crate) struct LocationsRequest { + pub locations: MigrationRoots, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub(crate) struct ResumeRequest { + pub run_id: String, +} + +#[tauri::command] +pub(crate) fn set_migration_locations( + state: State<'_, MigratorCoordinator>, + request: LocationsRequest, +) -> Result { + state.set_locations(request.locations) +} + +#[tauri::command] +pub(crate) fn new_migration_task( + state: State<'_, MigratorCoordinator>, + request: EmptyRequest, +) -> Result { + let _ = request; + state.new_task() +} + +#[tauri::command] +pub(crate) fn resume_migration_task( + state: State<'_, MigratorCoordinator>, + request: ResumeRequest, +) -> Result { + state.resume_task(&request.run_id) } #[tauri::command] @@ -89,9 +121,10 @@ pub(crate) fn export_migration_diagnostics( pub(crate) fn finish_legacy_migration( app: AppHandle, state: State<'_, MigratorCoordinator>, - request: PromptChoiceRequest, + request: EmptyRequest, ) -> Result<(), CommandError> { - state.finish_and_restart(request.choice)?; + let _ = request; + state.finish()?; app.exit(0); Ok(()) } diff --git a/src/apps/data-migrator/src/lib.rs b/src/apps/data-migrator/src/lib.rs index 64894deb98..b50be4419b 100644 --- a/src/apps/data-migrator/src/lib.rs +++ b/src/apps/data-migrator/src/lib.rs @@ -1,5 +1,6 @@ mod app_state; mod commands; +mod locations; use app_state::MigratorCoordinator; use std::fmt; @@ -22,10 +23,14 @@ impl fmt::Display for RunError { impl std::error::Error for RunError {} -pub fn run(run_id: &str) -> Result<(), RunError> { - let coordinator = MigratorCoordinator::bootstrap(run_id).map_err(|_| RunError::Bootstrap)?; +pub fn run() -> Result<(), RunError> { tauri::Builder::default() - .manage(coordinator) + .setup(|app| { + let settings = app.path().app_config_dir()?.join("locations.json"); + let coordinator = MigratorCoordinator::bootstrap(settings)?; + app.manage(coordinator); + Ok(()) + }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { let coordinator = window.app_handle().state::(); @@ -34,13 +39,16 @@ pub fn run(run_id: &str) -> Result<(), RunError> { coordinator.cancel(); return; } - if coordinator.close_and_restart().is_ok() { + if coordinator.finish().is_ok() { window.app_handle().exit(0); } } }) .invoke_handler(tauri::generate_handler![ commands::get_migrator_bootstrap, + commands::set_migration_locations, + commands::new_migration_task, + commands::resume_migration_task, commands::scan_legacy_migration, commands::prepare_legacy_migration, commands::retry_writer_check, diff --git a/src/apps/data-migrator/src/locations.rs b/src/apps/data-migrator/src/locations.rs new file mode 100644 index 0000000000..d2f0569d5f --- /dev/null +++ b/src/apps/data-migrator/src/locations.rs @@ -0,0 +1,89 @@ +//! Local user-selected locations, independent of Desktop configuration. +use openbitfun_legacy_migration::{LegacyMigrationError, LegacyMigrationResult, MigrationRoots}; +use std::path::{Component, Path, PathBuf}; + +pub(crate) fn validate(roots: &MigrationRoots) -> LegacyMigrationResult<()> { + let sources = [ + &roots.legacy_user_root, + &roots.legacy_home_root, + &roots.legacy_skills_root, + &roots.legacy_ssh_root, + ]; + let targets = [ + &roots.target_user_root, + &roots.target_home_root, + &roots.target_skills_root, + &roots.target_ssh_root, + ]; + let sources = sources + .into_iter() + .map(normalized) + .collect::>>()?; + let targets = targets + .into_iter() + .map(normalized) + .collect::>>()?; + for source in &sources { + for target in &targets { + if source.starts_with(target) || target.starts_with(source) { + return Err(LegacyMigrationError::SourceEqualsTarget(source.clone())); + } + } + } + Ok(()) +} + +fn normalized(path: &PathBuf) -> LegacyMigrationResult { + if !path.is_absolute() + || path.parent().is_none() + || path + .components() + .any(|part| matches!(part, Component::ParentDir)) + { + return Err(LegacyMigrationError::PathUnavailable( + "choose an absolute local data directory".into(), + )); + } + // Resolve existing parents too: destinations often do not exist yet. + let mut parent: &Path = path; + let mut suffix = Vec::new(); + while !parent.exists() { + suffix.push( + parent + .file_name() + .ok_or_else(|| LegacyMigrationError::PathUnavailable("data directory".into()))?, + ); + parent = parent + .parent() + .ok_or_else(|| LegacyMigrationError::PathUnavailable("data directory".into()))?; + } + if !parent.is_dir() { + return Err(LegacyMigrationError::PathUnavailable( + "data directory".into(), + )); + } + let mut resolved = + std::fs::canonicalize(parent).map_err(|source| LegacyMigrationError::Io { + path: parent.to_path_buf(), + source, + })?; + for part in suffix.into_iter().rev() { + resolved.push(part); + } + if cfg!(windows) { + resolved = PathBuf::from(resolved.to_string_lossy().to_lowercase()); + } + Ok(resolved) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn normalizes_missing_children_and_rejects_relative_paths() { + let temp = tempfile::tempdir().unwrap(); + assert!(normalized(&temp.path().join("missing/child")).is_ok()); + assert!(normalized(&PathBuf::from("relative/data")).is_err()); + assert!(normalized(&temp.path().join("../escape")).is_err()); + } +} diff --git a/src/apps/data-migrator/src/main.rs b/src/apps/data-migrator/src/main.rs index d73be4545e..27588eb2a5 100644 --- a/src/apps/data-migrator/src/main.rs +++ b/src/apps/data-migrator/src/main.rs @@ -3,18 +3,7 @@ use std::process::ExitCode; fn main() -> ExitCode { - let mut arguments = std::env::args_os().skip(1); - let Some(run_id) = arguments.next() else { - return ExitCode::from(2); - }; - if arguments.next().is_some() { - return ExitCode::from(2); - } - let Some(run_id) = run_id.to_str() else { - return ExitCode::from(2); - }; - - match openbitfun_data_migrator_lib::run(run_id) { + match openbitfun_data_migrator_lib::run() { Ok(()) => ExitCode::SUCCESS, Err(_) => ExitCode::from(1), } diff --git a/src/apps/data-migrator/tauri.conf.json b/src/apps/data-migrator/tauri.conf.json index c7b678c1c6..5383a891e9 100644 --- a/src/apps/data-migrator/tauri.conf.json +++ b/src/apps/data-migrator/tauri.conf.json @@ -1,6 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenBitFun Data Migrator", + "version": "0.1.0", "mainBinaryName": "openbitfun-data-migrator", "identifier": "com.openbitfun.data-migrator", "build": { @@ -11,9 +12,9 @@ "targets": "all", "publisher": "OpenBitFun Team", "icon": [ - "../desktop/icons/openbitfun-app-icon.icns", - "../desktop/icons/openbitfun-app-icon.ico", - "../desktop/icons/openbitfun-app-icon.png" + "icons/openbitfun-app-icon.icns", + "icons/openbitfun-app-icon.ico", + "icons/openbitfun-app-icon.png" ], "resources": { "../../../THIRD_PARTY_NOTICES.md": "THIRD_PARTY_NOTICES.md" @@ -34,7 +35,7 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src ipc: http://ipc.localhost" + "csp": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost" }, "withGlobalTauri": true } diff --git a/src/apps/data-migrator/ui/app.js b/src/apps/data-migrator/ui/app.js index df1ca5b99b..ec21b9f52a 100644 --- a/src/apps/data-migrator/ui/app.js +++ b/src/apps/data-migrator/ui/app.js @@ -1,90 +1,27 @@ -const invoke = window.__TAURI__.core.invoke; +import { invoke } from './transport.js'; + +async function loadTranslations() { + try { + const response = await fetch('locales.json'); + if (!response.ok) throw new Error('Locale asset unavailable'); + const catalogs = await response.json(); + if (!catalogs.en) throw new Error('Default locale unavailable'); + return catalogs; + } catch (error) { + const output = document.getElementById('notice'); + output.textContent = 'Data Migrator could not load its language files. Close it and download or rebuild the complete package.'; + output.hidden = false; + document.querySelectorAll('button, select').forEach((node) => { node.disabled = true; }); + throw error; + } +} +const translations = await loadTranslations(); -const translations = { - en: { - eyebrow: 'OpenBitFun maintenance', title: 'Import data from BitFun', - intro: 'Choose what to bring forward. Your original BitFun data will not be deleted.', - stepSource: 'Step 1', sourceTitle: 'Legacy source', firstLaunch: 'First launch', - choiceTitle: 'What would you like to do?', - choiceHelp: 'Migration runs only after OpenBitFun and other data writers have stopped.', - migrateNow: 'Migrate now', remindLater: 'Remind me later', doNotRemind: 'Do not remind me', - stepScope: 'Step 2', scopeTitle: 'Choose migration scope', scan: 'Scan selected data', - stepReview: 'Step 3', reviewTitle: 'Review scan', prepare: 'Run preflight plan', - stepConfirm: 'Step 4', planTitle: 'Confirm migration', retryWriters: 'Check processes again', - start: 'Start migration', stepProgress: 'Step 5', progressTitle: 'Migration progress', - phase: 'Phase', domain: 'Domain', count: 'Completed steps', - cancel: 'Cancel', stepDone: 'Result', reportTitle: 'Migration report', - reportPrivacy: 'This summary contains counts and result codes, not credentials or user content.', - exportDiagnostics: 'Export failure diagnostics', - diagnosticsExported: 'Sanitized diagnostics saved to {path}', - openDesktop: 'Open OpenBitFun', ready: 'Ready', unsupported: 'Unsupported', missing: 'Not found', - closeMigrator: 'Close Data Migrator', - devRestartHelp: 'Development build: close Data Migrator, then run pnpm run desktop:dev again.', - bootstrapPending: 'Data Migrator is still loading. Please try again.', - bootstrapFailed: 'Data Migrator could not load its authenticated migration request.', - sourceFound: 'BitFun {version} was found. The source stays read-only.', - recovery: 'A previous migration journal was found and can be resumed.', - blockers: '{count} data-writing process(es) must stop before migration can continue.', - noBlockers: 'No data-writing processes are blocking migration.', - steps: '{count} migration step(s)', conflicts: '{count} conflict(s)', - imported: 'imported', staged: 'staged', skipped: 'skipped', warnings: 'warnings', - }, - 'zh-CN': { - eyebrow: 'OpenBitFun 数据维护', title: '从 BitFun 导入数据', - intro: '选择要迁移的内容。原始 BitFun 数据不会被删除。', stepSource: '第 1 步', - sourceTitle: '旧版数据来源', firstLaunch: '首次启动', choiceTitle: '你希望如何处理?', - choiceHelp: '迁移只会在 OpenBitFun 和其他数据写入进程停止后运行。', migrateNow: '立即迁移', - remindLater: '稍后提醒', doNotRemind: '不再提醒', stepScope: '第 2 步', - scopeTitle: '选择迁移范围', scan: '扫描所选数据', stepReview: '第 3 步', - reviewTitle: '检查扫描结果', prepare: '运行迁移预检', stepConfirm: '第 4 步', - planTitle: '确认迁移', retryWriters: '重新检查进程', start: '开始迁移', - stepProgress: '第 5 步', progressTitle: '迁移进度', phase: '阶段', domain: '领域', - count: '已完成步骤', cancel: '取消', stepDone: '结果', reportTitle: '迁移报告', - reportPrivacy: '此摘要仅包含计数和结果码,不包含凭据或用户正文。', - exportDiagnostics: '导出失败诊断', diagnosticsExported: '去敏诊断已保存到 {path}', - openDesktop: '打开 OpenBitFun', - closeMigrator: '关闭数据迁移器', - devRestartHelp: '开发版本:关闭数据迁移器,然后重新运行 pnpm run desktop:dev。', - bootstrapPending: '数据迁移器仍在加载,请稍后重试。', - bootstrapFailed: '数据迁移器无法加载已认证的迁移请求。', - ready: '可迁移', unsupported: '不受支持', missing: '未发现', - sourceFound: '已发现 BitFun {version}。迁移期间来源保持只读。', - recovery: '发现上次迁移日志,可以从安全状态继续。', - blockers: '迁移前还需停止 {count} 个数据写入进程。', noBlockers: '没有进程阻止迁移。', - steps: '{count} 个迁移步骤', conflicts: '{count} 个冲突', - imported: '已导入', staged: '已暂存', skipped: '已跳过', warnings: '警告', - }, - 'zh-TW': { - eyebrow: 'OpenBitFun 資料維護', title: '從 BitFun 匯入資料', - intro: '選擇要遷移的內容。原始 BitFun 資料不會被刪除。', stepSource: '第 1 步', - sourceTitle: '舊版資料來源', firstLaunch: '首次啟動', choiceTitle: '你希望如何處理?', - choiceHelp: '遷移只會在 OpenBitFun 和其他資料寫入程序停止後執行。', migrateNow: '立即遷移', - remindLater: '稍後提醒', doNotRemind: '不再提醒', stepScope: '第 2 步', - scopeTitle: '選擇遷移範圍', scan: '掃描所選資料', stepReview: '第 3 步', - reviewTitle: '檢查掃描結果', prepare: '執行遷移預檢', stepConfirm: '第 4 步', - planTitle: '確認遷移', retryWriters: '重新檢查程序', start: '開始遷移', - stepProgress: '第 5 步', progressTitle: '遷移進度', phase: '階段', domain: '領域', - count: '已完成步驟', cancel: '取消', stepDone: '結果', reportTitle: '遷移報告', - reportPrivacy: '此摘要僅包含計數和結果碼,不包含憑據或使用者正文。', - exportDiagnostics: '匯出失敗診斷', diagnosticsExported: '去敏診斷已儲存至 {path}', - openDesktop: '開啟 OpenBitFun', - closeMigrator: '關閉資料遷移器', - devRestartHelp: '開發版本:關閉資料遷移器,然後重新執行 pnpm run desktop:dev。', - bootstrapPending: '資料遷移器仍在載入,請稍後重試。', - bootstrapFailed: '資料遷移器無法載入已驗證的遷移請求。', - ready: '可遷移', unsupported: '不支援', missing: '未發現', - sourceFound: '已發現 BitFun {version}。遷移期間來源保持唯讀。', - recovery: '發現上次遷移日誌,可以從安全狀態繼續。', - blockers: '遷移前還需停止 {count} 個資料寫入程序。', noBlockers: '沒有程序阻止遷移。', - steps: '{count} 個遷移步驟', conflicts: '{count} 個衝突', - imported: '已匯入', staged: '已暫存', skipped: '已略過', warnings: '警告', - }, -}; -const locale = navigator.language.startsWith('zh-TW') || navigator.language.startsWith('zh-HK') +const locale = localStorage.getItem('migrator.language') || (navigator.language.startsWith('zh-TW') || navigator.language.startsWith('zh-HK') ? 'zh-TW' - : navigator.language.startsWith('zh') ? 'zh-CN' : 'en'; -const text = translations[locale]; + : navigator.language.startsWith('zh') ? 'zh-CN' : 'en'); +const text = translations[locale] || translations.en; document.documentElement.lang = locale; document.querySelectorAll('[data-i18n]').forEach((node) => { node.textContent = text[node.dataset.i18n] || translations.en[node.dataset.i18n]; @@ -120,6 +57,9 @@ const groups = [ let current; let pollTimer; +let locationsDirty = false; +let locationSnapshot; +let scopeSnapshot; function format(template, values) { return Object.entries(values).reduce((value, [key, replacement]) => @@ -134,9 +74,10 @@ function setBusy(busy) { document.querySelectorAll('button').forEach((button) => { button.disabled = busy; }); } -function notice(message) { +function notice(message, tone = 'danger') { const node = document.getElementById('notice'); node.textContent = message || ''; + node.dataset.tone = tone; node.hidden = !message; } @@ -161,8 +102,16 @@ function transferLabel(result) { return result.state === 'verified' ? text.imported : text.staged; } +function groupLabel(id) { + const labels = groups.find(([group]) => group === id)?.[1]; + return (labels?.[locale] || labels?.en || [id])[0]; +} + function renderScopes(selection) { const list = document.getElementById('scope-list'); + const signature = JSON.stringify([selection, current?.recovery, current?.running]); + if (signature === scopeSnapshot) return; + scopeSnapshot = signature; list.replaceChildren(); const selected = new Set(selection?.groups?.length ? selection.groups : groups.map(([id]) => id)); groups.forEach(([id, labels]) => { @@ -173,7 +122,7 @@ function renderScopes(selection) { checkbox.id = `scope-${id}`; checkbox.value = id; checkbox.checked = selected.has(id); - if (current?.mode === 'execute') checkbox.disabled = true; + checkbox.disabled = Boolean(current?.running || current?.recovery); const label = document.createElement('label'); label.htmlFor = checkbox.id; const strong = document.createElement('strong'); @@ -196,13 +145,28 @@ function render(view) { const source = view.source; document.getElementById('source-badge').textContent = !source ? text.missing : source.supported ? text.ready : text.unsupported; + document.getElementById('source-badge').dataset.tone = !source || !source.supported + ? 'warning' : 'success'; document.getElementById('source-summary').textContent = source ? format(text.sourceFound, { version: source.productVersion }) : text.missing; - document.getElementById('source-path').textContent = source?.roots?.[0]?.displayPath || ''; - notice(view.error?.message || (view.recovery ? text.recovery : '')); + if (!source) document.getElementById('source-summary').textContent = text.noSource; + renderLocations(view.locations); + document.getElementById('tool-version').textContent = 'v' + view.toolVersion; + const tasks = document.getElementById('saved-task'); + const previousTask = tasks.value; + tasks.replaceChildren(...view.savedTasks.map((task) => { + const option = document.createElement('option'); + option.value = task.runId; + option.textContent = task.runId + ' · ' + (task.readable ? task.status : text.unreadableTask); + option.disabled = !task.readable; + return option; + })); + if ([...tasks.options].some((option) => option.value === previousTask && !option.disabled)) tasks.value = previousTask; + show('history-card', view.savedTasks.length > 0 && !view.running); + document.getElementById('resume-task').disabled = !tasks.selectedOptions[0] || tasks.selectedOptions[0].disabled || view.running || locationsDirty; + notice(view.error?.message || (view.recovery ? text.recovery : ''), view.error ? 'danger' : 'info'); - show('choice-card', view.mode === 'onboarding' && !view.findings.length && !view.plan && !view.running); - show('scope-card', Boolean(source) && (view.mode === 'execute' || view.findings.length || view.plan)); + show('scope-card', !view.recovery && !view.running && !view.plan); renderScopes(view.selection); const findings = document.getElementById('findings'); @@ -213,11 +177,12 @@ function render(view) { const planSummary = document.getElementById('plan-summary'); if (view.plan) { planSummary.replaceChildren( - row(text.steps.replace('{count}', view.plan.steps.length), view.plan.planHash), - row(text.conflicts.replace('{count}', view.plan.conflicts.length), `${view.plan.estimatedWriteBytes} byte(s)`), + row(text.steps.replace('{count}', view.plan.steps.length), view.plan.selection.groups.map(groupLabel).join(' · ')), + row(text.conflicts.replace('{count}', view.plan.conflicts.length), text.confirmHelp), + ...view.plan.conflicts.map((conflict) => row(conflict.domain, conflict.code || conflict.resolution || '')), ); } - show('plan-card', Boolean(view.plan) && !view.running && !view.report); + show('plan-card', Boolean(view.plan) && !view.running && !['completed', 'completed_with_warnings'].includes(view.status)); const blocker = document.getElementById('blockers'); blocker.textContent = view.blockers.length ? format(text.blockers, { count: view.blockers.length }) : text.noBlockers; @@ -239,9 +204,6 @@ function render(view) { reportSummary.replaceChildren(...view.report.domainResults.map((result) => row(result.domain, `${result.imported} ${transferLabel(result)}, ${result.skipped} ${text.skipped}, ${result.warnings.filter((item) => item.severity !== 'info').length} ${text.warnings}`))); } - show('dev-restart-help', !view.restartDesktopOnFinish); - document.getElementById('open-desktop').textContent = view.restartDesktopOnFinish - ? text.openDesktop : text.closeMigrator; show('report-card', !view.running && (Boolean(view.report) || view.status === 'cancelled')); const canExportDiagnostics = ['failed_recoverable', 'failed_manual_action_required'].includes(view.status); show('export-diagnostics', canExportDiagnostics); @@ -250,7 +212,12 @@ function render(view) { output.textContent = ''; output.hidden = true; } - document.getElementById('start').disabled = !view.canExecute; + document.querySelectorAll('button, #locations input, #language, #saved-task').forEach((node) => { node.disabled = view.running; }); + document.getElementById('cancel').disabled = !view.running; + document.getElementById('start').disabled = !view.canExecute || locationsDirty; + document.getElementById('resume-task').disabled = !tasks.selectedOptions[0] || tasks.selectedOptions[0].disabled || view.running || locationsDirty; + for (const id of ['scan', 'prepare']) document.getElementById(id).disabled = view.running || locationsDirty; + if (locationsDirty) notice(text.dirtyLocations, 'info'); if (view.running && !pollTimer) { pollTimer = window.setInterval(refresh, 500); @@ -262,16 +229,18 @@ function render(view) { async function call(command, request = {}) { setBusy(true); + let failure; try { const result = await invoke(command, { request }); if (result) render(result); return result; } catch (error) { - notice(error?.message || String(error)); + failure = error?.message || String(error); return undefined; } finally { setBusy(false); if (current) render(current); + if (failure) notice(failure); } } @@ -286,16 +255,47 @@ async function refresh() { } } -document.getElementById('migrate-now').addEventListener('click', () => { +function renderLocations(locations) { + const signature = JSON.stringify(locations); + if (signature === locationSnapshot || locationsDirty) return; + locationSnapshot = signature; + const host = document.getElementById('locations'); + host.replaceChildren(); + for (const [prefix, title] of [['legacy', text.source], ['target', text.target]]) { + const group = document.createElement('fieldset'); + const legend = document.createElement('legend'); + legend.textContent = title; + group.append(legend); + for (const suffix of ['UserRoot', 'HomeRoot', 'SkillsRoot', 'SshRoot']) { + const key = prefix + suffix; + const label = document.createElement('label'); + const input = document.createElement('input'); + label.htmlFor = key; + label.textContent = text[suffix[0].toLowerCase() + suffix.slice(1)]; + input.type = 'text'; input.id = key; input.value = locations[key]; + input.spellcheck = false; input.autocomplete = 'off'; + input.addEventListener('input', () => { locationsDirty = true; if (current) render(current); }); + group.append(label, input); + } + host.append(group); + } +} + +document.getElementById('language').value = locale; +document.getElementById('language').addEventListener('change', (event) => { + localStorage.setItem('migrator.language', event.target.value); + window.location.reload(); +}); +document.getElementById('apply-locations').addEventListener('click', async () => { if (!requireBootstrap()) return; - show('choice-card', false); - show('scope-card'); - renderScopes(current.selection); + const locations = Object.fromEntries([...document.querySelectorAll('#locations input')].map((input) => [input.id, input.value.trim()])); + const result = await call('set_migration_locations', { locations }); + if (result) { locationsDirty = false; locationSnapshot = undefined; render(result); } }); -document.getElementById('remind-later').addEventListener('click', () => - call('finish_legacy_migration', { choice: 'remind_later' })); -document.getElementById('do-not-remind').addEventListener('click', () => - call('finish_legacy_migration', { choice: 'do_not_remind' })); +document.getElementById('resume-task').addEventListener('click', () => call('resume_migration_task', { runId: document.getElementById('saved-task').value })); +document.getElementById('new-task').addEventListener('click', () => call('new_migration_task')); +for (const id of ['close', 'finish']) document.getElementById(id).addEventListener('click', () => call('finish_legacy_migration')); + document.getElementById('scan').addEventListener('click', () => call('scan_legacy_migration', { selection: selection() })); document.getElementById('prepare').addEventListener('click', () => @@ -308,21 +308,18 @@ document.getElementById('cancel').addEventListener('click', () => call('cancel_legacy_migration')); document.getElementById('export-diagnostics').addEventListener('click', async () => { setBusy(true); + let failure; try { const result = await invoke('export_migration_diagnostics', { request: {} }); const output = document.getElementById('diagnostics-path'); output.textContent = format(text.diagnosticsExported, { path: result.filePath }); output.hidden = false; } catch (error) { - notice(error?.message || String(error)); + failure = error?.message || String(error); } finally { setBusy(false); if (current) render(current); + if (failure) notice(failure); } }); -document.getElementById('open-desktop').addEventListener('click', () => - call('finish_legacy_migration', { choice: current.report ? 'migrate_now' : 'remind_later' })); - -refresh().then(() => { - if (current?.mode === 'execute') show('scope-card'); -}); +refresh(); diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css new file mode 100644 index 0000000000..70e3125ba3 --- /dev/null +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -0,0 +1,1026 @@ +/* Generated by scripts/generate-data-migrator-theme.mjs. Do not edit. */ +@layer openbitfun.tokens.system, openbitfun.tokens.theme, openbitfun.reset, openbitfun.base, openbitfun.components, openbitfun.overrides; + +@layer openbitfun.tokens.system { + :where([data-openbitfun-design-system-root]) { + --openbitfun-border-width-default: 1px; + --openbitfun-border-width-strong: 2px; + --openbitfun-control-action-card-actions-gap: 0px; + --openbitfun-control-action-card-actions-padding-inline-end: var(--openbitfun-space-2); + --openbitfun-control-action-card-content-gap: var(--openbitfun-space-1); + --openbitfun-control-action-card-gap: var(--openbitfun-space-2); + --openbitfun-control-action-card-icon-size: 16px; + --openbitfun-control-action-card-leading-size: 30px; + --openbitfun-control-action-card-md-min-block-size: 62px; + --openbitfun-control-action-card-padding-block: var(--openbitfun-space-2); + --openbitfun-control-action-card-padding-inline: var(--openbitfun-space-3); + --openbitfun-control-action-card-radius: var(--openbitfun-radius-base); + --openbitfun-control-action-card-sm-min-block-size: 54px; + --openbitfun-control-activity-item-content-gap: var(--openbitfun-space-1); + --openbitfun-control-activity-item-divider-block-size: 16px; + --openbitfun-control-activity-item-inline-gap: var(--openbitfun-space-1); + --openbitfun-control-activity-item-inline-icon-size: 12px; + --openbitfun-control-activity-item-surface-gap: var(--openbitfun-space-2); + --openbitfun-control-activity-item-surface-height: 30px; + --openbitfun-control-activity-item-surface-icon-size: 14px; + --openbitfun-control-activity-item-surface-padding-block: var(--openbitfun-space-1); + --openbitfun-control-activity-item-surface-padding-inline-end: 3px; + --openbitfun-control-activity-item-surface-padding-inline-start: var(--openbitfun-space-2); + --openbitfun-control-activity-item-surface-radius: var(--openbitfun-radius-base); + --openbitfun-control-ask-user-body-gap: var(--openbitfun-space-3); + --openbitfun-control-ask-user-body-padding: var(--openbitfun-space-4); + --openbitfun-control-ask-user-description-max-width: 500px; + --openbitfun-control-ask-user-header-height: 30px; + --openbitfun-control-ask-user-icon-size: 14px; + --openbitfun-control-ask-user-option-content-gap: var(--openbitfun-space-2); + --openbitfun-control-ask-user-option-gap: var(--openbitfun-space-1); + --openbitfun-control-ask-user-option-padding-block: 7px; + --openbitfun-control-ask-user-option-padding-inline: var(--openbitfun-space-2); + --openbitfun-control-ask-user-question-options-gap: var(--openbitfun-space-3); + --openbitfun-control-ask-user-question-padding-inline: var(--openbitfun-space-1); + --openbitfun-control-ask-user-summary-action-size: 22px; + --openbitfun-control-ask-user-summary-padding-block: var(--openbitfun-space-1); + --openbitfun-control-ask-user-summary-padding-inline-end: var(--openbitfun-space-1); + --openbitfun-control-ask-user-summary-padding-inline-start: var(--openbitfun-space-2); + --openbitfun-control-button-xs-height: 24px; + --openbitfun-control-button-xs-leading-icon-size: 12px; + --openbitfun-control-button-xs-padding-inline: 6px; + --openbitfun-control-button-xs-trailing-icon-size: 10px; + --openbitfun-control-change-count-gap: var(--openbitfun-space-1); + --openbitfun-control-change-count-padding-block: 2px; + --openbitfun-control-change-count-padding-inline: var(--openbitfun-space-1); + --openbitfun-control-change-count-radius: var(--openbitfun-radius-xs); + --openbitfun-control-chat-composer-action-icon-size: 14px; + --openbitfun-control-chat-composer-compact-gap: 9px; + --openbitfun-control-chat-composer-compact-height: 45px; + --openbitfun-control-chat-composer-compact-padding-block: 9px; + --openbitfun-control-chat-composer-compact-padding-inline: 9px; + --openbitfun-control-chat-composer-compact-track-height: 25px; + --openbitfun-control-chat-composer-control-height: 25px; + --openbitfun-control-composer-context-gap: var(--openbitfun-space-2); + --openbitfun-control-composer-context-offset: 32px; + --openbitfun-control-composer-context-padding-block: 6px; + --openbitfun-control-composer-context-padding-inline: var(--openbitfun-space-2); + --openbitfun-control-composer-divider-block-size: 16px; + --openbitfun-control-composer-editor-padding: var(--openbitfun-space-1); + --openbitfun-control-composer-min-block-size: 120px; + --openbitfun-control-composer-surface-gap: var(--openbitfun-space-3); + --openbitfun-control-composer-surface-padding: var(--openbitfun-space-2); + --openbitfun-control-composer-surface-radius: var(--openbitfun-radius-xl); + --openbitfun-control-composer-toolbar-gap: var(--openbitfun-space-2); + --openbitfun-control-flow-chat-card-expanded-padding-block: 0.5rem; + --openbitfun-control-flow-chat-card-expanded-padding-inline: 0.625rem; + --openbitfun-control-flow-chat-card-gap: 0.42rem; + --openbitfun-control-flow-chat-card-padding-block: 0.625rem; + --openbitfun-control-flow-chat-card-padding-inline: 0.75rem; + --openbitfun-control-flow-chat-card-radius: var(--openbitfun-radius-base); + --openbitfun-control-flow-chat-code-block-padding-block: 0.55rem; + --openbitfun-control-flow-chat-code-block-padding-inline: 0.75rem; + --openbitfun-control-flow-chat-content-padding-inline: 3rem; + --openbitfun-control-flow-chat-content-padding-inline-mobile: 1.5rem; + --openbitfun-control-flow-chat-control-gap: 0.5rem; + --openbitfun-control-flow-chat-control-padding-block: 0.35rem; + --openbitfun-control-flow-chat-control-padding-inline: 0.75rem; + --openbitfun-control-flow-chat-flow-item-gap: 0.42rem; + --openbitfun-control-flow-chat-inline-gap: 0.35rem; + --openbitfun-control-flow-chat-turn-gap: var(--openbitfun-space-4); + --openbitfun-control-height-lg: 48px; + --openbitfun-control-height-md: 40px; + --openbitfun-control-height-sm: 32px; + --openbitfun-control-hit-target: 40px; + --openbitfun-control-icon-size2xs: 8px; + --openbitfun-control-icon-size-lg: 24px; + --openbitfun-control-icon-size-md: 16px; + --openbitfun-control-icon-size-sm: 14px; + --openbitfun-control-icon-size-xs: 12px; + --openbitfun-control-icon-button-xs-icon-size: 14px; + --openbitfun-control-icon-button-xs-size: 22px; + --openbitfun-control-launcher-button-block-size: var(--openbitfun-control-height-md); + --openbitfun-control-launcher-button-gap: var(--openbitfun-space-2); + --openbitfun-control-launcher-button-icon-size: 16px; + --openbitfun-control-launcher-button-min-inline-size: 104px; + --openbitfun-control-launcher-button-padding-inline: 14px; + --openbitfun-control-launcher-button-radius: var(--openbitfun-radius-lg); + --openbitfun-control-segmented-control-gap: 2px; + --openbitfun-control-segmented-control-icon-size: 12px; + --openbitfun-control-segmented-control-icon-size-md: 14px; + --openbitfun-control-segmented-control-padding: 2px; + --openbitfun-control-segmented-control-padding-md: 3px; + --openbitfun-control-segmented-control-pill-segment-height: 24px; + --openbitfun-control-segmented-control-pill-segment-radius: var(--openbitfun-radius-sm); + --openbitfun-control-segmented-control-radius: var(--openbitfun-radius-pill); + --openbitfun-control-segmented-control-segment-gap: var(--openbitfun-space-1); + --openbitfun-control-segmented-control-segment-height: 22px; + --openbitfun-control-segmented-control-segment-height-md: 28px; + --openbitfun-control-segmented-control-segment-padding-inline: var(--openbitfun-space-2); + --openbitfun-control-segmented-control-segment-radius: var(--openbitfun-radius-pill); + --openbitfun-control-select-content-gap: var(--openbitfun-space-2); + --openbitfun-control-select-indicator-size: 14px; + --openbitfun-control-select-leading-inset: var(--openbitfun-space-3); + --openbitfun-control-select-padding-inline: var(--openbitfun-space-3); + --openbitfun-control-select-radius: var(--openbitfun-radius-base); + --openbitfun-control-select-trailing-inset: var(--openbitfun-space-3); + --openbitfun-control-status-pill-gap: 2px; + --openbitfun-control-status-pill-icon-size: 14px; + --openbitfun-control-status-pill-padding-block: 3px; + --openbitfun-control-status-pill-padding-inline: 6px; + --openbitfun-control-status-pill-radius: var(--openbitfun-radius-pill); + --openbitfun-control-switch-thumb-inset: 2px; + --openbitfun-control-switch-thumb-size: 12px; + --openbitfun-control-switch-thumb-travel: 12px; + --openbitfun-control-switch-thumb-travel-reverse: -12px; + --openbitfun-control-switch-track-height: 16px; + --openbitfun-control-switch-track-width: 28px; + --openbitfun-control-tab-group-gap: var(--openbitfun-space-2); + --openbitfun-control-tab-group-item-action-inset: var(--openbitfun-space-2); + --openbitfun-control-tab-group-item-action-size: var(--openbitfun-space-5); + --openbitfun-control-tab-group-item-gap: 6px; + --openbitfun-control-tab-group-item-height: var(--openbitfun-control-height-md); + --openbitfun-control-tab-group-item-height-sm: 30px; + --openbitfun-control-tab-group-item-icon-size: 16px; + --openbitfun-control-tab-group-item-padding-block-sm: 7px; + --openbitfun-control-tab-group-item-padding-inline: var(--openbitfun-space-4); + --openbitfun-control-tab-group-item-padding-inline-sm: var(--openbitfun-space-3); + --openbitfun-control-tab-group-item-radius: var(--openbitfun-radius-pill); + --openbitfun-control-tool-card-ambient-row-min-block-size: 22px; + --openbitfun-control-tool-card-expanded-padding-inline: 0.625rem; + --openbitfun-control-tool-card-header-icon-slot: 34px; + --openbitfun-control-tool-card-header-padding-block: 0.44rem; + --openbitfun-control-tool-card-header-padding-inline-end: 0.625rem; + --openbitfun-focus-offset: 2px; + --openbitfun-focus-width: 2px; + --openbitfun-font-family-control: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --openbitfun-font-family-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, 'Cascadia Mono', 'Cascadia Code', Consolas, 'Liberation Mono', 'Courier New', monospace; + --openbitfun-font-family-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --openbitfun-font-size-2xl: 18px; + --openbitfun-font-size-2xl-plus: 20px; + --openbitfun-font-size-2xs: 9px; + --openbitfun-font-size-3xl: 22px; + --openbitfun-font-size-3xl-plus: 24px; + --openbitfun-font-size-3xs: 8px; + --openbitfun-font-size-4xl: 26px; + --openbitfun-font-size-4xs: 7px; + --openbitfun-font-size-5xl: 32px; + --openbitfun-font-size-6xl: 40px; + --openbitfun-font-size-7xl: 48px; + --openbitfun-font-size-8xl: 56px; + --openbitfun-font-size-9xl: 64px; + --openbitfun-font-size-base: 14px; + --openbitfun-font-size-lg: 15px; + --openbitfun-font-size-meta: 11px; + --openbitfun-font-size-micro: 10px; + --openbitfun-font-size-sm: 13px; + --openbitfun-font-size-xl: 16px; + --openbitfun-font-size-xl-plus: 17px; + --openbitfun-font-size-xs: 12px; + --openbitfun-font-weight-bold: 700; + --openbitfun-font-weight-medium: 500; + --openbitfun-font-weight-regular: 400; + --openbitfun-font-weight-semibold: 600; + --openbitfun-layer-base: 0; + --openbitfun-layer-content: 2; + --openbitfun-layer-context-menu: 500; + --openbitfun-layer-decoration: 1; + --openbitfun-layer-dropdown: 60; + --openbitfun-layer-floating: 50; + --openbitfun-layer-fullscreen: 280; + --openbitfun-layer-modal: 200; + --openbitfun-layer-modal-active: 250; + --openbitfun-layer-notification: 400; + --openbitfun-layer-overlay: 100; + --openbitfun-layer-overlay-host: 300; + --openbitfun-layer-popover: 360; + --openbitfun-layer-sticky: 15; + --openbitfun-layer-toast: 400; + --openbitfun-layer-tooltip: 350; + --openbitfun-layout-card-footer-gap: var(--openbitfun-space-2); + --openbitfun-layout-card-gap-lg: 30px; + --openbitfun-layout-card-gap-md: var(--openbitfun-space-3); + --openbitfun-layout-card-gap-sm: var(--openbitfun-space-2); + --openbitfun-layout-card-header-gap: var(--openbitfun-space-2); + --openbitfun-layout-card-media-min-block-size: 120px; + --openbitfun-layout-card-padding-md: var(--openbitfun-space-5); + --openbitfun-layout-card-padding-sm: var(--openbitfun-space-3); + --openbitfun-layout-card-radius-lg: 28px; + --openbitfun-layout-card-radius-md: var(--openbitfun-radius-lg); + --openbitfun-layout-card-radius-sm: var(--openbitfun-radius-base); + --openbitfun-layout-card-title-description-gap: var(--openbitfun-space-1); + --openbitfun-layout-confirm-dialog-content-gap: var(--openbitfun-space-4); + --openbitfun-layout-confirm-dialog-icon-glyph-size: var(--openbitfun-control-icon-size-md); + --openbitfun-layout-confirm-dialog-icon-size: 32px; + --openbitfun-layout-confirm-dialog-message-gap: var(--openbitfun-space-2); + --openbitfun-layout-confirm-dialog-preview-max-block-size: 240px; + --openbitfun-layout-confirm-dialog-preview-padding-block: var(--openbitfun-space-3); + --openbitfun-layout-confirm-dialog-preview-padding-inline: var(--openbitfun-space-4); + --openbitfun-layout-confirm-dialog-preview-radius: var(--openbitfun-radius-base); + --openbitfun-layout-disclosure-actions-gap: var(--openbitfun-space-1); + --openbitfun-layout-disclosure-content-padding-block: var(--openbitfun-space-3); + --openbitfun-layout-disclosure-content-padding-inline: var(--openbitfun-space-8); + --openbitfun-layout-disclosure-indicator-size: 14px; + --openbitfun-layout-disclosure-trigger-gap: var(--openbitfun-space-2); + --openbitfun-layout-disclosure-trigger-min-block-size: 30px; + --openbitfun-layout-disclosure-trigger-padding-block: var(--openbitfun-space-1); + --openbitfun-layout-disclosure-trigger-padding-inline: var(--openbitfun-space-2); + --openbitfun-layout-disclosure-trigger-radius: var(--openbitfun-radius-base); + --openbitfun-layout-field-content-gap: var(--openbitfun-space-1); + --openbitfun-layout-field-control-gap: var(--openbitfun-space-2); + --openbitfun-layout-field-horizontal-gap: var(--openbitfun-space-5); + --openbitfun-layout-field-horizontal-gap-wide: var(--openbitfun-space-10); + --openbitfun-layout-field-label-action-gap: var(--openbitfun-space-2); + --openbitfun-layout-field-label-gap: 2px; + --openbitfun-layout-field-label-width-lg: 400px; + --openbitfun-layout-field-label-width-md: 200px; + --openbitfun-layout-field-label-width-sm: 150px; + --openbitfun-layout-field-root-gap: var(--openbitfun-space-2); + --openbitfun-layout-field-group-radius: var(--openbitfun-radius-lg); + --openbitfun-layout-field-group-row-padding-block: var(--openbitfun-space-4); + --openbitfun-layout-field-group-row-padding-inline: var(--openbitfun-space-5); + --openbitfun-layout-form-section-gap: var(--openbitfun-space-4); + --openbitfun-layout-form-section-header-gap: var(--openbitfun-space-5); + --openbitfun-layout-form-section-title-description-gap: var(--openbitfun-space-1); + --openbitfun-layout-navigation-panel-content-gap: var(--openbitfun-space-4); + --openbitfun-layout-navigation-panel-footer-height: 40px; + --openbitfun-layout-navigation-panel-footer-padding: var(--openbitfun-space-2); + --openbitfun-layout-navigation-panel-heading-action-size: 22px; + --openbitfun-layout-navigation-panel-heading-gap: var(--openbitfun-space-5); + --openbitfun-layout-navigation-panel-heading-height: 22px; + --openbitfun-layout-navigation-panel-heading-padding-inline: var(--openbitfun-space-2); + --openbitfun-layout-navigation-panel-inline-size: 216px; + --openbitfun-layout-navigation-panel-item-gap: var(--openbitfun-space-2); + --openbitfun-layout-navigation-panel-item-height: 30px; + --openbitfun-layout-navigation-panel-item-icon-size: 14px; + --openbitfun-layout-navigation-panel-item-padding-inline: var(--openbitfun-space-2); + --openbitfun-layout-navigation-panel-item-radius: var(--openbitfun-radius-base); + --openbitfun-layout-navigation-panel-scrollbar-gap: 2px; + --openbitfun-layout-navigation-panel-section-gap: var(--openbitfun-space-1); + --openbitfun-layout-navigation-panel-surface-padding: var(--openbitfun-space-2); + --openbitfun-layout-overflow-text-fade-extent: var(--openbitfun-space-4); + --openbitfun-layout-spinner-matrix-cell-lg: 8px; + --openbitfun-layout-spinner-matrix-cell-md: 6px; + --openbitfun-layout-spinner-matrix-cell-sm: 4px; + --openbitfun-layout-spinner-matrix-cell-xs: 3px; + --openbitfun-layout-spinner-matrix-gap-md: 2px; + --openbitfun-layout-spinner-matrix-gap-sm: 1px; + --openbitfun-layout-spinner-matrix-radius: 1px; + --openbitfun-layout-split-view-content-panel-radius: var(--openbitfun-radius-3xl); + --openbitfun-layout-toolbar-badge-size: 24px; + --openbitfun-layout-toolbar-content-gap: 40px; + --openbitfun-layout-toolbar-group-gap-md: var(--openbitfun-space-2); + --openbitfun-layout-toolbar-group-gap-sm: var(--openbitfun-space-1); + --openbitfun-layout-toolbar-md-height: 45px; + --openbitfun-layout-toolbar-md-padding-block-end: 6px; + --openbitfun-layout-toolbar-md-padding-block-start: var(--openbitfun-space-2); + --openbitfun-layout-toolbar-md-padding-inline-end: 11px; + --openbitfun-layout-toolbar-md-padding-inline-start: var(--openbitfun-space-2); + --openbitfun-layout-toolbar-overflow-fade-extent: 16px; + --openbitfun-layout-toolbar-separator-block-size: 16px; + --openbitfun-layout-toolbar-sm-height: 33px; + --openbitfun-layout-toolbar-sm-padding-block: var(--openbitfun-space-1); + --openbitfun-layout-toolbar-sm-padding-inline: var(--openbitfun-space-2); + --openbitfun-letter-spacing-caps: 0.1em; + --openbitfun-letter-spacing-expanded: 0.26em; + --openbitfun-letter-spacing-normal: 0em; + --openbitfun-letter-spacing-snug: -0.01em; + --openbitfun-letter-spacing-subtle: 0.01em; + --openbitfun-letter-spacing-tight: -0.02em; + --openbitfun-letter-spacing-tighter: -0.04em; + --openbitfun-letter-spacing-wide: 0.02em; + --openbitfun-letter-spacing-wider: 0.04em; + --openbitfun-letter-spacing-widest: 0.08em; + --openbitfun-line-height-balanced: 1.35; + --openbitfun-line-height-base: 1.5; + --openbitfun-line-height-code: 1.52; + --openbitfun-line-height-comfortable: 1.55; + --openbitfun-line-height-compact: 1.32; + --openbitfun-line-height-dense: 1.25; + --openbitfun-line-height-display: 1.1; + --openbitfun-line-height-loose: 1.8; + --openbitfun-line-height-none: 1; + --openbitfun-line-height-reading: 1.58; + --openbitfun-line-height-relaxed: 1.6; + --openbitfun-line-height-snug: 1.3; + --openbitfun-line-height-spacious: 1.7; + --openbitfun-line-height-support: 1.45; + --openbitfun-line-height-tight: 1.2; + --openbitfun-line-height-ui: 1.4; + --openbitfun-motion-distance-sm: var(--openbitfun-space-1); + --openbitfun-motion-duration-base: 220ms; + --openbitfun-motion-duration-fast: 140ms; + --openbitfun-motion-duration-instant: 80ms; + --openbitfun-motion-duration-lazy: 1s; + --openbitfun-motion-duration-loop: 720ms; + --openbitfun-motion-duration-normal: var(--openbitfun-motion-duration-base); + --openbitfun-motion-duration-slow: 420ms; + --openbitfun-motion-easing-accelerate: cubic-bezier(0.4, 0, 1, 1); + --openbitfun-motion-easing-decelerate: var(--openbitfun-motion-easing-standard); + --openbitfun-motion-easing-enter: var(--openbitfun-motion-easing-standard); + --openbitfun-motion-easing-exit: cubic-bezier(0.3, 0, 1, 1); + --openbitfun-motion-easing-smooth: cubic-bezier(0.77, 0, 0.175, 1); + --openbitfun-motion-easing-standard: cubic-bezier(0.23, 1, 0.32, 1); + --openbitfun-overlay-dialog-backdrop-blur: blur(20px); + --openbitfun-overlay-dialog-content-padding-lg: var(--openbitfun-space-6); + --openbitfun-overlay-dialog-content-padding-md: var(--openbitfun-space-4); + --openbitfun-overlay-dialog-content-padding-sm: var(--openbitfun-space-3); + --openbitfun-overlay-dialog-content-padding-xl: var(--openbitfun-space-8); + --openbitfun-overlay-dialog-description-gap: var(--openbitfun-space-2); + --openbitfun-overlay-dialog-edge-gutter: 8px; + --openbitfun-overlay-dialog-footer-action-min-width: 100px; + --openbitfun-overlay-dialog-footer-blur: blur(10px); + --openbitfun-overlay-dialog-footer-content-inset: 104px; + --openbitfun-overlay-dialog-footer-fade-extent: var(--openbitfun-space-6); + --openbitfun-overlay-dialog-footer-gap: var(--openbitfun-space-2); + --openbitfun-overlay-dialog-footer-height: 68px; + --openbitfun-overlay-dialog-footer-padding-block-end: var(--openbitfun-space-5); + --openbitfun-overlay-dialog-footer-padding-block-start: var(--openbitfun-space-2); + --openbitfun-overlay-dialog-footer-padding-inline: var(--openbitfun-space-6); + --openbitfun-overlay-dialog-header-actions-gap: var(--openbitfun-space-2); + --openbitfun-overlay-dialog-header-gap: var(--openbitfun-space-5); + --openbitfun-overlay-dialog-header-padding-block-end: var(--openbitfun-space-5); + --openbitfun-overlay-dialog-header-padding-block-start: var(--openbitfun-space-6); + --openbitfun-overlay-dialog-header-padding-inline: var(--openbitfun-space-6); + --openbitfun-overlay-dialog-max-inline-size-large: 600px; + --openbitfun-overlay-dialog-max-inline-size-medium: 560px; + --openbitfun-overlay-dialog-max-inline-size-small: 420px; + --openbitfun-overlay-dialog-max-inline-size-wide: 1200px; + --openbitfun-overlay-dialog-max-inline-size-xlarge: 720px; + --openbitfun-overlay-dialog-max-inline-size-xxlarge: 960px; + --openbitfun-overlay-dialog-scrollbar-width: var(--openbitfun-scrollbar-width); + --openbitfun-overlay-dialog-surface-radius: 28px; + --openbitfun-overlay-dialog-viewport-gutter: 24px; + --openbitfun-overlay-menu-heading-action-size: 22px; + --openbitfun-overlay-menu-heading-gap: var(--openbitfun-space-5); + --openbitfun-overlay-menu-heading-height: 24px; + --openbitfun-overlay-menu-heading-padding-inline: var(--openbitfun-space-2); + --openbitfun-overlay-menu-inline-size: 220px; + --openbitfun-overlay-menu-item-gap: var(--openbitfun-space-2); + --openbitfun-overlay-menu-item-height: 30px; + --openbitfun-overlay-menu-item-icon-size: 14px; + --openbitfun-overlay-menu-item-padding-inline: var(--openbitfun-space-2); + --openbitfun-overlay-menu-item-radius: var(--openbitfun-radius-base); + --openbitfun-overlay-menu-max-block-size: 480px; + --openbitfun-overlay-menu-scrollbar-gap: 2px; + --openbitfun-overlay-menu-section-gap: var(--openbitfun-space-2); + --openbitfun-overlay-menu-surface-padding: var(--openbitfun-space-2); + --openbitfun-overlay-menu-surface-radius: var(--openbitfun-radius-xl); + --openbitfun-overlay-tooltip-arrow-size: 8px; + --openbitfun-overlay-tooltip-gap: var(--openbitfun-space-2); + --openbitfun-overlay-tooltip-max-block-size: 320px; + --openbitfun-overlay-tooltip-max-inline-size: 280px; + --openbitfun-overlay-tooltip-padding-block: 6px; + --openbitfun-overlay-tooltip-padding-inline: 10px; + --openbitfun-overlay-tooltip-surface-radius: var(--openbitfun-radius-sm); + --openbitfun-radius-2xl: 20px; + --openbitfun-radius-3xl: 24px; + --openbitfun-radius-4xl: 32px; + --openbitfun-radius-base: 8px; + --openbitfun-radius-lg: 12px; + --openbitfun-radius-md: var(--openbitfun-radius-base); + --openbitfun-radius-pill: 9999px; + --openbitfun-radius-sm: 6px; + --openbitfun-radius-xl: 16px; + --openbitfun-radius-xs: 4px; + --openbitfun-scrollbar-radius: var(--openbitfun-radius-pill); + --openbitfun-scrollbar-width: 6px; + --openbitfun-space-0: 0px; + --openbitfun-space-1: 4px; + --openbitfun-space-10: 40px; + --openbitfun-space-12: 48px; + --openbitfun-space-16: 64px; + --openbitfun-space-2: 8px; + --openbitfun-space-3: 12px; + --openbitfun-space-4: 16px; + --openbitfun-space-5: 20px; + --openbitfun-space-6: 24px; + --openbitfun-space-8: 32px; + --openbitfun-space-component-block: 8px; + --openbitfun-space-component-inline: 12px; + --openbitfun-type-body-lg-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-body-lg-font-size: var(--openbitfun-font-size-lg); + --openbitfun-type-body-lg-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-body-lg-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-body-lg-line-height: var(--openbitfun-line-height-relaxed); + --openbitfun-type-body-md-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-body-md-font-size: var(--openbitfun-font-size-base); + --openbitfun-type-body-md-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-body-md-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-body-md-line-height: var(--openbitfun-line-height-base); + --openbitfun-type-body-sm-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-body-sm-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-body-sm-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-body-sm-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-body-sm-line-height: var(--openbitfun-line-height-base); + --openbitfun-type-body-xs-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-body-xs-font-size: var(--openbitfun-font-size-xs); + --openbitfun-type-body-xs-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-body-xs-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-body-xs-line-height: var(--openbitfun-line-height-base); + --openbitfun-type-code-md-font-family: var(--openbitfun-font-family-mono); + --openbitfun-type-code-md-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-code-md-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-code-md-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-code-md-line-height: var(--openbitfun-line-height-code); + --openbitfun-type-code-sm-font-family: var(--openbitfun-font-family-mono); + --openbitfun-type-code-sm-font-size: var(--openbitfun-font-size-xs); + --openbitfun-type-code-sm-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-code-sm-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-code-sm-line-height: var(--openbitfun-line-height-code); + --openbitfun-type-display-fluid-hero-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-fluid-hero-font-size: clamp(var(--openbitfun-font-size-6xl), 4vw, var(--openbitfun-font-size-8xl)); + --openbitfun-type-display-fluid-hero-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-fluid-hero-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-fluid-hero-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-display-fluid-title-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-fluid-title-font-size: clamp(var(--openbitfun-font-size-5xl), 3.2vw, var(--openbitfun-font-size-7xl)); + --openbitfun-type-display-fluid-title-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-fluid-title-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-fluid-title-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-display-lg-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-lg-font-size: var(--openbitfun-font-size-7xl); + --openbitfun-type-display-lg-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-lg-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-lg-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-display-md-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-md-font-size: var(--openbitfun-font-size-6xl); + --openbitfun-type-display-md-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-md-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-md-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-display-sm-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-sm-font-size: var(--openbitfun-font-size-5xl); + --openbitfun-type-display-sm-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-sm-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-sm-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-display-xl-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-xl-font-size: var(--openbitfun-font-size-8xl); + --openbitfun-type-display-xl-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-xl-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-xl-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-display-xxl-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-display-xxl-font-size: var(--openbitfun-font-size-9xl); + --openbitfun-type-display-xxl-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-display-xxl-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-display-xxl-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-flow-body-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-flow-body-font-size: var(--openbitfun-font-size-base); + --openbitfun-type-flow-body-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-body-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-body-line-height: var(--openbitfun-line-height-reading); + --openbitfun-type-flow-code-font-family: var(--openbitfun-font-family-mono); + --openbitfun-type-flow-code-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-flow-code-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-code-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-code-line-height: var(--openbitfun-line-height-code); + --openbitfun-type-flow-control-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-flow-control-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-flow-control-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-control-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-control-line-height: var(--openbitfun-line-height-support); + --openbitfun-type-flow-lead-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-flow-lead-font-size: var(--openbitfun-font-size-lg); + --openbitfun-type-flow-lead-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-lead-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-lead-line-height: var(--openbitfun-line-height-relaxed); + --openbitfun-type-flow-meta-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-flow-meta-font-size: var(--openbitfun-font-size-meta); + --openbitfun-type-flow-meta-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-meta-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-meta-line-height: var(--openbitfun-line-height-compact); + --openbitfun-type-flow-micro-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-flow-micro-font-size: var(--openbitfun-font-size-micro); + --openbitfun-type-flow-micro-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-micro-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-micro-line-height: var(--openbitfun-line-height-compact); + --openbitfun-type-flow-page-title-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-flow-page-title-font-size: var(--openbitfun-font-size-3xl); + --openbitfun-type-flow-page-title-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-flow-page-title-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-flow-page-title-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-flow-section-title-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-flow-section-title-font-size: var(--openbitfun-font-size-2xl); + --openbitfun-type-flow-section-title-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-flow-section-title-letter-spacing: var(--openbitfun-letter-spacing-snug); + --openbitfun-type-flow-section-title-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-flow-support-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-flow-support-font-size: var(--openbitfun-font-size-xs); + --openbitfun-type-flow-support-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-flow-support-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-flow-support-line-height: var(--openbitfun-line-height-support); + --openbitfun-type-flow-title-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-flow-title-font-size: var(--openbitfun-font-size-xl); + --openbitfun-type-flow-title-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-flow-title-letter-spacing: var(--openbitfun-letter-spacing-snug); + --openbitfun-type-flow-title-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-card-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-heading-card-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-heading-card-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-card-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-heading-card-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-compact-page-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-heading-compact-page-font-size: var(--openbitfun-font-size-2xl-plus); + --openbitfun-type-heading-compact-page-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-compact-page-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-heading-compact-page-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-dialog-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-heading-dialog-font-size: var(--openbitfun-font-size-3xl); + --openbitfun-type-heading-dialog-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-dialog-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-heading-dialog-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-display-font-family: var(--openbitfun-font-family-sans); + --openbitfun-type-heading-display-font-size: var(--openbitfun-font-size-4xl); + --openbitfun-type-heading-display-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-display-letter-spacing: var(--openbitfun-letter-spacing-tight); + --openbitfun-type-heading-display-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-heading-navigation-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-heading-navigation-font-size: var(--openbitfun-font-size-xl-plus); + --openbitfun-type-heading-navigation-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-navigation-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-heading-navigation-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-page-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-heading-page-font-size: var(--openbitfun-font-size-3xl-plus); + --openbitfun-type-heading-page-font-weight: var(--openbitfun-font-weight-bold); + --openbitfun-type-heading-page-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-heading-page-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-section-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-heading-section-font-size: var(--openbitfun-font-size-lg); + --openbitfun-type-heading-section-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-section-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-heading-section-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-label-lg-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-label-lg-font-size: var(--openbitfun-font-size-base); + --openbitfun-type-label-lg-font-weight: var(--openbitfun-font-weight-medium); + --openbitfun-type-label-lg-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-label-lg-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-label-md-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-label-md-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-label-md-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-label-md-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-label-md-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-label-selected-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-label-selected-font-size: var(--openbitfun-font-size-sm); + --openbitfun-type-label-selected-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-label-selected-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-label-selected-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-label-sm-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-label-sm-font-size: var(--openbitfun-font-size-xs); + --openbitfun-type-label-sm-font-weight: var(--openbitfun-font-weight-medium); + --openbitfun-type-label-sm-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-label-sm-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-label-xs-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-label-xs-font-size: var(--openbitfun-font-size-meta); + --openbitfun-type-label-xs-font-weight: var(--openbitfun-font-weight-medium); + --openbitfun-type-label-xs-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-label-xs-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-meta-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-meta-font-size: var(--openbitfun-font-size-meta); + --openbitfun-type-meta-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-meta-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-meta-line-height: var(--openbitfun-line-height-compact); + --openbitfun-type-micro-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-micro-font-size: var(--openbitfun-font-size-micro); + --openbitfun-type-micro-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-micro-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-micro-line-height: var(--openbitfun-line-height-compact); + --openbitfun-type-modifier-leading-balanced-line-height: var(--openbitfun-line-height-balanced); + --openbitfun-type-modifier-leading-dense-line-height: var(--openbitfun-line-height-dense); + --openbitfun-type-modifier-leading-loose-line-height: var(--openbitfun-line-height-loose); + --openbitfun-type-modifier-leading-none-line-height: var(--openbitfun-line-height-none); + --openbitfun-type-modifier-leading-snug-line-height: var(--openbitfun-line-height-snug); + --openbitfun-type-modifier-leading-spacious-line-height: var(--openbitfun-line-height-spacious); + --openbitfun-type-modifier-leading-support-line-height: var(--openbitfun-line-height-support); + --openbitfun-type-modifier-leading-ui-line-height: var(--openbitfun-line-height-ui); + --openbitfun-type-modifier-tracking-caps-letter-spacing: var(--openbitfun-letter-spacing-caps); + --openbitfun-type-modifier-tracking-expanded-letter-spacing: var(--openbitfun-letter-spacing-expanded); + --openbitfun-type-modifier-tracking-subtle-letter-spacing: var(--openbitfun-letter-spacing-subtle); + --openbitfun-type-modifier-tracking-tighter-letter-spacing: var(--openbitfun-letter-spacing-tighter); + --openbitfun-type-modifier-tracking-wide-letter-spacing: var(--openbitfun-letter-spacing-wide); + --openbitfun-type-modifier-tracking-wider-letter-spacing: var(--openbitfun-letter-spacing-wider); + --openbitfun-type-modifier-tracking-widest-letter-spacing: var(--openbitfun-letter-spacing-widest); + --openbitfun-type-overline-micro-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-overline-micro-font-size: var(--openbitfun-font-size-4xs); + --openbitfun-type-overline-micro-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-overline-micro-letter-spacing: var(--openbitfun-letter-spacing-widest); + --openbitfun-type-overline-micro-line-height: var(--openbitfun-line-height-none); + --openbitfun-type-overline-sm-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-overline-sm-font-size: var(--openbitfun-font-size-2xs); + --openbitfun-type-overline-sm-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-overline-sm-letter-spacing: var(--openbitfun-letter-spacing-wider); + --openbitfun-type-overline-sm-line-height: var(--openbitfun-line-height-none); + --openbitfun-type-overline-xs-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-overline-xs-font-size: var(--openbitfun-font-size-3xs); + --openbitfun-type-overline-xs-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-overline-xs-letter-spacing: var(--openbitfun-letter-spacing-widest); + --openbitfun-type-overline-xs-line-height: var(--openbitfun-line-height-none); + --openbitfun-type-support-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-support-font-size: var(--openbitfun-font-size-meta); + --openbitfun-type-support-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-support-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-support-line-height: var(--openbitfun-line-height-comfortable); + } +} + +@layer openbitfun.tokens.system { + :where([data-openbitfun-design-system-root][data-density="compact"]) { + --openbitfun-control-height-lg: 44px; + --openbitfun-control-height-md: 36px; + --openbitfun-control-height-sm: 28px; + --openbitfun-control-hit-target: 36px; + --openbitfun-control-launcher-button-block-size: var(--openbitfun-control-height-md); + --openbitfun-control-tab-group-item-height: var(--openbitfun-control-height-md); + --openbitfun-space-component-block: 6px; + --openbitfun-space-component-inline: 10px; + } +} + +@layer openbitfun.tokens.system { + :where([data-openbitfun-design-system-root][data-density="touch"]) { + --openbitfun-control-height-lg: 56px; + --openbitfun-control-height-md: 48px; + --openbitfun-control-height-sm: 40px; + --openbitfun-control-hit-target: 48px; + --openbitfun-control-launcher-button-block-size: var(--openbitfun-control-height-md); + --openbitfun-control-tab-group-item-height: var(--openbitfun-control-height-md); + --openbitfun-space-component-block: 12px; + --openbitfun-space-component-inline: 16px; + } +} + +@media (prefers-reduced-motion: reduce) { + + :where([data-openbitfun-design-system-root]) { + + --openbitfun-motion-duration-fast: 0ms; + + --openbitfun-motion-duration-normal: 0ms; + + --openbitfun-motion-duration-slow: 0ms; + + --openbitfun-motion-duration-loop: 0ms; + + } + +} +@layer openbitfun.tokens.theme { + :where([data-openbitfun-design-system-root]), :where([data-openbitfun-design-system-root][data-color-scheme="light"]) { + --openbitfun-color-accent-border: color-mix(in srgb, #059cb0 40%, transparent); + --openbitfun-color-accent-border-subtle: color-mix(in srgb, #059cb0 25%, transparent); + --openbitfun-color-accent-default: #059cb0; + --openbitfun-color-accent-disabled: rgba(5, 156, 176, 0.30); + --openbitfun-color-accent-hover: #059cb0; + --openbitfun-color-accent-secondary: #7c6b99; + --openbitfun-color-accent-secondary-border: color-mix(in srgb, #7c6b99 25%, transparent); + --openbitfun-color-accent-secondary-hover: #655680; + --openbitfun-color-accent-secondary-surface: color-mix(in srgb, #7c6b99 9%, transparent); + --openbitfun-color-accent-secondary-surface-strong: color-mix(in srgb, #7c6b99 15%, transparent); + --openbitfun-color-accent-surface: color-mix(in srgb, #059cb0 9%, transparent); + --openbitfun-color-accent-surface-strong: color-mix(in srgb, #059cb0 15%, transparent); + --openbitfun-color-accent-surface-subtle: color-mix(in srgb, #059cb0 5%, transparent); + --openbitfun-color-action-neutral-border: rgba(0, 0, 0, 0.05); + --openbitfun-color-action-neutral-content: rgba(0, 0, 0, 0.80); + --openbitfun-color-action-neutral-content-disabled: rgba(0, 0, 0, 0.30); + --openbitfun-color-action-neutral-fill-border: rgba(0, 0, 0, 0.05); + --openbitfun-color-action-neutral-surface: rgba(0, 0, 0, 0.05); + --openbitfun-color-action-neutral-surface-hover: rgba(0, 0, 0, 0.08); + --openbitfun-color-action-neutral-surface-pressed: rgba(0, 0, 0, 0.10); + --openbitfun-color-action-primary-background: #101a27; + --openbitfun-color-action-primary-content: #ffffff; + --openbitfun-color-action-primary-hover: #1c1c1f; + --openbitfun-color-action-primary-pressed: #000000; + --openbitfun-color-action-quiet-content: rgba(0, 0, 0, 0.60); + --openbitfun-color-action-quiet-hover: #f3f3f5; + --openbitfun-color-action-quiet-pressed: rgba(16, 26, 39, 0.09); + --openbitfun-color-action-secondary-background: #f3f3f5; + --openbitfun-color-action-secondary-content: rgba(0, 0, 0, 0.80); + --openbitfun-color-action-secondary-hover: rgba(16, 26, 39, 0.09); + --openbitfun-color-action-secondary-pressed: rgba(16, 26, 39, 0.13); + --openbitfun-color-border-default: rgba(16, 26, 39, 0.15); + --openbitfun-color-border-strong: rgba(16, 26, 39, 0.34); + --openbitfun-color-border-subtle: rgba(16, 26, 39, 0.08); + --openbitfun-color-code-change-added: #1aa73e; + --openbitfun-color-code-change-removed: #ec221f; + --openbitfun-color-content-disabled: rgba(0, 0, 0, 0.30); + --openbitfun-color-content-inverse: #ffffff; + --openbitfun-color-content-muted: #6a6a6a; + --openbitfun-color-content-on-dark: #ffffff; + --openbitfun-color-content-on-light: #000000; + --openbitfun-color-content-primary: rgba(0, 0, 0, 0.80); + --openbitfun-color-content-required-indicator: #059cb0; + --openbitfun-color-content-secondary: rgba(0, 0, 0, 0.60); + --openbitfun-color-control-highlight-background: #059cb0; + --openbitfun-color-control-highlight-content: #000000; + --openbitfun-color-control-launcher-background: rgba(0, 0, 0, 0.10); + --openbitfun-color-control-launcher-background-hover: color-mix(in srgb, #059cb0 20%, transparent); + --openbitfun-color-control-launcher-background-pressed: color-mix(in srgb, #059cb0 30%, transparent); + --openbitfun-color-control-launcher-content: rgba(0, 0, 0, 0.80); + --openbitfun-color-control-launcher-content-disabled: rgba(0, 0, 0, 0.30); + --openbitfun-color-control-launcher-content-hover: #059cb0; + --openbitfun-color-control-launcher-content-pressed: #059cb0; + --openbitfun-color-control-switch-thumb: #ffffff; + --openbitfun-color-control-switch-track: #dddddd; + --openbitfun-color-control-switch-track-checked: #059cb0; + --openbitfun-color-field-background: #ffffff; + --openbitfun-color-field-background-hover: #ffffff; + --openbitfun-color-field-border: rgba(16, 26, 39, 0.15); + --openbitfun-color-field-border-focus: #858585; + --openbitfun-color-field-border-hover: rgba(16, 26, 39, 0.24); + --openbitfun-color-focus-ring: #6a6a6a; + --openbitfun-color-identity-assistant-border: color-mix(in srgb, #db2777 15%, transparent); + --openbitfun-color-identity-assistant-content: #db2777; + --openbitfun-color-identity-assistant-content-hover: #be185d; + --openbitfun-color-identity-assistant-surface: color-mix(in srgb, #db2777 9%, transparent); + --openbitfun-color-identity-assistant-surface-subtle: color-mix(in srgb, #db2777 5%, transparent); + --openbitfun-color-identity-global-search-new-project: #3271d7; + --openbitfun-color-identity-global-search-new-session: #ec221f; + --openbitfun-color-identity-global-search-open-browser: #ff8c00; + --openbitfun-color-identity-global-search-open-files: #9e54ff; + --openbitfun-color-identity-global-search-open-project: #059cb0; + --openbitfun-color-identity-global-search-open-terminal: rgba(0, 0, 0, 0.80); + --openbitfun-color-identity-harness-creative: #2e7eff; + --openbitfun-color-identity-harness-minimal: #b434ef; + --openbitfun-color-identity-harness-standard: #1aa73e; + --openbitfun-color-identity-harness-ultimate: #ff8c00; + --openbitfun-color-key-hint-background: rgba(0, 0, 0, 0.05); + --openbitfun-color-link-default: #2563eb; + --openbitfun-color-link-hover: #005fcc; + --openbitfun-color-overlay-scrim: rgba(16, 26, 39, 0.20); + --openbitfun-color-scrollbar-thumb: rgba(0, 0, 0, 0.08); + --openbitfun-color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.10); + --openbitfun-color-selection-surface: rgba(0, 0, 0, 0.08); + --openbitfun-color-status-danger-border: rgba(236, 34, 31, 0.3); + --openbitfun-color-status-danger-content: #bd1b19; + --openbitfun-color-status-danger-emphasis: #ec221f; + --openbitfun-color-status-danger-surface: rgba(236, 34, 31, 0.1); + --openbitfun-color-status-info-border: rgba(46, 126, 255, 0.3); + --openbitfun-color-status-info-content: #2360c2; + --openbitfun-color-status-info-emphasis: #2e7eff; + --openbitfun-color-status-info-surface: rgba(46, 126, 255, 0.1); + --openbitfun-color-status-success-border: rgba(26, 167, 62, 0.3); + --openbitfun-color-status-success-content: #12722a; + --openbitfun-color-status-success-emphasis: #1aa73e; + --openbitfun-color-status-success-surface: rgba(26, 167, 62, 0.1); + --openbitfun-color-status-warning-border: rgba(255, 140, 0, 0.3); + --openbitfun-color-status-warning-content: #8a4c00; + --openbitfun-color-status-warning-emphasis: #ff8c00; + --openbitfun-color-status-warning-surface: rgba(255, 140, 0, 0.1); + --openbitfun-color-surface-canvas: #fdfdfd; + --openbitfun-color-surface-chrome: #f8f8f9; + --openbitfun-color-surface-panel: #ffffff; + --openbitfun-color-surface-raised: #ffffff; + --openbitfun-color-surface-scene: #ffffff; + --openbitfun-color-surface-subtle: rgba(16, 26, 39, 0.03); + --openbitfun-color-surface-tertiary: #f7f7f7; + --openbitfun-color-surface-workbench: #f3f3f5; + --openbitfun-effect-blur-base: blur(8px) saturate(1.05); + --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); + --openbitfun-effect-blur-subtle: blur(4px) saturate(1.02); + --openbitfun-opacity-disabled: 0.55; + --openbitfun-opacity-focus: 0.9; + --openbitfun-opacity-hover: 0.75; + --openbitfun-opacity-muted: 0.75; + --openbitfun-shadow-accent-glow: 0 12px 32px color-mix(in srgb, #059cb0 25%, transparent), 0 6px 16px color-mix(in srgb, #059cb0 18%, transparent), 0 3px 8px rgba(0, 0, 0, 0.12); + --openbitfun-shadow-base: 0 4px 8px rgba(16, 26, 39, 0.07); + --openbitfun-shadow-composer: 0 2px 6px rgba(0, 0, 0, 0.08); + --openbitfun-shadow-inner-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.08); + --openbitfun-shadow-inner-highlight-hover: inset 0 1px 0 rgba(255, 255, 255, 0.24); + --openbitfun-shadow-lg: 0 8px 16px rgba(16, 26, 39, 0.09); + --openbitfun-shadow-menu: 0 4px 10px rgba(0, 0, 0, 0.12); + --openbitfun-shadow-overlay: 0 4px 20px rgba(0, 0, 0, 0.12); + --openbitfun-shadow-raised: 0 2px 4px rgba(16, 26, 39, 0.055); + --openbitfun-shadow-sm: 0 2px 4px rgba(16, 26, 39, 0.055); + --openbitfun-shadow-xl: 0 12px 24px rgba(16, 26, 39, 0.11); + --openbitfun-shadow-xs: 0 1px 2px rgba(16, 26, 39, 0.04); + } +} + +@layer openbitfun.tokens.theme { + :where([data-openbitfun-design-system-root][data-color-scheme="dark"]) { + --openbitfun-color-accent-border: color-mix(in srgb, #60a5fa 40%, transparent); + --openbitfun-color-accent-border-subtle: color-mix(in srgb, #60a5fa 25%, transparent); + --openbitfun-color-accent-default: #60a5fa; + --openbitfun-color-accent-disabled: rgba(96, 165, 250, 0.30); + --openbitfun-color-accent-hover: #3b82f6; + --openbitfun-color-accent-secondary: #8b5cf6; + --openbitfun-color-accent-secondary-border: color-mix(in srgb, #8b5cf6 25%, transparent); + --openbitfun-color-accent-secondary-hover: #7c6b99; + --openbitfun-color-accent-secondary-surface: color-mix(in srgb, #8b5cf6 9%, transparent); + --openbitfun-color-accent-secondary-surface-strong: color-mix(in srgb, #8b5cf6 15%, transparent); + --openbitfun-color-accent-surface: color-mix(in srgb, #60a5fa 9%, transparent); + --openbitfun-color-accent-surface-strong: color-mix(in srgb, #60a5fa 15%, transparent); + --openbitfun-color-accent-surface-subtle: color-mix(in srgb, #60a5fa 5%, transparent); + --openbitfun-color-action-neutral-border: rgba(255, 255, 255, 0.18); + --openbitfun-color-action-neutral-content: #b0b0b0; + --openbitfun-color-action-neutral-content-disabled: #555555; + --openbitfun-color-action-neutral-fill-border: rgba(255, 255, 255, 0.1); + --openbitfun-color-action-neutral-surface: rgba(255, 255, 255, 0.1); + --openbitfun-color-action-neutral-surface-hover: rgba(255, 255, 255, 0.12); + --openbitfun-color-action-neutral-surface-pressed: rgba(255, 255, 255, 0.15); + --openbitfun-color-action-primary-background: rgba(255, 255, 255, 0.16); + --openbitfun-color-action-primary-content: #f3f3f5; + --openbitfun-color-action-primary-hover: rgba(255, 255, 255, 0.24); + --openbitfun-color-action-primary-pressed: rgba(255, 255, 255, 0.2); + --openbitfun-color-action-quiet-content: #b0b0b0; + --openbitfun-color-action-quiet-hover: rgba(255, 255, 255, 0.06); + --openbitfun-color-action-quiet-pressed: rgba(255, 255, 255, 0.1); + --openbitfun-color-action-secondary-background: rgba(255, 255, 255, 0.06); + --openbitfun-color-action-secondary-content: #e8e8e8; + --openbitfun-color-action-secondary-hover: rgba(255, 255, 255, 0.1); + --openbitfun-color-action-secondary-pressed: rgba(255, 255, 255, 0.12); + --openbitfun-color-border-default: rgba(255, 255, 255, 0.18); + --openbitfun-color-border-strong: rgba(255, 255, 255, 0.3); + --openbitfun-color-border-subtle: rgba(255, 255, 255, 0.12); + --openbitfun-color-code-change-added: #1aa73e; + --openbitfun-color-code-change-removed: #ec221f; + --openbitfun-color-content-disabled: #555555; + --openbitfun-color-content-inverse: #0e0e10; + --openbitfun-color-content-muted: #858585; + --openbitfun-color-content-on-dark: #ffffff; + --openbitfun-color-content-on-light: #000000; + --openbitfun-color-content-primary: #e8e8e8; + --openbitfun-color-content-required-indicator: #059cb0; + --openbitfun-color-content-secondary: #b0b0b0; + --openbitfun-color-control-highlight-background: #059cb0; + --openbitfun-color-control-highlight-content: #000000; + --openbitfun-color-control-launcher-background: rgba(255, 255, 255, 0.15); + --openbitfun-color-control-launcher-background-hover: color-mix(in srgb, #059cb0 20%, transparent); + --openbitfun-color-control-launcher-background-pressed: color-mix(in srgb, #059cb0 30%, transparent); + --openbitfun-color-control-launcher-content: #b0b0b0; + --openbitfun-color-control-launcher-content-disabled: #555555; + --openbitfun-color-control-launcher-content-hover: #059cb0; + --openbitfun-color-control-launcher-content-pressed: #059cb0; + --openbitfun-color-control-switch-thumb: #ffffff; + --openbitfun-color-control-switch-track: #555555; + --openbitfun-color-control-switch-track-checked: #059cb0; + --openbitfun-color-field-background: #1c1c1f; + --openbitfun-color-field-background-hover: rgba(255, 255, 255, 0.06); + --openbitfun-color-field-border: rgba(255, 255, 255, 0.18); + --openbitfun-color-field-border-focus: #858585; + --openbitfun-color-field-border-hover: rgba(255, 255, 255, 0.24); + --openbitfun-color-focus-ring: #60a5fa; + --openbitfun-color-identity-assistant-border: color-mix(in srgb, #ec4899 15%, transparent); + --openbitfun-color-identity-assistant-content: #ec4899; + --openbitfun-color-identity-assistant-content-hover: #f472b6; + --openbitfun-color-identity-assistant-surface: color-mix(in srgb, #ec4899 9%, transparent); + --openbitfun-color-identity-assistant-surface-subtle: color-mix(in srgb, #ec4899 5%, transparent); + --openbitfun-color-identity-global-search-new-project: #3271d7; + --openbitfun-color-identity-global-search-new-session: #ec221f; + --openbitfun-color-identity-global-search-open-browser: #ff8c00; + --openbitfun-color-identity-global-search-open-files: #9e54ff; + --openbitfun-color-identity-global-search-open-project: #059cb0; + --openbitfun-color-identity-global-search-open-terminal: #b0b0b0; + --openbitfun-color-identity-harness-creative: #2e7eff; + --openbitfun-color-identity-harness-minimal: #b434ef; + --openbitfun-color-identity-harness-standard: #1aa73e; + --openbitfun-color-identity-harness-ultimate: #ff8c00; + --openbitfun-color-key-hint-background: rgba(255, 255, 255, 0.1); + --openbitfun-color-link-default: #60a5fa; + --openbitfun-color-link-hover: #93c5fd; + --openbitfun-color-overlay-scrim: rgba(0, 0, 0, 0.56); + --openbitfun-color-scrollbar-thumb: rgba(255, 255, 255, 0.12); + --openbitfun-color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15); + --openbitfun-color-selection-surface: rgba(255, 255, 255, 0.12); + --openbitfun-color-status-danger-border: rgba(236, 34, 31, 0.3); + --openbitfun-color-status-danger-content: #f47f7d; + --openbitfun-color-status-danger-emphasis: #ec221f; + --openbitfun-color-status-danger-surface: rgba(236, 34, 31, 0.1); + --openbitfun-color-status-info-border: rgba(46, 126, 255, 0.3); + --openbitfun-color-status-info-content: #7dafff; + --openbitfun-color-status-info-emphasis: #2e7eff; + --openbitfun-color-status-info-surface: rgba(46, 126, 255, 0.1); + --openbitfun-color-status-success-border: rgba(26, 167, 62, 0.3); + --openbitfun-color-status-success-content: #5fc178; + --openbitfun-color-status-success-emphasis: #1aa73e; + --openbitfun-color-status-success-surface: rgba(26, 167, 62, 0.1); + --openbitfun-color-status-warning-border: rgba(255, 140, 0, 0.3); + --openbitfun-color-status-warning-content: #ff930f; + --openbitfun-color-status-warning-emphasis: #ff8c00; + --openbitfun-color-status-warning-surface: rgba(255, 140, 0, 0.1); + --openbitfun-color-surface-canvas: #0e0e10; + --openbitfun-color-surface-chrome: #0e0e10; + --openbitfun-color-surface-panel: #1c1c1f; + --openbitfun-color-surface-raised: #1c1c1f; + --openbitfun-color-surface-scene: #1c1c1f; + --openbitfun-color-surface-subtle: rgba(255, 255, 255, 0.06); + --openbitfun-color-surface-tertiary: #0e0e10; + --openbitfun-color-surface-workbench: #0e0e10; + --openbitfun-effect-blur-base: blur(8px) saturate(1.1); + --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); + --openbitfun-effect-blur-subtle: blur(4px) saturate(1.05); + --openbitfun-opacity-disabled: 0.6; + --openbitfun-opacity-focus: 0.9; + --openbitfun-opacity-hover: 0.8; + --openbitfun-opacity-muted: 0.8; + --openbitfun-shadow-accent-glow: 0 12px 32px color-mix(in srgb, #3b82f6 25%, transparent), 0 6px 16px color-mix(in srgb, #60a5fa 18%, transparent), 0 3px 8px rgba(0, 0, 0, 0.12); + --openbitfun-shadow-base: 0 4px 8px rgba(0, 0, 0, 0.7); + --openbitfun-shadow-composer: 0 2px 6px rgba(0, 0, 0, 0.32); + --openbitfun-shadow-inner-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.08); + --openbitfun-shadow-inner-highlight-hover: inset 0 1px 0 rgba(255, 255, 255, 0.24); + --openbitfun-shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.6); + --openbitfun-shadow-menu: 0 4px 10px rgba(0, 0, 0, 0.48); + --openbitfun-shadow-overlay: 0 4px 20px rgba(0, 0, 0, 0.48); + --openbitfun-shadow-raised: 0 2px 4px rgba(0, 0, 0, 0.8); + --openbitfun-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.8); + --openbitfun-shadow-xl: 0 12px 24px rgba(0, 0, 0, 0.5); + --openbitfun-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.9); + } +} + +@layer openbitfun.tokens.theme { + :where([data-openbitfun-design-system-root][data-color-scheme="light"][data-contrast="high"]) { + --openbitfun-color-action-neutral-border: #000000; + --openbitfun-color-action-neutral-content: #000000; + --openbitfun-color-action-neutral-content-disabled: #343434; + --openbitfun-color-action-neutral-fill-border: #000000; + --openbitfun-color-action-neutral-surface: #e6e6e6; + --openbitfun-color-action-neutral-surface-hover: #cccccc; + --openbitfun-color-action-neutral-surface-pressed: #b3b3b3; + --openbitfun-color-action-primary-background: #000000; + --openbitfun-color-action-primary-hover: #262626; + --openbitfun-color-action-quiet-content: #161616; + --openbitfun-color-action-secondary-content: #000000; + --openbitfun-color-border-default: #3b3b3b; + --openbitfun-color-border-strong: #000000; + --openbitfun-color-border-subtle: #707070; + --openbitfun-color-content-muted: #343434; + --openbitfun-color-content-primary: #000000; + --openbitfun-color-content-required-indicator: #005fcc; + --openbitfun-color-content-secondary: #161616; + --openbitfun-color-control-highlight-background: #005fcc; + --openbitfun-color-control-highlight-content: #ffffff; + --openbitfun-color-control-launcher-background: #b3b3b3; + --openbitfun-color-control-launcher-background-hover: color-mix(in srgb, #005fcc 20%, transparent); + --openbitfun-color-control-launcher-background-pressed: color-mix(in srgb, #005fcc 30%, transparent); + --openbitfun-color-control-launcher-content: #000000; + --openbitfun-color-control-launcher-content-disabled: #343434; + --openbitfun-color-control-launcher-content-hover: #005fcc; + --openbitfun-color-control-launcher-content-pressed: #005fcc; + --openbitfun-color-control-switch-track: #3b3b3b; + --openbitfun-color-control-switch-track-checked: #005fcc; + --openbitfun-color-field-border: #3b3b3b; + --openbitfun-color-field-border-focus: #005fcc; + --openbitfun-color-field-border-hover: #000000; + --openbitfun-color-focus-ring: #005fcc; + --openbitfun-color-identity-global-search-open-terminal: #000000; + --openbitfun-color-key-hint-background: #e6e6e6; + --openbitfun-color-scrollbar-thumb: #cccccc; + --openbitfun-color-scrollbar-thumb-hover: #b3b3b3; + --openbitfun-color-selection-surface: #cccccc; + --openbitfun-color-status-danger-content: #8e1413; + --openbitfun-color-status-info-content: #1c4c99; + --openbitfun-color-status-success-content: #0d541f; + --openbitfun-color-status-warning-content: #663800; + --openbitfun-color-status-warning-surface: rgba(255, 140, 0, 0.04); + --openbitfun-color-surface-canvas: #ffffff; + } +} + +@layer openbitfun.tokens.theme { + :where([data-openbitfun-design-system-root][data-color-scheme="dark"][data-contrast="high"]) { + --openbitfun-color-action-neutral-border: #ffffff; + --openbitfun-color-action-neutral-content: #ffffff; + --openbitfun-color-action-neutral-content-disabled: #d0d0d0; + --openbitfun-color-action-neutral-fill-border: #ffffff; + --openbitfun-color-action-neutral-surface: #343434; + --openbitfun-color-action-neutral-surface-hover: #505050; + --openbitfun-color-action-neutral-surface-pressed: #6b6b6b; + --openbitfun-color-action-primary-background: #ffffff; + --openbitfun-color-action-primary-content: #000000; + --openbitfun-color-action-primary-hover: #e6e6e6; + --openbitfun-color-action-primary-pressed: #ffffff; + --openbitfun-color-border-default: #b5b5b5; + --openbitfun-color-border-strong: #ffffff; + --openbitfun-color-border-subtle: #8a8a8a; + --openbitfun-color-content-muted: #d0d0d0; + --openbitfun-color-content-primary: #ffffff; + --openbitfun-color-content-required-indicator: #ffcc00; + --openbitfun-color-content-secondary: #f0f0f0; + --openbitfun-color-control-highlight-background: #ffcc00; + --openbitfun-color-control-launcher-background: #6b6b6b; + --openbitfun-color-control-launcher-background-hover: color-mix(in srgb, #ffcc00 20%, transparent); + --openbitfun-color-control-launcher-background-pressed: color-mix(in srgb, #ffcc00 30%, transparent); + --openbitfun-color-control-launcher-content: #ffffff; + --openbitfun-color-control-launcher-content-disabled: #d0d0d0; + --openbitfun-color-control-launcher-content-hover: #ffcc00; + --openbitfun-color-control-launcher-content-pressed: #ffcc00; + --openbitfun-color-control-switch-thumb: #000000; + --openbitfun-color-control-switch-track: #b5b5b5; + --openbitfun-color-control-switch-track-checked: #ffcc00; + --openbitfun-color-field-border: #b5b5b5; + --openbitfun-color-field-border-focus: #ffcc00; + --openbitfun-color-field-border-hover: #ffffff; + --openbitfun-color-focus-ring: #ffcc00; + --openbitfun-color-identity-global-search-open-terminal: #ffffff; + --openbitfun-color-key-hint-background: #343434; + --openbitfun-color-scrollbar-thumb: #505050; + --openbitfun-color-scrollbar-thumb-hover: #6b6b6b; + --openbitfun-color-selection-surface: #505050; + --openbitfun-color-status-danger-content: #f7a7a5; + --openbitfun-color-status-info-content: #abcbff; + --openbitfun-color-status-success-content: #afe0bb; + --openbitfun-color-status-warning-content: #ffa940; + --openbitfun-color-status-warning-surface: rgba(255, 140, 0, 0.04); + --openbitfun-color-surface-canvas: #000000; + --openbitfun-color-surface-chrome: #000000; + --openbitfun-color-surface-panel: #090909; + --openbitfun-color-surface-scene: #090909; + --openbitfun-color-surface-tertiary: #000000; + --openbitfun-color-surface-workbench: #000000; + } +} diff --git a/src/apps/data-migrator/ui/index.html b/src/apps/data-migrator/ui/index.html index 38dd720570..5efdedb074 100644 --- a/src/apps/data-migrator/ui/index.html +++ b/src/apps/data-migrator/ui/index.html @@ -1,11 +1,13 @@ - + OpenBitFun Data Migrator + + - +
@@ -15,6 +17,11 @@

Import data from BitFun

Choose what to bring forward. Your original BitFun data will not be deleted.

+
+ + + +
@@ -25,23 +32,19 @@

Import data from BitFun

Step 1

Legacy source

- Checking + Checking

-

+

+
+
-