@devframes/plugin-code-server 0.7.5 → 0.7.7
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/README.md +40 -27
- package/dist/cli.mjs +1 -1
- package/dist/client/index.d.mts +8 -70
- package/dist/client/index.mjs +9 -433
- package/dist/{constants-DZKOXnxd.d.mts → constants-DtQz38ex.d.mts} +5 -5
- package/dist/constants.d.mts +1 -1
- package/dist/constants.mjs +5 -5
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/node/index.d.mts +122 -27
- package/dist/node/index.mjs +358 -90
- package/dist/rpc/index.mjs +1 -1
- package/dist/{rpc-DjerPudu.mjs → rpc-BdNzxqDi.mjs} +10 -6
- package/dist/spa/assets/index-CT2PEkxJ.js +2 -0
- package/dist/spa/assets/index-qPxwYWWC.css +1 -0
- package/dist/spa/index.html +2 -2
- package/dist/{src-CjjdHGFH.mjs → src-BrJNMY5c.mjs} +2 -2
- package/dist/types.d.mts +114 -33
- package/dist/vite.mjs +1 -1
- package/package.json +11 -9
- package/dist/spa/assets/index-CkuBbEzE.js +0 -2
- package/dist/spa/assets/index-YTtYo-AL.css +0 -1
package/README.md
CHANGED
|
@@ -4,28 +4,37 @@
|
|
|
4
4
|
> This plugin is experimental and may change without a major version bump until
|
|
5
5
|
> it stabilizes.
|
|
6
6
|
|
|
7
|
-
Run
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Run VS Code in the browser as a devframe panel. The plugin detects a local
|
|
8
|
+
editor binary, launches it on demand, and embeds the editor in an
|
|
9
|
+
auto-authenticated `<iframe>`. The launcher is a **Vue** SPA built on the shared
|
|
10
|
+
`@antfu/design` system.
|
|
11
11
|
|
|
12
12
|
## How it works
|
|
13
13
|
|
|
14
|
-
- **Detection** — on startup it
|
|
15
|
-
is
|
|
14
|
+
- **Detection** — on startup it probes the resolved binary with `--version`.
|
|
15
|
+
When none is found, the launcher renders install instructions instead of a
|
|
16
16
|
launch button.
|
|
17
|
-
- **Launch** — the launcher's button starts
|
|
17
|
+
- **Launch** — the launcher's button starts the editor as a managed child
|
|
18
18
|
process bound to a free port, scoped to the workspace. Readiness is probed
|
|
19
|
-
|
|
20
|
-
- **Auto-auth** —
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
editor opens already signed in — no code-server login page.
|
|
19
|
+
before the iframe loads.
|
|
20
|
+
- **Auto-auth** — the plugin generates fresh auth material per launch and hands
|
|
21
|
+
it to the already-authorized devframe client, so the editor opens already
|
|
22
|
+
signed in. `code-server` uses a session cookie (`HASHED_PASSWORD`);
|
|
23
|
+
`code serve-web` uses a connection token on the URL (`?tkn=`).
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
## Backends & modes
|
|
26
|
+
|
|
27
|
+
`mode: 'local'` (default) embeds a server on this machine; `backend` selects it,
|
|
28
|
+
or leave it unset to auto-detect (prefers `code-server`, then `code serve-web`).
|
|
29
|
+
|
|
30
|
+
| `backend` | Binary | Auth |
|
|
31
|
+
|-----------|--------|------|
|
|
32
|
+
| `'code-server'` | Coder [`code-server`](https://github.com/coder/code-server) | password + session cookie |
|
|
33
|
+
| `'ms-code-serve-web'` | Microsoft [`code serve-web`](https://code.visualstudio.com/docs/remote/vscode-server) | connection token (`?tkn=`) |
|
|
34
|
+
|
|
35
|
+
`mode: 'tunnel'` runs Microsoft's `code tunnel` and embeds the hosted
|
|
36
|
+
`vscode.dev` editor. The first launch surfaces a device-login prompt in the
|
|
37
|
+
launcher; `vscode.dev` handles authentication.
|
|
29
38
|
|
|
30
39
|
## Usage
|
|
31
40
|
|
|
@@ -41,9 +50,12 @@ npx @devframes/plugin-code-server # dev server + launcher
|
|
|
41
50
|
import { createCodeServerDevframe } from '@devframes/plugin-code-server'
|
|
42
51
|
|
|
43
52
|
export default createCodeServerDevframe({
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
53
|
+
// backend: 'code-server', // 'code-server' | 'ms-code-serve-web' (default: auto)
|
|
54
|
+
// mode: 'local', // 'local' | 'tunnel'
|
|
55
|
+
// serverPort: 8080, // force a port (default: free port near 8080)
|
|
56
|
+
// startOnBoot: true, // launch during setup instead of on demand
|
|
57
|
+
// reuseExistingServer: true, // adopt a server already answering on the port
|
|
58
|
+
// tunnel: { name: 'my-box' },
|
|
47
59
|
})
|
|
48
60
|
```
|
|
49
61
|
|
|
@@ -61,24 +73,25 @@ export default {
|
|
|
61
73
|
|
|
62
74
|
| Function | Type | Purpose |
|
|
63
75
|
|----------|------|---------|
|
|
64
|
-
| `devframes:plugin:code-server:detect` | query | Re-probe
|
|
65
|
-
| `devframes:plugin:code-server:status` | query | Current status +
|
|
76
|
+
| `devframes:plugin:code-server:detect` | query | Re-probe; returns `{ installed, version, bin, backend, mode }`. |
|
|
77
|
+
| `devframes:plugin:code-server:status` | query | Current status + connect descriptor when running. |
|
|
66
78
|
| `devframes:plugin:code-server:start` | action | Launch and wait for readiness. |
|
|
67
79
|
| `devframes:plugin:code-server:stop` | action | Stop the process. |
|
|
68
80
|
|
|
69
|
-
Status (minus the
|
|
81
|
+
Status (minus the connect descriptor) is mirrored into the
|
|
70
82
|
`devframes:plugin:code-server:state` shared state for reactive UIs.
|
|
71
83
|
|
|
72
84
|
## UI
|
|
73
85
|
|
|
74
|
-
The launcher
|
|
75
|
-
from RPC
|
|
76
|
-
|
|
86
|
+
The launcher is a Vue SPA (`src/spa`). `LauncherView.vue` is a pure,
|
|
87
|
+
state-driven view decoupled from RPC — every phase renders in isolation and has
|
|
88
|
+
a Storybook story; `App.vue` wires the live connection to it and mounts the
|
|
89
|
+
editor in a full-bleed, auto-authenticated iframe (`EditorFrame.vue`).
|
|
77
90
|
|
|
78
91
|
```sh
|
|
79
92
|
pnpm storybook # dev
|
|
80
93
|
pnpm build-storybook # static build
|
|
81
94
|
```
|
|
82
95
|
|
|
83
|
-
Stories: connecting, not-installed, launch, launch-
|
|
84
|
-
|
|
96
|
+
Stories: connecting, not-installed, launch, launch (serve-web), launch-error,
|
|
97
|
+
starting, tunnel-login, and a running editor frame (mock editor).
|
package/dist/cli.mjs
CHANGED
package/dist/client/index.d.mts
CHANGED
|
@@ -1,74 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { o as STATE_KEY } from "../constants-
|
|
3
|
-
import { DevframeRpcClient } from "devframe/client";
|
|
4
|
-
//#region src/client/view.d.ts
|
|
5
|
-
/** The data the view renders from. Pure — no RPC, no process knowledge. */
|
|
6
|
-
interface CodeServerViewState {
|
|
7
|
-
detection: CodeServerDetection;
|
|
8
|
-
server: CodeServerServerInfo;
|
|
9
|
-
/** Auth handoff; present only once the editor is reachable. */
|
|
10
|
-
auth?: CodeServerAuth;
|
|
11
|
-
/** A request (launch / re-check) is in flight. */
|
|
12
|
-
busy?: boolean;
|
|
13
|
-
}
|
|
14
|
-
/** User intents the view surfaces. */
|
|
15
|
-
interface CodeServerViewActions {
|
|
16
|
-
launch: () => void;
|
|
17
|
-
recheck: () => void;
|
|
18
|
-
}
|
|
19
|
-
interface CodeServerViewOptions {
|
|
20
|
-
actions?: Partial<CodeServerViewActions>;
|
|
21
|
-
/**
|
|
22
|
-
* Build the editor iframe URL from the running port. Defaults to the
|
|
23
|
-
* current page host so the cookie set on this origin reaches the iframe.
|
|
24
|
-
*/
|
|
25
|
-
resolveEditorUrl?: (port: number) => string;
|
|
26
|
-
/**
|
|
27
|
-
* Apply code-server's session cookie before the iframe loads. Defaults to
|
|
28
|
-
* `document.cookie`. Storybook overrides this to a no-op.
|
|
29
|
-
*/
|
|
30
|
-
applyAuth?: (auth: CodeServerAuth) => void;
|
|
31
|
-
}
|
|
32
|
-
/** Discrete UI states, one per Storybook story. */
|
|
33
|
-
type CodeServerPhase = 'connecting' | 'not-installed' | 'launch' | 'starting' | 'running';
|
|
34
|
-
interface CodeServerViewHandle {
|
|
35
|
-
/** Re-render from a new state (cheap; reuses the live iframe when unchanged). */
|
|
36
|
-
update: (state: CodeServerViewState) => void;
|
|
37
|
-
dispose: () => void;
|
|
38
|
-
}
|
|
39
|
-
/** Map a state to its discrete UI phase. */
|
|
40
|
-
declare function resolvePhase(state: CodeServerViewState): CodeServerPhase;
|
|
41
|
-
/**
|
|
42
|
-
* Create the code-server launcher view inside `container`. The view is a pure
|
|
43
|
-
* function of {@link CodeServerViewState}: callers (the SPA via
|
|
44
|
-
* `mountCodeServer`, or Storybook) push state with `update()` and wire intent
|
|
45
|
-
* through `options.actions`. It performs no RPC and owns no process state, so
|
|
46
|
-
* every UI phase can be rendered in isolation.
|
|
47
|
-
*/
|
|
48
|
-
declare function createCodeServerView(container: HTMLElement, options?: CodeServerViewOptions): CodeServerViewHandle;
|
|
49
|
-
//#endregion
|
|
1
|
+
import { CodeServerBackend, CodeServerConnect, CodeServerDetection, CodeServerLogin, CodeServerMode, CodeServerServerInfo, CodeServerSharedState, CodeServerStartResult, CodeServerStatus, CodeServerStatusResult } from "../types.mjs";
|
|
2
|
+
import { o as STATE_KEY } from "../constants-DtQz38ex.mjs";
|
|
3
|
+
import { DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientOptions } from "devframe/client";
|
|
50
4
|
//#region src/client/index.d.ts
|
|
51
|
-
interface MountCodeServerOptions {
|
|
52
|
-
/** Pre-connected client. When omitted, `connectDevframe()` is awaited. */
|
|
53
|
-
rpc?: DevframeRpcClient;
|
|
54
|
-
}
|
|
55
|
-
interface CodeServerHandle {
|
|
56
|
-
rpc: DevframeRpcClient;
|
|
57
|
-
/** Stop the code-server process (no UI control is rendered over the editor). */
|
|
58
|
-
stop: () => Promise<void>;
|
|
59
|
-
dispose: () => void;
|
|
60
|
-
}
|
|
61
5
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* launch screen when stopped, a progress state while starting, and the editor
|
|
66
|
-
* in a full-bleed, auto-authenticated iframe once running.
|
|
67
|
-
*
|
|
68
|
-
* The connection is assumed already authorized with devframe's auth, so the
|
|
69
|
-
* server-issued session cookie is applied transparently before the iframe
|
|
70
|
-
* loads — the user never sees code-server's login page.
|
|
6
|
+
* Connect to the code-server plugin's devframe backend. A thin, typed wrapper
|
|
7
|
+
* around devframe's {@link connectDevframe}; the SPA derives its base from
|
|
8
|
+
* `document.baseURI`, so no options are required in the common case.
|
|
71
9
|
*/
|
|
72
|
-
declare function
|
|
10
|
+
declare function connectCodeServer(options?: DevframeRpcClientOptions): Promise<DevframeRpcClient>;
|
|
73
11
|
//#endregion
|
|
74
|
-
export {
|
|
12
|
+
export { type CodeServerBackend, type CodeServerConnect, type CodeServerDetection, type CodeServerLogin, type CodeServerMode, type CodeServerServerInfo, type CodeServerSharedState, type CodeServerStartResult, type CodeServerStatus, type CodeServerStatusResult, type DevframeConnectionStatus, type DevframeRpcClient, STATE_KEY, connectCodeServer };
|
package/dist/client/index.mjs
CHANGED
|
@@ -1,445 +1,21 @@
|
|
|
1
1
|
import { connectDevframe } from "devframe/client";
|
|
2
|
-
import { createIframePanes } from "iframe-pane";
|
|
3
2
|
//#region src/constants.ts
|
|
4
3
|
/**
|
|
5
4
|
* Shared-state key holding the serializable, secret-free server status and
|
|
6
|
-
* detection result.
|
|
7
|
-
*
|
|
8
|
-
* client (see {@link
|
|
5
|
+
* detection result. Authentication material is never published here — it is
|
|
6
|
+
* returned only from the `start` / `status` RPCs to the already-authorized
|
|
7
|
+
* client (see {@link import('./types').CodeServerConnect}).
|
|
9
8
|
*/
|
|
10
9
|
const STATE_KEY = "devframes:plugin:code-server:state";
|
|
11
10
|
//#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/connection-state.ts
|
|
36
|
-
const COPY = {
|
|
37
|
-
connecting: {
|
|
38
|
-
icon: "i-ph-plugs-connected-duotone",
|
|
39
|
-
title: "Connecting…",
|
|
40
|
-
body: "Establishing a connection to the devframe server."
|
|
41
|
-
},
|
|
42
|
-
disconnected: {
|
|
43
|
-
icon: "i-ph-plugs-duotone",
|
|
44
|
-
title: "Disconnected",
|
|
45
|
-
body: "Lost the connection to the devframe server. Reload once it is back up."
|
|
46
|
-
},
|
|
47
|
-
unauthorized: {
|
|
48
|
-
icon: "i-ph-lock-key-duotone",
|
|
49
|
-
title: "Not authorized",
|
|
50
|
-
body: "Reopen the link printed by your dev server, then reload."
|
|
51
|
-
},
|
|
52
|
-
error: {
|
|
53
|
-
icon: "i-ph-warning-octagon-duotone",
|
|
54
|
-
title: "Connection failed",
|
|
55
|
-
body: "Could not reach the devframe server."
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* A full-container connection state, shown whenever the devframe client isn't
|
|
60
|
-
* `connected` so the launcher never sits on an unexplained blank while the
|
|
61
|
-
* socket is down. Reload is the recovery path (no auto-reconnect).
|
|
62
|
-
*/
|
|
63
|
-
function createConnectionState(container) {
|
|
64
|
-
const root = document.createElement("div");
|
|
65
|
-
root.className = "absolute inset-0 flex flex-col items-center justify-center gap-4 bg-base color-base p-8 text-center z-nav";
|
|
66
|
-
root.hidden = true;
|
|
67
|
-
container.append(root);
|
|
68
|
-
function update(status, error) {
|
|
69
|
-
if (status === "connected") {
|
|
70
|
-
root.hidden = true;
|
|
71
|
-
root.replaceChildren();
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
const copy = COPY[status];
|
|
75
|
-
root.replaceChildren();
|
|
76
|
-
const glyph = document.createElement("div");
|
|
77
|
-
glyph.className = cx(copy.icon, "text-4xl color-active");
|
|
78
|
-
root.append(glyph);
|
|
79
|
-
const text = document.createElement("div");
|
|
80
|
-
text.className = "flex flex-col gap-1";
|
|
81
|
-
const title = document.createElement("p");
|
|
82
|
-
title.className = "text-lg font-medium";
|
|
83
|
-
title.textContent = copy.title;
|
|
84
|
-
const body = document.createElement("p");
|
|
85
|
-
body.className = "text-sm color-muted max-w-sm";
|
|
86
|
-
body.textContent = copy.body;
|
|
87
|
-
text.append(title, body);
|
|
88
|
-
if (error && status === "error") {
|
|
89
|
-
const code = document.createElement("code");
|
|
90
|
-
code.className = "mt-1 max-w-sm break-words font-mono text-xs color-faint";
|
|
91
|
-
code.textContent = error;
|
|
92
|
-
text.append(code);
|
|
93
|
-
}
|
|
94
|
-
root.append(text);
|
|
95
|
-
if (status !== "connecting") {
|
|
96
|
-
const reload = document.createElement("button");
|
|
97
|
-
reload.type = "button";
|
|
98
|
-
reload.className = button({
|
|
99
|
-
variant: "primary",
|
|
100
|
-
size: "sm"
|
|
101
|
-
});
|
|
102
|
-
reload.textContent = "Reload";
|
|
103
|
-
reload.addEventListener("click", () => location.reload());
|
|
104
|
-
root.append(reload);
|
|
105
|
-
}
|
|
106
|
-
root.hidden = false;
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
update,
|
|
111
|
-
dispose() {
|
|
112
|
-
root.remove();
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/client/view.ts
|
|
118
|
-
const DOCS_URL = "https://coder.com/docs/code-server/latest/install";
|
|
119
|
-
const REPO_URL = "https://github.com/coder/code-server";
|
|
120
|
-
const panes = createIframePanes();
|
|
121
|
-
let paneCounter = 0;
|
|
122
|
-
const INSTALL_COMMANDS = [
|
|
123
|
-
{
|
|
124
|
-
label: "Install script (Linux / macOS)",
|
|
125
|
-
command: "curl -fsSL https://code-server.dev/install.sh | sh"
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
label: "npm",
|
|
129
|
-
command: "npm install -g code-server"
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
label: "Homebrew",
|
|
133
|
-
command: "brew install code-server"
|
|
134
|
-
}
|
|
135
|
-
];
|
|
136
|
-
/** Map a state to its discrete UI phase. */
|
|
137
|
-
function resolvePhase(state) {
|
|
138
|
-
const { detection, server, auth, busy } = state;
|
|
139
|
-
if (server.status === "running" && server.port != null) return auth ? "running" : "starting";
|
|
140
|
-
if (server.status === "starting" || busy && server.status !== "error") return "starting";
|
|
141
|
-
if (!detection.checked) return "connecting";
|
|
142
|
-
if (!detection.installed) return "not-installed";
|
|
143
|
-
return "launch";
|
|
144
|
-
}
|
|
145
|
-
function el(tag, className, text) {
|
|
146
|
-
const node = document.createElement(tag);
|
|
147
|
-
if (className) node.className = className;
|
|
148
|
-
if (text != null) node.textContent = text;
|
|
149
|
-
return node;
|
|
150
|
-
}
|
|
151
|
-
/** A Phosphor icon (UnoCSS presetIcons), matching the other devframe plugins. */
|
|
152
|
-
function icon(name, cls = "") {
|
|
153
|
-
return el("span", cx(name, "shrink-0", cls));
|
|
154
|
-
}
|
|
155
|
-
/** The uppercase "code-server" eyebrow, prefixed with the brand icon. */
|
|
156
|
-
function eyebrow() {
|
|
157
|
-
const p = el("p", "flex items-center gap-1.5 mb-2.5 text-xs tracking-wider uppercase color-muted");
|
|
158
|
-
p.append(icon("i-ph-code-duotone", "text-sm"), document.createTextNode("code-server"));
|
|
159
|
-
return p;
|
|
160
|
-
}
|
|
161
|
-
function defaultEditorUrl(port) {
|
|
162
|
-
return `${location.protocol}//${location.hostname}:${port}/`;
|
|
163
|
-
}
|
|
164
|
-
function defaultApplyAuth(auth) {
|
|
165
|
-
document.cookie = `${auth.cookieName}=${auth.cookieValue}; path=/; SameSite=Lax`;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Create the code-server launcher view inside `container`. The view is a pure
|
|
169
|
-
* function of {@link CodeServerViewState}: callers (the SPA via
|
|
170
|
-
* `mountCodeServer`, or Storybook) push state with `update()` and wire intent
|
|
171
|
-
* through `options.actions`. It performs no RPC and owns no process state, so
|
|
172
|
-
* every UI phase can be rendered in isolation.
|
|
173
|
-
*/
|
|
174
|
-
function createCodeServerView(container, options = {}) {
|
|
175
|
-
const resolveEditorUrl = options.resolveEditorUrl ?? defaultEditorUrl;
|
|
176
|
-
const applyAuth = options.applyAuth ?? defaultApplyAuth;
|
|
177
|
-
const launch = () => options.actions?.launch?.();
|
|
178
|
-
const recheck = () => options.actions?.recheck?.();
|
|
179
|
-
const root = el("div", "absolute inset-0 flex flex-col of-hidden bg-base color-base font-sans");
|
|
180
|
-
container.append(root);
|
|
181
|
-
const paneId = `code-server-editor-${++paneCounter}`;
|
|
182
|
-
const editorTarget = el("div", "dcs-frame flex-1 w-full");
|
|
183
|
-
let editorUrl = null;
|
|
184
|
-
function shell(...children) {
|
|
185
|
-
panes.get(paneId)?.unmount();
|
|
186
|
-
editorUrl = null;
|
|
187
|
-
const center = el("div", "flex-1 flex items-center justify-center p-6");
|
|
188
|
-
const card = el("div", "w-full max-w-[560px]");
|
|
189
|
-
card.append(...children);
|
|
190
|
-
center.append(card);
|
|
191
|
-
root.replaceChildren(center);
|
|
192
|
-
}
|
|
193
|
-
function renderConnecting() {
|
|
194
|
-
const wrap = el("div", "flex items-center gap-2.5");
|
|
195
|
-
wrap.append(el("div", spinner("color-muted")), el("span", "text-sm color-muted", "Connecting to devframe…"));
|
|
196
|
-
shell(wrap);
|
|
197
|
-
}
|
|
198
|
-
function renderNotInstalled(busy) {
|
|
199
|
-
const nodes = [];
|
|
200
|
-
nodes.push(eyebrow());
|
|
201
|
-
nodes.push(el("h1", "mb-2 text-[1.35rem] font-semibold", "code-server is not installed"));
|
|
202
|
-
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."));
|
|
203
|
-
const install = el("div", "flex flex-col gap-3 mb-5");
|
|
204
|
-
for (const { label, command } of INSTALL_COMMANDS) {
|
|
205
|
-
const row = el("div");
|
|
206
|
-
row.append(el("label", "block mb-1 text-xs color-muted", label));
|
|
207
|
-
const cmd = el("div", "flex items-center gap-2 px-2.5 py-2 rounded-md border border-base bg-secondary");
|
|
208
|
-
cmd.append(el("code", "flex-1 of-x-auto whitespace-nowrap font-mono text-xs color-base", command));
|
|
209
|
-
const copy = el("button", button({
|
|
210
|
-
variant: "outline",
|
|
211
|
-
size: "sm",
|
|
212
|
-
class: "shrink-0 text-[11px]"
|
|
213
|
-
}), "Copy");
|
|
214
|
-
copy.onclick = () => {
|
|
215
|
-
navigator.clipboard?.writeText(command).then(() => {
|
|
216
|
-
copy.textContent = "Copied";
|
|
217
|
-
setTimeout(() => {
|
|
218
|
-
copy.textContent = "Copy";
|
|
219
|
-
}, 1200);
|
|
220
|
-
}).catch(() => {});
|
|
221
|
-
};
|
|
222
|
-
cmd.append(copy);
|
|
223
|
-
row.append(cmd);
|
|
224
|
-
install.append(row);
|
|
225
|
-
}
|
|
226
|
-
nodes.push(install);
|
|
227
|
-
const actions = el("div", "flex flex-wrap items-center gap-2.5");
|
|
228
|
-
const recheckBtn = el("button", button({ variant: "primary" }));
|
|
229
|
-
recheckBtn.append(icon("i-ph-arrows-clockwise", cx("size-4", busy && "animate-spin")), document.createTextNode(busy ? "Checking…" : "Re-check"));
|
|
230
|
-
recheckBtn.disabled = busy;
|
|
231
|
-
recheckBtn.onclick = recheck;
|
|
232
|
-
actions.append(recheckBtn);
|
|
233
|
-
actions.append(link$1("Installation docs", DOCS_URL));
|
|
234
|
-
actions.append(link$1("GitHub", REPO_URL));
|
|
235
|
-
nodes.push(actions);
|
|
236
|
-
shell(...nodes);
|
|
237
|
-
}
|
|
238
|
-
function renderLaunch(state) {
|
|
239
|
-
const { detection, server, busy } = state;
|
|
240
|
-
const nodes = [];
|
|
241
|
-
nodes.push(eyebrow());
|
|
242
|
-
nodes.push(el("h1", "mb-2 text-[1.35rem] font-semibold", "Launch the editor"));
|
|
243
|
-
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."));
|
|
244
|
-
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));
|
|
245
|
-
const meta = el("div", "flex flex-wrap gap-x-4 gap-y-1.5 mb-5 text-xs color-muted");
|
|
246
|
-
if (detection.version) {
|
|
247
|
-
const v = el("span");
|
|
248
|
-
v.append(document.createTextNode("version "), el("code", "font-mono color-base", detection.version));
|
|
249
|
-
meta.append(v);
|
|
250
|
-
}
|
|
251
|
-
const b = el("span");
|
|
252
|
-
b.append(document.createTextNode("binary "), el("code", "font-mono color-base", detection.bin));
|
|
253
|
-
meta.append(b);
|
|
254
|
-
nodes.push(meta);
|
|
255
|
-
const actions = el("div", "flex flex-wrap items-center gap-2.5");
|
|
256
|
-
const launchBtn = el("button", button({ variant: "primary" }));
|
|
257
|
-
if (busy) launchBtn.append(el("span", spinner()), document.createTextNode("Starting…"));
|
|
258
|
-
else launchBtn.append(icon("i-ph-rocket-launch-duotone", "size-4"), document.createTextNode("Launch code-server"));
|
|
259
|
-
launchBtn.disabled = busy ?? false;
|
|
260
|
-
launchBtn.onclick = launch;
|
|
261
|
-
actions.append(launchBtn);
|
|
262
|
-
nodes.push(actions);
|
|
263
|
-
shell(...nodes);
|
|
264
|
-
}
|
|
265
|
-
function renderStarting() {
|
|
266
|
-
const wrap = el("div", "flex items-center gap-2.5");
|
|
267
|
-
wrap.append(el("div", spinner("color-muted")), el("span", "text-sm color-muted", "Starting code-server…"));
|
|
268
|
-
shell(wrap);
|
|
269
|
-
}
|
|
270
|
-
function renderRunning(state) {
|
|
271
|
-
const url = resolveEditorUrl(state.server.port);
|
|
272
|
-
if (editorUrl !== url) {
|
|
273
|
-
if (state.auth) applyAuth(state.auth);
|
|
274
|
-
editorUrl = url;
|
|
275
|
-
}
|
|
276
|
-
const pane = panes.ensure(paneId, {
|
|
277
|
-
src: url,
|
|
278
|
-
attrs: { allow: "clipboard-read; clipboard-write; cross-origin-isolated" },
|
|
279
|
-
style: { border: "0" }
|
|
280
|
-
});
|
|
281
|
-
if (pane.iframe.src !== url) pane.iframe.src = url;
|
|
282
|
-
if (root.firstElementChild !== editorTarget) root.replaceChildren(editorTarget);
|
|
283
|
-
pane.mount(editorTarget);
|
|
284
|
-
}
|
|
285
|
-
function link$1(text, href) {
|
|
286
|
-
const a = el("a", link("inline-flex items-center gap-1 text-sm"));
|
|
287
|
-
a.append(document.createTextNode(text), icon("i-ph-arrow-square-out", "size-3.5"));
|
|
288
|
-
a.href = href;
|
|
289
|
-
a.target = "_blank";
|
|
290
|
-
a.rel = "noreferrer";
|
|
291
|
-
return a;
|
|
292
|
-
}
|
|
293
|
-
function update(state) {
|
|
294
|
-
switch (resolvePhase(state)) {
|
|
295
|
-
case "running":
|
|
296
|
-
renderRunning(state);
|
|
297
|
-
break;
|
|
298
|
-
case "starting":
|
|
299
|
-
renderStarting();
|
|
300
|
-
break;
|
|
301
|
-
case "connecting":
|
|
302
|
-
renderConnecting();
|
|
303
|
-
break;
|
|
304
|
-
case "not-installed":
|
|
305
|
-
renderNotInstalled(state.busy ?? false);
|
|
306
|
-
break;
|
|
307
|
-
case "launch":
|
|
308
|
-
renderLaunch(state);
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
update,
|
|
314
|
-
dispose() {
|
|
315
|
-
panes.get(paneId)?.dispose();
|
|
316
|
-
root.remove();
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
//#endregion
|
|
321
11
|
//#region src/client/index.ts
|
|
322
12
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* launch screen when stopped, a progress state while starting, and the editor
|
|
327
|
-
* in a full-bleed, auto-authenticated iframe once running.
|
|
328
|
-
*
|
|
329
|
-
* The connection is assumed already authorized with devframe's auth, so the
|
|
330
|
-
* server-issued session cookie is applied transparently before the iframe
|
|
331
|
-
* loads — the user never sees code-server's login page.
|
|
13
|
+
* Connect to the code-server plugin's devframe backend. A thin, typed wrapper
|
|
14
|
+
* around devframe's {@link connectDevframe}; the SPA derives its base from
|
|
15
|
+
* `document.baseURI`, so no options are required in the common case.
|
|
332
16
|
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (rpc.connectionMeta.backend === "websocket") await rpc.ensureTrusted(5e3).catch(() => {});
|
|
336
|
-
if (!container.style.position) container.style.position = "relative";
|
|
337
|
-
const connectionState = createConnectionState(container);
|
|
338
|
-
const syncConnection = () => {
|
|
339
|
-
connectionState.update(rpc.status, rpc.connectionError?.message);
|
|
340
|
-
};
|
|
341
|
-
syncConnection();
|
|
342
|
-
const offConnection = rpc.events.on("connection:status", syncConnection);
|
|
343
|
-
let detection = {
|
|
344
|
-
checked: false,
|
|
345
|
-
installed: false,
|
|
346
|
-
bin: "code-server"
|
|
347
|
-
};
|
|
348
|
-
let server = { status: "stopped" };
|
|
349
|
-
let auth;
|
|
350
|
-
let busy = false;
|
|
351
|
-
let disposed = false;
|
|
352
|
-
const view = createCodeServerView(container, { actions: {
|
|
353
|
-
launch,
|
|
354
|
-
recheck
|
|
355
|
-
} });
|
|
356
|
-
function sync() {
|
|
357
|
-
if (disposed) return;
|
|
358
|
-
view.update({
|
|
359
|
-
detection,
|
|
360
|
-
server,
|
|
361
|
-
auth,
|
|
362
|
-
busy
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
function applyResult(result) {
|
|
366
|
-
detection = result.detection;
|
|
367
|
-
server = result.server;
|
|
368
|
-
if (result.auth) auth = result.auth;
|
|
369
|
-
}
|
|
370
|
-
async function call(method, ...args) {
|
|
371
|
-
try {
|
|
372
|
-
return await rpc.call(method, ...args);
|
|
373
|
-
} catch (error) {
|
|
374
|
-
server = {
|
|
375
|
-
status: "error",
|
|
376
|
-
error: error instanceof Error ? error.message : String(error)
|
|
377
|
-
};
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async function launch() {
|
|
382
|
-
if (busy) return;
|
|
383
|
-
busy = true;
|
|
384
|
-
sync();
|
|
385
|
-
const result = await call("devframes:plugin:code-server:start", {});
|
|
386
|
-
if (result) applyResult(result);
|
|
387
|
-
busy = false;
|
|
388
|
-
sync();
|
|
389
|
-
}
|
|
390
|
-
async function stop() {
|
|
391
|
-
if (busy) return;
|
|
392
|
-
busy = true;
|
|
393
|
-
sync();
|
|
394
|
-
const result = await call("devframes:plugin:code-server:stop");
|
|
395
|
-
if (result) applyResult(result);
|
|
396
|
-
auth = void 0;
|
|
397
|
-
busy = false;
|
|
398
|
-
sync();
|
|
399
|
-
}
|
|
400
|
-
async function recheck() {
|
|
401
|
-
if (busy) return;
|
|
402
|
-
busy = true;
|
|
403
|
-
sync();
|
|
404
|
-
await call("devframes:plugin:code-server:detect");
|
|
405
|
-
busy = false;
|
|
406
|
-
sync();
|
|
407
|
-
}
|
|
408
|
-
sync();
|
|
409
|
-
const initial = await call("devframes:plugin:code-server:status");
|
|
410
|
-
if (initial) applyResult(initial);
|
|
411
|
-
sync();
|
|
412
|
-
const state = await rpc.sharedState.get(STATE_KEY, { initialValue: {
|
|
413
|
-
detection,
|
|
414
|
-
server
|
|
415
|
-
} });
|
|
416
|
-
const current = state.value();
|
|
417
|
-
detection = current.detection ?? detection;
|
|
418
|
-
server = current.server ?? server;
|
|
419
|
-
const off = state.on("updated", (full) => {
|
|
420
|
-
detection = full.detection ?? detection;
|
|
421
|
-
server = full.server ?? server;
|
|
422
|
-
if (server.status === "running" && !auth) {
|
|
423
|
-
call("devframes:plugin:code-server:status").then((result) => {
|
|
424
|
-
if (result?.auth) auth = result.auth;
|
|
425
|
-
sync();
|
|
426
|
-
});
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
sync();
|
|
430
|
-
});
|
|
431
|
-
sync();
|
|
432
|
-
return {
|
|
433
|
-
rpc,
|
|
434
|
-
stop,
|
|
435
|
-
dispose() {
|
|
436
|
-
disposed = true;
|
|
437
|
-
off?.();
|
|
438
|
-
offConnection?.();
|
|
439
|
-
connectionState.dispose();
|
|
440
|
-
view.dispose();
|
|
441
|
-
}
|
|
442
|
-
};
|
|
17
|
+
function connectCodeServer(options) {
|
|
18
|
+
return connectDevframe(options);
|
|
443
19
|
}
|
|
444
20
|
//#endregion
|
|
445
|
-
export { STATE_KEY,
|
|
21
|
+
export { STATE_KEY, connectCodeServer };
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
declare const PLUGIN_ID = "devframes_plugin_code-server";
|
|
4
4
|
/**
|
|
5
5
|
* Shared-state key holding the serializable, secret-free server status and
|
|
6
|
-
* detection result.
|
|
7
|
-
*
|
|
8
|
-
* client (see {@link
|
|
6
|
+
* detection result. Authentication material is never published here — it is
|
|
7
|
+
* returned only from the `start` / `status` RPCs to the already-authorized
|
|
8
|
+
* client (see {@link import('./types').CodeServerConnect}).
|
|
9
9
|
*/
|
|
10
10
|
declare const STATE_KEY = "devframes:plugin:code-server:state";
|
|
11
11
|
/** Default dev-server port for the plugin's own launcher SPA (standalone CLI). */
|
|
12
12
|
declare const DEFAULT_PORT = 9013;
|
|
13
|
-
/** Preferred port for the spawned
|
|
13
|
+
/** Preferred port for the spawned editor process (falls back if taken). */
|
|
14
14
|
declare const DEFAULT_CODE_SERVER_PORT = 8080;
|
|
15
|
-
/** How long to wait for
|
|
15
|
+
/** How long to wait for the editor to answer its readiness probe. */
|
|
16
16
|
declare const DEFAULT_START_TIMEOUT = 30000;
|
|
17
17
|
/** code-server's session cookie base name (see `getCookieSessionName`). */
|
|
18
18
|
declare const SESSION_COOKIE_BASE = "code-server-session";
|