Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Desktop installers

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]

jobs:
desktop:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm
- name: Install Linux packaging and display dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y fakeroot dpkg xvfb
- run: pnpm install --frozen-lockfile
- name: Build libraries and Desktop targets
run: |
pnpm run build
pnpm --filter @cossackframework/example-desktop-counter build
pnpm --filter @cossackframework/example-desktop-counter build:desktop
- name: Create unpacked application and native installer
run: |
pnpm --filter @cossackframework/example-desktop-counter desktop:package
pnpm --filter @cossackframework/example-desktop-counter desktop:make
- name: Smoke packaged app (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a node scripts/smoke-desktop.mjs
- name: Smoke packaged app
if: runner.os != 'Linux'
run: node scripts/smoke-desktop.mjs
- uses: actions/upload-artifact@v4
with:
name: cossack-desktop-${{ runner.os }}
path: |
examples/desktop-counter/out/make/**
examples/desktop-counter/out/*-*/**
13 changes: 13 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,25 @@ jobs:
node-version: '22'
cache: 'pnpm'

- name: Setup Deno 2.9
uses: denoland/setup-deno@v2
with:
deno-version: v2.9.x

- name: Install dependencies
run: pnpm install

- name: Build workspace libraries
run: pnpm run build

- name: Check Deno web adapter
run: |
deno check packages/deno-adapter/src/index.ts
deno test -A packages/deno-adapter/tests/deno-smoke.test.ts

- name: Build Desktop counter web target
run: pnpm --filter @cossackframework/example-desktop-counter build

- name: Test generated app with local package tarballs
run: pnpm run test:scaffold:local

Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
run: |
pnpm --filter @cossackframework/core test -- --run
pnpm --filter @cossackframework/renderer test -- --run
pnpm --filter @cossackframework/deno-adapter test
pnpm --filter @cossackframework/desktop test
pnpm --filter @cossackframework/ui test -- --run
pnpm --filter @cossackframework/database test
pnpm --filter @cossackframework/database audit:bundle
Expand Down Expand Up @@ -79,6 +81,16 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cossackframework/deno-adapter
run: pnpm publish --filter @cossackframework/deno-adapter --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cossackframework/desktop
run: pnpm publish --filter @cossackframework/desktop --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish @cossackframework/database
run: pnpm publish --filter @cossackframework/database --access public --no-git-checks
env:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ packages/*/pnpm-lock.yaml
# Build artifacts
**/dist
**/build
**/out

# TypeScript incremental build cache
**/*.tsbuildinfo
Expand Down Expand Up @@ -43,8 +44,9 @@ test-results
.claude
reasonix.toml
.reasonix
.zcode
# Planning documents (local scratch)
plan-*.md
reviews.md
request-update.md
todo.md
todo.md
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The project is a `pnpm` workspace. All packages are located in the `packages` di

## 4. Development Workflow

1. **Build Dependencies**: Build `core`, `renderer`, `node-adapter`, `database`, and `auth` first.
1. **Build Dependencies**: Build packages first `pnpm build`.
2. **Run Application**: Use `pnpm run dev`.

## Security: Code Stripping
Expand Down
7 changes: 4 additions & 3 deletions docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ To use the Node.js adapter, your application entry point must initialize a Node.

```typescript
import { serve } from '@hono/node-server';
import { CossackNodeAdapter } from '@cossackframework/node-adapter';
import { CossackNodeAdapter, nodeRuntimeAdapter } from '@cossackframework/node-adapter';
import { createApp } from './router';
// Import your pages/components registry logic here

const app = createApp();
const componentRegistry = ...; // Map of ComponentName -> ComponentClass
const app = createApp({ runtimeAdapter: nodeRuntimeAdapter });
// Keys must match Cossack route paths, e.g. '/account/:id'.
const componentRegistry = ...; // Map<RoutePath, ComponentClass>

const server = serve({
fetch: app.fetch,
Expand Down
12 changes: 10 additions & 2 deletions docs/cossack-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,25 @@ These work across every command:

### `cossack create <name>`

Scaffold a new Cossack project in a directory named `<name>`. Prompts for the runtime adapter (Cloudflare Workers or Node.js) unless `--adapter` is provided.
Scaffold a new Cossack project in a directory named `<name>`. Prompts for the runtime adapter (Cloudflare Workers, Node.js, or Deno) unless `--adapter` is provided.

```bash
cossack create my-app
cossack create my-app --adapter node --preset full-stack
cossack create my-app --adapter cloudflare --preset minimal --features desktop
```

`cossack create` writes a `.cossack/scaffold.json` manifest used by
`cossack add`, `cossack remove`, `cossack adapter`, and `cossack upgrade`.

### `cossack adapter <node|cloudflare>`
### `cossack add desktop`

Add the independent Electron target without changing the web adapter. The
command writes `src/desktop/index.ts`, `forge.config.ts`, platform icons,
Electron/Forge dependencies, and `desktop:dev`, `desktop:package`, and
`desktop:make` scripts. Electron installers are built on their native host.

### `cossack adapter <node|cloudflare|deno>`

Switch an existing schema-v3 scaffolded project to one active runtime. The
command re-renders the complete recorded recipe, previews the change set,
Expand Down
11 changes: 6 additions & 5 deletions docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ src/

The model barrel imports `reflect-metadata` once and exports deterministic
entity registration. Node recipes create one caller-owned ORM singleton.
Workers recipes create an ORM per request from D1, libSQL/Turso, or Hyperdrive
bindings; `ormMiddleware` closes factory-created instances after downstream
work completes.
Deno and Workers recipes create an ORM per request from Turso, D1, or
Hyperdrive bindings; `ormMiddleware` closes factory-created instances after
downstream work completes.

## Models

Expand Down Expand Up @@ -78,8 +78,9 @@ context through application services.

| Runtime | Providers |
|---|---|
| Node | SQLite (`node:sqlite`), libSQL/Turso, PostgreSQL, MySQL |
| Workers | D1, Workers-safe libSQL/Turso, Hyperdrive PostgreSQL, Hyperdrive MySQL |
| Node | SQLite (`node:sqlite`), Turso, PostgreSQL, MySQL |
| Deno | Turso Database (embedded SQLite), remote Turso, PostgreSQL, MySQL |
| Workers | D1, remote Turso, Hyperdrive PostgreSQL, Hyperdrive MySQL |

Hyperdrive recipes enable `nodejs_compat` and install only the selected
PostgreSQL or MySQL driver. Other Workers recipes use `nodejs_als`.
Expand Down
30 changes: 30 additions & 0 deletions docs/desktop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: Desktop
description: Package a shared Cossack application with Electron.
---

# Desktop applications

Cossack Desktop is an Electron side target for any Cloudflare, Node, or Deno
web project. Run `cossack add desktop`; the web adapter remains unchanged while
the generated Node-based Electron main process uses the same page tree.

Desktop requests use the private `cossack://app` origin for assets, SSR, and
RPC. `@Page({ transport: 'http' })` is currently required. Native APIs stay in
the main process and are available to server methods through
`this.env.COSSACK_DESKTOP`; no preload or generic renderer IPC API is created.

Use `pnpm desktop:dev`, `pnpm desktop:package`, and `pnpm desktop:make`.
Electron Forge creates the host installer only: DEB on Linux, MSI on Windows,
and DMG on macOS. See the package [guide](../packages/desktop/docs/guide.md)
and [migration guide](./migrations/deno-desktop-to-electron.md).

On GNOME Wayland, generated DEB launchers use XWayland for reliable tray
registration. Unpacked Forge output bypasses the launcher and should be tested
with `--no-sandbox --ozone-platform=x11`; installed DEBs do not need the
testing-only sandbox fallback.

Use `configureDesktopClose()` to choose `quit`, `hide-to-tray`, or
`confirm-quit`. Linux tray activation depends on the desktop host, so
`confirm-quit` is the conservative default; use `hide-to-tray` only after a
tray has been constructed and tested on the target OS.
44 changes: 44 additions & 0 deletions docs/migrations/deno-desktop-to-electron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Migrate Deno Desktop to Electron

This alpha-stage release intentionally removes the Deno Desktop compatibility
layer. Keep `@cossackframework/deno-adapter` only for Deno web or Deno Deploy.

1. Replace `@cossackframework/deno-adapter/desktop` and `/desktop/client` with
`@cossackframework/desktop` main-process imports. Delete direct binding and
renderer `invoke()` code.
2. Delete Desktop sections/tasks from `deno.json`, `--desktop-backend`, and
Desktop-only `@types/deno`. Generate `src/desktop/index.ts`,
`forge.config.ts`, Electron/Forge dependencies, and the platform assets with
`cossack add desktop`.
3. Replace the local Deno adapter with `createDesktopApp()`. Pass the shared
Cossack app's `fetch`, an absolute `dist/client` path, environment values,
product name, and stable identifier.
4. Move native behavior to the Electron bootstrap or an ordinary `@Server()`
method using `this.env.COSSACK_DESKTOP`. Keep context isolation and sandbox
enabled; do not enable Node integration, preload, or broad IPC.
5. Use async Electron dialogs, `Notification.isSupported()`, native menu/tray
events, macOS Dock APIs, Linux badges, and Windows overlay/taskbar APIs.
Create trays with `createDesktopTray()`, keep them strongly referenced, and
let the last window close if tray construction fails. The helper returns a
native Electron `Tray` and repairs Ubuntu StatusNotifier registration.
Replace manual close listeners with `configureDesktopClose()`: prefer
`confirm-quit` on unverified Linux hosts, and select `hide-to-tray` on
Windows/macOS only after tray creation succeeds.
6. Commit PNG sizes through 512px, multi-frame ICO, ICNS, and OS-specific tray
images. Encode tray PNGs as 8-bit RGBA for Linux StatusNotifier hosts. Build
`.deb`, `.msi`, and `.dmg` on their native runners.

Cloudflare-only bindings such as D1 are not automatically available in the
Node-based Electron process; supply a compatible binding or isolate that code
to the web target. Linux tray support requires AppIndicator or
KStatusNotifierItem. Reliable macOS notifications generally require a signed,
packaged application. Installing a DEB from a parent directory unreadable by
`_apt` may print a harmless unsandboxed warning; copy it to `/tmp` first.
Generated Linux desktop entries select XWayland for reliable GNOME tray
registration. When testing Forge's unpacked output directly, pass
`--no-sandbox --ozone-platform=x11`; an installed DEB configures the sandbox
helper and its launcher supplies the display switch.

Review Electron's [security checklist](https://www.electronjs.org/docs/latest/tutorial/security),
[custom protocol API](https://www.electronjs.org/docs/latest/api/protocol), and
[Forge overview](https://www.electronjs.org/docs/latest/tutorial/forge-overview).
2 changes: 1 addition & 1 deletion docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Studio still binds parameters and uses safe row locators for edits.

## Transactions

Node SQLite, libSQL, PostgreSQL, and MySQL support interactive transactions:
Node SQLite, PostgreSQL, and MySQL support interactive transactions:

```ts
await sql.transaction(async () => {
Expand Down
1 change: 1 addition & 0 deletions docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Cossack Skills are instruction packs that teach AI coding assistants (Claude Cod
| `/setup-auth` | Task | Set up authentication with `@cossackframework/auth` (multi-file workflow) |
| `/setup-websocket` | Task | Set up real-time features — pick SSE or Durable Object transport |
| `/setup-ui` | Task | Set up the UI package (`@cossackframework/ui`) — theme, CSS imports, component usage, focus helpers, Toaster |
| `/create-desktop-app` | Task | Add an Electron Desktop side target, secure native shell capabilities, icons, lifecycle, and Forge packaging without changing the web runtime |
| `cossack-best-practices` | Background | Guardrails for `@Server()` RPC, ORM entities, validation, caching, UI, tasks, stores, auth, and error handling. |

The `cossack-best-practices` skill is not user-invocable. It auto-loads when you work on Cossack-specific files (`src/pages/**`, `src/components/**`, `src/services/**`, `src/middlewares/**`, `src/App.ts`, `src/root.ts`) and steers the AI toward framework built-ins.
Expand Down
52 changes: 52 additions & 0 deletions examples/desktop-counter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Cossack Desktop counter

One Cossack page runs on the Node adapter for the web target and inside
Electron for Desktop. Both use ordinary Cossack `@Server()` actions. Electron
serves SSR, RPC, and renderer assets over `cossack://app`; no preload script,
renderer IPC bridge, or client-side Electron import is used.

The Desktop count is atomically persisted under Electron's `userData` path.
The example includes native application, renderer context, Dock, tray, and
Windows taskbar menus; async reset/close confirmation; supported native
notifications; numeric Linux/macOS badges; and a generated Windows overlay.
Its close policy is intentionally platform-specific: Linux confirms before
quitting and creates no tray, while Windows/macOS hide to a tray only after
tray construction succeeds.

```sh
pnpm dev # Node web target
pnpm desktop:dev # watched renderer + Electron main process
pnpm desktop:package # unpacked host package
pnpm desktop:make # .deb, .msi, or .dmg on the native host
```

Use Node 22 or 24 LTS for packaging. The current Electron Packager ZIP stack is
pathologically slow under Node 26; this example's `engines` field warns about
that unsupported combination.

On Linux, the development runner may report that it is using Electron's
testing-only `--no-sandbox` fallback because pnpm's `chrome-sandbox` file is
user-owned. This affects `desktop:dev` only; the generated DEB installs the
helper with secure root-owned/setuid permissions.
The runner also selects XWayland when available to avoid Chromium's current
Wayland/Vulkan warning. Use `COSSACK_DESKTOP_OZONE_PLATFORM=wayland` to force
native Wayland or `COSSACK_DESKTOP_DEBUG=1` to show Chromium GPU diagnostics.
The installed DEB launcher selects XWayland too. When testing the unpacked
Forge output directly, run:

```sh
./cossack-counter --no-sandbox --ozone-platform=x11
```

The unpacked binary bypasses the generated desktop entry, and its uninstalled
sandbox helper does not yet have the permissions assigned by the DEB.

Electron Forge builds only the installer for the current operating system.
macOS signing/notarization activates when `APPLE_IDENTITY`, `APPLE_ID`,
`APPLE_PASSWORD`, and `APPLE_TEAM_ID` are supplied. Linux tray support remains
available through `createDesktopTray()`, but activation differs among desktop
hosts, so this example deliberately demonstrates `confirm-quit` instead.

When installing a DEB, `_apt` can emit a harmless unsandboxed warning if the
package is inside a parent directory it cannot traverse. Copy the DEB to
`/tmp` before `apt install` to avoid that warning.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/desktop-counter/desktop-assets/icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file added examples/desktop-counter/desktop-assets/icon.ico
Binary file not shown.
10 changes: 10 additions & 0 deletions examples/desktop-counter/desktop-assets/linux.desktop.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=<%= productName %>
Comment=<%= description %>
GenericName=<%= genericName %>
Exec=<%= name %> --ozone-platform=x11 %U
Icon=<%= name %>
Type=Application
StartupNotify=true
StartupWMClass=dev.cossack.counter
Categories=<%= categories.join(';') %>;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading