esedre 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/dist/esedre.mjs CHANGED
@@ -9,7 +9,7 @@ import fs3 from "node:fs";
9
9
  import path3 from "node:path";
10
10
 
11
11
  // src/types.ts
12
- var CURRENT_ESEDRE_VERSION = "0.1.6";
12
+ var CURRENT_ESEDRE_VERSION = "0.1.8";
13
13
  var EsedreConflictError = class extends Error {
14
14
  constructor(ticketId, currentHash, lastHash) {
15
15
  super(
@@ -30,8 +30,8 @@ import fs from "node:fs";
30
30
  import path from "node:path";
31
31
  import os from "node:os";
32
32
  var DEFAULT_ESEDRE_PORT = 5674;
33
- var MAX_PROJECT_CODE_LENGTH = 7;
34
- var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,7}$/;
33
+ var MAX_PROJECT_CODE_LENGTH = 8;
34
+ var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,8}$/;
35
35
  var MAX_PROJECT_NAME_LENGTH = 48;
36
36
  function resolvePorts(config, overrides) {
37
37
  const globalConfig = readGlobalConfig();
@@ -244,9 +244,11 @@ function removeLocationFromGlobalConfig(identifier) {
244
244
  if (current.dataDir) {
245
245
  const currentDataDirs = Array.isArray(current.dataDir) ? current.dataDir : [current.dataDir];
246
246
  const initialLen = currentDataDirs.length;
247
- const remaining = currentDataDirs.filter(
248
- (d) => path.resolve(expandHome(d)).toLowerCase() !== path.resolve(expandHome(trimmed)).toLowerCase()
249
- );
247
+ const remaining = currentDataDirs.filter((d) => {
248
+ const matchPath = path.resolve(expandHome(d)).toLowerCase() === path.resolve(expandHome(trimmed)).toLowerCase();
249
+ const matchBasename = path.basename(d).toLowerCase() === trimmed.toLowerCase();
250
+ return !matchPath && !matchBasename;
251
+ });
250
252
  if (remaining.length !== initialLen) {
251
253
  current.dataDir = remaining.length === 0 ? void 0 : remaining.length === 1 ? remaining[0] : remaining;
252
254
  writeGlobalConfig(current);
@@ -537,12 +539,12 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
537
539
  }
538
540
 
539
541
  // src/storage/filesystem.ts
540
- function isProjectMatch(code, slug, filter) {
542
+ function isProjectMatch(code, filter) {
543
+ if (!code || !filter) return false;
541
544
  const f = filter.trim().toLowerCase();
542
545
  const c = code.trim().toLowerCase();
543
- const s = slug.trim().toLowerCase();
544
- if (c === f || s === f) return true;
545
- if ((f === "prof" || f === "core" || f === "pasrc") && (c === "prof" || c === "core")) return true;
546
+ if (c === f) return true;
547
+ if ((f === "profe" || f === "prof" || f === "core" || f === "pasrc") && (c === "profe" || c === "prof" || c === "core")) return true;
546
548
  if ((f === "esedre" || f === "ese" || f === "docs") && (c === "esedre" || c === "docs")) return true;
547
549
  if ((f === "alce" || f === "web") && (c === "alce" || c === "web")) return true;
548
550
  return false;
@@ -570,17 +572,51 @@ function writeSafeFile(filePath, content) {
570
572
  var FilesystemStorageAdapter = class {
571
573
  workspaceRoot;
572
574
  config;
575
+ isConfigExplicit = false;
576
+ lastConfigCheck = 0;
577
+ configTtlMs = 3e3;
578
+ duplicateProjectWarnings = [];
579
+ getDuplicateProjectWarnings() {
580
+ this.resolveProjectLocations();
581
+ return [...this.duplicateProjectWarnings];
582
+ }
573
583
  constructor(workspaceRoot, config) {
574
584
  this.workspaceRoot = workspaceRoot || this.resolveWorkspaceRoot();
575
585
  if (config) {
576
586
  this.config = config;
587
+ this.isConfigExplicit = true;
588
+ this.lastConfigCheck = Date.now();
577
589
  } else {
578
- try {
579
- const discovered = findEsedreConfig(this.workspaceRoot);
580
- this.config = discovered.config || {};
581
- } catch {
582
- this.config = {};
590
+ this.config = {};
591
+ this.refreshConfig();
592
+ }
593
+ }
594
+ refreshConfig() {
595
+ try {
596
+ const discovered = findEsedreConfig(this.workspaceRoot);
597
+ this.config = discovered.config || {};
598
+ } catch {
599
+ this.config = {};
600
+ }
601
+ this.lastConfigCheck = Date.now();
602
+ }
603
+ ensureFreshConfig() {
604
+ if (this.isConfigExplicit && this.config.allowedProjects?.includes("*")) {
605
+ if (Date.now() - this.lastConfigCheck > this.configTtlMs) {
606
+ try {
607
+ const discovered = findEsedreConfig(this.workspaceRoot, { fallbackToGlobal: true });
608
+ this.config = {
609
+ ...discovered.config,
610
+ allowedProjects: ["*"]
611
+ };
612
+ } catch {
613
+ }
614
+ this.lastConfigCheck = Date.now();
583
615
  }
616
+ return;
617
+ }
618
+ if (!this.isConfigExplicit && Date.now() - this.lastConfigCheck > this.configTtlMs) {
619
+ this.refreshConfig();
584
620
  }
585
621
  }
586
622
  resolveWorkspaceRoot() {
@@ -599,8 +635,11 @@ var FilesystemStorageAdapter = class {
599
635
  return process.cwd();
600
636
  }
601
637
  resolveProjectLocations() {
638
+ this.ensureFreshConfig();
602
639
  const locations = [];
603
640
  const seenCodes = /* @__PURE__ */ new Set();
641
+ const codeToSource = /* @__PURE__ */ new Map();
642
+ this.duplicateProjectWarnings = [];
604
643
  const rawDataDirs = Array.isArray(this.config.dataDir) ? [...this.config.dataDir] : this.config.dataDir ? [this.config.dataDir] : [];
605
644
  if (rawDataDirs.length === 0) {
606
645
  const candidateHubDirs = [
@@ -640,9 +679,6 @@ var FilesystemStorageAdapter = class {
640
679
  if (fs3.existsSync(pJsonPath)) {
641
680
  try {
642
681
  desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
643
- if (desc && !desc.slug) {
644
- desc.slug = (desc.code || ent.name).toLowerCase();
645
- }
646
682
  } catch {
647
683
  }
648
684
  }
@@ -654,7 +690,7 @@ var FilesystemStorageAdapter = class {
654
690
  try {
655
691
  const hubProjs = JSON.parse(fs3.readFileSync(f, "utf-8"));
656
692
  desc = hubProjs.find(
657
- (p) => p.code.toLowerCase() === ent.name.toLowerCase() || p.slug.toLowerCase() === ent.name.toLowerCase()
693
+ (p) => p.code.toLowerCase() === ent.name.toLowerCase()
658
694
  );
659
695
  if (desc) break;
660
696
  } catch {
@@ -666,20 +702,32 @@ var FilesystemStorageAdapter = class {
666
702
  desc = {
667
703
  id: locations.length + 1,
668
704
  code: ent.name,
669
- slug: ent.name.toLowerCase(),
670
705
  name: ent.name,
671
706
  description: `${ent.name} project`
672
707
  };
673
708
  }
674
709
  const codeKey = desc.code.toLowerCase();
710
+ const sourceName = resolvedHub;
675
711
  if (!seenCodes.has(codeKey)) {
676
712
  seenCodes.add(codeKey);
713
+ codeToSource.set(codeKey, sourceName);
677
714
  locations.push({
678
715
  project: desc,
679
716
  ticketsDir: projectTicketsDir,
680
717
  sourceType: "hub",
681
718
  hubDir: resolvedHub
682
719
  });
720
+ } else {
721
+ const firstSource = codeToSource.get(codeKey) || "configured location";
722
+ if (firstSource.toLowerCase() !== sourceName.toLowerCase()) {
723
+ if (!this.duplicateProjectWarnings.some((w) => w.code.toLowerCase() === codeKey && w.duplicateHub.toLowerCase() === sourceName.toLowerCase())) {
724
+ this.duplicateProjectWarnings.push({
725
+ code: desc.code,
726
+ firstHub: firstSource,
727
+ duplicateHub: sourceName
728
+ });
729
+ }
730
+ }
683
731
  }
684
732
  }
685
733
  } else {
@@ -705,7 +753,6 @@ var FilesystemStorageAdapter = class {
705
753
  descs = [{
706
754
  id: 1,
707
755
  code: pCode,
708
- slug: pCode.toLowerCase(),
709
756
  name: pCode,
710
757
  description: `${pCode} project`
711
758
  }];
@@ -713,8 +760,10 @@ var FilesystemStorageAdapter = class {
713
760
  const isShared = descs.length > 1;
714
761
  for (const desc of descs) {
715
762
  const codeKey = desc.code.toLowerCase();
763
+ const sourceName = resolvedHub;
716
764
  if (!seenCodes.has(codeKey)) {
717
765
  seenCodes.add(codeKey);
766
+ codeToSource.set(codeKey, sourceName);
718
767
  locations.push({
719
768
  project: desc,
720
769
  ticketsDir: actualTicketsDir,
@@ -722,6 +771,17 @@ var FilesystemStorageAdapter = class {
722
771
  hubDir: resolvedHub,
723
772
  isSharedDir: isShared
724
773
  });
774
+ } else {
775
+ const firstSource = codeToSource.get(codeKey) || "configured location";
776
+ if (firstSource.toLowerCase() !== sourceName.toLowerCase()) {
777
+ if (!this.duplicateProjectWarnings.some((w) => w.code.toLowerCase() === codeKey && w.duplicateHub.toLowerCase() === sourceName.toLowerCase())) {
778
+ this.duplicateProjectWarnings.push({
779
+ code: desc.code,
780
+ firstHub: firstSource,
781
+ duplicateHub: sourceName
782
+ });
783
+ }
784
+ }
725
785
  }
726
786
  }
727
787
  }
@@ -747,9 +807,6 @@ var FilesystemStorageAdapter = class {
747
807
  if (fs3.existsSync(cand)) {
748
808
  try {
749
809
  desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
750
- if (desc && !desc.slug) {
751
- desc.slug = (desc.code || code).toLowerCase();
752
- }
753
810
  break;
754
811
  } catch {
755
812
  }
@@ -759,19 +816,31 @@ var FilesystemStorageAdapter = class {
759
816
  desc = {
760
817
  id: locations.length + 1,
761
818
  code,
762
- slug: code.toLowerCase(),
763
819
  name: code,
764
820
  description: `${code} project`
765
821
  };
766
822
  }
767
823
  const codeKey = desc.code.toLowerCase();
824
+ const sourceName = projectDir;
768
825
  if (!seenCodes.has(codeKey)) {
769
826
  seenCodes.add(codeKey);
827
+ codeToSource.set(codeKey, sourceName);
770
828
  locations.push({
771
829
  project: desc,
772
830
  ticketsDir: actualTicketsDir,
773
831
  sourceType: "federated"
774
832
  });
833
+ } else {
834
+ const firstSource = codeToSource.get(codeKey) || "configured location";
835
+ if (firstSource.toLowerCase() !== sourceName.toLowerCase()) {
836
+ if (!this.duplicateProjectWarnings.some((w) => w.code.toLowerCase() === codeKey && w.duplicateHub.toLowerCase() === sourceName.toLowerCase())) {
837
+ this.duplicateProjectWarnings.push({
838
+ code: desc.code,
839
+ firstHub: firstSource,
840
+ duplicateHub: sourceName
841
+ });
842
+ }
843
+ }
775
844
  }
776
845
  }
777
846
  }
@@ -814,7 +883,6 @@ var FilesystemStorageAdapter = class {
814
883
  descs = [{
815
884
  id: 1,
816
885
  code: this.config.projectCode,
817
- slug: this.config.projectCode.toLowerCase(),
818
886
  name: pName,
819
887
  description: `${pName} project`
820
888
  }];
@@ -846,7 +914,7 @@ var FilesystemStorageAdapter = class {
846
914
  const cleanCode = rawCode;
847
915
  const cleanName = input.name?.trim() || cleanCode;
848
916
  const cleanDesc = input.description?.trim() || `${cleanName} project`;
849
- const slug = cleanCode.toLowerCase();
917
+ const cleanLower = cleanCode.toLowerCase();
850
918
  const colors2 = input.colors || {
851
919
  badge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300",
852
920
  dot: "bg-cyan-400",
@@ -872,17 +940,91 @@ var FilesystemStorageAdapter = class {
872
940
  }
873
941
  }
874
942
  }
875
- let targetHub = null;
943
+ const existingHubs = [];
876
944
  for (const dDir of rawDataDirs) {
877
945
  const resolved = path3.resolve(this.workspaceRoot, expandHome(dDir));
878
- if (fs3.existsSync(resolved)) {
879
- targetHub = resolved;
880
- break;
946
+ if (fs3.existsSync(resolved) && !existingHubs.includes(resolved)) {
947
+ existingHubs.push(resolved);
948
+ }
949
+ }
950
+ let targetHub = null;
951
+ if (input.hub) {
952
+ const targetQuery = input.hub.trim();
953
+ const resolvedQuery = path3.resolve(this.workspaceRoot, expandHome(targetQuery)).toLowerCase();
954
+ const isPathLike = targetQuery.includes("/") || targetQuery.includes("\\");
955
+ const basenameMatches = existingHubs.filter(
956
+ (h) => path3.basename(h).toLowerCase() === targetQuery.toLowerCase()
957
+ );
958
+ if (!isPathLike && basenameMatches.length > 1) {
959
+ const list = basenameMatches.map((h) => ` \u2022 ${h}`).join("\n");
960
+ throw new Error(
961
+ `Hub name collision: Multiple configured data hubs share basename "${targetQuery}". Please specify by unambiguous path:
962
+ ${list}`
963
+ );
964
+ } else if (basenameMatches.length === 1 && !isPathLike) {
965
+ targetHub = basenameMatches[0];
966
+ } else {
967
+ const exactPathMatch = existingHubs.find((h) => h.toLowerCase() === resolvedQuery);
968
+ if (exactPathMatch) {
969
+ targetHub = exactPathMatch;
970
+ } else if (basenameMatches.length === 1) {
971
+ targetHub = basenameMatches[0];
972
+ } else {
973
+ const directHubPath = path3.resolve(this.workspaceRoot, expandHome(targetQuery));
974
+ if (fs3.existsSync(directHubPath) && (fs3.existsSync(path3.join(directHubPath, "projects")) || fs3.existsSync(path3.join(directHubPath, "projects.json")))) {
975
+ targetHub = directHubPath;
976
+ } else {
977
+ const available = existingHubs.map((h) => `${path3.basename(h)} (${h})`).join(", ");
978
+ throw new Error(
979
+ `Hub "${targetQuery}" not found. Configured data hubs: ${available || "(none)"}`
980
+ );
981
+ }
982
+ }
983
+ }
984
+ } else {
985
+ if (existingHubs.length === 1) {
986
+ targetHub = existingHubs[0];
987
+ } else if (existingHubs.length > 1) {
988
+ const basenames = existingHubs.map((h) => path3.basename(h));
989
+ const hasCollision = new Set(basenames.map((b) => b.toLowerCase())).size < basenames.length;
990
+ const formattedList = existingHubs.map((h) => ` \u2022 ${path3.basename(h)} (${h})`).join("\n");
991
+ throw new Error(
992
+ `Multiple data hubs configured. Please specify --hub <name${hasCollision ? "|path" : ""}>:
993
+ ${formattedList}`
994
+ );
881
995
  }
882
996
  }
883
997
  if (targetHub) {
998
+ const normalizedTargetHub = path3.resolve(targetHub).toLowerCase();
999
+ const existingLocations = this.resolveProjectLocations();
1000
+ const existingConflict = existingLocations.find((l) => {
1001
+ if (l.project.code.toLowerCase() !== cleanLower) return false;
1002
+ if (l.sourceType === "hub" && l.hubDir) {
1003
+ return path3.resolve(l.hubDir).toLowerCase() !== normalizedTargetHub;
1004
+ }
1005
+ return false;
1006
+ });
1007
+ if (existingConflict && existingConflict.hubDir) {
1008
+ const conflictLoc = path3.basename(existingConflict.hubDir);
1009
+ throw new Error(
1010
+ `Project code "${cleanCode}" already exists in another data hub (${conflictLoc}). Duplicate project codes across hubs are not supported.`
1011
+ );
1012
+ }
884
1013
  const hubProjectsDir = path3.join(targetHub, "projects");
885
- const projectDir = path3.join(hubProjectsDir, cleanCode);
1014
+ let resolvedDirName = cleanCode;
1015
+ if (fs3.existsSync(hubProjectsDir)) {
1016
+ try {
1017
+ const entries = fs3.readdirSync(hubProjectsDir, { withFileTypes: true });
1018
+ const matchedDir = entries.find(
1019
+ (e) => e.isDirectory() && e.name.toLowerCase() === cleanLower
1020
+ );
1021
+ if (matchedDir) {
1022
+ resolvedDirName = matchedDir.name;
1023
+ }
1024
+ } catch {
1025
+ }
1026
+ }
1027
+ const projectDir = path3.join(hubProjectsDir, resolvedDirName);
886
1028
  const ticketsDir2 = path3.join(projectDir, "tickets");
887
1029
  if (!fs3.existsSync(ticketsDir2)) {
888
1030
  fs3.mkdirSync(ticketsDir2, { recursive: true });
@@ -895,13 +1037,13 @@ var FilesystemStorageAdapter = class {
895
1037
  } catch {
896
1038
  }
897
1039
  }
1040
+ const resolvedCode2 = existingPJson2?.code || resolvedDirName || cleanCode;
898
1041
  const existingProjects = await this.getProjects();
899
1042
  const maxId = existingProjects.reduce((max, p) => Math.max(max, p.id || 0), 0);
900
1043
  const id2 = existingPJson2?.id || maxId + 1;
901
1044
  const projectDesc2 = {
902
1045
  id: id2,
903
- code: cleanCode,
904
- slug,
1046
+ code: resolvedCode2,
905
1047
  name: cleanName,
906
1048
  description: cleanDesc,
907
1049
  colors: colors2
@@ -911,7 +1053,7 @@ var FilesystemStorageAdapter = class {
911
1053
  if (fs3.existsSync(hubProjectsFile)) {
912
1054
  try {
913
1055
  const arr = JSON.parse(fs3.readFileSync(hubProjectsFile, "utf-8"));
914
- const idx = arr.findIndex((p) => p.code.toLowerCase() === slug);
1056
+ const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
915
1057
  if (idx >= 0) {
916
1058
  arr[idx] = projectDesc2;
917
1059
  } else {
@@ -936,11 +1078,11 @@ var FilesystemStorageAdapter = class {
936
1078
  } catch {
937
1079
  }
938
1080
  }
1081
+ const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() === cleanLower ? existingPJson.code : cleanCode;
939
1082
  const id = existingPJson?.id || 1;
940
1083
  const projectDesc = {
941
1084
  id,
942
- code: cleanCode,
943
- slug,
1085
+ code: resolvedCode,
944
1086
  name: cleanName,
945
1087
  description: cleanDesc,
946
1088
  colors: colors2
@@ -950,7 +1092,7 @@ var FilesystemStorageAdapter = class {
950
1092
  if (fs3.existsSync(projectsJsonPath)) {
951
1093
  try {
952
1094
  const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
953
- const idx = arr.findIndex((p) => p.code.toLowerCase() === slug);
1095
+ const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
954
1096
  if (idx >= 0) {
955
1097
  arr[idx] = projectDesc;
956
1098
  } else {
@@ -966,11 +1108,11 @@ var FilesystemStorageAdapter = class {
966
1108
  const locations = this.resolveProjectLocations();
967
1109
  if (locations.length === 0) return null;
968
1110
  const strId = String(id).trim();
969
- const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,7})[-_:](\d+)$/i);
1111
+ const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,8})[-_:](\d+)$/i);
970
1112
  if (prefixMatch) {
971
1113
  const code = prefixMatch[1].toLowerCase();
972
1114
  const num2 = parseInt(prefixMatch[2], 10);
973
- const loc = locations.find((l) => isProjectMatch(l.project.code, l.project.slug, code));
1115
+ const loc = locations.find((l) => isProjectMatch(l.project.code, code));
974
1116
  if (!loc) return null;
975
1117
  const ticketDir = path3.join(loc.ticketsDir, String(num2));
976
1118
  const metaPath = path3.join(ticketDir, "meta.json");
@@ -982,7 +1124,7 @@ var FilesystemStorageAdapter = class {
982
1124
  meta.type = meta.type || meta.category || "Feature";
983
1125
  meta.category = meta.type;
984
1126
  const mProj = (meta.project || "").toLowerCase();
985
- if (mProj && mProj !== code && mProj !== loc.project.slug.toLowerCase()) {
1127
+ if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
986
1128
  return null;
987
1129
  }
988
1130
  } catch {
@@ -1007,7 +1149,7 @@ var FilesystemStorageAdapter = class {
1007
1149
  const metaRaw = fs3.readFileSync(metaPath, "utf-8");
1008
1150
  const meta = JSON.parse(metaRaw);
1009
1151
  const matchedLoc = locations.find(
1010
- (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && (l.project.code.toLowerCase() === meta.project.toLowerCase() || l.project.slug.toLowerCase() === meta.project.toLowerCase())
1152
+ (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
1011
1153
  ) || loc;
1012
1154
  matches.push({ loc: matchedLoc, ticketDir, id: num });
1013
1155
  } catch {
@@ -1020,7 +1162,7 @@ var FilesystemStorageAdapter = class {
1020
1162
  if (this.config.projectCode) {
1021
1163
  const prefLower = this.config.projectCode.toLowerCase();
1022
1164
  const preferredMatch = matches.find(
1023
- (m) => m.loc.project.code.toLowerCase() === prefLower || m.loc.project.slug.toLowerCase() === prefLower
1165
+ (m) => m.loc.project.code.toLowerCase() === prefLower
1024
1166
  );
1025
1167
  if (preferredMatch) return preferredMatch;
1026
1168
  }
@@ -1042,7 +1184,7 @@ var FilesystemStorageAdapter = class {
1042
1184
  for (const loc of locations) {
1043
1185
  if (filter?.project && filter.project !== "all") {
1044
1186
  const pLower = filter.project.toLowerCase();
1045
- if (!isProjectMatch(loc.project.code, loc.project.slug, filter.project)) {
1187
+ if (!isProjectMatch(loc.project.code, filter.project)) {
1046
1188
  continue;
1047
1189
  }
1048
1190
  }
@@ -1074,7 +1216,7 @@ var FilesystemStorageAdapter = class {
1074
1216
  let effectiveLoc = loc;
1075
1217
  if (loc.isSharedDir) {
1076
1218
  const found = locations.find(
1077
- (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && (l.project.code.toLowerCase() === meta.project.toLowerCase() || l.project.slug.toLowerCase() === meta.project.toLowerCase())
1219
+ (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
1078
1220
  );
1079
1221
  if (found) effectiveLoc = found;
1080
1222
  }
@@ -1082,7 +1224,7 @@ var FilesystemStorageAdapter = class {
1082
1224
  meta.projectId = effectiveLoc.project.id;
1083
1225
  if (filter?.project && filter.project !== "all") {
1084
1226
  const pLower = filter.project.toLowerCase();
1085
- if (!isProjectMatch(meta.project, effectiveLoc.project.slug, filter.project)) {
1227
+ if (!isProjectMatch(meta.project, filter.project)) {
1086
1228
  continue;
1087
1229
  }
1088
1230
  }
@@ -1249,7 +1391,7 @@ var FilesystemStorageAdapter = class {
1249
1391
  if (requestedIdentifier) {
1250
1392
  const lower = String(requestedIdentifier).trim().toLowerCase();
1251
1393
  targetLoc = locations.find(
1252
- (l) => l.project.code?.toLowerCase() === lower || l.project.slug?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
1394
+ (l) => l.project.code?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
1253
1395
  );
1254
1396
  if (!targetLoc) {
1255
1397
  throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
@@ -1333,6 +1475,7 @@ ${input.summary || "Summary to be defined."}
1333
1475
  ...existing.meta,
1334
1476
  ...updates,
1335
1477
  id: locInfo.id,
1478
+ project: locInfo.loc.project.code,
1336
1479
  updatedAt: now,
1337
1480
  completedAt,
1338
1481
  revision
@@ -1433,6 +1576,9 @@ var SecurityFilter = class {
1433
1576
  }
1434
1577
  target;
1435
1578
  config;
1579
+ getDuplicateProjectWarnings() {
1580
+ return this.target.getDuplicateProjectWarnings ? this.target.getDuplicateProjectWarnings() : [];
1581
+ }
1436
1582
  async getProjects() {
1437
1583
  const projs = await this.target.getProjects();
1438
1584
  if (!this.config.allowedProjects || this.config.allowedProjects.length === 0) {
@@ -2451,11 +2597,13 @@ async function startDaemon(options = {}) {
2451
2597
  if (!pid) {
2452
2598
  throw new Error("Failed to spawn Esedre daemon background process.");
2453
2599
  }
2600
+ const timeoutMs = process.platform === "win32" ? 1e4 : 5e3;
2601
+ const pingTimeout = process.platform === "win32" ? 500 : 300;
2454
2602
  const startTime = Date.now();
2455
2603
  let ready = false;
2456
- while (Date.now() - startTime < 3500) {
2457
- await new Promise((r) => setTimeout(r, 100));
2458
- const ping = await pingDaemon(port, 150);
2604
+ while (Date.now() - startTime < timeoutMs) {
2605
+ await new Promise((r) => setTimeout(r, 150));
2606
+ const ping = await pingDaemon(port, pingTimeout);
2459
2607
  if (ping.responding && ping.isEsedre) {
2460
2608
  ready = true;
2461
2609
  break;
@@ -3056,7 +3204,10 @@ function configureWorkspace(targetDir, options = {}) {
3056
3204
  }
3057
3205
  }
3058
3206
  let config;
3059
- const projectCode = options.projectCode || existingRaw?.projectCode;
3207
+ let projectCode = options.projectCode || existingRaw?.projectCode;
3208
+ if (existingRaw?.projectCode && options.projectCode && existingRaw.projectCode.toLowerCase() === options.projectCode.toLowerCase()) {
3209
+ projectCode = existingRaw.projectCode;
3210
+ }
3060
3211
  const projectName = options.projectName || existingRaw?.projectName || (projectCode ? projectCode : void 0);
3061
3212
  const dataDir = options.dataDir !== void 0 ? options.dataDir : existingRaw?.dataDir;
3062
3213
  if (existingRaw) {
@@ -3097,7 +3248,6 @@ function configureWorkspace(targetDir, options = {}) {
3097
3248
  const projDesc = {
3098
3249
  id: 1,
3099
3250
  code: projectCode.toUpperCase(),
3100
- slug: projectCode.toLowerCase(),
3101
3251
  name: projectName || projectCode.toUpperCase(),
3102
3252
  description: `${projectName || projectCode.toUpperCase()} project`,
3103
3253
  colors: {
@@ -3270,16 +3420,30 @@ function sendJson(res, status, data) {
3270
3420
  "Content-Type": "application/json; charset=utf-8",
3271
3421
  "Access-Control-Allow-Origin": "*",
3272
3422
  "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
3273
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
3423
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
3274
3424
  });
3275
3425
  res.end(JSON.stringify(data));
3276
3426
  }
3277
- var cachedAllData = null;
3278
- var cachedAllTimestamp = 0;
3427
+ var cachedAllData = /* @__PURE__ */ new Map();
3279
3428
  var CACHE_TTL_MS = 5e3;
3280
3429
  function invalidateApiCache() {
3281
- cachedAllData = null;
3282
- cachedAllTimestamp = 0;
3430
+ cachedAllData.clear();
3431
+ }
3432
+ function getRequestStorage(baseStorage, req, url) {
3433
+ const headerAllowed = req.headers["x-esedre-allowed-projects"];
3434
+ const queryAllowed = url.searchParams.get("allowedProjects");
3435
+ const rawAllowed = (typeof headerAllowed === "string" ? headerAllowed : Array.isArray(headerAllowed) ? headerAllowed[0] : null) || queryAllowed;
3436
+ if (rawAllowed && typeof rawAllowed === "string") {
3437
+ const list = rawAllowed.split(",").map((s) => s.trim()).filter(Boolean);
3438
+ if (list.length > 0 && !list.includes("*")) {
3439
+ const sortedKey = [...list].sort().join(",");
3440
+ return {
3441
+ storage: new SecurityFilter(baseStorage, { allowedProjects: list }),
3442
+ cacheKey: sortedKey
3443
+ };
3444
+ }
3445
+ }
3446
+ return { storage: baseStorage, cacheKey: "*" };
3283
3447
  }
3284
3448
  function createApiHandler(storage, workspaceRoot) {
3285
3449
  return async (req, res) => {
@@ -3305,6 +3469,7 @@ function createApiHandler(storage, workspaceRoot) {
3305
3469
  if (pathname.startsWith("/esedre/api")) {
3306
3470
  pathname = pathname.slice("/esedre".length);
3307
3471
  }
3472
+ const { storage: reqStorage, cacheKey } = getRequestStorage(storage, req, url);
3308
3473
  try {
3309
3474
  if (req.method === "GET") {
3310
3475
  if (pathname === "/api/ping") {
@@ -3312,7 +3477,7 @@ function createApiHandler(storage, workspaceRoot) {
3312
3477
  return true;
3313
3478
  }
3314
3479
  if (pathname === "/api/planning/projects") {
3315
- const projects = await storage.getProjects();
3480
+ const projects = await reqStorage.getProjects();
3316
3481
  sendJson(res, 200, projects);
3317
3482
  return true;
3318
3483
  }
@@ -3322,7 +3487,7 @@ function createApiHandler(storage, workspaceRoot) {
3322
3487
  const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
3323
3488
  const category = type;
3324
3489
  const search = url.searchParams.get("search") || void 0;
3325
- const tickets = await storage.listTickets({
3490
+ const tickets = await reqStorage.listTickets({
3326
3491
  project: project === "all" ? void 0 : project,
3327
3492
  status,
3328
3493
  type,
@@ -3334,11 +3499,11 @@ function createApiHandler(storage, workspaceRoot) {
3334
3499
  }
3335
3500
  if (pathname.startsWith("/api/planning/ticket/")) {
3336
3501
  const idStr = pathname.slice("/api/planning/ticket/".length);
3337
- if (!idStr || !/^([a-zA-Z0-9]{1,7}-)?\d+$/.test(idStr)) {
3502
+ if (!idStr || !/^([a-zA-Z0-9]{1,8}-)?\d+$/.test(idStr)) {
3338
3503
  sendJson(res, 400, { error: "Invalid ticket ID" });
3339
3504
  return true;
3340
3505
  }
3341
- const ticket = await storage.getTicket(idStr);
3506
+ const ticket = await reqStorage.getTicket(idStr);
3342
3507
  if (!ticket) {
3343
3508
  sendJson(res, 400, { error: "Ticket not found" });
3344
3509
  return true;
@@ -3348,11 +3513,12 @@ function createApiHandler(storage, workspaceRoot) {
3348
3513
  }
3349
3514
  if (pathname === "/api/planning/all") {
3350
3515
  const now = Date.now();
3351
- if (cachedAllData && now - cachedAllTimestamp < CACHE_TTL_MS) {
3352
- sendJson(res, 200, cachedAllData);
3516
+ const cached = cachedAllData.get(cacheKey);
3517
+ if (cached && now - cached.timestamp < CACHE_TTL_MS) {
3518
+ sendJson(res, 200, cached.data);
3353
3519
  return true;
3354
3520
  }
3355
- const tickets = await storage.listTickets({});
3521
+ const tickets = await reqStorage.listTickets({});
3356
3522
  const metasMap = {};
3357
3523
  const detailsMap = {};
3358
3524
  const plansMap = {};
@@ -3382,7 +3548,7 @@ function createApiHandler(storage, workspaceRoot) {
3382
3548
  }
3383
3549
  }
3384
3550
  }
3385
- const projects = await storage.getProjects();
3551
+ const projects = await reqStorage.getProjects();
3386
3552
  const responsePayload = {
3387
3553
  success: true,
3388
3554
  projects,
@@ -3395,13 +3561,12 @@ function createApiHandler(storage, workspaceRoot) {
3395
3561
  planHistory: {},
3396
3562
  ticketHistory: {}
3397
3563
  };
3398
- cachedAllData = responsePayload;
3399
- cachedAllTimestamp = now;
3564
+ cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
3400
3565
  sendJson(res, 200, responsePayload);
3401
3566
  return true;
3402
3567
  }
3403
3568
  if (pathname === "/api/planning/metas") {
3404
- const tickets = await storage.listTickets({});
3569
+ const tickets = await reqStorage.listTickets({});
3405
3570
  const metasMap = {};
3406
3571
  for (const t of tickets) {
3407
3572
  metasMap[String(t.meta.id)] = t.meta;
@@ -3410,7 +3575,7 @@ function createApiHandler(storage, workspaceRoot) {
3410
3575
  return true;
3411
3576
  }
3412
3577
  if (pathname === "/api/planning/details") {
3413
- const tickets = await storage.listTickets({});
3578
+ const tickets = await reqStorage.listTickets({});
3414
3579
  const detailsMap = {};
3415
3580
  for (const t of tickets) {
3416
3581
  if (t.detail?.raw) {
@@ -3421,7 +3586,7 @@ function createApiHandler(storage, workspaceRoot) {
3421
3586
  return true;
3422
3587
  }
3423
3588
  if (pathname === "/api/planning/plans") {
3424
- const tickets = await storage.listTickets({});
3589
+ const tickets = await reqStorage.listTickets({});
3425
3590
  const plansMap = {};
3426
3591
  for (const t of tickets) {
3427
3592
  if (t.planMarkdown) {
@@ -3432,7 +3597,7 @@ function createApiHandler(storage, workspaceRoot) {
3432
3597
  return true;
3433
3598
  }
3434
3599
  if (pathname === "/api/planning/comments") {
3435
- const tickets = await storage.listTickets({});
3600
+ const tickets = await reqStorage.listTickets({});
3436
3601
  const commentsMap = {};
3437
3602
  for (const t of tickets) {
3438
3603
  commentsMap[String(t.meta.id)] = t.comments || [];
@@ -3462,7 +3627,7 @@ function createApiHandler(storage, workspaceRoot) {
3462
3627
  sendJson(res, 400, { error: "Project query parameter is required for snapshot generation (no default fallback)." });
3463
3628
  return true;
3464
3629
  }
3465
- const snapshot = await generateProjectSnapshot(storage, projectCode, workspaceRoot);
3630
+ const snapshot = await generateProjectSnapshot(reqStorage, projectCode, workspaceRoot);
3466
3631
  sendJson(res, 200, snapshot);
3467
3632
  return true;
3468
3633
  }
@@ -3471,12 +3636,12 @@ function createApiHandler(storage, workspaceRoot) {
3471
3636
  const body = await readJsonBody(req);
3472
3637
  if (pathname === "/api/planning/tickets") {
3473
3638
  invalidateApiCache();
3474
- const created = await storage.createTicket(body);
3639
+ const created = await reqStorage.createTicket(body);
3475
3640
  sendJson(res, 201, { ticketId: String(created.meta.id), meta: created.meta });
3476
3641
  return true;
3477
3642
  }
3478
3643
  if (pathname === "/api/planning/create-project") {
3479
- const { code, name, description, colors: colors2 } = body;
3644
+ const { code, name, description, colors: colors2, hub } = body;
3480
3645
  if (!code || !name) {
3481
3646
  sendJson(res, 400, { error: "Both code and name are required to register a project." });
3482
3647
  return true;
@@ -3492,11 +3657,12 @@ function createApiHandler(storage, workspaceRoot) {
3492
3657
  return true;
3493
3658
  }
3494
3659
  invalidateApiCache();
3495
- const project = await storage.registerProject({
3660
+ const project = await reqStorage.registerProject({
3496
3661
  code,
3497
3662
  name,
3498
3663
  description,
3499
- colors: colors2
3664
+ colors: colors2,
3665
+ hub
3500
3666
  });
3501
3667
  sendJson(res, 201, project);
3502
3668
  return true;
@@ -3504,27 +3670,27 @@ function createApiHandler(storage, workspaceRoot) {
3504
3670
  if (pathname === "/api/planning/plans") {
3505
3671
  const { ticketId, planMarkdown, lastHash } = body;
3506
3672
  invalidateApiCache();
3507
- await storage.savePlan(ticketId, planMarkdown, lastHash);
3673
+ await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
3508
3674
  sendJson(res, 200, { success: true });
3509
3675
  return true;
3510
3676
  }
3511
3677
  if (pathname === "/api/planning/comments") {
3512
3678
  const { ticketId, text, author } = body;
3513
3679
  invalidateApiCache();
3514
- await storage.addComment(ticketId, { author: author || "Developer", text });
3515
- const ticket = await storage.getTicket(ticketId);
3680
+ await reqStorage.addComment(ticketId, { author: author || "Developer", text });
3681
+ const ticket = await reqStorage.getTicket(ticketId);
3516
3682
  sendJson(res, 200, { comments: ticket?.comments || [] });
3517
3683
  return true;
3518
3684
  }
3519
3685
  if (pathname === "/api/planning/update-meta") {
3520
3686
  const { ticketId, updates, lastHash } = body;
3521
- const updated = await storage.updateTicket(ticketId, updates, lastHash);
3687
+ const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
3522
3688
  sendJson(res, 200, { meta: updated.meta });
3523
3689
  return true;
3524
3690
  }
3525
3691
  if (pathname === "/api/planning/toggle-flag") {
3526
3692
  const { ticketId, flagged } = body;
3527
- const updated = await storage.updateTicket(ticketId, {
3693
+ const updated = await reqStorage.updateTicket(ticketId, {
3528
3694
  featureFlag: flagged ? "chat_enhanced" : ""
3529
3695
  });
3530
3696
  sendJson(res, 200, { meta: updated.meta });
@@ -3533,7 +3699,7 @@ function createApiHandler(storage, workspaceRoot) {
3533
3699
  if (pathname === "/api/planning/details") {
3534
3700
  const { ticketId, detailMarkdown, metaUpdates } = body;
3535
3701
  if (metaUpdates) {
3536
- await storage.updateTicket(ticketId, metaUpdates);
3702
+ await reqStorage.updateTicket(ticketId, metaUpdates);
3537
3703
  }
3538
3704
  sendJson(res, 200, { success: true, detail: detailMarkdown });
3539
3705
  return true;
@@ -3542,6 +3708,10 @@ function createApiHandler(storage, workspaceRoot) {
3542
3708
  sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
3543
3709
  return true;
3544
3710
  } catch (err) {
3711
+ if (err.name === "EsedreAuthorizationError" || err.message?.includes("outside this workspace's authorized scope") || err.message?.includes("Access Denied")) {
3712
+ sendJson(res, 403, { error: err.message });
3713
+ return true;
3714
+ }
3545
3715
  console.error("[Esedre API Error]:", err);
3546
3716
  sendJson(res, 500, { error: err.message });
3547
3717
  return true;
@@ -3744,12 +3914,14 @@ ${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
3744
3914
  ${colors.bold}mcp${colors.reset} Start the Model Context Protocol (MCP) JSON-RPC 2.0 stdio server for autonomous LLM coding agents.
3745
3915
 
3746
3916
  ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.reset}
3747
- ${colors.bold}init${colors.reset} [<path>] [--project <code>] [-n|--name <name>] [--allow <c1,c2>] [--port <n>] [--hub] [--proxy] [--no-proxy] [-y|--yes] [--json]
3748
- Bring a project repository online (.esedre footprint, wrappers, snapshot) or bootstrap a data hub (--hub).
3917
+ ${colors.bold}init${colors.reset} [<path>] [--project <code>] [-n|--name <name>] [--hub <name|path>] [--allow <c1,c2>] [--port <n>] [-y|--yes] [--json]
3918
+ Bring a project repository online (.esedre footprint, wrappers, snapshot), bootstrap a data hub (--hub), or register a project directly into an Esedre ticket data hub (--project <code>).
3919
+ \u2022 In-repo workspace: ese init (creates .esedre/esedre.json in current directory)
3920
+ \u2022 Data hub project: ese init --project <code> --name "<name>" (creates project in configured dataDir hub)
3749
3921
 
3750
3922
  ${colors.bold}configure${colors.reset} [add <path> | remove <code|path> | set <key> <val> | get <key>] [--json]
3751
3923
  Inspect or mutate central Esedre configuration (~/.esedre/config.json).
3752
- \u2022 add <path> Smart-detect and link a data hub or project repository
3924
+ \u2022 add <path> Link an external project repository (federated projects map) or register a data hub (dataDir)
3753
3925
  \u2022 remove <target> Unlink a project or data hub
3754
3926
  \u2022 set <key> <val> Update a central configuration setting
3755
3927
 
@@ -3829,6 +4001,15 @@ function parseArgs(rawArgs) {
3829
4001
  if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
3830
4002
  return { command, positionals, flags };
3831
4003
  }
4004
+ function printDuplicateProjectWarnings(storage, isJson) {
4005
+ if (isJson) return;
4006
+ const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
4007
+ for (const warn of warnings) {
4008
+ console.error(
4009
+ `${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path9.basename(warn.firstHub)} and ${path9.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path9.basename(warn.firstHub)}' is active.${colors.reset}`
4010
+ );
4011
+ }
4012
+ }
3832
4013
  async function main() {
3833
4014
  const rawArgs = process.argv.slice(2);
3834
4015
  const { command, positionals, flags } = parseArgs(rawArgs);
@@ -3855,9 +4036,12 @@ async function main() {
3855
4036
  try {
3856
4037
  switch (command) {
3857
4038
  case "init": {
3858
- const targetDir = positionals[0] ? path9.resolve(positionals[0]) : discovered.workspaceRoot;
3859
- const isHub = Boolean(flags["hub"]);
3860
- if (isHub) {
4039
+ const rawTarget = positionals[0];
4040
+ const targetDir = rawTarget ? path9.resolve(rawTarget) : discovered.workspaceRoot;
4041
+ const rawHubFlag = flags["hub"];
4042
+ const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
4043
+ const isHubRequested = Boolean(rawHubFlag);
4044
+ if (isHubRequested && !flags["project"] && !flags["p"]) {
3861
4045
  const hubRes = initHub(targetDir);
3862
4046
  try {
3863
4047
  addLocationToGlobalConfig(targetDir);
@@ -3932,26 +4116,66 @@ ${colors.bold}Next Steps:${colors.reset}`);
3932
4116
  console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
3933
4117
  process.exit(1);
3934
4118
  }
3935
- const existingProjects = await storage.getProjects();
3936
- const existing = existingProjects.find(
3937
- (p) => p.code.toLowerCase() === projectCode.toLowerCase()
3938
- );
4119
+ const isTargetHub = fs9.existsSync(path9.join(targetDir, "projects.json")) || fs9.existsSync(path9.join(targetDir, "projects")) && !fs9.existsSync(path9.join(targetDir, ".esedre", "esedre.json"));
4120
+ const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
3939
4121
  let projectNewlyRegistered = false;
3940
4122
  let registeredProj;
3941
- if (!existing) {
3942
- registeredProj = await rawStorage.registerProject({
3943
- code: projectCode,
3944
- name: projectName
3945
- });
3946
- projectNewlyRegistered = true;
3947
- } else {
3948
- registeredProj = existing;
3949
- if (flags["name"] || flags["n"]) {
4123
+ try {
4124
+ const existingProjects = await storage.getProjects();
4125
+ const existing = existingProjects.find(
4126
+ (p) => p.code.toLowerCase() === projectCode.toLowerCase()
4127
+ );
4128
+ if (!existing) {
3950
4129
  registeredProj = await rawStorage.registerProject({
3951
4130
  code: projectCode,
3952
- name: projectName
4131
+ name: projectName,
4132
+ hub: hubArg
3953
4133
  });
4134
+ projectNewlyRegistered = true;
4135
+ } else {
4136
+ registeredProj = existing;
4137
+ if (flags["name"] || flags["n"]) {
4138
+ registeredProj = await rawStorage.registerProject({
4139
+ code: projectCode,
4140
+ name: projectName,
4141
+ hub: hubArg
4142
+ });
4143
+ }
3954
4144
  }
4145
+ } catch (err) {
4146
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
4147
+ process.exit(1);
4148
+ }
4149
+ if (isWorkspaceless) {
4150
+ if (isJson) {
4151
+ console.log(JSON.stringify({
4152
+ project: registeredProj,
4153
+ projectNewlyRegistered,
4154
+ registeredInHub: true,
4155
+ commands: {
4156
+ create: `ese create --title "..." --project ${registeredProj.code}`,
4157
+ list: `ese list --project ${registeredProj.code}`
4158
+ }
4159
+ }, null, 2));
4160
+ } else {
4161
+ if (projectNewlyRegistered) {
4162
+ console.log(`${colors.bold}${colors.green}\u2714 Registered project '${registeredProj.code}' (${registeredProj.name}) in Esedre data hub!${colors.reset}`);
4163
+ } else {
4164
+ console.log(`${colors.bold}${colors.green}\u2714 Verified project '${registeredProj.code}' (${registeredProj.name}) in Esedre data hub!${colors.reset}`);
4165
+ }
4166
+ console.log(` \u2022 Code: ${registeredProj.code}`);
4167
+ console.log(` \u2022 Name: ${registeredProj.name}`);
4168
+ if (registeredProj.description) {
4169
+ console.log(` \u2022 Description: ${registeredProj.description}`);
4170
+ }
4171
+ console.log(`
4172
+ ${colors.bold}Next Steps:${colors.reset}`);
4173
+ console.log(` 1. Create your first ticket:`);
4174
+ console.log(` ${colors.cyan}ese create --title "First feature" --type Feature --project ${registeredProj.code}${colors.reset}`);
4175
+ console.log(` 2. Inspect roadmap & tickets:`);
4176
+ console.log(` ${colors.cyan}ese list --project ${registeredProj.code}${colors.reset}`);
4177
+ }
4178
+ return;
3955
4179
  }
3956
4180
  const allowedProjects = allowArg ? allowArg.split(",").map((s) => s.trim()) : discovered.config?.allowedProjects || [projectCode];
3957
4181
  let setupProxy = void 0;
@@ -4186,10 +4410,12 @@ ${colors.bold}Linked Projects (projects):${colors.reset}`);
4186
4410
  }
4187
4411
  console.log(`
4188
4412
  ${colors.bold}Commands:${colors.reset}`);
4189
- console.log(` \u2022 Link a hub or project: ${colors.cyan}ese configure add <path>${colors.reset}`);
4190
- console.log(` \u2022 Unlink a project or hub: ${colors.cyan}ese configure remove <code|path>${colors.reset}`);
4191
- console.log(` \u2022 Set setting value: ${colors.cyan}ese configure set <key> <value>${colors.reset}`);
4192
- console.log(` \u2022 Bring new repo online: ${colors.cyan}ese init [path]${colors.reset}`);
4413
+ console.log(` \u2022 Link external repository: ${colors.cyan}ese configure add <repoPath>${colors.reset}`);
4414
+ console.log(` \u2022 Register a data hub: ${colors.cyan}ese configure add <hubPath>${colors.reset}`);
4415
+ console.log(` \u2022 Create hub project: ${colors.cyan}ese init --project <code> --name "<name>"${colors.reset}`);
4416
+ console.log(` \u2022 Unlink a project or hub: ${colors.cyan}ese configure remove <code|path>${colors.reset}`);
4417
+ console.log(` \u2022 Set setting value: ${colors.cyan}ese configure set <key> <value>${colors.reset}`);
4418
+ console.log(` \u2022 Bring new repo online: ${colors.cyan}ese init [path]${colors.reset}`);
4193
4419
  return;
4194
4420
  }
4195
4421
  case "upgrade": {
@@ -4251,12 +4477,18 @@ ${colors.bold}Commands:${colors.reset}`);
4251
4477
  const ports = resolvePorts(discovered.config, { port: flagPort });
4252
4478
  const port = ports.gateway;
4253
4479
  const isForeground = Boolean(flags["foreground"] || flags["f"]);
4480
+ const serverConfig = {
4481
+ ...discovered.config,
4482
+ allowedProjects: ["*"]
4483
+ };
4484
+ const serverStorage = new FilesystemStorageAdapter(discovered.workspaceRoot, serverConfig);
4485
+ printDuplicateProjectWarnings(serverStorage, isJson);
4254
4486
  if (isForeground) {
4255
4487
  const cluster = startGatewayCluster({
4256
4488
  gatewayPort: ports.gateway,
4257
4489
  uiPort: ports.ui,
4258
4490
  apiPort: ports.api,
4259
- storage,
4491
+ storage: serverStorage,
4260
4492
  workspaceRoot: discovered.workspaceRoot
4261
4493
  });
4262
4494
  console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);
@@ -4291,6 +4523,7 @@ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
4291
4523
  return;
4292
4524
  }
4293
4525
  case "status": {
4526
+ printDuplicateProjectWarnings(storage, isJson);
4294
4527
  const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
4295
4528
  const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
4296
4529
  const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
@@ -4328,6 +4561,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
4328
4561
  return;
4329
4562
  }
4330
4563
  case "projects": {
4564
+ printDuplicateProjectWarnings(storage, isJson);
4331
4565
  const projects = await storage.getProjects();
4332
4566
  if (isJson) {
4333
4567
  console.log(JSON.stringify(projects, null, 2));
@@ -4340,6 +4574,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
4340
4574
  return;
4341
4575
  }
4342
4576
  case "list": {
4577
+ printDuplicateProjectWarnings(storage, isJson);
4343
4578
  const rawProject = flags["project"];
4344
4579
  const project = rawProject === "all" ? void 0 : rawProject || discovered.config?.projectCode || void 0;
4345
4580
  const status = flags["status"] || void 0;