@woosau/dsh-custom-env 0.1.6 → 0.1.8

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
@@ -28,9 +28,11 @@ dsh plugin --profile desktop remove @woosau/dsh-custom-env
28
28
 
29
29
  1. Open **Settings → Environment**
30
30
  2. Edit a row as a draft → **Confirm** to save, **Cancel** to discard
31
- 3. **Restore** brings back the value from when this app session started
32
- 4. At startup, values are injected into the Host `process.env` first; writing the user environment (registry / launchctl) is deferred off the boot path and is not broadcast. Log off or reboot before other tools need those values
33
- 5. Use **Install** only when shown, to wire this Host’s `dsh` command into your user PATH (Windows / macOS)
31
+ 3. Paste `NAME=VALUE` lines into the empty row to add or update several variables at once (same names overlay existing rows as drafts)
32
+ 4. 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)
33
+ 5. **Restore** brings back the value from when this app session started
34
+ 6. At startup, values are injected into the Host `process.env` first; writing the user environment (registry / launchctl) is deferred off the boot path and is not broadcast. Log off or reboot before other tools need those values
35
+ 7. Use **Install** only when shown, to wire this Host’s `dsh` command into your user PATH (Windows / macOS)
34
36
 
35
37
  System-reserved names (such as `PATH` and names starting with `DSH_` / `XDG_`) cannot be edited here.
36
38
 
package/README.zh-CN.md CHANGED
@@ -28,9 +28,11 @@ dsh plugin --profile desktop remove @woosau/dsh-custom-env
28
28
 
29
29
  1. 打开 **设置 → 环境变量**
30
30
  2. 先改草稿 → **确定** 保存,**取消** 放弃
31
- 3. **恢复** 可回到本次应用启动时的值
32
- 4. 启动时先注入当前 Host 的 `process.env`;写入用户环境(注册表 / launchctl)会延后到启动路径之后,且不会广播。其它工具若要读到,请注销/重启后再开终端
33
- 5. 仅在出现 **安装** 时使用:把本 Host 的 `dsh` 接到用户 PATH(Windows / macOS)
31
+ 3. 在空行粘贴多行 `NAME=VALUE` 可批量新增或覆盖同名变量(同名写入原行草稿)
32
+ 4. 拖动可多选行,Ctrl/Cmd+点击切换选中;Ctrl/Cmd+C 按 `NAME=VALUE` 复制选中行(若 Input 内有文字选区则优先复制文字)
33
+ 5. **恢复** 可回到本次应用启动时的值
34
+ 6. 启动时先注入当前 Host 的 `process.env`;写入用户环境(注册表 / launchctl)会延后到启动路径之后,且不会广播。其它工具若要读到,请注销/重启后再开终端
35
+ 7. 仅在出现 **安装** 时使用:把本 Host 的 `dsh` 接到用户 PATH(Windows / macOS)
34
36
 
35
37
  系统保留名(如 `PATH`,以及 `DSH_` / `XDG_` 开头)不可在此编辑。
36
38
 
package/lib/client.js CHANGED
@@ -553,8 +553,80 @@ function formatRemoteError(error, t) {
553
553
  return t("error.generic");
554
554
  }
555
555
 
556
+ // src/client/parse-env-paste.ts
557
+ var NAME_OK = /^[A-Za-z_][A-Za-z0-9_]*$/u;
558
+ function parseEnvPaste(text) {
559
+ const lastByName = /* @__PURE__ */ new Map();
560
+ const order = [];
561
+ for (const rawLine of text.split(/\r\n|\n|\r/u)) {
562
+ const line = rawLine.trim();
563
+ if (line === "") continue;
564
+ const eq = line.indexOf("=");
565
+ if (eq <= 0) continue;
566
+ const name = line.slice(0, eq).trim();
567
+ const value = line.slice(eq + 1).trim();
568
+ if (!NAME_OK.test(name)) continue;
569
+ if (!lastByName.has(name)) order.push(name);
570
+ lastByName.set(name, value);
571
+ }
572
+ return order.map((name) => ({ name, value: lastByName.get(name) }));
573
+ }
574
+ function formatEnvCopy(pairs) {
575
+ const lines = [];
576
+ for (const pair of pairs) {
577
+ const name = pair.name.trim();
578
+ if (name === "") continue;
579
+ lines.push(`${name}=${pair.value}`);
580
+ }
581
+ return lines.join("\n");
582
+ }
583
+
584
+ // src/client/env-row-select.ts
585
+ function isSelectionEditKey(event) {
586
+ return event.ctrlKey || event.metaKey;
587
+ }
588
+ function rangeIds(ordered, from, to) {
589
+ const a = ordered.indexOf(from);
590
+ const b = ordered.indexOf(to);
591
+ if (a < 0 || b < 0) return from === to && a >= 0 ? [from] : [];
592
+ const start = Math.min(a, b);
593
+ const end = Math.max(a, b);
594
+ return ordered.slice(start, end + 1);
595
+ }
596
+ function clearDomTextSelection() {
597
+ const selection = window.getSelection();
598
+ if (selection !== null && selection.rangeCount > 0) selection.removeAllRanges();
599
+ }
600
+ function hasFocusedInputTextSelection() {
601
+ const active = document.activeElement;
602
+ if (!(active instanceof HTMLInputElement) && !(active instanceof HTMLTextAreaElement)) return false;
603
+ const start = active.selectionStart;
604
+ const end = active.selectionEnd;
605
+ return start !== null && end !== null && start !== end;
606
+ }
607
+ function selectedEnvCopyText(selectedIds, orderedIds, drafts) {
608
+ if (selectedIds.size === 0) return "";
609
+ const pairs = orderedIds.flatMap((id) => {
610
+ if (!selectedIds.has(id)) return [];
611
+ const draft = drafts[id];
612
+ if (draft === void 0) return [];
613
+ const name = draft.name.trim();
614
+ if (name === "") return [];
615
+ return [{ name, value: draft.value }];
616
+ });
617
+ return formatEnvCopy(pairs);
618
+ }
619
+ function rowIdFromPoint(clientX, clientY) {
620
+ const hit = document.elementFromPoint(clientX, clientY);
621
+ if (!(hit instanceof Element)) return void 0;
622
+ const row = hit.closest("[data-env-row]");
623
+ if (!(row instanceof HTMLElement)) return void 0;
624
+ const id = row.dataset.envRow;
625
+ return id === void 0 || id === "" ? void 0 : id;
626
+ }
627
+
556
628
  // src/client/EnvSettingsSection.module.css
557
- var css = ".EnvSettingsSection_section {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_heading {\n margin: 0;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.3;\n}\n\n/* \u2014\u2014 dsh CLI: exactly two lines (label + path) \u2014\u2014 */\n.EnvSettingsSection_cli {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.EnvSettingsSection_cliHeading {\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.EnvSettingsSection_cliBody {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 28px;\n gap: 8px;\n align-items: center;\n height: 28px;\n}\n\n.EnvSettingsSection_cliPath {\n margin: 0;\n min-width: 0;\n font-size: 13px;\n line-height: 28px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.EnvSettingsSection_cliSlot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_cliSlot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_notice,\n.EnvSettingsSection_empty {\n margin: 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_notice[data-tone='error'] {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n/* \u2014\u2014 variable list: CSS grid, not table (avoids Input overflow overlap) \u2014\u2014 */\n.EnvSettingsSection_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.EnvSettingsSection_header,\n.EnvSettingsSection_row {\n display: grid;\n grid-template-columns: minmax(120px, 0.85fr) minmax(0, 1.55fr) 100px;\n gap: 10px;\n align-items: center;\n min-width: 0;\n}\n\n.EnvSettingsSection_header {\n padding: 0 0 2px;\n}\n\n.EnvSettingsSection_headerLabel {\n margin: 0;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_headerActions {\n width: 100px;\n}\n\n.EnvSettingsSection_field {\n min-width: 0;\n overflow: hidden;\n}\n\n.EnvSettingsSection_fieldInput {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n}\n\n.EnvSettingsSection_actions {\n display: grid;\n grid-template-columns: repeat(3, 28px);\n gap: 4px;\n align-items: center;\n justify-content: end;\n width: 100px;\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.EnvSettingsSection_slot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_slot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n margin: 0;\n padding: 0;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.EnvSettingsSection_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_iconButton:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.EnvSettingsSection_iconButtonDanger:hover:not(:disabled) {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n.EnvSettingsSection_iconButtonPrimary {\n color: var(--dsw-alias-brand-primary, #247bbf);\n}\n\n.EnvSettingsSection_restartBadge {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-warning, #c67c00);\n}\n\n.EnvSettingsSection_spinner {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .EnvSettingsSection_spinner {\n animation: none;\n }\n}\n";
629
+ var css = ".EnvSettingsSection_section {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_heading {\n margin: 0;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.3;\n}\n\n/* \u2014\u2014 dsh CLI: exactly two lines (label + path) \u2014\u2014 */\n.EnvSettingsSection_cli {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.EnvSettingsSection_cliHeading {\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.EnvSettingsSection_cliBody {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 28px;\n gap: 8px;\n align-items: center;\n height: 28px;\n}\n\n.EnvSettingsSection_cliPath {\n margin: 0;\n min-width: 0;\n font-size: 13px;\n line-height: 28px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.EnvSettingsSection_cliSlot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_cliSlot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_notice,\n.EnvSettingsSection_empty {\n margin: 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_notice[data-tone='error'] {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n/* \u2014\u2014 variable list: CSS grid, not table (avoids Input overflow overlap) \u2014\u2014 */\n.EnvSettingsSection_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.EnvSettingsSection_header,\n.EnvSettingsSection_row {\n display: grid;\n grid-template-columns: minmax(120px, 0.85fr) minmax(0, 1.55fr) 100px;\n gap: 10px;\n align-items: center;\n min-width: 0;\n}\n\n.EnvSettingsSection_row {\n padding: 4px 6px;\n margin: 0 -6px;\n user-select: none;\n}\n\n.EnvSettingsSection_row[data-selected='true'] {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.1));\n}\n\n.EnvSettingsSection_row[data-composer='true'] {\n user-select: auto;\n}\n\n.EnvSettingsSection_header {\n padding: 0 0 2px;\n}\n\n.EnvSettingsSection_headerLabel {\n margin: 0;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_headerActions {\n width: 100px;\n}\n\n.EnvSettingsSection_field {\n min-width: 0;\n overflow: hidden;\n}\n\n.EnvSettingsSection_fieldInput {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n /* Keep unfocused inputs out of drag text-select; focused field can still edit/select. */\n user-select: none;\n}\n\n.EnvSettingsSection_field:focus-within,\n.EnvSettingsSection_field:focus-within .EnvSettingsSection_fieldInput,\n.EnvSettingsSection_row[data-composer='true'] .EnvSettingsSection_fieldInput {\n user-select: text;\n}\n\n.EnvSettingsSection_actions {\n display: grid;\n grid-template-columns: repeat(3, 28px);\n gap: 4px;\n align-items: center;\n justify-content: end;\n width: 100px;\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.EnvSettingsSection_slot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_slot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n margin: 0;\n padding: 0;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.EnvSettingsSection_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_iconButton:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.EnvSettingsSection_iconButtonDanger:hover:not(:disabled) {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n.EnvSettingsSection_iconButtonPrimary {\n color: var(--dsw-alias-brand-primary, #247bbf);\n}\n\n.EnvSettingsSection_restartBadge {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-warning, #c67c00);\n}\n\n.EnvSettingsSection_spinner {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .EnvSettingsSection_spinner {\n animation: none;\n }\n}\n";
558
630
  if (typeof document !== "undefined" && !document.getElementById("dsh-custom-env-css")) {
559
631
  const style = document.createElement("style");
560
632
  style.id = "dsh-custom-env-css";
@@ -565,8 +637,12 @@ var EnvSettingsSection_default = { "section": "EnvSettingsSection_section", "hea
565
637
 
566
638
  // src/client/EnvSettingsSection.tsx
567
639
  var import_jsx_runtime = require("react/jsx-runtime");
568
- var NAME_OK = /^[A-Za-z_][A-Za-z0-9_]*$/u;
569
- var NEW_ID = "";
640
+ var NAME_OK2 = /^[A-Za-z_][A-Za-z0-9_]*$/u;
641
+ var COMPOSER_ID = "";
642
+ var PENDING_PREFIX = "pending:";
643
+ function isPendingId(id) {
644
+ return id.startsWith(PENDING_PREFIX);
645
+ }
570
646
  function IconAction({
571
647
  label,
572
648
  disabled,
@@ -622,9 +698,17 @@ function Field(props) {
622
698
  title: props.title,
623
699
  "aria-invalid": props.invalid || void 0,
624
700
  "aria-label": props.label,
701
+ onMouseDown: (event) => {
702
+ if (!props.rowSelectGestures) {
703
+ event.stopPropagation();
704
+ return;
705
+ }
706
+ if (isSelectionEditKey(event)) event.preventDefault();
707
+ },
625
708
  onChange: (event) => {
626
709
  props.onChange(event.target.value);
627
- }
710
+ },
711
+ onPaste: props.onPaste
628
712
  }
629
713
  ) });
630
714
  }
@@ -639,20 +723,57 @@ function EnvSettingsSection({
639
723
  }) {
640
724
  const [snapshot, setSnapshot] = (0, import_react.useState)();
641
725
  const [cli, setCli] = (0, import_react.useState)();
642
- const [drafts, setDrafts] = (0, import_react.useState)({ [NEW_ID]: { name: "", value: "" } });
726
+ const [drafts, setDrafts] = (0, import_react.useState)({ [COMPOSER_ID]: { name: "", value: "" } });
727
+ const [pendingOrder, setPendingOrder] = (0, import_react.useState)([]);
643
728
  const [busyTarget, setBusyTarget] = (0, import_react.useState)();
644
729
  const [error, setError] = (0, import_react.useState)();
645
730
  const [nameError, setNameError] = (0, import_react.useState)(false);
646
731
  const [pendingRemove, setPendingRemove] = (0, import_react.useState)();
732
+ const [selectedIds, setSelectedIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
733
+ const pendingSeq = (0, import_react.useRef)(0);
734
+ const draftsRef = (0, import_react.useRef)(drafts);
735
+ draftsRef.current = drafts;
736
+ const pendingOrderRef = (0, import_react.useRef)(pendingOrder);
737
+ pendingOrderRef.current = pendingOrder;
738
+ const selectedRef = (0, import_react.useRef)(selectedIds);
739
+ selectedRef.current = selectedIds;
740
+ const anchorIdRef = (0, import_react.useRef)(void 0);
741
+ const dragModeRef = (0, import_react.useRef)(void 0);
742
+ const dragVisitedRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
743
+ const dragPendingCrossRef = (0, import_react.useRef)(false);
744
+ const pendingRemoveRef = (0, import_react.useRef)(pendingRemove);
745
+ pendingRemoveRef.current = pendingRemove;
647
746
  const busy = busyTarget !== void 0;
648
747
  const entries = snapshot?.entries ?? [];
649
748
  const taken = new Set(entries.map((entry) => entry.name));
650
- const applySnapshot = (next) => {
749
+ const selectableOrder = [...entries.map((entry) => entry.id), ...pendingOrder];
750
+ const selectableOrderRef = (0, import_react.useRef)(selectableOrder);
751
+ selectableOrderRef.current = selectableOrder;
752
+ const keepPendingDrafts = (current, dropId) => {
753
+ const kept = {};
754
+ for (const [id, draft] of Object.entries(current)) {
755
+ if (!isPendingId(id) || id === dropId) continue;
756
+ kept[id] = draft;
757
+ }
758
+ return kept;
759
+ };
760
+ const applySnapshot = (next, dropPendingId) => {
651
761
  setSnapshot(next);
652
- setDrafts({
653
- [NEW_ID]: { name: "", value: "" },
762
+ if (dropPendingId !== void 0) {
763
+ setPendingOrder((order) => order.filter((item) => item !== dropPendingId));
764
+ setSelectedIds((previous) => {
765
+ if (!previous.has(dropPendingId)) return previous;
766
+ const ids = new Set(previous);
767
+ ids.delete(dropPendingId);
768
+ return ids;
769
+ });
770
+ if (anchorIdRef.current === dropPendingId) anchorIdRef.current = void 0;
771
+ }
772
+ setDrafts((current) => ({
773
+ [COMPOSER_ID]: { name: "", value: "" },
774
+ ...keepPendingDrafts(current, dropPendingId),
654
775
  ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
655
- });
776
+ }));
656
777
  };
657
778
  const setDraft = (id, patch) => {
658
779
  setDrafts((previous) => ({
@@ -660,12 +781,46 @@ function EnvSettingsSection({
660
781
  [id]: { name: patch.name ?? previous[id]?.name ?? "", value: patch.value ?? previous[id]?.value ?? "" }
661
782
  }));
662
783
  };
663
- const run = async (target, action) => {
784
+ const dropPending = (id) => {
785
+ setPendingOrder((order) => order.filter((item) => item !== id));
786
+ setSelectedIds((previous) => {
787
+ if (!previous.has(id)) return previous;
788
+ const next = new Set(previous);
789
+ next.delete(id);
790
+ return next;
791
+ });
792
+ if (anchorIdRef.current === id) anchorIdRef.current = void 0;
793
+ setDrafts((previous) => {
794
+ const next = { ...previous };
795
+ delete next[id];
796
+ return next;
797
+ });
798
+ };
799
+ const clearSelection = () => {
800
+ setSelectedIds(/* @__PURE__ */ new Set());
801
+ anchorIdRef.current = void 0;
802
+ clearDomTextSelection();
803
+ };
804
+ const pruneSelection = (allowed) => {
805
+ setSelectedIds((previous) => {
806
+ let changed = false;
807
+ const next = /* @__PURE__ */ new Set();
808
+ for (const id of previous) {
809
+ if (allowed.has(id)) next.add(id);
810
+ else changed = true;
811
+ }
812
+ return changed ? next : previous;
813
+ });
814
+ if (anchorIdRef.current !== void 0 && !allowed.has(anchorIdRef.current)) {
815
+ anchorIdRef.current = void 0;
816
+ }
817
+ };
818
+ const run = async (target, action, dropPendingId) => {
664
819
  if (busyTarget !== void 0) return;
665
820
  setBusyTarget(target);
666
821
  setError(void 0);
667
822
  try {
668
- applySnapshot(await action());
823
+ applySnapshot(await action(), dropPendingId);
669
824
  } catch (err) {
670
825
  setError(formatRemoteError(err, t));
671
826
  } finally {
@@ -676,7 +831,14 @@ function EnvSettingsSection({
676
831
  setBusyTarget("boot");
677
832
  setError(void 0);
678
833
  try {
679
- applySnapshot(await load());
834
+ setPendingOrder([]);
835
+ clearSelection();
836
+ const next = await load();
837
+ setSnapshot(next);
838
+ setDrafts({
839
+ [COMPOSER_ID]: { name: "", value: "" },
840
+ ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
841
+ });
680
842
  try {
681
843
  setCli(await cliStatus());
682
844
  } catch {
@@ -691,21 +853,207 @@ function EnvSettingsSection({
691
853
  (0, import_react.useEffect)(() => {
692
854
  void refresh();
693
855
  }, [refresh]);
694
- const saveRow = (id, entryName) => {
856
+ (0, import_react.useEffect)(() => {
857
+ const allowed = new Set(selectableOrder);
858
+ pruneSelection(allowed);
859
+ }, [selectableOrder.join("\0")]);
860
+ const stopEditingGesture = () => {
861
+ clearDomTextSelection();
862
+ const active = document.activeElement;
863
+ if (active instanceof HTMLInputElement && active.closest(`.${EnvSettingsSection_default.list}`) !== null) active.blur();
864
+ };
865
+ const applyDragOverRow = (id) => {
866
+ if (dragModeRef.current === void 0) return;
867
+ if (dragModeRef.current === "toggle") {
868
+ if (dragVisitedRef.current.has(id)) return;
869
+ stopEditingGesture();
870
+ dragVisitedRef.current.add(id);
871
+ setSelectedIds((previous) => {
872
+ const next = new Set(previous);
873
+ if (next.has(id)) next.delete(id);
874
+ else next.add(id);
875
+ return next;
876
+ });
877
+ return;
878
+ }
879
+ if (dragPendingCrossRef.current && id === anchorIdRef.current) return;
880
+ if (dragPendingCrossRef.current || id !== anchorIdRef.current) {
881
+ dragPendingCrossRef.current = false;
882
+ stopEditingGesture();
883
+ }
884
+ dragVisitedRef.current.add(id);
885
+ setSelectedIds(new Set(rangeIds(selectableOrderRef.current, anchorIdRef.current ?? id, id)));
886
+ };
887
+ (0, import_react.useEffect)(() => {
888
+ if (selectedIds.size > 0) clearDomTextSelection();
889
+ }, [selectedIds]);
890
+ (0, import_react.useEffect)(() => {
891
+ const endDrag = () => {
892
+ dragModeRef.current = void 0;
893
+ dragVisitedRef.current = /* @__PURE__ */ new Set();
894
+ dragPendingCrossRef.current = false;
895
+ };
896
+ const onMove = (event) => {
897
+ if (event.buttons !== 1 || dragModeRef.current === void 0) return;
898
+ const id = rowIdFromPoint(event.clientX, event.clientY);
899
+ if (id !== void 0) applyDragOverRow(id);
900
+ };
901
+ const onPointerDown = (event) => {
902
+ if (selectedRef.current.size === 0 || pendingRemoveRef.current !== void 0) return;
903
+ if (!(event.target instanceof Element)) return;
904
+ if (event.target.closest("[data-env-row]") !== null) return;
905
+ clearSelection();
906
+ };
907
+ const copyText = () => selectedEnvCopyText(selectedRef.current, selectableOrderRef.current, draftsRef.current);
908
+ const onKeyDown = (event) => {
909
+ if (selectedRef.current.size === 0) return;
910
+ if (event.key !== "c" && event.key !== "C" || !isSelectionEditKey(event) || event.altKey || event.shiftKey) return;
911
+ if (hasFocusedInputTextSelection()) return;
912
+ const text = copyText();
913
+ if (text === "") return;
914
+ event.preventDefault();
915
+ void navigator.clipboard.writeText(text);
916
+ };
917
+ const onCopy = (event) => {
918
+ if (selectedRef.current.size === 0 || hasFocusedInputTextSelection()) return;
919
+ const text = copyText();
920
+ if (text === "") return;
921
+ event.preventDefault();
922
+ event.clipboardData?.setData("text/plain", text);
923
+ };
924
+ window.addEventListener("mouseup", endDrag);
925
+ window.addEventListener("mousemove", onMove);
926
+ document.addEventListener("mousedown", onPointerDown, true);
927
+ document.addEventListener("keydown", onKeyDown, true);
928
+ document.addEventListener("copy", onCopy, true);
929
+ return () => {
930
+ window.removeEventListener("mouseup", endDrag);
931
+ window.removeEventListener("mousemove", onMove);
932
+ document.removeEventListener("mousedown", onPointerDown, true);
933
+ document.removeEventListener("keydown", onKeyDown, true);
934
+ document.removeEventListener("copy", onCopy, true);
935
+ };
936
+ }, []);
937
+ const saveExisting = (id, entryName) => {
695
938
  const draft = drafts[id] ?? { name: "", value: "" };
696
939
  const name = draft.name.trim();
697
940
  const takenByOther = name !== entryName && taken.has(name);
698
- if (!NAME_OK.test(name) || takenByOther) {
941
+ if (!NAME_OK2.test(name) || takenByOther) {
942
+ setNameError(true);
943
+ return;
944
+ }
945
+ if (draft.value === "") return;
946
+ setNameError(false);
947
+ void run(`row:${id}`, () => upsert(id, name, draft.value));
948
+ };
949
+ const savePending = (id) => {
950
+ const draft = drafts[id] ?? { name: "", value: "" };
951
+ const name = draft.name.trim();
952
+ if (!NAME_OK2.test(name) || taken.has(name)) {
699
953
  setNameError(true);
700
954
  return;
701
955
  }
702
956
  if (draft.value === "") return;
703
957
  setNameError(false);
704
- const target = id === NEW_ID ? "new" : `row:${id}`;
705
- void run(target, () => upsert(id, name, draft.value));
958
+ void run(`pending:${id}`, () => upsert(COMPOSER_ID, name, draft.value), id);
959
+ };
960
+ const typeIntoComposer = (patch, focus) => {
961
+ setNameError(false);
962
+ const base = draftsRef.current[COMPOSER_ID] ?? { name: "", value: "" };
963
+ const next = { name: patch.name ?? base.name, value: patch.value ?? base.value };
964
+ const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
965
+ setDrafts((previous) => ({
966
+ ...previous,
967
+ [id]: next,
968
+ [COMPOSER_ID]: { name: "", value: "" }
969
+ }));
970
+ setPendingOrder((order) => [...order, id]);
971
+ queueMicrotask(() => {
972
+ document.getElementById(`env-${focus}-${id}`)?.focus();
973
+ });
974
+ };
975
+ const appendPendingPairs = (pairs) => {
976
+ if (pairs.length === 0) return;
977
+ setNameError(false);
978
+ const previous = draftsRef.current;
979
+ const order = pendingOrderRef.current;
980
+ const nextDrafts = { ...previous, [COMPOSER_ID]: { name: "", value: "" } };
981
+ const savedByName = new Map(entries.map((entry) => [entry.name, entry.id]));
982
+ const existingPendingByName = /* @__PURE__ */ new Map();
983
+ for (const id of order) {
984
+ const draft = previous[id];
985
+ if (draft !== void 0) existingPendingByName.set(draft.name.trim(), id);
986
+ }
987
+ const added = [];
988
+ const dropPending2 = /* @__PURE__ */ new Set();
989
+ for (const pair of pairs) {
990
+ const savedId = savedByName.get(pair.name);
991
+ if (savedId !== void 0) {
992
+ nextDrafts[savedId] = { name: pair.name, value: pair.value };
993
+ const pendingDup = existingPendingByName.get(pair.name);
994
+ if (pendingDup !== void 0) {
995
+ delete nextDrafts[pendingDup];
996
+ dropPending2.add(pendingDup);
997
+ existingPendingByName.delete(pair.name);
998
+ }
999
+ continue;
1000
+ }
1001
+ const reuse = existingPendingByName.get(pair.name);
1002
+ if (reuse !== void 0) {
1003
+ nextDrafts[reuse] = { name: pair.name, value: pair.value };
1004
+ continue;
1005
+ }
1006
+ const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
1007
+ nextDrafts[id] = { name: pair.name, value: pair.value };
1008
+ existingPendingByName.set(pair.name, id);
1009
+ added.push(id);
1010
+ }
1011
+ setDrafts(nextDrafts);
1012
+ if (added.length > 0 || dropPending2.size > 0) {
1013
+ setPendingOrder([...order.filter((id) => !dropPending2.has(id)), ...added]);
1014
+ }
1015
+ };
1016
+ const onComposerPaste = (event) => {
1017
+ const text = event.clipboardData.getData("text");
1018
+ const pairs = parseEnvPaste(text);
1019
+ if (pairs.length === 0) return;
1020
+ event.preventDefault();
1021
+ appendPendingPairs(pairs);
706
1022
  };
707
- const newDraft = drafts[NEW_ID] ?? { name: "", value: "" };
708
- const newDirty = newDraft.name.trim() !== "" || newDraft.value !== "";
1023
+ const onRowMouseDown = (id, event) => {
1024
+ if (event.button !== 0) return;
1025
+ const target = event.target;
1026
+ const onInput = target?.closest("input") !== null;
1027
+ const onControl = target?.closest('button, a, [role="button"]') !== null;
1028
+ if (isSelectionEditKey(event)) {
1029
+ event.preventDefault();
1030
+ clearDomTextSelection();
1031
+ dragPendingCrossRef.current = false;
1032
+ if (event.shiftKey && anchorIdRef.current !== void 0) {
1033
+ const spanned = rangeIds(selectableOrder, anchorIdRef.current, id);
1034
+ setSelectedIds(new Set(spanned));
1035
+ dragModeRef.current = "range";
1036
+ dragVisitedRef.current = new Set(spanned);
1037
+ return;
1038
+ }
1039
+ setSelectedIds((previous) => {
1040
+ const next = new Set(previous);
1041
+ if (next.has(id)) next.delete(id);
1042
+ else next.add(id);
1043
+ return next;
1044
+ });
1045
+ anchorIdRef.current = id;
1046
+ dragModeRef.current = "toggle";
1047
+ dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
1048
+ return;
1049
+ }
1050
+ if (!onInput && !onControl) event.preventDefault();
1051
+ anchorIdRef.current = id;
1052
+ dragModeRef.current = "range";
1053
+ dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
1054
+ dragPendingCrossRef.current = true;
1055
+ };
1056
+ const composer = drafts[COMPOSER_ID] ?? { name: "", value: "" };
709
1057
  const cliInstalling = busyTarget === "cli";
710
1058
  const cliBooting = busyTarget === "boot" || cli === void 0;
711
1059
  const showCliAction = cli?.canInstall === true || cliInstalling;
@@ -755,120 +1103,189 @@ function EnvSettingsSection({
755
1103
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.value") }),
756
1104
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.headerActions, "aria-hidden": "true" })
757
1105
  ] }),
758
- snapshot !== void 0 && entries.length === 0 && notice === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.empty, children: t("panel.empty") }) : null,
1106
+ snapshot !== void 0 && entries.length === 0 && pendingOrder.length === 0 && notice === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.empty, children: t("panel.empty") }) : null,
759
1107
  entries.map((entry) => {
760
1108
  const draft = drafts[entry.id] ?? { name: entry.name, value: entry.value };
761
1109
  const dirty = draft.name !== entry.name || draft.value !== entry.value;
762
1110
  const pending = entry.kind === "added" || entry.kind === "modified";
763
1111
  const loading = busyTarget === `row:${entry.id}`;
764
1112
  const nextName = draft.name.trim();
765
- const canConfirm = NAME_OK.test(nextName) && draft.value !== "" && !(nextName !== entry.name && taken.has(nextName));
766
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, children: [
767
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
768
- Field,
769
- {
770
- id: `env-name-${entry.id}`,
771
- value: draft.name,
772
- disabled: busy,
773
- title: draft.name,
774
- invalid: nameError && dirty,
775
- label: t("field.name"),
776
- onChange: (name) => {
777
- setNameError(false);
778
- setDraft(entry.id, { name });
779
- }
780
- }
781
- ),
782
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
783
- Field,
784
- {
785
- id: `env-value-${entry.id}`,
786
- value: draft.value,
787
- disabled: busy,
788
- title: draft.value,
789
- label: t("field.value"),
790
- onChange: (value) => {
791
- setDraft(entry.id, { value });
792
- }
793
- }
794
- ),
795
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
796
- RowActions,
797
- {
798
- t,
799
- busy,
800
- loading,
801
- dirty: dirty || loading,
802
- canConfirm,
803
- pending,
804
- onConfirm: () => {
805
- saveRow(entry.id, entry.name);
806
- },
807
- onCancel: () => {
808
- setNameError(false);
809
- setDraft(entry.id, { name: entry.name, value: entry.value });
810
- },
811
- onRestore: () => {
812
- void run(`row:${entry.id}`, () => restore(entry.id));
813
- },
814
- onRemove: () => {
815
- setPendingRemove({ id: entry.id, name: entry.name });
816
- }
817
- }
818
- )
819
- ] }, entry.id);
1113
+ const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !(nextName !== entry.name && taken.has(nextName));
1114
+ const selected = selectedIds.has(entry.id);
1115
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1116
+ "div",
1117
+ {
1118
+ className: EnvSettingsSection_default.row,
1119
+ "data-env-row": entry.id,
1120
+ "data-selected": selected ? "true" : "false",
1121
+ "aria-selected": selected,
1122
+ onMouseDown: (event) => {
1123
+ onRowMouseDown(entry.id, event);
1124
+ },
1125
+ children: [
1126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1127
+ Field,
1128
+ {
1129
+ id: `env-name-${entry.id}`,
1130
+ value: draft.name,
1131
+ disabled: busy,
1132
+ title: draft.name,
1133
+ invalid: nameError && dirty,
1134
+ label: t("field.name"),
1135
+ rowSelectGestures: true,
1136
+ onChange: (name) => {
1137
+ setNameError(false);
1138
+ setDraft(entry.id, { name });
1139
+ }
1140
+ }
1141
+ ),
1142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1143
+ Field,
1144
+ {
1145
+ id: `env-value-${entry.id}`,
1146
+ value: draft.value,
1147
+ disabled: busy,
1148
+ title: draft.value,
1149
+ label: t("field.value"),
1150
+ rowSelectGestures: true,
1151
+ onChange: (value) => {
1152
+ setDraft(entry.id, { value });
1153
+ }
1154
+ }
1155
+ ),
1156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1157
+ RowActions,
1158
+ {
1159
+ t,
1160
+ busy,
1161
+ loading,
1162
+ dirty: dirty || loading,
1163
+ canConfirm,
1164
+ pending,
1165
+ onConfirm: () => {
1166
+ saveExisting(entry.id, entry.name);
1167
+ },
1168
+ onCancel: () => {
1169
+ setNameError(false);
1170
+ setDraft(entry.id, { name: entry.name, value: entry.value });
1171
+ },
1172
+ onRestore: () => {
1173
+ void run(`row:${entry.id}`, () => restore(entry.id));
1174
+ },
1175
+ onRemove: () => {
1176
+ setPendingRemove({ id: entry.id, name: entry.name });
1177
+ }
1178
+ }
1179
+ )
1180
+ ]
1181
+ },
1182
+ entry.id
1183
+ );
1184
+ }),
1185
+ pendingOrder.map((id) => {
1186
+ const draft = drafts[id] ?? { name: "", value: "" };
1187
+ const loading = busyTarget === `pending:${id}`;
1188
+ const nextName = draft.name.trim();
1189
+ const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !taken.has(nextName);
1190
+ const selected = selectedIds.has(id);
1191
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1192
+ "div",
1193
+ {
1194
+ className: EnvSettingsSection_default.row,
1195
+ "data-env-row": id,
1196
+ "data-selected": selected ? "true" : "false",
1197
+ "aria-selected": selected,
1198
+ onMouseDown: (event) => {
1199
+ onRowMouseDown(id, event);
1200
+ },
1201
+ children: [
1202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1203
+ Field,
1204
+ {
1205
+ id: `env-name-${id}`,
1206
+ value: draft.name,
1207
+ disabled: busy,
1208
+ title: draft.name,
1209
+ invalid: nameError,
1210
+ label: t("field.name"),
1211
+ rowSelectGestures: true,
1212
+ onChange: (name) => {
1213
+ setNameError(false);
1214
+ setDraft(id, { name });
1215
+ }
1216
+ }
1217
+ ),
1218
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1219
+ Field,
1220
+ {
1221
+ id: `env-value-${id}`,
1222
+ value: draft.value,
1223
+ disabled: busy,
1224
+ title: draft.value,
1225
+ label: t("field.value"),
1226
+ rowSelectGestures: true,
1227
+ onChange: (value) => {
1228
+ setDraft(id, { value });
1229
+ }
1230
+ }
1231
+ ),
1232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1233
+ RowActions,
1234
+ {
1235
+ t,
1236
+ busy,
1237
+ loading,
1238
+ dirty: true,
1239
+ canConfirm,
1240
+ showRemove: false,
1241
+ onConfirm: () => {
1242
+ savePending(id);
1243
+ },
1244
+ onCancel: () => {
1245
+ setNameError(false);
1246
+ dropPending(id);
1247
+ },
1248
+ onRestore: () => void 0,
1249
+ onRemove: () => void 0
1250
+ }
1251
+ )
1252
+ ]
1253
+ },
1254
+ id
1255
+ );
820
1256
  }),
821
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, children: [
1257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, "data-composer": "true", children: [
822
1258
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
823
1259
  Field,
824
1260
  {
825
1261
  id: "env-new-name",
826
- value: newDraft.name,
1262
+ value: composer.name,
827
1263
  disabled: busy,
828
1264
  placeholder: t("field.namePlaceholder"),
829
- title: newDraft.name || t("field.namePlaceholder"),
830
- invalid: nameError,
1265
+ title: composer.name || t("field.namePlaceholder"),
831
1266
  label: t("field.name"),
832
1267
  onChange: (name) => {
833
- setNameError(false);
834
- setDraft(NEW_ID, { name });
835
- }
1268
+ typeIntoComposer({ name }, "name");
1269
+ },
1270
+ onPaste: onComposerPaste
836
1271
  }
837
1272
  ),
838
1273
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
839
1274
  Field,
840
1275
  {
841
1276
  id: "env-new-value",
842
- value: newDraft.value,
1277
+ value: composer.value,
843
1278
  disabled: busy,
844
1279
  placeholder: t("field.valuePlaceholder"),
845
- title: newDraft.value || t("field.valuePlaceholder"),
1280
+ title: composer.value || t("field.valuePlaceholder"),
846
1281
  label: t("field.value"),
847
1282
  onChange: (value) => {
848
- setDraft(NEW_ID, { value });
849
- }
850
- }
851
- ),
852
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
853
- RowActions,
854
- {
855
- t,
856
- busy,
857
- loading: busyTarget === "new",
858
- dirty: newDirty || busyTarget === "new",
859
- canConfirm: NAME_OK.test(newDraft.name.trim()) && newDraft.value !== "" && !taken.has(newDraft.name.trim()),
860
- showRemove: false,
861
- onConfirm: () => {
862
- saveRow(NEW_ID, void 0);
863
- },
864
- onCancel: () => {
865
- setNameError(false);
866
- setDraft(NEW_ID, { name: "", value: "" });
1283
+ typeIntoComposer({ value }, "value");
867
1284
  },
868
- onRestore: () => void 0,
869
- onRemove: () => void 0
1285
+ onPaste: onComposerPaste
870
1286
  }
871
- )
1287
+ ),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.actions, "aria-hidden": "true" })
872
1289
  ] })
873
1290
  ] }),
874
1291
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -0,0 +1,36 @@
1
+ /** Pure helpers for Environment row multi-select and KEY=VALUE copy. */
2
+ /** Ctrl (Windows/Linux) or Meta (macOS) — required for click-to-toggle selection. */
3
+ export declare function isSelectionEditKey(event: {
4
+ ctrlKey: boolean;
5
+ metaKey: boolean;
6
+ }): boolean;
7
+ /**
8
+ * Inclusive id span between two anchors in list order.
9
+ * @param ordered - Selectable row ids in display order.
10
+ * @param from - Range start id.
11
+ * @param to - Range end id.
12
+ * @returns Ordered ids from the earlier index through the later; empty if either id is missing.
13
+ */
14
+ export declare function rangeIds(ordered: readonly string[], from: string, to: string): string[];
15
+ /** Drop the browser text selection (not row selection). */
16
+ export declare function clearDomTextSelection(): void;
17
+ /** True when the focused input/textarea has a non-empty character range. */
18
+ export declare function hasFocusedInputTextSelection(): boolean;
19
+ /**
20
+ * Build clipboard text for selected rows in list order.
21
+ * @param selectedIds - Selected row ids.
22
+ * @param orderedIds - Display order (saved then pending).
23
+ * @param drafts - Current name/value drafts by id.
24
+ * @returns `NAME=VALUE` lines, or empty when nothing usable.
25
+ */
26
+ export declare function selectedEnvCopyText(selectedIds: ReadonlySet<string>, orderedIds: readonly string[], drafts: Readonly<Record<string, {
27
+ name: string;
28
+ value: string;
29
+ }>>): string;
30
+ /**
31
+ * Resolve the env row under a client point, if any.
32
+ * @param clientX - Viewport X.
33
+ * @param clientY - Viewport Y.
34
+ * @returns `data-env-row` id, or undefined.
35
+ */
36
+ export declare function rowIdFromPoint(clientX: number, clientY: number): string | undefined;
@@ -0,0 +1,21 @@
1
+ /** Parse clipboard text into environment name/value pairs for Settings paste. */
2
+ /** One parsed assignment from a paste. */
3
+ export interface ParsedEnvPair {
4
+ readonly name: string;
5
+ readonly value: string;
6
+ }
7
+ /**
8
+ * Parse multiline `NAME=VALUE` paste text into pairs.
9
+ * Trims lines and spaces around `=`; splits on the first `=`; skips blanks and invalid names.
10
+ * Duplicate names in one paste keep the last value.
11
+ * @param text - Clipboard text.
12
+ * @returns Ordered unique pairs (last duplicate wins).
13
+ */
14
+ export declare function parseEnvPaste(text: string): ParsedEnvPair[];
15
+ /**
16
+ * Format pairs for clipboard copy (one `NAME=VALUE` per line).
17
+ * Skips empty names. Does not trim values beyond what the caller provides.
18
+ * @param pairs - Ordered name/value pairs.
19
+ * @returns Clipboard text; empty string when no usable pairs.
20
+ */
21
+ export declare function formatEnvCopy(pairs: readonly ParsedEnvPair[]): string;
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.6",
4
+ "version": "0.1.8",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",