// retoor <retoor@molodetz.nl>
const fs = require("fs");
const vscode = require("vscode");
const AGENT_PATH = "/usr/bin/dpc";
const AGENT_TERMINAL = "DevPlace Code";
const SHELL_TERMINAL = "pravda@workspace";
const BOOT_KEY = "devplace.bootMarker";
const PANEL_STEPS = { short: 0, normal: 2, tall: 5, maximized: 0 };
class Profile {
constructor() {
this.data = Object.assign(
{
theme: "devplace-dark",
layout: "standard",
panel_preset: "tall",
boot_agent: "dpc",
boot_shell: true,
trust_all: true,
},
this.fromFile(),
this.fromEnv(),
);
}
fromFile() {
const path = process.env.DEVPLACE_EDITOR_PROFILE;
if (!path) return {};
try {
const parsed = JSON.parse(fs.readFileSync(path, "utf8"));
return parsed && parsed.editor ? parsed.editor : {};
} catch (error) {
return {};
}
}
fromEnv() {
const values = {};
if (process.env.DEVPLACE_EDITOR_PANEL_PRESET) {
values.panel_preset = process.env.DEVPLACE_EDITOR_PANEL_PRESET;
}
if (process.env.DEVPLACE_EDITOR_BOOT_AGENT) {
values.boot_agent = process.env.DEVPLACE_EDITOR_BOOT_AGENT;
}
if (process.env.DEVPLACE_EDITOR_BOOT_SHELL) {
values.boot_shell = process.env.DEVPLACE_EDITOR_BOOT_SHELL === "1";
}
return values;
}
get bootMarker() {
return process.env.DEVPLACE_CONTAINER_BOOT || `host-${process.pid}`;
}
get wantsAgent() {
return this.data.boot_agent === "dpc" && fs.existsSync(AGENT_PATH);
}
get wantsShell() {
return Boolean(this.data.boot_shell);
}
get panelPreset() {
return this.data.panel_preset || "tall";
}
}
class BootTerminals {
constructor(profile, memento) {
this.profile = profile;
this.memento = memento;
}
alreadyBooted() {
return this.memento.get(BOOT_KEY) === this.profile.bootMarker;
}
async open() {
if (this.alreadyBooted()) return false;
await this.memento.update(BOOT_KEY, this.profile.bootMarker);
const shell = this.profile.wantsShell ? this.createShell() : null;
const agent = this.profile.wantsAgent ? this.createAgent() : null;
if (agent) agent.show(true);
else if (shell) shell.show(true);
return Boolean(agent || shell);
}
createAgent() {
return vscode.window.createTerminal({
name: AGENT_TERMINAL,
shellPath: AGENT_PATH,
iconPath: new vscode.ThemeIcon("rocket"),
isTransient: false,
});
}
createShell() {
return vscode.window.createTerminal({
name: SHELL_TERMINAL,
shellPath: "/bin/bash",
shellArgs: ["-l"],
iconPath: new vscode.ThemeIcon("terminal-bash"),
isTransient: false,
});
}
}
class Layout {
constructor(profile) {
this.profile = profile;
}
async apply(panelIsOpen) {
const preset = this.profile.panelPreset;
if (!panelIsOpen) return;
if (preset === "maximized") {
await vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel");
return;
}
const steps = PANEL_STEPS[preset] === undefined ? 5 : PANEL_STEPS[preset];
for (let index = 0; index < steps; index += 1) {
await vscode.commands.executeCommand("workbench.action.increaseViewSize");
}
}
}
class Presence {
constructor(context) {
this.context = context;
this.item = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
100,
);
}
get projectTitle() {
return process.env.DEVPLACE_PROJECT_TITLE || "DevPlace";
}
register() {
this.item.text = `$(rocket) ${this.projectTitle}`;
this.item.tooltip = this.tooltip();
this.item.command = "devplace.openProject";
this.item.show();
this.context.subscriptions.push(this.item);
this.registerCommands();
}
tooltip() {
const owner = process.env.DEVPLACE_WORKSPACE_OWNER || "";
const name = process.env.DEVPLACE_TUNNEL_NAME || "";
return `DevPlace workspace ${name}${owner ? ` for ${owner}` : ""}`;
}
registerCommands() {
const commands = {
"devplace.runAgent": () => this.runAgent(),
"devplace.openProject": () => this.open(process.env.DEVPLACE_PROJECT_URL),
"devplace.openWorkspacePage": () =>
this.open(process.env.DEVPLACE_WORKSPACE_URL),
"devplace.openDocs": () => this.openDocs(),
"devplace.showTunnels": () => this.showTunnels(),
};
for (const [name, handler] of Object.entries(commands)) {
this.context.subscriptions.push(
vscode.commands.registerCommand(name, handler),
);
}
}
runAgent() {
const terminal = vscode.window.createTerminal({
name: AGENT_TERMINAL,
shellPath: AGENT_PATH,
iconPath: new vscode.ThemeIcon("rocket"),
});
terminal.show(true);
}
openDocs() {
const base = process.env.DEVPLACE_BASE_URL || "";
this.open(base ? `${base}/docs/workspace-editor.html` : "");
}
open(url) {
if (!url) {
vscode.window.showWarningMessage(
"DevPlace has not published a site URL for this workspace yet.",
);
return;
}
vscode.env.openExternal(vscode.Uri.parse(url));
}
async showTunnels() {
const path = process.env.DEVPLACE_TUNNEL_MANIFEST;
const rows = this.readManifest(path);
if (!rows.length) {
vscode.window.showInformationMessage(
"This workspace has no public tunnels yet.",
);
return;
}
const picked = await vscode.window.showQuickPick(
rows.map((row) => ({
label: row.label || row.hostname,
description: row.url,
detail: `port ${row.container_port} - ${row.status}`,
url: row.url,
})),
{ placeHolder: "Open a public tunnel" },
);
if (picked) this.open(picked.url);
}
readManifest(path) {
if (!path) return [];
try {
const parsed = JSON.parse(fs.readFileSync(path, "utf8"));
return Array.isArray(parsed.tunnels) ? parsed.tunnels : [];
} catch (error) {
return [];
}
}
}
async function stage(output, name, run) {
try {
return await run();
} catch (error) {
output.appendLine(`${name} failed: ${error}`);
return undefined;
}
}
async function activate(context) {
const output = vscode.window.createOutputChannel("DevPlace");
context.subscriptions.push(output);
const profile = new Profile();
await stage(output, "presence", () => new Presence(context).register());
const opened = await stage(output, "terminals", () =>
new BootTerminals(profile, context.workspaceState).open(),
);
await stage(output, "layout", () => new Layout(profile).apply(Boolean(opened)));
}
function deactivate() {}
module.exports = { activate, deactivate };