@devframes/plugin-code-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +84 -0
- package/bin.mjs +13 -0
- package/dist/cli.d.mts +12 -0
- package/dist/cli.mjs +13 -0
- package/dist/client/index.d.mts +75 -0
- package/dist/client/index.mjs +354 -0
- package/dist/constants-ICLvdiRe.d.mts +36 -0
- package/dist/constants.d.mts +2 -0
- package/dist/constants.mjs +38 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.mjs +3 -0
- package/dist/node/index.d.mts +165 -0
- package/dist/node/index.mjs +491 -0
- package/dist/rpc/index.d.mts +71 -0
- package/dist/rpc/index.mjs +2 -0
- package/dist/rpc-njPEEDSx.mjs +96 -0
- package/dist/spa/assets/index-D5nEkfpr.js +2 -0
- package/dist/spa/assets/index-DzYBAZJX.css +1 -0
- package/dist/spa/index.html +14 -0
- package/dist/src-BDRH3xta.mjs +58 -0
- package/dist/types.d.mts +99 -0
- package/dist/types.mjs +1 -0
- package/dist/vite.d.mts +18 -0
- package/dist/vite.mjs +20 -0
- package/package.json +80 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @devframes/plugin-code-server
|
|
2
|
+
|
|
3
|
+
> [!WARNING] Experimental
|
|
4
|
+
> This plugin is experimental and may change without a major version bump until
|
|
5
|
+
> it stabilizes.
|
|
6
|
+
|
|
7
|
+
Run [code-server](https://github.com/coder/code-server) (VS Code in the
|
|
8
|
+
browser) as a devframe panel. The plugin detects a local `code-server`
|
|
9
|
+
install, launches it on demand, and embeds the editor in an
|
|
10
|
+
auto-authenticated `<iframe>`.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
- **Detection** — on startup it runs `code-server --version`. When the binary
|
|
15
|
+
is missing, the launcher renders install instructions and links instead of a
|
|
16
|
+
launch button.
|
|
17
|
+
- **Launch** — the launcher's button starts code-server as a managed child
|
|
18
|
+
process bound to a free port, scoped to the workspace. Readiness is probed
|
|
19
|
+
via code-server's `/healthz` endpoint.
|
|
20
|
+
- **Auto-auth** — code-server runs with password auth. The plugin generates a
|
|
21
|
+
random token, sets `HASHED_PASSWORD` to its SHA-256, and hands the matching
|
|
22
|
+
session cookie back to the already-authorized devframe client. The launcher
|
|
23
|
+
applies that cookie for the current host before loading the iframe, so the
|
|
24
|
+
editor opens already signed in — no code-server login page.
|
|
25
|
+
|
|
26
|
+
The editor iframe points at code-server's own origin
|
|
27
|
+
(`<protocol>//<host>:<port>/`), so WebSocket traffic flows directly without a
|
|
28
|
+
reverse proxy.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Standalone CLI
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx @devframes/plugin-code-server # dev server + launcher
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Programmatic
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createCodeServerDevframe } from '@devframes/plugin-code-server'
|
|
42
|
+
|
|
43
|
+
export default createCodeServerDevframe({
|
|
44
|
+
// bin: 'code-server', // binary to detect/launch (default: PATH)
|
|
45
|
+
// serverPort: 8080, // force a port (default: free port near 8080)
|
|
46
|
+
// args: ['--disable-getting-started-override'],
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Vite host
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { codeServerVite } from '@devframes/plugin-code-server/vite'
|
|
54
|
+
|
|
55
|
+
export default {
|
|
56
|
+
plugins: [codeServerVite()],
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## RPC
|
|
61
|
+
|
|
62
|
+
| Function | Type | Purpose |
|
|
63
|
+
|----------|------|---------|
|
|
64
|
+
| `devframes-plugin-code-server:detect` | query | Re-probe for the binary; returns `{ installed, version, bin }`. |
|
|
65
|
+
| `devframes-plugin-code-server:status` | query | Current status + auth cookie when running. |
|
|
66
|
+
| `devframes-plugin-code-server:start` | action | Launch and wait for readiness. |
|
|
67
|
+
| `devframes-plugin-code-server:stop` | action | Stop the process. |
|
|
68
|
+
|
|
69
|
+
Status (minus the auth cookie) is mirrored into the
|
|
70
|
+
`devframes-plugin-code-server:state` shared state for reactive UIs.
|
|
71
|
+
|
|
72
|
+
## UI
|
|
73
|
+
|
|
74
|
+
The launcher UI is a pure, state-driven view (`src/client/view.ts`) decoupled
|
|
75
|
+
from RPC, so every state renders in isolation. `mountCodeServer` wires the live
|
|
76
|
+
connection to it. Each UI state has a Storybook story:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pnpm storybook # dev
|
|
80
|
+
pnpm build-storybook # static build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Stories: connecting, not-installed, launch, launch-error, starting, running
|
|
84
|
+
(the running story mounts a mock editor instead of a live server).
|
package/bin.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { createCodeServerCli } from './dist/cli.mjs'
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const cli = createCodeServerCli()
|
|
7
|
+
await cli.parse()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
main().catch((error) => {
|
|
11
|
+
console.error(error)
|
|
12
|
+
process.exit(1)
|
|
13
|
+
})
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CodeServerOptions } from "./types.mjs";
|
|
2
|
+
import { CliHandle, CreateCliOptions } from "devframe/adapters/cli";
|
|
3
|
+
|
|
4
|
+
//#region src/cli.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build a standalone CLI for the code-server panel — `dev` / `build` / `mcp`
|
|
7
|
+
* subcommands, backed by {@link createCodeServerDevframe}. Used by the package
|
|
8
|
+
* `bin` (`devframe-code-server`).
|
|
9
|
+
*/
|
|
10
|
+
declare function createCodeServerCli(options?: CodeServerOptions, cliOptions?: CreateCliOptions): CliHandle;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { createCodeServerCli };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { n as createCodeServerDevframe } from "./src-BDRH3xta.mjs";
|
|
2
|
+
import { createCli } from "devframe/adapters/cli";
|
|
3
|
+
//#region src/cli.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build a standalone CLI for the code-server panel — `dev` / `build` / `mcp`
|
|
6
|
+
* subcommands, backed by {@link createCodeServerDevframe}. Used by the package
|
|
7
|
+
* `bin` (`devframe-code-server`).
|
|
8
|
+
*/
|
|
9
|
+
function createCodeServerCli(options = {}, cliOptions = {}) {
|
|
10
|
+
return createCli(createCodeServerDevframe(options), cliOptions);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { createCodeServerCli };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CodeServerAuth, CodeServerDetection, CodeServerServerInfo, CodeServerSharedState, CodeServerStatus } from "../types.mjs";
|
|
2
|
+
import { o as STATE_KEY } from "../constants-ICLvdiRe.mjs";
|
|
3
|
+
import { DevframeRpcClient } from "devframe/client";
|
|
4
|
+
|
|
5
|
+
//#region src/client/view.d.ts
|
|
6
|
+
/** The data the view renders from. Pure — no RPC, no process knowledge. */
|
|
7
|
+
interface CodeServerViewState {
|
|
8
|
+
detection: CodeServerDetection;
|
|
9
|
+
server: CodeServerServerInfo;
|
|
10
|
+
/** Auth handoff; present only once the editor is reachable. */
|
|
11
|
+
auth?: CodeServerAuth;
|
|
12
|
+
/** A request (launch / re-check) is in flight. */
|
|
13
|
+
busy?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** User intents the view surfaces. */
|
|
16
|
+
interface CodeServerViewActions {
|
|
17
|
+
launch: () => void;
|
|
18
|
+
recheck: () => void;
|
|
19
|
+
}
|
|
20
|
+
interface CodeServerViewOptions {
|
|
21
|
+
actions?: Partial<CodeServerViewActions>;
|
|
22
|
+
/**
|
|
23
|
+
* Build the editor iframe URL from the running port. Defaults to the
|
|
24
|
+
* current page host so the cookie set on this origin reaches the iframe.
|
|
25
|
+
*/
|
|
26
|
+
resolveEditorUrl?: (port: number) => string;
|
|
27
|
+
/**
|
|
28
|
+
* Apply code-server's session cookie before the iframe loads. Defaults to
|
|
29
|
+
* `document.cookie`. Storybook overrides this to a no-op.
|
|
30
|
+
*/
|
|
31
|
+
applyAuth?: (auth: CodeServerAuth) => void;
|
|
32
|
+
}
|
|
33
|
+
/** Discrete UI states, one per Storybook story. */
|
|
34
|
+
type CodeServerPhase = 'connecting' | 'not-installed' | 'launch' | 'starting' | 'running';
|
|
35
|
+
interface CodeServerViewHandle {
|
|
36
|
+
/** Re-render from a new state (cheap; reuses the live iframe when unchanged). */
|
|
37
|
+
update: (state: CodeServerViewState) => void;
|
|
38
|
+
dispose: () => void;
|
|
39
|
+
}
|
|
40
|
+
/** Map a state to its discrete UI phase. */
|
|
41
|
+
declare function resolvePhase(state: CodeServerViewState): CodeServerPhase;
|
|
42
|
+
/**
|
|
43
|
+
* Create the code-server launcher view inside `container`. The view is a pure
|
|
44
|
+
* function of {@link CodeServerViewState}: callers (the SPA via
|
|
45
|
+
* `mountCodeServer`, or Storybook) push state with `update()` and wire intent
|
|
46
|
+
* through `options.actions`. It performs no RPC and owns no process state, so
|
|
47
|
+
* every UI phase can be rendered in isolation.
|
|
48
|
+
*/
|
|
49
|
+
declare function createCodeServerView(container: HTMLElement, options?: CodeServerViewOptions): CodeServerViewHandle;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/client/index.d.ts
|
|
52
|
+
interface MountCodeServerOptions {
|
|
53
|
+
/** Pre-connected client. When omitted, `connectDevframe()` is awaited. */
|
|
54
|
+
rpc?: DevframeRpcClient;
|
|
55
|
+
}
|
|
56
|
+
interface CodeServerHandle {
|
|
57
|
+
rpc: DevframeRpcClient;
|
|
58
|
+
/** Stop the code-server process (no UI control is rendered over the editor). */
|
|
59
|
+
stop: () => Promise<void>;
|
|
60
|
+
dispose: () => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Mount the code-server launcher into `container`. Connects to the devframe
|
|
64
|
+
* backend, then drives a {@link createCodeServerView} from the live
|
|
65
|
+
* detection/server state: install instructions when the binary is missing, a
|
|
66
|
+
* launch screen when stopped, a progress state while starting, and the editor
|
|
67
|
+
* in a full-bleed, auto-authenticated iframe once running.
|
|
68
|
+
*
|
|
69
|
+
* The connection is assumed already authorized with devframe's auth, so the
|
|
70
|
+
* server-issued session cookie is applied transparently before the iframe
|
|
71
|
+
* loads — the user never sees code-server's login page.
|
|
72
|
+
*/
|
|
73
|
+
declare function mountCodeServer(container: HTMLElement, options?: MountCodeServerOptions): Promise<CodeServerHandle>;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { CodeServerHandle, type CodeServerPhase, type CodeServerServerInfo, type CodeServerSharedState, type CodeServerStatus, type CodeServerViewActions, type CodeServerViewHandle, type CodeServerViewOptions, type CodeServerViewState, MountCodeServerOptions, STATE_KEY, createCodeServerView, mountCodeServer, resolvePhase };
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { connectDevframe } from "devframe/client";
|
|
2
|
+
import { createIframePanes } from "iframe-pane";
|
|
3
|
+
//#region src/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* Shared-state key holding the serializable, secret-free server status and
|
|
6
|
+
* detection result. The authentication cookie is never published here — it
|
|
7
|
+
* is returned only from the `start` / `status` RPCs to the already-authorized
|
|
8
|
+
* client (see {@link CodeServerAuth}).
|
|
9
|
+
*/
|
|
10
|
+
const STATE_KEY = "devframes-plugin-code-server:state";
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/client/design.ts
|
|
13
|
+
function cx(...parts) {
|
|
14
|
+
return parts.filter(Boolean).join(" ");
|
|
15
|
+
}
|
|
16
|
+
function button({ variant = "primary", size = "md", class: extra } = {}) {
|
|
17
|
+
const variantClass = {
|
|
18
|
+
primary: "btn-primary",
|
|
19
|
+
secondary: "btn-action",
|
|
20
|
+
outline: "btn-action",
|
|
21
|
+
ghost: "inline-flex items-center justify-center gap-1.5 rounded px2 py1 op75 hover:op100 hover:bg-active transition disabled:pointer-events-none disabled:op30!",
|
|
22
|
+
destructive: "btn-action text-error border-error/30!",
|
|
23
|
+
link: "inline-flex items-center gap-1.5 color-active hover:underline underline-offset-2"
|
|
24
|
+
};
|
|
25
|
+
const sizeClass = size === "sm" ? variant === "primary" ? "text-sm px-2.5! py-1!" : "text-sm" : size === "lg" ? "text-base px-4! py-2!" : "";
|
|
26
|
+
return cx(variantClass[variant], sizeClass, extra);
|
|
27
|
+
}
|
|
28
|
+
function link(extra) {
|
|
29
|
+
return cx("color-active hover:underline underline-offset-2", extra);
|
|
30
|
+
}
|
|
31
|
+
function spinner(extra) {
|
|
32
|
+
return cx("inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin", extra);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/client/view.ts
|
|
36
|
+
const DOCS_URL = "https://coder.com/docs/code-server/latest/install";
|
|
37
|
+
const REPO_URL = "https://github.com/coder/code-server";
|
|
38
|
+
const panes = createIframePanes();
|
|
39
|
+
let paneCounter = 0;
|
|
40
|
+
const INSTALL_COMMANDS = [
|
|
41
|
+
{
|
|
42
|
+
label: "Install script (Linux / macOS)",
|
|
43
|
+
command: "curl -fsSL https://code-server.dev/install.sh | sh"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "npm",
|
|
47
|
+
command: "npm install -g code-server"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Homebrew",
|
|
51
|
+
command: "brew install code-server"
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
/** Map a state to its discrete UI phase. */
|
|
55
|
+
function resolvePhase(state) {
|
|
56
|
+
const { detection, server, auth, busy } = state;
|
|
57
|
+
if (server.status === "running" && server.port != null) return auth ? "running" : "starting";
|
|
58
|
+
if (server.status === "starting" || busy && server.status !== "error") return "starting";
|
|
59
|
+
if (!detection.checked) return "connecting";
|
|
60
|
+
if (!detection.installed) return "not-installed";
|
|
61
|
+
return "launch";
|
|
62
|
+
}
|
|
63
|
+
function el(tag, className, text) {
|
|
64
|
+
const node = document.createElement(tag);
|
|
65
|
+
if (className) node.className = className;
|
|
66
|
+
if (text != null) node.textContent = text;
|
|
67
|
+
return node;
|
|
68
|
+
}
|
|
69
|
+
/** A Phosphor icon (UnoCSS presetIcons), matching the other devframe plugins. */
|
|
70
|
+
function icon(name, cls = "") {
|
|
71
|
+
return el("span", cx(name, "shrink-0", cls));
|
|
72
|
+
}
|
|
73
|
+
/** The uppercase "code-server" eyebrow, prefixed with the brand icon. */
|
|
74
|
+
function eyebrow() {
|
|
75
|
+
const p = el("p", "flex items-center gap-1.5 mb-2.5 text-xs tracking-wider uppercase color-muted");
|
|
76
|
+
p.append(icon("i-ph-code-duotone", "text-sm"), document.createTextNode("code-server"));
|
|
77
|
+
return p;
|
|
78
|
+
}
|
|
79
|
+
function defaultEditorUrl(port) {
|
|
80
|
+
return `${location.protocol}//${location.hostname}:${port}/`;
|
|
81
|
+
}
|
|
82
|
+
function defaultApplyAuth(auth) {
|
|
83
|
+
document.cookie = `${auth.cookieName}=${auth.cookieValue}; path=/; SameSite=Lax`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create the code-server launcher view inside `container`. The view is a pure
|
|
87
|
+
* function of {@link CodeServerViewState}: callers (the SPA via
|
|
88
|
+
* `mountCodeServer`, or Storybook) push state with `update()` and wire intent
|
|
89
|
+
* through `options.actions`. It performs no RPC and owns no process state, so
|
|
90
|
+
* every UI phase can be rendered in isolation.
|
|
91
|
+
*/
|
|
92
|
+
function createCodeServerView(container, options = {}) {
|
|
93
|
+
const resolveEditorUrl = options.resolveEditorUrl ?? defaultEditorUrl;
|
|
94
|
+
const applyAuth = options.applyAuth ?? defaultApplyAuth;
|
|
95
|
+
const launch = () => options.actions?.launch?.();
|
|
96
|
+
const recheck = () => options.actions?.recheck?.();
|
|
97
|
+
const root = el("div", "absolute inset-0 flex flex-col of-hidden bg-base color-base font-sans");
|
|
98
|
+
container.append(root);
|
|
99
|
+
const paneId = `code-server-editor-${++paneCounter}`;
|
|
100
|
+
const editorTarget = el("div", "dcs-frame flex-1 w-full");
|
|
101
|
+
let editorUrl = null;
|
|
102
|
+
function shell(...children) {
|
|
103
|
+
panes.get(paneId)?.unmount();
|
|
104
|
+
editorUrl = null;
|
|
105
|
+
const center = el("div", "flex-1 flex items-center justify-center p-6");
|
|
106
|
+
const card = el("div", "w-full max-w-[560px]");
|
|
107
|
+
card.append(...children);
|
|
108
|
+
center.append(card);
|
|
109
|
+
root.replaceChildren(center);
|
|
110
|
+
}
|
|
111
|
+
function renderConnecting() {
|
|
112
|
+
const wrap = el("div", "flex items-center gap-2.5");
|
|
113
|
+
wrap.append(el("div", spinner("color-muted")), el("span", "text-sm color-muted", "Connecting to devframe…"));
|
|
114
|
+
shell(wrap);
|
|
115
|
+
}
|
|
116
|
+
function renderNotInstalled(busy) {
|
|
117
|
+
const nodes = [];
|
|
118
|
+
nodes.push(eyebrow());
|
|
119
|
+
nodes.push(el("h1", "mb-2 text-[1.35rem] font-semibold", "code-server is not installed"));
|
|
120
|
+
nodes.push(el("p", "mb-5 text-sm leading-relaxed color-muted", "Install code-server (VS Code in the browser) to open the editor here. Pick whichever fits your setup, then re-check."));
|
|
121
|
+
const install = el("div", "flex flex-col gap-3 mb-5");
|
|
122
|
+
for (const { label, command } of INSTALL_COMMANDS) {
|
|
123
|
+
const row = el("div");
|
|
124
|
+
row.append(el("label", "block mb-1 text-xs color-muted", label));
|
|
125
|
+
const cmd = el("div", "flex items-center gap-2 px-2.5 py-2 rounded-md border border-base bg-secondary");
|
|
126
|
+
cmd.append(el("code", "flex-1 of-x-auto whitespace-nowrap font-mono text-xs color-base", command));
|
|
127
|
+
const copy = el("button", button({
|
|
128
|
+
variant: "outline",
|
|
129
|
+
size: "sm",
|
|
130
|
+
class: "shrink-0 text-[11px]"
|
|
131
|
+
}), "Copy");
|
|
132
|
+
copy.onclick = () => {
|
|
133
|
+
navigator.clipboard?.writeText(command).then(() => {
|
|
134
|
+
copy.textContent = "Copied";
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
copy.textContent = "Copy";
|
|
137
|
+
}, 1200);
|
|
138
|
+
}).catch(() => {});
|
|
139
|
+
};
|
|
140
|
+
cmd.append(copy);
|
|
141
|
+
row.append(cmd);
|
|
142
|
+
install.append(row);
|
|
143
|
+
}
|
|
144
|
+
nodes.push(install);
|
|
145
|
+
const actions = el("div", "flex flex-wrap items-center gap-2.5");
|
|
146
|
+
const recheckBtn = el("button", button({ variant: "primary" }));
|
|
147
|
+
recheckBtn.append(icon("i-ph-arrows-clockwise", cx("size-4", busy && "animate-spin")), document.createTextNode(busy ? "Checking…" : "Re-check"));
|
|
148
|
+
recheckBtn.disabled = busy;
|
|
149
|
+
recheckBtn.onclick = recheck;
|
|
150
|
+
actions.append(recheckBtn);
|
|
151
|
+
actions.append(link$1("Installation docs", DOCS_URL));
|
|
152
|
+
actions.append(link$1("GitHub", REPO_URL));
|
|
153
|
+
nodes.push(actions);
|
|
154
|
+
shell(...nodes);
|
|
155
|
+
}
|
|
156
|
+
function renderLaunch(state) {
|
|
157
|
+
const { detection, server, busy } = state;
|
|
158
|
+
const nodes = [];
|
|
159
|
+
nodes.push(eyebrow());
|
|
160
|
+
nodes.push(el("h1", "mb-2 text-[1.35rem] font-semibold", "Launch the editor"));
|
|
161
|
+
nodes.push(el("p", "mb-5 text-sm leading-relaxed color-muted", "Start a code-server instance scoped to this workspace and open VS Code right here. The server runs with a generated password and the editor is signed in automatically."));
|
|
162
|
+
if (server.status === "error" && server.error) nodes.push(el("div", "mb-4 px-3 py-2.5 rounded-md border border-error/35 bg-error/10 text-error text-sm whitespace-pre-wrap break-words", server.error));
|
|
163
|
+
const meta = el("div", "flex flex-wrap gap-x-4 gap-y-1.5 mb-5 text-xs color-muted");
|
|
164
|
+
if (detection.version) {
|
|
165
|
+
const v = el("span");
|
|
166
|
+
v.append(document.createTextNode("version "), el("code", "font-mono color-base", detection.version));
|
|
167
|
+
meta.append(v);
|
|
168
|
+
}
|
|
169
|
+
const b = el("span");
|
|
170
|
+
b.append(document.createTextNode("binary "), el("code", "font-mono color-base", detection.bin));
|
|
171
|
+
meta.append(b);
|
|
172
|
+
nodes.push(meta);
|
|
173
|
+
const actions = el("div", "flex flex-wrap items-center gap-2.5");
|
|
174
|
+
const launchBtn = el("button", button({ variant: "primary" }));
|
|
175
|
+
if (busy) launchBtn.append(el("span", spinner()), document.createTextNode("Starting…"));
|
|
176
|
+
else launchBtn.append(icon("i-ph-rocket-launch-duotone", "size-4"), document.createTextNode("Launch code-server"));
|
|
177
|
+
launchBtn.disabled = busy ?? false;
|
|
178
|
+
launchBtn.onclick = launch;
|
|
179
|
+
actions.append(launchBtn);
|
|
180
|
+
nodes.push(actions);
|
|
181
|
+
shell(...nodes);
|
|
182
|
+
}
|
|
183
|
+
function renderStarting() {
|
|
184
|
+
const wrap = el("div", "flex items-center gap-2.5");
|
|
185
|
+
wrap.append(el("div", spinner("color-muted")), el("span", "text-sm color-muted", "Starting code-server…"));
|
|
186
|
+
shell(wrap);
|
|
187
|
+
}
|
|
188
|
+
function renderRunning(state) {
|
|
189
|
+
const url = resolveEditorUrl(state.server.port);
|
|
190
|
+
if (editorUrl !== url) {
|
|
191
|
+
if (state.auth) applyAuth(state.auth);
|
|
192
|
+
editorUrl = url;
|
|
193
|
+
}
|
|
194
|
+
const pane = panes.ensure(paneId, {
|
|
195
|
+
src: url,
|
|
196
|
+
attrs: { allow: "clipboard-read; clipboard-write; cross-origin-isolated" },
|
|
197
|
+
style: { border: "0" }
|
|
198
|
+
});
|
|
199
|
+
if (pane.iframe.src !== url) pane.iframe.src = url;
|
|
200
|
+
if (root.firstElementChild !== editorTarget) root.replaceChildren(editorTarget);
|
|
201
|
+
pane.mount(editorTarget);
|
|
202
|
+
}
|
|
203
|
+
function link$1(text, href) {
|
|
204
|
+
const a = el("a", link("inline-flex items-center gap-1 text-sm"));
|
|
205
|
+
a.append(document.createTextNode(text), icon("i-ph-arrow-square-out", "size-3.5"));
|
|
206
|
+
a.href = href;
|
|
207
|
+
a.target = "_blank";
|
|
208
|
+
a.rel = "noreferrer";
|
|
209
|
+
return a;
|
|
210
|
+
}
|
|
211
|
+
function update(state) {
|
|
212
|
+
switch (resolvePhase(state)) {
|
|
213
|
+
case "running":
|
|
214
|
+
renderRunning(state);
|
|
215
|
+
break;
|
|
216
|
+
case "starting":
|
|
217
|
+
renderStarting();
|
|
218
|
+
break;
|
|
219
|
+
case "connecting":
|
|
220
|
+
renderConnecting();
|
|
221
|
+
break;
|
|
222
|
+
case "not-installed":
|
|
223
|
+
renderNotInstalled(state.busy ?? false);
|
|
224
|
+
break;
|
|
225
|
+
case "launch":
|
|
226
|
+
renderLaunch(state);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
update,
|
|
232
|
+
dispose() {
|
|
233
|
+
panes.get(paneId)?.dispose();
|
|
234
|
+
root.remove();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/client/index.ts
|
|
240
|
+
/**
|
|
241
|
+
* Mount the code-server launcher into `container`. Connects to the devframe
|
|
242
|
+
* backend, then drives a {@link createCodeServerView} from the live
|
|
243
|
+
* detection/server state: install instructions when the binary is missing, a
|
|
244
|
+
* launch screen when stopped, a progress state while starting, and the editor
|
|
245
|
+
* in a full-bleed, auto-authenticated iframe once running.
|
|
246
|
+
*
|
|
247
|
+
* The connection is assumed already authorized with devframe's auth, so the
|
|
248
|
+
* server-issued session cookie is applied transparently before the iframe
|
|
249
|
+
* loads — the user never sees code-server's login page.
|
|
250
|
+
*/
|
|
251
|
+
async function mountCodeServer(container, options = {}) {
|
|
252
|
+
const rpc = options.rpc ?? await connectDevframe();
|
|
253
|
+
if (rpc.connectionMeta.backend === "websocket") await rpc.ensureTrusted(5e3).catch(() => {});
|
|
254
|
+
let detection = {
|
|
255
|
+
checked: false,
|
|
256
|
+
installed: false,
|
|
257
|
+
bin: "code-server"
|
|
258
|
+
};
|
|
259
|
+
let server = { status: "stopped" };
|
|
260
|
+
let auth;
|
|
261
|
+
let busy = false;
|
|
262
|
+
let disposed = false;
|
|
263
|
+
const view = createCodeServerView(container, { actions: {
|
|
264
|
+
launch,
|
|
265
|
+
recheck
|
|
266
|
+
} });
|
|
267
|
+
function sync() {
|
|
268
|
+
if (disposed) return;
|
|
269
|
+
view.update({
|
|
270
|
+
detection,
|
|
271
|
+
server,
|
|
272
|
+
auth,
|
|
273
|
+
busy
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function applyResult(result) {
|
|
277
|
+
detection = result.detection;
|
|
278
|
+
server = result.server;
|
|
279
|
+
if (result.auth) auth = result.auth;
|
|
280
|
+
}
|
|
281
|
+
async function call(method, ...args) {
|
|
282
|
+
try {
|
|
283
|
+
return await rpc.call(method, ...args);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
server = {
|
|
286
|
+
status: "error",
|
|
287
|
+
error: error instanceof Error ? error.message : String(error)
|
|
288
|
+
};
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function launch() {
|
|
293
|
+
if (busy) return;
|
|
294
|
+
busy = true;
|
|
295
|
+
sync();
|
|
296
|
+
const result = await call("devframes-plugin-code-server:start", {});
|
|
297
|
+
if (result) applyResult(result);
|
|
298
|
+
busy = false;
|
|
299
|
+
sync();
|
|
300
|
+
}
|
|
301
|
+
async function stop() {
|
|
302
|
+
if (busy) return;
|
|
303
|
+
busy = true;
|
|
304
|
+
sync();
|
|
305
|
+
const result = await call("devframes-plugin-code-server:stop");
|
|
306
|
+
if (result) applyResult(result);
|
|
307
|
+
auth = void 0;
|
|
308
|
+
busy = false;
|
|
309
|
+
sync();
|
|
310
|
+
}
|
|
311
|
+
async function recheck() {
|
|
312
|
+
if (busy) return;
|
|
313
|
+
busy = true;
|
|
314
|
+
sync();
|
|
315
|
+
await call("devframes-plugin-code-server:detect");
|
|
316
|
+
busy = false;
|
|
317
|
+
sync();
|
|
318
|
+
}
|
|
319
|
+
sync();
|
|
320
|
+
const initial = await call("devframes-plugin-code-server:status");
|
|
321
|
+
if (initial) applyResult(initial);
|
|
322
|
+
sync();
|
|
323
|
+
const state = await rpc.sharedState.get(STATE_KEY, { initialValue: {
|
|
324
|
+
detection,
|
|
325
|
+
server
|
|
326
|
+
} });
|
|
327
|
+
const current = state.value();
|
|
328
|
+
detection = current.detection ?? detection;
|
|
329
|
+
server = current.server ?? server;
|
|
330
|
+
const off = state.on("updated", (full) => {
|
|
331
|
+
detection = full.detection ?? detection;
|
|
332
|
+
server = full.server ?? server;
|
|
333
|
+
if (server.status === "running" && !auth) {
|
|
334
|
+
call("devframes-plugin-code-server:status").then((result) => {
|
|
335
|
+
if (result?.auth) auth = result.auth;
|
|
336
|
+
sync();
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
sync();
|
|
341
|
+
});
|
|
342
|
+
sync();
|
|
343
|
+
return {
|
|
344
|
+
rpc,
|
|
345
|
+
stop,
|
|
346
|
+
dispose() {
|
|
347
|
+
disposed = true;
|
|
348
|
+
off?.();
|
|
349
|
+
view.dispose();
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
export { STATE_KEY, createCodeServerView, mountCodeServer, resolvePhase };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/constants.d.ts
|
|
2
|
+
/** Stable devframe id for the code-server plugin. */
|
|
3
|
+
declare const PLUGIN_ID = "devframes-plugin-code-server";
|
|
4
|
+
/**
|
|
5
|
+
* Shared-state key holding the serializable, secret-free server status and
|
|
6
|
+
* detection result. The authentication cookie is never published here — it
|
|
7
|
+
* is returned only from the `start` / `status` RPCs to the already-authorized
|
|
8
|
+
* client (see {@link CodeServerAuth}).
|
|
9
|
+
*/
|
|
10
|
+
declare const STATE_KEY = "devframes-plugin-code-server:state";
|
|
11
|
+
/** Default dev-server port for the plugin's own launcher SPA (standalone CLI). */
|
|
12
|
+
declare const DEFAULT_PORT = 9013;
|
|
13
|
+
/** Preferred port for the spawned code-server process (falls back if taken). */
|
|
14
|
+
declare const DEFAULT_CODE_SERVER_PORT = 8080;
|
|
15
|
+
/** How long to wait for code-server to answer its `/healthz` probe. */
|
|
16
|
+
declare const DEFAULT_START_TIMEOUT = 30000;
|
|
17
|
+
/** code-server's session cookie base name (see `getCookieSessionName`). */
|
|
18
|
+
declare const SESSION_COOKIE_BASE = "code-server-session";
|
|
19
|
+
/**
|
|
20
|
+
* Title for the read-only terminal session surfaced when the plugin is mounted
|
|
21
|
+
* in a hub (`ctx.terminals`). Mirrors the plugin's own name.
|
|
22
|
+
*/
|
|
23
|
+
declare const TERMINAL_SESSION_TITLE = "Code Server";
|
|
24
|
+
/**
|
|
25
|
+
* Icon for that terminal session. Mirrors the plugin's declared icon so the
|
|
26
|
+
* hub's terminals panel shows the same glyph as the rest of the plugin.
|
|
27
|
+
*/
|
|
28
|
+
declare const TERMINAL_SESSION_ICON = "ph:code-duotone";
|
|
29
|
+
/**
|
|
30
|
+
* Compute code-server's session cookie name for an optional `--cookie-suffix`.
|
|
31
|
+
* Mirrors code-server's own `getCookieSessionName` so the client sets the
|
|
32
|
+
* exact cookie the server reads.
|
|
33
|
+
*/
|
|
34
|
+
declare function getCookieSessionName(suffix?: string): string;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { SESSION_COOKIE_BASE as a, TERMINAL_SESSION_TITLE as c, PLUGIN_ID as i, getCookieSessionName as l, DEFAULT_PORT as n, STATE_KEY as o, DEFAULT_START_TIMEOUT as r, TERMINAL_SESSION_ICON as s, DEFAULT_CODE_SERVER_PORT as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as SESSION_COOKIE_BASE, c as TERMINAL_SESSION_TITLE, i as PLUGIN_ID, l as getCookieSessionName, n as DEFAULT_PORT, o as STATE_KEY, r as DEFAULT_START_TIMEOUT, s as TERMINAL_SESSION_ICON, t as DEFAULT_CODE_SERVER_PORT } from "./constants-ICLvdiRe.mjs";
|
|
2
|
+
export { DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, DEFAULT_START_TIMEOUT, PLUGIN_ID, SESSION_COOKIE_BASE, STATE_KEY, TERMINAL_SESSION_ICON, TERMINAL_SESSION_TITLE, getCookieSessionName };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
/** Stable devframe id for the code-server plugin. */
|
|
3
|
+
const PLUGIN_ID = "devframes-plugin-code-server";
|
|
4
|
+
/**
|
|
5
|
+
* Shared-state key holding the serializable, secret-free server status and
|
|
6
|
+
* detection result. The authentication cookie is never published here — it
|
|
7
|
+
* is returned only from the `start` / `status` RPCs to the already-authorized
|
|
8
|
+
* client (see {@link CodeServerAuth}).
|
|
9
|
+
*/
|
|
10
|
+
const STATE_KEY = "devframes-plugin-code-server:state";
|
|
11
|
+
/** Default dev-server port for the plugin's own launcher SPA (standalone CLI). */
|
|
12
|
+
const DEFAULT_PORT = 9013;
|
|
13
|
+
/** Preferred port for the spawned code-server process (falls back if taken). */
|
|
14
|
+
const DEFAULT_CODE_SERVER_PORT = 8080;
|
|
15
|
+
/** How long to wait for code-server to answer its `/healthz` probe. */
|
|
16
|
+
const DEFAULT_START_TIMEOUT = 3e4;
|
|
17
|
+
/** code-server's session cookie base name (see `getCookieSessionName`). */
|
|
18
|
+
const SESSION_COOKIE_BASE = "code-server-session";
|
|
19
|
+
/**
|
|
20
|
+
* Title for the read-only terminal session surfaced when the plugin is mounted
|
|
21
|
+
* in a hub (`ctx.terminals`). Mirrors the plugin's own name.
|
|
22
|
+
*/
|
|
23
|
+
const TERMINAL_SESSION_TITLE = "Code Server";
|
|
24
|
+
/**
|
|
25
|
+
* Icon for that terminal session. Mirrors the plugin's declared icon so the
|
|
26
|
+
* hub's terminals panel shows the same glyph as the rest of the plugin.
|
|
27
|
+
*/
|
|
28
|
+
const TERMINAL_SESSION_ICON = "ph:code-duotone";
|
|
29
|
+
/**
|
|
30
|
+
* Compute code-server's session cookie name for an optional `--cookie-suffix`.
|
|
31
|
+
* Mirrors code-server's own `getCookieSessionName` so the client sets the
|
|
32
|
+
* exact cookie the server reads.
|
|
33
|
+
*/
|
|
34
|
+
function getCookieSessionName(suffix) {
|
|
35
|
+
return suffix ? `${SESSION_COOKIE_BASE}-${suffix.replace(/[^a-z0-9-]/gi, "-")}` : SESSION_COOKIE_BASE;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, DEFAULT_START_TIMEOUT, PLUGIN_ID, SESSION_COOKIE_BASE, STATE_KEY, TERMINAL_SESSION_ICON, TERMINAL_SESSION_TITLE, getCookieSessionName };
|