@woosau/dsh-custom-env 0.1.0 → 0.1.4

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,6 +686,7 @@ 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;
@@ -799,6 +705,9 @@ function loadBindings() {
799
705
  openKey: advapi32.func(
800
706
  "long __stdcall RegOpenKeyExW(uintptr_t hKey, const char16_t *subKey, uint32_t options, uint32_t samDesired, _Out_ void **result)"
801
707
  ),
708
+ queryValue: advapi32.func(
709
+ "long __stdcall RegQueryValueExW(void *hKey, const char16_t *valueName, void *reserved, _Inout_ uint32_t *type, uint8_t *data, _Inout_ uint32_t *cbData)"
710
+ ),
802
711
  setValue: advapi32.func(
803
712
  "long __stdcall RegSetValueExW(void *hKey, const char16_t *valueName, uint32_t reserved, uint32_t type, const uint8_t *data, uint32_t cbData)"
804
713
  ),
@@ -827,7 +736,28 @@ function broadcastEnvironmentChange(api) {
827
736
  broadcastResult
828
737
  );
829
738
  }
830
- function persistWindowsUserEnv(entries, platform = process.platform) {
739
+ function hasWindowsUserEnv(name, platform = process.platform) {
740
+ if (platform !== "win32") return false;
741
+ const api = loadBindings();
742
+ const keySlot = [null];
743
+ const openStatus = api.openKey(HKEY_CURRENT_USER, "Environment", 0, KEY_QUERY_VALUE, keySlot);
744
+ if (openStatus !== ERROR_SUCCESS) {
745
+ throw new Error(`custom-env: RegOpenKeyExW failed (Win32 ${openStatus})`);
746
+ }
747
+ const hKey = keySlot[0];
748
+ try {
749
+ const cbData = [0];
750
+ const status = api.queryValue(hKey, name, null, null, null, cbData);
751
+ if (status === ERROR_FILE_NOT_FOUND) return false;
752
+ if (status !== ERROR_SUCCESS && status !== 234) {
753
+ throw new Error(`custom-env: RegQueryValueExW(${name}) failed (Win32 ${status})`);
754
+ }
755
+ return true;
756
+ } finally {
757
+ api.closeKey(hKey);
758
+ }
759
+ }
760
+ function writeWindowsUserEnv(entries, platform = process.platform) {
831
761
  if (platform !== "win32") return;
832
762
  const pairs = Object.entries(entries);
833
763
  if (pairs.length === 0) return;
@@ -851,8 +781,8 @@ function persistWindowsUserEnv(entries, platform = process.platform) {
851
781
  }
852
782
  broadcastEnvironmentChange(api);
853
783
  }
854
- function removeWindowsUserEnv(name, platform = process.platform) {
855
- if (platform !== "win32") return;
784
+ function removeWindowsUserEnvs(names, platform = process.platform) {
785
+ if (platform !== "win32" || names.length === 0) return;
856
786
  const api = loadBindings();
857
787
  const keySlot = [null];
858
788
  const openStatus = api.openKey(HKEY_CURRENT_USER, "Environment", 0, KEY_SET_VALUE, keySlot);
@@ -861,9 +791,11 @@ function removeWindowsUserEnv(name, platform = process.platform) {
861
791
  }
862
792
  const hKey = keySlot[0];
863
793
  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})`);
794
+ for (const name of names) {
795
+ const status = api.deleteValue(hKey, name);
796
+ if (status !== ERROR_SUCCESS && status !== ERROR_FILE_NOT_FOUND) {
797
+ throw new Error(`custom-env: RegDeleteValueW(${name}) failed (Win32 ${status})`);
798
+ }
867
799
  }
868
800
  } finally {
869
801
  api.closeKey(hKey);
@@ -872,13 +804,172 @@ function removeWindowsUserEnv(name, platform = process.platform) {
872
804
  }
873
805
 
874
806
  // src/os-user-env/index.ts
875
- function persistUserEnv(entries) {
876
- persistWindowsUserEnv(entries);
877
- persistMacosUserEnv(entries);
807
+ function hasUserEnv(name) {
808
+ return hasWindowsUserEnv(name) || hasMacosUserEnv(name);
809
+ }
810
+ function writeUserEnv(entries) {
811
+ writeWindowsUserEnv(entries);
812
+ writeMacosUserEnv(entries);
878
813
  }
879
- function removeUserEnv(name) {
880
- removeWindowsUserEnv(name);
881
- removeMacosUserEnv(name);
814
+ function removeUserEnvs(names) {
815
+ if (names.length === 0) return;
816
+ removeWindowsUserEnvs(names);
817
+ removeMacosUserEnvs(names);
818
+ }
819
+
820
+ // src/env-file.ts
821
+ function envFilePath() {
822
+ return join7(resolveDshHome(), ".env");
823
+ }
824
+ function readRaw() {
825
+ const path = envFilePath();
826
+ return existsSync7(path) ? readFileSync2(path, "utf8") : "";
827
+ }
828
+ function writeEnvFile(content) {
829
+ const path = envFilePath();
830
+ mkdirSync3(dirname5(path), { recursive: true });
831
+ writeFileSync3(path, content, "utf8");
832
+ }
833
+ function encodeValue(value) {
834
+ if (/^[A-Za-z0-9_./:@+-]*$/u.test(value)) return value;
835
+ return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"').replace(/\n/gu, "\\n")}"`;
836
+ }
837
+ function parseLines(content) {
838
+ const text = content.replace(/^\uFEFF/u, "");
839
+ if (text === "") return [];
840
+ return text.split(/\r?\n/u).map((raw) => {
841
+ const trimmed = raw.trim();
842
+ if (trimmed === "" || trimmed.startsWith("#")) return { kind: "other", raw };
843
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/u.exec(trimmed);
844
+ if (match?.[1] === void 0) return { kind: "other", raw };
845
+ return { kind: "assignment", raw, name: match[1] };
846
+ });
847
+ }
848
+ function serializeLines(lines) {
849
+ const kept = lines.filter((line) => !(line.kind === "other" && line.raw.trim() === ""));
850
+ return kept.length === 0 ? "" : `${kept.map((line) => line.raw).join("\n")}
851
+ `;
852
+ }
853
+ function assertWritableName(name) {
854
+ if (!isWritableEnvName(name)) {
855
+ throw new Error(`custom-env: refused bootstrap or invalid name "${name}"`);
856
+ }
857
+ }
858
+ function readEnvMap() {
859
+ const raw = readRaw().replace(/^\uFEFF/u, "").trim();
860
+ if (raw === "") return {};
861
+ let parsed;
862
+ try {
863
+ parsed = parseEnv(raw);
864
+ } catch (error) {
865
+ throw new Error(`custom-env: invalid dotenv syntax in ${envFilePath()}`, { cause: error });
866
+ }
867
+ const out = {};
868
+ for (const [key, value] of Object.entries(parsed)) {
869
+ if (typeof value === "string" && isWritableEnvName(key)) out[key] = value;
870
+ }
871
+ return out;
872
+ }
873
+ function listEnvEntries() {
874
+ const path = envFilePath();
875
+ const exists = existsSync7(path);
876
+ if (!exists) return { path, exists, entries: [] };
877
+ const map = readEnvMap();
878
+ const entries = [];
879
+ const seen = /* @__PURE__ */ new Set();
880
+ for (const line of parseLines(readRaw())) {
881
+ if (line.kind !== "assignment" || line.name === void 0 || seen.has(line.name)) continue;
882
+ const value = map[line.name];
883
+ if (value === void 0) continue;
884
+ seen.add(line.name);
885
+ entries.push({ name: line.name, value });
886
+ }
887
+ return { path, exists, entries };
888
+ }
889
+ function hasEnvKey(name) {
890
+ return Object.hasOwn(readEnvMap(), name) || hasUserEnv(name);
891
+ }
892
+ function selectEnvWrites(entries, options = {}) {
893
+ if (entries.length === 0) return [];
894
+ const lastByName = /* @__PURE__ */ new Map();
895
+ for (const entry of entries) {
896
+ assertWritableName(entry.name);
897
+ lastByName.set(entry.name, entry.value);
898
+ }
899
+ const forceEmpty = options.forceEmpty === true;
900
+ const selected = [];
901
+ for (const [name, value] of lastByName) {
902
+ if (value === "" && !forceEmpty && hasEnvKey(name)) continue;
903
+ selected.push({ name, value });
904
+ }
905
+ return selected;
906
+ }
907
+ function writeEnvEntries(entries, options = {}) {
908
+ const selected = selectEnvWrites(entries, options);
909
+ if (selected.length === 0) return selected;
910
+ const pending = new Map(selected.map((entry) => [entry.name, entry.value]));
911
+ const next = [];
912
+ for (const line of parseLines(readRaw())) {
913
+ if (line.kind === "assignment" && line.name !== void 0 && pending.has(line.name)) {
914
+ const value = pending.get(line.name);
915
+ pending.delete(line.name);
916
+ next.push({
917
+ kind: "assignment",
918
+ raw: `${line.name}=${encodeValue(value)}`,
919
+ name: line.name
920
+ });
921
+ continue;
922
+ }
923
+ next.push(line);
924
+ }
925
+ for (const [name, value] of pending) {
926
+ next.push({ kind: "assignment", raw: `${name}=${encodeValue(value)}`, name });
927
+ }
928
+ writeEnvFile(serializeLines(next));
929
+ return selected;
930
+ }
931
+ function writeEnvEntry(name, value, options = {}) {
932
+ return writeEnvEntries([{ name, value }], options).length > 0;
933
+ }
934
+ function renameEnvEntry(from, to, value) {
935
+ assertWritableName(from);
936
+ assertWritableName(to);
937
+ const assignment = `${to}=${encodeValue(value)}`;
938
+ let replaced = false;
939
+ const next = [];
940
+ for (const line of parseLines(readRaw())) {
941
+ if (line.kind === "assignment" && line.name === from) {
942
+ replaced = true;
943
+ next.push({ kind: "assignment", raw: assignment, name: to });
944
+ continue;
945
+ }
946
+ if (line.kind === "assignment" && line.name === to) continue;
947
+ next.push(line);
948
+ }
949
+ if (!replaced) next.push({ kind: "assignment", raw: assignment, name: to });
950
+ writeEnvFile(serializeLines(next));
951
+ }
952
+ function removeEnvEntries(names) {
953
+ if (names.length === 0) return 0;
954
+ const drop = /* @__PURE__ */ new Set();
955
+ for (const name of names) {
956
+ assertWritableName(name);
957
+ drop.add(name);
958
+ }
959
+ if (!existsSync7(envFilePath())) return 0;
960
+ const lines = parseLines(readRaw());
961
+ let removed = 0;
962
+ const next = [];
963
+ for (const line of lines) {
964
+ if (line.kind === "assignment" && line.name !== void 0 && drop.has(line.name)) {
965
+ removed += 1;
966
+ continue;
967
+ }
968
+ next.push(line);
969
+ }
970
+ if (removed === 0) return 0;
971
+ writeEnvFile(serializeLines(next));
972
+ return removed;
882
973
  }
883
974
 
884
975
  // src/types.ts
@@ -893,26 +984,32 @@ function classifyEntry(id, name, value, baselineById) {
893
984
  function asMessage(error) {
894
985
  return error instanceof Error ? error.message : String(error);
895
986
  }
896
- function applyLive(entries) {
987
+ function writeLive(entries) {
897
988
  for (const [key, value] of Object.entries(entries)) process.env[key] = value;
898
989
  try {
899
- persistUserEnv(entries);
990
+ writeUserEnv(entries);
900
991
  } catch (error) {
901
- process.stderr.write(`custom-env: failed to persist OS user environment: ${asMessage(error)}
992
+ process.stderr.write(`custom-env: failed to write OS user environment: ${asMessage(error)}
902
993
  `);
903
994
  }
904
995
  }
905
- function clearLive(name) {
906
- delete process.env[name];
996
+ function removeLives(names) {
997
+ if (names.length === 0) return;
998
+ for (const name of names) delete process.env[name];
907
999
  try {
908
- removeUserEnv(name);
1000
+ removeUserEnvs(names);
909
1001
  } catch (error) {
910
- process.stderr.write(`custom-env: failed to remove OS user environment ${name}: ${asMessage(error)}
911
- `);
1002
+ process.stderr.write(
1003
+ `custom-env: failed to remove OS user environment (${names.join(", ")}): ${asMessage(error)}
1004
+ `
1005
+ );
912
1006
  }
913
1007
  }
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) {
1008
+ function removeLive(name) {
1009
+ removeLives([name]);
1010
+ }
1011
+ var _cliInstall_dec, _cliStatus_dec, _restore_dec, _deleteEntry_dec, _removeVar_dec, _removeVars_dec, _setVar_dec, _setVars_dec, _upsert_dec, _list_dec, _a, _init;
1012
+ 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
1013
  constructor(ctx) {
917
1014
  super(ctx, "customEnv");
918
1015
  __runInitializers(_init, 5, this);
@@ -920,13 +1017,13 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
920
1017
  __publicField(this, "idByName", /* @__PURE__ */ new Map());
921
1018
  __publicField(this, "nameById", /* @__PURE__ */ new Map());
922
1019
  __publicField(this, "idSeq", 0);
923
- const loaded = loadEnvMap();
1020
+ const loaded = readEnvMap();
924
1021
  for (const [name, value] of Object.entries(loaded)) {
925
1022
  const id = this.allocId();
926
1023
  this.bind(id, name);
927
1024
  this.baselineById.set(id, { name, value });
928
1025
  }
929
- applyLive(loaded);
1026
+ writeLive(loaded);
930
1027
  }
931
1028
  allocId() {
932
1029
  return `env-${++this.idSeq}`;
@@ -949,21 +1046,25 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
949
1046
  }
950
1047
  return name;
951
1048
  }
952
- /** Persist `name=value` replacing `from` in place; sync process.env / OS. */
953
- commit(from, name, value) {
1049
+ /**
1050
+ * Write `name=value` replacing `from` in place; sync process.env / OS.
1051
+ * Empty values create missing keys only, unless `forceEmpty` (restore).
1052
+ */
1053
+ commit(from, name, value, forceEmpty = false) {
954
1054
  if (!isWritableEnvName(name) || from !== name && !isWritableEnvName(from)) {
955
1055
  throw new RemoteError("custom-env/invalid-name", `refused name "${name}"`, { name });
956
1056
  }
957
- if (value === "") {
958
- throw new RemoteError("custom-env/invalid-value", `empty value for "${name}"`, { name });
959
- }
960
1057
  try {
961
- writeEnvEntry(from, name, value);
1058
+ if (from === name) {
1059
+ if (!writeEnvEntry(name, value, { forceEmpty })) return;
1060
+ } else {
1061
+ renameEnvEntry(from, name, value);
1062
+ removeLive(from);
1063
+ }
962
1064
  } catch (error) {
963
1065
  throw new RemoteError("custom-env/invalid-name", asMessage(error), { name }, { cause: error });
964
1066
  }
965
- if (from !== name) clearLive(from);
966
- applyLive({ [name]: value });
1067
+ writeLive({ [name]: value });
967
1068
  }
968
1069
  async list() {
969
1070
  const { path, exists, entries: raw } = listEnvEntries();
@@ -993,18 +1094,66 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
993
1094
  this.bind(id, name);
994
1095
  return this.list();
995
1096
  }
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 });
1097
+ async setVars(entries) {
1098
+ if (entries.length === 0) return;
1099
+ const lastByName = /* @__PURE__ */ new Map();
1100
+ for (const entry of entries) {
1101
+ if (!isWritableEnvName(entry.name)) {
1102
+ throw new RemoteError("custom-env/invalid-name", `refused name "${entry.name}"`, { name: entry.name });
1103
+ }
1104
+ lastByName.set(entry.name, entry.value);
1000
1105
  }
1106
+ const normalized = [...lastByName].map(([name, value]) => ({ name, value }));
1107
+ let written;
1001
1108
  try {
1002
- removeEnvEntry(name);
1109
+ written = writeEnvEntries(normalized);
1003
1110
  } catch (error) {
1111
+ const name = normalized[0]?.name ?? "";
1004
1112
  throw new RemoteError("custom-env/invalid-name", asMessage(error), { name }, { cause: error });
1005
1113
  }
1006
- clearLive(name);
1007
- this.unbind(id);
1114
+ if (written.length === 0) return;
1115
+ const live = {};
1116
+ for (const entry of written) {
1117
+ live[entry.name] = entry.value;
1118
+ if (!this.idByName.has(entry.name)) this.bind(this.allocId(), entry.name);
1119
+ }
1120
+ writeLive(live);
1121
+ }
1122
+ async setVar(name, value) {
1123
+ await this.setVars([{ name, value }]);
1124
+ }
1125
+ async removeVars(names) {
1126
+ if (names.length === 0) return 0;
1127
+ const unique = [];
1128
+ const seen = /* @__PURE__ */ new Set();
1129
+ for (const name of names) {
1130
+ if (!isWritableEnvName(name)) {
1131
+ throw new RemoteError("custom-env/invalid-name", `refused name "${name}"`, { name });
1132
+ }
1133
+ if (seen.has(name)) continue;
1134
+ seen.add(name);
1135
+ unique.push(name);
1136
+ }
1137
+ let removed;
1138
+ try {
1139
+ removed = removeEnvEntries(unique);
1140
+ } catch (error) {
1141
+ const name = unique[0] ?? "";
1142
+ throw new RemoteError("custom-env/invalid-name", asMessage(error), { name }, { cause: error });
1143
+ }
1144
+ removeLives(unique);
1145
+ for (const name of unique) {
1146
+ const id = this.idByName.get(name);
1147
+ if (id !== void 0) this.unbind(id);
1148
+ }
1149
+ return removed;
1150
+ }
1151
+ async removeVar(name) {
1152
+ return await this.removeVars([name]) > 0;
1153
+ }
1154
+ async deleteEntry(id) {
1155
+ const name = this.requireName(id, id);
1156
+ await this.removeVars([name]);
1008
1157
  return this.list();
1009
1158
  }
1010
1159
  async restore(id) {
@@ -1014,9 +1163,9 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1014
1163
  }
1015
1164
  const base = this.baselineById.get(id);
1016
1165
  if (base === void 0) return this.deleteEntry(id);
1017
- if (name === base.name && loadEnvMap()[base.name] === base.value) return this.list();
1166
+ if (name === base.name && readEnvMap()[base.name] === base.value) return this.list();
1018
1167
  try {
1019
- this.commit(name, base.name, base.value);
1168
+ this.commit(name, base.name, base.value, true);
1020
1169
  } catch (error) {
1021
1170
  if (error instanceof RemoteError) {
1022
1171
  throw new RemoteError("custom-env/restore-failed", error.message, { name }, { cause: error });
@@ -1041,6 +1190,10 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1041
1190
  _init = __decoratorStart(_a);
1042
1191
  __decorateElement(_init, 1, "list", _list_dec, CustomEnvController);
1043
1192
  __decorateElement(_init, 1, "upsert", _upsert_dec, CustomEnvController);
1193
+ __decorateElement(_init, 1, "setVars", _setVars_dec, CustomEnvController);
1194
+ __decorateElement(_init, 1, "setVar", _setVar_dec, CustomEnvController);
1195
+ __decorateElement(_init, 1, "removeVars", _removeVars_dec, CustomEnvController);
1196
+ __decorateElement(_init, 1, "removeVar", _removeVar_dec, CustomEnvController);
1044
1197
  __decorateElement(_init, 1, "deleteEntry", _deleteEntry_dec, CustomEnvController);
1045
1198
  __decorateElement(_init, 1, "restore", _restore_dec, CustomEnvController);
1046
1199
  __decorateElement(_init, 1, "cliStatus", _cliStatus_dec, CustomEnvController);