@woosau/dsh-custom-env 0.1.6 → 0.1.9

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
@@ -951,6 +951,153 @@ function removeEnvEntries(names) {
951
951
  return removed;
952
952
  }
953
953
 
954
+ // src/groups-store.ts
955
+ import { randomBytes } from "node:crypto";
956
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
957
+ import { dirname as dirname6, join as join8 } from "node:path";
958
+ import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
959
+
960
+ // src/group-ids.ts
961
+ var DEFAULT_GROUP_ID = "default";
962
+ var DEFAULT_GROUP_TITLE = "\u9ED8\u8BA4\u5206\u7EC4";
963
+
964
+ // src/groups-store.ts
965
+ function groupsFilePath() {
966
+ return join8(resolveDshHome2(), ".dsh-custom-env-groups.json");
967
+ }
968
+ function defaultFile() {
969
+ return {
970
+ version: 1,
971
+ groups: [{ id: DEFAULT_GROUP_ID, title: DEFAULT_GROUP_TITLE }],
972
+ membership: {}
973
+ };
974
+ }
975
+ function isRecord(value) {
976
+ return typeof value === "object" && value !== null && !Array.isArray(value);
977
+ }
978
+ function normalizeGroupsFile(raw) {
979
+ if (!isRecord(raw)) return defaultFile();
980
+ const groupsIn = Array.isArray(raw.groups) ? raw.groups : [];
981
+ const groups = [];
982
+ const seen = /* @__PURE__ */ new Set();
983
+ let hasDefault = false;
984
+ for (const item of groupsIn) {
985
+ if (!isRecord(item)) continue;
986
+ const id = typeof item.id === "string" ? item.id.trim() : "";
987
+ const title = typeof item.title === "string" ? item.title.trim() : "";
988
+ if (id === "" || title === "" || seen.has(id)) continue;
989
+ seen.add(id);
990
+ if (id === DEFAULT_GROUP_ID) {
991
+ hasDefault = true;
992
+ groups.unshift({ id: DEFAULT_GROUP_ID, title: DEFAULT_GROUP_TITLE });
993
+ continue;
994
+ }
995
+ groups.push({ id, title });
996
+ }
997
+ if (!hasDefault) groups.unshift({ id: DEFAULT_GROUP_ID, title: DEFAULT_GROUP_TITLE });
998
+ const groupIds = new Set(groups.map((group) => group.id));
999
+ const membership = {};
1000
+ if (isRecord(raw.membership)) {
1001
+ for (const [entryId, groupId] of Object.entries(raw.membership)) {
1002
+ if (typeof groupId !== "string" || entryId.trim() === "" || groupId.trim() === "") continue;
1003
+ membership[entryId] = groupIds.has(groupId) ? groupId : DEFAULT_GROUP_ID;
1004
+ }
1005
+ }
1006
+ return { version: 1, groups, membership };
1007
+ }
1008
+ function readGroupsFile() {
1009
+ const path = groupsFilePath();
1010
+ if (!existsSync8(path)) return defaultFile();
1011
+ try {
1012
+ return normalizeGroupsFile(JSON.parse(readFileSync3(path, "utf8")));
1013
+ } catch {
1014
+ return defaultFile();
1015
+ }
1016
+ }
1017
+ function writeGroupsFile(file) {
1018
+ const path = groupsFilePath();
1019
+ mkdirSync4(dirname6(path), { recursive: true });
1020
+ const normalized = normalizeGroupsFile(file);
1021
+ writeFileSync4(path, `${JSON.stringify(normalized, null, 2)}
1022
+ `, "utf8");
1023
+ }
1024
+ function pruneMembership(file, liveIds) {
1025
+ let changed = false;
1026
+ const membership = {};
1027
+ for (const [entryId, groupId] of Object.entries(file.membership)) {
1028
+ if (!liveIds.has(entryId)) {
1029
+ changed = true;
1030
+ continue;
1031
+ }
1032
+ membership[entryId] = groupId;
1033
+ }
1034
+ return changed ? { ...file, membership } : file;
1035
+ }
1036
+ function resolveGroupId(file, entryId) {
1037
+ const groupId = file.membership[entryId];
1038
+ if (groupId === void 0) return DEFAULT_GROUP_ID;
1039
+ return file.groups.some((group) => group.id === groupId) ? groupId : DEFAULT_GROUP_ID;
1040
+ }
1041
+ function setMembership(file, entryId, groupId) {
1042
+ const nextId = file.groups.some((group) => group.id === groupId) ? groupId : DEFAULT_GROUP_ID;
1043
+ if (file.membership[entryId] === nextId) return file;
1044
+ return { ...file, membership: { ...file.membership, [entryId]: nextId } };
1045
+ }
1046
+ function clearMembership(file, entryId) {
1047
+ if (file.membership[entryId] === void 0) return file;
1048
+ const membership = { ...file.membership };
1049
+ delete membership[entryId];
1050
+ return { ...file, membership };
1051
+ }
1052
+ function clearMembershipMany(file, entryIds) {
1053
+ let next = file;
1054
+ for (const entryId of entryIds) next = clearMembership(next, entryId);
1055
+ return next;
1056
+ }
1057
+ function allocGroupId(existing) {
1058
+ for (; ; ) {
1059
+ const id = `g-${randomBytes(6).toString("hex")}`;
1060
+ if (!existing.has(id)) return id;
1061
+ }
1062
+ }
1063
+ function createGroup(file, title) {
1064
+ const trimmed = title.trim();
1065
+ if (trimmed === "") throw new Error("empty group title");
1066
+ const existing = new Set(file.groups.map((group2) => group2.id));
1067
+ const group = { id: allocGroupId(existing), title: trimmed };
1068
+ return { file: { ...file, groups: [...file.groups, group] }, group };
1069
+ }
1070
+ function renameGroup(file, id, title) {
1071
+ if (id === DEFAULT_GROUP_ID) throw new Error("cannot rename default group");
1072
+ const trimmed = title.trim();
1073
+ if (trimmed === "") throw new Error("empty group title");
1074
+ const index = file.groups.findIndex((group) => group.id === id);
1075
+ if (index < 0) throw new Error(`unknown group "${id}"`);
1076
+ const groups = file.groups.map((group, i) => i === index ? { id, title: trimmed } : group);
1077
+ return { ...file, groups };
1078
+ }
1079
+ function deleteGroupMeta(file, id, moveToDefault) {
1080
+ if (id === DEFAULT_GROUP_ID) throw new Error("cannot delete default group");
1081
+ if (!file.groups.some((group) => group.id === id)) throw new Error(`unknown group "${id}"`);
1082
+ const memberIds = Object.entries(file.membership).filter(([, groupId]) => groupId === id).map(([entryId]) => entryId);
1083
+ const membership = { ...file.membership };
1084
+ for (const entryId of memberIds) {
1085
+ if (moveToDefault) membership[entryId] = DEFAULT_GROUP_ID;
1086
+ else delete membership[entryId];
1087
+ }
1088
+ return {
1089
+ file: {
1090
+ ...file,
1091
+ groups: file.groups.filter((group) => group.id !== id),
1092
+ membership
1093
+ },
1094
+ memberIds
1095
+ };
1096
+ }
1097
+ function hasGroup(file, groupId) {
1098
+ return file.groups.some((group) => group.id === groupId);
1099
+ }
1100
+
954
1101
  // src/types.ts
955
1102
  function classifyEntry(id, name, value, baselineById) {
956
1103
  const base = baselineById.get(id);
@@ -994,8 +1141,8 @@ function removeLives(names) {
994
1141
  function removeLive(name) {
995
1142
  removeLives([name]);
996
1143
  }
997
- var _cliInstall_dec, _cliStatus_dec, _restore_dec, _deleteEntry_dec, _removeVar_dec, _removeVars_dec, _setVar_dec, _setVars_dec, _upsert_dec, _list_dec, _a, _init;
998
- var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [Remote("list")], _upsert_dec = [Remote("upsert")], _setVars_dec = [Remote("setVars")], _setVar_dec = [Remote("setVar")], _removeVars_dec = [Remote("removeVars")], _removeVar_dec = [Remote("removeVar")], _deleteEntry_dec = [Remote("deleteEntry")], _restore_dec = [Remote("restore")], _cliStatus_dec = [Remote("cliStatus")], _cliInstall_dec = [Remote("cliInstall")], _a) {
1144
+ var _cliInstall_dec, _cliStatus_dec, _moveEntry_dec, _deleteGroup_dec, _renameGroup_dec, _createGroup_dec, _restore_dec, _deleteEntry_dec, _removeVar_dec, _removeVars_dec, _setVar_dec, _setVars_dec, _upsert_dec, _list_dec, _a, _init;
1145
+ 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")], _createGroup_dec = [Remote("createGroup")], _renameGroup_dec = [Remote("renameGroup")], _deleteGroup_dec = [Remote("deleteGroup")], _moveEntry_dec = [Remote("moveEntry")], _cliStatus_dec = [Remote("cliStatus")], _cliInstall_dec = [Remote("cliInstall")], _a) {
999
1146
  constructor(ctx) {
1000
1147
  super(ctx, "customEnv");
1001
1148
  __runInitializers(_init, 5, this);
@@ -1040,6 +1187,19 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1040
1187
  }
1041
1188
  return name;
1042
1189
  }
1190
+ loadGroups() {
1191
+ return readGroupsFile();
1192
+ }
1193
+ saveGroups(file) {
1194
+ writeGroupsFile(file);
1195
+ }
1196
+ requireGroup(file, groupId) {
1197
+ const id = groupId.trim() === "" ? DEFAULT_GROUP_ID : groupId;
1198
+ if (!hasGroup(file, id)) {
1199
+ throw new RemoteError("custom-env/invalid-group", `unknown group "${id}"`, { group: id });
1200
+ }
1201
+ return id;
1202
+ }
1043
1203
  /**
1044
1204
  * Write `name=value` replacing `from` in place; sync process.env / OS.
1045
1205
  * Empty values create missing keys only, unless `forceEmpty` (restore).
@@ -1066,6 +1226,7 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1066
1226
  for (const [name, id] of [...this.idByName]) {
1067
1227
  if (!present.has(name)) this.unbind(id);
1068
1228
  }
1229
+ let groupsFile = this.loadGroups();
1069
1230
  const entries = raw.map(({ name, value }) => {
1070
1231
  let id = this.idByName.get(name);
1071
1232
  if (id === void 0) {
@@ -1073,31 +1234,58 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1073
1234
  this.bind(id, name);
1074
1235
  }
1075
1236
  const classified = classifyEntry(id, name, value, this.baselineById);
1076
- return { id, name, value, kind: classified.kind, previous: classified.previous };
1237
+ const groupId = resolveGroupId(groupsFile, id);
1238
+ return { id, name, value, groupId, kind: classified.kind, previous: classified.previous };
1077
1239
  });
1078
- return { path, exists, entries };
1240
+ const liveIds = new Set(entries.map((entry) => entry.id));
1241
+ const pruned = pruneMembership(groupsFile, liveIds);
1242
+ if (pruned !== groupsFile) {
1243
+ this.saveGroups(pruned);
1244
+ groupsFile = pruned;
1245
+ }
1246
+ return { path, exists, groups: groupsFile.groups, entries };
1079
1247
  }
1080
- async upsert(id, name, value) {
1248
+ async upsert(id, name, value, groupId) {
1081
1249
  if (id === "") {
1250
+ let groupsFile = this.loadGroups();
1251
+ const target = this.requireGroup(groupsFile, groupId ?? DEFAULT_GROUP_ID);
1082
1252
  this.commit(name, name, value);
1083
- if (!this.idByName.has(name)) this.bind(this.allocId(), name);
1253
+ let rowId = this.idByName.get(name);
1254
+ if (rowId === void 0) {
1255
+ rowId = this.allocId();
1256
+ this.bind(rowId, name);
1257
+ }
1258
+ groupsFile = setMembership(groupsFile, rowId, target);
1259
+ this.saveGroups(groupsFile);
1084
1260
  return this.list();
1085
1261
  }
1086
1262
  const from = this.requireName(id, name);
1087
1263
  this.commit(from, name, value);
1088
1264
  this.bind(id, name);
1265
+ if (groupId !== void 0) {
1266
+ let groupsFile = this.loadGroups();
1267
+ const target = this.requireGroup(groupsFile, groupId);
1268
+ groupsFile = setMembership(groupsFile, id, target);
1269
+ this.saveGroups(groupsFile);
1270
+ }
1089
1271
  return this.list();
1090
1272
  }
1091
1273
  async setVars(entries) {
1092
1274
  if (entries.length === 0) return;
1275
+ let groupsFile = this.loadGroups();
1093
1276
  const lastByName = /* @__PURE__ */ new Map();
1094
1277
  for (const entry of entries) {
1095
1278
  if (!isWritableEnvName(entry.name)) {
1096
1279
  throw new RemoteError("custom-env/invalid-name", `refused name "${entry.name}"`, { name: entry.name });
1097
1280
  }
1098
- lastByName.set(entry.name, entry.value);
1281
+ if (entry.group !== void 0) {
1282
+ const group = this.requireGroup(groupsFile, entry.group);
1283
+ lastByName.set(entry.name, { value: entry.value, group });
1284
+ } else {
1285
+ lastByName.set(entry.name, { value: entry.value });
1286
+ }
1099
1287
  }
1100
- const normalized = [...lastByName].map(([name, value]) => ({ name, value }));
1288
+ const normalized = [...lastByName].map(([name, { value }]) => ({ name, value }));
1101
1289
  let written;
1102
1290
  try {
1103
1291
  written = writeEnvEntries(normalized);
@@ -1107,14 +1295,29 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1107
1295
  }
1108
1296
  if (written.length === 0) return;
1109
1297
  const live = {};
1298
+ let membershipChanged = false;
1110
1299
  for (const entry of written) {
1111
1300
  live[entry.name] = entry.value;
1112
- if (!this.idByName.has(entry.name)) this.bind(this.allocId(), entry.name);
1301
+ let rowId = this.idByName.get(entry.name);
1302
+ const isNewRow = rowId === void 0;
1303
+ if (rowId === void 0) {
1304
+ rowId = this.allocId();
1305
+ this.bind(rowId, entry.name);
1306
+ }
1307
+ const explicitGroup = lastByName.get(entry.name)?.group;
1308
+ if (explicitGroup !== void 0) {
1309
+ groupsFile = setMembership(groupsFile, rowId, explicitGroup);
1310
+ membershipChanged = true;
1311
+ } else if (isNewRow) {
1312
+ groupsFile = setMembership(groupsFile, rowId, DEFAULT_GROUP_ID);
1313
+ membershipChanged = true;
1314
+ }
1113
1315
  }
1316
+ if (membershipChanged) this.saveGroups(groupsFile);
1114
1317
  writeLive(live);
1115
1318
  }
1116
- async setVar(name, value) {
1117
- await this.setVars([{ name, value }]);
1319
+ async setVar(name, value, group) {
1320
+ await this.setVars([{ name, value, group }]);
1118
1321
  }
1119
1322
  async removeVars(names) {
1120
1323
  if (names.length === 0) return 0;
@@ -1128,6 +1331,11 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1128
1331
  seen.add(name);
1129
1332
  unique.push(name);
1130
1333
  }
1334
+ const dropIds = [];
1335
+ for (const name of unique) {
1336
+ const id = this.idByName.get(name);
1337
+ if (id !== void 0) dropIds.push(id);
1338
+ }
1131
1339
  let removed;
1132
1340
  try {
1133
1341
  removed = removeEnvEntries(unique);
@@ -1140,6 +1348,9 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1140
1348
  const id = this.idByName.get(name);
1141
1349
  if (id !== void 0) this.unbind(id);
1142
1350
  }
1351
+ if (dropIds.length > 0) {
1352
+ this.saveGroups(clearMembershipMany(this.loadGroups(), dropIds));
1353
+ }
1143
1354
  return removed;
1144
1355
  }
1145
1356
  async removeVar(name) {
@@ -1169,6 +1380,66 @@ var CustomEnvController = class extends (_a = TypertRemoteService, _list_dec = [
1169
1380
  this.bind(id, base.name);
1170
1381
  return this.list();
1171
1382
  }
1383
+ async createGroup(title) {
1384
+ try {
1385
+ const { file, group } = createGroup(this.loadGroups(), title);
1386
+ this.saveGroups(file);
1387
+ return group;
1388
+ } catch (error) {
1389
+ throw new RemoteError("custom-env/invalid-group", asMessage(error), { group: title }, { cause: error });
1390
+ }
1391
+ }
1392
+ async renameGroup(id, title) {
1393
+ try {
1394
+ const file = renameGroup(this.loadGroups(), id, title);
1395
+ this.saveGroups(file);
1396
+ const group = file.groups.find((item) => item.id === id);
1397
+ if (group === void 0) {
1398
+ throw new RemoteError("custom-env/invalid-group", `unknown group "${id}"`, { group: id });
1399
+ }
1400
+ return group;
1401
+ } catch (error) {
1402
+ if (error instanceof RemoteError) throw error;
1403
+ throw new RemoteError("custom-env/invalid-group", asMessage(error), { group: id }, { cause: error });
1404
+ }
1405
+ }
1406
+ async deleteGroup(id, options) {
1407
+ let meta;
1408
+ try {
1409
+ meta = deleteGroupMeta(this.loadGroups(), id, options.moveToDefault);
1410
+ } catch (error) {
1411
+ throw new RemoteError("custom-env/invalid-group", asMessage(error), { group: id }, { cause: error });
1412
+ }
1413
+ if (!options.moveToDefault && meta.memberIds.length > 0) {
1414
+ const names = [];
1415
+ for (const entryId of meta.memberIds) {
1416
+ const name = this.nameById.get(entryId);
1417
+ if (name !== void 0) names.push(name);
1418
+ }
1419
+ if (names.length > 0) {
1420
+ try {
1421
+ removeEnvEntries(names);
1422
+ } catch (error) {
1423
+ throw new RemoteError("custom-env/invalid-name", asMessage(error), { name: names[0] }, { cause: error });
1424
+ }
1425
+ removeLives(names);
1426
+ for (const name of names) {
1427
+ const entryId = this.idByName.get(name);
1428
+ if (entryId !== void 0) this.unbind(entryId);
1429
+ }
1430
+ }
1431
+ }
1432
+ this.saveGroups(meta.file);
1433
+ return this.list();
1434
+ }
1435
+ async moveEntry(id, groupId) {
1436
+ this.requireName(id, id);
1437
+ let groupsFile = this.loadGroups();
1438
+ const target = this.requireGroup(groupsFile, groupId);
1439
+ groupsFile = setMembership(groupsFile, id, target);
1440
+ this.saveGroups(groupsFile);
1441
+ return this.list();
1442
+ }
1172
1443
  async cliStatus() {
1173
1444
  return cliStatus();
1174
1445
  }
@@ -1190,10 +1461,16 @@ __decorateElement(_init, 1, "removeVars", _removeVars_dec, CustomEnvController);
1190
1461
  __decorateElement(_init, 1, "removeVar", _removeVar_dec, CustomEnvController);
1191
1462
  __decorateElement(_init, 1, "deleteEntry", _deleteEntry_dec, CustomEnvController);
1192
1463
  __decorateElement(_init, 1, "restore", _restore_dec, CustomEnvController);
1464
+ __decorateElement(_init, 1, "createGroup", _createGroup_dec, CustomEnvController);
1465
+ __decorateElement(_init, 1, "renameGroup", _renameGroup_dec, CustomEnvController);
1466
+ __decorateElement(_init, 1, "deleteGroup", _deleteGroup_dec, CustomEnvController);
1467
+ __decorateElement(_init, 1, "moveEntry", _moveEntry_dec, CustomEnvController);
1193
1468
  __decorateElement(_init, 1, "cliStatus", _cliStatus_dec, CustomEnvController);
1194
1469
  __decorateElement(_init, 1, "cliInstall", _cliInstall_dec, CustomEnvController);
1195
1470
  __decoratorMetadata(_init, CustomEnvController);
1196
1471
  export {
1472
+ DEFAULT_GROUP_ID,
1473
+ DEFAULT_GROUP_TITLE,
1197
1474
  classifyEntry,
1198
1475
  CustomEnvController as default,
1199
1476
  envFilePath