Skip to content

Commit a52f00c

Browse files
committed
Merge upstream/main
2 parents aacb412 + 3875e45 commit a52f00c

168 files changed

Lines changed: 3583 additions & 2706 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
3838
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
3939
- Dependencies go through the pnpm catalogs in `pnpm-workspace.yaml` (`cli`, `inlined`, `testing`, `types`) — add to a catalog and reference as `catalog:<name>`, don't pin versions in `package.json`.
4040

41+
### Design system
42+
43+
All five built-in plugins — and every example under `examples/` — share one design system, `@internal/design`, so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is vanilla DOM, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a private, source-only package — never built or published; consumers import its TypeScript/CSS directly (resolved through `alias.ts` for bundlers and the package `exports` for config loaders), so editing it needs no rebuild.
44+
45+
- **Respect the skills.** This design system is built to the `antfu` and `antfu-design` skills (UnoCSS-first, class-based semantic tokens, dual light/dark, anti-slop) — load and follow them when building or changing any UI here. The surfaces deliberately echo the upstream devtools they descend from; reference their UI/UX when in doubt: [`antfu/node-modules-inspector`](https://github.com/antfu/node-modules-inspector), [`antfu/vite-plugin-inspect`](https://github.com/antfu/vite-plugin-inspect), [`eslint/config-inspector`](https://github.com/eslint/config-inspector), and [`vitejs/devtools` → `packages/rolldown`](https://github.com/vitejs/devtools/tree/main/packages/rolldown).
46+
- **One preset to extend.** Each consumer's `uno.config.ts` is just `presets: [presetDevframe()]` (imported from `@internal/design/preset`). The preset bundles `presetWind4` + `presetIcons` (Phosphor) + the directive/variant-group transformers, the semantic token theme, and the shared `df-*` component shortcuts (which it safelists). Don't re-declare presets, palettes, or shortcuts per consumer.
47+
- **One token source.** Import `@internal/design/theme.css` once on the page (after the generated UnoCSS stylesheet so its base layer wins). Token *values* (the `--df-*` custom properties, light + dark via the `.dark` class) live only there — never hardcode a palette. The brand primary is devframe's sage green; flip the OS preference onto `<html class="dark">` from the SPA entry.
48+
- **Shared component vocabulary.** Build UI from the `df-*` classes (`df-btn`, `df-badge`, `df-tab`, `df-navtab`, `df-nav`, `df-nav-brand`, `df-toolbar`, `df-card`, `df-input`, `df-dot`, `df-tag-*`, …) and the semantic token utilities (`bg-primary`, `text-muted-foreground`, `bg-card`, `border-border`, …). Markup differs per framework; the classes resolve identically, which is what keeps the surfaces consistent.
49+
- **Component builders.** Prefer the framework-neutral recipes from `@internal/design/components` (`button`, `iconButton`, `badge`, `tab`, `tabsList`, `navTab`, `nav`, `navBrand`, `toolbar`, `card`, `panel`, `input`, `link`, `dot`, `spinner`, `tag`) over hand-written class strings — they return the canonical `df-*` classes and read the same in React (`className=`), Svelte/Vue (`class=`), vanilla DOM and Solid. Because these classes are assembled at runtime, the preset safelists the `df-*` vocabulary so UnoCSS always emits it; add new component classes to `DF_SAFELIST` when you extend the set.
50+
- **One nav, three buttons, one tab selector — strictly.** Every surface opens with the same top bar: `nav()` (a single row, one fixed height) led by `navBrand()` (a primary-tinted `i-ph-*` icon + the product name). Buttons come in exactly three forms and nothing else: a **text button** (`button({ variant, size })`), an **icon button** (`iconButton()` — bordered), and a **borderless icon button** (`iconButton({ variant: 'ghost' })`). Multi-view tools (inspect, git) switch views with the one shared segmented selector — `tabsList()` wrapping `tab()` buttons that carry `data-state="active"` and a leading `i-ph-*` icon per tab. Don't invent bespoke nav bars, button shapes, or tab styles.
51+
- **Icons** come from the shared Phosphor set (`i-ph-*`, duotone preferred) via `presetIcons` — use them everywhere instead of per-consumer icon libraries or bespoke SVG.
52+
- **A surface keeping its own component CSS** (inspect, a11y) re-bases its color/radius tokens onto the `--df-*` variables rather than hardcoding a palette, so it tracks the shared theme.
53+
- **Plain `.ts`/vanilla views** must opt `.ts` into UnoCSS extraction (`content.pipeline.include` for Vite, or `content.filesystem` globs for the `@unocss/postcss` setup Next uses), since UnoCSS only scans framework files by default.
54+
4155
### Devframe design principles
4256

4357
These reinforce devframe's positioning as "the container for one devtool integration, portable to multiple viewers". When in doubt, err on the side of "devframe provides primitives, the hub provides UX".

‎alias.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export const alias = {
4545
'@devframes/hub': r('hub/src/index.ts'),
4646
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
4747
'@devframes/nuxt': r('nuxt/src/index.ts'),
48+
'@internal/design/preset': r('design/src/preset.ts'),
49+
'@internal/design/components': r('design/src/components.ts'),
50+
'@internal/design/tokens': r('design/src/tokens.ts'),
51+
'@internal/design/theme.css': r('design/src/theme.css'),
52+
'@internal/design': r('design/src/index.ts'),
4853
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
4954
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
5055
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),
@@ -60,6 +65,7 @@ export const alias = {
6065
'@devframes/plugin-terminals/cli': p('terminals/src/cli.ts'),
6166
'@devframes/plugin-terminals/vite': p('terminals/src/vite.ts'),
6267
'@devframes/plugin-terminals': p('terminals/src/index.ts'),
68+
'@devframes/plugin-git': p('git/src/index.ts'),
6369
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
6470
'devframe/client': r('devframe/src/client/index.ts'),
6571
'devframe': r('devframe/src'),
@@ -68,13 +74,21 @@ export const alias = {
6874
'@devframes/plugin-inspect/cli': p('inspect/src/cli.ts'),
6975
'@devframes/plugin-inspect/vite': p('inspect/src/vite.ts'),
7076
'@devframes/plugin-inspect': p('inspect/src/index.ts'),
77+
'@devframes/plugin-a11y/client': p('a11y/src/client/index.ts'),
78+
'@devframes/plugin-a11y/node': p('a11y/src/node/index.ts'),
79+
'@devframes/plugin-a11y/cli': p('a11y/src/cli.ts'),
80+
'@devframes/plugin-a11y/vite': p('a11y/src/vite.ts'),
81+
'@devframes/plugin-a11y': p('a11y/src/index.ts'),
7182
}
7283

73-
// update tsconfig.base.json
84+
// update tsconfig.base.json — CSS aliases exist for Vite resolution only;
85+
// TypeScript resolves `*.css` side-effect imports through ambient shims.
7486
const raw = fs.readFileSync(join(root, 'tsconfig.base.json'), 'utf-8').trim()
7587
const tsconfig = JSON.parse(raw)
7688
tsconfig.compilerOptions.paths = Object.fromEntries(
77-
Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
89+
Object.entries(alias)
90+
.filter(([key]) => !key.endsWith('.css'))
91+
.map(([key, value]) => [key, [`./${relative(root, value)}`]]),
7892
)
7993
const newRaw = JSON.stringify(tsconfig, null, 2)
8094
if (newRaw !== raw)

‎docs/.vitepress/config.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ function helpersItems(prefix: string): DefaultTheme.NavItemWithLink[] {
5757
]
5858
}
5959

60+
function pluginsItems(prefix: string): DefaultTheme.NavItemWithLink[] {
61+
return [
62+
{ text: 'Overview', link: `${prefix}/plugins/` },
63+
{ text: 'Devframe Inspector', link: `${prefix}/plugins/inspect` },
64+
{ text: 'Accessibility Inspector', link: `${prefix}/plugins/a11y` },
65+
{ text: 'Git', link: `${prefix}/plugins/git` },
66+
{ text: 'Terminals', link: `${prefix}/plugins/terminals` },
67+
{ text: 'Code Server', link: `${prefix}/plugins/code-server` },
68+
]
69+
}
70+
71+
function examplesItems(prefix: string): DefaultTheme.NavItemWithLink[] {
72+
return [
73+
{ text: 'Overview', link: `${prefix}/examples/` },
74+
{ text: 'files-inspector', link: `${prefix}/examples/files-inspector` },
75+
{ text: 'streaming-chat', link: `${prefix}/examples/streaming-chat` },
76+
{ text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` },
77+
{ text: 'minimal-vite-devframe-hub', link: `${prefix}/examples/minimal-vite-devframe-hub` },
78+
{ text: 'minimal-next-devframe-hub', link: `${prefix}/examples/minimal-next-devframe-hub` },
79+
]
80+
}
81+
6082
export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
6183
return [
6284
{
@@ -71,6 +93,14 @@ export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
7193
text: 'Helpers',
7294
items: helpersItems(prefix),
7395
},
96+
{
97+
text: 'Plugins',
98+
items: pluginsItems(prefix),
99+
},
100+
{
101+
text: 'Examples',
102+
items: examplesItems(prefix),
103+
},
74104
{
75105
text: 'Error Reference',
76106
link: `${prefix}/errors/`,
@@ -88,6 +118,8 @@ export function devframeNav(prefix = ''): DefaultTheme.NavItem[] {
88118
{ text: 'Guide', items: guideItems(prefix) },
89119
{ text: 'Adapters', items: adaptersItems(prefix) },
90120
{ text: 'Helpers', items: helpersItems(prefix) },
121+
{ text: 'Plugins', items: pluginsItems(prefix) },
122+
{ text: 'Examples', items: examplesItems(prefix) },
91123
{ text: 'Errors', link: `${prefix}/errors/` },
92124
{
93125
text: `v${pkg.version}`,

‎docs/examples/files-inspector.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# files-inspector
6+
7+
Lists the files in the current working directory and renders them through a **Preact** SPA. A node-modules-inspector-style demo that exercises every devframe surface end to end.
8+
9+
Package: `files-inspector-example` · framework: **Preact + Vite**
10+
11+
## What it shows
12+
13+
- **CLI dev server** — `node bin.mjs` boots an HTTP + WebSocket server backing live RPC.
14+
- **Static build** — `node bin.mjs build` produces a self-contained directory (SPA + baked RPC dump) deployable to any static host.
15+
- **Runtime base discovery** — the client is built with `vite.base: './'` and reads `document.baseURI` at runtime, so the same `dist/client` works under any base path without rebuilding.
16+
- **Two RPC types** — `:list-files` is a `query` baked into the dump; `:get-cwd` is a `static` RPC.
17+
18+
## Run it
19+
20+
```sh
21+
pnpm -C examples/files-inspector run build # build the Preact client
22+
pnpm -C examples/files-inspector run dev # CLI dev server (live RPC)
23+
pnpm -C examples/files-inspector run cli:build # static deploy → dist/static
24+
```
25+
26+
The dev server prints its URL. Serve `dist/static` from any static host — relative asset paths make it portable.
27+
28+
## Source
29+
30+
[`examples/files-inspector`](https://github.com/devframes/devframe/tree/main/examples/files-inspector)

‎docs/examples/index.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Examples
6+
7+
End-to-end examples that exercise the full adapter surface, each a runnable app in the repository. Like the [built-in plugins](/plugins/), they are written across different UI frameworks on purpose: the node-side definition stays the same while the browser bundle varies, so the set demonstrates that devframe is framework-agnostic at both the plugin and the host level.
8+
9+
| Example | UI framework | What it shows |
10+
|---------|--------------|---------------|
11+
| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build / spa surfaces. |
12+
| [streaming-chat](./streaming-chat) | Preact | Streams synthetic chat tokens server → client, with history kept in shared state. |
13+
| [next-runtime-snapshot](./next-runtime-snapshot) | React (Next.js) | A Next.js App Router SPA over RPC, surfacing the host Node runtime. |
14+
| [minimal-vite-devframe-hub](./minimal-vite-devframe-hub) | Vanilla TypeScript (Vite) | A ~120-line Vite host wiring `@devframes/hub` end to end. |
15+
| [minimal-next-devframe-hub](./minimal-next-devframe-hub) | React (Next.js) | The same hub protocol, hosted from a Next.js route handler. |
16+
17+
## Two kinds of example
18+
19+
The first three are **single-tool devframes** — one `DevframeDefinition` deployed through the [adapters](/adapters/), showing how RPC, streaming, and a chosen SPA framework fit together.
20+
21+
The last two are **hub hosts** built on [`@devframes/hub`](/guide/hub). They are protocol witnesses: each is a small host that exercises every hub subsystem (docks, commands, messages, terminals) so you can read one file and see the whole shape. One is a Vite plugin; the other a Next.js route handler — same hub, different host runtime.
22+
23+
## Run any example
24+
25+
Each example ships its own scripts; from the repository root:
26+
27+
```sh
28+
pnpm install
29+
pnpm --filter <example-name> dev
30+
```
31+
32+
See the individual pages for the package name, the build / static-deploy commands, and what to look for in the running app.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# minimal-next-devframe-hub
6+
7+
The same hub protocol as the [Vite host](./minimal-vite-devframe-hub), hosted from a **Next.js** App Router app. It wires [`@devframes/hub`](/guide/hub) by lazily starting a side-car RPC / WebSocket server from a Node route handler — proof that the hub is host-runtime-agnostic.
8+
9+
Package: `minimal-next-devframe-hub` · framework: **React (Next.js)**
10+
11+
## What it proves
12+
13+
- `createHubContext()` boots a hub without any Vite-specific code path.
14+
- A `DevframeHost` implementation plugs the Next host specifics into the hub uniformly.
15+
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
16+
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
17+
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json`, which starts the singleton host on demand.
18+
19+
## Run it
20+
21+
```sh
22+
pnpm install
23+
pnpm --filter minimal-next-devframe-hub dev
24+
```
25+
26+
Open the printed URL to see the docks, commands, messages, and terminals lists, plus a button that dispatches a sample command through `hub:commands:execute`.
27+
28+
## Source
29+
30+
[`examples/minimal-next-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/minimal-next-devframe-hub)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# minimal-vite-devframe-hub
6+
7+
A protocol-witness host: roughly 120 lines of Vite plugin code that wire [`@devframes/hub`](/guide/hub) into a Vite dev server. The browser UI is plain **vanilla TypeScript**, so nothing distracts from the hub protocol itself. Every framework's hub host follows the same shape.
8+
9+
Package: `minimal-vite-devframe-hub` · framework: **Vanilla TypeScript (Vite)**
10+
11+
## What it proves
12+
13+
- `createHubContext()` boots a hub without any Vite-specific code path.
14+
- A `DevframeHost` implementation plugs framework specifics (storage paths, origin resolution) into the hub uniformly.
15+
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
16+
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
17+
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware.
18+
19+
## Run it
20+
21+
```sh
22+
pnpm install
23+
pnpm --filter minimal-vite-devframe-hub dev
24+
```
25+
26+
Open the printed URL to see the docks, commands, messages, and terminals lists the hub exposes, plus a button that dispatches a sample command through `hub:commands:execute`.
27+
28+
## Source
29+
30+
[`examples/minimal-vite-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devframe-hub)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# next-runtime-snapshot
6+
7+
A **Next.js App Router** SPA over RPC, surfacing the host Node runtime — system info, memory, and environment variables. It shows that a React + Next.js build is a drop-in replacement for a Preact + Vite SPA: devframe serves the static export, and the client calls into the host Node process through the same type-safe RPC.
8+
9+
Package: `next-runtime-snapshot-example` · framework: **React (Next.js)**
10+
11+
## What it shows
12+
13+
- `…:system` — a `static` RPC. Runs once at build time when baked into a static dump, otherwise resolved live over WebSocket. Returns Node version, platform / arch, pid, cwd, and start time.
14+
- `…:memory` — a `query` RPC the UI re-invokes from a refresh button.
15+
- `…:env` — a `query` with valibot-validated args, listing environment variables matching a regex and redacting keys that look secret.
16+
- Next.js App Router with `'use client'` components calling `connectDevframe()` once, then sharing the scoped client through React context.
17+
18+
The Next.js config carries three non-defaults that each map to a devframe design principle: `output: 'export'` (devframe owns the server), `assetPrefix: '.'` (relative assets so the same build works at any base), and `trailingSlash: true` (composes with devframe's directory-with-index static resolution).
19+
20+
## Run it
21+
22+
```sh
23+
pnpm -C examples/next-runtime-snapshot run build # next build → static export
24+
pnpm -C examples/next-runtime-snapshot run dev # devframe CLI dev server
25+
pnpm -C examples/next-runtime-snapshot run cli:build # static deploy → dist/static
26+
```
27+
28+
The three cards populate from RPC; the static deploy still works because the `static` and `query` RPCs that opted into the dump are baked at build time.
29+
30+
## Source
31+
32+
[`examples/next-runtime-snapshot`](https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot)

‎docs/examples/streaming-chat.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# streaming-chat
6+
7+
A **Preact** demo of devframe's [streaming-channel API](/guide/streaming) combined with [shared state](/guide/shared-state) for persistent chat history. The server emits synthesized "tokens" one at a time over a streaming channel, while the conversation log lives in shared state so it survives reloads, syncs across panels, and replays cleanly when a client re-joins mid-stream.
8+
9+
Package: `streaming-chat-example` · framework: **Preact + Vite**
10+
11+
## What it shows
12+
13+
- A scoped context (`ctx.scope('devframe-streaming-chat')`) auto-namespaces every id.
14+
- `my.rpc.streaming.create('tokens', …)` registers a streaming channel for low-latency token rendering.
15+
- `my.rpc.sharedState('history', …)` keeps the message log on the server; each `send` appends a user + assistant pair atomically.
16+
- The producer streams tokens live, then commits the joined content back to shared state when done — so refreshes and new clients see the finished message immediately.
17+
- `reader.cancel()` aborts mid-stream; the assistant message is marked cancelled with whatever content accumulated.
18+
- `replayWindow` lets a panel reopened mid-stream replay buffered tokens before resuming live.
19+
20+
To wire it to a real LLM, replace the fake token generator in `src/devframe.ts` with anything that yields strings — the stream's `signal` propagates cancellation from the browser all the way to the upstream request.
21+
22+
## Run it
23+
24+
```sh
25+
pnpm -C examples/streaming-chat run build
26+
pnpm -C examples/streaming-chat run dev
27+
```
28+
29+
Open the printed URL, type a prompt, watch tokens stream in, refresh mid-conversation, and cancel a long answer.
30+
31+
## Source
32+
33+
[`examples/streaming-chat`](https://github.com/devframes/devframe/tree/main/examples/streaming-chat)

0 commit comments

Comments
 (0)