@woosau/dsh-custom-env 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +132 -2
- package/README.zh-CN.md +133 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +864 -0
- package/lib/index.js +1053 -0
- package/lib/typert.host.d.ts +5 -0
- package/lib/typert.host.js +295 -0
- package/lib/typert.remote-client.d.ts +15 -0
- package/lib/typert.remote-client.js +231 -0
- package/lib/types/cli-path/index.d.ts +12 -0
- package/lib/types/cli-path/inspect-active.d.ts +33 -0
- package/lib/types/cli-path/macos.d.ts +21 -0
- package/lib/types/cli-path/paths.d.ts +46 -0
- package/lib/types/cli-path/resolve-expected.d.ts +24 -0
- package/lib/types/cli-path/status.d.ts +32 -0
- package/lib/types/cli-path/windows.d.ts +26 -0
- package/lib/types/client/EnvSettingsSection.d.ts +20 -0
- package/lib/types/client/index.d.ts +17 -0
- package/lib/types/client/locales.d.ts +9 -0
- package/lib/types/client/mount.d.ts +11 -0
- package/lib/types/env-file.d.ts +40 -0
- package/lib/types/index.d.ts +60 -0
- package/lib/types/names.d.ts +16 -0
- package/lib/types/os-user-env/index.d.ts +13 -0
- package/lib/types/os-user-env/macos.d.ts +15 -0
- package/lib/types/os-user-env/windows.d.ts +37 -0
- package/lib/types/types.d.ts +44 -0
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +87 -4
- package/scripts/command-path.ps1 +150 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Infer the CLI launcher this Host process should expose on PATH. */
|
|
2
|
+
/** Resolved expected CLI locations, or a reason when resolution fails. */
|
|
3
|
+
export interface ExpectedCli {
|
|
4
|
+
/** Absolute path of the launcher file (`dsh` / `dsh.cmd`). */
|
|
5
|
+
readonly launcher: string;
|
|
6
|
+
/** Directory that should appear on PATH (Windows) or be the symlink target's directory. */
|
|
7
|
+
readonly directory: string;
|
|
8
|
+
/** How the expectation was derived. */
|
|
9
|
+
readonly kind: 'electron-resources' | 'argv' | 'walk';
|
|
10
|
+
}
|
|
11
|
+
/** Inputs for tests; defaults to the live process. */
|
|
12
|
+
export interface ResolveExpectedOptions {
|
|
13
|
+
readonly platform?: NodeJS.Platform;
|
|
14
|
+
readonly execPath?: string;
|
|
15
|
+
readonly argv?: readonly string[];
|
|
16
|
+
readonly electronRunAsNode?: string | undefined;
|
|
17
|
+
readonly exists?: (path: string) => boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Infer the expected dsh launcher from how this Host was started.
|
|
21
|
+
* @param options - optional process overrides for tests.
|
|
22
|
+
* @returns expected locations, or `undefined` when unknown.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveExpectedCli(options?: ResolveExpectedOptions): ExpectedCli | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Compose CLI PATH status for the settings UI. */
|
|
2
|
+
import { type ActiveCli } from './inspect-active.ts';
|
|
3
|
+
import { type ExpectedCli } from './resolve-expected.ts';
|
|
4
|
+
/** Snapshot returned by `customEnv/cliStatus` and `cliInstall`. */
|
|
5
|
+
export interface CliStatus {
|
|
6
|
+
/** Host platform. */
|
|
7
|
+
readonly platform: NodeJS.Platform;
|
|
8
|
+
/** First `dsh` found on the user environment PATH, if any. */
|
|
9
|
+
readonly activePath: string | null;
|
|
10
|
+
/** Expected launcher for this Host, if resolved. */
|
|
11
|
+
readonly expectedPath: string | null;
|
|
12
|
+
/** Expected bin directory (Windows PATH entry). */
|
|
13
|
+
readonly expectedDirectory: string | null;
|
|
14
|
+
/** Whether active matches expected. */
|
|
15
|
+
readonly matches: boolean;
|
|
16
|
+
/** Whether the Install button should be shown. */
|
|
17
|
+
readonly canInstall: boolean;
|
|
18
|
+
/** Why install is unavailable or paths could not be resolved. */
|
|
19
|
+
readonly reason: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** Optional overrides for tests / Windows registry inspect. */
|
|
22
|
+
export interface BuildCliStatusOptions {
|
|
23
|
+
readonly platform?: NodeJS.Platform;
|
|
24
|
+
readonly expected?: ExpectedCli | undefined;
|
|
25
|
+
readonly active?: ActiveCli;
|
|
26
|
+
readonly resolveFailedReason?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a status object from expected + active probes.
|
|
30
|
+
* @param options - optional overrides.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildCliStatus(options?: BuildCliStatusOptions): CliStatus;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Windows: inspect / install dsh onto the user PATH via adapted command-path.ps1. */
|
|
2
|
+
/** State returned by the PowerShell worker. */
|
|
3
|
+
export interface WindowsCommandState {
|
|
4
|
+
readonly fingerprint: string;
|
|
5
|
+
readonly directory: string;
|
|
6
|
+
readonly activeCommand?: string;
|
|
7
|
+
readonly available: boolean;
|
|
8
|
+
readonly managed: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare class WindowsCommandError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
constructor(code: string, message: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the packaged `scripts/command-path.ps1` next to this module's package root.
|
|
16
|
+
* Works for `lib/index.js` (bundled) and source layouts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function commandPathScript(fromUrl?: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Run inspect or install against the user PATH.
|
|
21
|
+
* @param operation - `inspect` or `install`.
|
|
22
|
+
* @param directory - absolute directory containing `dsh.cmd`.
|
|
23
|
+
* @param expected - fingerprint from a prior inspect (required for install).
|
|
24
|
+
*/
|
|
25
|
+
export declare function runWindowsCommandPath(operation: 'inspect' | 'install', directory: string, expected?: string): Promise<WindowsCommandState>;
|
|
26
|
+
export { WindowsCommandError };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Settings section: `$DSH_HOME/.env` editor + global `dsh` path. */
|
|
2
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { CliStatus, EnvSnapshot } from '../types.ts';
|
|
4
|
+
import type { EnvSettingsKey } from './locales.ts';
|
|
5
|
+
export interface EnvSettingsInjected {
|
|
6
|
+
readonly load: () => Promise<EnvSnapshot>;
|
|
7
|
+
/** Create (`id === ""`) or update/rename one row by stable id. */
|
|
8
|
+
readonly upsert: (id: string, name: string, value: string) => Promise<EnvSnapshot>;
|
|
9
|
+
readonly remove: (id: string) => Promise<EnvSnapshot>;
|
|
10
|
+
readonly restore: (id: string) => Promise<EnvSnapshot>;
|
|
11
|
+
readonly cliStatus: () => Promise<CliStatus>;
|
|
12
|
+
readonly cliInstall: () => Promise<CliStatus>;
|
|
13
|
+
}
|
|
14
|
+
export type EnvSettingsSectionProps = PropsRuntime<'settings.section'> & PropsLocale<'settings.env'> & InjectFace<EnvSettingsInjected>;
|
|
15
|
+
/**
|
|
16
|
+
* Render the environment settings page.
|
|
17
|
+
* @param props - locale `t` and Host remote callbacks.
|
|
18
|
+
*/
|
|
19
|
+
export declare function EnvSettingsSection({ t, load, upsert, remove, restore, cliStatus, cliInstall, }: EnvSettingsSectionProps): any;
|
|
20
|
+
export type { EnvSettingsKey };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Browser entry for `$DSH_HOME/.env` settings. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
export { inject } from './mount.ts';
|
|
4
|
+
export type { EnvSettingsKey } from './locales.ts';
|
|
5
|
+
export type { EnvSettingsInjected, EnvSettingsSectionProps } from './EnvSettingsSection.tsx';
|
|
6
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
7
|
+
interface LocaleNamespaceMap {
|
|
8
|
+
/** Environment variables settings copy. */
|
|
9
|
+
'settings.env': import('./locales.ts').EnvSettingsKey;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mount the environment settings page and its Remote methods.
|
|
14
|
+
* @param ctx - browser plugin context.
|
|
15
|
+
* @returns disposer joining UI and Remote withdrawal.
|
|
16
|
+
*/
|
|
17
|
+
export declare function apply(ctx: Context): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Locale dictionaries for the environment-variables settings section. */
|
|
2
|
+
/** Dictionary namespace owned by this plugin. */
|
|
3
|
+
export declare const NS = "settings.env";
|
|
4
|
+
/** Keys in the environment settings dictionaries. */
|
|
5
|
+
export type EnvSettingsKey = 'nav' | 'title' | 'empty' | 'name' | 'value' | 'actions' | 'confirm' | 'cancel' | 'remove' | 'removeConfirmTitle' | 'removeConfirm' | 'restore' | 'close' | 'namePlaceholder' | 'valuePlaceholder' | 'loading' | 'error' | 'invalidName' | 'restartRequired' | 'cliTitle' | 'cliActiveMissing' | 'cliInstall' | 'cliMacConflict' | 'cliInstallError' | 'cliReasonUnresolved' | 'cliReasonUnsupported';
|
|
6
|
+
/** English copy. */
|
|
7
|
+
export declare const en: Record<EnvSettingsKey, string>;
|
|
8
|
+
/** Chinese copy. */
|
|
9
|
+
export declare const zh: Record<EnvSettingsKey, string>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Mount Remote contribution and the settings.section page. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
|
|
4
|
+
export declare const inject: readonly ["remote", "slots", "locale"];
|
|
5
|
+
/**
|
|
6
|
+
* Mount the custom-env Remote and settings UI.
|
|
7
|
+
* @param ctx - Client plugin lifetime.
|
|
8
|
+
* @param contribution - generated controller Remote definitions.
|
|
9
|
+
* @returns disposer that removes both contributions.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mountCustomEnv(ctx: Context, contribution: TypertRemoteContribution): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Line-oriented `$DSH_HOME/.env` read/write helpers. */
|
|
2
|
+
/** Name/value pair before Host baseline classification. */
|
|
3
|
+
export interface RawEnvEntry {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly value: string;
|
|
6
|
+
}
|
|
7
|
+
/** Absolute path of `$DSH_HOME/.env`. */
|
|
8
|
+
export declare function envFilePath(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Read non-empty writable entries from `$DSH_HOME/.env`.
|
|
11
|
+
* @returns Variable map, or `{}` when absent / empty.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadEnvMap(): Readonly<Record<string, string>>;
|
|
14
|
+
/**
|
|
15
|
+
* List writable entries in `.env` file order (new keys stay at the end).
|
|
16
|
+
* @returns Path, existence, and ordered entries.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listEnvEntries(): {
|
|
19
|
+
path: string;
|
|
20
|
+
exists: boolean;
|
|
21
|
+
entries: RawEnvEntry[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Write `name=value` in place of `replaceName` (keeps order), or append when missing.
|
|
25
|
+
* Use the same name for both to update; different names to rename in place.
|
|
26
|
+
* @param replaceName - line to replace.
|
|
27
|
+
* @param name - name to store.
|
|
28
|
+
* @param value - non-empty value.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeEnvEntry(replaceName: string, name: string, value: string): void;
|
|
31
|
+
/** Create or replace one key (append when new). */
|
|
32
|
+
export declare function upsertEnvEntry(name: string, value: string): void;
|
|
33
|
+
/** Rename one key in place (keeps file order). */
|
|
34
|
+
export declare function renameEnvEntry(from: string, to: string, value: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Remove one key from `$DSH_HOME/.env` when present.
|
|
37
|
+
* @param name - variable name.
|
|
38
|
+
* @returns true when a line was removed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function removeEnvEntry(name: string): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host service: load `$DSH_HOME/.env` into process.env and expose CRUD remotes.
|
|
3
|
+
*/
|
|
4
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
6
|
+
import { envFilePath } from './env-file.ts';
|
|
7
|
+
import { type CliStatus, type EnvSnapshot } from './types.ts';
|
|
8
|
+
export type { BaselineRow, CliStatus, EnvEntry, EnvSnapshot } from './types.ts';
|
|
9
|
+
export { classifyEntry } from './types.ts';
|
|
10
|
+
declare module '@deepseek-ai/cordis' {
|
|
11
|
+
interface Context {
|
|
12
|
+
customEnv: CustomEnvController;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
16
|
+
interface RemoteErrorDetailsMap {
|
|
17
|
+
'custom-env/invalid-name': {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
};
|
|
20
|
+
'custom-env/invalid-value': {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
};
|
|
23
|
+
'custom-env/cli-install-failed': {
|
|
24
|
+
readonly code: string;
|
|
25
|
+
};
|
|
26
|
+
'custom-env/restore-failed': {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Host remote for `$DSH_HOME/.env` CRUD with live Host/OS sync. */
|
|
32
|
+
export default class CustomEnvController extends TypertRemoteService {
|
|
33
|
+
private readonly baselineById;
|
|
34
|
+
private readonly idByName;
|
|
35
|
+
private readonly nameById;
|
|
36
|
+
private idSeq;
|
|
37
|
+
constructor(ctx: Context);
|
|
38
|
+
private allocId;
|
|
39
|
+
private bind;
|
|
40
|
+
private unbind;
|
|
41
|
+
private requireName;
|
|
42
|
+
/** Persist `name=value` replacing `from` in place; sync process.env / OS. */
|
|
43
|
+
private commit;
|
|
44
|
+
/** @returns path, existence, and file-ordered entries. */
|
|
45
|
+
list(): Promise<EnvSnapshot>;
|
|
46
|
+
/**
|
|
47
|
+
* Create or update one row. Empty `id` creates; otherwise keeps that baseline id across renames.
|
|
48
|
+
* @param id - row id, or `""` for a new row.
|
|
49
|
+
* @param name - variable name.
|
|
50
|
+
* @param value - non-empty value.
|
|
51
|
+
*/
|
|
52
|
+
upsert(id: string, name: string, value: string): Promise<EnvSnapshot>;
|
|
53
|
+
/** @param id - row id from list(). */
|
|
54
|
+
deleteEntry(id: string): Promise<EnvSnapshot>;
|
|
55
|
+
/** Restore Host-start original name+value, or delete if added after start. */
|
|
56
|
+
restore(id: string): Promise<EnvSnapshot>;
|
|
57
|
+
cliStatus(): Promise<CliStatus>;
|
|
58
|
+
cliInstall(): Promise<CliStatus>;
|
|
59
|
+
}
|
|
60
|
+
export { envFilePath };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names and prefixes that must not be written through `$DSH_HOME/.env`
|
|
3
|
+
* (mirrors app-boot bootstrap rejection).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Whether a variable may come only from the inherited process environment.
|
|
7
|
+
* @param name - candidate variable name.
|
|
8
|
+
* @returns true when `.env` must not set this name.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isBootstrapOnly(name: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `name` is a writable non-bootstrap environment key.
|
|
13
|
+
* @param name - candidate variable name.
|
|
14
|
+
* @returns true when the key may be stored in `$DSH_HOME/.env`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isWritableEnvName(name: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** OS user-environment sync for Windows and macOS (no-op elsewhere). */
|
|
2
|
+
/**
|
|
3
|
+
* Persist entries into the current user's OS environment when supported.
|
|
4
|
+
* @param entries - Variable map to write.
|
|
5
|
+
*/
|
|
6
|
+
export declare function persistUserEnv(entries: Readonly<Record<string, string>>): void;
|
|
7
|
+
/**
|
|
8
|
+
* Remove one key from the current user's OS environment when supported.
|
|
9
|
+
* @param name - Variable name to delete.
|
|
10
|
+
*/
|
|
11
|
+
export declare function removeUserEnv(name: string): void;
|
|
12
|
+
export { persistWindowsUserEnv, removeWindowsUserEnv } from './windows.ts';
|
|
13
|
+
export { persistMacosUserEnv, removeMacosUserEnv } from './macos.ts';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist user environment variables on macOS via launchctl and a Login LaunchAgent.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Set user-domain environment variables for new GUI sessions and refresh the Login Agent.
|
|
6
|
+
* @param entries - Variable map to persist.
|
|
7
|
+
* @param platform - Override for tests; defaults to `process.platform`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function persistMacosUserEnv(entries: Readonly<Record<string, string>>, platform?: NodeJS.Platform): void;
|
|
10
|
+
/**
|
|
11
|
+
* Unset one user-domain environment variable and refresh the Login Agent.
|
|
12
|
+
* @param name - Variable name to remove.
|
|
13
|
+
* @param platform - Override for tests; defaults to `process.platform`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function removeMacosUserEnv(name: string, platform?: NodeJS.Platform): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist or delete variables in the Windows user environment (HKCU\\Environment)
|
|
3
|
+
* via koffi + advapi32/user32 — same approach as the former in-tree custom-env.
|
|
4
|
+
*/
|
|
5
|
+
/** Win32 registry and broadcast calls used to publish user environment values. */
|
|
6
|
+
export interface WindowsUserEnvBindings {
|
|
7
|
+
readonly openKey: (hKey: number, subKey: string, options: number, samDesired: number, result: [unknown]) => number;
|
|
8
|
+
readonly setValue: (hKey: unknown, valueName: string, reserved: number, type: number, data: Buffer, cbData: number) => number;
|
|
9
|
+
readonly deleteValue: (hKey: unknown, valueName: string) => number;
|
|
10
|
+
readonly closeKey: (hKey: unknown) => number;
|
|
11
|
+
readonly sendMessageTimeout: (hWnd: number, msg: number, wParam: number, lParam: string, flags: number, timeoutMs: number, result: [number]) => number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Install FFI bindings for tests (skips loading koffi).
|
|
15
|
+
* @param next - Mock bindings, or `undefined` to clear the cache.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setWindowsUserEnvBindingsForTests(next: WindowsUserEnvBindings | undefined): void;
|
|
18
|
+
/**
|
|
19
|
+
* Encode a REG_SZ payload (UTF-16LE including trailing NUL).
|
|
20
|
+
* @param value - String to store.
|
|
21
|
+
* @returns Buffer accepted by RegSetValueExW.
|
|
22
|
+
*/
|
|
23
|
+
export declare function encodeRegSz(value: string): Buffer;
|
|
24
|
+
/**
|
|
25
|
+
* Write entries into HKCU\\Environment and broadcast WM_SETTINGCHANGE.
|
|
26
|
+
* No-op on non-Windows hosts or when `entries` is empty.
|
|
27
|
+
* @param entries - Variable map to persist.
|
|
28
|
+
* @param platform - Override for tests; defaults to `process.platform`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function persistWindowsUserEnv(entries: Readonly<Record<string, string>>, platform?: NodeJS.Platform): void;
|
|
31
|
+
/**
|
|
32
|
+
* Delete one user environment value from HKCU\\Environment.
|
|
33
|
+
* Missing values are ignored.
|
|
34
|
+
* @param name - Variable name to remove.
|
|
35
|
+
* @param platform - Override for tests; defaults to `process.platform`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function removeWindowsUserEnv(name: string, platform?: NodeJS.Platform): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Session types and Host-start baseline classification. */
|
|
2
|
+
export type EnvEntryKind = 'unchanged' | 'modified' | 'added';
|
|
3
|
+
/** Host-start original name and value for one row id. */
|
|
4
|
+
export interface BaselineRow {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly value: string;
|
|
7
|
+
}
|
|
8
|
+
/** One editable environment variable row. */
|
|
9
|
+
export interface EnvEntry {
|
|
10
|
+
/** Stable row id; survives renames. */
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly value: string;
|
|
14
|
+
readonly kind: EnvEntryKind;
|
|
15
|
+
/** Host-start original value when `kind === 'modified'`; otherwise `null`. */
|
|
16
|
+
readonly previous: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Snapshot returned by `customEnv/list`. */
|
|
19
|
+
export interface EnvSnapshot {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly exists: boolean;
|
|
22
|
+
readonly entries: readonly EnvEntry[];
|
|
23
|
+
}
|
|
24
|
+
/** Snapshot returned by `customEnv/cliStatus` and `cliInstall`. */
|
|
25
|
+
export interface CliStatus {
|
|
26
|
+
readonly platform: string;
|
|
27
|
+
readonly activePath: string | null;
|
|
28
|
+
readonly expectedPath: string | null;
|
|
29
|
+
readonly expectedDirectory: string | null;
|
|
30
|
+
readonly matches: boolean;
|
|
31
|
+
readonly canInstall: boolean;
|
|
32
|
+
readonly reason: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Classify one row against the Host-start baseline by stable id.
|
|
36
|
+
* @param id - row id.
|
|
37
|
+
* @param name - current file name.
|
|
38
|
+
* @param value - current file value.
|
|
39
|
+
* @param baselineById - Host-start originals.
|
|
40
|
+
*/
|
|
41
|
+
export declare function classifyEntry(id: string, name: string, value: string, baselineById: ReadonlyMap<string, BaselineRow>): {
|
|
42
|
+
readonly kind: EnvEntryKind;
|
|
43
|
+
readonly previous: string | null;
|
|
44
|
+
};
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woosau/dsh-custom-env",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "DSH plugin: manage $DSH_HOME/.env from Settings, sync Host process.env and Windows/macOS user environment",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./lib/types/client/index.d.ts",
|
|
15
|
+
"default": "./lib/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./remote": {
|
|
18
|
+
"types": "./lib/typert.remote-client.d.ts",
|
|
19
|
+
"default": "./lib/typert.remote-client.js"
|
|
20
|
+
},
|
|
21
|
+
"./typert": {
|
|
22
|
+
"types": "./lib/typert.host.d.ts",
|
|
23
|
+
"default": "./lib/typert.host.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
28
|
+
"./locale/*.json": "./locale/*.json"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"platform": "web",
|
|
36
|
+
"inject": [
|
|
37
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
38
|
+
"@deepseek-ai/dsh-client-locale",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/ownduck/dsh-custom-env.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/ownduck/dsh-custom-env/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/ownduck/dsh-custom-env#readme",
|
|
53
|
+
"engines": {
|
|
54
|
+
"dsh": ">=0.2.0",
|
|
55
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@deepseek-ai/cordis": ">=0.2.0",
|
|
63
|
+
"@deepseek-ai/dsh-home-paths": ">=0.2.0",
|
|
64
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.2.0"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"koffi": "3.1.1",
|
|
68
|
+
"zod": "^4.4.3"
|
|
69
|
+
},
|
|
70
|
+
"files": [
|
|
71
|
+
"locale/*.json",
|
|
72
|
+
"cordis.patch.yml",
|
|
73
|
+
"scripts/command-path.ps1",
|
|
74
|
+
"lib/index.js",
|
|
75
|
+
"lib/client.js",
|
|
76
|
+
"lib/types/**/*.d.ts",
|
|
77
|
+
"lib/typert.host.js",
|
|
78
|
+
"lib/typert.host.d.ts",
|
|
79
|
+
"lib/typert.remote-client.js",
|
|
80
|
+
"lib/typert.remote-client.d.ts",
|
|
81
|
+
"README.md",
|
|
82
|
+
"README.zh-CN.md",
|
|
83
|
+
"LICENSE"
|
|
84
|
+
],
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "node ./scripts/build.mjs",
|
|
87
|
+
"test": "node ./scripts/run-tests.mjs"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<# Per-user PATH ownership for dsh-custom-env (not DeepSeekHarness Desktop). #>
|
|
2
|
+
function Invoke-DshCommandPath {
|
|
3
|
+
param([hashtable]$Request, [string]$EnvironmentKey, [string]$OwnerKey, [string]$MachinePath, [string]$MutexName)
|
|
4
|
+
$ErrorActionPreference = 'Stop'
|
|
5
|
+
function Fail([string]$Code, [string]$Message) {
|
|
6
|
+
$error = [InvalidOperationException]::new($Message)
|
|
7
|
+
$error.Data['code'] = $Code
|
|
8
|
+
throw $error
|
|
9
|
+
}
|
|
10
|
+
function Comparable([string]$Path) {
|
|
11
|
+
$expanded = [Environment]::ExpandEnvironmentVariables($Path.Trim().Trim('"'))
|
|
12
|
+
try {
|
|
13
|
+
if (-not [IO.Path]::IsPathRooted($expanded)) { return $expanded }
|
|
14
|
+
return [IO.Path]::GetFullPath($expanded).TrimEnd('\','/').ToUpperInvariant()
|
|
15
|
+
} catch {
|
|
16
|
+
# Invalid user PATH entries cannot identify the managed directory.
|
|
17
|
+
return $expanded
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
$directory = [string]$Request.directory
|
|
21
|
+
if (-not [IO.Path]::IsPathRooted($directory) -or $directory.Contains(';') -or $directory.Contains([char]0)) {
|
|
22
|
+
Fail 'EUNSUPPORTED' 'The command directory cannot be represented in PATH.'
|
|
23
|
+
}
|
|
24
|
+
$directory = [IO.Path]::GetFullPath($directory)
|
|
25
|
+
if ($Request.operation -notin @('inspect','install','remove')) { Fail 'EINVAL' 'Unknown command operation.' }
|
|
26
|
+
$mutex = [Threading.Mutex]::new($false, $MutexName)
|
|
27
|
+
$locked = $false
|
|
28
|
+
try {
|
|
29
|
+
try { $locked = $mutex.WaitOne(5000) } catch [Threading.AbandonedMutexException] { $locked = $true }
|
|
30
|
+
if (-not $locked) { Fail 'EBUSY' 'Another command-management operation is running.' }
|
|
31
|
+
$environment = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($EnvironmentKey)
|
|
32
|
+
$owner = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($OwnerKey)
|
|
33
|
+
try {
|
|
34
|
+
$raw = if ($environment) { $environment.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } else { $null }
|
|
35
|
+
if ($null -ne $raw -and $raw -isnot [string]) { Fail 'EUNSUPPORTED' 'The user PATH value is not a string.' }
|
|
36
|
+
$kind = if ($null -ne $raw) { $environment.GetValueKind('Path') } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
|
37
|
+
$owned = if ($owner) { $owner.GetValue('Directory', $null) } else { $null }
|
|
38
|
+
$wasAbsent = if ($owner) { $owner.GetValue('PathWasAbsent', 0) -eq 1 } else { $false }
|
|
39
|
+
$retained = if ($owner) { [int]$owner.GetValue('RetainedEntries', 0) } else { 0 }
|
|
40
|
+
} finally {
|
|
41
|
+
if ($environment) { $environment.Dispose() }
|
|
42
|
+
if ($owner) { $owner.Dispose() }
|
|
43
|
+
}
|
|
44
|
+
$snapshot = [ordered]@{ path=$raw; kind=[string]$kind; owned=$owned; absent=$wasAbsent; retained=$retained; directory=$directory; machine=$MachinePath }
|
|
45
|
+
$sha = [Security.Cryptography.SHA256]::Create()
|
|
46
|
+
try { $fingerprint = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes(($snapshot | ConvertTo-Json -Compress))))).Replace('-','').ToLowerInvariant() }
|
|
47
|
+
finally { $sha.Dispose() }
|
|
48
|
+
if ($Request.operation -ne 'inspect' -and $Request.expected -ne $fingerprint) { Fail 'ESTALE' 'PATH changed after confirmation.' }
|
|
49
|
+
$parts = if ($null -eq $raw) { @() } else { @($raw.Split(';')) }
|
|
50
|
+
$current = Comparable $directory
|
|
51
|
+
$old = if ($owned) { Comparable ([string]$owned) } else { $null }
|
|
52
|
+
$kept = $parts
|
|
53
|
+
if ($old -and ($Request.operation -eq 'install' -or ($Request.operation -eq 'remove' -and $old -eq $current))) {
|
|
54
|
+
$ownedMatches = @($parts | Where-Object { $_ -ceq $owned }).Count
|
|
55
|
+
if ($ownedMatches -gt $retained) {
|
|
56
|
+
$index = [Array]::IndexOf($parts, $owned)
|
|
57
|
+
$kept = @(); for ($i = 0; $i -lt $parts.Count; $i++) { if ($i -ne $index) { $kept += $parts[$i] } }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if ($Request.operation -eq 'install') {
|
|
61
|
+
if (-not (Test-Path -LiteralPath (Join-Path $directory 'dsh.cmd') -PathType Leaf)) { Fail 'ENOENT' 'The installed launcher is unavailable.' }
|
|
62
|
+
$next = (@($directory) + $kept) -join ';'
|
|
63
|
+
$environment = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($EnvironmentKey)
|
|
64
|
+
$owner = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($OwnerKey)
|
|
65
|
+
try {
|
|
66
|
+
$environment.SetValue('Path', $next, $kind)
|
|
67
|
+
$owner.SetValue('Directory', $directory, [Microsoft.Win32.RegistryValueKind]::String)
|
|
68
|
+
$absent = ($null -eq $raw) -or ($owned -and $wasAbsent)
|
|
69
|
+
$owner.SetValue('PathWasAbsent', [int]$absent, [Microsoft.Win32.RegistryValueKind]::DWord)
|
|
70
|
+
$owner.SetValue('RetainedEntries', @($kept | Where-Object { $_ -ceq $directory }).Count, [Microsoft.Win32.RegistryValueKind]::DWord)
|
|
71
|
+
} finally { $environment.Dispose(); $owner.Dispose() }
|
|
72
|
+
$raw = $next
|
|
73
|
+
$owned = $directory
|
|
74
|
+
} elseif ($Request.operation -eq 'remove') {
|
|
75
|
+
if ($kept.Count -ne $parts.Count) {
|
|
76
|
+
$environment = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($EnvironmentKey)
|
|
77
|
+
try {
|
|
78
|
+
if ($kept.Count -eq 0 -and $wasAbsent) { $environment.DeleteValue('Path', $false); $raw = $null }
|
|
79
|
+
else { $raw = $kept -join ';'; $environment.SetValue('Path', $raw, $kind) }
|
|
80
|
+
} finally { $environment.Dispose() }
|
|
81
|
+
}
|
|
82
|
+
$owner = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($OwnerKey, $true)
|
|
83
|
+
if ($owner -and $old -eq $current) {
|
|
84
|
+
try { $owner.DeleteValue('Directory', $false); $owner.DeleteValue('PathWasAbsent', $false); $owner.DeleteValue('RetainedEntries', $false) }
|
|
85
|
+
finally { $owner.Dispose() }
|
|
86
|
+
$owned = $null
|
|
87
|
+
} elseif ($owner) {
|
|
88
|
+
$owner.Dispose()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
$active = $null
|
|
92
|
+
foreach ($part in (($MachinePath + ';' + [string]$raw).Split(';'))) {
|
|
93
|
+
$path = [Environment]::ExpandEnvironmentVariables($part.Trim().Trim('"'))
|
|
94
|
+
try { $rooted = [IO.Path]::IsPathRooted($path) } catch { continue }
|
|
95
|
+
if (-not $rooted) { continue }
|
|
96
|
+
$extensions = if ($env:PATHEXT) { @($env:PATHEXT.Split(';')) } else { @('.com','.exe','.bat','.cmd') }
|
|
97
|
+
foreach ($extension in (@('.ps1') + $extensions)) {
|
|
98
|
+
try {
|
|
99
|
+
$candidate = Join-Path $path ('dsh' + $extension)
|
|
100
|
+
if (Test-Path -LiteralPath $candidate -PathType Leaf) { $active = $candidate; break }
|
|
101
|
+
} catch {
|
|
102
|
+
# Invalid or unavailable PATH candidates do not prevent checking later entries.
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ($active) { break }
|
|
107
|
+
}
|
|
108
|
+
$onPath = @(([string]$raw).Split(';') | Where-Object { (Comparable $_) -eq $current }).Count -gt 0
|
|
109
|
+
return [ordered]@{ fingerprint=$fingerprint; directory=$directory; ownedDirectory=$owned; managed=([bool]$owned -and (Comparable ([string]$owned)) -eq $current); activeCommand=$active; available=($onPath -and (Test-Path -LiteralPath (Join-Path $directory 'dsh.cmd') -PathType Leaf)) }
|
|
110
|
+
} finally {
|
|
111
|
+
if ($locked) { $mutex.ReleaseMutex() }
|
|
112
|
+
$mutex.Dispose()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Send-DshCommandEnvironmentChange {
|
|
117
|
+
try {
|
|
118
|
+
if (-not ('CustomEnvCommandEnvironment' -as [type])) {
|
|
119
|
+
Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class CustomEnvCommandEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, IntPtr w, string l, uint f, uint t, out IntPtr r); }'
|
|
120
|
+
}
|
|
121
|
+
$ignored = [IntPtr]::Zero
|
|
122
|
+
[void][CustomEnvCommandEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [IntPtr]::Zero, 'Environment', 2, 2000, [ref]$ignored)
|
|
123
|
+
} catch {
|
|
124
|
+
# The PATH write is committed. A failed notification only delays other processes observing it.
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if ($MyInvocation.InvocationName -ne '.') {
|
|
130
|
+
$ErrorActionPreference = 'Stop'
|
|
131
|
+
[Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
|
|
132
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
133
|
+
try {
|
|
134
|
+
$inputValue = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
135
|
+
$request = @{ operation=$inputValue.operation; directory=$inputValue.directory; expected=$inputValue.expected }
|
|
136
|
+
$machine = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment')
|
|
137
|
+
try { $machinePath = [string]$machine.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) }
|
|
138
|
+
finally { $machine.Dispose() }
|
|
139
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
140
|
+
$state = Invoke-DshCommandPath -Request $request -EnvironmentKey 'Environment' -OwnerKey 'Software\dsh-custom-env\Command' -MachinePath $machinePath -MutexName ('Global\dsh-custom-env.Command.' + $sid)
|
|
141
|
+
if ($request.operation -ne 'inspect') {
|
|
142
|
+
Send-DshCommandEnvironmentChange
|
|
143
|
+
}
|
|
144
|
+
@{ ok=$true; state=$state } | ConvertTo-Json -Compress -Depth 5
|
|
145
|
+
} catch {
|
|
146
|
+
$code = if ($_.Exception.Data['code']) { $_.Exception.Data['code'] } else { 'EIO' }
|
|
147
|
+
@{ ok=$false; code=$code; message=$_.Exception.Message } | ConvertTo-Json -Compress
|
|
148
|
+
exit 1
|
|
149
|
+
}
|
|
150
|
+
}
|