@woosau/dsh-custom-env 0.1.1 → 0.1.6

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/lib/index.js CHANGED
@@ -519,8 +519,8 @@ async function cliInstall() {
519
519
  }
520
520
 
521
521
  // src/env-file.ts
522
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
523
- import { dirname as dirname5, join as join6 } from "node:path";
522
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
523
+ import { dirname as dirname5, join as join7 } from "node:path";
524
524
  import { parseEnv } from "node:util";
525
525
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
526
526
 
@@ -586,116 +586,14 @@ function isWritableEnvName(name) {
586
586
  return ENV_NAME.test(name) && !isBootstrapOnly(name);
587
587
  }
588
588
 
589
- // src/env-file.ts
590
- function envFilePath() {
591
- return join6(resolveDshHome(), ".env");
592
- }
593
- function readRaw() {
594
- const path = envFilePath();
595
- return existsSync6(path) ? readFileSync2(path, "utf8") : "";
596
- }
597
- function writeEnvFile(content) {
598
- const path = envFilePath();
599
- mkdirSync2(dirname5(path), { recursive: true });
600
- writeFileSync2(path, content, "utf8");
601
- }
602
- function encodeValue(value) {
603
- if (/^[A-Za-z0-9_./:@+-]*$/u.test(value)) return value;
604
- return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"').replace(/\n/gu, "\\n")}"`;
605
- }
606
- function parseLines(content) {
607
- const text = content.replace(/^\uFEFF/u, "");
608
- if (text === "") return [];
609
- return text.split(/\r?\n/u).map((raw) => {
610
- const trimmed = raw.trim();
611
- if (trimmed === "" || trimmed.startsWith("#")) return { kind: "other", raw };
612
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/u.exec(trimmed);
613
- if (match?.[1] === void 0) return { kind: "other", raw };
614
- return { kind: "assignment", raw, name: match[1] };
615
- });
616
- }
617
- function serializeLines(lines) {
618
- const kept = lines.filter((line) => !(line.kind === "other" && line.raw.trim() === ""));
619
- return kept.length === 0 ? "" : `${kept.map((line) => line.raw).join("\n")}
620
- `;
621
- }
622
- function assertWritable(name, value) {
623
- if (!isWritableEnvName(name)) {
624
- throw new Error(`custom-env: refused bootstrap or invalid name "${name}"`);
625
- }
626
- if (value === "") {
627
- throw new Error(`custom-env: empty value for "${name}"`);
628
- }
629
- }
630
- function loadEnvMap() {
631
- const raw = readRaw().replace(/^\uFEFF/u, "").trim();
632
- if (raw === "") return {};
633
- let parsed;
634
- try {
635
- parsed = parseEnv(raw);
636
- } catch (error) {
637
- throw new Error(`custom-env: invalid dotenv syntax in ${envFilePath()}`, { cause: error });
638
- }
639
- const out = {};
640
- for (const [key, value] of Object.entries(parsed)) {
641
- if (typeof value === "string" && value !== "" && isWritableEnvName(key)) out[key] = value;
642
- }
643
- return out;
644
- }
645
- function listEnvEntries() {
646
- const path = envFilePath();
647
- const exists = existsSync6(path);
648
- if (!exists) return { path, exists, entries: [] };
649
- const map = loadEnvMap();
650
- const entries = [];
651
- const seen = /* @__PURE__ */ new Set();
652
- for (const line of parseLines(readRaw())) {
653
- if (line.kind !== "assignment" || line.name === void 0 || seen.has(line.name)) continue;
654
- const value = map[line.name];
655
- if (value === void 0) continue;
656
- seen.add(line.name);
657
- entries.push({ name: line.name, value });
658
- }
659
- return { path, exists, entries };
660
- }
661
- function writeEnvEntry(replaceName, name, value) {
662
- assertWritable(replaceName, value);
663
- assertWritable(name, value);
664
- const assignment = `${name}=${encodeValue(value)}`;
665
- let replaced = false;
666
- const next = [];
667
- for (const line of parseLines(readRaw())) {
668
- if (line.kind === "assignment" && line.name === replaceName) {
669
- replaced = true;
670
- next.push({ kind: "assignment", raw: assignment, name });
671
- continue;
672
- }
673
- if (replaceName !== name && line.kind === "assignment" && line.name === name) continue;
674
- next.push(line);
675
- }
676
- if (!replaced) next.push({ kind: "assignment", raw: assignment, name });
677
- writeEnvFile(serializeLines(next));
678
- }
679
- function removeEnvEntry(name) {
680
- if (!isWritableEnvName(name)) {
681
- throw new Error(`custom-env: refused bootstrap or invalid name "${name}"`);
682
- }
683
- if (!existsSync6(envFilePath())) return false;
684
- const lines = parseLines(readRaw());
685
- const next = lines.filter((line) => !(line.kind === "assignment" && line.name === name));
686
- if (next.length === lines.length) return false;
687
- writeEnvFile(serializeLines(next));
688
- return true;
689
- }
690
-
691
589
  // src/os-user-env/macos.ts
692
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "node:fs";
590
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "node:fs";
693
591
  import { homedir } from "node:os";
694
- import { join as join7 } from "node:path";
592
+ import { join as join6 } from "node:path";
695
593
  import { spawnSync } from "node:child_process";
696
594
  var AGENT_LABEL = "ai.dsh.custom-env";
697
- var AGENT_DIR = () => join7(homedir(), "Library", "LaunchAgents");
698
- var AGENT_PATH = () => join7(AGENT_DIR(), `${AGENT_LABEL}.plist`);
595
+ var AGENT_DIR = () => join6(homedir(), "Library", "LaunchAgents");
596
+ var AGENT_PATH = () => join6(AGENT_DIR(), `${AGENT_LABEL}.plist`);
699
597
  var cached;
700
598
  function escapeXml(value) {
701
599
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;");
@@ -739,7 +637,7 @@ function ensureCache() {
739
637
  function writeAgent(entries) {
740
638
  const path = AGENT_PATH();
741
639
  if (Object.keys(entries).length === 0) {
742
- if (existsSync7(path)) {
640
+ if (existsSync6(path)) {
743
641
  try {
744
642
  launchctl(["unload", path]);
745
643
  } catch (_error) {
@@ -748,15 +646,20 @@ function writeAgent(entries) {
748
646
  }
749
647
  return;
750
648
  }
751
- mkdirSync3(AGENT_DIR(), { recursive: true });
752
- writeFileSync3(path, buildPlist(entries), "utf8");
649
+ mkdirSync2(AGENT_DIR(), { recursive: true });
650
+ writeFileSync2(path, buildPlist(entries), "utf8");
753
651
  try {
754
652
  launchctl(["unload", path]);
755
653
  } catch (_error) {
756
654
  }
757
655
  launchctl(["load", path]);
758
656
  }
759
- function persistMacosUserEnv(entries, platform = process.platform) {
657
+ function hasMacosUserEnv(name, platform = process.platform) {
658
+ if (platform !== "darwin") return false;
659
+ if (Object.hasOwn(process.env, name)) return true;
660
+ return Object.hasOwn(ensureCache(), name);
661
+ }
662
+ function writeMacosUserEnv(entries, platform = process.platform) {
760
663
  if (platform !== "darwin") return;
761
664
  const pairs = Object.entries(entries);
762
665
  if (pairs.length === 0) return;
@@ -767,13 +670,15 @@ function persistMacosUserEnv(entries, platform = process.platform) {
767
670
  }
768
671
  writeAgent(map);
769
672
  }
770
- function removeMacosUserEnv(name, platform = process.platform) {
771
- if (platform !== "darwin") return;
673
+ function removeMacosUserEnvs(names, platform = process.platform) {
674
+ if (platform !== "darwin" || names.length === 0) return;
772
675
  const map = ensureCache();
773
- delete map[name];
774
- try {
775
- launchctl(["unsetenv", name]);
776
- } catch (_error) {
676
+ for (const name of names) {
677
+ delete map[name];
678
+ try {
679
+ launchctl(["unsetenv", name]);
680
+ } catch (_error) {
681
+ }
777
682
  }
778
683
  writeAgent(map);
779
684
  }
@@ -781,53 +686,59 @@ function removeMacosUserEnv(name, platform = process.platform) {
781
686
  // src/os-user-env/windows.ts
782
687
  import { createRequire } from "node:module";
783
688
  var HKEY_CURRENT_USER = 2147483649;
689
+ var KEY_QUERY_VALUE = 1;
784
690
  var KEY_SET_VALUE = 2;
785
691
  var REG_SZ = 1;
786
692
  var ERROR_SUCCESS = 0;
787
693
  var ERROR_FILE_NOT_FOUND = 2;
788
- var HWND_BROADCAST = 65535;
789
- var WM_SETTINGCHANGE = 26;
790
- var SMTO_ABORTIFHUNG = 2;
791
694
  var bindings;
792
695
  function loadBindings() {
793
696
  if (bindings !== void 0) return bindings;
794
697
  const require2 = createRequire(import.meta.url);
795
698
  const koffi = require2("koffi");
796
699
  const advapi32 = koffi.load("advapi32.dll");
797
- const user32 = koffi.load("user32.dll");
798
700
  bindings = {
799
701
  openKey: advapi32.func(
800
702
  "long __stdcall RegOpenKeyExW(uintptr_t hKey, const char16_t *subKey, uint32_t options, uint32_t samDesired, _Out_ void **result)"
801
703
  ),
704
+ queryValue: advapi32.func(
705
+ "long __stdcall RegQueryValueExW(void *hKey, const char16_t *valueName, void *reserved, _Inout_ uint32_t *type, uint8_t *data, _Inout_ uint32_t *cbData)"
706
+ ),
802
707
  setValue: advapi32.func(
803
708
  "long __stdcall RegSetValueExW(void *hKey, const char16_t *valueName, uint32_t reserved, uint32_t type, const uint8_t *data, uint32_t cbData)"
804
709
  ),
805
710
  deleteValue: advapi32.func(
806
711
  "long __stdcall RegDeleteValueW(void *hKey, const char16_t *valueName)"
807
712
  ),
808
- closeKey: advapi32.func("long __stdcall RegCloseKey(void *hKey)"),
809
- sendMessageTimeout: user32.func(
810
- "intptr_t __stdcall SendMessageTimeoutW(uintptr_t hWnd, uint32_t msg, uintptr_t wParam, const char16_t *lParam, uint32_t flags, uint32_t timeout, _Out_ uintptr_t *result)"
811
- )
713
+ closeKey: advapi32.func("long __stdcall RegCloseKey(void *hKey)")
812
714
  };
813
715
  return bindings;
814
716
  }
815
717
  function encodeRegSz(value) {
816
718
  return Buffer.from(`${value}\0`, "utf16le");
817
719
  }
818
- function broadcastEnvironmentChange(api) {
819
- const broadcastResult = [0];
820
- api.sendMessageTimeout(
821
- HWND_BROADCAST,
822
- WM_SETTINGCHANGE,
823
- 0,
824
- "Environment",
825
- SMTO_ABORTIFHUNG,
826
- 1e3,
827
- broadcastResult
828
- );
720
+ function hasWindowsUserEnv(name, platform = process.platform) {
721
+ if (platform !== "win32") return false;
722
+ const api = loadBindings();
723
+ const keySlot = [null];
724
+ const openStatus = api.openKey(HKEY_CURRENT_USER, "Environment", 0, KEY_QUERY_VALUE, keySlot);
725
+ if (openStatus !== ERROR_SUCCESS) {
726
+ throw new Error(`custom-env: RegOpenKeyExW failed (Win32 ${openStatus})`);
727
+ }
728
+ const hKey = keySlot[0];
729
+ try {
730
+ const cbData = [0];
731
+ const status = api.queryValue(hKey, name, null, null, null, cbData);
732
+ if (status === ERROR_FILE_NOT_FOUND) return false;
733
+ if (status !== ERROR_SUCCESS && status !== 234) {
734
+ throw new Error(`custom-env: RegQueryValueExW(${name}) failed (Win32 ${status})`);
735
+ }
736
+ return true;
737
+ } finally {
738
+ api.closeKey(hKey);
739
+ }
829
740
  }
830
- function persistWindowsUserEnv(entries, platform = process.platform) {
741
+ function writeWindowsUserEnv(entries, platform = process.platform) {
831
742
  if (platform !== "win32") return;
832
743
  const pairs = Object.entries(entries);
833
744
  if (pairs.length === 0) return;
@@ -849,10 +760,9 @@ function persistWindowsUserEnv(entries, platform = process.platform) {
849
760
  } finally {
850
761
  api.closeKey(hKey);
851
762
  }
852
- broadcastEnvironmentChange(api);
853
763
  }
854
- function removeWindowsUserEnv(name, platform = process.platform) {
855
- if (platform !== "win32") return;
764
+ function removeWindowsUserEnvs(names, platform = process.platform) {
765
+ if (platform !== "win32" || names.length === 0) return;
856
766
  const api = loadBindings();
857
767
  const keySlot = [null];
858
768
  const openStatus = api.openKey(HKEY_CURRENT_USER, "Environment", 0, KEY_SET_VALUE, keySlot);
@@ -861,24 +771,184 @@ function removeWindowsUserEnv(name, platform = process.platform) {
861
771
  }
862
772
  const hKey = keySlot[0];
863
773
  try {
864
- const status = api.deleteValue(hKey, name);
865
- if (status !== ERROR_SUCCESS && status !== ERROR_FILE_NOT_FOUND) {
866
- throw new Error(`custom-env: RegDeleteValueW(${name}) failed (Win32 ${status})`);
774
+ for (const name of names) {
775
+ const status = api.deleteValue(hKey, name);
776
+ if (status !== ERROR_SUCCESS && status !== ERROR_FILE_NOT_FOUND) {
777
+ throw new Error(`custom-env: RegDeleteValueW(${name}) failed (Win32 ${status})`);
778
+ }
867
779
  }
868
780
  } finally {
869
781
  api.closeKey(hKey);
870
782
  }
871
- broadcastEnvironmentChange(api);
872
783
  }
873
784
 
874
785
  // src/os-user-env/index.ts
875
- function persistUserEnv(entries) {
876
- persistWindowsUserEnv(entries);
877
- persistMacosUserEnv(entries);
786
+ function hasUserEnv(name) {
787
+ return hasWindowsUserEnv(name) || hasMacosUserEnv(name);
788
+ }
789
+ function writeUserEnv(entries) {
790
+ writeWindowsUserEnv(entries);
791
+ writeMacosUserEnv(entries);
878
792
  }
879
- function removeUserEnv(name) {
880
- removeWindowsUserEnv(name);
881
- removeMacosUserEnv(name);
793
+ function removeUserEnvs(names) {
794
+ if (names.length === 0) return;
795
+ removeWindowsUserEnvs(names);
796
+ removeMacosUserEnvs(names);
797
+ }
798
+
799
+ // src/env-file.ts
800
+ function envFilePath() {
801
+ return join7(resolveDshHome(), ".env");
802
+ }
803
+ function readRaw() {
804
+ const path = envFilePath();
805
+ return existsSync7(path) ? readFileSync2(path, "utf8") : "";
806
+ }
807
+ function writeEnvFile(content) {
808
+ const path = envFilePath();
809
+ mkdirSync3(dirname5(path), { recursive: true });
810
+ writeFileSync3(path, content, "utf8");
811
+ }
812
+ function encodeValue(value) {
813
+ if (/^[A-Za-z0-9_./:@+-]*$/u.test(value)) return value;
814
+ return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"').replace(/\n/gu, "\\n")}"`;
815
+ }
816
+ function parseLines(content) {
817
+ const text = content.replace(/^\uFEFF/u, "");
818
+ if (text === "") return [];
819
+ return text.split(/\r?\n/u).map((raw) => {
820
+ const trimmed = raw.trim();
821
+ if (trimmed === "" || trimmed.startsWith("#")) return { kind: "other", raw };
822
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/u.exec(trimmed);
823
+ if (match?.[1] === void 0) return { kind: "other", raw };
824
+ return { kind: "assignment", raw, name: match[1] };
825
+ });
826
+ }
827
+ function serializeLines(lines) {
828
+ const kept = lines.filter((line) => !(line.kind === "other" && line.raw.trim() === ""));
829
+ return kept.length === 0 ? "" : `${kept.map((line) => line.raw).join("\n")}
830
+ `;
831
+ }
832
+ function assertWritableName(name) {
833
+ if (!isWritableEnvName(name)) {
834
+ throw new Error(`custom-env: refused bootstrap or invalid name "${name}"`);
835
+ }
836
+ }
837
+ function readEnvMap() {
838
+ const raw = readRaw().replace(/^\uFEFF/u, "").trim();
839
+ if (raw === "") return {};
840
+ let parsed;
841
+ try {
842
+ parsed = parseEnv(raw);
843
+ } catch (error) {
844
+ throw new Error(`custom-env: invalid dotenv syntax in ${envFilePath()}`, { cause: error });
845
+ }
846
+ const out = {};
847
+ for (const [key, value] of Object.entries(parsed)) {
848
+ if (typeof value === "string" && isWritableEnvName(key)) out[key] = value;
849
+ }
850
+ return out;
851
+ }
852
+ function listEnvEntries() {
853
+ const path = envFilePath();
854
+ const exists = existsSync7(path);
855
+ if (!exists) return { path, exists, entries: [] };
856
+ const map = readEnvMap();
857
+ const entries = [];
858
+ const seen = /* @__PURE__ */ new Set();
859
+ for (const line of parseLines(readRaw())) {
860
+ if (line.kind !== "assignment" || line.name === void 0 || seen.has(line.name)) continue;
861
+ const value = map[line.name];
862
+ if (value === void 0) continue;
863
+ seen.add(line.name);
864
+ entries.push({ name: line.name, value });
865
+ }
866
+ return { path, exists, entries };
867
+ }
868
+ function hasEnvKey(name) {
869
+ return Object.hasOwn(readEnvMap(), name) || hasUserEnv(name);
870
+ }
871
+ function selectEnvWrites(entries, options = {}) {
872
+ if (entries.length === 0) return [];
873
+ const lastByName = /* @__PURE__ */ new Map();
874
+ for (const entry of entries) {
875
+ assertWritableName(entry.name);
876
+ lastByName.set(entry.name, entry.value);
877
+ }
878
+ const forceEmpty = options.forceEmpty === true;
879
+ const selected = [];
880
+ for (const [name, value] of lastByName) {
881
+ if (value === "" && !forceEmpty && hasEnvKey(name)) continue;
882
+ selected.push({ name, value });
883
+ }
884
+ return selected;
885
+ }
886
+ function writeEnvEntries(entries, options = {}) {
887
+ const selected = selectEnvWrites(entries, options);
888
+ if (selected.length === 0) return selected;
889
+ const pending = new Map(selected.map((entry) => [entry.name, entry.value]));
890
+ const next = [];
891
+ for (const line of parseLines(readRaw())) {
892
+ if (line.kind === "assignment" && line.name !== void 0 && pending.has(line.name)) {
893
+ const value = pending.get(line.name);
894
+ pending.delete(line.name);
895
+ next.push({
896
+ kind: "assignment",
897
+ raw: `${line.name}=${encodeValue(value)}`,
898
+ name: line.name
899
+ });
900
+ continue;
901
+ }
902
+ next.push(line);
903
+ }
904
+ for (const [name, value] of pending) {
905
+ next.push({ kind: "assignment", raw: `${name}=${encodeValue(value)}`, name });
906
+ }
907
+ writeEnvFile(serializeLines(next));
908
+ return selected;
909
+ }
910
+ function writeEnvEntry(name, value, options = {}) {
911
+ return writeEnvEntries([{ name, value }], options).length > 0;
912
+ }
913
+ function renameEnvEntry(from, to, value) {
914
+ assertWritableName(from);
915
+ assertWritableName(to);
916
+ const assignment = `${to}=${encodeValue(value)}`;
917
+ let replaced = false;
918
+ const next = [];
919
+ for (const line of parseLines(readRaw())) {
920
+ if (line.kind === "assignment" && line.name === from) {
921
+ replaced = true;
922
+ next.push({ kind: "assignment", raw: assignment, name: to });
923
+ continue;
924
+ }
925
+ if (line.kind === "assignment" && line.name === to) continue;
926
+ next.push(line);
927
+ }
928
+ if (!replaced) next.push({ kind: "assignment", raw: assignment, name: to });
929
+ writeEnvFile(serializeLines(next));
930
+ }
931
+ function removeEnvEntries(names) {
932
+ if (names.length === 0) return 0;
933
+ const drop = /* @__PURE__ */ new Set();
934
+ for (const name of names) {
935
+ assertWritableName(name);
936
+ drop.add(name);
937
+ }
938
+ if (!existsSync7(envFilePath())) return 0;
939
+ const lines = parseLines(readRaw());
940
+ let removed = 0;
941
+ const next = [];
942
+ for (const line of lines) {
943
+ if (line.kind === "assignment" && line.name !== void 0 && drop.has(line.name)) {
944
+ removed += 1;
945
+ continue;
946
+ }
947
+ next.push(line);
948
+ }
949
+ if (removed === 0) return 0;
950
+ writeEnvFile(serializeLines(next));
951
+ return removed;
882
952
  }
883
953
 
884
954
  // src/types.ts
@@ -893,26 +963,39 @@ function classifyEntry(id, name, value, baselineById) {
893
963
  function asMessage(error) {
894
964
  return error instanceof Error ? error.message : String(error);
895
965
  }
896
- function applyLive(entries) {
966
+ function applyProcessEnv(entries) {
897
967
  for (const [key, value] of Object.entries(entries)) process.env[key] = value;
968
+ }
969
+ function applyOsUserEnv(entries) {
970
+ if (Object.keys(entries).length === 0) return;
898
971
  try {
899
- persistUserEnv(entries);
972
+ writeUserEnv(entries);
900
973
  } catch (error) {
901
- process.stderr.write(`custom-env: failed to persist OS user environment: ${asMessage(error)}
974
+ process.stderr.write(`custom-env: failed to write OS user environment: ${asMessage(error)}
902
975
  `);
903
976
  }
904
977
  }
905
- function clearLive(name) {
906
- delete process.env[name];
978
+ function writeLive(entries) {
979
+ applyProcessEnv(entries);
980
+ applyOsUserEnv(entries);
981
+ }
982
+ function removeLives(names) {
983
+ if (names.length === 0) return;
984
+ for (const name of names) delete process.env[name];
907
985
  try {
908
- removeUserEnv(name);
986
+ removeUserEnvs(names);
909
987
  } catch (error) {
910
- process.stderr.write(`custom-env: failed to remove OS user environment ${name}: ${asMessage(error)}
911
- `);
988
+ process.stderr.write(
989
+ `custom-env: failed to remove OS user environment (${names.join(", ")}): ${asMessage(error)}
990
+ `
991
+ );
912
992
  }
913
993
  }
914
- var _cliInstall_dec, _cliStatus_dec, _restore_dec, _deleteEntry_dec, _upsert_dec, _list_dec, _a, _init;
915
- var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [Remote("list")], _upsert_dec = [Remote("upsert")], _deleteEntry_dec = [Remote("deleteEntry")], _restore_dec = [Remote("restore")], _cliStatus_dec = [Remote("cliStatus")], _cliInstall_dec = [Remote("cliInstall")], _a) {
994
+ function removeLive(name) {
995
+ removeLives([name]);
996
+ }
997
+ var _cliInstall_dec, _cliStatus_dec, _restore_dec, _deleteEntry_dec, _removeVar_dec, _removeVars_dec, _setVar_dec, _setVars_dec, _upsert_dec, _list_dec, _a, _init;
998
+ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [Remote("list")], _upsert_dec = [Remote("upsert")], _setVars_dec = [Remote("setVars")], _setVar_dec = [Remote("setVar")], _removeVars_dec = [Remote("removeVars")], _removeVar_dec = [Remote("removeVar")], _deleteEntry_dec = [Remote("deleteEntry")], _restore_dec = [Remote("restore")], _cliStatus_dec = [Remote("cliStatus")], _cliInstall_dec = [Remote("cliInstall")], _a) {
916
999
  constructor(ctx) {
917
1000
  super(ctx, "customEnv");
918
1001
  __runInitializers(_init, 5, this);
@@ -920,13 +1003,21 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
920
1003
  __publicField(this, "idByName", /* @__PURE__ */ new Map());
921
1004
  __publicField(this, "nameById", /* @__PURE__ */ new Map());
922
1005
  __publicField(this, "idSeq", 0);
923
- const loaded = loadEnvMap();
1006
+ const loaded = readEnvMap();
924
1007
  for (const [name, value] of Object.entries(loaded)) {
925
1008
  const id = this.allocId();
926
1009
  this.bind(id, name);
927
1010
  this.baselineById.set(id, { name, value });
928
1011
  }
929
- applyLive(loaded);
1012
+ applyProcessEnv(loaded);
1013
+ if (Object.keys(loaded).length > 0) {
1014
+ const handle = setImmediate(() => {
1015
+ applyOsUserEnv(loaded);
1016
+ });
1017
+ ctx.effect(() => () => {
1018
+ clearImmediate(handle);
1019
+ }, "custom-env: deferred OS env sync");
1020
+ }
930
1021
  }
931
1022
  allocId() {
932
1023
  return `env-${++this.idSeq}`;
@@ -949,21 +1040,25 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
949
1040
  }
950
1041
  return name;
951
1042
  }
952
- /** Persist `name=value` replacing `from` in place; sync process.env / OS. */
953
- commit(from, name, value) {
1043
+ /**
1044
+ * Write `name=value` replacing `from` in place; sync process.env / OS.
1045
+ * Empty values create missing keys only, unless `forceEmpty` (restore).
1046
+ */
1047
+ commit(from, name, value, forceEmpty = false) {
954
1048
  if (!isWritableEnvName(name) || from !== name && !isWritableEnvName(from)) {
955
1049
  throw new RemoteError("custom-env/invalid-name", `refused name "${name}"`, { name });
956
1050
  }
957
- if (value === "") {
958
- throw new RemoteError("custom-env/invalid-value", `empty value for "${name}"`, { name });
959
- }
960
1051
  try {
961
- writeEnvEntry(from, name, value);
1052
+ if (from === name) {
1053
+ if (!writeEnvEntry(name, value, { forceEmpty })) return;
1054
+ } else {
1055
+ renameEnvEntry(from, name, value);
1056
+ removeLive(from);
1057
+ }
962
1058
  } catch (error) {
963
1059
  throw new RemoteError("custom-env/invalid-name", asMessage(error), { name }, { cause: error });
964
1060
  }
965
- if (from !== name) clearLive(from);
966
- applyLive({ [name]: value });
1061
+ writeLive({ [name]: value });
967
1062
  }
968
1063
  async list() {
969
1064
  const { path, exists, entries: raw } = listEnvEntries();
@@ -993,18 +1088,66 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
993
1088
  this.bind(id, name);
994
1089
  return this.list();
995
1090
  }
996
- async deleteEntry(id) {
997
- const name = this.requireName(id, id);
998
- if (!isWritableEnvName(name)) {
999
- throw new RemoteError("custom-env/invalid-name", `refused name "${name}"`, { name });
1091
+ async setVars(entries) {
1092
+ if (entries.length === 0) return;
1093
+ const lastByName = /* @__PURE__ */ new Map();
1094
+ for (const entry of entries) {
1095
+ if (!isWritableEnvName(entry.name)) {
1096
+ throw new RemoteError("custom-env/invalid-name", `refused name "${entry.name}"`, { name: entry.name });
1097
+ }
1098
+ lastByName.set(entry.name, entry.value);
1000
1099
  }
1100
+ const normalized = [...lastByName].map(([name, value]) => ({ name, value }));
1101
+ let written;
1001
1102
  try {
1002
- removeEnvEntry(name);
1103
+ written = writeEnvEntries(normalized);
1003
1104
  } catch (error) {
1105
+ const name = normalized[0]?.name ?? "";
1004
1106
  throw new RemoteError("custom-env/invalid-name", asMessage(error), { name }, { cause: error });
1005
1107
  }
1006
- clearLive(name);
1007
- this.unbind(id);
1108
+ if (written.length === 0) return;
1109
+ const live = {};
1110
+ for (const entry of written) {
1111
+ live[entry.name] = entry.value;
1112
+ if (!this.idByName.has(entry.name)) this.bind(this.allocId(), entry.name);
1113
+ }
1114
+ writeLive(live);
1115
+ }
1116
+ async setVar(name, value) {
1117
+ await this.setVars([{ name, value }]);
1118
+ }
1119
+ async removeVars(names) {
1120
+ if (names.length === 0) return 0;
1121
+ const unique = [];
1122
+ const seen = /* @__PURE__ */ new Set();
1123
+ for (const name of names) {
1124
+ if (!isWritableEnvName(name)) {
1125
+ throw new RemoteError("custom-env/invalid-name", `refused name "${name}"`, { name });
1126
+ }
1127
+ if (seen.has(name)) continue;
1128
+ seen.add(name);
1129
+ unique.push(name);
1130
+ }
1131
+ let removed;
1132
+ try {
1133
+ removed = removeEnvEntries(unique);
1134
+ } catch (error) {
1135
+ const name = unique[0] ?? "";
1136
+ throw new RemoteError("custom-env/invalid-name", asMessage(error), { name }, { cause: error });
1137
+ }
1138
+ removeLives(unique);
1139
+ for (const name of unique) {
1140
+ const id = this.idByName.get(name);
1141
+ if (id !== void 0) this.unbind(id);
1142
+ }
1143
+ return removed;
1144
+ }
1145
+ async removeVar(name) {
1146
+ return await this.removeVars([name]) > 0;
1147
+ }
1148
+ async deleteEntry(id) {
1149
+ const name = this.requireName(id, id);
1150
+ await this.removeVars([name]);
1008
1151
  return this.list();
1009
1152
  }
1010
1153
  async restore(id) {
@@ -1014,9 +1157,9 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1014
1157
  }
1015
1158
  const base = this.baselineById.get(id);
1016
1159
  if (base === void 0) return this.deleteEntry(id);
1017
- if (name === base.name && loadEnvMap()[base.name] === base.value) return this.list();
1160
+ if (name === base.name && readEnvMap()[base.name] === base.value) return this.list();
1018
1161
  try {
1019
- this.commit(name, base.name, base.value);
1162
+ this.commit(name, base.name, base.value, true);
1020
1163
  } catch (error) {
1021
1164
  if (error instanceof RemoteError) {
1022
1165
  throw new RemoteError("custom-env/restore-failed", error.message, { name }, { cause: error });
@@ -1041,6 +1184,10 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1041
1184
  _init = __decoratorStart(_a);
1042
1185
  __decorateElement(_init, 1, "list", _list_dec, CustomEnvController);
1043
1186
  __decorateElement(_init, 1, "upsert", _upsert_dec, CustomEnvController);
1187
+ __decorateElement(_init, 1, "setVars", _setVars_dec, CustomEnvController);
1188
+ __decorateElement(_init, 1, "setVar", _setVar_dec, CustomEnvController);
1189
+ __decorateElement(_init, 1, "removeVars", _removeVars_dec, CustomEnvController);
1190
+ __decorateElement(_init, 1, "removeVar", _removeVar_dec, CustomEnvController);
1044
1191
  __decorateElement(_init, 1, "deleteEntry", _deleteEntry_dec, CustomEnvController);
1045
1192
  __decorateElement(_init, 1, "restore", _restore_dec, CustomEnvController);
1046
1193
  __decorateElement(_init, 1, "cliStatus", _cliStatus_dec, CustomEnvController);