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
9 changes: 9 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 @@ -44,6 +45,14 @@ export const alias = {
'@devframes/hub': r('hub/src/index.ts'),
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
'@devframes/nuxt': r('nuxt/src/index.ts'),
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),
'@devframes/plugin-code-server/types': p('code-server/src/types.ts'),
'@devframes/plugin-code-server/rpc': p('code-server/src/rpc/index.ts'),
'@devframes/plugin-code-server/cli': p('code-server/src/cli.ts'),
'@devframes/plugin-code-server/vite': p('code-server/src/vite.ts'),
'@devframes/plugin-code-server': p('code-server/src/index.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
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default antfu({
ignores: [
'skills',
'**/dist',
'**/storybook-static',
'**/.next',
'**/out',
'**/next-env.d.ts',
Expand Down
4 changes: 4 additions & 0 deletions plugins/code-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
storybook-static
.turbo
*.tsbuildinfo
11 changes: 11 additions & 0 deletions plugins/code-server/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { StorybookConfig } from '@storybook/html-vite'

const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
framework: {
name: '@storybook/html-vite',
options: {},
},
}

export default config
11 changes: 11 additions & 0 deletions plugins/code-server/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Preview } from '@storybook/html-vite'
import '../src/client/style.css'

const preview: Preview = {
parameters: {
layout: 'fullscreen',
controls: { expanded: true },
},
}

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

Run [code-server](https://github.com/coder/code-server) (VS Code in the
browser) as a devframe panel. The plugin detects a local `code-server`
install, launches it on demand, and embeds the editor in an
auto-authenticated `<iframe>`.

## How it works

- **Detection** — on startup it runs `code-server --version`. When the binary
is missing, the launcher renders install instructions and links instead of a
launch button.
- **Launch** — the launcher's button starts code-server as a managed child
process bound to a free port, scoped to the workspace. Readiness is probed
via code-server's `/healthz` endpoint.
- **Auto-auth** — code-server runs with password auth. The plugin generates a
random token, sets `HASHED_PASSWORD` to its SHA-256, and hands the matching
session cookie back to the already-authorized devframe client. The launcher
applies that cookie for the current host before loading the iframe, so the
editor opens already signed in — no code-server login page.

The editor iframe points at code-server's own origin
(`<protocol>//<host>:<port>/`), so WebSocket traffic flows directly without a
reverse proxy.

## Usage

### Standalone CLI

```sh
npx @devframes/plugin-code-server # dev server + launcher
```

### Programmatic

```ts
import { createCodeServerDevframe } from '@devframes/plugin-code-server'

export default createCodeServerDevframe({
// bin: 'code-server', // binary to detect/launch (default: PATH)
// serverPort: 8080, // force a port (default: free port near 8080)
// args: ['--disable-getting-started-override'],
})
```

### Vite host

```ts
import { codeServerVite } from '@devframes/plugin-code-server/vite'

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

## RPC

| Function | Type | Purpose |
|----------|------|---------|
| `devframes-plugin-code-server:detect` | query | Re-probe for the binary; returns `{ installed, version, bin }`. |
| `devframes-plugin-code-server:status` | query | Current status + auth cookie when running. |
| `devframes-plugin-code-server:start` | action | Launch and wait for readiness. |
| `devframes-plugin-code-server:stop` | action | Stop the process. |

Status (minus the auth cookie) is mirrored into the
`devframes-plugin-code-server:state` shared state for reactive UIs.

## UI

The launcher UI is a pure, state-driven view (`src/client/view.ts`) decoupled
from RPC, so every state renders in isolation. `mountCodeServer` wires the live
connection to it. Each UI state has a Storybook story:

```sh
pnpm storybook # dev
pnpm build-storybook # static build
```

Stories: connecting, not-installed, launch, launch-error, starting, running
(the running story mounts a mock editor instead of a live server).
13 changes: 13 additions & 0 deletions plugins/code-server/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 { createCodeServerCli } from './dist/cli.mjs'

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

main().catch((error) => {
console.error(error)
process.exit(1)
})
75 changes: 75 additions & 0 deletions plugins/code-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@devframes/plugin-code-server",
"type": "module",
"version": "0.5.2",
"description": "Run code-server (VS Code in the browser) as a devframe panel — detect a local install, launch it on demand, and embed the editor in an auto-authenticated iframe.",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"homepage": "https://github.com/devframes/devframe#readme",
"repository": {
"directory": "plugins/code-server",
"type": "git",
"url": "git+https://github.com/devframes/devframe.git"
},
"bugs": "https://github.com/devframes/devframe/issues",
"keywords": [
"devframe",
"devframe-plugin",
"devtools",
"code-server",
"vscode"
],
"sideEffects": false,
"exports": {
".": "./dist/index.mjs",
"./client": "./dist/client/index.mjs",
"./cli": "./dist/cli.mjs",
"./constants": "./dist/constants.mjs",
"./node": "./dist/node/index.mjs",
"./rpc": "./dist/rpc/index.mjs",
"./types": "./dist/types.mjs",
"./vite": "./dist/vite.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"bin": {
"devframe-code-server": "./bin.mjs"
},
"files": [
"bin.mjs",
"dist"
],
"scripts": {
"build": "tsdown && vite build --config src/spa/vite.config.ts",
"watch": "tsdown --watch",
"dev": "vite --config src/spa/vite.config.ts --host 0.0.0.0",
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
"build-storybook": "storybook build",
"test": "vitest run",
"prepack": "pnpm run build"
},
"peerDependencies": {
"devframe": "workspace:*",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
},
"dependencies": {
"get-port-please": "catalog:deps",
"nostics": "catalog:deps"
},
"devDependencies": {
"@storybook/html-vite": "catalog:storybook",
"@types/node": "catalog:types",
"devframe": "workspace:*",
"h3": "catalog:deps",
"storybook": "catalog:storybook",
"tsdown": "catalog:build",
"vite": "catalog:build",
"vitest": "catalog:testing",
"ws": "catalog:deps"
}
}
16 changes: 16 additions & 0 deletions plugins/code-server/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { CliHandle, CreateCliOptions } from 'devframe/adapters/cli'
import type { CodeServerOptions } from './types'
import { createCli } from 'devframe/adapters/cli'
import { createCodeServerDevframe } from './index'

/**
* Build a standalone CLI for the code-server panel — `dev` / `build` / `mcp`
* subcommands, backed by {@link createCodeServerDevframe}. Used by the package
* `bin` (`devframe-code-server`).
*/
export function createCodeServerCli(
options: CodeServerOptions = {},
cliOptions: CreateCliOptions = {},
): CliHandle {
return createCli(createCodeServerDevframe(options), cliOptions)
}
97 changes: 97 additions & 0 deletions plugins/code-server/src/client/code-server.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Meta, StoryObj } from '@storybook/html-vite'
import type { CodeServerViewState } from './view'
import { createCodeServerView } from './view'
import './style.css'

// A stand-in for the real code-server iframe so the "running" story renders
// without a live server.
const MOCK_EDITOR = `data:text/html;charset=utf-8,${encodeURIComponent(`
<!doctype html><html><head><meta name="color-scheme" content="dark light" /><style>
html,body{height:100%;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#1e1e1e;color:#d4d4d4}
.bar{height:35px;background:#333;display:flex;align-items:center;padding:0 12px;font-size:12px;color:#ccc}
.body{display:flex;height:calc(100% - 35px)}
.side{width:48px;background:#333}
.main{flex:1;padding:16px;font-size:13px;line-height:1.6}
.c{color:#569cd6}.s{color:#ce9178}.f{color:#dcdcaa}
</style></head><body>
<div class="bar">code-server — mock editor (Storybook)</div>
<div class="body"><div class="side"></div><div class="main">
<div><span class="c">export function</span> <span class="f">createCodeServerDevframe</span>(<span class="c">options</span>) {</div>
<div>&nbsp;&nbsp;<span class="c">return</span> <span class="f">defineDevframe</span>({ <span class="s">id</span>, <span class="s">name</span> })</div>
<div>}</div>
</div></div>
</body></html>`)}`

function renderState(state: CodeServerViewState): HTMLElement {
const container = document.createElement('div')
container.style.cssText = 'position:relative;width:100%;height:100vh'
const view = createCodeServerView(container, {
actions: {
launch: () => console.warn('[story] launch'),
recheck: () => console.warn('[story] recheck'),
},
// Keep stories hermetic: never touch real cookies or hosts.
resolveEditorUrl: () => MOCK_EDITOR,
applyAuth: () => {},
})
view.update(state)
return container
}

const meta: Meta = {
title: 'Code Server/Launcher',
parameters: { layout: 'fullscreen' },
}

export default meta
type Story = StoryObj

/** Awaiting the devframe connection / first status. */
export const Connecting: Story = {
render: () => renderState({
detection: { checked: false, installed: false, bin: 'code-server' },
server: { status: 'stopped' },
}),
}

/** Binary missing — install instructions and links. */
export const NotInstalled: Story = {
render: () => renderState({
detection: { checked: true, installed: false, bin: 'code-server' },
server: { status: 'stopped' },
}),
}

/** Installed and idle — the launch screen. */
export const Launch: Story = {
render: () => renderState({
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
server: { status: 'stopped' },
}),
}

/** A previous launch failed — the error surfaces above the launch button. */
export const LaunchError: Story = {
render: () => renderState({
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
server: { status: 'error', error: 'Failed to spawn code-server: EADDRINUSE 127.0.0.1:8080' },
}),
}

/** Spawned, waiting on the readiness probe (and the auth handoff). */
export const Starting: Story = {
render: () => renderState({
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
server: { status: 'starting', port: 8080 },
busy: true,
}),
}

/** Ready — the editor fills the panel, signed in, with no chrome over it. */
export const Running: Story = {
render: () => renderState({
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
server: { status: 'running', port: 8080, pid: 4242 },
auth: { cookieName: 'code-server-session', cookieValue: 'a'.repeat(64) },
}),
}
Loading
Loading