Skip to content

Commit a81d1d4

Browse files
committed
Merge remote-tracking branch 'origin/main' into frank-insects-fly
# Conflicts: # plugins/data-inspector/src/spa/App.vue # plugins/data-inspector/src/spa/composables/workbench.ts
2 parents da795b3 + 191e81c commit a81d1d4

149 files changed

Lines changed: 5780 additions & 153 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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ These reinforce devframe's positioning as "the container for one devtool integra
6464
- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` / `createSpa` copy SPA output verbatim — no HTML rewriting, no build-time `--base` injection. The client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically.
6565
- **CLI flags compose from both sides.** The `cac` instance backing `createCac` is exposed both to the `DevframeDefinition` (`cli.configure(cli)`) — for capabilities contributed by the tool itself — and to the `createCac` caller — for flags added at the final assembly stage. Parsed flag values are forwarded to `setup(ctx, { flags })`. Never hardcode domain-specific flags into `createCac`.
6666

67+
### Hub example parity
68+
69+
`examples/vite-devframe-hub/` (Vite plugin + vanilla client) and `examples/next-devframe-hub/` (Next.js App Router + React client) are the two reference hosts, and they stay at **feature parity**. They mount the same set of plugins and demo devframes, expose the same dock rail / iframe stage / subsystem drawer, and speak the same hub protocol — the only differences should be the host framework's own plumbing (how static assets are mounted, how the side-car server starts, how the client is rendered).
70+
71+
Any change to one lands in the other in the same PR: adding a dock, wiring a new hub subsystem, changing the drawer layout, adopting a new client-runtime API. Their READMEs mirror each other too. If a capability genuinely can't exist on one host, say so explicitly in both READMEs rather than letting the examples silently drift.
72+
6773
## Structured Diagnostics (Error Codes)
6874

6975
All node-side warnings and errors use structured diagnostics via [`nostics`](https://www.npmjs.com/package/nostics). Never use raw `console.warn`, `console.error`, or `throw new Error` with ad-hoc messages in node-side code — always define a coded diagnostic.

alias.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export const alias = {
108108
'@devframes/plugin-messages/cli': p('messages/src/cli.ts'),
109109
'@devframes/plugin-messages/vite': p('messages/src/vite.ts'),
110110
'@devframes/plugin-messages': p('messages/src/index.ts'),
111+
'@devframes/plugin-assets/client': p('assets/src/client/index.ts'),
112+
'@devframes/plugin-assets/node': p('assets/src/node/index.ts'),
113+
'@devframes/plugin-assets/rpc': p('assets/src/rpc/index.ts'),
114+
'@devframes/plugin-assets/cli': p('assets/src/cli.ts'),
115+
'@devframes/plugin-assets/vite': p('assets/src/vite.ts'),
116+
'@devframes/plugin-assets': p('assets/src/index.ts'),
111117
}
112118

113119
// update tsconfig.base.json — CSS aliases exist for Vite resolution only;

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function guideItems(prefix: string) {
3232
{ text: 'Security', link: `${prefix}/guide/security` },
3333
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
3434
{ text: 'Hub', link: `${prefix}/guide/hub` },
35+
{ text: 'Deep Linking', link: `${prefix}/guide/deep-linking` },
3536
{ text: 'Client Scripts & Context', link: `${prefix}/guide/client-context` },
3637
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },
3738
] satisfies DefaultTheme.NavItemWithLink[]
@@ -71,6 +72,7 @@ function pluginsItems(prefix: string) {
7172
{ text: 'Git', link: `${prefix}/plugins/git` },
7273
{ text: 'Terminals', link: `${prefix}/plugins/terminals` },
7374
{ text: 'Code Server', link: `${prefix}/plugins/code-server` },
75+
{ text: 'Assets', link: `${prefix}/plugins/assets` },
7476
] satisfies DefaultTheme.NavItemWithLink[]
7577
}
7678

docs/errors/DF0042.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+
# DF0042: Static Build Disabled By The Definition
6+
7+
## Message
8+
9+
> "`{id}`" declares `capabilities.build: false` — its static export is not meaningful (writes are excluded and any live-served data won't be there).
10+
11+
## Cause
12+
13+
`createBuild` runs unconditionally when called directly, but a definition can opt out of static export via `capabilities.build: false` — typically because the devframe is inherently live (it manages real files on disk, spawns a process, etc.) and a `mode: 'build'` export would only ever produce a broken, write-less shell of the tool. `createCac` already skips registering the `build` subcommand for such a definition; this diagnostic covers the remaining path — a caller invoking `createBuild()` directly, bypassing the CLI.
14+
15+
## Example
16+
17+
```ts
18+
// ✗ Bad — builds a devframe that opted out of static export
19+
await createBuild(assetsDevframe) // throws DF0042
20+
21+
// ✓ Good — the degraded export is still useful to you
22+
await createBuild(assetsDevframe, { force: true })
23+
```
24+
25+
## Fix
26+
27+
- Pass `{ force: true }` to `createBuild()` if the degraded export is still useful to you.
28+
- Otherwise, drop `capabilities.build: false` on the definition if a static export should be supported after all.
29+
30+
## Source
31+
32+
- [`packages/devframe/src/adapters/build.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/build.ts)`createBuild()` throws this when `capabilities.build` is `false` and `force` isn't set.

docs/guide/client.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,42 @@ For SPA authors, that means:
3232

3333
That's how `createBuild` deploys SPA output verbatim under any URL — no build-time HTML rewriting needed.
3434

35+
### Sharing a connection with an external viewer
36+
37+
`setupDevframeConnection()` prepares a serializable connection independently
38+
of an RPC client. It records the metadata URL alongside the descriptor so a
39+
viewer running on another origin resolves relative paths and side-car ports
40+
against the Devframe server:
41+
42+
```ts
43+
import { setupDevframeConnection } from 'devframe/client'
44+
45+
const connection = await setupDevframeConnection({
46+
baseURL: '/__devframe/',
47+
})
48+
```
49+
50+
Pass that connection to `connectDevframe()` in the viewer:
51+
52+
```ts
53+
import { connectDevframe } from 'devframe/client'
54+
55+
const rpc = await connectDevframe({ connection })
56+
```
57+
58+
The RPC client retains the complete connection as `rpc.connection`, including
59+
the metadata source URL external viewers use to resolve relative resources.
60+
61+
`getDevframeConnection()` returns the prepared connection in the current
62+
window or an accessible parent window. Cross-realm viewers can read the
63+
serializable value through `DEVFRAME_CONNECTION_KEY` from
64+
`devframe/constants`.
65+
3566
### Options
3667

3768
```ts
3869
await connectDevframe({
70+
connection, // prepared by setupDevframeConnection()
3971
baseURL: './', // string or string[] fallback list — see notes below
4072
authToken: 'user-provided-token',
4173
cacheOptions: true, // enable response caching
@@ -46,6 +78,7 @@ await connectDevframe({
4678

4779
| Option | Description |
4880
|--------|-------------|
81+
| `connection` | A connection prepared by `setupDevframeConnection()`. Includes metadata, its source URL, and an optional auth token. |
4982
| `baseURL` | Mount path to probe for `__connection.json`. Accepts an array for fallback. Default: `'./'` — resolved relative to `document.baseURI` so the SPA finds its meta wherever it was deployed. Pass an explicit absolute path (e.g. `'/__devframe/'`) when calling from outside the SPA — say, an embedded webcomponent injected into a host app. |
5083
| `authToken` | Override the auth token. Defaults to a locally-persisted human-readable id. |
5184
| `cacheOptions` | `true` to enable caching with defaults, or an options object. |

docs/guide/deep-linking.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Deep Linking
6+
7+
Send a user straight to a specific view inside a devframe — a particular terminal session, a particular data source — from another dock, an agent, or a copied URL. There are two paths: the hub relays a **dock activation** to focus a dock in place, and a standalone SPA reads its own **URL hash** to restore the view on load.
8+
9+
## Focusing a dock inside a hub
10+
11+
The viewer's active dock is client-local state — which dock is on screen lives in the shell page, not in shared state. A mounted devframe runs in its own iframe on its own RPC client, so it reaches that selection through the hub. `hub:docks:activate` switches the active dock and carries an opaque `params` bag the target dock reads:
12+
13+
```ts
14+
await rpc.call('hub:docks:activate', {
15+
dockId: 'devframes:plugin:data-inspector',
16+
params: { sourceId: 'my-plugin:store' },
17+
})
18+
```
19+
20+
The hub broadcasts the request live and mirrors it into the [`devframe:docks:active`](/guide/shared-state) shared-state slot, so a dock that mounts *because* of the switch still converges on the request instead of missing the broadcast. The target dock subscribes to that slot, filters on its own `dockId`, and reads the `params` field it recognizes — see [Cross-iframe dock activation](/guide/hub#cross-iframe-dock-activation) for the full relay.
21+
22+
```mermaid
23+
sequenceDiagram
24+
participant Source as Other dock / agent
25+
participant Hub
26+
participant Slot as devframe:docks:active
27+
participant Target as Target dock
28+
Source->>Hub: hub:docks:activate { dockId, params }
29+
Hub->>Slot: mirror latest activation
30+
Hub-->>Target: switch active dock (if open)
31+
Slot-->>Target: read on mount + on update
32+
Target->>Target: params.dockId matches? focus params.<key>
33+
```
34+
35+
Focus is one-shot and tolerant: the [terminals dock](/plugins/terminals#focusing-a-session) reads `params.sessionId`, the [Data Inspector](/plugins/data-inspector#deep-linking) reads `params.sourceId`, and a target that names something not yet registered waits for it to appear, then fires once — the user's own clicks stay honored afterward. An id that never arrives is a no-op; a `dockId` the viewer doesn't know degrades to a warning ([DF8107](/errors/DF8107)).
36+
37+
## Standalone URL deep links
38+
39+
Running standalone — a CLI server, a static build — a devframe SPA owns its own URL. Encode the shareable view in the **hash** (`#…`): it round-trips through a copied link, survives a reload, and stays clear of the query string that the server handshake (`?devframe_auth_token=`) rides on. Parse it with `URLSearchParams` for a familiar key/value shape:
40+
41+
```ts
42+
// read on load
43+
const params = new URLSearchParams(location.hash.replace(/^#/, ''))
44+
const sourceId = params.get('source')
45+
46+
// write back, without stacking history entries
47+
history.replaceState(history.state, '', `#${params.toString()}`)
48+
49+
// react to back/forward and manual edits
50+
window.addEventListener('hashchange', applyState)
51+
```
52+
53+
The [terminals dock](/plugins/terminals#deep-linking) keys a single selection as `#id=<sessionId>`; the [Data Inspector](/plugins/data-inspector#deep-linking) encodes its whole workbench — `#source=…&query=…` plus filter and auto-rerun flags — so a link reproduces an exact query result. Read the hash once at boot to restore the view, then keep it in sync as the user works. `replaceState` writes never fire `hashchange`, so the boot read, the live listener, and the write-back compose without looping.
54+
55+
Keep credentials out of anything shareable. A pre-shared handshake token belongs in the query string and should be scrubbed from the address bar as soon as it's read, the way the Data Inspector consumes `?devframe_auth_token=` — never in the hash a user copies to share a view.

docs/plugins/assets.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Assets
6+
7+
Browse, preview, upload, rename, and delete the files in a directory, built as a **Vue** SPA on `@antfu/design` — a framework-neutral port of Nuxt DevTools' Assets tab.
8+
9+
Package: `@devframes/plugin-assets` · framework: **Vue + @antfu/design**
10+
11+
## What it does
12+
13+
Search by name and filter by type from an inline chip row, switch between a thumbnail grid (grouped by folder) and a file tree, and open a resizable right-hand details panel with a live preview (image, video, audio, font, or text), file metadata, and ready-to-copy usage snippets (`<img>`, CSS `background-image`, `@font-face`, a download link). Upload files with the toolbar button (native file picker) or by dropping them anywhere on the frame, and select multiple assets to delete them together. A live file watcher keeps every connected client's listing in sync with changes made outside the UI.
14+
15+
The standalone server requires devframe's trust handshake by default because it can read, write, and delete real files. Uploads, renames, deletes, and folder creation are enabled by default — pass `{ write: false }` (or `--read-only` on the standalone CLI) for a browse-only deployment.
16+
17+
## Standalone
18+
19+
```sh
20+
pnpx @devframes/plugin-assets # manages <cwd>/public
21+
pnpx @devframes/plugin-assets --read-only # disable upload / rename / delete / mkdir
22+
```
23+
24+
## Mount into a Vite host
25+
26+
```ts
27+
// vite.config.ts
28+
import { assetsVitePlugin } from '@devframes/plugin-assets/vite'
29+
import { defineConfig } from 'vite'
30+
31+
export default defineConfig({
32+
plugins: [
33+
assetsVitePlugin(),
34+
],
35+
})
36+
```
37+
38+
## Programmatic
39+
40+
`createAssetsDevframe(options)` returns a definition you can deploy through any adapter:
41+
42+
```ts
43+
import { createAssetsDevframe } from '@devframes/plugin-assets'
44+
45+
export default createAssetsDevframe({
46+
dir: 'static', // defaults to `<cwd>/public`
47+
baseURL: '/', // the URL the host serves `dir` at
48+
write: true,
49+
uploadExtensions: ['png', 'jpg', 'svg', 'webp'], // defaults to Nuxt DevTools' own allow-list, or '*' for any
50+
})
51+
```
52+
53+
| Option | Default | Description |
54+
|--------|---------|-------------|
55+
| `dir` | `<cwd>/public` | Directory this devframe manages. |
56+
| `baseURL` | `/` | URL base the host serves `dir` at — each asset's `publicPath` is `baseURL` + its path. Match a non-root deployment base (e.g. Nuxt's `app.baseURL`). |
57+
| `write` | `true` | Enable upload, rename, delete, and folder creation from the UI. |
58+
| `uploadExtensions` | Nuxt DevTools' allow-list | Extensions `upload` accepts, or `'*'` for any. |
59+
| `serveStatic` | `false` | Serve the directory's bytes from this devframe itself. Left off when mounted into a host that already serves `public/`; the standalone CLI turns it on. |
60+
| `build` | `false` | Register the `build` CLI subcommand. See [why it's off by default](#static-export) below. |
61+
62+
## How previews are served
63+
64+
Asset previews (`<img>`, `<video>`, download links) load the files by their **public URL**, and the host the plugin is mounted into serves those files — Vite, Nuxt, and most frameworks already serve their `public/` folder at `/`. The plugin never stands up its own byte-serving route; it just resolves each asset's `publicPath` as `baseURL` + the file's path. Point `baseURL` at wherever the host serves `dir` (the default `/` matches the usual `public/` convention). The standalone CLI (`pnpx @devframes/plugin-assets`) is its own host, so it flips `serveStatic` on and serves the directory under a dedicated base.
65+
66+
## RPC surface
67+
68+
All functions are namespaced `devframes:plugin:assets:*`:
69+
70+
| Function | Type | Notes |
71+
|----------|------|-------|
72+
| `list` | `query`, `snapshot: true` | Every file under the managed directory, with type, size, and last-modified time. |
73+
| `capabilities` | `query`, `snapshot: true` | Whether write actions are enabled, and the upload allow-list — lets the UI gate itself proactively. |
74+
| `read-image-meta` | `query` | Width, height, and orientation for an image asset. |
75+
| `read-text` | `query` | Truncated text content, for preview. |
76+
| `upload` | `action` | Allocates a streaming upload slot; the client pipes the file's bytes over the paired channel. |
77+
| `rename` | `action` | Renames an asset within its folder, preserving its extension. |
78+
| `delete` | `action` | Deletes one or more assets in a single call. |
79+
| `mkdir` | `action` | Creates a folder, including missing parents. |
80+
| `open-in-editor` / `reveal-in-folder` | `action` | Launch the asset in your editor, or reveal its containing folder in the OS file manager. Always registered, regardless of `write`. |
81+
82+
`upload` / `rename` / `delete` / `mkdir` are registered only when `write` is enabled.
83+
84+
## Static export
85+
86+
Every devframe's `build` CLI subcommand is disabled here by default (`capabilities: { build: false }`). A static export has no live host serving the files, and every write action is inherently excluded from a static dump. Rather than ship a broken, preview-less, write-less shell of the tool, the `build` command is simply not registered. Pass `{ build: true }` to `createAssetsDevframe()` (and `{ force: true }` if calling `createBuild()` directly) if that degraded export is still useful to you — the file listing itself still bakes into the static RPC dump.
87+
88+
## Source
89+
90+
[`plugins/assets`](https://github.com/devframes/devframe/tree/main/plugins/assets)

docs/plugins/data-inspector.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Package: `@devframes/plugin-data-inspector` · framework: **Vue + Vite**
1010

1111
## What it does
1212

13-
- **Query workbench** — a CodeMirror jora editor with syntax highlighting and server-computed autocomplete; queries auto-run as you type, with a client-side syntax gate so malformed input never hits the wire. A toolbar copies the query, and the editor pairs with expand-all / collapse-all and copy-as-JSON controls over the results. Source, query, filters, and the auto-rerun setting persist in the URL, so any workbench state is shareable.
13+
- **Query workbench** — a CodeMirror jora editor with syntax highlighting and server-computed autocomplete; queries auto-run as you type, with a client-side syntax gate so malformed input never hits the wire. A toolbar copies the query, and the editor pairs with expand-all / collapse-all and copy-as-JSON controls over the results. Source, query, filters, and the auto-rerun setting persist in the URL hash, so any workbench state is shareable (see [Deep linking](#deep-linking)).
1414
- **Auto rerun** — an optional poller under the filters (`auto rerun every N seconds`) re-runs the current query against the live object on a fixed period, so a value that changes over time updates on its own. Ticks are skipped while a run is in flight or the query is syntactically broken.
1515
- **Result viewer** — results normalize to strict JSON (circulars become `$ref` markers; Maps, Sets, class instances, functions, and Dates get type badges) with per-query stats: jora / normalize / rpc timings, payload size, node count. The value-actions popup copies paths and turns any key into a query.
1616
- **Lazy expansion** — deep graphs return one level at a time: a node past the depth cap renders a `load deeper` link that fetches just that subtree with a fresh budget and splices it in place, so a huge object stays responsive and loads on demand.
@@ -70,6 +70,19 @@ ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources)
7070
> [!WARNING]
7171
> Queries are eval-grade access to registered objects: jora can invoke any function reachable as an own property and fires own getters. Register live objects with that in mind, and keep inspector endpoints on loopback.
7272
73+
## Deep linking
74+
75+
The whole workbench state lives in the URL hash — `#source=<id>&query=<jora>` plus the filter and auto-rerun flags — so a copied link reproduces an exact query result. It's read on load and kept in sync (via `replaceState`) as you work, and a `hashchange` listener re-applies it on back/forward and manual edits. The handshake token rides the query string (`?devframe_auth_token=`) and is scrubbed on read, so it never lands in a link you share.
76+
77+
Mounted in a hub, another dock can jump the user straight to a source through [dock activation](../guide/deep-linking#focusing-a-dock-inside-a-hub) — an activation targeting `devframes:plugin:data-inspector` with a `sourceId` selects that source, waiting for it to register if it hasn't yet:
78+
79+
```ts
80+
await rpc.call('hub:docks:activate', {
81+
dockId: 'devframes:plugin:data-inspector',
82+
params: { sourceId: 'my-plugin:store' },
83+
})
84+
```
85+
7386
## Standalone
7487

7588
```sh

0 commit comments

Comments
 (0)