Files
openidea/src/platform/remoteSettings.ts
T
Andrea Basileandandreabasile 589cf81bae
CI / Changes (push) Successful in 1s
Language smoke / Language smoke (go) (push) Failing after 6s
Language smoke / Language smoke (kotlin) (push) Failing after 6s
Language smoke / Language smoke (java) (push) Failing after 7s
Language smoke / Language smoke (python) (push) Failing after 3s
Language smoke / Language smoke (typescript) (push) Failing after 5s
Language smoke / Language smoke (rust) (push) Failing after 5s
CI / Plugins (SDK, official, fixtures) (push) Successful in 3m9s
CI / Cargo deny (push) Successful in 52s
CI / Remote server (linux musl) (push) Successful in 5m57s
CI / E2E (push) Failing after 15m55s
CI / Frontend (push) Successful in 25m38s
CI / Rust (ubuntu-22.04) (push) Failing after 46m4s
CI / Rust (macos-latest) (push) Canceled after 0s
CI / Rust (windows-latest) (push) Canceled after 0s
feat(run): Docker and Compose run targets (#1640)
A run target can be a Docker image or a Compose service: the configuration runs as a local docker run --rm / docker compose run --rm with the project bind-mounted, so the console, PTY, stdin, Stop and test parsers work unchanged. Only daemons on this machine are accepted; on Linux the container runs as the project folder's owner; Stop runs docker stop with the same docker CLI. Debugging, coverage and profiling stay on this machine. Refs #1083 (step A1).

Co-authored-by: andreabasile <[email protected]>
2026-11-06 07:34:00 +08:00

252 lines
7.5 KiB
TypeScript

/**
* SSH configurations, deployment servers and run targets as features consume
* them (mirrors `ide-core::remote_settings` defaults). Normalisation never throws.
*/
export type SshAuthValid = "password" | "keyPair" | "openSsh";
export type AutoUploadValid = "never" | "onExplicitSave" | "always";
export interface SshConfigValid {
id: string;
name: string;
host: string;
port: number | null;
user: string;
auth: SshAuthValid;
keyFile: string;
saveSecret: boolean;
}
export interface DeploymentMappingValid {
local: string;
deployment: string;
web: string;
}
export interface DeploymentServerValid {
kind: "sftp" | "local";
id: string;
name: string;
sshConfig: string;
rootPath: string;
webUrl: string;
mappings: DeploymentMappingValid[];
excludedPaths: string[];
}
export interface DeploymentSettingsValid {
servers: DeploymentServerValid[];
defaultServer: string;
autoUpload: AutoUploadValid;
deleteRemote: boolean;
preserveTimestamps: boolean;
exclude: string[];
}
export type RunTargetKindValid = "ssh" | "docker" | "compose";
export interface RunTargetValid {
kind: RunTargetKindValid;
id: string;
name: string;
sshConfig: string;
projectRoot: string;
syncBeforeRun: boolean;
python: string;
node: string;
java: string;
/** Services tool window Docker connection id; empty = the CLI's daemon. */
dockerConnection: string;
image: string;
composeFiles: string[];
service: string;
containerRoot: string;
ports: string[];
runOptions: string;
imageUser: boolean;
}
export interface RemoteSettings {
ssh: { configs: SshConfigValid[] };
deployment: DeploymentSettingsValid;
runTargets: RunTargetValid[];
}
export const DEFAULT_SSH_CONFIG: SshConfigValid = {
id: "",
name: "",
host: "",
port: null,
user: "",
auth: "openSsh",
keyFile: "",
saveSecret: false,
};
export const DEFAULT_DEPLOYMENT_SERVER: DeploymentServerValid = {
kind: "sftp",
id: "",
name: "",
sshConfig: "",
rootPath: "",
webUrl: "",
mappings: [{ local: "", deployment: "", web: "" }],
excludedPaths: [],
};
export const DEFAULT_DEPLOYMENT: DeploymentSettingsValid = {
servers: [],
defaultServer: "",
autoUpload: "never",
deleteRemote: false,
preserveTimestamps: true,
exclude: [".git", ".svn", ".hg", ".idea", ".openidea", ".DS_Store", "*.pyc", "__pycache__"],
};
export const DEFAULT_RUN_TARGET: RunTargetValid = {
kind: "ssh",
id: "",
name: "",
sshConfig: "",
projectRoot: "",
syncBeforeRun: true,
python: "",
node: "",
java: "",
dockerConnection: "",
image: "",
composeFiles: [],
service: "",
containerRoot: "",
ports: [],
runOptions: "",
imageUser: false,
};
export const DEFAULT_REMOTE: RemoteSettings = {
ssh: { configs: [] },
deployment: DEFAULT_DEPLOYMENT,
runTargets: [],
};
type Obj = Record<string, unknown>;
const obj = (v: unknown): Obj => (v && typeof v === "object" && !Array.isArray(v) ? (v as Obj) : {});
const text = (v: unknown, d: string) => (typeof v === "string" ? v : d);
const bool = (v: unknown, d: boolean) => (typeof v === "boolean" ? v : d);
const strings = (v: unknown, d: string[]) =>
Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : d;
const oneOf = <T extends string>(v: unknown, values: readonly T[], d: T): T => (values.includes(v as T) ? (v as T) : d);
const port = (v: unknown): number | null =>
typeof v === "number" && Number.isInteger(v) && v > 0 && v <= 65535 ? v : null;
/** Items with a unique, non-empty id (duplicates get `_` appended, missing ones `<prefix><n>`). */
function withIds<T extends { id: string }>(raw: unknown, prefix: string, item: (o: Obj) => T | null): T[] {
if (!Array.isArray(raw)) return [];
const seen = new Set<string>();
const out: T[] = [];
raw.forEach((x, i) => {
const v = item(obj(x));
if (!v) return;
let id = v.id.trim() || `${prefix}${i + 1}`;
while (seen.has(id)) id += "_";
seen.add(id);
out.push({ ...v, id });
});
return out;
}
export function validateSshConfigs(raw: unknown): SshConfigValid[] {
const d = DEFAULT_SSH_CONFIG;
return withIds(raw, "ssh", (o) => ({
id: text(o.id, ""),
name: text(o.name, ""),
host: text(o.host, ""),
port: port(o.port),
user: text(o.user, ""),
auth: oneOf(o.auth, ["password", "keyPair", "openSsh"] as const, d.auth),
keyFile: text(o.keyFile, ""),
saveSecret: bool(o.saveSecret, d.saveSecret),
}));
}
function validateMappings(raw: unknown): DeploymentMappingValid[] {
if (!Array.isArray(raw)) return [];
return raw.map((m) => {
const o = obj(m);
return { local: text(o.local, ""), deployment: text(o.deployment, ""), web: text(o.web, "") };
});
}
export function validateDeployment(raw: unknown): DeploymentSettingsValid {
const o = obj(raw);
const d = DEFAULT_DEPLOYMENT;
const servers = withIds(o.servers, "server", (s) => ({
kind: oneOf(s.kind, ["sftp", "local"] as const, "sftp"),
id: text(s.id, ""),
name: text(s.name, ""),
sshConfig: text(s.sshConfig, ""),
rootPath: text(s.rootPath, ""),
webUrl: text(s.webUrl, ""),
mappings: validateMappings(s.mappings),
excludedPaths: strings(s.excludedPaths, []),
}));
return {
servers,
defaultServer: text(o.defaultServer, d.defaultServer),
autoUpload: oneOf(o.autoUpload, ["never", "onExplicitSave", "always"] as const, d.autoUpload),
deleteRemote: bool(o.deleteRemote, d.deleteRemote),
preserveTimestamps: bool(o.preserveTimestamps, d.preserveTimestamps),
exclude: strings(o.exclude, d.exclude),
};
}
export function validateRunTargets(raw: unknown): RunTargetValid[] {
const d = DEFAULT_RUN_TARGET;
return withIds(raw, "target", (o) => ({
kind: oneOf(o.kind, ["ssh", "docker", "compose"] as const, d.kind),
id: text(o.id, ""),
name: text(o.name, ""),
sshConfig: text(o.sshConfig, ""),
projectRoot: text(o.projectRoot, ""),
syncBeforeRun: bool(o.syncBeforeRun, d.syncBeforeRun),
python: text(o.python, ""),
node: text(o.node, ""),
java: text(o.java, ""),
dockerConnection: text(o.dockerConnection, ""),
image: text(o.image, ""),
composeFiles: strings(o.composeFiles, []),
service: text(o.service, ""),
containerRoot: text(o.containerRoot, ""),
ports: strings(o.ports, []),
runOptions: text(o.runOptions, ""),
imageUser: bool(o.imageUser, d.imageUser),
}));
}
export function validateRemote(root: unknown): RemoteSettings {
const o = obj(root);
return {
ssh: { configs: validateSshConfigs(obj(o.ssh).configs) },
deployment: validateDeployment(o.deployment),
runTargets: validateRunTargets(o.runTargets),
};
}
/** `user@host:port` with what is known (the name when the host is empty). */
export function sshLabel(c: Pick<SshConfigValid, "name" | "host" | "port" | "user">): string {
const host = c.host.trim();
if (!host) return c.name;
return `${c.user.trim() ? `${c.user.trim()}@` : ""}${host}${c.port ? `:${c.port}` : ""}`;
}
let seq = 0;
/** A fresh id for a new SSH configuration, server or target. */
export const newRemoteId = (prefix: string) => `${prefix}-${Date.now().toString(36)}-${++seq}`;
/** Whether `target` runs configurations in a container (Docker, Compose) rather than over SSH. */
export const inContainer = (target: Pick<RunTargetValid, "kind">) => target.kind !== "ssh";
/** Backend connection descriptor for deployment and folder browsing. */
export function deploymentConnection(server: DeploymentServerValid): { kind: "local" } | { kind: "sftp"; ssh: string } {
return server.kind === "local" ? { kind: "local" } : { kind: "sftp", ssh: server.sshConfig };
}