@woosau/dsh-custom-env 0.1.9 → 0.1.10
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/README.md
CHANGED
|
@@ -32,7 +32,7 @@ dsh plugin --profile desktop remove @woosau/dsh-custom-env
|
|
|
32
32
|
4. Paste `NAME=VALUE` lines into an empty row to add or update several variables at once (same names overlay existing rows as drafts and keep their group)
|
|
33
33
|
5. Drag across rows to multi-select, or Ctrl/Cmd+click to toggle; Ctrl/Cmd+C copies selected rows as `NAME=VALUE` (input text selection still wins when present)
|
|
34
34
|
6. **Restore** brings back the value from when this app session started
|
|
35
|
-
7. At startup, values are injected into the Host `process.env` first; writing the user environment (registry / launchctl) is deferred
|
|
35
|
+
7. At startup, values are injected into the Host `process.env` first; writing the user environment (registry / launchctl) is deferred via `setImmediate` into a child process and is not broadcast. Log off or reboot before other tools need those values
|
|
36
36
|
8. Use **Install** only when shown, to wire this Host’s `dsh` command into your user PATH (Windows / macOS)
|
|
37
37
|
|
|
38
38
|
System-reserved names (such as `PATH` and names starting with `DSH_` / `XDG_`) cannot be edited here.
|
package/README.zh-CN.md
CHANGED
|
@@ -32,7 +32,7 @@ dsh plugin --profile desktop remove @woosau/dsh-custom-env
|
|
|
32
32
|
4. 在空行粘贴多行 `NAME=VALUE` 可批量新增或覆盖同名变量(同名写入原行草稿,且不改其所属分组)
|
|
33
33
|
5. 拖动可多选行,Ctrl/Cmd+点击切换选中;Ctrl/Cmd+C 按 `NAME=VALUE` 复制选中行(若 Input 内有文字选区则优先复制文字)
|
|
34
34
|
6. **恢复** 可回到本次应用启动时的值
|
|
35
|
-
7. 启动时先注入当前 Host 的 `process.env`;写入用户环境(注册表 / launchctl
|
|
35
|
+
7. 启动时先注入当前 Host 的 `process.env`;写入用户环境(注册表 / launchctl)会在启动路径之后的 `setImmediate` 里交给子进程,且不会广播。其它工具若要读到,请注销/重启后再开终端
|
|
36
36
|
8. 仅在出现 **安装** 时使用:把本 Host 的 `dsh` 接到用户 PATH(Windows / macOS)
|
|
37
37
|
|
|
38
38
|
系统保留名(如 `PATH`,以及 `DSH_` / `XDG_` 开头)不可在此编辑。
|
package/lib/index.js
CHANGED
|
@@ -1098,6 +1098,24 @@ function hasGroup(file, groupId) {
|
|
|
1098
1098
|
return file.groups.some((group) => group.id === groupId);
|
|
1099
1099
|
}
|
|
1100
1100
|
|
|
1101
|
+
// src/os-user-env/write-user-env-child.ts
|
|
1102
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1103
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1104
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1105
|
+
function startWriteUserEnvChild(entries) {
|
|
1106
|
+
const scriptUrl = new URL("./write-user-env.child.js", import.meta.url);
|
|
1107
|
+
const script = fileURLToPath2(scriptUrl);
|
|
1108
|
+
if (!existsSync9(script)) return void 0;
|
|
1109
|
+
const child = spawn2(process.execPath, [script], {
|
|
1110
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
1111
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
|
1112
|
+
windowsHide: true
|
|
1113
|
+
});
|
|
1114
|
+
child.stdin?.end(JSON.stringify(entries), "utf8");
|
|
1115
|
+
child.stderr?.pipe(process.stderr);
|
|
1116
|
+
return child;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1101
1119
|
// src/types.ts
|
|
1102
1120
|
function classifyEntry(id, name, value, baselineById) {
|
|
1103
1121
|
const base = baselineById.get(id);
|
|
@@ -1122,6 +1140,36 @@ function applyOsUserEnv(entries) {
|
|
|
1122
1140
|
`);
|
|
1123
1141
|
}
|
|
1124
1142
|
}
|
|
1143
|
+
function scheduleOsUserEnvSync(ctx, entries) {
|
|
1144
|
+
if (Object.keys(entries).length === 0) return;
|
|
1145
|
+
let child;
|
|
1146
|
+
const handle = setImmediate(() => {
|
|
1147
|
+
try {
|
|
1148
|
+
child = startWriteUserEnvChild(entries);
|
|
1149
|
+
if (child === void 0) {
|
|
1150
|
+
applyOsUserEnv(entries);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
child.on("error", (error) => {
|
|
1154
|
+
process.stderr.write(
|
|
1155
|
+
`custom-env: OS user env child failed (${asMessage(error)}); falling back to sync write
|
|
1156
|
+
`
|
|
1157
|
+
);
|
|
1158
|
+
applyOsUserEnv(entries);
|
|
1159
|
+
});
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
process.stderr.write(
|
|
1162
|
+
`custom-env: could not start OS user env child (${asMessage(error)}); falling back to sync write
|
|
1163
|
+
`
|
|
1164
|
+
);
|
|
1165
|
+
applyOsUserEnv(entries);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
ctx.effect(() => () => {
|
|
1169
|
+
clearImmediate(handle);
|
|
1170
|
+
child?.kill();
|
|
1171
|
+
}, "custom-env: deferred OS env sync");
|
|
1172
|
+
}
|
|
1125
1173
|
function writeLive(entries) {
|
|
1126
1174
|
applyProcessEnv(entries);
|
|
1127
1175
|
applyOsUserEnv(entries);
|
|
@@ -1157,14 +1205,7 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
|
|
|
1157
1205
|
this.baselineById.set(id, { name, value });
|
|
1158
1206
|
}
|
|
1159
1207
|
applyProcessEnv(loaded);
|
|
1160
|
-
|
|
1161
|
-
const handle = setImmediate(() => {
|
|
1162
|
-
applyOsUserEnv(loaded);
|
|
1163
|
-
});
|
|
1164
|
-
ctx.effect(() => () => {
|
|
1165
|
-
clearImmediate(handle);
|
|
1166
|
-
}, "custom-env: deferred OS env sync");
|
|
1167
|
-
}
|
|
1208
|
+
scheduleOsUserEnvSync(ctx, loaded);
|
|
1168
1209
|
}
|
|
1169
1210
|
allocId() {
|
|
1170
1211
|
return `env-${++this.idSeq}`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn a child to persist OS user env without blocking the Host.
|
|
3
|
+
* Sets `ELECTRON_RUN_AS_NODE` so Desktop's Electron binary runs the script as Node.
|
|
4
|
+
*/
|
|
5
|
+
import { type ChildProcess } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Start a child that writes `entries` into the OS user environment.
|
|
8
|
+
* @param entries - Variable map to persist.
|
|
9
|
+
* @returns The child process, or `undefined` when the bundled script is missing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startWriteUserEnvChild(entries: Readonly<Record<string, string>>): ChildProcess | undefined;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/os-user-env/macos.ts
|
|
2
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
var AGENT_LABEL = "ai.dsh.custom-env";
|
|
7
|
+
var AGENT_DIR = () => join(homedir(), "Library", "LaunchAgents");
|
|
8
|
+
var AGENT_PATH = () => join(AGENT_DIR(), `${AGENT_LABEL}.plist`);
|
|
9
|
+
var cached;
|
|
10
|
+
function escapeXml(value) {
|
|
11
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """);
|
|
12
|
+
}
|
|
13
|
+
function buildPlist(entries) {
|
|
14
|
+
const pairs = Object.entries(entries).map(([key, value]) => ` <key>${escapeXml(key)}</key>
|
|
15
|
+
<string>${escapeXml(value)}</string>`).join("\n");
|
|
16
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
17
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
18
|
+
<plist version="1.0">
|
|
19
|
+
<dict>
|
|
20
|
+
<key>Label</key>
|
|
21
|
+
<string>${AGENT_LABEL}</string>
|
|
22
|
+
<key>ProgramArguments</key>
|
|
23
|
+
<array>
|
|
24
|
+
<string>/bin/true</string>
|
|
25
|
+
</array>
|
|
26
|
+
<key>RunAtLoad</key>
|
|
27
|
+
<true/>
|
|
28
|
+
<key>EnvironmentVariables</key>
|
|
29
|
+
<dict>
|
|
30
|
+
${pairs}
|
|
31
|
+
</dict>
|
|
32
|
+
</dict>
|
|
33
|
+
</plist>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
function launchctl(args) {
|
|
37
|
+
const result = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
38
|
+
if (result.error) throw result.error;
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
41
|
+
throw new Error(`custom-env: launchctl ${args.join(" ")} failed${detail === "" ? "" : `: ${detail}`}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function ensureCache() {
|
|
45
|
+
if (cached !== void 0) return cached;
|
|
46
|
+
cached = {};
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
function writeAgent(entries) {
|
|
50
|
+
const path = AGENT_PATH();
|
|
51
|
+
if (Object.keys(entries).length === 0) {
|
|
52
|
+
if (existsSync(path)) {
|
|
53
|
+
try {
|
|
54
|
+
launchctl(["unload", path]);
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
}
|
|
57
|
+
unlinkSync(path);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
mkdirSync(AGENT_DIR(), { recursive: true });
|
|
62
|
+
writeFileSync(path, buildPlist(entries), "utf8");
|
|
63
|
+
try {
|
|
64
|
+
launchctl(["unload", path]);
|
|
65
|
+
} catch (_error) {
|
|
66
|
+
}
|
|
67
|
+
launchctl(["load", path]);
|
|
68
|
+
}
|
|
69
|
+
function writeMacosUserEnv(entries, platform = process.platform) {
|
|
70
|
+
if (platform !== "darwin") return;
|
|
71
|
+
const pairs = Object.entries(entries);
|
|
72
|
+
if (pairs.length === 0) return;
|
|
73
|
+
const map = ensureCache();
|
|
74
|
+
for (const [name, value] of pairs) {
|
|
75
|
+
map[name] = value;
|
|
76
|
+
launchctl(["setenv", name, value]);
|
|
77
|
+
}
|
|
78
|
+
writeAgent(map);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/os-user-env/windows.ts
|
|
82
|
+
import { createRequire } from "node:module";
|
|
83
|
+
var HKEY_CURRENT_USER = 2147483649;
|
|
84
|
+
var KEY_SET_VALUE = 2;
|
|
85
|
+
var REG_SZ = 1;
|
|
86
|
+
var ERROR_SUCCESS = 0;
|
|
87
|
+
var bindings;
|
|
88
|
+
function loadBindings() {
|
|
89
|
+
if (bindings !== void 0) return bindings;
|
|
90
|
+
const require2 = createRequire(import.meta.url);
|
|
91
|
+
const koffi = require2("koffi");
|
|
92
|
+
const advapi32 = koffi.load("advapi32.dll");
|
|
93
|
+
bindings = {
|
|
94
|
+
openKey: advapi32.func(
|
|
95
|
+
"long __stdcall RegOpenKeyExW(uintptr_t hKey, const char16_t *subKey, uint32_t options, uint32_t samDesired, _Out_ void **result)"
|
|
96
|
+
),
|
|
97
|
+
queryValue: advapi32.func(
|
|
98
|
+
"long __stdcall RegQueryValueExW(void *hKey, const char16_t *valueName, void *reserved, _Inout_ uint32_t *type, uint8_t *data, _Inout_ uint32_t *cbData)"
|
|
99
|
+
),
|
|
100
|
+
setValue: advapi32.func(
|
|
101
|
+
"long __stdcall RegSetValueExW(void *hKey, const char16_t *valueName, uint32_t reserved, uint32_t type, const uint8_t *data, uint32_t cbData)"
|
|
102
|
+
),
|
|
103
|
+
deleteValue: advapi32.func(
|
|
104
|
+
"long __stdcall RegDeleteValueW(void *hKey, const char16_t *valueName)"
|
|
105
|
+
),
|
|
106
|
+
closeKey: advapi32.func("long __stdcall RegCloseKey(void *hKey)")
|
|
107
|
+
};
|
|
108
|
+
return bindings;
|
|
109
|
+
}
|
|
110
|
+
function encodeRegSz(value) {
|
|
111
|
+
return Buffer.from(`${value}\0`, "utf16le");
|
|
112
|
+
}
|
|
113
|
+
function writeWindowsUserEnv(entries, platform = process.platform) {
|
|
114
|
+
if (platform !== "win32") return;
|
|
115
|
+
const pairs = Object.entries(entries);
|
|
116
|
+
if (pairs.length === 0) return;
|
|
117
|
+
const api = loadBindings();
|
|
118
|
+
const keySlot = [null];
|
|
119
|
+
const openStatus = api.openKey(HKEY_CURRENT_USER, "Environment", 0, KEY_SET_VALUE, keySlot);
|
|
120
|
+
if (openStatus !== ERROR_SUCCESS) {
|
|
121
|
+
throw new Error(`custom-env: RegOpenKeyExW failed (Win32 ${openStatus})`);
|
|
122
|
+
}
|
|
123
|
+
const hKey = keySlot[0];
|
|
124
|
+
try {
|
|
125
|
+
for (const [name, value] of pairs) {
|
|
126
|
+
const data = encodeRegSz(value);
|
|
127
|
+
const status = api.setValue(hKey, name, 0, REG_SZ, data, data.length);
|
|
128
|
+
if (status !== ERROR_SUCCESS) {
|
|
129
|
+
throw new Error(`custom-env: RegSetValueExW(${name}) failed (Win32 ${status})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
api.closeKey(hKey);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/os-user-env/index.ts
|
|
138
|
+
function writeUserEnv(entries) {
|
|
139
|
+
writeWindowsUserEnv(entries);
|
|
140
|
+
writeMacosUserEnv(entries);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/os-user-env/write-user-env.child.ts
|
|
144
|
+
var chunks = [];
|
|
145
|
+
for await (const chunk of process.stdin) {
|
|
146
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
147
|
+
}
|
|
148
|
+
var raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
149
|
+
if (raw === "") process.exit(0);
|
|
150
|
+
try {
|
|
151
|
+
const entries = JSON.parse(raw);
|
|
152
|
+
writeUserEnv(entries);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
process.stderr.write(`custom-env child: ${message}
|
|
156
|
+
`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woosau/dsh-custom-env",
|
|
3
3
|
"description": "DSH plugin: manage $DSH_HOME/.env from Settings",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"cordis.patch.yml",
|
|
73
73
|
"scripts/command-path.ps1",
|
|
74
74
|
"lib/index.js",
|
|
75
|
+
"lib/write-user-env.child.js",
|
|
75
76
|
"lib/client.js",
|
|
76
77
|
"lib/types/**/*.d.ts",
|
|
77
78
|
"lib/typert.host.js",
|