Public dry run dryrun-a43e907efd9d
Source commit: a43e907efd9d1561c23fe73499478e881f868355 Public tree identity: sha256:453dea30195485dd8939f575a69b39f4bb7acd84c4df23f8aa29b55d044f2673
This commit is contained in:
commit
6f52bb46cd
210 changed files with 77158 additions and 0 deletions
730
vscode-extension/extension.js
Normal file
730
vscode-extension/extension.js
Normal file
|
|
@ -0,0 +1,730 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const childProcess = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
|
||||
|
||||
let vscode = null;
|
||||
try {
|
||||
vscode = require("vscode");
|
||||
} catch (_) {
|
||||
vscode = null;
|
||||
}
|
||||
|
||||
function discoverEnvironmentNames(projectRoot) {
|
||||
const envsDir = path.join(projectRoot, "envs");
|
||||
if (!fs.existsSync(envsDir)) return [];
|
||||
|
||||
return fs
|
||||
.readdirSync(envsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => {
|
||||
const dir = path.join(envsDir, name);
|
||||
return (
|
||||
fs.existsSync(path.join(dir, "Containerfile")) ||
|
||||
fs.existsSync(path.join(dir, "Dockerfile"))
|
||||
);
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
function findEnvReferences(text) {
|
||||
const references = [];
|
||||
const pattern = /env!\(\s*"([^"]+)"\s*\)/g;
|
||||
let match;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
references.push({
|
||||
name: match[1],
|
||||
start: match.index,
|
||||
end: pattern.lastIndex
|
||||
});
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
function diagnoseEnvReferences(text, environmentNames) {
|
||||
const known = new Set(environmentNames);
|
||||
return findEnvReferences(text)
|
||||
.filter((reference) => !known.has(reference.name))
|
||||
.map((reference) => ({
|
||||
...reference,
|
||||
message: `Missing Clusterflux environment "${reference.name}". Add envs/${reference.name}/Containerfile or envs/${reference.name}/Dockerfile.`
|
||||
}));
|
||||
}
|
||||
|
||||
function binaryCandidates(projectRoot, name) {
|
||||
const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
|
||||
return names.map((candidate) => path.join(projectRoot, candidate));
|
||||
}
|
||||
|
||||
function executableWorkspaceBinary(projectRoot, name) {
|
||||
if (!projectRoot) return null;
|
||||
return binaryCandidates(projectRoot, name).find((candidate) => {
|
||||
try {
|
||||
return fs.statSync(candidate).isFile();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function hasCargoWorkspace(root) {
|
||||
return fs.existsSync(path.join(root, "Cargo.toml"));
|
||||
}
|
||||
|
||||
function resolveProjectPath(folder, value) {
|
||||
const workspaceRoot = folder && folder.uri && folder.uri.fsPath;
|
||||
if (!value || value === "${workspaceFolder}") return workspaceRoot;
|
||||
if (workspaceRoot && typeof value === "string") {
|
||||
return value.replace(/\$\{workspaceFolder\}/g, workspaceRoot);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function bundleInspectCommand(projectRoot, adapterWorkspace = path.resolve(__dirname, "..")) {
|
||||
const workspaceCli = executableWorkspaceBinary(projectRoot, "clusterflux");
|
||||
if (workspaceCli) {
|
||||
return {
|
||||
command: workspaceCli,
|
||||
args: ["bundle", "inspect", "--project", projectRoot, "--json"],
|
||||
options: {
|
||||
cwd: projectRoot,
|
||||
encoding: "utf8"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "cargo",
|
||||
args: [
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"clusterflux-cli",
|
||||
"--bin",
|
||||
"clusterflux",
|
||||
"--",
|
||||
"bundle",
|
||||
"inspect",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--json"
|
||||
],
|
||||
options: {
|
||||
cwd: adapterWorkspace,
|
||||
encoding: "utf8"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function clusterfluxCliCommand(
|
||||
projectRoot,
|
||||
args,
|
||||
adapterWorkspace = path.resolve(__dirname, "..")
|
||||
) {
|
||||
const workspaceCli = executableWorkspaceBinary(projectRoot, "clusterflux");
|
||||
if (workspaceCli) {
|
||||
return {
|
||||
command: workspaceCli,
|
||||
args,
|
||||
options: { cwd: projectRoot, encoding: "utf8" }
|
||||
};
|
||||
}
|
||||
if (!hasCargoWorkspace(adapterWorkspace)) {
|
||||
return {
|
||||
command: "clusterflux",
|
||||
args,
|
||||
options: { cwd: projectRoot, encoding: "utf8" }
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: "cargo",
|
||||
args: ["run", "-q", "-p", "clusterflux-cli", "--bin", "clusterflux", "--", ...args],
|
||||
options: { cwd: adapterWorkspace, encoding: "utf8" }
|
||||
};
|
||||
}
|
||||
|
||||
function runClusterfluxCliJson(
|
||||
projectRoot,
|
||||
args,
|
||||
adapterWorkspace = path.resolve(__dirname, ".."),
|
||||
runner = childProcess.spawnSync
|
||||
) {
|
||||
const invocation = clusterfluxCliCommand(projectRoot, [...args, "--json"], adapterWorkspace);
|
||||
const result = runner(invocation.command, invocation.args, invocation.options);
|
||||
if (result.error) {
|
||||
throw new Error(`Clusterflux CLI failed: ${result.error.message}`);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const detail = String(result.stderr || result.stdout || "").trim();
|
||||
throw new Error(detail || `Clusterflux CLI exited with status ${result.status}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(result.stdout);
|
||||
} catch (error) {
|
||||
throw new Error(`Clusterflux CLI returned invalid JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadLiveProcesses(
|
||||
projectRoot,
|
||||
adapterWorkspace = path.resolve(__dirname, ".."),
|
||||
runner = childProcess.spawnSync
|
||||
) {
|
||||
try {
|
||||
const report = runClusterfluxCliJson(
|
||||
projectRoot,
|
||||
["process", "list"],
|
||||
adapterWorkspace,
|
||||
runner
|
||||
);
|
||||
return {
|
||||
processes: asArray(report.processes),
|
||||
coordinator: report.coordinator || null,
|
||||
tenant: report.tenant || null,
|
||||
project: report.project || null,
|
||||
user: report.user || null,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
processes: [],
|
||||
coordinator: null,
|
||||
tenant: null,
|
||||
project: null,
|
||||
user: null,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function digestFromParts(parts) {
|
||||
const hasher = crypto.createHash("sha256");
|
||||
for (const value of parts) {
|
||||
const part = Buffer.from(value);
|
||||
const length = Buffer.alloc(8);
|
||||
length.writeBigUInt64BE(BigInt(part.length));
|
||||
hasher.update(length);
|
||||
hasher.update(part);
|
||||
}
|
||||
return hasher.digest("hex");
|
||||
}
|
||||
|
||||
function clusterfluxProcessId(projectRoot, entry = "build") {
|
||||
return `vp-${digestFromParts(["dap-process:v1", projectRoot, entry]).slice(0, 12)}`;
|
||||
}
|
||||
|
||||
function refreshBundleBeforeLaunch(
|
||||
projectRoot,
|
||||
adapterWorkspace = path.resolve(__dirname, ".."),
|
||||
runner = childProcess.spawnSync
|
||||
) {
|
||||
const command = bundleInspectCommand(projectRoot, adapterWorkspace);
|
||||
const result = runner(command.command, command.args, command.options);
|
||||
if (result.error) {
|
||||
throw new Error(`Clusterflux bundle refresh failed: ${result.error.message}`);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const detail = (result.stderr || result.stdout || "").trim();
|
||||
throw new Error(
|
||||
`Clusterflux bundle refresh failed before debug launch${detail ? `: ${detail}` : "."}`
|
||||
);
|
||||
}
|
||||
|
||||
let inspection;
|
||||
try {
|
||||
inspection = JSON.parse(result.stdout);
|
||||
} catch (error) {
|
||||
throw new Error(`Clusterflux bundle refresh returned invalid metadata: ${error.message}`);
|
||||
}
|
||||
if (!inspection.metadata || !inspection.metadata.identity) {
|
||||
throw new Error("Clusterflux bundle refresh did not return a bundle identity.");
|
||||
}
|
||||
return inspection;
|
||||
}
|
||||
|
||||
function clusterfluxViewDescriptors() {
|
||||
return [
|
||||
{ id: "clusterflux.nodes", emptyLabel: "No attached nodes" },
|
||||
{ id: "clusterflux.processes", emptyLabel: "No virtual processes" },
|
||||
{ id: "clusterflux.logs", emptyLabel: "No log streams" },
|
||||
{ id: "clusterflux.artifacts", emptyLabel: "No artifacts" },
|
||||
{ id: "clusterflux.inspector", emptyLabel: "No inspector state" }
|
||||
];
|
||||
}
|
||||
|
||||
function clusterfluxViewStatePath(projectRoot) {
|
||||
return path.join(projectRoot, ".clusterflux", "views.json");
|
||||
}
|
||||
|
||||
function emptyClusterfluxViewState() {
|
||||
return {
|
||||
nodes: [],
|
||||
processes: [],
|
||||
logs: [],
|
||||
artifacts: [],
|
||||
inspector: []
|
||||
};
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function loadClusterfluxViewState(projectRoot, reader = fs.readFileSync) {
|
||||
const statePath = clusterfluxViewStatePath(projectRoot);
|
||||
if (!fs.existsSync(statePath)) {
|
||||
return emptyClusterfluxViewState();
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(reader(statePath, "utf8"));
|
||||
} catch (error) {
|
||||
return {
|
||||
...emptyClusterfluxViewState(),
|
||||
inspector: [
|
||||
{
|
||||
label: "View state error",
|
||||
value: error.message
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: asArray(parsed.nodes),
|
||||
processes: asArray(parsed.processes),
|
||||
logs: asArray(parsed.logs),
|
||||
artifacts: asArray(parsed.artifacts),
|
||||
inspector: asArray(parsed.inspector)
|
||||
};
|
||||
}
|
||||
|
||||
function item(label, description = "", options = {}) {
|
||||
return {
|
||||
label: String(label || ""),
|
||||
description: String(description || ""),
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
function clusterfluxViewItems(state, viewId) {
|
||||
switch (viewId) {
|
||||
case "clusterflux.nodes":
|
||||
return state.nodes.map((node) =>
|
||||
item(node.id || node.name || "node", [node.status, node.capabilities].filter(Boolean).join(" "))
|
||||
);
|
||||
case "clusterflux.processes":
|
||||
return state.processes.map((process) =>
|
||||
item(
|
||||
process.id || process.process || "process",
|
||||
[process.state || process.status, process.entry].filter(Boolean).join(" "),
|
||||
{
|
||||
processId: process.id || process.process,
|
||||
processState: process.state || process.status || "unknown",
|
||||
contextValue: "clusterflux.virtualProcess",
|
||||
command: {
|
||||
command: "clusterflux.process.attach",
|
||||
title: "Attach to Virtual Process",
|
||||
arguments: [process.id || process.process]
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
case "clusterflux.logs":
|
||||
return state.logs.map((log) =>
|
||||
item(log.task || log.stream || "log", [log.message, log.bytes].filter(Boolean).join(" "))
|
||||
);
|
||||
case "clusterflux.artifacts":
|
||||
return state.artifacts.map((artifact) =>
|
||||
item(artifact.path || artifact.id || "artifact", [artifact.status, artifact.size].filter(Boolean).join(" "))
|
||||
);
|
||||
case "clusterflux.inspector":
|
||||
return state.inspector.map((entry) =>
|
||||
item(entry.label || entry.name || "inspector", entry.value || entry.detail || "")
|
||||
);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function treeItemsForView(projectRoot, descriptor, liveProcesses = null) {
|
||||
const state = projectRoot ? loadClusterfluxViewState(projectRoot) : emptyClusterfluxViewState();
|
||||
if (descriptor.id === "clusterflux.processes" && liveProcesses) {
|
||||
if (liveProcesses.error) {
|
||||
return [
|
||||
item("Live process state unavailable", liveProcesses.error, {
|
||||
contextValue: "clusterflux.processError"
|
||||
})
|
||||
];
|
||||
}
|
||||
state.processes = liveProcesses.processes;
|
||||
}
|
||||
const items = clusterfluxViewItems(state, descriptor.id);
|
||||
return items.length > 0 ? items : [item(descriptor.emptyLabel)];
|
||||
}
|
||||
|
||||
function toVsCodeTreeItem(viewItem) {
|
||||
const treeItem = new vscode.TreeItem(
|
||||
viewItem.label,
|
||||
vscode.TreeItemCollapsibleState.None
|
||||
);
|
||||
treeItem.description = viewItem.description;
|
||||
treeItem.tooltip = viewItem.description || viewItem.label;
|
||||
treeItem.contextValue = viewItem.contextValue;
|
||||
treeItem.command = viewItem.command;
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
function resolveClusterfluxDebugConfiguration(folder, config = {}) {
|
||||
const project = resolveProjectPath(folder, config.project);
|
||||
return {
|
||||
...config,
|
||||
name:
|
||||
config.name ||
|
||||
(config.request === "attach"
|
||||
? "Clusterflux: Attach to Virtual Process"
|
||||
: "Clusterflux: Launch Virtual Process"),
|
||||
type: "clusterflux",
|
||||
request: config.request || "launch",
|
||||
entry: config.entry || "build",
|
||||
project,
|
||||
runtimeBackend: config.runtimeBackend || "local-services"
|
||||
};
|
||||
}
|
||||
|
||||
function debugAdapterExecutableSpec(
|
||||
projectRoot,
|
||||
adapterWorkspace = path.resolve(__dirname, "..")
|
||||
) {
|
||||
const workspaceAdapter = executableWorkspaceBinary(projectRoot, "clusterflux-debug-dap");
|
||||
if (workspaceAdapter) {
|
||||
return {
|
||||
command: workspaceAdapter,
|
||||
args: [],
|
||||
options: { cwd: projectRoot }
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasCargoWorkspace(adapterWorkspace)) {
|
||||
return {
|
||||
command: "clusterflux-debug-dap",
|
||||
args: [],
|
||||
options: { cwd: projectRoot || process.cwd() }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "cargo",
|
||||
args: ["run", "-q", "-p", "clusterflux-dap", "--bin", "clusterflux-debug-dap"],
|
||||
options: { cwd: adapterWorkspace }
|
||||
};
|
||||
}
|
||||
|
||||
function registerClusterfluxViews(context) {
|
||||
if (!vscode) return null;
|
||||
const workspaceRoot =
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders[0] &&
|
||||
vscode.workspace.workspaceFolders[0].uri.fsPath;
|
||||
const emitters = new Map();
|
||||
let liveProcesses = null;
|
||||
const refreshProcesses = () => {
|
||||
liveProcesses = workspaceRoot ? loadLiveProcesses(workspaceRoot) : null;
|
||||
const emitter = emitters.get("clusterflux.processes");
|
||||
if (emitter) emitter.fire();
|
||||
return liveProcesses;
|
||||
};
|
||||
for (const descriptor of clusterfluxViewDescriptors()) {
|
||||
const emitter = new vscode.EventEmitter();
|
||||
emitters.set(descriptor.id, emitter);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider(descriptor.id, {
|
||||
onDidChangeTreeData: emitter.event,
|
||||
getTreeItem(item) {
|
||||
return item;
|
||||
},
|
||||
getChildren() {
|
||||
if (descriptor.id === "clusterflux.processes" && liveProcesses === null) {
|
||||
refreshProcesses();
|
||||
}
|
||||
return treeItemsForView(workspaceRoot, descriptor, liveProcesses).map(toVsCodeTreeItem);
|
||||
}
|
||||
}),
|
||||
emitter
|
||||
);
|
||||
}
|
||||
if (workspaceRoot && vscode.workspace.createFileSystemWatcher) {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(workspaceRoot, ".clusterflux/views.json")
|
||||
);
|
||||
const refresh = () => emitters.forEach((emitter) => emitter.fire());
|
||||
context.subscriptions.push(
|
||||
watcher,
|
||||
watcher.onDidChange(refresh),
|
||||
watcher.onDidCreate(refresh),
|
||||
watcher.onDidDelete(refresh)
|
||||
);
|
||||
}
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("clusterflux.refreshProcesses", refreshProcesses)
|
||||
);
|
||||
return { workspaceRoot, refreshProcesses, getLiveProcesses: () => liveProcesses };
|
||||
}
|
||||
|
||||
function commandProcessId(value) {
|
||||
if (typeof value === "string") return value;
|
||||
return value && (value.processId || value.label);
|
||||
}
|
||||
|
||||
function liveDebugConfiguration(workspaceRoot, processId, liveProcesses) {
|
||||
return {
|
||||
type: "clusterflux",
|
||||
request: "attach",
|
||||
name: `Clusterflux: Attach to ${processId}`,
|
||||
project: workspaceRoot,
|
||||
entry: "build",
|
||||
runtimeBackend: "live-services",
|
||||
processId
|
||||
};
|
||||
}
|
||||
|
||||
function processAction(projectRoot, action, processId) {
|
||||
return runClusterfluxCliJson(projectRoot, ["process", action, "--process", processId, "--yes"]);
|
||||
}
|
||||
|
||||
function registerProcessCommands(context, viewController) {
|
||||
if (!viewController || !viewController.workspaceRoot) return;
|
||||
const projectRoot = viewController.workspaceRoot;
|
||||
const folder = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0];
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("clusterflux.process.attach", async (value) => {
|
||||
const processId = commandProcessId(value);
|
||||
if (!processId) return;
|
||||
const live = viewController.refreshProcesses();
|
||||
if (live.error) {
|
||||
vscode.window.showErrorMessage(`Unable to load Clusterflux process state: ${live.error}`);
|
||||
return;
|
||||
}
|
||||
await vscode.debug.startDebugging(
|
||||
folder,
|
||||
liveDebugConfiguration(projectRoot, processId, live)
|
||||
);
|
||||
}),
|
||||
vscode.commands.registerCommand("clusterflux.process.cancel", async (value) => {
|
||||
const processId = commandProcessId(value);
|
||||
if (!processId) return;
|
||||
const selected = await vscode.window.showWarningMessage(
|
||||
`Request cooperative cancellation of ${processId}? The process remains active until its code exits.`,
|
||||
{ modal: true },
|
||||
"Request Cancellation"
|
||||
);
|
||||
if (selected !== "Request Cancellation") return;
|
||||
try {
|
||||
processAction(projectRoot, "cancel", processId);
|
||||
viewController.refreshProcesses();
|
||||
vscode.window.showInformationMessage(`Cancellation requested for ${processId}.`);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Clusterflux cancellation failed: ${error.message}`);
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand("clusterflux.process.abort", async (value) => {
|
||||
const processId = commandProcessId(value);
|
||||
if (!processId) return;
|
||||
const selected = await vscode.window.showWarningMessage(
|
||||
`Abort ${processId}? This forcibly terminates the virtual process and releases the project slot.`,
|
||||
{ modal: true },
|
||||
"Abort Process"
|
||||
);
|
||||
if (selected !== "Abort Process") return;
|
||||
try {
|
||||
processAction(projectRoot, "abort", processId);
|
||||
viewController.refreshProcesses();
|
||||
vscode.window.showInformationMessage(`Aborted ${processId}.`);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Clusterflux abort failed: ${error.message}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function existingProcessRelationship(activeProcess, intendedProcessId) {
|
||||
return activeProcess && activeProcess.process === intendedProcessId
|
||||
? "same_launch_target"
|
||||
: "different_launch_target";
|
||||
}
|
||||
|
||||
async function resolveExistingProcessBeforeLaunch(folder, config) {
|
||||
if (
|
||||
!folder ||
|
||||
config.request !== "launch" ||
|
||||
config.runtimeBackend !== "live-services" ||
|
||||
!config.project
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
const live = loadLiveProcesses(config.project);
|
||||
if (live.error || live.processes.length === 0) return config;
|
||||
|
||||
const active = live.processes[0];
|
||||
const intendedProcessId =
|
||||
config.processId || clusterfluxProcessId(config.project, config.entry || "build");
|
||||
const relationship = existingProcessRelationship(active, intendedProcessId);
|
||||
const runningId = active.process;
|
||||
const detail = `${runningId} is ${active.state || "running"} in Coordinator Project ${
|
||||
live.project || "selected by the authenticated CLI session"
|
||||
}.`;
|
||||
|
||||
const choices =
|
||||
relationship === "same_launch_target"
|
||||
? [
|
||||
{ label: "Attach", description: "Debug the existing virtual process.", action: "attach" },
|
||||
{ label: "Restart", description: "Restart this virtual process with the current launch.", action: "restart" },
|
||||
...(active.state === "cancelling"
|
||||
? []
|
||||
: [{ label: "Request cancellation", description: "Let the process handle cancellation and exit.", action: "cancel" }]),
|
||||
{ label: "Abort and launch", description: "Forcibly terminate it, then start this launch.", action: "abort_launch" },
|
||||
{ label: "Show details", description: detail, action: "details" },
|
||||
{ label: "Cancel launch", description: "Leave the running process unchanged.", action: "dismiss" }
|
||||
]
|
||||
: [
|
||||
{ label: "Show running process", description: detail, action: "details" },
|
||||
...(active.state === "cancelling"
|
||||
? []
|
||||
: [{ label: "Request cancellation", description: "Ask the other process to exit cooperatively.", action: "cancel" }]),
|
||||
{ label: "Abort it and launch this workspace", description: "Forcibly free this Coordinator Project slot.", action: "abort_launch" },
|
||||
{ label: "Use another Coordinator Project", description: "Keep the other process and select a different authenticated project.", action: "other_project" },
|
||||
{ label: "Cancel launch", description: "Leave the running process unchanged.", action: "dismiss" }
|
||||
];
|
||||
|
||||
const selected = await vscode.window.showQuickPick(choices, {
|
||||
title:
|
||||
relationship === "same_launch_target"
|
||||
? "This Clusterflux virtual process is already running"
|
||||
: "Another launch is using this Coordinator Project",
|
||||
placeHolder: detail,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (!selected || selected.action === "dismiss") return undefined;
|
||||
if (selected.action === "details") {
|
||||
await vscode.window.showInformationMessage(detail, { modal: true });
|
||||
return undefined;
|
||||
}
|
||||
if (selected.action === "other_project") {
|
||||
await vscode.window.showInformationMessage(
|
||||
`Choose or create another Coordinator Project with the Clusterflux CLI, then run \`clusterflux project select\` from this workspace. The current project is ${
|
||||
live.project || "the project in the authenticated CLI session"
|
||||
}.`,
|
||||
{ modal: true }
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (selected.action === "cancel") {
|
||||
processAction(config.project, "cancel", runningId);
|
||||
vscode.commands.executeCommand("clusterflux.refreshProcesses");
|
||||
return undefined;
|
||||
}
|
||||
if (selected.action === "abort_launch") {
|
||||
processAction(config.project, "abort", runningId);
|
||||
vscode.commands.executeCommand("clusterflux.refreshProcesses");
|
||||
return config;
|
||||
}
|
||||
if (selected.action === "attach") {
|
||||
return {
|
||||
...config,
|
||||
request: "attach",
|
||||
name: `Clusterflux: Attach to ${runningId}`,
|
||||
processId: runningId
|
||||
};
|
||||
}
|
||||
if (selected.action === "restart") {
|
||||
return { ...config, processId: runningId, restartExisting: true };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function activate(context) {
|
||||
if (!vscode) return;
|
||||
|
||||
const diagnostics = vscode.languages.createDiagnosticCollection("clusterflux");
|
||||
context.subscriptions.push(diagnostics);
|
||||
const viewController = registerClusterfluxViews(context);
|
||||
registerProcessCommands(context, viewController);
|
||||
|
||||
const refresh = (document) => {
|
||||
if (document.languageId !== "rust") return;
|
||||
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||
if (!folder) return;
|
||||
const environmentNames = discoverEnvironmentNames(folder.uri.fsPath);
|
||||
const items = diagnoseEnvReferences(document.getText(), environmentNames).map((diagnostic) => {
|
||||
const start = document.positionAt(diagnostic.start);
|
||||
const end = document.positionAt(diagnostic.end);
|
||||
return new vscode.Diagnostic(
|
||||
new vscode.Range(start, end),
|
||||
diagnostic.message,
|
||||
vscode.DiagnosticSeverity.Error
|
||||
);
|
||||
});
|
||||
diagnostics.set(document.uri, items);
|
||||
};
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidOpenTextDocument(refresh),
|
||||
vscode.workspace.onDidChangeTextDocument((event) => refresh(event.document)),
|
||||
vscode.workspace.onDidSaveTextDocument(refresh),
|
||||
vscode.debug.registerDebugAdapterDescriptorFactory("clusterflux", {
|
||||
createDebugAdapterDescriptor(session) {
|
||||
const folder = session.workspaceFolder;
|
||||
const project = session.configuration.project || (folder && folder.uri.fsPath);
|
||||
if (!project) {
|
||||
throw new Error("Clusterflux debug launch requires a workspace folder or project path.");
|
||||
}
|
||||
const adapterWorkspace = path.resolve(__dirname, "..");
|
||||
refreshBundleBeforeLaunch(project, adapterWorkspace);
|
||||
const spec = debugAdapterExecutableSpec(project, adapterWorkspace);
|
||||
return new vscode.DebugAdapterExecutable(
|
||||
spec.command,
|
||||
spec.args,
|
||||
spec.options
|
||||
);
|
||||
}
|
||||
}),
|
||||
vscode.debug.registerDebugConfigurationProvider("clusterflux", {
|
||||
async resolveDebugConfiguration(folder, config) {
|
||||
const resolved = resolveClusterfluxDebugConfiguration(folder, config);
|
||||
return resolveExistingProcessBeforeLaunch(folder, resolved);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
refresh(document);
|
||||
}
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
bundleInspectCommand,
|
||||
debugAdapterExecutableSpec,
|
||||
diagnoseEnvReferences,
|
||||
clusterfluxCliCommand,
|
||||
clusterfluxProcessId,
|
||||
clusterfluxViewDescriptors,
|
||||
clusterfluxViewItems,
|
||||
clusterfluxViewStatePath,
|
||||
discoverEnvironmentNames,
|
||||
findEnvReferences,
|
||||
existingProcessRelationship,
|
||||
loadLiveProcesses,
|
||||
loadClusterfluxViewState,
|
||||
registerClusterfluxViews,
|
||||
refreshBundleBeforeLaunch,
|
||||
runClusterfluxCliJson,
|
||||
resolveClusterfluxDebugConfiguration
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue