Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ playwright-report
playwright/.cache
blob-report
.ecosystem
storybook-static
6 changes: 6 additions & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join, relative } from 'pathe'

const root = fileURLToPath(new URL('.', import.meta.url))
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))
const p = (path: string) => fileURLToPath(new URL(`./plugins/${path}`, import.meta.url))

export const alias = {
'devframe/rpc/transports/ws-server': r('devframe/src/rpc/transports/ws-server.ts'),
Expand Down Expand Up @@ -47,6 +48,11 @@ export const alias = {
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
'devframe/client': r('devframe/src/client/index.ts'),
'devframe': r('devframe/src'),
'@devframes/plugin-inspect/client': p('inspect/src/client/index.ts'),
'@devframes/plugin-inspect/node': p('inspect/src/node/index.ts'),
'@devframes/plugin-inspect/cli': p('inspect/src/cli.ts'),
'@devframes/plugin-inspect/vite': p('inspect/src/vite.ts'),
'@devframes/plugin-inspect': p('inspect/src/index.ts'),
}

// update tsconfig.base.json
Expand Down
9 changes: 3 additions & 6 deletions packages/devframe/src/node/__tests__/rpc-streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,22 @@ import { createRpcClient } from 'devframe/rpc/client'
import { createRpcServer } from 'devframe/rpc/server'
import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client'
import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server'
import { getPort } from 'get-port-please'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { WebSocket } from 'ws'

import { RpcFunctionsHost } from '../host-functions'

vi.stubGlobal('WebSocket', WebSocket)

let nextPort = 41000
function allocatePort(): number {
return nextPort++
}

interface Harness {
port: number
rpcHost: RpcFunctionsHost
close: () => Promise<void>
}

async function bootHost(): Promise<Harness> {
const port = allocatePort()
const port = await getPort({ random: true })
const mockContext = {} as DevframeNodeContext
const rpcHost = new RpcFunctionsHost(mockContext)

Expand Down
22 changes: 22 additions & 0 deletions plugins/inspect/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { StorybookConfig } from '@storybook/vue3-vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import { mergeConfig } from 'vite'
import { alias } from '../../../alias'

const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {},
async viteFinal(config) {
return mergeConfig(config, {
resolve: { alias },
plugins: [vue(), UnoCSS()],
})
},
}
export default config
29 changes: 29 additions & 0 deletions plugins/inspect/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Preview } from '@storybook/vue3'
import 'virtual:uno.css'
import '../src/spa/style.css'

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'dark',
values: [
{
name: 'dark',
value: '#111111',
},
{
name: 'light',
value: '#ffffff',
},
],
},
},
}

export default preview
56 changes: 56 additions & 0 deletions plugins/inspect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# @devframes/plugin-inspect

A devframe plugin that inspects *its own* connection (and, when mounted in a
host, the host's): browse every registered RPC function with its metadata,
invoke read-only `query`/`static` functions and inspect the results, watch
shared-state keys update live, and explore the agent-exposed surface.

Ported in spirit from the RPC & State panels of
[`vitejs/devtools`](https://github.com/vitejs/devtools); rebuilt on devframe's
framework-neutral client (`connectDevframe`, `rpc.sharedState`) with a Vue + Vite SPA.

## Use it standalone

```bash
npx @devframes/plugin-inspect
```

Opens the inspector against a fresh standalone devframe connection — useful as a
reference and for poking at the introspection RPCs themselves.

## Mount into a Vite host

```ts
// vite.config.ts
import { inspectVitePlugin } from '@devframes/plugin-inspect/vite'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
inspectVitePlugin(),
],
})
```

## Programmatic

```ts
import { createInspectDevframe } from '@devframes/plugin-inspect'

const devframe = createInspectDevframe({ port: 9100 })
```

## RPC surface

All functions are namespaced `devframes-plugin-inspect:*`:

| Function | Type | What it returns |
|----------|------|-----------------|
| `list-functions` | `query` (snapshot) | Every registered RPC function with metadata (type, JSON-serializable/snapshot flags, args/return JSON Schema, agent exposure). |
| `invoke` | `action` | Invokes a read-only `query`/`static` function by name and returns a result envelope. Refuses `action`/`event` functions. |
| `list-state-keys` | `query` (snapshot) | The keys of every shared-state entry on the connection. |
| `describe-agent` | `query` (snapshot) | The agent manifest — tools and readable resources. |

The three `query` functions are agent-exposed (read-only) and bake into the
static dump, so the inspector still lists functions, state keys, and the agent
surface when deployed as a static SPA.
13 changes: 13 additions & 0 deletions plugins/inspect/bin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node
import process from 'node:process'
import { createInspectCli } from './dist/cli.mjs'

async function main() {
const cli = createInspectCli()
await cli.parse()
}

main().catch((error) => {
console.error(error)
process.exit(1)
})
80 changes: 80 additions & 0 deletions plugins/inspect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "@devframes/plugin-inspect",
"type": "module",
"version": "0.5.2",
"description": "Devframe plugin — a self-inspector for the RPC registry, shared state, and agent surface of a devframe connection.",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"homepage": "https://github.com/devframes/devframe#readme",
"repository": {
"directory": "plugins/inspect",
"type": "git",
"url": "git+https://github.com/devframes/devframe.git"
},
"bugs": "https://github.com/devframes/devframe/issues",
"keywords": [
"devframe",
"devframe-plugin",
"devtools",
"rpc",
"inspector"
],
"sideEffects": false,
"exports": {
".": "./dist/index.mjs",
"./client": "./dist/client/index.mjs",
"./cli": "./dist/cli.mjs",
"./node": "./dist/node/index.mjs",
"./vite": "./dist/vite.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"bin": {
"devframe-inspect": "./bin.mjs"
},
"files": [
"bin.mjs",
"dist"
],
"scripts": {
"build": "tsdown && vite build --config src/spa/vite.config.ts",
"dev": "vite --config src/spa/vite.config.ts --host 0.0.0.0",
"watch": "tsdown --watch",
"cli:build": "node bin.mjs build --out-dir dist/static",
"prepack": "pnpm build",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "vitest run"
},
"peerDependencies": {
"devframe": "workspace:*",
"vite": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
},
"dependencies": {
"@valibot/to-json-schema": "catalog:deps",
"nostics": "catalog:deps"
},
"devDependencies": {
"@iconify-json/ph": "catalog:",
"@storybook/addon-essentials": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",
"@unocss/preset-icons": "catalog:",
"@vitejs/plugin-vue": "catalog:build",
"devframe": "workspace:*",
"get-port-please": "catalog:deps",
"h3": "catalog:deps",
"storybook": "catalog:",
"tsdown": "catalog:build",
"unocss": "catalog:",
"vite": "catalog:build",
"vitest": "catalog:testing",
"vue": "catalog:frontend",
"ws": "catalog:deps"
}
}
13 changes: 13 additions & 0 deletions plugins/inspect/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { CliHandle } from 'devframe/adapters/cli'
import { createCli } from 'devframe/adapters/cli'
import inspectDevframe from './index'

/**
* Build the standalone CLI for the inspector — backs the package `bin`
* (`devframe-inspect`) and `npx @devframes/plugin-inspect`. Wraps the
* default {@link createInspectDevframe} definition with devframe's
* `dev` / `build` / `spa` / `mcp` command shell.
*/
export function createInspectCli(): CliHandle {
return createCli(inspectDevframe)
}
14 changes: 14 additions & 0 deletions plugins/inspect/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DevframeRpcClient, DevframeRpcClientOptions } from 'devframe/client'
import { connectDevframe } from 'devframe/client'

export type { DevframeRpcClient }
export type { AgentManifest, InvokeResult, RpcFunctionAgentInfo, RpcFunctionInfo } from '../types'

/**
* Connect to the inspector's devframe backend. A thin, typed wrapper
* around devframe's {@link connectDevframe}; the SPA derives its base
* from `document.baseURI`, so no options are required in the common case.
*/
export function connectInspect(options?: DevframeRpcClientOptions): Promise<DevframeRpcClient> {
return connectDevframe(options)
}
23 changes: 23 additions & 0 deletions plugins/inspect/src/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineDiagnostics } from 'nostics'

/**
* Structured diagnostics for `@devframes/plugin-inspect`. Node-side only.
* Codes use the plugin-private `DP_INSPECT_` band (see the built-in
* plugins planning index) so they never collide with devframe core
* (`DF00xx`) or `@devframes/hub` (`DF80xx`).
*/
export const diagnostics = defineDiagnostics({
docsBase: 'https://devfra.me/errors',
codes: {
DP_INSPECT_0001: {
why: (p: { name: string }) =>
`Cannot invoke "${p.name}" — no RPC function with that name is registered on this connection.`,
fix: 'Call `devframes-plugin-inspect:list-functions` to see the registered names, or check for a typo.',
},
DP_INSPECT_0002: {
why: (p: { name: string, type: string }) =>
`Refusing to invoke "${p.name}" — only read-only "query" and "static" functions are invokable from the inspector, but this one is "${p.type}".`,
fix: 'The inspector deliberately blocks `action`/`event` functions to avoid triggering side effects. Invoke those through their own UI instead.',
},
},
})
70 changes: 70 additions & 0 deletions plugins/inspect/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { DevframeDefinition } from 'devframe/types'
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { defineDevframe } from 'devframe/types'
import { setupInspect } from './node/index'

/** Default devframe id — drives the hosted mount path `/__<id>/`. */
const DEFAULT_ID = 'devframes-plugin-inspect'

// The Vue SPA is built (by Vite) into `dist/spa`. From both the source
// entry (`src/index.ts`, via the workspace alias) and the published
// entry (`dist/index.mjs`), `../dist/spa` resolves to `<pkg>/dist/spa`.
const distDir = fileURLToPath(new URL('../dist/spa', import.meta.url))

export interface InspectDevframeOptions {
/** Override the devframe id (and default CLI command / mount path). */
id?: string
/** Override the display name shown in a host dock. */
name?: string
/** Override the dock icon. */
icon?: string
/**
* Override the mount path. Left unset, the SPA mounts at `/` standalone
* and `/__<id>/` when hosted (Vite/embedded).
*/
basePath?: string
/** Preferred standalone CLI port. */
port?: number
/**
* Require the trust handshake on the standalone server. Defaults to
* `false` (auto-trust) since the inspector is a single-user localhost
* tool. Hosted adapters manage their own auth.
*/
auth?: boolean
}

/**
* Build a {@link DevframeDefinition} for the RPC & State inspector. The
* same definition runs standalone (`/cli`, `/spa`, `/build`) and mounts
* into a host (`/vite`, hub).
*/
export function createInspectDevframe(options: InspectDevframeOptions = {}): DevframeDefinition {
const id = options.id ?? DEFAULT_ID
return defineDevframe({
id,
name: options.name ?? 'RPC & State Inspector',
icon: options.icon ?? 'ph:stethoscope-duotone',
basePath: options.basePath,
cli: {
command: id,
port: options.port ?? 9012,
distDir: existsSync(distDir) ? distDir : undefined,
// A single-user localhost inspector: skip the trust handshake so
// the SPA's shared-state subscription initializes without a manual
// auth round-trip. Hosted adapters (Vite/hub) supply their own
// auth layer and ignore this.
auth: options.auth ?? false,
},
spa: { loader: 'none' },
setup(ctx) {
setupInspect(ctx)
},
})
}

/** The default inspector devframe definition. */
const inspectDevframe: DevframeDefinition = createInspectDevframe()

export default inspectDevframe
export type { InvokeResult, RpcFunctionInfo } from './types'
14 changes: 14 additions & 0 deletions plugins/inspect/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DevframeNodeContext } from 'devframe/types'
import { serverFunctions } from '../rpc/index'

/**
* Register the inspector's introspection RPC functions on a devframe
* node context. Called from the definition's `setup(ctx)` and reusable
* by host adapters that wire their own context.
*/
export function setupInspect(ctx: DevframeNodeContext): void {
for (const fn of serverFunctions)
ctx.rpc.register(fn)
}

export { serverFunctions }
Loading