diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml index 9a8239a5..8c75b142 100644 --- a/.github/workflows/octop-desktop.yml +++ b/.github/workflows/octop-desktop.yml @@ -1,5 +1,9 @@ -name: Octop Desktop Package +name: FreeOS Desktop Package +# Filename kept as octop-desktop.yml so auto-tag / existing dispatch keep working. +# Publishes FreeOS-desktop-* / FreeOS-portable-* (Windows NSIS .exe is the +# product installer: Octop shell + bundled openXYOS). +# # 正式发版:Auto Tag 在打 v* 后与 Release / Docker Publish 一并 dispatch。 # 产物 upsert 到同一 GitHub Release。手工补包:workflow_dispatch + release_tag # (不必跑在 v* ref 上)。attach_from_run 只挂已有 artifact、不重打。 @@ -11,6 +15,7 @@ on: pull_request: paths: - "desktop/**" + - "modules/openxyos/**" - ".github/workflows/octop-desktop.yml" workflow_dispatch: inputs: @@ -30,7 +35,7 @@ on: default: "" type: string attach_from_run: - description: "Existing workflow run ID — attach its Octop-* artifacts and skip the 6-platform rebuild" + description: "Existing workflow run ID — attach its FreeOS-* artifacts and skip the 6-platform rebuild" required: false default: "" type: string @@ -160,6 +165,12 @@ jobs: enable-cache: true python-version: "3.12" + - uses: actions/setup-node@v5 + with: + node-version: "20" + cache: npm + cache-dependency-path: modules/openxyos/package-lock.json + # GitHub Cache outages must not fail the build — PBS download is cheap enough. - name: Cache python-build-standalone downloads continue-on-error: true @@ -184,7 +195,7 @@ jobs: - name: Smoke import (native host only) run: | set -euo pipefail - staging="desktop/portable/release/Octop-${{ matrix.plat }}" + staging="desktop/portable/release/FreeOS-${{ matrix.plat }}" if [[ -x "${staging}/runtime/bin/python3" ]]; then py="${staging}/runtime/bin/python3" elif [[ -f "${staging}/runtime/python.exe" ]]; then @@ -209,6 +220,16 @@ jobs: PYTHONNOUSERSITE=1 \ "$py" desktop/portable/verify_imports.py \ "${verify_args[@]}" + if [[ ! -f "${staging}/org-sidecar/openxyos/backend/server.ts" ]]; then + echo "org-sidecar missing under ${staging}" >&2 + exit 1 + fi + if [[ -f "${staging}/org-sidecar/node/node.exe" || -x "${staging}/org-sidecar/node/bin/node" ]]; then + echo "org-sidecar Node runtime present" + else + echo "bundled Node missing under ${staging}/org-sidecar/node" >&2 + exit 1 + fi # macOS: fail if any native extension linked Homebrew/MacPorts paths. if [[ "${{ matrix.plat }}" == darwin-* ]]; then REPO_ROOT="$PWD" # shellcheck source=desktop/portable/_common.sh @@ -247,14 +268,14 @@ jobs: wails3 task package ARCH=${{ matrix.arch }} VERSION=${{ env.OCTOP_VERSION }} - PORTABLE_ZIP=../portable/release/Octop-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip + PORTABLE_ZIP=../portable/release/FreeOS-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip # archive: false — upload the prebuilt zip as-is. Default archive=true would # wrap it again, so Actions UI / "Download artifact" becomes zip-in-zip. # With archive:false, artifact name is the filename (name: is ignored). - uses: actions/upload-artifact@v7 with: - path: desktop/portable/release/Octop-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip + path: desktop/portable/release/FreeOS-portable-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.zip archive: false if-no-files-found: error retention-days: 14 @@ -262,7 +283,7 @@ jobs: - name: Upload bundled desktop package uses: actions/upload-artifact@v7 with: - path: desktop/src/bin/Octop-desktop-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.* + path: desktop/src/bin/FreeOS-desktop-${{ matrix.plat }}-${{ env.OCTOP_VERSION }}.* archive: false if-no-files-found: error retention-days: 14 @@ -310,7 +331,7 @@ jobs: # zips into directories, so files: release-assets/* would skip them. - uses: actions/download-artifact@v8 with: - pattern: Octop-* + pattern: FreeOS-* path: release-assets merge-multiple: true skip-decompress: true diff --git a/README.md b/README.md index b54814e6..7b1c14bf 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,25 @@ Operator detail: [docs/asset-loop.md](docs/asset-loop.md). ## Run FreeOS +### Windows 用户:下载安装包 → 安装 → 打开即用 FreeOS(Octop壳+组织能力) + +给非开发者的路径: + +1. 打开 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases/latest),下载 + `FreeOS-desktop-windows-amd64-.exe`(普通 64 位电脑)或 + `FreeOS-desktop-windows-arm64-.exe`(ARM 电脑)。 +2. 双击安装包。安装程序会放到「程序文件」并创建开始菜单和桌面快捷方式。 +3. 打开 **FreeOS**。第一次启动会解压内置运行环境(可能要一两分钟),然后出现设置向导。 +4. 设好管理员密码后即可聊天。组织控制台(openXYOS)已随安装包内置,无需再装 Node;侧栏 **Organization** 默认打开。 + +数据目录默认是 `%USERPROFILE%\.freeos`(可用环境变量 `FREEOS_HOME` 改)。旧版 Octop 的 `~/.octop` 仍会被识别。 + +安装包由 CI 工作流 **FreeOS Desktop Package**(文件名仍是 `.github/workflows/octop-desktop.yml`,给现有发版脚本用)在 Windows runner 上打出来。本仓库的云环境打不出 `.exe`;合并后由该 job 产出。 + ### Prerequisites - Python 3.12+ (the project uses [uv](https://docs.astral.sh/uv/)) -- Node.js 20.19+ only if you start the organization sidecar or rebuild the dashboard +- Node.js 20.19+ only if you start the organization sidecar from source, or rebuild the dashboard. The Windows installer already bundles Node + openXYOS. ### From this repository @@ -133,7 +148,7 @@ Inbound import compiles the blueprint **and** registers a FreeOS agent (`org-pol ## Host platform features (from Octop) -Unchanged in this fork: multi-user JWT, multi-agent chat, expert library, connectors, ACP, knowledge bases, bundled plugins, cron, terminal/browser/desktop surfaces. Upstream-oriented installers and desktop artifacts still refer to **Octop** and `~/.octop/` — use `freeos` / `FREEOS_HOME` here. +Unchanged in this fork: multi-user JWT, multi-agent chat, expert library, connectors, ACP, knowledge bases, bundled plugins, cron, terminal/browser/desktop surfaces. The **Windows desktop installer** is branded **FreeOS** and defaults to `~/.freeos` / `FREEOS_HOME`. The Python package, `octop` CLI alias, `OCTOP_*` env vars, and `octop.db` stay for compatibility. Longer host docs: [docs/user-guide.md](docs/user-guide.md), [docs/configuration.md](docs/configuration.md), [docs/architecture.md](docs/architecture.md), [README_CN.md](README_CN.md). @@ -151,7 +166,8 @@ Intentional for package compatibility (not a rebrand miss): - Python package name `octop` and `import octop` - CLI `octop` alongside `freeos` -- `OCTOP_*` environment variables +- `OCTOP_*` environment variables (desktop also sets `FREEOS_HOME`) - SQLite file `octop.db` inside the home directory +- CI workflow **filename** `.github/workflows/octop-desktop.yml` (display name is FreeOS Desktop Package) See [docs/architecture-integration.md](docs/architecture-integration.md) for the control/data-plane split and the running self-growth loop. diff --git a/README_CN.md b/README_CN.md index 5607704a..8b475e66 100644 --- a/README_CN.md +++ b/README_CN.md @@ -204,13 +204,13 @@ curl -fsSL https://finnie-1258344699.cos.ap-guangzhou.myqcloud.com/octop/install 完整安装选项见 [scripts/README.md](scripts/README.md)(`--version`、`--from-source`、`--mirror` 及 Windows 参数)。 -**桌面客户端**(图形界面,无需终端)— 从 [GitHub Releases](https://github.com/TencentCloud/Octop/releases/latest) 下载对应平台的安装包: +**Windows 用户:下载安装包 → 安装 → 打开即用 FreeOS(Octop壳+组织能力)** — 从 [GitHub Releases](https://github.com/XYAIStudio/FreeOS/releases/latest) 下载: | 平台 | 制品 | |------|------| -| Windows | `Octop-desktop-windows-amd64-.exe`(64 位)/ `Octop-desktop-windows-arm64-.exe`(ARM64)— NSIS 安装程序 | -| macOS | `Octop-desktop-darwin-arm64-.dmg`(Apple 芯片)/ `Octop-desktop-darwin-amd64-.dmg`(Intel) | -| Linux | `Octop-desktop-linux-amd64-.tar.gz` / `Octop-desktop-linux-arm64-.tar.gz` | +| Windows | `FreeOS-desktop-windows-amd64-.exe`(64 位)/ `FreeOS-desktop-windows-arm64-.exe`(ARM64)— NSIS 安装程序,内置 openXYOS,打开即可聊天+组织 | +| macOS | `FreeOS-desktop-darwin-arm64-.dmg`(Apple 芯片)/ `FreeOS-desktop-darwin-amd64-.dmg`(Intel) | +| Linux | `FreeOS-desktop-linux-amd64-.tar.gz` / `FreeOS-desktop-linux-arm64-.tar.gz` | | 飞牛 NAS(FnOS) | `Octop-fnos-docker-.fpk`(依赖 Docker)/ `Octop-fnos-native-.fpk`(无需 Docker)— 通过应用中心安装 | 桌面客户端说明见 [desktop/README.md](desktop/README.md),飞牛打包指南见 [fnos/README.md](fnos/README.md)。 diff --git a/dashboard/index.html b/dashboard/index.html index 020a434e..5e90c309 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -27,7 +27,7 @@ name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> - + FreeOS @@ -85,14 +85,14 @@ position: absolute; inset: 0; border-radius: 50%; - border: 3px solid rgba(232, 93, 117, 0.18); - border-top-color: #e85d75; + border: 3px solid rgba(0, 51, 255, 0.18); + border-top-color: #0033ff; animation: octop-boot-spin 0.85s linear infinite; } @media (prefers-color-scheme: dark) { #octop-boot-ring { - border-color: rgba(240, 139, 154, 0.22); - border-top-color: #f08b9a; + border-color: rgba(107, 140, 255, 0.22); + border-top-color: #6b8cff; } } #octop-boot-logo { diff --git a/dashboard/public/offline.html b/dashboard/public/offline.html index 07685863..9f51bd10 100644 --- a/dashboard/public/offline.html +++ b/dashboard/public/offline.html @@ -39,7 +39,7 @@ font-size: 22px; font-weight: 600; margin-bottom: 10px; - color: #f08b9a; + color: #6b8cff; } p { font-size: 14px; @@ -51,7 +51,7 @@ padding: 10px 28px; border-radius: 999px; border: none; - background: #e85d75; + background: #0033ff; color: #fff; font-size: 14px; font-weight: 500; @@ -59,7 +59,7 @@ transition: background 0.2s; } button:hover { - background: #d14a62; + background: #002ee6; } diff --git a/dashboard/src/components/ExpertColorPicker.tsx b/dashboard/src/components/ExpertColorPicker.tsx index a434c9c9..4910728f 100644 --- a/dashboard/src/components/ExpertColorPicker.tsx +++ b/dashboard/src/components/ExpertColorPicker.tsx @@ -20,7 +20,7 @@ function isCurated(value: string): value is ThemePalette { } /** - * Curated 8-swatch picker for expert/agent accent color, plus a custom + * Curated swatch picker for expert/agent accent color, plus a custom * color swatch backed by the Ant Design color picker (palette + hex input). * The onChange callback receives a palette key or a hex string. */ diff --git a/dashboard/src/components/PaletteSwitcher.tsx b/dashboard/src/components/PaletteSwitcher.tsx index beb63a8e..42c6b3d9 100644 --- a/dashboard/src/components/PaletteSwitcher.tsx +++ b/dashboard/src/components/PaletteSwitcher.tsx @@ -6,7 +6,7 @@ import { PALETTE_SWATCH, VALID_PALETTES } from "../styles/themePalettes"; import styles from "./PaletteSwitcher.module.less"; /** - * Curated 8-swatch brand palette picker plus a custom color swatch. + * Curated brand palette picker plus a custom color swatch. * The custom swatch opens the Ant Design color picker (palette + hex input); * picking a color switches the active brand palette to "custom". */ diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index 6a6902ce..a4f70763 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -3301,6 +3301,7 @@ "themeLight": "Light", "themeDark": "Dark", "palette": { + "freeos": "FreeOS", "rose": "Rose", "tech": "Tech", "indigo": "Indigo", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 92f28857..fa18703f 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -3298,6 +3298,7 @@ "themeLight": "浅色", "themeDark": "深色", "palette": { + "freeos": "FreeOS 蓝", "rose": "玫瑰", "tech": "科技", "indigo": "靛蓝", diff --git a/dashboard/src/pages/Agent/Memory/Overview.module.less b/dashboard/src/pages/Agent/Memory/Overview.module.less index 52d1470b..478aa33a 100644 --- a/dashboard/src/pages/Agent/Memory/Overview.module.less +++ b/dashboard/src/pages/Agent/Memory/Overview.module.less @@ -456,7 +456,7 @@ /* ---- Dark-mode tweaks: brighter accents, chart grid, dossier header ---- */ :global(html[data-theme="dark"]) { .tone_rose { - --tone-bg: rgba(240, 139, 154, 0.14); + --tone-bg: color-mix(in srgb, var(--fn-color-brand) 14%, transparent); } .tone_amber { --tone-bg: rgba(251, 191, 36, 0.14); @@ -477,7 +477,7 @@ .dossierHeader { background: linear-gradient( 135deg, - rgba(240, 139, 154, 0.1), + color-mix(in srgb, var(--fn-color-brand) 10%, transparent), transparent 46% ); } diff --git a/dashboard/src/pages/Chat/chatInputPickers.partial.less b/dashboard/src/pages/Chat/chatInputPickers.partial.less index 4721da58..84a0b66f 100644 --- a/dashboard/src/pages/Chat/chatInputPickers.partial.less +++ b/dashboard/src/pages/Chat/chatInputPickers.partial.less @@ -129,7 +129,7 @@ } &:hover { - color: var(--fn-color-brand, #e85d75); + color: var(--fn-color-brand, #0033ff); } &:focus-visible { @@ -239,13 +239,13 @@ } .reasoningMenuChoiceActive { - background: rgba(232, 93, 117, 0.08); - color: var(--fn-color-brand, #e85d75); + background: var(--fn-color-brand-bg, rgba(0, 51, 255, 0.08)); + color: var(--fn-color-brand, #0033ff); } .reasoningStatusRow { justify-content: flex-start; - color: var(--fn-color-brand, #e85d75); + color: var(--fn-color-brand, #0033ff); span { flex: 1; diff --git a/dashboard/src/styles/appearanceStorage.test.ts b/dashboard/src/styles/appearanceStorage.test.ts index c46a87ad..6a3e15d0 100644 --- a/dashboard/src/styles/appearanceStorage.test.ts +++ b/dashboard/src/styles/appearanceStorage.test.ts @@ -17,7 +17,7 @@ afterEach(() => { }); describe("appearanceStorage", () => { - it("defaults to system preference and rose palette", () => { + it("defaults to system preference and the FreeOS palette", () => { expect(readStoredAppearance().preference).toBe("system"); expect(readStoredAppearance().palette).toBe(DEFAULT_PALETTE); }); diff --git a/dashboard/src/styles/theme-vars.css b/dashboard/src/styles/theme-vars.css index 4ab63214..2ff64af2 100644 --- a/dashboard/src/styles/theme-vars.css +++ b/dashboard/src/styles/theme-vars.css @@ -1,7 +1,7 @@ /* =================================================================== Octop Design System v3 – CSS Custom Properties - Default brand: Elegant Rose #E85D75 - Palettes via html[data-palette="rose|tech|indigo|teal|violet|emerald|amber|slate"] + Default brand: FreeOS logo-center blue #0033FF + Palettes via html[data-palette="freeos|rose|tech|indigo|teal|violet|emerald|amber|slate"] Mode via html[data-theme="light|dark"] =================================================================== */ @@ -14,9 +14,9 @@ html[data-theme="light"] { --fn-bg-tertiary: #f0f1f3; --fn-bg-elevated: #ffffff; --fn-bg-layout: #f5f6f8; - --fn-bg-hover: rgba(232, 93, 117, 0.04); - --fn-bg-active: rgba(232, 93, 117, 0.08); - --fn-bg-selected: rgba(232, 93, 117, 0.06); + --fn-bg-hover: rgba(0, 51, 255, 0.04); + --fn-bg-active: rgba(0, 51, 255, 0.08); + --fn-bg-selected: rgba(0, 51, 255, 0.06); --fn-bg-code: #f5f6f8; --fn-bg-toolbar: #fafbfc; --fn-bg-toolbar-hover: #f0f1f3; @@ -36,8 +36,8 @@ html[data-theme="light"] { --fn-text-placeholder: #c0c5ce; --fn-text-disabled: #d1d5db; --fn-text-inverse: #ffffff; - --fn-text-brand: #e85d75; - --fn-logo-color: #e05040; + --fn-text-brand: #0033ff; + --fn-logo-color: #0033ff; --fn-logo-filter: none; /* ---- Borders ---- */ @@ -46,33 +46,33 @@ html[data-theme="light"] { --fn-border-tertiary: #f9fafb; --fn-border-input: #d1d5db; --fn-border-input-alt: #e5e7eb; - --fn-border-focus: #e85d75; + --fn-border-focus: #0033ff; - /* ---- Brand (rose) ---- */ - --fn-color-brand: #e85d75; - --fn-color-brand-hover: #d14a62; - --fn-color-brand-soft: #d14a62; + /* ---- Brand (FreeOS blue) ---- */ + --fn-color-brand: #0033ff; + --fn-color-brand-hover: #002ee6; + --fn-color-brand-soft: #002ee6; --fn-color-brand-border: color-mix( in srgb, var(--fn-color-brand) 28%, transparent ); --fn-color-on-brand: #ffffff; - --fn-color-brand-bg: rgba(232, 93, 117, 0.06); - --fn-color-brand-light: #fde8ec; - --fn-color-brand-shadow: rgba(232, 93, 117, 0.15); - --fn-color-brand-glow: rgba(232, 93, 117, 0.08); + --fn-color-brand-bg: rgba(0, 51, 255, 0.06); + --fn-color-brand-light: #e6ebff; + --fn-color-brand-shadow: rgba(0, 51, 255, 0.15); + --fn-color-brand-glow: rgba(0, 51, 255, 0.08); /* ---- AI message bubble (frosted glass) ---- */ --fn-assistant-bubble-bg: rgba(255, 255, 255, 0.45); --fn-assistant-bubble-bg-gradient: linear-gradient( 135deg, - rgba(255, 240, 243, 0.6) 0%, + rgba(230, 235, 255, 0.7) 0%, rgba(255, 255, 255, 0.35) 50%, - rgba(255, 243, 245, 0.5) 100% + rgba(214, 222, 255, 0.45) 100% ); - --fn-assistant-bubble-border: rgba(232, 93, 117, 0.12); - --fn-assistant-glow-color: 232, 93, 117; + --fn-assistant-bubble-border: rgba(0, 51, 255, 0.12); + --fn-assistant-glow-color: 0, 51, 255; /* ---- Semantic ---- */ --fn-color-success: #10b981; @@ -97,7 +97,7 @@ html[data-theme="light"] { --fn-tag-remote-bg: #fff7ed; --fn-tag-remote-text: #f97316; --fn-tag-remote-border: #fed7aa; - --fn-tag-channel-text: #e85d75; + --fn-tag-channel-text: #0033ff; /* ---- Shadows ---- */ --fn-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04); @@ -108,15 +108,15 @@ html[data-theme="light"] { 0 4px 6px -4px rgba(0, 0, 0, 0.04); --fn-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.07), 0 8px 10px -6px rgba(0, 0, 0, 0.04); - --fn-shadow-brand: 0 4px 14px rgba(232, 93, 117, 0.18); - --fn-shadow-brand-lg: 0 8px 24px rgba(232, 93, 117, 0.24); + --fn-shadow-brand: 0 4px 14px rgba(0, 51, 255, 0.18); + --fn-shadow-brand-lg: 0 8px 24px rgba(0, 51, 255, 0.24); /* ---- Selected row ---- */ - --fn-row-selected-bg: rgba(232, 93, 117, 0.05); - --fn-row-selected-hover: rgba(232, 93, 117, 0.09); - --fn-row-selected-alt-bg: rgba(232, 93, 117, 0.04); - --fn-row-selected-alt-hover: rgba(232, 93, 117, 0.08); - --fn-row-selected-border: #e85d75; + --fn-row-selected-bg: rgba(0, 51, 255, 0.05); + --fn-row-selected-hover: rgba(0, 51, 255, 0.09); + --fn-row-selected-alt-bg: rgba(0, 51, 255, 0.04); + --fn-row-selected-alt-hover: rgba(0, 51, 255, 0.08); + --fn-row-selected-border: #0033ff; /* ---- Cards ---- */ --fn-card-border-normal: #e5e7eb; @@ -128,9 +128,9 @@ html[data-theme="light"] { --fn-sidebar-bg: #ffffff; --fn-sidebar-border: #ebebeb; --fn-sidebar-item-hover: #f5f5f5; - --fn-sidebar-item-active-bg: #fde8ec; - --fn-sidebar-item-active: rgba(232, 93, 117, 0.08); - --fn-sidebar-item-active-text: #e85d75; + --fn-sidebar-item-active-bg: #e6ebff; + --fn-sidebar-item-active: rgba(0, 51, 255, 0.08); + --fn-sidebar-item-active-text: #0033ff; --fn-sidebar-group-text: #b0b5be; /* ---- Header ---- */ @@ -167,9 +167,9 @@ html[data-theme="dark"] { --fn-bg-tertiary: #222222; --fn-bg-elevated: #1e1e1e; --fn-bg-layout: #0e0e0e; - --fn-bg-hover: rgba(232, 93, 117, 0.08); - --fn-bg-active: rgba(232, 93, 117, 0.14); - --fn-bg-selected: rgba(232, 93, 117, 0.1); + --fn-bg-hover: rgba(0, 51, 255, 0.1); + --fn-bg-active: rgba(0, 51, 255, 0.16); + --fn-bg-selected: rgba(0, 51, 255, 0.12); --fn-bg-code: #1e1e1e; --fn-bg-toolbar: #161616; --fn-bg-toolbar-hover: #222222; @@ -189,8 +189,8 @@ html[data-theme="dark"] { --fn-text-placeholder: rgba(255, 255, 255, 0.16); --fn-text-disabled: rgba(255, 255, 255, 0.16); --fn-text-inverse: #0e0e0e; - --fn-text-brand: #f08b9a; - --fn-logo-color: #f08b9a; + --fn-text-brand: #6b8cff; + --fn-logo-color: #6b8cff; --fn-logo-filter: brightness(1.15) saturate(0.9); /* ---- Borders ---- */ @@ -199,33 +199,33 @@ html[data-theme="dark"] { --fn-border-tertiary: rgba(255, 255, 255, 0.03); --fn-border-input: rgba(255, 255, 255, 0.12); --fn-border-input-alt: rgba(255, 255, 255, 0.09); - --fn-border-focus: #f08b9a; + --fn-border-focus: #6b8cff; - /* ---- Brand (rose, lightened for dark bg) ---- */ - --fn-color-brand: #f08b9a; - --fn-color-brand-hover: #e85d75; - --fn-color-brand-soft: #e85d75; + /* ---- Brand (FreeOS blue; text/link lightened for dark bg) ---- */ + --fn-color-brand: #0033ff; + --fn-color-brand-hover: #002ee6; + --fn-color-brand-soft: #002ee6; --fn-color-brand-border: color-mix( in srgb, var(--fn-color-brand) 35%, transparent ); --fn-color-on-brand: #ffffff; - --fn-color-brand-bg: rgba(232, 93, 117, 0.12); - --fn-color-brand-light: rgba(232, 93, 117, 0.16); - --fn-color-brand-shadow: rgba(232, 93, 117, 0.25); - --fn-color-brand-glow: rgba(232, 93, 117, 0.08); + --fn-color-brand-bg: rgba(0, 51, 255, 0.14); + --fn-color-brand-light: rgba(0, 51, 255, 0.18); + --fn-color-brand-shadow: rgba(0, 51, 255, 0.28); + --fn-color-brand-glow: rgba(0, 51, 255, 0.1); /* ---- AI message bubble (frosted glass) ---- */ --fn-assistant-bubble-bg: rgba(255, 255, 255, 0.06); --fn-assistant-bubble-bg-gradient: linear-gradient( 135deg, - rgba(240, 139, 154, 0.1) 0%, + rgba(107, 140, 255, 0.12) 0%, rgba(255, 255, 255, 0.04) 50%, - rgba(240, 139, 154, 0.08) 100% + rgba(107, 140, 255, 0.08) 100% ); - --fn-assistant-bubble-border: rgba(240, 139, 154, 0.14); - --fn-assistant-glow-color: 240, 139, 154; + --fn-assistant-bubble-border: rgba(107, 140, 255, 0.16); + --fn-assistant-glow-color: 107, 140, 255; /* ---- Semantic ---- */ --fn-color-success: #34d399; @@ -250,7 +250,7 @@ html[data-theme="dark"] { --fn-tag-remote-bg: rgba(249, 115, 22, 0.1); --fn-tag-remote-text: #fb923c; --fn-tag-remote-border: rgba(249, 115, 22, 0.2); - --fn-tag-channel-text: #f08b9a; + --fn-tag-channel-text: #6b8cff; /* ---- Shadows ---- */ --fn-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.25); @@ -258,15 +258,15 @@ html[data-theme="dark"] { --fn-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.22); --fn-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35), 0 4px 6px rgba(0, 0, 0, 0.22); --fn-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4), 0 8px 10px rgba(0, 0, 0, 0.25); - --fn-shadow-brand: 0 4px 14px rgba(232, 93, 117, 0.22); - --fn-shadow-brand-lg: 0 8px 24px rgba(232, 93, 117, 0.3); + --fn-shadow-brand: 0 4px 14px rgba(0, 51, 255, 0.24); + --fn-shadow-brand-lg: 0 8px 24px rgba(0, 51, 255, 0.32); /* ---- Selected row ---- */ - --fn-row-selected-bg: rgba(232, 93, 117, 0.1); - --fn-row-selected-hover: rgba(232, 93, 117, 0.16); - --fn-row-selected-alt-bg: rgba(232, 93, 117, 0.08); - --fn-row-selected-alt-hover: rgba(232, 93, 117, 0.13); - --fn-row-selected-border: #f08b9a; + --fn-row-selected-bg: rgba(0, 51, 255, 0.12); + --fn-row-selected-hover: rgba(0, 51, 255, 0.18); + --fn-row-selected-alt-bg: rgba(0, 51, 255, 0.1); + --fn-row-selected-alt-hover: rgba(0, 51, 255, 0.15); + --fn-row-selected-border: #6b8cff; /* ---- Cards ---- */ --fn-card-border-normal: rgba(255, 255, 255, 0.07); @@ -278,9 +278,9 @@ html[data-theme="dark"] { --fn-sidebar-bg: #141414; --fn-sidebar-border: rgba(255, 255, 255, 0.06); --fn-sidebar-item-hover: rgba(255, 255, 255, 0.05); - --fn-sidebar-item-active-bg: rgba(232, 93, 117, 0.14); - --fn-sidebar-item-active: rgba(232, 93, 117, 0.14); - --fn-sidebar-item-active-text: #f08b9a; + --fn-sidebar-item-active-bg: rgba(0, 51, 255, 0.16); + --fn-sidebar-item-active: rgba(0, 51, 255, 0.16); + --fn-sidebar-item-active-text: #6b8cff; --fn-sidebar-group-text: rgba(255, 255, 255, 0.28); /* ---- Header ---- */ @@ -308,9 +308,80 @@ html[data-theme="dark"] { /* =================================================================== Brand palettes — override only brand-tinted tokens - Default / data-palette="rose" uses the light/dark blocks above. + Default / data-palette="freeos" uses the light/dark blocks above. =================================================================== */ +/* ---------- ROSE (#E85D75) — optional, former default ---------- */ +html[data-palette="rose"]:not([data-theme="dark"]) { + --fn-bg-hover: rgba(232, 93, 117, 0.04); + --fn-bg-active: rgba(232, 93, 117, 0.08); + --fn-bg-selected: rgba(232, 93, 117, 0.06); + --fn-text-brand: #e85d75; + --fn-logo-color: #e05040; + --fn-border-focus: #e85d75; + --fn-color-brand: #e85d75; + --fn-color-brand-hover: #d14a62; + --fn-color-brand-soft: #d14a62; + --fn-color-brand-bg: rgba(232, 93, 117, 0.06); + --fn-color-brand-light: #fde8ec; + --fn-color-brand-shadow: rgba(232, 93, 117, 0.15); + --fn-color-brand-glow: rgba(232, 93, 117, 0.08); + --fn-assistant-bubble-bg-gradient: linear-gradient( + 135deg, + rgba(255, 240, 243, 0.6) 0%, + rgba(255, 255, 255, 0.35) 50%, + rgba(255, 243, 245, 0.5) 100% + ); + --fn-assistant-bubble-border: rgba(232, 93, 117, 0.12); + --fn-assistant-glow-color: 232, 93, 117; + --fn-tag-channel-text: #e85d75; + --fn-shadow-brand: 0 4px 14px rgba(232, 93, 117, 0.18); + --fn-shadow-brand-lg: 0 8px 24px rgba(232, 93, 117, 0.24); + --fn-row-selected-bg: rgba(232, 93, 117, 0.05); + --fn-row-selected-hover: rgba(232, 93, 117, 0.09); + --fn-row-selected-alt-bg: rgba(232, 93, 117, 0.04); + --fn-row-selected-alt-hover: rgba(232, 93, 117, 0.08); + --fn-row-selected-border: #e85d75; + --fn-sidebar-item-active-bg: #fde8ec; + --fn-sidebar-item-active: rgba(232, 93, 117, 0.08); + --fn-sidebar-item-active-text: #e85d75; +} + +html[data-palette="rose"][data-theme="dark"] { + --fn-bg-hover: rgba(232, 93, 117, 0.08); + --fn-bg-active: rgba(232, 93, 117, 0.14); + --fn-bg-selected: rgba(232, 93, 117, 0.1); + --fn-text-brand: #f08b9a; + --fn-logo-color: #f08b9a; + --fn-border-focus: #f08b9a; + --fn-color-brand: #f08b9a; + --fn-color-brand-hover: #e85d75; + --fn-color-brand-soft: #e85d75; + --fn-color-brand-bg: rgba(232, 93, 117, 0.12); + --fn-color-brand-light: rgba(232, 93, 117, 0.16); + --fn-color-brand-shadow: rgba(232, 93, 117, 0.25); + --fn-color-brand-glow: rgba(232, 93, 117, 0.08); + --fn-assistant-bubble-bg-gradient: linear-gradient( + 135deg, + rgba(240, 139, 154, 0.1) 0%, + rgba(255, 255, 255, 0.04) 50%, + rgba(240, 139, 154, 0.08) 100% + ); + --fn-assistant-bubble-border: rgba(240, 139, 154, 0.14); + --fn-assistant-glow-color: 240, 139, 154; + --fn-tag-channel-text: #f08b9a; + --fn-shadow-brand: 0 4px 14px rgba(232, 93, 117, 0.22); + --fn-shadow-brand-lg: 0 8px 24px rgba(232, 93, 117, 0.3); + --fn-row-selected-bg: rgba(232, 93, 117, 0.1); + --fn-row-selected-hover: rgba(232, 93, 117, 0.16); + --fn-row-selected-alt-bg: rgba(232, 93, 117, 0.08); + --fn-row-selected-alt-hover: rgba(232, 93, 117, 0.13); + --fn-row-selected-border: #f08b9a; + --fn-sidebar-item-active-bg: rgba(232, 93, 117, 0.14); + --fn-sidebar-item-active: rgba(232, 93, 117, 0.14); + --fn-sidebar-item-active-text: #f08b9a; +} + /* ---------- TECH (#4B74FA) ---------- */ html[data-palette="tech"]:not([data-theme="dark"]) { --fn-bg-hover: rgba(75, 116, 250, 0.04); diff --git a/dashboard/src/styles/themePalettes.test.ts b/dashboard/src/styles/themePalettes.test.ts index 0c74037f..9b3baf51 100644 --- a/dashboard/src/styles/themePalettes.test.ts +++ b/dashboard/src/styles/themePalettes.test.ts @@ -25,6 +25,7 @@ function contrastRatio(foreground: string, background: string): number { describe("theme palettes", () => { it("exposes the curated palette set", () => { expect(VALID_PALETTES).toEqual([ + "freeos", "rose", "tech", "indigo", @@ -36,7 +37,15 @@ describe("theme palettes", () => { ]); }); - it("keeps the historic Elegant Rose default brand tokens", () => { + it("uses FreeOS logo-center blue as the default brand tokens", () => { + expect(ANTD_BRAND_TOKENS.freeos.light.colorPrimary).toBe("#0033FF"); + expect(ANTD_BRAND_TOKENS.freeos.light.colorPrimaryHover).toBe("#002EE6"); + expect(ANTD_BRAND_TOKENS.freeos.light.colorPrimaryActive).toBe("#0024CC"); + expect(ANTD_BRAND_TOKENS.freeos.dark.colorPrimary).toBe("#0033FF"); + expect(ANTD_BRAND_TOKENS.freeos.dark.colorLink).toBe("#6B8CFF"); + }); + + it("keeps Elegant Rose as an optional palette", () => { expect(ANTD_BRAND_TOKENS.rose.light.colorPrimary).toBe("#E85D75"); expect(ANTD_BRAND_TOKENS.rose.light.colorPrimaryHover).toBe("#D14A62"); expect(ANTD_BRAND_TOKENS.rose.light.colorPrimaryActive).toBe("#B83A50"); diff --git a/dashboard/src/styles/themePalettes.ts b/dashboard/src/styles/themePalettes.ts index fe6b3cae..569c366f 100644 --- a/dashboard/src/styles/themePalettes.ts +++ b/dashboard/src/styles/themePalettes.ts @@ -1,6 +1,7 @@ /** Brand palettes — orthogonal to light/dark mode (`data-theme`). */ export type ThemePalette = + | "freeos" | "rose" | "tech" | "indigo" @@ -12,6 +13,7 @@ export type ThemePalette = | "custom"; export const VALID_PALETTES: ThemePalette[] = [ + "freeos", "rose", "tech", "indigo", @@ -25,8 +27,12 @@ export const VALID_PALETTES: ThemePalette[] = [ /** Curated palettes only — "custom" is handled separately via a hex value. */ export const CURATED_PALETTES: ThemePalette[] = [...VALID_PALETTES]; -export const DEFAULT_PALETTE: ThemePalette = "rose"; +/** FreeOS logo-center teardrop blue (sampled from the circular mark). */ +export const DEFAULT_PALETTE: ThemePalette = "freeos"; export const DEFAULT_CUSTOM_COLOR = "#4B74FA"; +export const FREEOS_BRAND_BLUE = "#0033FF"; +/** Dark-mode text/link: same hue, lightened to ≥4.5:1 on #141414. */ +export const FREEOS_BRAND_BLUE_ON_DARK = "#6B8CFF"; /** True when the value is one of the curated palette keys (not "custom"/hex). */ export function isCuratedPalette(value: string): value is ThemePalette { @@ -44,6 +50,7 @@ export const PALETTE_STORAGE_KEY = LEGACY_PALETTE_STORAGE_KEY; /** Swatch color shown in the palette picker (light brand). */ export const PALETTE_SWATCH: Record = { + freeos: FREEOS_BRAND_BLUE, rose: "#E85D75", tech: "#4B74FA", indigo: "#6366F1", @@ -74,6 +81,27 @@ export const ANTD_BRAND_TOKENS: Record< Exclude, { light: AntdBrandTokens; dark: AntdBrandTokens } > = { + freeos: { + light: { + colorPrimary: FREEOS_BRAND_BLUE, + colorPrimaryHover: "#002EE6", + colorPrimaryActive: "#0024CC", + colorLink: FREEOS_BRAND_BLUE, + }, + dark: { + colorPrimary: FREEOS_BRAND_BLUE, + colorPrimaryBg: "rgba(0, 51, 255, 0.14)", + colorPrimaryBgHover: "rgba(0, 51, 255, 0.2)", + colorPrimaryBorder: "rgba(0, 51, 255, 0.3)", + colorPrimaryBorderHover: "rgba(0, 51, 255, 0.4)", + colorPrimaryHover: "#002EE6", + colorPrimaryActive: "#0024CC", + colorPrimaryText: FREEOS_BRAND_BLUE_ON_DARK, + colorPrimaryTextHover: "#8AA8FF", + colorPrimaryTextActive: FREEOS_BRAND_BLUE, + colorLink: FREEOS_BRAND_BLUE_ON_DARK, + }, + }, rose: { light: { colorPrimary: "#E85D75", diff --git a/dashboard/src/utils/expertColor.test.ts b/dashboard/src/utils/expertColor.test.ts index 11510223..738ba204 100644 --- a/dashboard/src/utils/expertColor.test.ts +++ b/dashboard/src/utils/expertColor.test.ts @@ -10,13 +10,14 @@ import { describe("resolveExpertPalette", () => { it("matches exact curated swatches", () => { + expect(resolveExpertPalette("#0033FF")).toBe("freeos"); expect(resolveExpertPalette("#E85D75")).toBe("rose"); expect(resolveExpertPalette("#6366F1")).toBe("indigo"); }); - it("falls back to rose when color is missing", () => { - expect(resolveExpertPalette(null)).toBe("rose"); - expect(resolveExpertPalette(undefined)).toBe("rose"); + it("falls back to the default palette when color is missing", () => { + expect(resolveExpertPalette(null)).toBe("freeos"); + expect(resolveExpertPalette(undefined)).toBe("freeos"); }); it("snaps nearby template pastels onto the nearest swatch", () => { diff --git a/dashboard/src/utils/expertColor.ts b/dashboard/src/utils/expertColor.ts index 8004a2c6..a4a408fc 100644 --- a/dashboard/src/utils/expertColor.ts +++ b/dashboard/src/utils/expertColor.ts @@ -35,7 +35,7 @@ function colorDistance( return dr * dr + dg * dg + db * db; } -/** Map a stored expert hex (or null) onto the curated 8-swatch palette. */ +/** Map a stored expert hex (or null) onto the curated swatch palette. */ export function resolveExpertPalette( color: string | null | undefined, ): ThemePalette { diff --git a/desktop/README.md b/desktop/README.md index 51895384..0c48f877 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -1,44 +1,68 @@ -# Octop desktop (Wails v3 + green portable) +# FreeOS desktop (Wails v3 + green portable) -All desktop-client code lives here. This is **not** `src/octop/infra/desktop` -(remote desktop streaming). +Windows end-user product: **download the NSIS `.exe` → install → open FreeOS** +(Octop host shell + bundled openXYOS organization console). This is **not** +`src/octop/infra/desktop` (remote desktop streaming). | Path | Role | |------|------| -| [`portable/`](portable/) | Green zip packaging (was `scripts/green/`) | -| [`src/`](src/) | Wails v3 shell: load bundled zip, spawn Octop, tray/settings | +| [`portable/`](portable/) | Green zip packaging (CPython + host wheel + Node/openXYOS sidecar) | +| [`src/`](src/) | Wails v3 shell: extract zip, start host + sidecar, tray/settings | | [`package-release.sh`](package-release.sh) | Native end-to-end portable + Wails release build | ## Data directory -Same as the Octop CLI/server default: +Same precedence as the FreeOS CLI/server: -- `OCTOP_HOME` → `~/.octop` (or the existing `OCTOP_HOME` env) -- Green runtime extract → `~/.octop/portable/` -- Shell prefs → `~/.octop/desktop-settings.json` +1. `FREEOS_HOME` +2. `OCTOP_HOME` (legacy) +3. existing `~/.freeos` +4. existing `~/.octop` +5. new installs → `~/.freeos` + +- Green runtime extract → `{home}/portable/` +- Organization sidecar data → `{home}/org-os/` +- Shell prefs → `{home}/desktop-settings.json` + +## What the installer starts + +On first open the shell: + +1. Extracts the bundled portable runtime (Python host + Node + openXYOS). +2. Starts the openXYOS sidecar on `http://127.0.0.1:3780` when `org-sidecar/` is present. +3. Starts the FreeOS host with `FREEOS_HOME` and `FREEOS_ORG_ENABLE=1`. +4. Opens the desktop window on the host UI. After the setup wizard, chat and + **Organization** are both available — no separate Node install. ## Build green zip -From repo root: +From repo root (needs Node 20+ so openXYOS can be built into the zip): ```bash make -f desktop/portable/Makefile green ``` -CI: `.github/workflows/octop-desktop.yml` builds native platform/arch variants. -`v*` tags and `workflow_dispatch` (platforms=`all`) run all six; pull requests -that touch `desktop/` build `darwin-*` and `windows-*` (amd64 + arm64). Each job -first creates the green zip and then packages the matching Wails application. +Skip the sidecar only when debugging the Python runtime: `SKIP_ORG_SIDECAR=1`. + +CI: `.github/workflows/octop-desktop.yml` (**name:** FreeOS Desktop Package) +builds native platform/arch variants. `v*` tags and `workflow_dispatch` +(platforms=`all`) run all six; pull requests that touch `desktop/` or +`modules/openxyos/` build `darwin-*` and `windows-*` (amd64 + arm64). Each job +creates the green zip (including the sidecar) and packages the Wails app. + +This cloud/Linux VM cannot emit a Windows `.exe`. The job that does is +**FreeOS Desktop Package** → matrix `windows-amd64` / `windows-arm64` +(`wails3 task package` + NSIS). ## Build a complete desktop release Run the end-to-end script on the matching native host. It builds the Dashboard, -creates and verifies the portable runtime, embeds it into Wails, and produces -the final native package: +creates and verifies the portable runtime (with openXYOS), embeds it into Wails, +and produces the final native package: ```bash desktop/package-release.sh -# Reuse an existing desktop/portable/release/Octop-portable--.zip: +# Reuse an existing desktop/portable/release/FreeOS-portable--.zip: desktop/package-release.sh darwin-arm64 --reuse-portable ``` @@ -59,51 +83,59 @@ cd desktop/src go mod tidy wails3 build # development binary under desktop/src/bin/ wails3 task package ARCH=arm64 VERSION= \ - PORTABLE_ZIP=../portable/release/Octop-portable-darwin-arm64-.zip + PORTABLE_ZIP=../portable/release/FreeOS-portable-darwin-arm64-.zip ``` -Dev against an already-running Octop (skips the bundled green zip): +Dev against an already-running FreeOS host (skips the bundled green zip): ```bash cd desktop/src OCTOP_DESKTOP_URL=http://127.0.0.1:8088 wails3 dev ``` -Without `OCTOP_DESKTOP_URL`, first launch uses `~/.octop/portable/` if valid, +Without `OCTOP_DESKTOP_URL`, first launch uses `~/.freeos/portable/` if valid, otherwise extracts the matching zip shipped with the desktop package (embedded in the Windows and Linux binaries, under `Contents/Resources` on macOS). The -Wails shell never downloads Octop. For local runtime debugging, set -`OCTOP_DESKTOP_PORTABLE_ZIP=/absolute/path/Octop-portable--.zip`. +Wails shell never downloads the host over the network. For local runtime +debugging, set +`OCTOP_DESKTOP_PORTABLE_ZIP=/absolute/path/FreeOS-portable--.zip`. On later launches, a newer bundled portable version replaces the extracted -runtime after creating a consistent SQLite backup under `~/.octop/backups/`. -The upgraded Octop process then applies the normal database migrations during -startup. Newer extracted runtimes are never downgraded; PostgreSQL remains -externally managed and is not copied by the desktop shell. +runtime after creating a consistent SQLite backup under `{home}/backups/`. +The upgraded host then applies the normal database migrations during startup. +Newer extracted runtimes are never downgraded; PostgreSQL remains externally +managed and is not copied by the desktop shell. -GitHub Release names follow `Octop----.`: +GitHub Release names follow `FreeOS----.`: -- Desktop GUI: `Octop-desktop--.dmg` (macOS; open and drag - `Octop.app` into Applications), `.exe` (Windows NSIS installer — copies - into `Program Files\Octop` and creates Start Menu + desktop shortcuts), +- Desktop GUI: `FreeOS-desktop--.dmg` (macOS; open and drag + `FreeOS.app` into Applications), `.exe` (Windows NSIS installer — copies + into `Program Files\FreeOS` and creates Start Menu + desktop shortcuts), `.tar.gz` (Linux) -- Green runtime zip: `Octop-portable--.zip` +- Green runtime zip: `FreeOS-portable--.zip` - PyPI wheels stay `octop--py3-none-any.whl` (PEP 427) +Older `Octop-portable-*` / `Octop-.zip` files beside the binary are still +accepted when extracting a local/dev build. They are **not** published anymore. + The Linux tar.gz contains only the GUI binary; it has no separate portable zip or -server terminal process. Runtime upgrades remain owned by Octop: -the shell sets `OCTOP_GREEN_PACKAGES`, so `octop update` upgrades the extracted -`packages/` directory through Octop's existing `--target` logic. +server terminal process. Runtime upgrades remain owned by the host: +the shell sets `OCTOP_GREEN_PACKAGES`, so `octop update` / `freeos update` +upgrades the extracted `packages/` directory through the existing `--target` +logic. Linux also needs GTK4 + WebKitGTK 6 to link. macOS 12+. ## Icons +FreeOS circular mark (gray ring, yellow / green / red teardrops, blue center). + | File | Used for | Rule | |------|----------|------| -| `src/build/appicon.png` | Windows `.ico`, Linux | Full-bleed 512x512 artwork | +| `src/build/appicon.png` | Windows `.ico`, Linux | Full-bleed 512x512 artwork (mark ≥90% of canvas; regenerate with `python3 src/build/generate_appicons.py`) | | `src/build/appicon-macos.png` | macOS `.icns` | 1024x1024 canvas, artwork 824x824 centred | | `src/assets/tray-icon.png` | Tray + app icon on Windows/Linux | Full-bleed | | `src/assets/tray-icon-template.png` | macOS menu bar | 88px canvas, 64px black-on-transparent glyph | +| `src/assets/freeos-logo.png` | Splash / loading | Transparent circular mark | macOS sizes both surfaces to a fixed box, so the padding has to live in the artwork: the Dock follows Apple's 824/1024 icon grid, and Wails scales the menu diff --git a/desktop/package-release.sh b/desktop/package-release.sh index 3d8e16d9..9a856b5b 100755 --- a/desktop/package-release.sh +++ b/desktop/package-release.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Build one native Octop desktop release: Dashboard → portable runtime → Wails package. +# Build one native FreeOS desktop release: Dashboard → portable runtime +# (including bundled Node + openXYOS) → Wails package. # Run once per native platform; the GitHub Actions matrix runs all six variants. set -euo pipefail @@ -22,7 +23,7 @@ Platforms: windows-arm64 windows-amd64 The platform defaults to the current native host. --reuse-portable skips -rebuilding desktop/portable/release/Octop-portable--.zip when it already exists. +rebuilding desktop/portable/release/FreeOS-portable--.zip when it already exists. EOF } @@ -80,7 +81,7 @@ else bash "${REPO_ROOT}/desktop/portable/package.sh" "$plat" fi -staging="${REPO_ROOT}/desktop/portable/release/Octop-${plat}" +staging="${REPO_ROOT}/desktop/portable/release/$(portable_staging_name "$plat")" requirements="${REPO_ROOT}/desktop/portable/requirements-${plat}.txt" if [[ "$plat" == windows-* ]]; then portable_python="${staging}/runtime/python.exe" diff --git a/desktop/portable/AGENT_ELECTRON_INTEGRATION.md b/desktop/portable/AGENT_ELECTRON_INTEGRATION.md index 2fdee543..f94fb0d8 100644 --- a/desktop/portable/AGENT_ELECTRON_INTEGRATION.md +++ b/desktop/portable/AGENT_ELECTRON_INTEGRATION.md @@ -9,7 +9,7 @@ path (no system Python, no `PYTHONPATH=packages`). CI / `make -f desktop/portable/Makefile green` produces: ``` -desktop/portable/release/Octop-portable--.zip +desktop/portable/release/FreeOS-portable--.zip ``` Platforms: `darwin-arm64` `darwin-amd64` `linux-amd64` `linux-arm64` @@ -18,9 +18,10 @@ Platforms: `darwin-arm64` `darwin-amd64` `linux-amd64` `linux-arm64` Layout after extract (outside asar): ``` -Octop-/ +FreeOS-/ runtime/ portable CPython packages/ site-packages + org-sidecar/ bundled Node + openXYOS launch.py start.sh / start.bat ``` @@ -32,11 +33,11 @@ Octop-/ beside the executable (Linux); the desktop shell does not download it at runtime. 2. Extract to a writable user directory **outside** `app.asar`. 3. On macOS, after checksum: `xattr -dr com.apple.quarantine `. -4. Set `OCTOP_HOME` to Octop's default data dir (`~/.octop`, or `$OCTOP_HOME` - if already set). Do **not** use the zip's `./data` folder when launching - from the Wails desktop shell (`desktop/src`). - Extract the zip under `~/.octop/portable/` so runtime files stay next to - user data without overwriting `octop.db`. +4. Set `FREEOS_HOME` (and `OCTOP_HOME` to the same path). Default is `~/.freeos` + unless `OCTOP_HOME` is already set or `~/.octop` already exists. + Do **not** use the zip's `./data` folder when launching from the Wails + desktop shell (`desktop/src`). Extract the zip under `{home}/portable/` + so runtime files stay next to user data without overwriting `octop.db`. 5. Set `PYTHONNOUSERSITE=1` and `OCTOP_GREEN_PACKAGES=/packages`. **Do not set `PYTHONPATH`.** diff --git a/desktop/portable/README.md b/desktop/portable/README.md index eb76aa6c..1bd2142c 100644 --- a/desktop/portable/README.md +++ b/desktop/portable/README.md @@ -1,8 +1,8 @@ -# Octop 绿色便携包(多平台) +# FreeOS 绿色便携包(多平台) -解压即用:内置便携 CPython + Octop 及依赖,通过 `start.sh` / `start.bat` 启动。 -**不依赖**系统 Python,也**不包含** Wails / 桌面壳——用浏览器打开 Dashboard。 -首启走上游正常 setup wizard(本目录**不含** OOB / UI 裁剪)。 +解压即用:内置便携 CPython + FreeOS 宿主 + Node/openXYOS 组织边车,通过 `start.sh` / `start.bat` 启动。 +**不依赖**系统 Python / Node。单独解压绿包时用浏览器打开 Dashboard;完整「安装即用」产品是 Wails NSIS 安装包。 +首启走正常 setup wizard。 ## 与上游解耦 @@ -29,12 +29,13 @@ make -f desktop/portable/Makefile green ## 产物布局 -公开文件名:`Octop-portable--.zip`。zip 内目录仍是: +公开文件名:`FreeOS-portable--.zip`。zip 内目录是: ``` -Octop-/ +FreeOS-/ runtime/ # python-build-standalone - packages/ # Octop + 依赖(site-packages,可搬迁) + packages/ # FreeOS 宿主 + 依赖(site-packages,可搬迁) + org-sidecar/ # 内置 Node + 已构建的 openXYOS launch.py # 启动引导(site.addsitedir / Windows pywin32) start.sh # macOS / Linux start.bat # Windows @@ -114,5 +115,5 @@ Actions 使用 GitHub 上游 PBS(`PBS_BASE_URL`),本机构建默认 npmmir ## Electron 壳 -壳只消费 `Octop-portable--.zip`(zip 内仍是 `Octop-/`),不要把绿包编进 asar。步骤见 +壳只消费 `FreeOS-portable--.zip`(zip 内是 `FreeOS-/`),不要把绿包编进 asar。步骤见 [`AGENT_ELECTRON_INTEGRATION.md`](AGENT_ELECTRON_INTEGRATION.md)。 diff --git a/desktop/portable/_common.sh b/desktop/portable/_common.sh index 8963b9fe..1f1cecbd 100644 --- a/desktop/portable/_common.sh +++ b/desktop/portable/_common.sh @@ -23,16 +23,22 @@ ALL_PLATS=( windows-arm64 ) -# Public GitHub Release names: Octop----. -# Zip payload directory stays Octop-/ (the desktop unzip strips the first -# path component). PyPI wheels keep the PEP 427 name and are not renamed here. +# Public GitHub Release names: FreeOS----. +# Zip payload directory is FreeOS-/ (the desktop unzip strips the first +# path component). Octop-* aliases are no longer published; the unzip step still +# accepts an Octop-/ prefix from older local zips. PyPI wheels keep the +# PEP 427 name `octop` and are not renamed here. octop_version() { sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' \ "${REPO_ROOT}/pyproject.toml" | head -1 } +portable_staging_name() { + echo "FreeOS-${1}" +} + portable_zip_basename() { - echo "Octop-portable-${1}-$(octop_version).zip" + echo "FreeOS-portable-${1}-$(octop_version).zip" } desktop_pkg_basename() { @@ -40,9 +46,9 @@ desktop_pkg_basename() { local ver ver="$(octop_version)" case "$plat" in - darwin-*) echo "Octop-desktop-${plat}-${ver}.dmg" ;; - windows-*) echo "Octop-desktop-${plat}-${ver}.exe" ;; - linux-*) echo "Octop-desktop-${plat}-${ver}.tar.gz" ;; + darwin-*) echo "FreeOS-desktop-${plat}-${ver}.dmg" ;; + windows-*) echo "FreeOS-desktop-${plat}-${ver}.exe" ;; + linux-*) echo "FreeOS-desktop-${plat}-${ver}.tar.gz" ;; *) echo "unknown platform: ${plat}" >&2 return 1 diff --git a/desktop/portable/bundle-org-sidecar.sh b/desktop/portable/bundle-org-sidecar.sh new file mode 100755 index 00000000..d1055a7a --- /dev/null +++ b/desktop/portable/bundle-org-sidecar.sh @@ -0,0 +1,327 @@ +#!/usr/bin/env bash +# Bundle a Node runtime + built openXYOS into a green portable staging tree +# so the desktop installer can start the organization sidecar on a clean machine +# (no system Node required). +# +# Usage: +# bash desktop/portable/bundle-org-sidecar.sh +# +# Layout written under /org-sidecar/: +# node/ official Node.js runtime for +# openxyos/ built frontend (dist/) + backend TS + pruned node_modules +# start-sidecar.sh / start-sidecar.bat +# VERSION.txt +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=desktop/portable/_common.sh +source "${REPO_ROOT}/desktop/portable/_common.sh" + +NODE_VERSION="${NODE_VERSION:-20.19.5}" +NODE_BASE_URL="${NODE_BASE_URL:-https://nodejs.org/dist/v${NODE_VERSION}}" + +node_archive_name() { + case "$1" in + windows-amd64) echo "node-v${NODE_VERSION}-win-x64.zip" ;; + windows-arm64) echo "node-v${NODE_VERSION}-win-arm64.zip" ;; + linux-amd64) echo "node-v${NODE_VERSION}-linux-x64.tar.xz" ;; + linux-arm64) echo "node-v${NODE_VERSION}-linux-arm64.tar.xz" ;; + darwin-amd64) echo "node-v${NODE_VERSION}-darwin-x64.tar.gz" ;; + darwin-arm64) echo "node-v${NODE_VERSION}-darwin-arm64.tar.gz" ;; + *) + echo "unknown platform: $1" >&2 + return 1 + ;; + esac +} + +node_extract_dir() { + case "$1" in + windows-amd64) echo "node-v${NODE_VERSION}-win-x64" ;; + windows-arm64) echo "node-v${NODE_VERSION}-win-arm64" ;; + linux-amd64) echo "node-v${NODE_VERSION}-linux-x64" ;; + linux-arm64) echo "node-v${NODE_VERSION}-linux-arm64" ;; + darwin-amd64) echo "node-v${NODE_VERSION}-darwin-x64" ;; + darwin-arm64) echo "node-v${NODE_VERSION}-darwin-arm64" ;; + *) return 1 ;; + esac +} + +download_node() { + local plat="$1" + local archive dest url + archive="$(node_archive_name "$plat")" + dest="${GREEN_CACHE}/node/${archive}" + mkdir -p "$(dirname "$dest")" + if [[ -f "$dest" && -s "$dest" ]]; then + echo "[org-sidecar] reusing ${dest}" >&2 + echo "$dest" + return 0 + fi + url="${NODE_BASE_URL}/${archive}" + echo "[org-sidecar] downloading ${url}" >&2 + if command -v curl >/dev/null 2>&1; then + curl -fsSL --retry 4 --retry-delay 2 -o "${dest}.partial" "$url" + elif command -v wget >/dev/null 2>&1; then + wget -q -O "${dest}.partial" "$url" + else + echo "[org-sidecar] need curl or wget to download Node" >&2 + exit 1 + fi + mv "${dest}.partial" "$dest" + echo "$dest" +} + +extract_node() { + local archive="$1" + local dest="$2" + rm -rf "$dest" + mkdir -p "$dest" + case "$archive" in + *.zip) + if command -v unzip >/dev/null 2>&1; then + unzip -q "$archive" -d "$dest" + else + py="" + if command -v python3 >/dev/null 2>&1; then + py=python3 + elif command -v python >/dev/null 2>&1; then + py=python + fi + if [[ -z "$py" ]]; then + echo "[org-sidecar] need unzip or python to extract Node zip" >&2 + exit 1 + fi + "$py" -c "import shutil,sys; shutil.unpack_archive(sys.argv[1], sys.argv[2])" \ + "$archive" "$dest" + fi + ;; + *.tar.xz|*.tar.gz) + tar -xf "$archive" -C "$dest" + ;; + *) + echo "[org-sidecar] unsupported Node archive: ${archive}" >&2 + exit 1 + ;; + esac +} + +build_openxyos() { + local work="$1" + local src="${REPO_ROOT}/modules/openxyos" + if [[ ! -f "${src}/package.json" ]]; then + echo "[org-sidecar] openXYOS missing at ${src}" >&2 + exit 1 + fi + if ! command -v npm >/dev/null 2>&1; then + echo "[org-sidecar] npm is required to build openXYOS (Node ${NODE_VERSION}+)" >&2 + exit 1 + fi + echo "[org-sidecar] assembling openXYOS build tree → ${work}" >&2 + rm -rf "$work" + mkdir -p "$work" + # Copy sources only; install + build happen in the work tree. + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete \ + --exclude node_modules --exclude dist --exclude .git --exclude .github \ + --exclude backups --exclude dist-backup-20260626 --exclude V0.5 \ + "${src}/" "${work}/" + else + python3 - "$src" "$work" <<'PY' +import shutil, sys +from pathlib import Path +src, dest = Path(sys.argv[1]), Path(sys.argv[2]) +skip = {"node_modules", "dist", ".git", ".github", "backups", "dist-backup-20260626", "V0.5"} +for item in src.iterdir(): + if item.name in skip: + continue + target = dest / item.name + if item.is_dir(): + shutil.copytree(item, target, ignore=shutil.ignore_patterns(*skip)) + else: + shutil.copy2(item, target) +PY + fi + ( + cd "$work" + echo "[org-sidecar] npm ci" >&2 + npm ci --no-audit --no-fund + echo "[org-sidecar] npm run build" >&2 + npm run build + echo "[org-sidecar] prune to production + tsx (sidecar starts via node --import tsx)" >&2 + npm prune --omit=dev --no-audit --no-fund + npm install tsx@4.19.2 --omit=dev --no-audit --no-fund --no-package-lock + ) + if [[ ! -f "${work}/dist/index.html" ]]; then + echo "[org-sidecar] vite build did not produce dist/index.html" >&2 + exit 1 + fi +} + +write_sidecar_launchers() { + local dest="$1" + cat > "${dest}/start-sidecar.sh" <<'EOF' +#!/usr/bin/env bash +# Start the bundled openXYOS sidecar (no system Node required). +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -x "${ROOT}/node/bin/node" ]]; then + NODE="${ROOT}/node/bin/node" +elif [[ -f "${ROOT}/node/node.exe" ]]; then + NODE="${ROOT}/node/node.exe" +else + echo "start-sidecar: bundled Node missing under ${ROOT}/node" >&2 + exit 1 +fi +APP="${ROOT}/openxyos" +HOME_DIR="${FREEOS_HOME:-${OCTOP_HOME:-${HOME}/.freeos}}" +DATA_DIR="${FREEOS_ORG_DATA:-${HOME_DIR}/org-os}" +PORT="${FREEOS_ORG_SIDECAR_PORT:-3780}" +mkdir -p "${DATA_DIR}" +if [[ -f "${DATA_DIR}/sidecar.env" ]]; then + set -a + # shellcheck disable=SC1091 + source "${DATA_DIR}/sidecar.env" + set +a +fi +if [[ -z "${JWT_SECRET:-}" || -z "${COOKIE_SECRET:-}" ]]; then + JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32 2>/dev/null || python3 -c 'import secrets; print(secrets.token_hex(32))')}" + COOKIE_SECRET="${COOKIE_SECRET:-$(openssl rand -hex 32 2>/dev/null || python3 -c 'import secrets; print(secrets.token_hex(32))')}" + umask 077 + cat > "${DATA_DIR}/sidecar.env" < "${dest}/start-sidecar.bat" <<'EOF' +@echo off +setlocal EnableExtensions EnableDelayedExpansion +set "ROOT=%~dp0" +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" +set "NODE=%ROOT%\node\node.exe" +if not exist "%NODE%" ( + echo start-sidecar.bat: bundled Node missing at %NODE% + exit /b 1 +) +set "APP=%ROOT%\openxyos" +if not defined FREEOS_HOME if defined OCTOP_HOME set "FREEOS_HOME=%OCTOP_HOME%" +if not defined FREEOS_HOME set "FREEOS_HOME=%USERPROFILE%\.freeos" +if not defined FREEOS_ORG_DATA set "FREEOS_ORG_DATA=%FREEOS_HOME%\org-os" +if not defined FREEOS_ORG_SIDECAR_PORT set "FREEOS_ORG_SIDECAR_PORT=3780" +if not exist "%FREEOS_ORG_DATA%" mkdir "%FREEOS_ORG_DATA%" +if exist "%FREEOS_ORG_DATA%\sidecar.env" for /f "usebackq tokens=1,* delims==" %%A in ("%FREEOS_ORG_DATA%\sidecar.env") do set "%%A=%%B" +if not defined JWT_SECRET ( + for /f %%I in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString('N') + [guid]::NewGuid().ToString('N')"') do set "JWT_SECRET=%%I" +) +if not defined COOKIE_SECRET ( + for /f %%I in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString('N') + [guid]::NewGuid().ToString('N')"') do set "COOKIE_SECRET=%%I" +) +> "%FREEOS_ORG_DATA%\sidecar.env" echo JWT_SECRET=%JWT_SECRET% +>> "%FREEOS_ORG_DATA%\sidecar.env" echo COOKIE_SECRET=%COOKIE_SECRET% +set "NODE_ENV=production" +set "PORT=%FREEOS_ORG_SIDECAR_PORT%" +set "DB_DIALECT=sqlite" +set "DATABASE_PATH=%FREEOS_ORG_DATA%\xiongyuan.db" +set "AIR_GAP_MODE=true" +if not defined ALLOW_PUBLIC_REGISTRATION set "ALLOW_PUBLIC_REGISTRATION=false" +if not defined SEED_DEMO_DATA set "SEED_DEMO_DATA=false" +if not defined CORS_ORIGIN set "CORS_ORIGIN=http://127.0.0.1:8088,http://localhost:8088,http://127.0.0.1:18900,http://localhost:18900" +cd /d "%APP%" +"%NODE%" --import tsx backend/server.ts +exit /b %ERRORLEVEL% +EOF +} + +copy_openxyos_runtime() { + local work="$1" + local dest="$2" + rm -rf "$dest" + mkdir -p "$dest" + for part in backend dist node_modules package.json; do + if [[ ! -e "${work}/${part}" ]]; then + echo "[org-sidecar] missing ${work}/${part}" >&2 + exit 1 + fi + if [[ -d "${work}/${part}" ]]; then + cp -a "${work}/${part}" "${dest}/${part}" + else + cp -a "${work}/${part}" "${dest}/${part}" + fi + done + # tsx resolve + package scripts; lockfile optional. + if [[ -f "${work}/package-lock.json" ]]; then + cp -a "${work}/package-lock.json" "${dest}/package-lock.json" + fi +} + +main() { + local plat="${1:-}" + local staging="${2:-}" + if [[ -z "$plat" || -z "$staging" ]]; then + echo "usage: bundle-org-sidecar.sh " >&2 + exit 2 + fi + if ! is_known_plat "$plat"; then + echo "unknown platform: ${plat}" >&2 + exit 2 + fi + if [[ "${SKIP_ORG_SIDECAR:-0}" == "1" ]]; then + echo "[org-sidecar] SKIP_ORG_SIDECAR=1 — not bundling openXYOS" >&2 + return 0 + fi + + local dest="${staging}/org-sidecar" + mkdir -p "$dest" + + local archive extracted inner + archive="$(download_node "$plat")" + extracted="${GREEN_CACHE}/node/extract-${plat}" + extract_node "$archive" "$extracted" + inner="${extracted}/$(node_extract_dir "$plat")" + if [[ ! -d "$inner" ]]; then + echo "[org-sidecar] Node extract missing ${inner}" >&2 + exit 1 + fi + rm -rf "${dest}/node" + cp -a "$inner" "${dest}/node" + if [[ "$plat" == windows-* ]]; then + if [[ ! -f "${dest}/node/node.exe" ]]; then + echo "[org-sidecar] node.exe missing after extract" >&2 + exit 1 + fi + elif [[ ! -x "${dest}/node/bin/node" ]]; then + echo "[org-sidecar] bin/node missing after extract" >&2 + exit 1 + fi + + local work="${GREEN_CACHE}/openxyos-${plat}" + build_openxyos "$work" + copy_openxyos_runtime "$work" "${dest}/openxyos" + write_sidecar_launchers "$dest" + + { + echo "platform=${plat}" + echo "node=${NODE_VERSION}" + echo "openxyos_bundled=1" + sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/freeos_version=\1/p' \ + "${REPO_ROOT}/pyproject.toml" | head -1 + } > "${dest}/VERSION.txt" + + echo "[org-sidecar] bundled → ${dest}" >&2 +} + +main "${1:-}" "${2:-}" diff --git a/desktop/portable/package.sh b/desktop/portable/package.sh index f78c9546..f7e141a9 100755 --- a/desktop/portable/package.sh +++ b/desktop/portable/package.sh @@ -11,13 +11,14 @@ # OCTOP_GREEN_OFFLINE=1 bash desktop/portable/package.sh # require local wheels # # Layout of each zip: -# Octop-/ +# FreeOS-/ # runtime/ portable CPython # packages/ site-packages (uv --target, relocatable) +# org-sidecar/ bundled Node + openXYOS (organization console) # start.sh / start.bat # README.txt # -# Public filename is Octop-portable--.zip (see portable_zip_basename). +# Public filename is FreeOS-portable--.zip (see portable_zip_basename). set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -84,7 +85,7 @@ assemble_one() { local plat="$1" local runtime="${GREEN_RUNTIMES}/${plat}" local wheel_dir="${GREEN_WHEELS}/${plat}" - local staging="${GREEN_RELEASE}/Octop-${plat}" + local staging="${GREEN_RELEASE}/$(portable_staging_name "$plat")" local zip_path="${GREEN_RELEASE}/$(portable_zip_basename "$plat")" local pyplat @@ -235,6 +236,9 @@ assemble_one() { cp "${TEMPLATES}/README.txt" "${staging}/README.txt" chmod +x "${staging}/start.sh" + echo "[package] ${plat}: bundling openXYOS sidecar" >&2 + bash "${REPO_ROOT}/desktop/portable/bundle-org-sidecar.sh" "$plat" "$staging" + # Windows: pywin32 DLLs must be findable next to python.exe (or on PATH). # --target installs leave them under packages/pywin32_system32 only. if [[ "$plat" == windows-* ]]; then @@ -252,6 +256,9 @@ assemble_one() { echo "pbs_tag=${PBS_TAG}" sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/octop_version=\1/p' \ "${REPO_ROOT}/pyproject.toml" | head -1 + if [[ -d "${staging}/org-sidecar/openxyos" ]]; then + echo "org_sidecar=1" + fi } > "${staging}/VERSION.txt" rm -f "$zip_path" @@ -262,7 +269,7 @@ assemble_one() { ( cd "$GREEN_RELEASE" if command -v zip >/dev/null 2>&1; then - zip -qry "$zip_name" "Octop-${plat}" + zip -qry "$zip_name" "$(portable_staging_name "$plat")" else # Windows runners often have `python` but not `python3` / `zip`. py="" @@ -275,7 +282,7 @@ assemble_one() { echo "[package] need zip or python to create archive" >&2 exit 1 fi - "$py" -c "import shutil; shutil.make_archive('${zip_stem}', 'zip', '.', 'Octop-${plat}')" + "$py" -c "import shutil; shutil.make_archive('${zip_stem}', 'zip', '.', '$(portable_staging_name "$plat")')" fi ) echo "[package] wrote ${zip_path}" diff --git a/desktop/portable/templates/README.txt b/desktop/portable/templates/README.txt index d3e86635..536eb4f7 100644 --- a/desktop/portable/templates/README.txt +++ b/desktop/portable/templates/README.txt @@ -1,25 +1,29 @@ -Octop green portable package -============================ +FreeOS green portable package +============================= -Extract this zip anywhere. It includes a portable CPython runtime and Octop -dependencies. No system Python install is required. +Extract this zip anywhere. It includes a portable CPython runtime, FreeOS +(Octop-compatible host), and a bundled Node + openXYOS organization sidecar. +No system Python or Node install is required. Start ----- macOS / Linux: ./start.sh Windows: start.bat -Defaults: http://127.0.0.1:8088 data dir = ./data (OCTOP_HOME) +Defaults: http://127.0.0.1:8088 data dir = ./data (FREEOS_HOME) ./start.sh --home /path/to/data --host 127.0.0.1 --port 8088 -First launch follows the normal Octop setup wizard (create admin password). +First launch follows the normal FreeOS setup wizard (create admin password). +The organization module is enabled automatically; the sidecar listens on +http://127.0.0.1:3780 and the dashboard /organization page embeds it. Layout ------ - runtime/ portable CPython - packages/ Octop + locked dependencies (site-packages) - launch.py entry bootstrap (loads packages/ + Windows pywin32 DLLs) + runtime/ portable CPython + packages/ FreeOS host + locked dependencies (site-packages) + org-sidecar/ bundled Node + built openXYOS + launch.py entry bootstrap (loads packages/ + Windows pywin32 DLLs) start.sh / start.bat README.txt VERSION.txt @@ -35,3 +39,8 @@ Notes Windows: if import pywintypes fails, rebuild from a current green package (launch.py + pywin32 DLL copy). Do not set PYTHONPATH manually. + +Compatibility +------------- + OCTOP_HOME is still honored as a legacy alias for FREEOS_HOME. + The Python package and CLI remain `octop` alongside `freeos`. diff --git a/desktop/portable/templates/start.bat b/desktop/portable/templates/start.bat index bdf2d580..69866d2d 100644 --- a/desktop/portable/templates/start.bat +++ b/desktop/portable/templates/start.bat @@ -1,16 +1,18 @@ @echo off setlocal EnableExtensions EnableDelayedExpansion -rem Octop green portable launcher (Windows) +rem FreeOS green portable launcher (Windows) rem Usage: rem start.bat -rem start.bat --home D:\octop-data +rem start.bat --home D:\freeos-data rem start.bat --home .\data --host 0.0.0.0 --port 8088 set "ROOT=%~dp0" if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" -if not defined OCTOP_HOME set "OCTOP_HOME=%ROOT%\data" +if not defined FREEOS_HOME if defined OCTOP_HOME set "FREEOS_HOME=%OCTOP_HOME%" +if not defined FREEOS_HOME set "FREEOS_HOME=%ROOT%\data" +set "OCTOP_HOME=%FREEOS_HOME%" set "HOST=127.0.0.1" set "PORT=8088" set "EXTRA=" @@ -22,6 +24,7 @@ if /I "%~1"=="--home" ( echo start.bat: --home requires a path exit /b 1 ) + set "FREEOS_HOME=%~2" set "OCTOP_HOME=%~2" shift shift @@ -54,18 +57,21 @@ shift goto parse :help -echo Octop green portable launcher +echo FreeOS green portable launcher echo. -echo Usage: start.bat [--home DIR] [--host HOST] [--port PORT] [octop run args...] +echo Usage: start.bat [--home DIR] [--host HOST] [--port PORT] [freeos run args...] echo. echo Defaults: -echo OCTOP_HOME / --home %%ROOT%%\data -echo --host 127.0.0.1 -echo --port 8088 +echo FREEOS_HOME / --home %%ROOT%%\data +echo --host 127.0.0.1 +echo --port 8088 exit /b 0 :run -if not exist "%OCTOP_HOME%" mkdir "%OCTOP_HOME%" +if not defined FREEOS_ORG_ENABLE set "FREEOS_ORG_ENABLE=1" +if not defined FREEOS_ORG_SIDECAR_URL set "FREEOS_ORG_SIDECAR_URL=http://127.0.0.1:3780" +if not defined OPENXYOS_BASE_URL set "OPENXYOS_BASE_URL=%FREEOS_ORG_SIDECAR_URL%" +if not exist "%FREEOS_HOME%" mkdir "%FREEOS_HOME%" set "PY=%ROOT%\runtime\python.exe" if not exist "%PY%" ( @@ -82,7 +88,12 @@ rem Prefer launch.py (site.addsitedir + pywin32 DLL path). Do not set PYTHONPATH set "PYTHONNOUSERSITE=1" set "PYTHONPATH=" -echo [octop] home=%OCTOP_HOME% -echo [octop] http://%HOST%:%PORT% +if exist "%ROOT%\org-sidecar\start-sidecar.bat" ( + echo [freeos] organization sidecar → %FREEOS_ORG_SIDECAR_URL% + start "" /B "%ROOT%\org-sidecar\start-sidecar.bat" +) + +echo [freeos] home=%FREEOS_HOME% +echo [freeos] http://%HOST%:%PORT% "%PY%" "%ROOT%\launch.py" run --host %HOST% --port %PORT% %EXTRA% exit /b %ERRORLEVEL% diff --git a/desktop/portable/templates/start.sh b/desktop/portable/templates/start.sh index 8f4800d0..9065516b 100755 --- a/desktop/portable/templates/start.sh +++ b/desktop/portable/templates/start.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Octop green portable launcher (macOS / Linux). +# FreeOS green portable launcher (macOS / Linux). # Usage: # ./start.sh # ./start.sh --home /path/to/data @@ -7,7 +7,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -export OCTOP_HOME="${OCTOP_HOME:-${ROOT}/data}" +export FREEOS_HOME="${FREEOS_HOME:-${OCTOP_HOME:-${ROOT}/data}}" +export OCTOP_HOME="${FREEOS_HOME}" HOST="127.0.0.1" PORT="8088" @@ -17,6 +18,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --home) [[ $# -ge 2 ]] || { echo "start.sh: --home requires a path" >&2; exit 1; } + FREEOS_HOME="$2" OCTOP_HOME="$2" shift 2 ;; @@ -32,17 +34,18 @@ while [[ $# -gt 0 ]]; do ;; -h|--help) cat < 0.86 { + t.Fatalf("appicon-macos.png content fill %.1f%% want ~80.5%% (824/1024)", ratio*100) + } +} + +func contentFill(t *testing.T, path string) float64 { + t.Helper() + f, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + img, err := png.Decode(f) + if err != nil { + t.Fatal(err) + } + b := img.Bounds() + minX, minY := b.Max.X, b.Max.Y + maxX, maxY := b.Min.X, b.Min.Y + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + r, g, bl, a := img.At(x, y).RGBA() + if a < 16<<8 { + continue + } + if r>>8 > 245 && g>>8 > 245 && bl>>8 > 245 { + continue + } + if x < minX { + minX = x + } + if y < minY { + minY = y + } + if x > maxX { + maxX = x + } + if y > maxY { + maxY = y + } + } + } + if maxX < minX { + t.Fatalf("%s has no visible artwork", path) + } + w := float64(maxX - minX + 1) + return w / float64(b.Dx()) +} diff --git a/desktop/src/assets/freeos-logo.png b/desktop/src/assets/freeos-logo.png new file mode 100644 index 00000000..1f8ebe43 Binary files /dev/null and b/desktop/src/assets/freeos-logo.png differ diff --git a/desktop/src/assets/index.html b/desktop/src/assets/index.html index 8934bc43..b0118266 100644 --- a/desktop/src/assets/index.html +++ b/desktop/src/assets/index.html @@ -3,7 +3,7 @@ - Octop + FreeOS