feat(icons): no-frame wordmarks, squircle BrandMarks, on-demand generation
- website wordmarks (logo.png/logo-dark.png) drop the black/white frame: the girl alone on transparency, black for light navbar / white for dark (docusaurus srcDark stays) - BrandMark marks are now the app-icon squircle itself (transparent corners, 824px safe-zone composition) instead of plain tiles; the components no longer paint a tile/rounding - generated icon outputs are NOT committed anymore: all 35 targets are gitignored and regenerated on demand by the consuming pipelines via the new scripts/generate-icons.mjs (website prebuild, desktop prebuild+dev, installer prebuild, web prebuild) - freshness lane switched from byte-compare to structural verification (sizes, squircle corner transparency, ICO frame sets via header parse) — no more windows/ubuntu byte drift dance - marks saved as 8-bit palette PNGs (FASTOCTREE quantize keeps per-index alpha tRNS -> 2.5KB, AA edges preserved); compose_svg renders in the background's native space so resvg scales per output size
11
.github/workflows/icons-freshness-check.yml
vendored
@@ -2,11 +2,12 @@ name: Icon Assets Freshness
|
|||||||
|
|
||||||
# Guard for the icon pipeline. The sources of truth are the nous-girl SVGs
|
# Guard for the icon pipeline. The sources of truth are the nous-girl SVGs
|
||||||
# (assets/nous-girl-*.svg) and the per-platform backgrounds
|
# (assets/nous-girl-*.svg) and the per-platform backgrounds
|
||||||
# (assets/backgrounds/*.svg); assets/icon-master*.svg and every raster target
|
# (assets/backgrounds/*.svg). Generated outputs are NOT committed — every
|
||||||
# are generated from those (see scripts/generate_icons.py). This check
|
# consuming pipeline regenerates them via scripts/generate-icons.mjs — so
|
||||||
# regenerates everything and byte-compares against the committed files. The
|
# this lane runs --check, which regenerates every target in memory and
|
||||||
# repo can never hold a hand-edited or stale generated icon, and an art or
|
# asserts structural invariants (sizes, transparent squircle corners,
|
||||||
# background change can never land without its regenerated outputs.
|
# container frame sets). A source change can never land broken. See
|
||||||
|
# scripts/generate_icons.py.
|
||||||
#
|
#
|
||||||
# Byte-determinism note: resvg renders the same SVG to identical bytes on any
|
# Byte-determinism note: resvg renders the same SVG to identical bytes on any
|
||||||
# host for pure-shape artwork. If the master ever contains <text>, font
|
# host for pure-shape artwork. If the master ever contains <text>, font
|
||||||
|
|||||||
746
.gitignore
vendored
@@ -1,359 +1,389 @@
|
|||||||
|
|
||||||
!apps/desktop/src/global.d.ts
|
!apps/desktop/src/global.d.ts
|
||||||
!apps/desktop/src/plugins/*/plugin.js
|
!apps/desktop/src/plugins/*/plugin.js
|
||||||
!apps/desktop/src/vite-env.d.ts
|
!apps/desktop/src/vite-env.d.ts
|
||||||
!hermes_cli/data/
|
!hermes_cli/data/
|
||||||
!hermes_cli/data/plugin_index.json
|
!hermes_cli/data/plugin_index.json
|
||||||
# — ignore so `git status` stays clean and update's autostash skips them.
|
# — ignore so `git status` stays clean and update's autostash skips them.
|
||||||
# (launch-time stale-bytecode sweep). Runtime state, never a code change.
|
# (launch-time stale-bytecode sweep). Runtime state, never a code change.
|
||||||
# `data/` pattern above would otherwise swallow it.
|
# `data/` pattern above would otherwise swallow it.
|
||||||
# `npm run sync-assets` (see web/package.json).
|
# `npm run sync-assets` (see web/package.json).
|
||||||
# also created in-repo when an agent operates in this checkout). Plans, audit
|
# also created in-repo when an agent operates in this checkout). Plans, audit
|
||||||
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
|
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
|
||||||
# automation-blueprints-index.json is a build artifact emitted by
|
# automation-blueprints-index.json is a build artifact emitted by
|
||||||
# bootstrap installer. It is Hermes-managed runtime state, never a code change —
|
# bootstrap installer. It is Hermes-managed runtime state, never a code change —
|
||||||
# Bundled community plugin index seed (shipped as package data) — the bare
|
# Bundled community plugin index seed (shipped as package data) — the bare
|
||||||
# by `hermes update` / launch-time self-heal. Runtime state, never a code change
|
# by `hermes update` / launch-time self-heal. Runtime state, never a code change
|
||||||
# by accident via 3a69e34702, removed in the #72002 salvage).
|
# by accident via 3a69e34702, removed in the #72002 salvage).
|
||||||
# Checkout fingerprint the __pycache__ tree was last validated against
|
# Checkout fingerprint the __pycache__ tree was last validated against
|
||||||
# CLI config (may contain sensitive SSH paths)
|
# CLI config (may contain sensitive SSH paths)
|
||||||
# committed to the repo root. See the hermes-release skill.
|
# committed to the repo root. See the hermes-release skill.
|
||||||
# Cross-process web UI build lock (flock target, always empty)
|
# Cross-process web UI build lock (flock target, always empty)
|
||||||
# cut (passed to `gh release create --notes-file`); the GitHub Release itself
|
# cut (passed to `gh release create --notes-file`); the GitHub Release itself
|
||||||
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
|
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
|
||||||
# Desktop/bootstrap install marker written into the managed checkout root by the
|
# Desktop/bootstrap install marker written into the managed checkout root by the
|
||||||
# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx
|
# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx
|
||||||
# every build).
|
# every build).
|
||||||
# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins,
|
# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins,
|
||||||
# git for the same reason as skills-index.json (large, generated, change
|
# git for the same reason as skills-index.json (large, generated, change
|
||||||
# ignore it so `hermes update`'s `git stash push --include-untracked` does not
|
# ignore it so `hermes update`'s `git stash push --include-untracked` does not
|
||||||
# image-provider (fal.media) URL — they are NEVER committed to the repo. The
|
# image-provider (fal.media) URL — they are NEVER committed to the repo. The
|
||||||
# infographic-check CI job is what actually enforces this.
|
# infographic-check CI job is what actually enforces this.
|
||||||
# Installer-written method stamp in the managed checkout root (scripts/install.sh).
|
# Installer-written method stamp in the managed checkout root (scripts/install.sh).
|
||||||
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
|
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
|
||||||
# Interrupted-update breadcrumb + recovery lock written next to the shared venv
|
# Interrupted-update breadcrumb + recovery lock written next to the shared venv
|
||||||
# Local editor / agent tooling (machine-specific; keep in global config, not the repo)
|
# Local editor / agent tooling (machine-specific; keep in global config, not the repo)
|
||||||
# logs, and per-session caches are never artifacts of the codebase.
|
# logs, and per-session caches are never artifacts of the codebase.
|
||||||
# Nix
|
# Nix
|
||||||
# No trailing slash: also matches node_modules SYMLINKS (worktrees often
|
# No trailing slash: also matches node_modules SYMLINKS (worktrees often
|
||||||
# Per-release changelog drafts. These exist only transiently during a release
|
# Per-release changelog drafts. These exist only transiently during a release
|
||||||
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent)
|
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent)
|
||||||
# Playwright visual regression baselines — cached from main in CI, not committed
|
# Playwright visual regression baselines — cached from main in CI, not committed
|
||||||
# PR body is the archive. See the hermes-agent-dev skill's
|
# PR body is the archive. See the hermes-agent-dev skill's
|
||||||
# PR infographics are rendered locally and embedded in PR descriptions via the
|
# PR infographics are rendered locally and embedded in PR descriptions via the
|
||||||
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
|
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
|
||||||
# Private keys
|
# Private keys
|
||||||
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
|
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
|
||||||
# Release script temp files
|
# Release script temp files
|
||||||
# Repo-root build/debug artifacts that must never be committed
|
# Repo-root build/debug artifacts that must never be committed
|
||||||
# resolves the stale .js OVER the .tsx — never track these)
|
# resolves the stale .js OVER the .tsx — never track these)
|
||||||
# Runtime marker written by hermes update when a lazy dependency refresh is
|
# Runtime marker written by hermes update when a lazy dependency refresh is
|
||||||
# Runtime metadata only — never a code change. Ignore so `git status` stays clean
|
# Runtime metadata only — never a code change. Ignore so `git status` stays clean
|
||||||
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
|
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
|
||||||
# sibling exists, so the stale-shadow hazard above cannot apply.
|
# sibling exists, so the stale-shadow hazard above cannot apply.
|
||||||
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only
|
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only
|
||||||
# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case)
|
# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case)
|
||||||
# skills.json + skills-meta.json are build artifacts emitted by
|
# skills.json + skills-meta.json are build artifacts emitted by
|
||||||
# slip into a commit and break `npm ci` on CI with ENOTDIR).
|
# slip into a commit and break `npm ci` on CI with ENOTDIR).
|
||||||
# Spelling variants are listed because a single `infographic/` pattern was
|
# Spelling variants are listed because a single `infographic/` pattern was
|
||||||
# stores the published notes. They are not a build artifact and must never be
|
# stores the published notes. They are not a build artifact and must never be
|
||||||
# symlink node_modules to the main checkout; the dir-only pattern let one
|
# symlink node_modules to the main checkout; the dir-only pattern let one
|
||||||
# the first line of defence and cannot stop `git add -f` at all — the
|
# the first line of defence and cannot stop `git add -f` at all — the
|
||||||
# the route name, so each route gets its own tree and two can run at once.
|
# the route name, so each route gets its own tree and two can run at once.
|
||||||
# Tool Search live-test harness output — non-deterministic model transcripts,
|
# Tool Search live-test harness output — non-deterministic model transcripts,
|
||||||
# treat it as a local edit and autostash it on every run (#38529).
|
# treat it as a local edit and autostash it on every run (#38529).
|
||||||
# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then
|
# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then
|
||||||
# walkthroughs). Throwaway artifacts, never part of the app.
|
# walkthroughs). Throwaway artifacts, never part of the app.
|
||||||
# Web UI assets — synced from @nous-research/ui at build time via
|
# Web UI assets — synced from @nous-research/ui at build time via
|
||||||
# Web UI build output
|
# Web UI build output
|
||||||
# website/scripts/extract-automation-blueprints.py during prebuild.
|
# website/scripts/extract-automation-blueprints.py during prebuild.
|
||||||
# website/scripts/extract-skills.py during prebuild — keep them out of
|
# website/scripts/extract-skills.py during prebuild — keep them out of
|
||||||
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime;
|
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime;
|
||||||
#
|
#
|
||||||
%SystemDrive%/
|
%SystemDrive%/
|
||||||
*.pem
|
*.pem
|
||||||
*.ppk
|
*.ppk
|
||||||
*.pyc*
|
*.pyc*
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
*-snapshots/
|
*-snapshots/
|
||||||
.act-sandbox-agent.*
|
.act-sandbox-agent.*
|
||||||
.bytecode-fingerprint
|
.bytecode-fingerprint
|
||||||
.bytecode-fingerprint.tmp
|
.bytecode-fingerprint.tmp
|
||||||
.codex/
|
.codex/
|
||||||
.cursor/
|
.cursor/
|
||||||
.direnv/
|
.direnv/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
.env.development
|
.env.development
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.local
|
.env.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.test
|
.env.test
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.gemini/
|
.gemini/
|
||||||
.hermes/
|
.hermes/
|
||||||
.hermes-bootstrap-complete
|
.hermes-bootstrap-complete
|
||||||
.hermes-docker/
|
.hermes-docker/
|
||||||
.hermes-sandbox/
|
.hermes-sandbox/
|
||||||
.hermes-sandbox-e2e*/
|
.hermes-sandbox-e2e*/
|
||||||
.lazy-refresh-incomplete
|
.lazy-refresh-incomplete
|
||||||
.mcp.json
|
.mcp.json
|
||||||
.nix-stamps/
|
.nix-stamps/
|
||||||
.notebooklm-cli-venv/
|
.notebooklm-cli-venv/
|
||||||
.notebooklm-home/
|
.notebooklm-home/
|
||||||
.notebooklm-playwright/
|
.notebooklm-playwright/
|
||||||
.op.env
|
.op.env
|
||||||
.pip-cache/
|
.pip-cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.pytest-cache/
|
.pytest-cache/
|
||||||
.release_notes.md
|
.release_notes.md
|
||||||
.skills_prompt_snapshot.json
|
.skills_prompt_snapshot.json
|
||||||
.update-incomplete
|
.update-incomplete
|
||||||
.update-incomplete.lock
|
.update-incomplete.lock
|
||||||
.uv-cache/
|
.uv-cache/
|
||||||
.venv
|
.venv
|
||||||
.venv/
|
.venv/
|
||||||
.vscode/
|
.vscode/
|
||||||
.web_ui_build.lock
|
.web_ui_build.lock
|
||||||
.worktrees/
|
.worktrees/
|
||||||
.zed/
|
.zed/
|
||||||
/*.png.bak
|
/*.png.bak
|
||||||
/.hermes-runtime/
|
/.hermes-runtime/
|
||||||
/.install_method
|
/.install_method
|
||||||
/_pycache/
|
/_pycache/
|
||||||
/bin/
|
/bin/
|
||||||
/default.tar.gz
|
/default.tar.gz
|
||||||
/log.txt
|
/log.txt
|
||||||
/sqlite_leak_fix.png
|
/sqlite_leak_fix.png
|
||||||
/venv.old/
|
/venv.old/
|
||||||
/venv.stale.runtime-*/
|
/venv.stale.runtime-*/
|
||||||
/venv/
|
/venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
__pycache__/model_tools.cpython-310.pyc
|
__pycache__/model_tools.cpython-310.pyc
|
||||||
__pycache__/web_tools.cpython-310.pyc
|
__pycache__/web_tools.cpython-310.pyc
|
||||||
act/
|
act/
|
||||||
agent-browser/
|
agent-browser/
|
||||||
apps/desktop/build/
|
apps/desktop/build/
|
||||||
apps/desktop/demo/
|
apps/desktop/demo/
|
||||||
apps/desktop/dist/
|
apps/desktop/dist/
|
||||||
apps/desktop/release/
|
apps/desktop/release/
|
||||||
apps/desktop/src/**/*.d.ts
|
apps/desktop/src/**/*.d.ts
|
||||||
apps/desktop/src/**/*.js
|
apps/desktop/src/**/*.js
|
||||||
apps/desktop/src/**/*.js.map
|
apps/desktop/src/**/*.js.map
|
||||||
apps/shared/src/**/*.d.ts
|
apps/shared/src/**/*.d.ts
|
||||||
apps/shared/src/**/*.js
|
apps/shared/src/**/*.js
|
||||||
apps/shared/src/**/*.js.map
|
apps/shared/src/**/*.js.map
|
||||||
browser-use/
|
browser-use/
|
||||||
cli-config.yaml
|
cli-config.yaml
|
||||||
compose.hermes.local.yml
|
compose.hermes.local.yml
|
||||||
config/mcporter.json
|
config/mcporter.json
|
||||||
data/
|
data/
|
||||||
data/*
|
data/*
|
||||||
docs/superpowers/*
|
docs/superpowers/*
|
||||||
environments/benchmarks/evals/
|
environments/benchmarks/evals/
|
||||||
examples/
|
examples/
|
||||||
export*
|
export*
|
||||||
hermes-*/*
|
hermes-*/*
|
||||||
hermes_agent.egg-info/
|
hermes_agent.egg-info/
|
||||||
hermes_cli/scripts/
|
hermes_cli/scripts/
|
||||||
hermes_cli/tui_dist/*
|
hermes_cli/tui_dist/*
|
||||||
hermes_cli/web_dist/
|
hermes_cli/web_dist/
|
||||||
ignored/
|
ignored/
|
||||||
images/
|
images/
|
||||||
infografico/
|
infografico/
|
||||||
infograficos/
|
infograficos/
|
||||||
infographic/
|
infographic/
|
||||||
infographics/
|
infographics/
|
||||||
logs/
|
logs/
|
||||||
mini-swe-agent/
|
mini-swe-agent/
|
||||||
models-dev-upstream/
|
models-dev-upstream/
|
||||||
native/fts5_cjk/*.so
|
native/fts5_cjk/*.so
|
||||||
node_modules
|
node_modules
|
||||||
opencode.json
|
opencode.json
|
||||||
playwright-report/
|
playwright-report/
|
||||||
privvy*
|
privvy*
|
||||||
RELEASE_v*.md
|
RELEASE_v*.md
|
||||||
result
|
result
|
||||||
run_datagen_kimik2-thinking.sh
|
run_datagen_kimik2-thinking.sh
|
||||||
run_datagen_megascience_glm4-6.sh
|
run_datagen_megascience_glm4-6.sh
|
||||||
run_datagen_sonnet.sh
|
run_datagen_sonnet.sh
|
||||||
source-data/*
|
source-data/*
|
||||||
run_datagen_megascience_glm4-6.sh
|
run_datagen_megascience_glm4-6.sh
|
||||||
data/*
|
data/*
|
||||||
# No trailing slash: also matches node_modules SYMLINKS (worktrees often
|
# No trailing slash: also matches node_modules SYMLINKS (worktrees often
|
||||||
# symlink node_modules to the main checkout; the dir-only pattern let one
|
# symlink node_modules to the main checkout; the dir-only pattern let one
|
||||||
# slip into a commit and break `npm ci` on CI with ENOTDIR).
|
# slip into a commit and break `npm ci` on CI with ENOTDIR).
|
||||||
node_modules
|
node_modules
|
||||||
browser-use/
|
browser-use/
|
||||||
agent-browser/
|
agent-browser/
|
||||||
# Private keys
|
# Private keys
|
||||||
*.ppk
|
*.ppk
|
||||||
*.pem
|
*.pem
|
||||||
privvy*
|
privvy*
|
||||||
images/
|
images/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
hermes_agent.egg-info/
|
hermes_agent.egg-info/
|
||||||
wandb/
|
wandb/
|
||||||
testlogs
|
testlogs
|
||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
# Playwright visual regression baselines — cached from main in CI, not committed
|
# Playwright visual regression baselines — cached from main in CI, not committed
|
||||||
*-snapshots/
|
*-snapshots/
|
||||||
|
|
||||||
# CLI config (may contain sensitive SSH paths)
|
# CLI config (may contain sensitive SSH paths)
|
||||||
cli-config.yaml
|
cli-config.yaml
|
||||||
|
|
||||||
# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case)
|
# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case)
|
||||||
skills/.hub/
|
skills/.hub/
|
||||||
ignored/
|
ignored/
|
||||||
.worktrees/
|
.worktrees/
|
||||||
environments/benchmarks/evals/
|
environments/benchmarks/evals/
|
||||||
|
|
||||||
# Web UI build output
|
# Web UI build output
|
||||||
hermes_cli/web_dist/
|
hermes_cli/web_dist/
|
||||||
# Cross-process web UI build lock (flock target, always empty)
|
# Cross-process web UI build lock (flock target, always empty)
|
||||||
.web_ui_build.lock
|
.web_ui_build.lock
|
||||||
apps/desktop/build/
|
apps/desktop/build/
|
||||||
apps/desktop/dist/
|
apps/desktop/dist/
|
||||||
|
|
||||||
# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then
|
# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then
|
||||||
# resolves the stale .js OVER the .tsx — never track these)
|
# resolves the stale .js OVER the .tsx — never track these)
|
||||||
apps/desktop/src/**/*.js
|
apps/desktop/src/**/*.js
|
||||||
apps/desktop/src/**/*.js.map
|
apps/desktop/src/**/*.js.map
|
||||||
apps/desktop/src/**/*.d.ts
|
apps/desktop/src/**/*.d.ts
|
||||||
# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins,
|
# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins,
|
||||||
# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx
|
# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx
|
||||||
# sibling exists, so the stale-shadow hazard above cannot apply.
|
# sibling exists, so the stale-shadow hazard above cannot apply.
|
||||||
!apps/desktop/src/plugins/*/plugin.js
|
!apps/desktop/src/plugins/*/plugin.js
|
||||||
!apps/desktop/src/global.d.ts
|
!apps/desktop/src/global.d.ts
|
||||||
!apps/desktop/src/vite-env.d.ts
|
!apps/desktop/src/vite-env.d.ts
|
||||||
|
|
||||||
# Build/debug artifacts that must never be committed
|
# Build/debug artifacts that must never be committed
|
||||||
/log.txt
|
/log.txt
|
||||||
/sqlite_leak_fix.png
|
/sqlite_leak_fix.png
|
||||||
/*.png.bak
|
/*.png.bak
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.tgz
|
*.tgz
|
||||||
apps/shared/src/**/*.js
|
apps/shared/src/**/*.js
|
||||||
apps/shared/src/**/*.js.map
|
apps/shared/src/**/*.js.map
|
||||||
apps/shared/src/**/*.d.ts
|
apps/shared/src/**/*.d.ts
|
||||||
apps/desktop/release/
|
apps/desktop/release/
|
||||||
# stage-and-swap Desktop rebuild output (#86443); removed after the swap, but
|
# stage-and-swap Desktop rebuild output (#86443); removed after the swap, but
|
||||||
# a killed build must not leave the checkout dirty
|
# a killed build must not leave the checkout dirty
|
||||||
apps/desktop/.staging-*/
|
apps/desktop/.staging-*/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# Web UI assets — synced from @nous-research/ui at build time via
|
# Web UI assets — synced from @nous-research/ui at build time via
|
||||||
# `npm run sync-assets` (see web/package.json).
|
# `npm run sync-assets` (see web/package.json).
|
||||||
web/public/fonts/
|
web/public/fonts/
|
||||||
web/public/ds-assets/
|
web/public/ds-assets/
|
||||||
|
|
||||||
# Release script temp files
|
# Release script temp files
|
||||||
.release_notes.md
|
.release_notes.md
|
||||||
mini-swe-agent/
|
mini-swe-agent/
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
.direnv/
|
.direnv/
|
||||||
.nix-stamps/
|
.nix-stamps/
|
||||||
result
|
result
|
||||||
website/static/api/skills-index.json
|
website/static/api/skills-index.json
|
||||||
# skills.json + skills-meta.json are build artifacts emitted by
|
# skills.json + skills-meta.json are build artifacts emitted by
|
||||||
# website/scripts/extract-skills.py during prebuild — keep them out of
|
# website/scripts/extract-skills.py during prebuild — keep them out of
|
||||||
# git for the same reason as skills-index.json (large, generated, change
|
# git for the same reason as skills-index.json (large, generated, change
|
||||||
# every build).
|
# every build).
|
||||||
website/static/api/skills.json
|
website/static/api/skills.json
|
||||||
|
website/static/api/skills-meta.json
|
||||||
|
# automation-blueprints-index.json is a build artifact emitted by
|
||||||
|
# website/scripts/extract-automation-blueprints.py during prebuild.
|
||||||
|
website/static/api/automation-blueprints-index.json
|
||||||
|
models-dev-upstream/
|
||||||
|
|
||||||
|
# Local editor / agent tooling (machine-specific; keep in global config, not the repo)
|
||||||
|
.codex/
|
||||||
|
.cursor/
|
||||||
|
.gemini/
|
||||||
|
.zed/
|
||||||
|
.mcp.json
|
||||||
|
opencode.json
|
||||||
|
config/mcporter.json
|
||||||
|
|
||||||
|
hermes_cli/tui_dist/*
|
||||||
|
hermes_cli/scripts/
|
||||||
|
docs/superpowers/*
|
||||||
|
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime;
|
||||||
|
# also created in-repo when an agent operates in this checkout). Plans, audit
|
||||||
|
# logs, and per-session caches are never artifacts of the codebase.
|
||||||
|
.hermes/
|
||||||
|
|
||||||
|
# Desktop/bootstrap install marker written into the managed checkout root by the
|
||||||
|
# bootstrap installer. It is Hermes-managed runtime state, never a code change —
|
||||||
|
# ignore it so `hermes update`'s `git stash push --include-untracked` does not
|
||||||
|
# treat it as a local edit and autostash it on every run (#38529).
|
||||||
|
.hermes-bootstrap-complete
|
||||||
|
|
||||||
|
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent)
|
||||||
|
.hermes-sandbox/
|
||||||
|
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
|
||||||
|
# the route name, so each route gets its own tree and two can run at once.
|
||||||
|
.hermes-sandbox-e2e*/
|
||||||
|
|
||||||
|
# Interrupted-update breadcrumb + recovery lock written next to the shared venv
|
||||||
|
# by `hermes update` / launch-time self-heal. Runtime state, never a code change
|
||||||
|
# — ignore so `git status` stays clean and update's autostash skips them.
|
||||||
|
.update-incomplete
|
||||||
|
.update-incomplete.lock
|
||||||
|
|
||||||
|
# Checkout fingerprint the __pycache__ tree was last validated against
|
||||||
|
# (launch-time stale-bytecode sweep). Runtime state, never a code change.
|
||||||
|
.bytecode-fingerprint
|
||||||
|
.bytecode-fingerprint.tmp
|
||||||
|
|
||||||
|
# Installer-written method stamp in the managed checkout root (scripts/install.sh).
|
||||||
|
# Runtime metadata only — never a code change. Ignore so `git status` stays clean
|
||||||
|
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
|
||||||
|
/.install_method
|
||||||
|
|
||||||
|
# Tool Search live-test harness output — non-deterministic model transcripts,
|
||||||
|
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
|
||||||
|
|
||||||
|
scripts/out/
|
||||||
|
|
||||||
|
# Per-release changelog drafts. These exist only transiently during a release
|
||||||
|
# cut (passed to `gh release create --notes-file`); the GitHub Release itself
|
||||||
|
# stores the published notes. They are not a build artifact and must never be
|
||||||
|
# committed to the repo root. See the hermes-release skill.
|
||||||
|
|
||||||
|
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
|
||||||
|
# walkthroughs). Throwaway artifacts, never part of the app.
|
||||||
|
|
||||||
|
# PR infographics are rendered locally and embedded in PR descriptions via the
|
||||||
|
# image-provider (fal.media) URL — they are NEVER committed to the repo. The
|
||||||
|
# PR body is the archive. See the hermes-agent-dev skill's
|
||||||
|
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
|
||||||
|
#
|
||||||
|
# Spelling variants are listed because a single `infographic/` pattern was
|
||||||
|
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only
|
||||||
|
# the first line of defence and cannot stop `git add -f` at all — the
|
||||||
|
# infographic-check CI job is what actually enforces this.
|
||||||
|
# Runtime marker written by hermes update when a lazy dependency refresh is
|
||||||
|
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
|
||||||
|
# by accident via 3a69e34702, removed in the #72002 salvage).
|
||||||
|
|
||||||
|
# Disposable profile created by scripts/probe_active_session_exclusivity.py
|
||||||
|
.probe-home/
|
||||||
|
skills/.hub/
|
||||||
|
source-data/*
|
||||||
|
temp_vision_images/
|
||||||
|
test_durations.json
|
||||||
|
testlogs
|
||||||
|
test-results/
|
||||||
|
tests/quick_test_dataset.jsonl
|
||||||
|
tests/sample_dataset.jsonl
|
||||||
|
tmp/
|
||||||
|
wandb/
|
||||||
|
web/public/ds-assets/
|
||||||
|
web/public/fonts/
|
||||||
|
website/static/api/automation-blueprints-index.json
|
||||||
|
website/static/api/skills.json
|
||||||
|
website/static/api/skills-index.json
|
||||||
website/static/api/skills-meta.json
|
website/static/api/skills-meta.json
|
||||||
# automation-blueprints-index.json is a build artifact emitted by
|
# ── generated icon assets (see scripts/generate_icons.py) ──────────────────
|
||||||
# website/scripts/extract-automation-blueprints.py during prebuild.
|
# Regenerated on demand by every consuming pipeline (website/desktop/
|
||||||
website/static/api/automation-blueprints-index.json
|
# installer/web prebuild via scripts/generate-icons.mjs). Sources of truth:
|
||||||
models-dev-upstream/
|
# assets/nous-girl-*.svg + assets/backgrounds/.
|
||||||
|
assets/icon-master.svg
|
||||||
# Local editor / agent tooling (machine-specific; keep in global config, not the repo)
|
assets/icon-master-dark.svg
|
||||||
.codex/
|
apps/desktop/assets/icon.png
|
||||||
.cursor/
|
apps/desktop/assets/icon.ico
|
||||||
.gemini/
|
apps/desktop/assets/icon.icns
|
||||||
.zed/
|
apps/desktop/assets/icon-dark.png
|
||||||
.mcp.json
|
apps/desktop/assets/icon-dark.ico
|
||||||
opencode.json
|
apps/desktop/assets/icon-dark.icns
|
||||||
config/mcporter.json
|
apps/desktop/assets/appx/*.png
|
||||||
|
apps/desktop/public/apple-touch-icon.png
|
||||||
hermes_cli/tui_dist/*
|
apps/desktop/public/nous-girl.png
|
||||||
hermes_cli/scripts/
|
apps/desktop/public/nous-girl-dark.png
|
||||||
docs/superpowers/*
|
apps/bootstrap-installer/src-tauri/icons/*.png
|
||||||
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime;
|
apps/bootstrap-installer/src-tauri/icons/icon.ico
|
||||||
# also created in-repo when an agent operates in this checkout). Plans, audit
|
apps/bootstrap-installer/src-tauri/icons/icon.icns
|
||||||
# logs, and per-session caches are never artifacts of the codebase.
|
apps/bootstrap-installer/public/nous-girl.png
|
||||||
.hermes/
|
website/static/img/logo.png
|
||||||
|
website/static/img/logo-dark.png
|
||||||
# Desktop/bootstrap install marker written into the managed checkout root by the
|
website/static/img/nous-logo.png
|
||||||
# bootstrap installer. It is Hermes-managed runtime state, never a code change —
|
website/static/img/nous-logo-dark.png
|
||||||
# ignore it so `hermes update`'s `git stash push --include-untracked` does not
|
website/static/img/favicon-16x16.png
|
||||||
# treat it as a local edit and autostash it on every run (#38529).
|
website/static/img/favicon-32x32.png
|
||||||
.hermes-bootstrap-complete
|
website/static/img/apple-touch-icon.png
|
||||||
|
website/static/img/favicon.ico
|
||||||
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent)
|
website/static/img/favicon.svg
|
||||||
.hermes-sandbox/
|
web/public/favicon.ico
|
||||||
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
|
|
||||||
# the route name, so each route gets its own tree and two can run at once.
|
|
||||||
.hermes-sandbox-e2e*/
|
|
||||||
|
|
||||||
# Interrupted-update breadcrumb + recovery lock written next to the shared venv
|
|
||||||
# by `hermes update` / launch-time self-heal. Runtime state, never a code change
|
|
||||||
# — ignore so `git status` stays clean and update's autostash skips them.
|
|
||||||
.update-incomplete
|
|
||||||
.update-incomplete.lock
|
|
||||||
|
|
||||||
# Checkout fingerprint the __pycache__ tree was last validated against
|
|
||||||
# (launch-time stale-bytecode sweep). Runtime state, never a code change.
|
|
||||||
.bytecode-fingerprint
|
|
||||||
.bytecode-fingerprint.tmp
|
|
||||||
|
|
||||||
# Installer-written method stamp in the managed checkout root (scripts/install.sh).
|
|
||||||
# Runtime metadata only — never a code change. Ignore so `git status` stays clean
|
|
||||||
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
|
|
||||||
/.install_method
|
|
||||||
|
|
||||||
# Tool Search live-test harness output — non-deterministic model transcripts,
|
|
||||||
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
|
|
||||||
|
|
||||||
scripts/out/
|
|
||||||
|
|
||||||
# Per-release changelog drafts. These exist only transiently during a release
|
|
||||||
# cut (passed to `gh release create --notes-file`); the GitHub Release itself
|
|
||||||
# stores the published notes. They are not a build artifact and must never be
|
|
||||||
# committed to the repo root. See the hermes-release skill.
|
|
||||||
|
|
||||||
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
|
|
||||||
# walkthroughs). Throwaway artifacts, never part of the app.
|
|
||||||
|
|
||||||
# PR infographics are rendered locally and embedded in PR descriptions via the
|
|
||||||
# image-provider (fal.media) URL — they are NEVER committed to the repo. The
|
|
||||||
# PR body is the archive. See the hermes-agent-dev skill's
|
|
||||||
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
|
|
||||||
#
|
|
||||||
# Spelling variants are listed because a single `infographic/` pattern was
|
|
||||||
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only
|
|
||||||
# the first line of defence and cannot stop `git add -f` at all — the
|
|
||||||
# infographic-check CI job is what actually enforces this.
|
|
||||||
# Runtime marker written by hermes update when a lazy dependency refresh is
|
|
||||||
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
|
|
||||||
# by accident via 3a69e34702, removed in the #72002 salvage).
|
|
||||||
|
|
||||||
# Disposable profile created by scripts/probe_active_session_exclusivity.py
|
|
||||||
.probe-home/
|
|
||||||
skills/.hub/
|
|
||||||
source-data/*
|
|
||||||
temp_vision_images/
|
|
||||||
test_durations.json
|
|
||||||
testlogs
|
|
||||||
test-results/
|
|
||||||
tests/quick_test_dataset.jsonl
|
|
||||||
tests/sample_dataset.jsonl
|
|
||||||
tmp/
|
|
||||||
wandb/
|
|
||||||
web/public/ds-assets/
|
|
||||||
web/public/fonts/
|
|
||||||
website/static/api/automation-blueprints-index.json
|
|
||||||
website/static/api/skills.json
|
|
||||||
website/static/api/skills-index.json
|
|
||||||
website/static/api/skills-meta.json
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 127.0.0.1 --port 5175",
|
"dev": "vite --host 127.0.0.1 --port 5175",
|
||||||
|
"prebuild": "node ../../scripts/generate-icons.mjs",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@@ -2,12 +2,12 @@ import { cn } from '../lib/utils'
|
|||||||
|
|
||||||
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
||||||
|
|
||||||
// Brand badge: nous-girl mark on a white tile (the light BrandMark variant).
|
// Brand badge: the nous-girl mark in the app-icon squircle (light variant).
|
||||||
// Ported from apps/desktop's BrandMark; asset lives in this app's public/.
|
// Ported from apps/desktop's BrandMark; the installer has no theme system so
|
||||||
// The installer has no theme system, so it always uses the light asset.
|
// it always uses the light squircle asset. Generated by generate_icons.py.
|
||||||
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
|
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
|
||||||
return (
|
return (
|
||||||
<span className={cn('inline-flex size-14 shrink-0 items-center justify-center bg-white', className)} {...props}>
|
<span className={cn('inline-flex size-14 shrink-0 items-center justify-center', className)} {...props}>
|
||||||
<img alt="" className="size-full object-contain" src={assetPath('nous-girl.png')} />
|
<img alt="" className="size-full object-contain" src={assetPath('nous-girl.png')} />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ Sizes: `default`, `xs`, `overlay` (titlebar glyph counts).
|
|||||||
mode, white girl on the `#0d1117` dark tile in dark mode (keyed off
|
mode, white girl on the `#0d1117` dark tile in dark mode (keyed off
|
||||||
`renderedMode` so bright surfaces in dark skins stay on the light mark).
|
`renderedMode` so bright surfaces in dark skins stay on the light mark).
|
||||||
The marks are generated by `scripts/generate_icons.py` from the girl SVGs
|
The marks are generated by `scripts/generate_icons.py` from the girl SVGs
|
||||||
+ tile backgrounds (`assets/nous-girl-*.svg` × `assets/backgrounds/tile-*.svg`).
|
+ squircle backgrounds (`assets/nous-girl-*.svg` × `assets/backgrounds/`).
|
||||||
It replaced scattered Sparkles glyphs in updates / onboarding / about. Use it
|
It replaced scattered Sparkles glyphs in updates / onboarding / about. Use it
|
||||||
for hero/brand moments; don't reintroduce decorative star/sparkle icons.
|
for hero/brand moments; don't reintroduce decorative star/sparkle icons.
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 64 KiB |
@@ -19,7 +19,7 @@
|
|||||||
"clean:e2e": "tsc --build tsconfig.e2e.json --clean",
|
"clean:e2e": "tsc --build tsconfig.e2e.json --clean",
|
||||||
"clean:renderer": "tsc --build tsconfig.json --clean ",
|
"clean:renderer": "tsc --build tsconfig.json --clean ",
|
||||||
"clean:electron": "tsc --build tsconfig.electron.json --clean",
|
"clean:electron": "tsc --build tsconfig.electron.json --clean",
|
||||||
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
"dev": "node ../../scripts/generate-icons.mjs && concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||||
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
|
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
|
||||||
"dev:mock": "node scripts/dev-mock.mjs",
|
"dev:mock": "node scripts/dev-mock.mjs",
|
||||||
"dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174",
|
"dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
"profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||||
"profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
"profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||||
"start": "npm run build && electron .",
|
"start": "npm run build && electron .",
|
||||||
"prebuild": "node scripts/assert-root-install.mjs && npm run clean",
|
"prebuild": "node ../../scripts/generate-icons.mjs && node scripts/assert-root-install.mjs && npm run clean",
|
||||||
"build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs",
|
"build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs",
|
||||||
"postbuild": "node scripts/assert-dist-built.mjs",
|
"postbuild": "node scripts/assert-dist-built.mjs",
|
||||||
"prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs",
|
"prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
@@ -3,22 +3,18 @@ import { useTheme } from '@/themes'
|
|||||||
|
|
||||||
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
||||||
|
|
||||||
// Brand badge: nous-girl mark on a fixed tile, theme-aware — light mode shows
|
// Brand badge: the nous-girl mark in the app-icon squircle, theme-aware —
|
||||||
// the black girl on white, dark mode the white girl on the nous dark surface.
|
// light mode shows the black girl on the white squircle, dark mode the white
|
||||||
// Fills the tile (softly rounded); size via className (default size-14).
|
// girl on the #0d1117 squircle. The mark PNG carries the rounded shape and
|
||||||
|
// transparent corners, so no tile/rounding classes here. Generated by
|
||||||
|
// scripts/generate_icons.py from the girl SVGs + squircle backgrounds; size
|
||||||
|
// via className (default size-14).
|
||||||
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
|
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
|
||||||
const { renderedMode } = useTheme()
|
const { renderedMode } = useTheme()
|
||||||
const dark = renderedMode === 'dark'
|
const dark = renderedMode === 'dark'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={cn('inline-flex size-14 shrink-0 items-center justify-center', className)} {...props}>
|
||||||
className={cn(
|
|
||||||
'inline-flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white',
|
|
||||||
dark && 'bg-[#0d1117]',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<img alt="" className="size-full object-contain" src={assetPath(dark ? 'nous-girl-dark.png' : 'nous-girl.png')} />
|
<img alt="" className="size-full object-contain" src={assetPath(dark ? 'nous-girl-dark.png' : 'nous-girl.png')} />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1772 1799">
|
|
||||||
<!-- website wordmark background, dark: white frame (L/R 14, T 17, B 16) + #0d1117 interior -->
|
|
||||||
<rect x="0" y="0" width="1772" height="1799" rx="2" fill="#ffffff"/>
|
|
||||||
<rect x="14" y="17" width="1743" height="1766" fill="#0d1117"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 307 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1772 1799">
|
|
||||||
<!-- website wordmark background, light: black frame (L/R 14, T 17, B 16) + white interior -->
|
|
||||||
<rect x="0" y="0" width="1772" height="1799" rx="2" fill="#000000"/>
|
|
||||||
<rect x="14" y="17" width="1743" height="1766" fill="#ffffff"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 306 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
|
||||||
<!-- BrandMark tile, dark: solid #0d1117 -->
|
|
||||||
<rect x="0" y="0" width="256" height="256" fill="#0d1117"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 179 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
|
||||||
<!-- BrandMark tile, light: solid white -->
|
|
||||||
<rect x="0" y="0" width="256" height="256" fill="#ffffff"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 113 KiB |
57
scripts/generate-icons.mjs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Generate the icon assets on demand for the pipeline that needs them.
|
||||||
|
*
|
||||||
|
* Generated icons are NOT committed (see scripts/generate_icons.py) — every
|
||||||
|
* consuming pipeline regenerates them before building:
|
||||||
|
* - website: website/scripts/prebuild.mjs (docusaurus prebuild)
|
||||||
|
* - desktop: apps/desktop/package.json prebuild + predev
|
||||||
|
* - installer: apps/bootstrap-installer/package.json prebuild
|
||||||
|
* - web: web/package.json prebuild
|
||||||
|
*
|
||||||
|
* Finds the project venv python (resvg-py rides the dev extra) and runs the
|
||||||
|
* generator from the repo root. Fails loudly if the toolchain is missing so
|
||||||
|
* a build can never silently ship without icons.
|
||||||
|
*/
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
|
||||||
|
function findPython() {
|
||||||
|
const candidates = [
|
||||||
|
path.join(repoRoot, '.venv', 'Scripts', 'python.exe'), // windows
|
||||||
|
path.join(repoRoot, '.venv', 'bin', 'python'), // posix
|
||||||
|
path.join(repoRoot, 'venv', 'Scripts', 'python.exe'),
|
||||||
|
path.join(repoRoot, 'venv', 'bin', 'python'),
|
||||||
|
]
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (existsSync(candidate)) return candidate
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const python = findPython()
|
||||||
|
const generator = path.join(repoRoot, 'scripts', 'generate_icons.py')
|
||||||
|
let result
|
||||||
|
if (python) {
|
||||||
|
result = spawnSync(python, [generator], { cwd: repoRoot, stdio: 'inherit' })
|
||||||
|
} else if (process.env.VIRTUAL_ENV) {
|
||||||
|
const py = process.platform === 'win32' ? 'python.exe' : 'python'
|
||||||
|
result = spawnSync(py, [generator], { cwd: repoRoot, stdio: 'inherit', env: process.env })
|
||||||
|
} else {
|
||||||
|
// Fall back to uv-managed python (CI installs the dev extra).
|
||||||
|
result = spawnSync('uv', ['run', 'python', generator], { cwd: repoRoot, stdio: 'inherit' })
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
console.error('[generate-icons] failed to launch icon generator:', result.error.message)
|
||||||
|
console.error('[generate-icons] install the dev extra with: uv sync --extra dev')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
process.exit(result.status ?? 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Usage (from repo root):
|
Usage (from repo root):
|
||||||
.venv/Scripts/python.exe scripts/generate_icons.py # write
|
.venv/Scripts/python.exe scripts/generate_icons.py # write
|
||||||
.venv/Scripts/python.exe scripts/generate_icons.py --check # verify only
|
.venv/Scripts/python.exe scripts/generate_icons.py --check # verify structure
|
||||||
|
|
||||||
Sources of truth — two axes, composed per target:
|
Sources of truth — two axes, composed per target:
|
||||||
Girl art (vector): assets/nous-girl-black.svg (black positive space)
|
Girl art (vector): assets/nous-girl-black.svg (black positive space)
|
||||||
@@ -12,27 +12,26 @@ Sources of truth — two axes, composed per target:
|
|||||||
5487^2 viewBox, one path each).
|
5487^2 viewBox, one path each).
|
||||||
|
|
||||||
Backgrounds (per platform surface, light/dark):
|
Backgrounds (per platform surface, light/dark):
|
||||||
assets/backgrounds/squircle-light.svg white rounded
|
assets/backgrounds/squircle-light.svg white rounded
|
||||||
assets/backgrounds/squircle-dark.svg #0d1117 rounded
|
assets/backgrounds/squircle-dark.svg #0d1117 rounded
|
||||||
assets/backgrounds/logo-frame.svg black frame +
|
|
||||||
white interior (website)
|
|
||||||
assets/backgrounds/logo-frame-dark.svg white frame +
|
|
||||||
#0d1117 interior
|
|
||||||
assets/backgrounds/tile-light.svg solid white (BrandMark)
|
|
||||||
assets/backgrounds/tile-dark.svg solid #0d1117
|
|
||||||
|
|
||||||
The master SVGs (assets/icon-master.svg light, assets/icon-master-dark.svg
|
The master SVGs (assets/icon-master.svg light, assets/icon-master-dark.svg
|
||||||
dark) are GENERATED artifacts — squircle background + girl nested into the
|
dark) are GENERATED artifacts — squircle background + girl nested into the
|
||||||
824px HIG content safe zone — not hand-edited sources. The 1024 master is
|
824px HIG content safe zone. The light master drives every squircle target;
|
||||||
what every squircle target renders from.
|
the dark master drives the dark-appearance targets.
|
||||||
|
|
||||||
If the girl SVGs are missing a red-circle placeholder master is written so a
|
The girl is nested via its art bbox as viewBox, so it always lands centered in
|
||||||
partial checkout still generates; --check then flags the drift.
|
the box (824 safe zone for squircles / height-fitted for the marks) without
|
||||||
|
distortion. The girl art corners sit ~185px from the squircle arc centers vs
|
||||||
|
the 245px radius, so no art touches the rounded corners on any OS mask.
|
||||||
|
|
||||||
--check mode regenerates every target in memory and byte-compares it
|
GENERATED OUTPUTS ARE NOT COMMITTED. Everything this script writes is
|
||||||
against the committed file, exiting nonzero on any drift. This is what
|
gitignored and regenerated on demand by the consuming pipelines (website
|
||||||
CI runs (see .github/workflows/icons-freshness-check.yml) so the repo
|
prebuild, desktop prebuild/predev, installer prebuild, web prebuild — via
|
||||||
can never hold a hand-edited or stale generated icon.
|
scripts/generate-icons.mjs). The freshness lane (icons-freshness-check.yml)
|
||||||
|
runs --check, which regenerates in memory and asserts structural invariants
|
||||||
|
(sizes, transparency, container frame sets) — there are no committed bytes to
|
||||||
|
byte-compare against.
|
||||||
|
|
||||||
Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size.
|
Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size.
|
||||||
Containers: Pillow for multi-size .ico and .icns.
|
Containers: Pillow for multi-size .ico and .icns.
|
||||||
@@ -41,7 +40,7 @@ Deps:
|
|||||||
Pillow (core dependency), resvg-py (dev extra):
|
Pillow (core dependency), resvg-py (dev extra):
|
||||||
uv sync --extra dev
|
uv sync --extra dev
|
||||||
|
|
||||||
Outputs (35 files):
|
Outputs (30 files):
|
||||||
assets/icon-master.svg generated light master
|
assets/icon-master.svg generated light master
|
||||||
assets/icon-master-dark.svg generated dark master
|
assets/icon-master-dark.svg generated dark master
|
||||||
apps/desktop/assets/icon.png 1024x1024 squircle (light)
|
apps/desktop/assets/icon.png 1024x1024 squircle (light)
|
||||||
@@ -54,21 +53,18 @@ Outputs (35 files):
|
|||||||
apps/desktop/assets/appx/StoreLogo.png 50x50 squircle
|
apps/desktop/assets/appx/StoreLogo.png 50x50 squircle
|
||||||
apps/desktop/assets/appx/Square44x44Logo.png 44x44 squircle
|
apps/desktop/assets/appx/Square44x44Logo.png 44x44 squircle
|
||||||
apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle
|
apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle
|
||||||
apps/desktop/assets/appx/*-dark.png dark-appearance logos (ready
|
apps/desktop/assets/appx/*-dark.png dark-appearance logos
|
||||||
to wire into the MSIX
|
|
||||||
manifest when app-builder-lib
|
|
||||||
supports contrast images)
|
|
||||||
apps/desktop/public/apple-touch-icon.png 1024x1024 squircle
|
apps/desktop/public/apple-touch-icon.png 1024x1024 squircle
|
||||||
apps/desktop/public/nous-girl.png 256x256 PNG, black girl on white (light)
|
apps/desktop/public/nous-girl.png 256x256 squircle, black girl (light mark)
|
||||||
apps/desktop/public/nous-girl-dark.png 256x256 PNG, white girl on #0d1117 (dark)
|
apps/desktop/public/nous-girl-dark.png 256x256 squircle, white girl (dark mark)
|
||||||
apps/bootstrap-installer/src-tauri/icons/32x32.png 32x32
|
apps/bootstrap-installer/src-tauri/icons/32x32.png 32x32
|
||||||
apps/bootstrap-installer/src-tauri/icons/128x128.png 128x128
|
apps/bootstrap-installer/src-tauri/icons/128x128.png 128x128
|
||||||
apps/bootstrap-installer/src-tauri/icons/128x128@2x.png 256x256
|
apps/bootstrap-installer/src-tauri/icons/128x128@2x.png 256x256
|
||||||
apps/bootstrap-installer/src-tauri/icons/icon.ico 16,32,64,128,256
|
apps/bootstrap-installer/src-tauri/icons/icon.ico 16,32,64,128,256
|
||||||
apps/bootstrap-installer/src-tauri/icons/icon.icns 16..1024
|
apps/bootstrap-installer/src-tauri/icons/icon.icns 16..1024
|
||||||
apps/bootstrap-installer/public/nous-girl.png 256x256 PNG, black girl on white (light)
|
apps/bootstrap-installer/public/nous-girl.png 256x256 squircle mark (light)
|
||||||
website/static/img/logo.png 1772x1799 black-frame wordmark
|
website/static/img/logo.png 1772x1799 girl alone, transparent (light)
|
||||||
website/static/img/logo-dark.png 1772x1799 white-frame wordmark
|
website/static/img/logo-dark.png 1772x1799 girl alone, transparent (dark)
|
||||||
website/static/img/nous-logo.png 150x150 on white (opaque)
|
website/static/img/nous-logo.png 150x150 on white (opaque)
|
||||||
website/static/img/nous-logo-dark.png 150x150 on #0d1117 (opaque)
|
website/static/img/nous-logo-dark.png 150x150 on #0d1117 (opaque)
|
||||||
website/static/img/favicon-16x16.png 16x16
|
website/static/img/favicon-16x16.png 16x16
|
||||||
@@ -113,24 +109,41 @@ DARK_RGB = (13, 17, 23)
|
|||||||
|
|
||||||
# Girl placement per background: (x, y, w, h) in that background's coordinate
|
# Girl placement per background: (x, y, w, h) in that background's coordinate
|
||||||
# space. Squircle backgrounds put the girl in the 824px HIG content safe zone
|
# space. Squircle backgrounds put the girl in the 824px HIG content safe zone
|
||||||
# (centered, 100px pad on a 1024 canvas); logo frames fill the white/dark
|
# (centered, 100px pad on a 1024 canvas). Marks reuse the same squircles.
|
||||||
# interior at ~1.7x; BrandMark tiles take the girl height-fitted (meet).
|
|
||||||
GIRL_BOXES = {
|
GIRL_BOXES = {
|
||||||
"squircle-light.svg": (100, 100, 824, 824),
|
"squircle-light.svg": (100, 100, 824, 824),
|
||||||
"squircle-dark.svg": (100, 100, 824, 824),
|
"squircle-dark.svg": (100, 100, 824, 824),
|
||||||
"logo-frame.svg": (14, 17, 1743, 1766),
|
|
||||||
"logo-frame-dark.svg": (14, 17, 1743, 1766),
|
|
||||||
"tile-light.svg": (0, 0, 256, 256),
|
|
||||||
"tile-dark.svg": (0, 0, 256, 256),
|
|
||||||
}
|
}
|
||||||
# The brand-kit SVG canvas (both girl svgs share this viewBox).
|
# The brand-kit SVG canvas (both girl svgs share this viewBox).
|
||||||
GIRL_VIEWBOX = 5487.0615
|
GIRL_VIEWBOX = 5487.0615
|
||||||
|
|
||||||
PLACEHOLDER = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
# Target sizes for --check's structural verification: relpath -> (format, size)
|
||||||
<rect x="0" y="0" width="1024" height="1024" rx="{SQUIRCLE_RADIUS}" fill="#ffffff"/>
|
CHECK_SIZES: dict[str, tuple[str, tuple[int, int]]] = {
|
||||||
<circle cx="512" cy="512" r="420" fill="#e34c4c"/>
|
"apps/desktop/assets/icon.png": ("PNG", (1024, 1024)),
|
||||||
</svg>
|
"apps/desktop/assets/icon-dark.png": ("PNG", (1024, 1024)),
|
||||||
'''
|
"apps/desktop/assets/appx/Wide310x150Logo.png": ("PNG", (310, 150)),
|
||||||
|
"apps/desktop/assets/appx/Wide310x150Logo-dark.png": ("PNG", (310, 150)),
|
||||||
|
"apps/desktop/assets/appx/StoreLogo.png": ("PNG", (50, 50)),
|
||||||
|
"apps/desktop/assets/appx/StoreLogo-dark.png": ("PNG", (50, 50)),
|
||||||
|
"apps/desktop/assets/appx/Square44x44Logo.png": ("PNG", (44, 44)),
|
||||||
|
"apps/desktop/assets/appx/Square44x44Logo-dark.png": ("PNG", (44, 44)),
|
||||||
|
"apps/desktop/assets/appx/Square150x150Logo.png": ("PNG", (150, 150)),
|
||||||
|
"apps/desktop/assets/appx/Square150x150Logo-dark.png": ("PNG", (150, 150)),
|
||||||
|
"apps/desktop/public/apple-touch-icon.png": ("PNG", (1024, 1024)),
|
||||||
|
"apps/desktop/public/nous-girl.png": ("PNG", (256, 256)),
|
||||||
|
"apps/desktop/public/nous-girl-dark.png": ("PNG", (256, 256)),
|
||||||
|
"apps/bootstrap-installer/src-tauri/icons/32x32.png": ("PNG", (32, 32)),
|
||||||
|
"apps/bootstrap-installer/src-tauri/icons/128x128.png": ("PNG", (128, 128)),
|
||||||
|
"apps/bootstrap-installer/src-tauri/icons/128x128@2x.png": ("PNG", (256, 256)),
|
||||||
|
"apps/bootstrap-installer/public/nous-girl.png": ("PNG", (256, 256)),
|
||||||
|
"website/static/img/logo.png": ("PNG", (1772, 1799)),
|
||||||
|
"website/static/img/logo-dark.png": ("PNG", (1772, 1799)),
|
||||||
|
"website/static/img/nous-logo.png": ("PNG", (150, 150)),
|
||||||
|
"website/static/img/nous-logo-dark.png": ("PNG", (150, 150)),
|
||||||
|
"website/static/img/favicon-16x16.png": ("PNG", (16, 16)),
|
||||||
|
"website/static/img/favicon-32x32.png": ("PNG", (32, 32)),
|
||||||
|
"website/static/img/apple-touch-icon.png": ("PNG", (180, 180)),
|
||||||
|
}
|
||||||
|
|
||||||
# (relpath, kind, arg)
|
# (relpath, kind, arg)
|
||||||
TARGETS: list[tuple[str, str, object]] = [
|
TARGETS: list[tuple[str, str, object]] = [
|
||||||
@@ -216,7 +229,7 @@ def girl_layer(girl: str, box: tuple[float, float, float, float]) -> str:
|
|||||||
def background_inner(name: str) -> tuple[str, int, int]:
|
def background_inner(name: str) -> tuple[str, int, int]:
|
||||||
"""Inner content + (width, height) of a background SVG asset."""
|
"""Inner content + (width, height) of a background SVG asset."""
|
||||||
text = (BACKGROUNDS / name).read_text(encoding="utf-8")
|
text = (BACKGROUNDS / name).read_text(encoding="utf-8")
|
||||||
m = re.search(r"<svg\b[^>]*viewBox=\"0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)\"[^>]*>", text)
|
m = re.search(r'<svg\b[^>]*viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"[^>]*>', text)
|
||||||
assert m, f"cannot parse viewBox of {name}"
|
assert m, f"cannot parse viewBox of {name}"
|
||||||
w, h = float(m.group(1)), float(m.group(2))
|
w, h = float(m.group(1)), float(m.group(2))
|
||||||
inner = re.sub(r"^.*?>\s*", "", text, count=1, flags=re.S)
|
inner = re.sub(r"^.*?>\s*", "", text, count=1, flags=re.S)
|
||||||
@@ -224,14 +237,15 @@ def background_inner(name: str) -> tuple[str, int, int]:
|
|||||||
return inner, int(w), int(h)
|
return inner, int(w), int(h)
|
||||||
|
|
||||||
|
|
||||||
def compose_master(girl: str, bg: str) -> str:
|
def compose_svg(girl: str, bg: str) -> str:
|
||||||
"""Composed master SVG: background + girl layer."""
|
"""Full svg text: background + girl layer, in the background's native
|
||||||
|
coordinate space (resvg scales to whatever output size is requested, so
|
||||||
|
the composition is size-agnostic — no manual box scaling)."""
|
||||||
inner, w, h = background_inner(bg)
|
inner, w, h = background_inner(bg)
|
||||||
x, y, bx, bh = GIRL_BOXES[bg]
|
|
||||||
return (
|
return (
|
||||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n'
|
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n'
|
||||||
f" {inner.strip()}\n"
|
f" {inner.strip()}\n"
|
||||||
f" {girl_layer(girl, (x, y, bx, bh))}\n"
|
f" {girl_layer(girl, GIRL_BOXES[bg])}\n"
|
||||||
"</svg>\n"
|
"</svg>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,12 +257,17 @@ def ensure_masters(check: bool = False) -> None:
|
|||||||
if missing:
|
if missing:
|
||||||
if check:
|
if check:
|
||||||
sys.exit(f"[check] girl art missing: {[p.name for p in missing]}")
|
sys.exit(f"[check] girl art missing: {[p.name for p in missing]}")
|
||||||
for path, content in ((MASTER, PLACEHOLDER), (MASTER_DARK, PLACEHOLDER)):
|
placeholder = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||||
path.write_text(content, encoding="utf-8")
|
<rect x="0" y="0" width="1024" height="1024" rx="{SQUIRCLE_RADIUS}" fill="#ffffff"/>
|
||||||
|
<circle cx="512" cy="512" r="420" fill="#e34c4c"/>
|
||||||
|
</svg>
|
||||||
|
'''
|
||||||
|
for path in (MASTER, MASTER_DARK):
|
||||||
|
path.write_text(placeholder, encoding="utf-8")
|
||||||
print(f"[master] wrote placeholder masters ({[p.name for p in missing]})")
|
print(f"[master] wrote placeholder masters ({[p.name for p in missing]})")
|
||||||
return
|
return
|
||||||
MASTER.write_text(compose_master("black", "squircle-light.svg"), encoding="utf-8")
|
MASTER.write_text(compose_svg("black", "squircle-light.svg"), encoding="utf-8")
|
||||||
MASTER_DARK.write_text(compose_master("white", "squircle-dark.svg"), encoding="utf-8")
|
MASTER_DARK.write_text(compose_svg("white", "squircle-dark.svg"), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
# ─── rendering ──────────────────────────────────────────────────────────────
|
# ─── rendering ──────────────────────────────────────────────────────────────
|
||||||
@@ -261,67 +280,56 @@ def render(master: Path, size: int, *, background: str | None = None) -> Image.I
|
|||||||
return Image.open(io.BytesIO(data)).convert("RGBA")
|
return Image.open(io.BytesIO(data)).convert("RGBA")
|
||||||
|
|
||||||
|
|
||||||
|
def render_svg(svg: str, size: int | tuple[int, int]) -> Image.Image:
|
||||||
|
if isinstance(size, int):
|
||||||
|
w = h = size
|
||||||
|
else:
|
||||||
|
w, h = size
|
||||||
|
data = resvg_py.svg_to_bytes(svg_string=svg, width=w, height=h)
|
||||||
|
return Image.open(io.BytesIO(data)).convert("RGBA")
|
||||||
|
|
||||||
|
|
||||||
def paste_centered(canvas: Image.Image, img: Image.Image) -> None:
|
def paste_centered(canvas: Image.Image, img: Image.Image) -> None:
|
||||||
x = (canvas.width - img.width) // 2
|
x = (canvas.width - img.width) // 2
|
||||||
y = (canvas.height - img.height) // 2
|
y = (canvas.height - img.height) // 2
|
||||||
canvas.alpha_composite(img, (x, y))
|
canvas.alpha_composite(img, (x, y))
|
||||||
|
|
||||||
|
|
||||||
def girl_mark(kind: str, size: int) -> Image.Image:
|
def save_png(img: Image.Image, buf: io.BytesIO) -> None:
|
||||||
"""Composite the girl SVG onto a fixed tile (BrandMark asset), rendered
|
"""Save with alpha preserved. Flat art quantizes losslessly to an 8-bit
|
||||||
straight from the vector art (crisp at any size).
|
palette (tRNS per-index alpha keeps the AA edges), so use that when the
|
||||||
|
palette round-trips pixel-identically; fall back to RGBA otherwise."""
|
||||||
girl_light: black girl on white tile. girl_dark: white girl on #0d1117.
|
if img.mode != "RGBA":
|
||||||
"""
|
img.convert("RGBA").save(buf, "PNG", optimize=True)
|
||||||
if kind == "girl_light":
|
return
|
||||||
girl, bg = "black", "tile-light.svg"
|
quantized = img.quantize(colors=256, method=Image.FASTOCTREE, dither=Image.NONE)
|
||||||
|
if quantized.convert("RGBA").tobytes() == img.tobytes():
|
||||||
|
quantized.save(buf, "PNG", optimize=True)
|
||||||
else:
|
else:
|
||||||
girl, bg = "white", "tile-dark.svg"
|
img.save(buf, "PNG", optimize=True)
|
||||||
|
|
||||||
|
|
||||||
|
def girl_mark(kind: str, size: int) -> Image.Image:
|
||||||
|
"""The girl in the app-icon squircle (BrandMark asset) — the mark IS the
|
||||||
|
icon shape. girl_light: black girl on white squircle. girl_dark: white
|
||||||
|
girl on #0d1117 squircle."""
|
||||||
|
if kind == "girl_light":
|
||||||
|
girl, bg = "black", "squircle-light.svg"
|
||||||
|
else:
|
||||||
|
girl, bg = "white", "squircle-dark.svg"
|
||||||
if not GIRLS[girl].exists():
|
if not GIRLS[girl].exists():
|
||||||
print(f" [girl] {GIRLS[girl].name} missing — placeholder fallback")
|
print(f" [girl] {GIRLS[girl].name} missing — placeholder fallback")
|
||||||
return render(MASTER if kind == "girl_light" else MASTER_DARK, size)
|
return render(MASTER if kind == "girl_light" else MASTER_DARK, size)
|
||||||
|
return render_svg(compose_svg(girl, bg), size)
|
||||||
inner, w, h = background_inner(bg)
|
|
||||||
svg = (
|
|
||||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n'
|
|
||||||
f" {inner.strip()}\n"
|
|
||||||
f" {girl_layer(girl, GIRL_BOXES[bg])}\n"
|
|
||||||
"</svg>\n"
|
|
||||||
)
|
|
||||||
data = resvg_py.svg_to_bytes(svg_string=svg, width=size, height=size)
|
|
||||||
return Image.open(io.BytesIO(data)).convert("RGBA")
|
|
||||||
|
|
||||||
|
|
||||||
def build_logo_image(master: Path, dark: bool = False) -> Image.Image:
|
def build_logo_image(master: Path, dark: bool = False) -> Image.Image:
|
||||||
"""1772x1799 wordmark: frame (light: black / dark: white, ~14-17px) +
|
"""1772x1799 wordmark: the girl alone on transparency (no frame), centered.
|
||||||
interior (light: white / dark: #0d1117) + art at ~1.7x.
|
Light = black girl, dark = white girl — the consuming surface's background
|
||||||
|
(navbar light/dark) shows through."""
|
||||||
Measured from the real logo.png: border L/R 14, T 17, B 16; interior
|
|
||||||
spans (15,18)..(1758,1784) = 1743x1766.
|
|
||||||
"""
|
|
||||||
from PIL import ImageDraw
|
|
||||||
|
|
||||||
W, H = 1772, 1799
|
W, H = 1772, 1799
|
||||||
art_w, art_h = 1743, 1766
|
svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}">\n {girl_layer("white" if dark else "black", (0, 0, W, H))}\n</svg>\n'
|
||||||
border = 14
|
return render_svg(svg, (W, H))
|
||||||
top, bottom = 17, 16
|
|
||||||
if dark:
|
|
||||||
frame, interior = DARK_RGB, (255, 255, 255)
|
|
||||||
else:
|
|
||||||
frame, interior = (0, 0, 0), (255, 255, 255)
|
|
||||||
|
|
||||||
canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
||||||
draw = ImageDraw.Draw(canvas)
|
|
||||||
draw.rounded_rectangle((0, 0, W - 1, H - 1), radius=2, fill=frame)
|
|
||||||
draw.rectangle(
|
|
||||||
(border, top, W - 1 - border, H - 1 - bottom),
|
|
||||||
fill=interior,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Art: render the master square at ~1.7x, tiny vertical stretch to fill.
|
|
||||||
art = render(master, art_w).resize((art_w, art_h), Image.LANCZOS)
|
|
||||||
canvas.alpha_composite(art, (border, top))
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
|
|
||||||
def target_bytes(kind: str, arg: object) -> bytes:
|
def target_bytes(kind: str, arg: object) -> bytes:
|
||||||
@@ -335,23 +343,15 @@ def target_bytes(kind: str, arg: object) -> bytes:
|
|||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
if kind == "png":
|
if kind == "png":
|
||||||
render(MASTER, arg).save(buf, "PNG")
|
save_png(render(MASTER, arg), buf)
|
||||||
elif kind == "png_dark":
|
elif kind == "png_dark":
|
||||||
render(MASTER_DARK, arg).save(buf, "PNG")
|
save_png(render(MASTER_DARK, arg), buf)
|
||||||
elif kind == "png_white":
|
elif kind == "png_white":
|
||||||
render(MASTER, arg, background="#ffffff").save(buf, "PNG")
|
render(MASTER, arg, background="#ffffff").convert("RGB").save(buf, "PNG", optimize=True)
|
||||||
elif kind == "png_dark_white":
|
elif kind == "png_dark_white":
|
||||||
render(MASTER_DARK, arg, background=DARK_HEX).save(buf, "PNG")
|
render(MASTER_DARK, arg, background=DARK_HEX).convert("RGB").save(buf, "PNG", optimize=True)
|
||||||
elif kind == "girl_light" or kind == "girl_dark":
|
elif kind in ("girl_light", "girl_dark"):
|
||||||
# Flat 2-tone vector-style art: <256 distinct colors, so an 8-bit
|
save_png(girl_mark(kind, arg), buf)
|
||||||
# palette PNG holds it pixel-identically. Saved with compress_level=0
|
|
||||||
# (stored deflate blocks): the icons-freshness lane byte-compares
|
|
||||||
# against an ubuntu regen, and compressed PNG bytes drift per host
|
|
||||||
# zlib — stored blocks are spec-locked, identical on every platform.
|
|
||||||
# ~66KB vs ~15KB compressed is a non-issue for a bundled asset.
|
|
||||||
girl_mark(kind, arg).convert("RGB").convert("P", palette=Image.ADAPTIVE, colors=256).save(
|
|
||||||
buf, "PNG", compress_level=0
|
|
||||||
)
|
|
||||||
elif kind == "ico":
|
elif kind == "ico":
|
||||||
img = render(MASTER, max(arg))
|
img = render(MASTER, max(arg))
|
||||||
img.save(buf, format="ICO", sizes=[(s, s) for s in arg])
|
img.save(buf, format="ICO", sizes=[(s, s) for s in arg])
|
||||||
@@ -424,33 +424,84 @@ def cmd_write() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_check() -> int:
|
def cmd_check() -> int:
|
||||||
|
"""Structural verification: regenerate every target in memory and assert
|
||||||
|
the invariants that actually matter (there are no committed bytes to
|
||||||
|
byte-compare — outputs are generated on demand)."""
|
||||||
ensure_masters(check=True)
|
ensure_masters(check=True)
|
||||||
drifted: list[str] = []
|
problems: list[str] = []
|
||||||
|
|
||||||
|
# every target must generate without error
|
||||||
for rel, kind, arg in TARGETS:
|
for rel, kind, arg in TARGETS:
|
||||||
|
try:
|
||||||
|
target_bytes(kind, arg)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
problems.append(f"{rel}: REGENERATE FAILED ({exc})")
|
||||||
|
|
||||||
|
# PNG targets must have the expected format + size
|
||||||
|
for rel, (fmt, size) in CHECK_SIZES.items():
|
||||||
path = ROOT / rel
|
path = ROOT / rel
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
drifted.append(f"{rel}: MISSING (expected generated file)")
|
problems.append(f"{rel}: MISSING (expected generated file)")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
expected = target_bytes(kind, arg)
|
im = Image.open(path)
|
||||||
|
if im.format != fmt or im.size != size:
|
||||||
|
problems.append(f"{rel}: got {im.format} {im.size}, expected {fmt} {size}")
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
drifted.append(f"{rel}: REGENERATE FAILED ({exc})")
|
problems.append(f"{rel}: UNREADABLE ({exc})")
|
||||||
continue
|
|
||||||
actual = path.read_bytes()
|
|
||||||
if actual != expected:
|
|
||||||
drifted.append(f"{rel}: drift ({len(actual)} bytes on disk vs {len(expected)} generated)")
|
|
||||||
|
|
||||||
if not drifted:
|
# squircles must keep transparent corners (alpha extrema include 0)
|
||||||
print(f"[ok] all {len(TARGETS)} generated icons match the girl art + backgrounds")
|
for rel in (
|
||||||
|
"apps/desktop/assets/icon.png",
|
||||||
|
"apps/desktop/assets/icon-dark.png",
|
||||||
|
"apps/desktop/public/nous-girl.png",
|
||||||
|
"apps/desktop/public/nous-girl-dark.png",
|
||||||
|
"apps/desktop/public/apple-touch-icon.png",
|
||||||
|
):
|
||||||
|
path = ROOT / rel
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
alpha = Image.open(path).convert("RGBA").getchannel("A")
|
||||||
|
lo, hi = alpha.getextrema()
|
||||||
|
if lo != 0 or hi != 255:
|
||||||
|
problems.append(f"{rel}: alpha extrema {alpha.getextrema()}, expected (0, 255) transparent corners")
|
||||||
|
|
||||||
|
# containers must have the right frame sets (parse ICO headers directly —
|
||||||
|
# PIL's ICO n_frames is unreliable across versions)
|
||||||
|
def ico_sizes(path: Path) -> list[int]:
|
||||||
|
data = path.read_bytes()
|
||||||
|
count = int.from_bytes(data[4:6], "little")
|
||||||
|
sizes = []
|
||||||
|
for i in range(count):
|
||||||
|
entry = data[6 + i * 16 : 6 + (i + 1) * 16]
|
||||||
|
w = entry[0] or 256
|
||||||
|
h = entry[1] or 256
|
||||||
|
sizes.append(w)
|
||||||
|
return sorted(set(sizes))
|
||||||
|
|
||||||
|
for rel, sizes in (
|
||||||
|
("apps/desktop/assets/icon.ico", [16, 24, 32, 48, 64, 128, 256]),
|
||||||
|
("apps/desktop/assets/icon-dark.ico", [16, 24, 32, 48, 64, 128, 256]),
|
||||||
|
("apps/bootstrap-installer/src-tauri/icons/icon.ico", [16, 32, 64, 128, 256]),
|
||||||
|
):
|
||||||
|
path = ROOT / rel
|
||||||
|
if not path.exists():
|
||||||
|
problems.append(f"{rel}: MISSING")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
got = ico_sizes(path)
|
||||||
|
if got != sizes:
|
||||||
|
problems.append(f"{rel}: ICO frames {got}, expected {sizes}")
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
problems.append(f"{rel}: UNREADABLE ({exc})")
|
||||||
|
|
||||||
|
if not problems:
|
||||||
|
print(f"[ok] all {len(TARGETS)} targets generate and pass structural checks")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
print(f"[check] {len(drifted)} file(s) out of sync:")
|
print(f"[check] {len(problems)} problem(s):")
|
||||||
for line in drifted:
|
for line in problems:
|
||||||
print(f" - {line}")
|
print(f" - {line}")
|
||||||
print(
|
|
||||||
"\nFix: run `.venv/Scripts/python.exe scripts/generate_icons.py` "
|
|
||||||
"and commit the regenerated files."
|
|
||||||
)
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"prebuild": "node ../scripts/generate-icons.mjs",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.4 KiB |
@@ -29,9 +29,11 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
const websiteDir = resolve(scriptDir, "..");
|
const websiteDir = resolve(scriptDir, "..");
|
||||||
|
const repoRoot = resolve(websiteDir, "..");
|
||||||
const extractScript = join(scriptDir, "extract-skills.py");
|
const extractScript = join(scriptDir, "extract-skills.py");
|
||||||
const llmsScript = join(scriptDir, "generate-llms-txt.py");
|
const llmsScript = join(scriptDir, "generate-llms-txt.py");
|
||||||
const cronBlueprintsScript = join(scriptDir, "extract-automation-blueprints.py");
|
const cronBlueprintsScript = join(scriptDir, "extract-automation-blueprints.py");
|
||||||
|
const iconGenScript = join(repoRoot, "scripts", "generate-icons.mjs");
|
||||||
const outputFile = join(websiteDir, "static", "api", "skills.json");
|
const outputFile = join(websiteDir, "static", "api", "skills.json");
|
||||||
const unifiedIndexFile = join(websiteDir, "static", "api", "skills-index.json");
|
const unifiedIndexFile = join(websiteDir, "static", "api", "skills-index.json");
|
||||||
const UNIFIED_INDEX_URL =
|
const UNIFIED_INDEX_URL =
|
||||||
@@ -119,7 +121,19 @@ async function ensureUnifiedIndex() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0) Pull unified index if we don't have a fresh one.
|
// 0) Icon assets — the navbar logo, favicons, and apple-touch-icon are
|
||||||
|
// generated (not committed). This must fail loudly: a docs build without the
|
||||||
|
// logo ships a broken navbar.
|
||||||
|
console.log("[prebuild] generating icon assets…");
|
||||||
|
{
|
||||||
|
const r = spawnSync("node", [iconGenScript], { stdio: "inherit", cwd: repoRoot });
|
||||||
|
if (r.status !== 0) {
|
||||||
|
console.error("[prebuild] icon generation failed — install the dev extra with `uv sync --extra dev`");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0b) Pull unified index if we don't have a fresh one.
|
||||||
await ensureUnifiedIndex();
|
await ensureUnifiedIndex();
|
||||||
|
|
||||||
// 1) skills.json — required for the Skills Hub page.
|
// 1) skills.json — required for the Skills Hub page.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 491 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |