refactor(dsh): collect environment management under src/environment

Move the Host modules out of src/pyenv and the tab out of src/client so
the feature has one boundary:

  src/environment/types.ts        browser-safe RPC contract
  src/environment/host/*.ts       detection, install, service, workspace
  src/environment/client/*        the Environment tab
  src/environment/host/gateway    remeEnv RPC surface
  src/environment/host/manager    the operations the gateway exposes

The cross-boundary shapes (detection, reme-ai check, install job, .env
summary, service state) now live in one browser-safe module instead of
being pulled from Host modules that import node built-ins; Host-only
shapes stay beside the code that owns them. src/index.ts registers the
gateway and manager, src/client/index.tsx mounts the tab. The wire
namespace, method names and service keys are unchanged.
This commit is contained in:
LiangshouX 2026-10-28 16:20:25 +08:00
parent 11d2e90be7
commit 6261018436
19 changed files with 195 additions and 175 deletions

View file

@ -4,11 +4,11 @@ import type { ConfigForm } from "@deepseek-ai/dsh-client-ui-settings/client";
import type { ReMeSettings } from "../types.js";
import { validEndpoint } from "../endpoint.js";
import {
ReMeEnvironmentTab,
envEn,
envZh,
ReMeEnvironmentTab,
type EnvTranslator,
} from "./env-tab.js";
} from "../environment/client/tab.js";
import {
ReMeStatusPage,
statusEn,

View file

@ -1,23 +1,22 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import type { ConfigForm } from "@deepseek-ai/dsh-client-ui-settings/client";
import { safeError } from "../endpoint.js";
import type { InstallStatus } from "../pyenv/install.js";
import type { RemeCheck } from "../pyenv/reme.js";
import type {
ServiceLog,
ServiceStartResult,
ServiceState,
} from "../pyenv/service.js";
import { safeError } from "../../endpoint.js";
import { ReMeClient } from "../../reme/client.js";
import type { ReMeSettings } from "../../types.js";
import type { StatusRpc } from "../../client/status-page.js";
import type {
EnvSummary,
InstallStatus,
LlmTestResult,
PythonDetection,
PythonEnv,
PythonOrigin,
} from "../pyenv/types.js";
import type { EnvSummary, LlmTestResult } from "../pyenv/workspace.js";
import { ReMeClient } from "../reme/client.js";
import type { ReMeSettings } from "../types.js";
import type { StatusRpc } from "./status-page.js";
RemeCheck,
ServiceLog,
ServiceStartResult,
ServiceState,
} from "../types.js";
const MINICONDA_URL = "https://docs.conda.io/en/latest/miniconda.html";

View file

@ -1,10 +1,10 @@
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { join as posixJoin } from "node:path/posix";
import { safeError } from "../endpoint.js";
import { safeError } from "../../endpoint.js";
import type { RunCommand, RunResult } from "./runner.js";
import { probeReme, remeVersionAtLeast } from "./reme.js";
import type { PythonDetection, PythonEnv, PythonOrigin } from "./types.js";
import type { PythonDetection, PythonEnv, PythonOrigin } from "../types.js";
/** ReMe requires Python 3.11 or newer. */
export const MINIMUM_PYTHON = "3.11";

View file

@ -1,16 +1,17 @@
import type { Context } from "@deepseek-ai/cordis";
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
import type { InstallStatus } from "./pyenv/install.js";
import type { RemeCheck } from "./pyenv/reme.js";
import type {
EnvSummary,
InstallStatus,
LlmTestResult,
PythonDetection,
RemeCheck,
ServiceLog,
ServiceStartResult,
ServiceState,
} from "./pyenv/service.js";
import type { PythonDetection } from "./pyenv/types.js";
import type { EnvSummary, LlmTestResult } from "./pyenv/workspace.js";
import type { ReMeEnvManager } from "./env-manager.js";
} from "../types.js";
import type { ReMeEnvManager } from "./manager.js";
declare module "@deepseek-ai/cordis" {
interface Context {

View file

@ -2,30 +2,11 @@ import { spawn } from "node:child_process";
import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
import { isAbsolute, join } from "node:path";
export type InstallPhase = "running" | "succeeded" | "failed" | "cancelled";
export interface InstallRequest {
/** Absolute interpreter that receives the packages. */
pythonPath: string;
/** Upgrade an existing installation to the newest satisfying version. */
upgrade?: boolean;
/** Optional package index URL (mirror); empty keeps the user's pip config. */
indexUrl?: string;
}
export interface InstallStatus {
id: string;
phase: InstallPhase;
pythonPath: string;
/** Effective command line, for display and diagnostics. */
command: string;
startedAt: string;
finishedAt?: string;
exitCode?: number | null;
logPath: string;
/** Tail of the install log; safe to poll. */
logTail: string;
}
import type {
InstallPhase,
InstallRequest,
InstallStatus,
} from "../types.js";
export interface InstallChild {
pid: number | undefined;

View file

@ -2,28 +2,22 @@ import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { detectPythonEnvs } from "./pyenv/detect.js";
import { detectPythonEnvs } from "./detect.js";
import {
createInstallSpawner,
InstallJobs,
tailLog,
type InstallRequest,
type InstallStatus,
} from "./pyenv/install.js";
import { checkReme, remeVersionAtLeast, type RemeCheck } from "./pyenv/reme.js";
import { createNodeRunner } from "./pyenv/runner.js";
} from "./install.js";
import { checkReme, remeVersionAtLeast } from "./reme.js";
import { createNodeRunner } from "./runner.js";
import {
createServiceIO,
readServiceState,
serviceStatus,
startService,
stopService,
type ServiceIO,
type ServiceLog,
type ServiceStartResult,
type ServiceState,
} from "./pyenv/service.js";
import type { PythonDetection } from "./pyenv/types.js";
} from "./service.js";
import type { ServiceIO } from "./service.js";
import {
envSummary,
parseEnvFile,
@ -31,9 +25,18 @@ import {
testLlm,
validateEnvUpdate,
writeEnvFile,
type EnvSummary,
type LlmTestResult,
} from "./pyenv/workspace.js";
} from "./workspace.js";
import type {
EnvSummary,
InstallRequest,
InstallStatus,
LlmTestResult,
PythonDetection,
RemeCheck,
ServiceLog,
ServiceStartResult,
ServiceState,
} from "../types.js";
const CACHE_MS = 60_000;

View file

@ -1,5 +1,6 @@
import { dirname, isAbsolute, join } from "node:path";
import type { RemeCheck, RemeProbe } from "../types.js";
import type { RunCommand } from "./runner.js";
/** Minimum reme-ai version this DSH integration is tested against. */
@ -14,24 +15,6 @@ export interface RemeIO {
exists(path: string): boolean;
}
export interface RemeProbe {
installed: boolean;
version: string;
/** True when the version satisfies {@link MINIMUM_REME}. */
versionOk: boolean;
}
export interface RemeCheck extends RemeProbe {
/** True when the installed version satisfies {@link MINIMUM_REME}. */
versionOk: boolean;
/** True when the AgentScope `[core]` extras import (agentscope) works. */
hasCore: boolean;
/** Absolute reme CLI path, or "" when only the python wrapper is available. */
cliPath: string;
/** Non-empty when the interpreter itself could not be probed. */
error: string;
}
/** Numeric dotted-version comparison; returns -1, 0 or 1. */
export function compareVersions(a: string, b: string): number {
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);

View file

@ -11,32 +11,7 @@ import {
import { createConnection } from "node:net";
import { dirname } from "node:path";
export interface ServiceState {
phase: "running" | "stopped" | "conflict";
/** True when the running service was started by this plugin. */
managed: boolean;
endpoint: string;
pid?: number;
version?: string;
startedAt?: string;
logPath: string;
}
export interface ServiceStartResult {
started: boolean;
alreadyRunning: boolean;
pid?: number;
version?: string;
command: string;
logPath: string;
startedAt: string;
error?: string;
}
export interface ServiceLog {
logPath: string;
lines: string;
}
import type { ServiceStartResult, ServiceState } from "../types.js";
export interface PersistedService {
pid?: number;

View file

@ -1,7 +1,8 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { safeError } from "../endpoint.js";
import { safeError } from "../../endpoint.js";
import type { EnvSummary, LlmTestResult } from "../types.js";
import type { RunCommand } from "./runner.js";
/** Keys the Environment tab manages; split for the two form groups. */
@ -20,31 +21,6 @@ export const EMBEDDING_KEYS = [
const MANAGED_KEYS: readonly string[] = [...LLM_KEYS, ...EMBEDDING_KEYS];
const SECRET_KEYS = new Set(["LLM_API_KEY", "EMBEDDING_API_KEY"]);
export interface EnvEntry {
key: string;
secret: boolean;
/** True when the key has a non-empty value on disk. */
set: boolean;
/** Masked tail for secrets; the plain value for non-secret keys. */
masked: string;
/** Plain value for non-secret keys; empty for secrets. */
value: string;
}
export interface EnvSummary {
workspaceDir: string;
envPath: string;
exists: boolean;
entries: EnvEntry[];
}
export interface LlmTestResult {
ok: boolean;
status: number;
latencyMs: number;
error: string;
}
/** Empty means "~/.reme"; "~" expands against home; relative paths are rejected. */
export function resolveWorkspaceDir(raw: string, home: string): string {
const trimmed = (raw ?? "").trim();

View file

@ -0,0 +1,136 @@
/**
* Environment contract shared by the DSH Host and the Environment tab.
*
* Everything here crosses the `remeEnv` RPC boundary, so it stays browser-safe:
* this module must not import a node built-in. Host-only shapes (process I/O,
* spawners, persisted state) live beside the code that owns them.
*/
/** A local Python interpreter discovered on the DSH Host. */
export type PythonOrigin =
| { kind: "conda"; envName: string; root: string }
| { kind: "python.org" }
| { kind: "windows-store" }
| { kind: "venv"; baseExe: string }
| { kind: "system" }
| { kind: "unknown" };
export interface PythonEnv {
/** Verified interpreter executable (absolute, host-native separators). */
exe: string;
/** "major.minor.patch" reported by the interpreter; empty when unusable. */
version: string;
/** True when the interpreter answered and satisfies ReMe (Python >= 3.11). */
ok: boolean;
origin: PythonOrigin;
/** Human-readable discovery channels, e.g. "conda env list; PATH". */
source: string;
/** reme-ai installation facts, probed for the interpreter recommendation. */
reme?: { installed: boolean; version: string; versionOk?: boolean };
}
export interface PythonDetection {
platform: string;
scannedAt: string;
envs: PythonEnv[];
/** Preferred interpreter for a local service, or "" when none is usable. */
suggested: string;
/** Conda installation root, or "" when conda was not found. */
condaRoot: string;
/** Non-fatal probe failures worth showing for transparency. */
errors: string[];
}
/** Installed `reme-ai` facts for one interpreter. */
export interface RemeProbe {
installed: boolean;
version: string;
/** True when the version satisfies the minimum this integration targets. */
versionOk: boolean;
}
export interface RemeCheck extends RemeProbe {
/** True when the AgentScope `[core]` extras import (agentscope) works. */
hasCore: boolean;
/** Absolute reme CLI path, or "" when only the python wrapper is available. */
cliPath: string;
/** Non-empty when the interpreter itself could not be probed. */
error: string;
}
export type InstallPhase = "running" | "succeeded" | "failed" | "cancelled";
export interface InstallRequest {
/** Absolute interpreter that receives the packages. */
pythonPath: string;
/** Upgrade an existing installation to the newest satisfying version. */
upgrade?: boolean;
/** Optional package index URL (mirror); empty keeps the user's pip config. */
indexUrl?: string;
}
export interface InstallStatus {
id: string;
phase: InstallPhase;
pythonPath: string;
/** Effective command line, for display and diagnostics. */
command: string;
startedAt: string;
finishedAt?: string;
exitCode?: number | null;
logPath: string;
/** Tail of the install log; safe to poll. */
logTail: string;
}
export interface EnvEntry {
key: string;
secret: boolean;
/** True when the key has a non-empty value on disk. */
set: boolean;
/** Masked tail for secrets; the plain value for non-secret keys. */
masked: string;
/** Plain value for non-secret keys; empty for secrets. */
value: string;
}
export interface EnvSummary {
workspaceDir: string;
envPath: string;
exists: boolean;
entries: EnvEntry[];
}
export interface LlmTestResult {
ok: boolean;
status: number;
latencyMs: number;
error: string;
}
export interface ServiceState {
phase: "running" | "stopped" | "conflict";
/** True when the running service was started by this plugin. */
managed: boolean;
endpoint: string;
pid?: number;
version?: string;
startedAt?: string;
logPath: string;
}
export interface ServiceStartResult {
started: boolean;
alreadyRunning: boolean;
pid?: number;
version?: string;
command: string;
logPath: string;
startedAt: string;
error?: string;
}
export interface ServiceLog {
logPath: string;
lines: string;
}

View file

@ -6,8 +6,8 @@ import type {} from "@deepseek-ai/dsh-session";
import { ReMeClient } from "./reme/client.js";
import { resolveConfig } from "./config.js";
import { ReMeEnvGateway } from "./env-gateway.js";
import { ReMeEnvManager } from "./env-manager.js";
import { ReMeEnvGateway } from "./environment/host/gateway.js";
import { ReMeEnvManager } from "./environment/host/manager.js";
import { hasGuidance, memoryGuidance, REME_PLUGIN_SOURCE } from "./guidance.js";
import { ReMeRuntime } from "./runtime.js";
import { ReMeStatusGateway } from "./status-gateway.js";

View file

@ -1,34 +0,0 @@
/** A local Python interpreter discovered on this machine. */
export type PythonOrigin =
| { kind: "conda"; envName: string; root: string }
| { kind: "python.org" }
| { kind: "windows-store" }
| { kind: "venv"; baseExe: string }
| { kind: "system" }
| { kind: "unknown" };
export interface PythonEnv {
/** Verified interpreter executable (absolute, host-native separators). */
exe: string;
/** "major.minor.patch" reported by the interpreter; empty when unusable. */
version: string;
/** True when the interpreter answered and satisfies ReMe (Python >= 3.11). */
ok: boolean;
origin: PythonOrigin;
/** Human-readable discovery channels, e.g. "conda env list; PATH". */
source: string;
/** reme-ai installation facts, probed for the interpreter recommendation. */
reme?: { installed: boolean; version: string; versionOk?: boolean };
}
export interface PythonDetection {
platform: string;
scannedAt: string;
envs: PythonEnv[];
/** Preferred interpreter for a local service, or "" when none is usable. */
suggested: string;
/** Conda installation root, or "" when conda was not found. */
condaRoot: string;
/** Non-fatal probe failures worth showing for transparency. */
errors: string[];
}

View file

@ -14,7 +14,7 @@ import {
parseVerifyOutput,
suggestInterpreter,
versionAtLeast,
} from "../dist/pyenv/detect.js";
} from "../dist/environment/host/detect.js";
function fakeRun(rules) {
return async (exe, args) => {

View file

@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import { Context } from "@deepseek-ai/cordis";
import { ReMeEnvGateway } from "../dist/env-gateway.js";
import { ReMeEnvGateway } from "../dist/environment/host/gateway.js";
/** Manager stub: no subprocesses, just enough surface for the gateway. */
function stubManager() {

View file

@ -9,8 +9,8 @@ import {
compareVersions,
parseRemeVersion,
remeVersionAtLeast,
} from "../dist/pyenv/reme.js";
import { InstallJobs, pipArgs, tailLog } from "../dist/pyenv/install.js";
} from "../dist/environment/host/reme.js";
import { InstallJobs, pipArgs, tailLog } from "../dist/environment/host/install.js";
const SNIPPETS = {
version: "importlib.metadata",

View file

@ -12,7 +12,7 @@ import {
startService,
stopService,
writeServiceState,
} from "../dist/pyenv/service.js";
} from "../dist/environment/host/service.js";
function fakeIO(overrides = {}) {
const calls = { killTree: [], spawn: [] };

View file

@ -4,7 +4,7 @@ import test from "node:test";
import React from "react";
import { act, create } from "react-test-renderer";
import { ReMeEnvironmentTab, envZh } from "../dist/client/env-tab.js";
import { ReMeEnvironmentTab, envZh } from "../dist/environment/client/tab.js";
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

View file

@ -13,7 +13,7 @@ import {
resolveWorkspaceDir,
testLlm,
validateEnvUpdate,
} from "../dist/pyenv/workspace.js";
} from "../dist/environment/host/workspace.js";
test("resolves the workspace directory with ~/.reme defaults", () => {
const home = process.platform === "win32" ? "C:\\Users\\u" : "/home/u";