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
1 change: 1 addition & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const alias = {
'@devframes/plugin-terminals': p('terminals/src/index.ts'),
'@devframes/plugin-git': p('git/src/index.ts'),
'devframe/recipes/interactive-auth': r('devframe/src/recipes/interactive-auth.ts'),
'devframe/recipes/common-rpc-functions': r('devframe/src/recipes/common-rpc-functions.ts'),
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
'devframe/client': r('devframe/src/client/index.ts'),
'devframe': r('devframe/src'),
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function helpersItems(prefix: string) {
{ text: 'Vite Bridge', link: `${prefix}/helpers/vite-bridge` },
{ text: 'Nuxt Module', link: `${prefix}/helpers/nuxt` },
{ text: 'Next Helper', link: `${prefix}/helpers/next` },
{ text: 'Open Helpers', link: `${prefix}/helpers/open-helpers` },
{ text: 'Common RPC Functions', link: `${prefix}/helpers/common-rpc-functions` },
{ text: 'Interactive Auth', link: `${prefix}/helpers/interactive-auth` },
] satisfies DefaultTheme.NavItemWithLink[]
}
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/standalone-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ defineDevframe({

The adapter derives each flag's CAC option from its schema — booleans become `--verbose` / `--no-verbose`; everything else becomes `--depth <value>`. Keys are camelCase in TypeScript, kebab-case on the command line (`configFile` → `--config-file`). Flags that aren't in your schema (`--host`, `--port`, or anything added via `cli.configure`) still pass through untouched.

## Open helpers
## Common RPC functions

For the two actions every CLI devtool needs — open a file in the editor, reveal a path in the OS file explorer — use the prebuilt recipes from `devframe/recipes/open-helpers` instead of re-implementing them. See [Helpers → Open Helpers](/helpers/open-helpers) for the full reference.
For the two actions every CLI devtool needs — open a file in the editor, reveal a path in the OS file explorer — use the prebuilt recipes from `devframe/recipes/common-rpc-functions` instead of re-implementing them. See [Helpers → Common RPC Functions](/helpers/common-rpc-functions) for the full reference.

## Snapshot queries for static builds

Expand Down
61 changes: 61 additions & 0 deletions docs/helpers/common-rpc-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
outline: deep
---

# Common RPC Functions

Prebuilt RPC actions for the two file-system actions every CLI devtool needs — opening a file in the editor, revealing a path in the OS file explorer. Use the recipe instead of re-implementing them so every devframe converges on the same registered names and payload shape.

```ts
import { commonRpcFunctions } from 'devframe/recipes/common-rpc-functions'

defineDevframe({
id: 'my-tool',
name: 'My Tool',
setup(ctx) {
commonRpcFunctions.forEach(fn => ctx.rpc.register(fn))
},
})
```

## Exports

| Export | Registered name | Type | Args | Purpose |
|--------|------------------|------|------|---------|
| `openInEditor` | `devframe:open-in-editor` | `action` | `[filename: string, editor?: KnownEditor]` | Open the file in the user's editor via [`launchEditor`](./utilities#devframe-utils-launch-editor). `filename` accepts `file`, `file:line`, or `file:line:column`. The optional `editor` picks the editor command explicitly instead of relying on auto-detection. |
| `openInFinder` | `devframe:open-in-finder` | `action` | `[path: string]` | Reveal the path in the OS file explorer via [`open`](./utilities#devframe-utils-open). |
| `commonRpcFunctions` | — | `readonly [openInEditor, openInFinder]` | — | Convenience array for batch registration. |
| `KNOWN_EDITORS` | — | `readonly string[]` | — | The editor commands `openInEditor`'s `editor` argument accepts (`code`, `vim`, `subl`, `idea`, …). |
| `KnownEditor` | — | type | — | Union of `KNOWN_EDITORS`. |

Both functions are `action`-type RPCs returning `void` and use `valibot` schemas for their arguments — `openInEditor`'s `editor` argument is `v.optional(v.picklist(KNOWN_EDITORS))`, so a value outside `KNOWN_EDITORS` fails validation rather than reaching the underlying `launch-editor` process spawn. Both handlers dynamically `import()` their underlying `devframe/utils/*` implementation, so the `launch-editor` and `open` dependencies only load when the recipe actually runs.

The `devframe/recipes/open-helpers` entry (`openHelpers`) remains as a deprecated alias for this module — new code should import `commonRpcFunctions` from `devframe/recipes/common-rpc-functions`.

## Pick and choose

Register only the helper you need rather than the whole array:

```ts
import { openInEditor } from 'devframe/recipes/common-rpc-functions'

defineDevframe({
id: 'my-tool',
setup(ctx) {
ctx.rpc.register(openInEditor)
},
})
```

## On the client

The SPA calls these like any other RPC:

```ts
const rpc = await connectDevframe()
await rpc.call('devframe:open-in-editor', 'src/main.ts:42:7')
await rpc.call('devframe:open-in-editor', 'src/main.ts:42:7', 'code')
await rpc.call('devframe:open-in-finder', '/abs/path/to/dir')
```

`launchEditor`'s editor auto-detection reads the `LAUNCH_EDITOR` environment variable on the server side when no `editor` argument is passed — there is no client-side configuration.
2 changes: 1 addition & 1 deletion docs/helpers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Helpers are the optional, opt-in surface around the core `defineDevframe` API: s
| [Vite Bridge](./vite-bridge) | `devframe/helpers/vite` | Vite plugin for mounting a devframe inside any Vite-based host (Astro, SolidStart, plain Vite). |
| [Nuxt Module](./nuxt) | `@devframes/nuxt` | Nuxt module that wires a Nuxt SPA as a devframe client and serves the dev-time RPC bridge. |
| [Next Helper](./next) | `@devframes/next` | Route-handler host + React client for mounting devframes inside a Next.js App Router app (experimental). |
| [Open Helpers](./open-helpers) | `devframe/recipes/open-helpers` | Prebuilt RPC actions for "open in editor" and "reveal in Finder". |
| [Common RPC Functions](./common-rpc-functions) | `devframe/recipes/common-rpc-functions` | Prebuilt RPC actions for "open in editor" and "reveal in Finder". |
| [Interactive Auth](./interactive-auth) | `devframe/recipes/interactive-auth` | Ready-made OTP auth layer — handshake, resolver gate, connect-time trust, and the code/link banner. |

Helpers vs. [adapters](/adapters/): an adapter takes a `DevframeDefinition` and deploys it as a runnable surface (CLI, dev server, static build, MCP server). A helper is a smaller piece — a Vite plugin, a Nuxt module, a recipe, a utility function — that you compose alongside an adapter.
56 changes: 0 additions & 56 deletions docs/helpers/open-helpers.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/helpers/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ launchEditor('src/main.ts:42:7')
launchEditor('src/main.ts:42:7', 'code')
```

The auto-detection reads the `LAUNCH_EDITOR` environment variable and falls back to common defaults. Most devframes consume this through the prebuilt `openInEditor` recipe — see [Open helpers](./open-helpers).
The auto-detection reads the `LAUNCH_EDITOR` environment variable and falls back to common defaults. Most devframes consume this through the prebuilt `openInEditor` recipe — see [Common RPC Functions](./common-rpc-functions).

### `devframe/utils/hash`

Expand Down
1 change: 1 addition & 0 deletions packages/devframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"./node": "./dist/node/index.mjs",
"./node/auth": "./dist/node/auth.mjs",
"./node/hub-internals": "./dist/node/hub-internals.mjs",
"./recipes/common-rpc-functions": "./dist/recipes/common-rpc-functions.mjs",
"./recipes/interactive-auth": "./dist/recipes/interactive-auth.mjs",
"./recipes/open-helpers": "./dist/recipes/open-helpers.mjs",
"./rpc": "./dist/rpc/index.mjs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as v from 'valibot'
import { describe, expect, it } from 'vitest'
import { commonRpcFunctions, KNOWN_EDITORS, openInEditor, openInFinder } from '../common-rpc-functions'
import { openHelpers } from '../open-helpers'

describe('recipes/common-rpc-functions', () => {
it('exposes `openInEditor` as a devframe-namespaced action', () => {
expect(openInEditor.name).toBe('devframe:open-in-editor')
expect(openInEditor.type).toBe('action')
expect(openInEditor.args).toHaveLength(2)
expect(typeof openInEditor.handler).toBe('function')
})

it('restricts `openInEditor`\'s optional second argument to `KNOWN_EDITORS`', () => {
expect(KNOWN_EDITORS).toContain('code')
expect(KNOWN_EDITORS).toContain('vim')

const editorSchema = openInEditor.args[1]
expect(v.safeParse(editorSchema, undefined).success).toBe(true)
for (const editor of KNOWN_EDITORS)
expect(v.safeParse(editorSchema, editor).success).toBe(true)
expect(v.safeParse(editorSchema, 'not-a-real-editor').success).toBe(false)
})

it('exposes `openInFinder` as a devframe-namespaced action', () => {
expect(openInFinder.name).toBe('devframe:open-in-finder')
expect(openInFinder.type).toBe('action')
expect(openInFinder.args).toHaveLength(1)
expect(typeof openInFinder.handler).toBe('function')
})

it('bundles both helpers in `commonRpcFunctions`', () => {
expect(commonRpcFunctions).toHaveLength(2)
expect(commonRpcFunctions).toContain(openInEditor)
expect(commonRpcFunctions).toContain(openInFinder)
})

it('keeps the deprecated `devframe/recipes/open-helpers` entry working as an alias', () => {
expect(openHelpers).toBe(commonRpcFunctions)
})
})
24 changes: 0 additions & 24 deletions packages/devframe/src/recipes/__tests__/open-helpers.test.ts

This file was deleted.

147 changes: 147 additions & 0 deletions packages/devframe/src/recipes/common-rpc-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as v from 'valibot'
import { defineRpcFunction } from '../rpc/define'

/**
* Editor commands that `launch-editor` (the library behind
* `devframe/utils/launch-editor`) recognizes with a tailored
* `file:line:column` invocation. `openInEditor`'s optional second argument
* is restricted to this union, so the RPC surface can't be used to spawn an
* arbitrary command.
*/
export type KnownEditor
= | 'atom'
| 'subl'
| 'sublime'
| 'sublime_text'
| 'wstorm'
| 'charm'
| 'zed'
| 'notepad++'
| 'vim'
| 'mvim'
| 'joe'
| 'gvim'
| 'emacs'
| 'emacsclient'
| 'rmate'
| 'mate'
| 'code'
| 'code-insiders'
| 'codium'
| 'vscodium'
| 'trae'
| 'antigravity'
| 'cursor'
| 'appcode'
| 'clion'
| 'idea'
| 'phpstorm'
| 'pycharm'
| 'rubymine'
| 'webstorm'
| 'goland'
| 'rider'

/** Runtime list of every {@link KnownEditor}, in the order `v.picklist` reports them. */
export const KNOWN_EDITORS: KnownEditor[] = [
'atom',
'subl',
'sublime',
'sublime_text',
'wstorm',
'charm',
'zed',
'notepad++',
'vim',
'mvim',
'joe',
'gvim',
'emacs',
'emacsclient',
'rmate',
'mate',
'code',
'code-insiders',
'codium',
'vscodium',
'trae',
'antigravity',
'cursor',
'appcode',
'clion',
'idea',
'phpstorm',
'pycharm',
'rubymine',
'webstorm',
'goland',
'rider',
]

/**
* Prebuilt RPC action that opens a file in the user's configured editor.
*
* Registered name: `devframe:open-in-editor`.
*
* The optional second argument picks the editor command explicitly (must be
* one of {@link KNOWN_EDITORS}); otherwise it's auto-detected per
* `devframe/utils/launch-editor`.
*
* ```ts
* import { openInEditor } from 'devframe/recipes/common-rpc-functions'
*
* defineDevframe({
* id: 'my-tool',
* name: 'My Tool',
* setup(ctx) {
* ctx.rpc.register(openInEditor)
* },
* })
* ```
*/
export const openInEditor = defineRpcFunction({
name: 'devframe:open-in-editor',
type: 'action',
jsonSerializable: true,
args: [v.string(), v.optional(v.picklist<KnownEditor[]>(KNOWN_EDITORS))],
returns: v.void(),
async handler(filename: string, editor?: KnownEditor) {
const { launchEditor } = await import('devframe/utils/launch-editor')
launchEditor(filename, editor)
},
})

/**
* Prebuilt RPC action that reveals a path in the OS file explorer.
*
* Registered name: `devframe:open-in-finder`.
*
* ```ts
* import { openInFinder } from 'devframe/recipes/common-rpc-functions'
*
* ctx.rpc.register(openInFinder)
* ```
*/
export const openInFinder = defineRpcFunction({
name: 'devframe:open-in-finder',
type: 'action',
jsonSerializable: true,
args: [v.string()],
returns: v.void(),
async handler(path: string) {
const { open } = await import('devframe/utils/open')
await open(path)
},
})

/**
* Convenience array bundling both helpers so callers can register them
* in a single `forEach`.
*
* ```ts
* import { commonRpcFunctions } from 'devframe/recipes/common-rpc-functions'
*
* commonRpcFunctions.forEach(fn => ctx.rpc.register(fn))
* ```
*/
export const commonRpcFunctions = [openInEditor, openInFinder] as const
Loading
Loading