@woosau/dsh-custom-env 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -11
- package/README.zh-CN.md +28 -11
- package/lib/client.js +682 -408
- package/lib/index.js +337 -19
- package/lib/typert.host.js +407 -465
- package/lib/typert.remote-client.d.ts +2 -1
- package/lib/typert.remote-client.js +371 -373
- package/lib/types/client/EnvSettingsSection.d.ts +7 -3
- package/lib/types/client/locales.d.ts +22 -2
- package/lib/types/group-ids.d.ts +5 -0
- package/lib/types/groups-store.d.ts +73 -0
- package/lib/types/index.d.ts +40 -6
- package/lib/types/os-user-env/write-user-env-child.d.ts +11 -0
- package/lib/types/os-user-env/write-user-env.child.d.ts +4 -0
- package/lib/types/types.d.ts +22 -0
- package/lib/write-user-env.child.js +158 -0
- package/package.json +2 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/** Settings section: `$DSH_HOME/.env` editor + global `dsh` path. */
|
|
2
2
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
-
import type { CliStatus, EnvSnapshot } from '../types.ts';
|
|
3
|
+
import type { CliStatus, EnvGroup, EnvSnapshot } from '../types.ts';
|
|
4
4
|
import type { EnvSettingsKey } from './locales.ts';
|
|
5
5
|
export interface EnvSettingsInjected {
|
|
6
6
|
readonly load: () => Promise<EnvSnapshot>;
|
|
7
7
|
/** Create (`id === ""`) or update/rename one row by stable id. */
|
|
8
|
-
readonly upsert: (id: string, name: string, value: string) => Promise<EnvSnapshot>;
|
|
8
|
+
readonly upsert: (id: string, name: string, value: string, groupId?: string) => Promise<EnvSnapshot>;
|
|
9
9
|
readonly remove: (id: string) => Promise<EnvSnapshot>;
|
|
10
10
|
readonly restore: (id: string) => Promise<EnvSnapshot>;
|
|
11
|
+
readonly moveEntry: (id: string, groupId: string) => Promise<EnvSnapshot>;
|
|
12
|
+
readonly createGroup: (title: string) => Promise<EnvGroup>;
|
|
13
|
+
readonly renameGroup: (id: string, title: string) => Promise<EnvGroup>;
|
|
14
|
+
readonly deleteGroup: (id: string, moveToDefault: boolean) => Promise<EnvSnapshot>;
|
|
11
15
|
readonly cliStatus: () => Promise<CliStatus>;
|
|
12
16
|
readonly cliInstall: () => Promise<CliStatus>;
|
|
13
17
|
}
|
|
@@ -16,5 +20,5 @@ export type EnvSettingsSectionProps = PropsRuntime<'settings.section'> & PropsLo
|
|
|
16
20
|
* Render the environment settings page.
|
|
17
21
|
* @param props - locale `t` and Host remote callbacks.
|
|
18
22
|
*/
|
|
19
|
-
export declare function EnvSettingsSection({ t, load, upsert, remove, restore, cliStatus, cliInstall, }: EnvSettingsSectionProps): any;
|
|
23
|
+
export declare function EnvSettingsSection({ t, load, upsert, remove, restore, moveEntry, createGroup, renameGroup, deleteGroup, cliStatus, cliInstall, }: EnvSettingsSectionProps): any;
|
|
20
24
|
export type { EnvSettingsKey };
|
|
@@ -5,7 +5,6 @@ export declare const NS: "settings.env";
|
|
|
5
5
|
export declare const zh: {
|
|
6
6
|
'panel.nav': string;
|
|
7
7
|
'panel.title': string;
|
|
8
|
-
'panel.empty': string;
|
|
9
8
|
'panel.loading': string;
|
|
10
9
|
'field.name': string;
|
|
11
10
|
'field.value': string;
|
|
@@ -19,6 +18,16 @@ export declare const zh: {
|
|
|
19
18
|
'action.removeConfirm': string;
|
|
20
19
|
'action.restore': string;
|
|
21
20
|
'action.close': string;
|
|
21
|
+
'action.moveGroup': string;
|
|
22
|
+
'action.createGroup': string;
|
|
23
|
+
'action.renameGroup': string;
|
|
24
|
+
'action.deleteGroup': string;
|
|
25
|
+
'action.deleteGroupTitle': string;
|
|
26
|
+
'action.deleteGroupConfirm': string;
|
|
27
|
+
'action.deleteGroupMove': string;
|
|
28
|
+
'action.groupTitle': string;
|
|
29
|
+
'action.groupTitlePlaceholder': string;
|
|
30
|
+
'group.defaultTitle': string;
|
|
22
31
|
'status.restartRequired': string;
|
|
23
32
|
'cli.title': string;
|
|
24
33
|
'cli.activeMissing': string;
|
|
@@ -27,6 +36,7 @@ export declare const zh: {
|
|
|
27
36
|
'error.generic': string;
|
|
28
37
|
'error.invalidName': string;
|
|
29
38
|
'error.invalidValue': string;
|
|
39
|
+
'error.invalidGroup': string;
|
|
30
40
|
'error.restoreFailed': string;
|
|
31
41
|
'error.cliInstall': string;
|
|
32
42
|
'error.cliUnresolved': string;
|
|
@@ -38,7 +48,6 @@ export type EnvSettingsKey = keyof typeof zh;
|
|
|
38
48
|
export declare const en: {
|
|
39
49
|
'panel.nav': string;
|
|
40
50
|
'panel.title': string;
|
|
41
|
-
'panel.empty': string;
|
|
42
51
|
'panel.loading': string;
|
|
43
52
|
'field.name': string;
|
|
44
53
|
'field.value': string;
|
|
@@ -52,6 +61,16 @@ export declare const en: {
|
|
|
52
61
|
'action.removeConfirm': string;
|
|
53
62
|
'action.restore': string;
|
|
54
63
|
'action.close': string;
|
|
64
|
+
'action.moveGroup': string;
|
|
65
|
+
'action.createGroup': string;
|
|
66
|
+
'action.renameGroup': string;
|
|
67
|
+
'action.deleteGroup': string;
|
|
68
|
+
'action.deleteGroupTitle': string;
|
|
69
|
+
'action.deleteGroupConfirm': string;
|
|
70
|
+
'action.deleteGroupMove': string;
|
|
71
|
+
'action.groupTitle': string;
|
|
72
|
+
'action.groupTitlePlaceholder': string;
|
|
73
|
+
'group.defaultTitle': string;
|
|
55
74
|
'status.restartRequired': string;
|
|
56
75
|
'cli.title': string;
|
|
57
76
|
'cli.activeMissing': string;
|
|
@@ -60,6 +79,7 @@ export declare const en: {
|
|
|
60
79
|
'error.generic': string;
|
|
61
80
|
'error.invalidName': string;
|
|
62
81
|
'error.invalidValue': string;
|
|
82
|
+
'error.invalidGroup': string;
|
|
63
83
|
'error.restoreFailed': string;
|
|
64
84
|
'error.cliInstall': string;
|
|
65
85
|
'error.cliUnresolved': string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Shared group id constants (safe for Host and Client bundles). */
|
|
2
|
+
/** Stable id of the built-in default group. */
|
|
3
|
+
export declare const DEFAULT_GROUP_ID = "default";
|
|
4
|
+
/** Fixed title stored for the default group (UI may localize on display). */
|
|
5
|
+
export declare const DEFAULT_GROUP_TITLE = "\u9ED8\u8BA4\u5206\u7EC4";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Sidecar JSON for environment variable groups (UI / plugin metadata only). */
|
|
2
|
+
import type { EnvGroup } from './types.ts';
|
|
3
|
+
export { DEFAULT_GROUP_ID, DEFAULT_GROUP_TITLE } from './group-ids.ts';
|
|
4
|
+
/** On-disk groups document. */
|
|
5
|
+
export interface GroupsFile {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
readonly groups: readonly EnvGroup[];
|
|
8
|
+
/** entryId → groupId */
|
|
9
|
+
readonly membership: Readonly<Record<string, string>>;
|
|
10
|
+
}
|
|
11
|
+
/** Absolute path of `$DSH_HOME/.dsh-custom-env-groups.json`. */
|
|
12
|
+
export declare function groupsFilePath(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Normalize arbitrary JSON into a valid groups file (always includes default).
|
|
15
|
+
* @param raw - Parsed JSON or undefined.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeGroupsFile(raw: unknown): GroupsFile;
|
|
18
|
+
/** Read and normalize the groups sidecar (missing file → default only). */
|
|
19
|
+
export declare function readGroupsFile(): GroupsFile;
|
|
20
|
+
/** Persist a groups file under `$DSH_HOME`. */
|
|
21
|
+
export declare function writeGroupsFile(file: GroupsFile): void;
|
|
22
|
+
/**
|
|
23
|
+
* Drop membership keys that no longer exist as live row ids.
|
|
24
|
+
* @param file - Current groups file.
|
|
25
|
+
* @param liveIds - Row ids still present.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pruneMembership(file: GroupsFile, liveIds: ReadonlySet<string>): GroupsFile;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the group for a row id (missing → default).
|
|
30
|
+
* @param file - Groups file.
|
|
31
|
+
* @param entryId - Stable row id.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveGroupId(file: GroupsFile, entryId: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Assign a row to a group.
|
|
36
|
+
* @param file - Groups file.
|
|
37
|
+
* @param entryId - Row id.
|
|
38
|
+
* @param groupId - Target group (must exist, or default).
|
|
39
|
+
*/
|
|
40
|
+
export declare function setMembership(file: GroupsFile, entryId: string, groupId: string): GroupsFile;
|
|
41
|
+
/** Remove one row from membership. */
|
|
42
|
+
export declare function clearMembership(file: GroupsFile, entryId: string): GroupsFile;
|
|
43
|
+
/** Remove many rows from membership. */
|
|
44
|
+
export declare function clearMembershipMany(file: GroupsFile, entryIds: readonly string[]): GroupsFile;
|
|
45
|
+
/**
|
|
46
|
+
* Append a custom group.
|
|
47
|
+
* @param file - Groups file.
|
|
48
|
+
* @param title - Non-empty display title.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createGroup(file: GroupsFile, title: string): {
|
|
51
|
+
readonly file: GroupsFile;
|
|
52
|
+
readonly group: EnvGroup;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Rename a custom group (not default).
|
|
56
|
+
* @param file - Groups file.
|
|
57
|
+
* @param id - Group id.
|
|
58
|
+
* @param title - New title.
|
|
59
|
+
*/
|
|
60
|
+
export declare function renameGroup(file: GroupsFile, id: string, title: string): GroupsFile;
|
|
61
|
+
/**
|
|
62
|
+
* Delete a custom group and either move or drop its membership keys.
|
|
63
|
+
* Does not delete `.env` values — caller handles variable deletion when needed.
|
|
64
|
+
* @param file - Groups file.
|
|
65
|
+
* @param id - Group id to remove.
|
|
66
|
+
* @param moveToDefault - When true, reassign members to default; else clear membership.
|
|
67
|
+
*/
|
|
68
|
+
export declare function deleteGroupMeta(file: GroupsFile, id: string, moveToDefault: boolean): {
|
|
69
|
+
readonly file: GroupsFile;
|
|
70
|
+
readonly memberIds: readonly string[];
|
|
71
|
+
};
|
|
72
|
+
/** Whether `groupId` exists in the file (including default). */
|
|
73
|
+
export declare function hasGroup(file: GroupsFile, groupId: string): boolean;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import type { Context } from '@deepseek-ai/cordis';
|
|
5
5
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
6
6
|
import { envFilePath } from './env-file.ts';
|
|
7
|
-
import { type CliStatus, type EnvSnapshot, type EnvVarWrite } from './types.ts';
|
|
8
|
-
export type { BaselineRow, CliStatus, EnvEntry, EnvSnapshot, EnvVarWrite } from './types.ts';
|
|
7
|
+
import { type CliStatus, type DeleteGroupOptions, type EnvGroup, type EnvSnapshot, type EnvVarWrite } from './types.ts';
|
|
8
|
+
export type { BaselineRow, CliStatus, DeleteGroupOptions, EnvEntry, EnvGroup, EnvSnapshot, EnvVarWrite, } from './types.ts';
|
|
9
9
|
export { classifyEntry } from './types.ts';
|
|
10
|
+
export { DEFAULT_GROUP_ID, DEFAULT_GROUP_TITLE } from './group-ids.ts';
|
|
10
11
|
declare module '@deepseek-ai/cordis' {
|
|
11
12
|
interface Context {
|
|
12
13
|
customEnv: CustomEnvController;
|
|
@@ -20,6 +21,9 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
20
21
|
'custom-env/invalid-value': {
|
|
21
22
|
readonly name: string;
|
|
22
23
|
};
|
|
24
|
+
'custom-env/invalid-group': {
|
|
25
|
+
readonly group: string;
|
|
26
|
+
};
|
|
23
27
|
'custom-env/cli-install-failed': {
|
|
24
28
|
readonly code: string;
|
|
25
29
|
};
|
|
@@ -39,34 +43,41 @@ export default class CustomEnvController extends TypertRemoteService {
|
|
|
39
43
|
private bind;
|
|
40
44
|
private unbind;
|
|
41
45
|
private requireName;
|
|
46
|
+
private loadGroups;
|
|
47
|
+
private saveGroups;
|
|
48
|
+
private requireGroup;
|
|
42
49
|
/**
|
|
43
50
|
* Write `name=value` replacing `from` in place; sync process.env / OS.
|
|
44
51
|
* Empty values create missing keys only, unless `forceEmpty` (restore).
|
|
45
52
|
*/
|
|
46
53
|
private commit;
|
|
47
|
-
/** @returns path, existence, and file-ordered entries. */
|
|
54
|
+
/** @returns path, existence, groups, and file-ordered entries. */
|
|
48
55
|
list(): Promise<EnvSnapshot>;
|
|
49
56
|
/**
|
|
50
57
|
* Create or update one row. Empty `id` creates; otherwise keeps that baseline id across renames.
|
|
51
58
|
* @param id - row id, or `""` for a new row.
|
|
52
59
|
* @param name - variable name.
|
|
53
60
|
* @param value - value (empty creates only when missing; Settings UI may still reject empty drafts).
|
|
61
|
+
* @param groupId - required for create semantics (`id === ""`, defaults to `default`);
|
|
62
|
+
* on update, omit to keep membership, or pass to reassign (same as `moveEntry`).
|
|
54
63
|
*/
|
|
55
|
-
upsert(id: string, name: string, value: string): Promise<EnvSnapshot>;
|
|
64
|
+
upsert(id: string, name: string, value: string, groupId?: string): Promise<EnvSnapshot>;
|
|
56
65
|
/**
|
|
57
66
|
* Create or replace many variables by name (plugin-facing).
|
|
58
67
|
* Non-empty values always write; empty values create missing keys only and do not
|
|
59
68
|
* clear existing `$DSH_HOME/.env` or OS user values. One file rewrite; last
|
|
60
69
|
* duplicate name wins. Invalid names reject the whole batch.
|
|
61
|
-
* @param entries - name/value pairs.
|
|
70
|
+
* @param entries - name/value pairs; optional `group` assigns membership when set.
|
|
71
|
+
* Omitted `group`: new names → `default`; existing names keep their group.
|
|
62
72
|
*/
|
|
63
73
|
setVars(entries: readonly EnvVarWrite[]): Promise<void>;
|
|
64
74
|
/**
|
|
65
75
|
* Create or replace one variable by name (plugin-facing).
|
|
66
76
|
* @param name - writable variable name.
|
|
67
77
|
* @param value - non-empty always stores; `""` writes `NAME=` only when the key is absent.
|
|
78
|
+
* @param group - optional group id; when omitted, new names use `default` and existing keep theirs.
|
|
68
79
|
*/
|
|
69
|
-
setVar(name: string, value: string): Promise<void>;
|
|
80
|
+
setVar(name: string, value: string, group?: string): Promise<void>;
|
|
70
81
|
/**
|
|
71
82
|
* Remove many variables by name (plugin-facing).
|
|
72
83
|
* @param names - writable variable names.
|
|
@@ -83,6 +94,29 @@ export default class CustomEnvController extends TypertRemoteService {
|
|
|
83
94
|
deleteEntry(id: string): Promise<EnvSnapshot>;
|
|
84
95
|
/** Restore Host-start original name+value, or delete if added after start. */
|
|
85
96
|
restore(id: string): Promise<EnvSnapshot>;
|
|
97
|
+
/**
|
|
98
|
+
* Create a custom group.
|
|
99
|
+
* @param title - Non-empty display title.
|
|
100
|
+
*/
|
|
101
|
+
createGroup(title: string): Promise<EnvGroup>;
|
|
102
|
+
/**
|
|
103
|
+
* Rename a custom group (not the default group).
|
|
104
|
+
* @param id - Group id.
|
|
105
|
+
* @param title - New title.
|
|
106
|
+
*/
|
|
107
|
+
renameGroup(id: string, title: string): Promise<EnvGroup>;
|
|
108
|
+
/**
|
|
109
|
+
* Delete a custom group.
|
|
110
|
+
* @param id - Group id (not `default`).
|
|
111
|
+
* @param options - When `moveToDefault` is false, member variables are removed from `.env`.
|
|
112
|
+
*/
|
|
113
|
+
deleteGroup(id: string, options: DeleteGroupOptions): Promise<EnvSnapshot>;
|
|
114
|
+
/**
|
|
115
|
+
* Move one row into another group.
|
|
116
|
+
* @param id - Row id.
|
|
117
|
+
* @param groupId - Target group id.
|
|
118
|
+
*/
|
|
119
|
+
moveEntry(id: string, groupId: string): Promise<EnvSnapshot>;
|
|
86
120
|
cliStatus(): Promise<CliStatus>;
|
|
87
121
|
cliInstall(): Promise<CliStatus>;
|
|
88
122
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn a child to persist OS user env without blocking the Host.
|
|
3
|
+
* Sets `ELECTRON_RUN_AS_NODE` so Desktop's Electron binary runs the script as Node.
|
|
4
|
+
*/
|
|
5
|
+
import { type ChildProcess } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Start a child that writes `entries` into the OS user environment.
|
|
8
|
+
* @param entries - Variable map to persist.
|
|
9
|
+
* @returns The child process, or `undefined` when the bundled script is missing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startWriteUserEnvChild(entries: Readonly<Record<string, string>>): ChildProcess | undefined;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -5,12 +5,19 @@ export interface BaselineRow {
|
|
|
5
5
|
readonly name: string;
|
|
6
6
|
readonly value: string;
|
|
7
7
|
}
|
|
8
|
+
/** One settings grouping for environment rows (UI / plugin metadata only). */
|
|
9
|
+
export interface EnvGroup {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
}
|
|
8
13
|
/** One editable environment variable row. */
|
|
9
14
|
export interface EnvEntry {
|
|
10
15
|
/** Stable row id; survives renames. */
|
|
11
16
|
readonly id: string;
|
|
12
17
|
readonly name: string;
|
|
13
18
|
readonly value: string;
|
|
19
|
+
/** Group id from the groups sidecar; defaults to `default`. */
|
|
20
|
+
readonly groupId: string;
|
|
14
21
|
readonly kind: EnvEntryKind;
|
|
15
22
|
/** Host-start original value when `kind === 'modified'`; otherwise `null`. */
|
|
16
23
|
readonly previous: string | null;
|
|
@@ -19,12 +26,27 @@ export interface EnvEntry {
|
|
|
19
26
|
export interface EnvSnapshot {
|
|
20
27
|
readonly path: string;
|
|
21
28
|
readonly exists: boolean;
|
|
29
|
+
readonly groups: readonly EnvGroup[];
|
|
22
30
|
readonly entries: readonly EnvEntry[];
|
|
23
31
|
}
|
|
24
32
|
/** One name/value pair for plugin-facing `setVar` / `setVars`. */
|
|
25
33
|
export interface EnvVarWrite {
|
|
26
34
|
readonly name: string;
|
|
27
35
|
readonly value: string;
|
|
36
|
+
/**
|
|
37
|
+
* Target group id.
|
|
38
|
+
* When set, membership is assigned to that group.
|
|
39
|
+
* When omitted, new names use `default` and existing names keep their group.
|
|
40
|
+
*/
|
|
41
|
+
readonly group?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Options for `customEnv/deleteGroup`. */
|
|
44
|
+
export interface DeleteGroupOptions {
|
|
45
|
+
/**
|
|
46
|
+
* When true, move member rows into the default group.
|
|
47
|
+
* When false, delete those variables from `.env` as well.
|
|
48
|
+
*/
|
|
49
|
+
readonly moveToDefault: boolean;
|
|
28
50
|
}
|
|
29
51
|
/** Snapshot returned by `customEnv/cliStatus` and `cliInstall`. */
|
|
30
52
|
export interface CliStatus {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/os-user-env/macos.ts
|
|
2
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
var AGENT_LABEL = "ai.dsh.custom-env";
|
|
7
|
+
var AGENT_DIR = () => join(homedir(), "Library", "LaunchAgents");
|
|
8
|
+
var AGENT_PATH = () => join(AGENT_DIR(), `${AGENT_LABEL}.plist`);
|
|
9
|
+
var cached;
|
|
10
|
+
function escapeXml(value) {
|
|
11
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """);
|
|
12
|
+
}
|
|
13
|
+
function buildPlist(entries) {
|
|
14
|
+
const pairs = Object.entries(entries).map(([key, value]) => ` <key>${escapeXml(key)}</key>
|
|
15
|
+
<string>${escapeXml(value)}</string>`).join("\n");
|
|
16
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
17
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
18
|
+
<plist version="1.0">
|
|
19
|
+
<dict>
|
|
20
|
+
<key>Label</key>
|
|
21
|
+
<string>${AGENT_LABEL}</string>
|
|
22
|
+
<key>ProgramArguments</key>
|
|
23
|
+
<array>
|
|
24
|
+
<string>/bin/true</string>
|
|
25
|
+
</array>
|
|
26
|
+
<key>RunAtLoad</key>
|
|
27
|
+
<true/>
|
|
28
|
+
<key>EnvironmentVariables</key>
|
|
29
|
+
<dict>
|
|
30
|
+
${pairs}
|
|
31
|
+
</dict>
|
|
32
|
+
</dict>
|
|
33
|
+
</plist>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
function launchctl(args) {
|
|
37
|
+
const result = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
38
|
+
if (result.error) throw result.error;
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
41
|
+
throw new Error(`custom-env: launchctl ${args.join(" ")} failed${detail === "" ? "" : `: ${detail}`}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function ensureCache() {
|
|
45
|
+
if (cached !== void 0) return cached;
|
|
46
|
+
cached = {};
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
function writeAgent(entries) {
|
|
50
|
+
const path = AGENT_PATH();
|
|
51
|
+
if (Object.keys(entries).length === 0) {
|
|
52
|
+
if (existsSync(path)) {
|
|
53
|
+
try {
|
|
54
|
+
launchctl(["unload", path]);
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
}
|
|
57
|
+
unlinkSync(path);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
mkdirSync(AGENT_DIR(), { recursive: true });
|
|
62
|
+
writeFileSync(path, buildPlist(entries), "utf8");
|
|
63
|
+
try {
|
|
64
|
+
launchctl(["unload", path]);
|
|
65
|
+
} catch (_error) {
|
|
66
|
+
}
|
|
67
|
+
launchctl(["load", path]);
|
|
68
|
+
}
|
|
69
|
+
function writeMacosUserEnv(entries, platform = process.platform) {
|
|
70
|
+
if (platform !== "darwin") return;
|
|
71
|
+
const pairs = Object.entries(entries);
|
|
72
|
+
if (pairs.length === 0) return;
|
|
73
|
+
const map = ensureCache();
|
|
74
|
+
for (const [name, value] of pairs) {
|
|
75
|
+
map[name] = value;
|
|
76
|
+
launchctl(["setenv", name, value]);
|
|
77
|
+
}
|
|
78
|
+
writeAgent(map);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/os-user-env/windows.ts
|
|
82
|
+
import { createRequire } from "node:module";
|
|
83
|
+
var HKEY_CURRENT_USER = 2147483649;
|
|
84
|
+
var KEY_SET_VALUE = 2;
|
|
85
|
+
var REG_SZ = 1;
|
|
86
|
+
var ERROR_SUCCESS = 0;
|
|
87
|
+
var bindings;
|
|
88
|
+
function loadBindings() {
|
|
89
|
+
if (bindings !== void 0) return bindings;
|
|
90
|
+
const require2 = createRequire(import.meta.url);
|
|
91
|
+
const koffi = require2("koffi");
|
|
92
|
+
const advapi32 = koffi.load("advapi32.dll");
|
|
93
|
+
bindings = {
|
|
94
|
+
openKey: advapi32.func(
|
|
95
|
+
"long __stdcall RegOpenKeyExW(uintptr_t hKey, const char16_t *subKey, uint32_t options, uint32_t samDesired, _Out_ void **result)"
|
|
96
|
+
),
|
|
97
|
+
queryValue: advapi32.func(
|
|
98
|
+
"long __stdcall RegQueryValueExW(void *hKey, const char16_t *valueName, void *reserved, _Inout_ uint32_t *type, uint8_t *data, _Inout_ uint32_t *cbData)"
|
|
99
|
+
),
|
|
100
|
+
setValue: advapi32.func(
|
|
101
|
+
"long __stdcall RegSetValueExW(void *hKey, const char16_t *valueName, uint32_t reserved, uint32_t type, const uint8_t *data, uint32_t cbData)"
|
|
102
|
+
),
|
|
103
|
+
deleteValue: advapi32.func(
|
|
104
|
+
"long __stdcall RegDeleteValueW(void *hKey, const char16_t *valueName)"
|
|
105
|
+
),
|
|
106
|
+
closeKey: advapi32.func("long __stdcall RegCloseKey(void *hKey)")
|
|
107
|
+
};
|
|
108
|
+
return bindings;
|
|
109
|
+
}
|
|
110
|
+
function encodeRegSz(value) {
|
|
111
|
+
return Buffer.from(`${value}\0`, "utf16le");
|
|
112
|
+
}
|
|
113
|
+
function writeWindowsUserEnv(entries, platform = process.platform) {
|
|
114
|
+
if (platform !== "win32") return;
|
|
115
|
+
const pairs = Object.entries(entries);
|
|
116
|
+
if (pairs.length === 0) return;
|
|
117
|
+
const api = loadBindings();
|
|
118
|
+
const keySlot = [null];
|
|
119
|
+
const openStatus = api.openKey(HKEY_CURRENT_USER, "Environment", 0, KEY_SET_VALUE, keySlot);
|
|
120
|
+
if (openStatus !== ERROR_SUCCESS) {
|
|
121
|
+
throw new Error(`custom-env: RegOpenKeyExW failed (Win32 ${openStatus})`);
|
|
122
|
+
}
|
|
123
|
+
const hKey = keySlot[0];
|
|
124
|
+
try {
|
|
125
|
+
for (const [name, value] of pairs) {
|
|
126
|
+
const data = encodeRegSz(value);
|
|
127
|
+
const status = api.setValue(hKey, name, 0, REG_SZ, data, data.length);
|
|
128
|
+
if (status !== ERROR_SUCCESS) {
|
|
129
|
+
throw new Error(`custom-env: RegSetValueExW(${name}) failed (Win32 ${status})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
api.closeKey(hKey);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/os-user-env/index.ts
|
|
138
|
+
function writeUserEnv(entries) {
|
|
139
|
+
writeWindowsUserEnv(entries);
|
|
140
|
+
writeMacosUserEnv(entries);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/os-user-env/write-user-env.child.ts
|
|
144
|
+
var chunks = [];
|
|
145
|
+
for await (const chunk of process.stdin) {
|
|
146
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
147
|
+
}
|
|
148
|
+
var raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
149
|
+
if (raw === "") process.exit(0);
|
|
150
|
+
try {
|
|
151
|
+
const entries = JSON.parse(raw);
|
|
152
|
+
writeUserEnv(entries);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
process.stderr.write(`custom-env child: ${message}
|
|
156
|
+
`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woosau/dsh-custom-env",
|
|
3
3
|
"description": "DSH plugin: manage $DSH_HOME/.env from Settings",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"cordis.patch.yml",
|
|
73
73
|
"scripts/command-path.ps1",
|
|
74
74
|
"lib/index.js",
|
|
75
|
+
"lib/write-user-env.child.js",
|
|
75
76
|
"lib/client.js",
|
|
76
77
|
"lib/types/**/*.d.ts",
|
|
77
78
|
"lib/typert.host.js",
|