@runeya/runeya 2.0.146 → 2.0.147

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.
Files changed (171) hide show
  1. package/agent/index.js +10 -4
  2. package/agent/index.js.map +1 -1
  3. package/{agent-manager-R7BPQO6D.js → agent-manager-46C4NIQ2.js} +4 -2
  4. package/{chat-LPML22J5.js → chat-CMNDMXIP.js} +9 -5
  5. package/{chunk-SEPAURTQ.js → chunk-2NE52CL5.js} +156 -923
  6. package/chunk-2NE52CL5.js.map +1 -0
  7. package/chunk-4D3GCBPO.js +462 -0
  8. package/chunk-4D3GCBPO.js.map +1 -0
  9. package/{chunk-KV4GJKXU.js → chunk-5XG3X2TM.js} +27 -1479
  10. package/chunk-5XG3X2TM.js.map +1 -0
  11. package/chunk-7QXP4FXD.js +969 -0
  12. package/chunk-7QXP4FXD.js.map +1 -0
  13. package/chunk-DA2DZBTL.js +43 -0
  14. package/chunk-DA2DZBTL.js.map +1 -0
  15. package/chunk-DFPZ4MTI.js +1041 -0
  16. package/chunk-DFPZ4MTI.js.map +1 -0
  17. package/{chunk-UD42LACH.js → chunk-JNK6H5KH.js} +164 -1007
  18. package/chunk-JNK6H5KH.js.map +1 -0
  19. package/chunk-WDHLEO5S.js +809 -0
  20. package/chunk-WDHLEO5S.js.map +1 -0
  21. package/{companion-W3UMZWGH.js → companion-XKMNRIJB.js} +8 -3
  22. package/environment-resolver-ZDTF3MHI.js +18 -0
  23. package/environment-resolver-ZDTF3MHI.js.map +1 -0
  24. package/index.js +7 -7
  25. package/{install-gate-PVMGJDWE.js → install-gate-OJJEUU4E.js} +2 -2
  26. package/mcp-ready-TAPBAOZY.js +9 -0
  27. package/mcp-ready-TAPBAOZY.js.map +1 -0
  28. package/mcp-server/index.js +24 -0
  29. package/mcp-server/index.js.map +1 -1
  30. package/mcp-server/launch.d.ts +6 -0
  31. package/mcp-server/launch.js +2 -1
  32. package/mcp-server/launch.js.map +1 -1
  33. package/package.json +1 -1
  34. package/project-store-FLXTAD3J.js +23 -0
  35. package/project-store-FLXTAD3J.js.map +1 -0
  36. package/{src-LEDGW5KP.js → src-KWO3CKZU.js} +139 -59
  37. package/src-KWO3CKZU.js.map +1 -0
  38. package/web/assets/{AgentScalarChatInterface.vue-BYszptaB.js → AgentScalarChatInterface.vue-DKBinB_l.js} +2 -2
  39. package/web/assets/AgentTerminalPage-CixU7x6O.js +1 -0
  40. package/web/assets/AiChatModals-BvGUd-QD.css +1 -0
  41. package/web/assets/AiChatModals-C-hXImkO.js +4 -0
  42. package/web/assets/AppLayout-At9wk0sx.js +1 -0
  43. package/web/assets/{AppLayout-tsw5Ow0w.css → AppLayout-BlhwflMq.css} +1 -1
  44. package/web/assets/AuditPage-BkqbHxjY.js +1 -0
  45. package/web/assets/CloudAccountPage-D5C2SCS9.js +1 -0
  46. package/web/assets/CloudMessagingPage-Bi57GGHO.js +2 -0
  47. package/web/assets/DashboardPage-dEQop9ql.js +1 -0
  48. package/web/assets/DirectoryPickerDialog-DIMSeDhw.js +1 -0
  49. package/web/assets/{EditorIcon-CypGLV6w.js → EditorIcon-W3Szca4J.js} +1 -1
  50. package/web/assets/EnvironmentForm-C5emavNh.js +1 -0
  51. package/web/assets/EnvironmentFormPage-BIoy8fsW.js +1 -0
  52. package/web/assets/EnvironmentListPage-BDYeh7XF.js +1 -0
  53. package/web/assets/{FlagChip-R_T5qYpL.js → FlagChip-DPtIBCjQ.js} +1 -1
  54. package/web/assets/KanbanBoardPage-BlCcjBwV.js +4 -0
  55. package/web/assets/KanbanListPage-DbXONxfC.js +1 -0
  56. package/web/assets/LaunchProjectPage-Cg92hHwQ.js +1 -0
  57. package/web/assets/LaunchServicesPage-CO8gOB9D.js +1 -0
  58. package/web/assets/LaunchServicesPage-h5o3zLcy.css +1 -0
  59. package/web/assets/{LaunchShell-DUrSunB4.js → LaunchShell-CDb21ihZ.js} +1 -1
  60. package/web/assets/LogViewer-DsusKk2b.js +3 -0
  61. package/web/assets/OrgKeyPrompt-CX9K7r9I.js +1 -0
  62. package/web/assets/ProcessControls-B7hWVqiR.css +1 -0
  63. package/web/assets/ProcessControls-TEZutuxq.js +1 -0
  64. package/web/assets/ProcessDetailPage-Cj23FECq.js +1 -0
  65. package/web/assets/ProcessMetrics-1P6iE2Il.js +1 -0
  66. package/web/assets/ProjectCreateDialog-D3aMWKfw.js +1 -0
  67. package/web/assets/{ProjectLocationGate-DhMVdIjN.js → ProjectLocationGate-DU0Jj8Nw.js} +1 -1
  68. package/web/assets/ProjectPublishDialog-D7sNKei3.js +1 -0
  69. package/web/assets/{RandomJobTestPage-BUS2Ty6-.js → RandomJobTestPage-DQUMRHyf.js} +1 -1
  70. package/web/assets/{ScenarioEditorPage-vMoRt0Qn.css → ScenarioEditorPage-BSjSX4bw.css} +1 -1
  71. package/web/assets/ScenarioEditorPage-DvCdcY9m.js +14 -0
  72. package/web/assets/SendToChatDialog-BdWKjYmv.js +1 -0
  73. package/web/assets/ServiceControlsBar-B_J5m-Kc.js +1 -0
  74. package/web/assets/ServiceDetailPage-DhJcjFfY.js +2 -0
  75. package/web/assets/{ServiceDetailPage-COdaCZ6r.css → ServiceDetailPage-DnSf5cm9.css} +1 -1
  76. package/web/assets/ServiceFormPage-fp8NCHSv.js +1 -0
  77. package/web/assets/{SettingsPage-4pKUgKPm.css → SettingsPage-NH2fQap4.css} +1 -1
  78. package/web/assets/SettingsPage-s28BodWe.js +2086 -0
  79. package/web/assets/UnlockPage-BXXIPHW_.js +1 -0
  80. package/web/assets/VaultMigrationPage-BfNuKtkJ.js +1 -0
  81. package/web/assets/{VersionBadge-BmArk24p.js → VersionBadge-CW7czacg.js} +1 -1
  82. package/web/assets/{WikiPage-DADu8WUA.js → WikiPage-BiwpCM3E.js} +1 -1
  83. package/web/assets/WikiView-CMch_06Z.js +1 -0
  84. package/web/assets/{add-scalar-classes-dWEWPlFD.js → add-scalar-classes-Ckanjq1h.js} +14 -14
  85. package/web/assets/{agent.store-CKLAkF9a.js → agent.store-piokQ9Dt.js} +1 -1
  86. package/web/assets/{auth-Cg9BWTfK.js → auth-BT_jf-5e.js} +2 -2
  87. package/web/assets/cloud-chat.store-CDKh4tIy.js +1 -0
  88. package/web/assets/{cloud-client-D_zCiW0s.js → cloud-client-CzPdZLDq.js} +2 -2
  89. package/web/assets/{cloud-session-DOaDpZTy.js → cloud-session-BYfn6YM4.js} +2 -2
  90. package/web/assets/floating-ui.dom-DF3oyfHd.js +1 -0
  91. package/web/assets/i18n-D0FT7rCj.js +1 -0
  92. package/web/assets/index-3LTzgeU-.css +1 -0
  93. package/web/assets/index-Ci2CNnit.js +309 -0
  94. package/web/assets/modal-iRaP1izE.js +5 -0
  95. package/web/assets/notification.store-CvvuTBVz.js +1 -0
  96. package/web/assets/{project-launch.store-y7JBMNiE.js → project-launch.store-CTmbl8UY.js} +1 -1
  97. package/web/assets/{project.store-DGJr0Qr9.js → project.store-CNC6_4r4.js} +1 -1
  98. package/web/assets/runtime-dom.esm-bundler-CyEVRK6B.js +1 -0
  99. package/web/assets/scenario.store-BIFQPXgl.js +7 -0
  100. package/web/assets/{service.store-5jWsJxPx.js → service.store-B40U4QzI.js} +1 -1
  101. package/web/assets/settings-scope-CyWMCjRY.js +1 -0
  102. package/web/assets/settings.store-Dw6bAoWE.js +1 -0
  103. package/web/assets/{src-Dnwk6NpV.js → src-CrNi4Pjg.js} +3 -3
  104. package/web/assets/src-DIBHat7I.js +3 -0
  105. package/web/assets/src-DtCW_XCh.js +1 -0
  106. package/web/assets/{trpc-Drabzwpd.js → trpc-BAZGRZBD.js} +3 -3
  107. package/web/assets/{useCloudOrg-BAQEtFPl.js → useCloudOrg-EnXFBwt1.js} +1 -1
  108. package/web/assets/useEnvVarAutocomplete-BxJh2Lw0.js +1 -0
  109. package/web/assets/useProjectCatalog-BrvUGv9A.js +1 -0
  110. package/web/assets/{useProjectCloudOrg-CEN9-_OA.js → useProjectCloudOrg-Czxlg00k.js} +1 -1
  111. package/web/assets/{useServiceUrlHost-BuoPEXZX.js → useServiceUrlHost-Szz9y3rh.js} +1 -1
  112. package/web/assets/vue-i18n-o7yraQpR.js +3 -0
  113. package/web/index.html +21 -21
  114. package/chunk-KV4GJKXU.js.map +0 -1
  115. package/chunk-SEPAURTQ.js.map +0 -1
  116. package/chunk-UD42LACH.js.map +0 -1
  117. package/src-LEDGW5KP.js.map +0 -1
  118. package/web/assets/AgentTerminalPage-C3TJ1NL9.js +0 -1
  119. package/web/assets/AiChatModals-D3scRE3S.js +0 -4
  120. package/web/assets/AiChatModals-EfRuvId7.css +0 -1
  121. package/web/assets/AppLayout-BMQUjPKe.js +0 -1
  122. package/web/assets/AuditPage-CKcExBws.js +0 -1
  123. package/web/assets/CloudAccountPage-B6oPg0BZ.js +0 -1
  124. package/web/assets/CloudMessagingPage-C5fwVe-F.js +0 -2
  125. package/web/assets/DashboardPage-9RV7sx3e.js +0 -1
  126. package/web/assets/DirectoryPickerDialog-C06BXyfJ.js +0 -1
  127. package/web/assets/EnvironmentForm-CAvDoira.js +0 -1
  128. package/web/assets/EnvironmentFormPage-BmAxhN-m.js +0 -1
  129. package/web/assets/EnvironmentListPage-B8uATPBI.js +0 -1
  130. package/web/assets/KanbanBoardPage-CQsOkRUf.js +0 -4
  131. package/web/assets/KanbanListPage-CWZqkjD6.js +0 -1
  132. package/web/assets/LaunchProjectPage-saSmC7Pr.js +0 -1
  133. package/web/assets/LaunchServicesPage-BkKyLMEz.js +0 -1
  134. package/web/assets/LaunchServicesPage-DoLDPqQq.css +0 -1
  135. package/web/assets/LogViewer-CZBYkH9J.js +0 -3
  136. package/web/assets/OrgKeyPrompt-DzQ7pwbs.js +0 -1
  137. package/web/assets/ProcessControls-BEeoNlNV.js +0 -1
  138. package/web/assets/ProcessControls-BlpnVylq.css +0 -1
  139. package/web/assets/ProcessDetailPage-CQgu5uKm.js +0 -1
  140. package/web/assets/ProcessMetrics-Crlfkelj.js +0 -1
  141. package/web/assets/ProjectCreateDialog-BolrhRD-.js +0 -1
  142. package/web/assets/ProjectPublishDialog-DsZNWrfz.js +0 -1
  143. package/web/assets/ScenarioEditorPage-MIAGh2us.js +0 -14
  144. package/web/assets/SendToChatDialog-BxWsyv29.js +0 -1
  145. package/web/assets/ServiceControlsBar-DqDPAZt7.js +0 -1
  146. package/web/assets/ServiceDetailPage-hkSAzZ-l.js +0 -2
  147. package/web/assets/ServiceFormPage-DXqhqXg8.js +0 -1
  148. package/web/assets/SettingsPage-DS2YhMIM.js +0 -2086
  149. package/web/assets/UnlockPage-s8SgVcKk.js +0 -1
  150. package/web/assets/VaultMigrationPage-s0b44MDM.js +0 -1
  151. package/web/assets/WikiView-DtGwIt_Q.js +0 -1
  152. package/web/assets/cloud-chat.store-b-BHGz7I.js +0 -1
  153. package/web/assets/editor.store-DCAZSeVG.js +0 -1
  154. package/web/assets/floating-ui.dom-DWhsZyqT.js +0 -1
  155. package/web/assets/i18n-D0afQEGs.js +0 -1
  156. package/web/assets/index-D8VRbyoP.js +0 -309
  157. package/web/assets/index-DsezNbSM.css +0 -1
  158. package/web/assets/modal-qYgTMkYb.js +0 -5
  159. package/web/assets/scenario.store-YWHL2_wu.js +0 -7
  160. package/web/assets/settings-scope-Bv7QGPrt.js +0 -1
  161. package/web/assets/settings.store-bEsB7oKb.js +0 -1
  162. package/web/assets/src--GO-VSJ0.js +0 -5
  163. package/web/assets/src-C-D_y7ou.js +0 -1
  164. package/web/assets/useEnvVarAutocomplete-BqGTODUv.js +0 -1
  165. package/web/assets/useProjectCatalog-yocjIKt6.js +0 -1
  166. package/web/assets/vue-draggable-plus-BHsveB8w.css +0 -1
  167. package/web/assets/vue-draggable-plus-DX4rTBiu.js +0 -1
  168. /package/{agent-manager-R7BPQO6D.js.map → agent-manager-46C4NIQ2.js.map} +0 -0
  169. /package/{chat-LPML22J5.js.map → chat-CMNDMXIP.js.map} +0 -0
  170. /package/{companion-W3UMZWGH.js.map → companion-XKMNRIJB.js.map} +0 -0
  171. /package/{install-gate-PVMGJDWE.js.map → install-gate-OJJEUU4E.js.map} +0 -0
@@ -0,0 +1,969 @@
1
+ import {
2
+ serviceStore
3
+ } from "./chunk-4D3GCBPO.js";
4
+ import {
5
+ AmbiguousProjectError,
6
+ StoreDiskCoordinator,
7
+ environmentStore,
8
+ forgetProject,
9
+ projectKey,
10
+ recordDetached,
11
+ registerProjectLoader,
12
+ setProjectServices,
13
+ setProjectVault
14
+ } from "./chunk-DFPZ4MTI.js";
15
+ import {
16
+ atomicWriteSecure,
17
+ fingerprintFiles
18
+ } from "./chunk-4LJQIQMN.js";
19
+ import {
20
+ collidesWithMachineRoot,
21
+ forgetLocalProjectRoot,
22
+ listCloudVaults,
23
+ localProjectVaultRoot,
24
+ localVaultCollidesWithMachineRoot,
25
+ localVaultRoot,
26
+ machineFilePath,
27
+ machineRoot,
28
+ readLocalProjectDirs,
29
+ registerLocalProjectDir,
30
+ setLocalProjectRoot,
31
+ unregisterLocalProjectDir,
32
+ vaultFor
33
+ } from "./chunk-VPSQJOSN.js";
34
+ import {
35
+ ProjectSchema
36
+ } from "./chunk-MZSDTA4A.js";
37
+
38
+ // ../server/src/services/project-store.ts
39
+ import { randomUUID as randomUUID2 } from "crypto";
40
+ import { EventEmitter } from "events";
41
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm } from "fs/promises";
42
+ import { dirname, join as join3, resolve as resolve2 } from "path";
43
+
44
+ // ../server/src/utils/ensure-data-gitignore.ts
45
+ import { writeFile } from "fs/promises";
46
+ import { join } from "path";
47
+ var GITIGNORE_CONTENT = `
48
+ #################
49
+ # Do not edit this file #
50
+ #################
51
+
52
+
53
+ # Encryption key (contains secrets \u2014 never commit)
54
+ .encryption.key
55
+ .encryption.key.enc
56
+
57
+
58
+ # Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
59
+ !.runeya-verify
60
+
61
+ # This machine's cloud session token (encrypted, and specific to this install)
62
+ cloud-session.json
63
+
64
+ # Backup in local only
65
+ backup-*
66
+
67
+ # Per-user environment overrides (never commit \u2014 each dev has their own)
68
+ environment-overrides.json
69
+
70
+ # Private user data (conversations, personal state \u2014 never commit)
71
+ *.private
72
+ *.private.json*
73
+ conversations/
74
+ conversations.private.json
75
+
76
+
77
+
78
+ # Runtime logs
79
+ *.log
80
+ *.log.*
81
+ `;
82
+ async function ensureDataGitignore(dataDir) {
83
+ const gitignorePath = join(dataDir, ".gitignore");
84
+ await writeFile(gitignorePath, GITIGNORE_CONTENT, "utf-8");
85
+ }
86
+
87
+ // ../server/src/services/conversation-section-store.ts
88
+ import { randomUUID } from "crypto";
89
+ import { readFile } from "fs/promises";
90
+ var FILENAME = "conversation-sections.json";
91
+ function isSectionMap(value) {
92
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
93
+ return Object.values(value).every((list) => Array.isArray(list) && list.every((s) => !!s && typeof s === "object" && typeof s.id === "string" && typeof s.name === "string" && typeof s.order === "number"));
94
+ }
95
+ var ConversationSectionStore = class {
96
+ async read() {
97
+ try {
98
+ const parsed = JSON.parse(await readFile(machineFilePath(FILENAME), "utf-8"));
99
+ return isSectionMap(parsed) ? parsed : {};
100
+ } catch {
101
+ return {};
102
+ }
103
+ }
104
+ async write(map) {
105
+ await atomicWriteSecure(machineFilePath(FILENAME), JSON.stringify(map, null, 2));
106
+ }
107
+ /** Les sections d'un projet, dans l'ordre choisi. */
108
+ async list(projectId) {
109
+ const map = await this.read();
110
+ return (map[projectId] ?? []).slice().sort((a, b) => a.order - b.order);
111
+ }
112
+ async get(projectId, id) {
113
+ return (await this.list(projectId)).find((s) => s.id === id) ?? null;
114
+ }
115
+ /** Crée une section, posée après les autres. */
116
+ async create(projectId, name) {
117
+ const map = await this.read();
118
+ const list = map[projectId] ?? [];
119
+ const order = list.length > 0 ? Math.max(...list.map((s) => s.order)) + 1 : 0;
120
+ const section = { id: randomUUID(), name, order };
121
+ map[projectId] = [...list, section];
122
+ await this.write(map);
123
+ return section;
124
+ }
125
+ async rename(projectId, id, name) {
126
+ const map = await this.read();
127
+ const section = map[projectId]?.find((s) => s.id === id);
128
+ if (!section) return null;
129
+ section.name = name;
130
+ await this.write(map);
131
+ return section;
132
+ }
133
+ /** Retire une section. Rend `false` si elle n'existait pas. */
134
+ async remove(projectId, id) {
135
+ const map = await this.read();
136
+ const list = map[projectId] ?? [];
137
+ const next = list.filter((s) => s.id !== id);
138
+ if (next.length === list.length) return false;
139
+ if (next.length > 0) map[projectId] = next;
140
+ else delete map[projectId];
141
+ await this.write(map);
142
+ return true;
143
+ }
144
+ /**
145
+ * Range les sections du projet dans l'ordre de `ids`.
146
+ *
147
+ * Une section absente de `ids` passe après les autres, dans son ordre
148
+ * d'avant : un appel parti avant la création d'une section ne la fait pas
149
+ * disparaître. Un id inconnu est ignoré.
150
+ */
151
+ async reorder(projectId, ids) {
152
+ const map = await this.read();
153
+ const list = (map[projectId] ?? []).slice().sort((a, b) => a.order - b.order);
154
+ if (list.length === 0) return;
155
+ const rank = new Map(Array.from(new Set(ids)).map((id, i) => [id, i]));
156
+ const listed = list.filter((s) => rank.has(s.id)).sort((a, b) => rank.get(a.id) - rank.get(b.id));
157
+ const others = list.filter((s) => !rank.has(s.id));
158
+ map[projectId] = [...listed, ...others].map((s, i) => ({ ...s, order: i }));
159
+ await this.write(map);
160
+ }
161
+ /** Oublie les sections d'un projet supprimé. */
162
+ async forgetProject(projectId) {
163
+ const map = await this.read();
164
+ if (!(projectId in map)) return;
165
+ delete map[projectId];
166
+ await this.write(map);
167
+ }
168
+ };
169
+ var conversationSectionStore = new ConversationSectionStore();
170
+
171
+ // ../server/src/services/project-path-store.ts
172
+ import { readFile as readFile2, writeFile as writeFile2, rename, mkdir, chmod, stat } from "fs/promises";
173
+ import { isAbsolute, join as join2, resolve } from "path";
174
+ var FILENAME2 = "project-paths.json";
175
+ var ProjectPathStore = class {
176
+ paths = null;
177
+ async load() {
178
+ if (this.paths) return this.paths;
179
+ try {
180
+ const raw = await readFile2(machineFilePath(FILENAME2), "utf-8");
181
+ const parsed = JSON.parse(raw);
182
+ this.paths = isPathMap(parsed) ? parsed : {};
183
+ } catch {
184
+ this.paths = {};
185
+ }
186
+ return this.paths;
187
+ }
188
+ async persist() {
189
+ await mkdir(machineRoot(), { recursive: true });
190
+ const filePath = machineFilePath(FILENAME2);
191
+ const tmpPath = `${filePath}.tmp`;
192
+ await writeFile2(tmpPath, JSON.stringify(this.paths ?? {}, null, 2), "utf-8");
193
+ await rename(tmpPath, filePath);
194
+ await chmod(filePath, 384);
195
+ }
196
+ /** The directory chosen for this project, or null while it has none. */
197
+ async get(ref) {
198
+ const paths = await this.load();
199
+ return paths[projectKey(ref)] ?? paths[ref.id] ?? null;
200
+ }
201
+ async all() {
202
+ return { ...await this.load() };
203
+ }
204
+ /**
205
+ * The project already placed in this directory, if any other one is.
206
+ *
207
+ * Answers with the key as stored — the couple, or the bare id for an entry an
208
+ * older version wrote — which is enough to say "taken" and to name it.
209
+ *
210
+ * `isKnown` écarte les entrées d'un projet que ce poste ne connaît plus : un
211
+ * projet disparu ne doit pas continuer de retenir son dossier.
212
+ */
213
+ async projectAt(path, exclude, isKnown = () => true) {
214
+ const target = resolve(path);
215
+ const excluded = exclude ? [projectKey(exclude), exclude.id] : [];
216
+ for (const [key, dir] of Object.entries(await this.load())) {
217
+ if (!excluded.includes(key) && resolve(dir) === target && isKnown(key)) return key;
218
+ }
219
+ return null;
220
+ }
221
+ async set(ref, path) {
222
+ if (!isAbsolute(path)) throw new Error("Le chemin du projet doit \xEAtre absolu");
223
+ const paths = await this.load();
224
+ delete paths[ref.id];
225
+ paths[projectKey(ref)] = resolve(path);
226
+ await this.persist();
227
+ }
228
+ /**
229
+ * Faire suivre le répertoire quand un projet change d'organisation.
230
+ *
231
+ * La clé est le couple `(organisation, projet)` : publier un projet local le
232
+ * fait passer de `local:<id>` à `<org>:<id>`, et l'entrée d'avant cesse alors
233
+ * de répondre — `get` ne retombe que sur l'identifiant nu, forme héritée, pas
234
+ * sur `local:<id>`. Le projet se retrouvait donc sans emplacement connu juste
235
+ * après avoir été publié, alors que ses sources n'avaient pas bougé d'un
236
+ * octet.
237
+ */
238
+ async relocate(from, to) {
239
+ const paths = await this.load();
240
+ const previous = paths[projectKey(from)] ?? paths[from.id];
241
+ if (!previous) return;
242
+ delete paths[projectKey(from)];
243
+ delete paths[from.id];
244
+ paths[projectKey(to)] = previous;
245
+ await this.persist();
246
+ }
247
+ async clear(ref) {
248
+ const paths = await this.load();
249
+ const keys = [projectKey(ref), ref.id].filter((k) => k in paths);
250
+ if (keys.length === 0) return;
251
+ for (const key of keys) delete paths[key];
252
+ await this.persist();
253
+ }
254
+ /** Test seam: forget what was read from disk. */
255
+ reset() {
256
+ this.paths = null;
257
+ }
258
+ };
259
+ function isPathMap(value) {
260
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
261
+ }
262
+ async function holdsRuneyaProject(path) {
263
+ return stat(join2(path, ".runeya", "project.json")).then(() => true, () => false);
264
+ }
265
+ var projectPathStore = new ProjectPathStore();
266
+
267
+ // ../server/src/services/project-store.ts
268
+ var FILENAME3 = "project.json";
269
+ var LEGACY_FILENAME = "projects.json";
270
+ function toDiskForm(project) {
271
+ const { activeEnvironmentId: _runtimeOnly, ...rest } = project;
272
+ return rest;
273
+ }
274
+ async function readLocalProject() {
275
+ const root = localVaultRoot();
276
+ let current = null;
277
+ try {
278
+ const raw = await readFile3(join3(root, FILENAME3), "utf-8");
279
+ const parsed = ProjectSchema.safeParse(JSON.parse(raw));
280
+ if (parsed.success) current = parsed.data;
281
+ } catch {
282
+ }
283
+ let legacy;
284
+ try {
285
+ legacy = JSON.parse(await readFile3(join3(root, LEGACY_FILENAME), "utf-8"));
286
+ } catch {
287
+ return { local: current, strayCloud: [] };
288
+ }
289
+ const strayCloud = legacy.filter((p) => p.orgId);
290
+ if (current) return { local: current, strayCloud };
291
+ const locals = legacy.filter((p) => !p.orgId);
292
+ if (locals.length > 1) {
293
+ const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
294
+ throw new Error(
295
+ `${join3(root, LEGACY_FILENAME)} holds ${locals.length} local projects: ${names}. A directory now holds a single local project \u2014 move the others to their own directory, or into an organization, then start again.`
296
+ );
297
+ }
298
+ return { local: locals[0] ?? null, strayCloud };
299
+ }
300
+ function isRegistrable(root) {
301
+ return localProjectVaultRoot(dirname(root)) === resolve2(root);
302
+ }
303
+ async function readLocalProjectAt(root) {
304
+ try {
305
+ const parsed = ProjectSchema.safeParse(JSON.parse(await readFile3(join3(root, FILENAME3), "utf-8")));
306
+ return parsed.success && !parsed.data.orgId ? parsed.data : null;
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+ async function readCloudProjects() {
312
+ const found = [];
313
+ for (const { orgId, projectId, root } of await listCloudVaults()) {
314
+ try {
315
+ const raw = await readFile3(join3(root, FILENAME3), "utf-8");
316
+ const parsed = ProjectSchema.safeParse({ ...JSON.parse(raw), id: projectId, orgId });
317
+ if (parsed.success) found.push({ project: parsed.data, root });
318
+ } catch {
319
+ }
320
+ }
321
+ return found;
322
+ }
323
+ var LocalProjectRefusedError = class extends Error {
324
+ };
325
+ var LocalProjectExistsError = class extends LocalProjectRefusedError {
326
+ constructor(projectName) {
327
+ super(
328
+ `This directory already holds the local project "${projectName}". A directory holds one local project: use another directory, or create the project in an organization.`
329
+ );
330
+ this.projectName = projectName;
331
+ this.name = "LocalProjectExistsError";
332
+ }
333
+ code = "LOCAL_PROJECT_EXISTS";
334
+ };
335
+ var LocalProjectInMachineRootError = class extends LocalProjectRefusedError {
336
+ code = "LOCAL_PROJECT_IN_MACHINE_ROOT";
337
+ constructor() {
338
+ super(
339
+ "This directory holds Runeya's own data. A local project here would mix its files with the machine's vaults: launch Runeya from another directory, or create the project in an organization."
340
+ );
341
+ this.name = "LocalProjectInMachineRootError";
342
+ }
343
+ };
344
+ var LocalProjectCloudLinkedError = class extends LocalProjectRefusedError {
345
+ code = "LOCAL_PROJECT_CLOUD_LINKED";
346
+ constructor() {
347
+ super("This directory is already linked to an organization project. Choose another directory.");
348
+ this.name = "LocalProjectCloudLinkedError";
349
+ }
350
+ };
351
+ var LocalProjectNotFoundError = class extends LocalProjectRefusedError {
352
+ code = "LOCAL_PROJECT_NOT_FOUND";
353
+ constructor() {
354
+ super("This directory holds no local Runeya project to import.");
355
+ this.name = "LocalProjectNotFoundError";
356
+ }
357
+ };
358
+ var ProjectStore = class {
359
+ /**
360
+ * Keyed by the (organization, project) couple, never by the project id alone:
361
+ * the same id can name a different project in each organization, and keying
362
+ * by id let the second one read from disk overwrite the first — then `save()`
363
+ * wrote that survivor back into both vaults.
364
+ */
365
+ projects = /* @__PURE__ */ new Map();
366
+ /**
367
+ * Les coffres où un projet local a été lu ou écrit. À l'écriture, un coffre
368
+ * de cette liste qui n'a plus de projet perd sa fiche : c'est ainsi qu'une
369
+ * suppression ou une publication vide le dossier qu'elle quitte.
370
+ */
371
+ localRoots = /* @__PURE__ */ new Set();
372
+ loaded = false;
373
+ /**
374
+ * L'état du disque tel qu'il était au dernier chargement.
375
+ *
376
+ * Plusieurs Runeya partagent la racine machine — l'application installée et
377
+ * celle qu'on développe, couramment — et créent donc des projets dans les
378
+ * mêmes coffres. Un store chargé une fois pour toutes ne voit jamais le
379
+ * travail de l'autre : il faudrait le redémarrer pour qu'un projet créé à
380
+ * côté existe pour lui.
381
+ */
382
+ fingerprint = "";
383
+ disk = new StoreDiskCoordinator();
384
+ /** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */
385
+ events = new EventEmitter();
386
+ getFilePath(root = localVaultRoot()) {
387
+ return join3(root, FILENAME3);
388
+ }
389
+ /** Adopt a project read from disk, remembering which vault it came from. */
390
+ async absorb(into, p, root) {
391
+ const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
392
+ const defaultEnv = projectEnvs.find((e) => e.default);
393
+ into.set(projectKey(p), {
394
+ ...p,
395
+ environmentIds: p.environmentIds ?? [],
396
+ groupOrder: p.groupOrder ?? [],
397
+ // Prefer the env marked default, fall back to first env
398
+ activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
399
+ });
400
+ if (!p.orgId) {
401
+ setLocalProjectRoot(p.id, root);
402
+ this.localRoots.add(root);
403
+ }
404
+ setProjectVault(p, root);
405
+ setProjectServices(p, p.serviceIds ?? []);
406
+ }
407
+ /**
408
+ * Ce que le disque porte : quels coffres, et quand leur fiche a changé.
409
+ *
410
+ * Les coffres sont demandés à `listCloudVaults`, qui les découvre déjà par
411
+ * balayage — en refaire un ici donnerait deux inventaires à tenir d'accord.
412
+ * Seules les dates sont relevées : lire et analyser les fiches, puis les
413
+ * absorber (ce qui va interroger les environnements), est justement ce que ce
414
+ * repère évite de refaire à chaque appel.
415
+ */
416
+ async diskFingerprint() {
417
+ return fingerprintFiles([
418
+ this.getFilePath(),
419
+ ...(await readLocalProjectDirs()).map((dir) => this.getFilePath(localProjectVaultRoot(dir))),
420
+ ...(await listCloudVaults()).map((v) => this.getFilePath(v.root))
421
+ ]);
422
+ }
423
+ /**
424
+ * Read every vault: the launch directory, then each organization's projects.
425
+ *
426
+ * Cloud vaults are discovered by walking `~/.runeya/orgs/*&#47;projects/*` rather
427
+ * than from an index. An index is one more thing that can disagree with the
428
+ * disk — and a project present on disk but missing from it is exactly the
429
+ * "already here, still shown as downloadable" bug.
430
+ *
431
+ * Relu quand le disque a bougé, pas une fois pour toutes : une autre instance
432
+ * partageant cette racine machine y crée des projets, et ne jamais relire
433
+ * reviendrait à exiger un redémarrage pour les voir.
434
+ */
435
+ async load() {
436
+ let migrated = false;
437
+ await this.disk.load(async () => {
438
+ const fingerprint = await this.diskFingerprint();
439
+ if (this.loaded && fingerprint === this.fingerprint) return null;
440
+ const next = await this.readVaults();
441
+ return () => {
442
+ for (const [key, project] of next.projects) {
443
+ const before = this.projects.get(key)?.activeEnvironmentId;
444
+ if (before && project.environmentIds.includes(before)) {
445
+ project.activeEnvironmentId = before;
446
+ } else {
447
+ this.unsettled.add(key);
448
+ }
449
+ }
450
+ this.projects = next.projects;
451
+ this.fingerprint = fingerprint;
452
+ this.loaded = true;
453
+ migrated = next.migrated;
454
+ };
455
+ });
456
+ if (migrated) {
457
+ await this.save();
458
+ await serviceStore.relocateAll();
459
+ await environmentStore.relocateAll();
460
+ }
461
+ }
462
+ /** Le parcours proprement dit, dans une table neuve que `load` substitue. */
463
+ async readVaults() {
464
+ const projects = /* @__PURE__ */ new Map();
465
+ this.localRoots = /* @__PURE__ */ new Set();
466
+ const { local, strayCloud } = await readLocalProject();
467
+ if (local) {
468
+ await this.absorb(projects, local, localVaultRoot());
469
+ if (!localVaultCollidesWithMachineRoot() && isRegistrable(localVaultRoot())) {
470
+ await registerLocalProjectDir(dirname(localVaultRoot())).catch(() => {
471
+ });
472
+ }
473
+ }
474
+ for (const dir of await readLocalProjectDirs()) {
475
+ const root = localProjectVaultRoot(dir);
476
+ if (root === localVaultRoot()) continue;
477
+ const found = await readLocalProjectAt(root);
478
+ if (!found || projects.has(projectKey(found))) continue;
479
+ await this.absorb(projects, found, root);
480
+ }
481
+ let migrated = false;
482
+ for (const project of strayCloud) {
483
+ try {
484
+ await this.absorb(projects, project, vaultFor(project).root);
485
+ migrated = true;
486
+ } catch {
487
+ }
488
+ }
489
+ for (const { project, root } of await readCloudProjects()) {
490
+ await this.absorb(projects, project, root);
491
+ }
492
+ return { projects, migrated };
493
+ }
494
+ /**
495
+ * Persist every vault this store knows about.
496
+ *
497
+ * Writing all of them rather than only the one that changed keeps a project
498
+ * that moved between vaults — a local project adopted by an org, a cloud
499
+ * project pulled for the first time — from lingering in the vault it left.
500
+ */
501
+ save() {
502
+ return this.disk.save(() => this.writeVaults());
503
+ }
504
+ async writeVaults() {
505
+ const locals = [];
506
+ const cloud = [];
507
+ for (const project of this.projects.values()) {
508
+ if (!project.orgId) locals.push(project);
509
+ else cloud.push({ project, root: vaultFor(project).root });
510
+ }
511
+ const written = /* @__PURE__ */ new Map();
512
+ for (const local of locals) {
513
+ const root = vaultFor(local).root;
514
+ const other = written.get(root);
515
+ if (other) {
516
+ throw new Error(
517
+ `Refusing to save two local projects in ${root}: "${other.name}" and "${local.name}". A directory holds a single local project.`
518
+ );
519
+ }
520
+ written.set(root, local);
521
+ }
522
+ for (const [root, local] of written) {
523
+ await mkdir2(root, { recursive: true });
524
+ await ensureDataGitignore(root);
525
+ await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(local), null, 2));
526
+ this.localRoots.add(root);
527
+ }
528
+ for (const root of this.localRoots) {
529
+ if (written.has(root)) continue;
530
+ await rm(this.getFilePath(root), { force: true }).catch(() => {
531
+ });
532
+ if (isRegistrable(root)) await unregisterLocalProjectDir(dirname(root)).catch(() => {
533
+ });
534
+ this.localRoots.delete(root);
535
+ }
536
+ await rename2(join3(localVaultRoot(), LEGACY_FILENAME), join3(localVaultRoot(), LEGACY_FILENAME + ".bak")).catch(() => {
537
+ });
538
+ for (const { project, root } of cloud) {
539
+ await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
540
+ setProjectVault(project, root);
541
+ }
542
+ this.fingerprint = await this.diskFingerprint();
543
+ this.events.emit("saved");
544
+ }
545
+ /**
546
+ * The project a caller means, from a couple or from a bare id.
547
+ *
548
+ * A ref carrying its `orgId` — even an explicit `null` for a local project —
549
+ * names exactly one project and is looked up directly. A bare id is what a
550
+ * contained entity (an environment, a conversation) still carries, and it is
551
+ * resolved only while it answers for a single project: electing one of two
552
+ * would write into the wrong organization's vault.
553
+ */
554
+ resolveKey(target) {
555
+ if (typeof target !== "string" && target.orgId !== void 0) return projectKey(target);
556
+ const id = typeof target === "string" ? target : target.id;
557
+ const matches = Array.from(this.projects.values()).filter((p) => p.id === id);
558
+ if (matches.length > 1) {
559
+ throw new AmbiguousProjectError(id, matches.map((p) => p.orgId ?? null));
560
+ }
561
+ return matches[0] ? projectKey(matches[0]) : null;
562
+ }
563
+ find(target) {
564
+ const key = this.resolveKey(target);
565
+ const project = key ? this.projects.get(key) : void 0;
566
+ return key && project ? { key, project } : null;
567
+ }
568
+ /**
569
+ * Le projet déjà placé dans ce dossier, parmi ceux que ce poste connaît.
570
+ *
571
+ * Une entrée de `project-paths.json` peut survivre à son projet (supprimé
572
+ * ailleurs, ou avant que la suppression ne la nettoie). Elle ne réserve plus
573
+ * rien : le refus qui en découlait nommait un projet introuvable.
574
+ */
575
+ async projectAtPath(path, exclude) {
576
+ await this.load();
577
+ const ids = new Set(Array.from(this.projects.values(), (p) => p.id));
578
+ return projectPathStore.projectAt(path, exclude, (key) => this.projects.has(key) || ids.has(key));
579
+ }
580
+ async list() {
581
+ await this.load();
582
+ await this.settleActiveEnvironments();
583
+ return Array.from(this.projects.values());
584
+ }
585
+ async get(target) {
586
+ await this.load();
587
+ await this.settleActiveEnvironments();
588
+ return this.find(target)?.project ?? null;
589
+ }
590
+ /**
591
+ * Projets dont l'environnement actif vient d'un repli et n'a pas été vérifié.
592
+ *
593
+ * Au chargement, les environnements ne sont pas encore lisibles : leur magasin
594
+ * attend justement la fin de ce chargement. `absorb` retient donc le premier
595
+ * id de `environmentIds`, qui peut désigner un environnement disparu. Le
596
+ * sélecteur affichait alors cet id brut, absent de sa liste.
597
+ */
598
+ unsettled = /* @__PURE__ */ new Set();
599
+ settling = false;
600
+ /** Retenir, pour chaque projet non vérifié, un environnement qui existe. */
601
+ async settleActiveEnvironments() {
602
+ if (this.settling || this.unsettled.size === 0) return;
603
+ this.settling = true;
604
+ try {
605
+ for (const key of this.unsettled) {
606
+ const project = this.projects.get(key);
607
+ if (!project) {
608
+ this.unsettled.delete(key);
609
+ continue;
610
+ }
611
+ const envs = (await environmentStore.getByProjectId(project.id, project.orgId ?? null).catch(() => [])).filter((e) => e.scope !== "service");
612
+ if (envs.length === 0) continue;
613
+ const current = envs.find((e) => e.id === project.activeEnvironmentId);
614
+ const chosen = envs.find((e) => e.default) ?? current ?? envs[0];
615
+ project.activeEnvironmentId = chosen.id;
616
+ this.unsettled.delete(key);
617
+ }
618
+ } finally {
619
+ this.settling = false;
620
+ }
621
+ }
622
+ /**
623
+ * Vérifier qu'un dossier peut accueillir un projet local, et rendre son coffre.
624
+ *
625
+ * Refusé si son `.runeya` serait la racine machine, si le dossier est déjà
626
+ * l'emplacement d'un projet d'organisation, ou s'il porte déjà un projet.
627
+ */
628
+ async localRootFor(root) {
629
+ if (collidesWithMachineRoot(root)) throw new LocalProjectInMachineRootError();
630
+ if (await this.projectAtPath(dirname(root))) throw new LocalProjectCloudLinkedError();
631
+ const known = Array.from(this.projects.values()).find((p) => !p.orgId && vaultFor(p).root === root);
632
+ if (known) throw new LocalProjectExistsError(known.name);
633
+ const onDisk = await readLocalProjectAt(root);
634
+ if (onDisk) throw new LocalProjectExistsError(onDisk.name);
635
+ if (root !== localVaultRoot() && await holdsRuneyaProject(dirname(root))) throw new LocalProjectExistsError("");
636
+ return root;
637
+ }
638
+ /**
639
+ * `localDir` : le dossier d'un projet local. Omis, c'est le dossier de
640
+ * lancement, comme avant que les projets locaux aient chacun le leur.
641
+ */
642
+ async create(input, localDir) {
643
+ await this.load();
644
+ const localRoot = input.orgId ? null : await this.localRootFor(localDir ? localProjectVaultRoot(localDir) : localVaultRoot());
645
+ const project = {
646
+ id: randomUUID2(),
647
+ orgId: input.orgId ?? null,
648
+ name: input.name,
649
+ description: input.description ?? "",
650
+ serviceIds: input.serviceIds ?? [],
651
+ environmentIds: input.environmentIds ?? [],
652
+ activeEnvironmentId: input.activeEnvironmentId,
653
+ groupOrder: input.groupOrder ?? []
654
+ };
655
+ this.projects.set(projectKey(project), project);
656
+ if (localRoot) {
657
+ setLocalProjectRoot(project.id, localRoot);
658
+ if (isRegistrable(localRoot)) await registerLocalProjectDir(dirname(localRoot));
659
+ }
660
+ setProjectVault(project, vaultFor(project).root);
661
+ setProjectServices(project, project.serviceIds);
662
+ await this.save();
663
+ return project;
664
+ }
665
+ /**
666
+ * Ce qu'un dossier permet pour un projet local : en créer un (`available`),
667
+ * importer celui qu'il porte (`importable`), rien parce qu'il est déjà dans
668
+ * la liste (`listed`), ou rien parce qu'il est refusé.
669
+ */
670
+ async inspectLocalDir(dir) {
671
+ await this.load();
672
+ const root = localProjectVaultRoot(dir);
673
+ if (collidesWithMachineRoot(root)) return { status: "machine-root", projectName: null };
674
+ if (await this.projectAtPath(resolve2(dir))) return { status: "cloud-linked", projectName: null };
675
+ const known = Array.from(this.projects.values()).find((p) => !p.orgId && vaultFor(p).root === root);
676
+ if (known) return { status: "listed", projectName: known.name };
677
+ const onDisk = await readLocalProjectAt(root);
678
+ if (onDisk) return { status: "importable", projectName: onDisk.name };
679
+ if (await holdsRuneyaProject(dirname(root))) return { status: "unreadable", projectName: null };
680
+ return { status: "available", projectName: null };
681
+ }
682
+ /**
683
+ * Ajouter à la liste le projet local déjà présent dans ce dossier.
684
+ *
685
+ * Son `.runeya` n'est pas touché : le dossier est seulement enregistré, et le
686
+ * projet relu comme les autres.
687
+ */
688
+ async importLocal(dir) {
689
+ await this.load();
690
+ const root = localProjectVaultRoot(dir);
691
+ if (collidesWithMachineRoot(root)) throw new LocalProjectInMachineRootError();
692
+ if (await this.projectAtPath(resolve2(dir))) throw new LocalProjectCloudLinkedError();
693
+ const found = await readLocalProjectAt(root);
694
+ if (!found) throw new LocalProjectNotFoundError();
695
+ const known = this.projects.get(projectKey(found));
696
+ if (known) return known;
697
+ await registerLocalProjectDir(dirname(root));
698
+ this.loaded = false;
699
+ await this.load();
700
+ return this.projects.get(projectKey(found)) ?? found;
701
+ }
702
+ /**
703
+ * `input.orgId` names which project is being updated, it does not move it:
704
+ * a project is born local or in an organization and stays there, so treating
705
+ * it as a new value would silently relocate a vault instead of editing it.
706
+ */
707
+ async update(input) {
708
+ await this.load();
709
+ const found = this.find(input);
710
+ if (!found) return null;
711
+ const { key, project: existing } = found;
712
+ const updated = {
713
+ ...existing,
714
+ ...input.name !== void 0 && { name: input.name },
715
+ ...input.description !== void 0 && { description: input.description },
716
+ ...input.serviceIds !== void 0 && { serviceIds: input.serviceIds },
717
+ ...input.environmentIds !== void 0 && { environmentIds: input.environmentIds },
718
+ ...input.activeEnvironmentId !== void 0 && { activeEnvironmentId: input.activeEnvironmentId },
719
+ ...input.groupOrder !== void 0 && { groupOrder: input.groupOrder }
720
+ };
721
+ recordDetached(existing.id, "serviceIds", existing.serviceIds ?? [], updated.serviceIds ?? []);
722
+ recordDetached(existing.id, "environmentIds", existing.environmentIds ?? [], updated.environmentIds ?? []);
723
+ this.projects.set(key, updated);
724
+ setProjectVault(updated, vaultFor(updated).root);
725
+ setProjectServices(updated, updated.serviceIds);
726
+ await this.save();
727
+ return updated;
728
+ }
729
+ /**
730
+ * Faire passer le projet local de ce dossier dans une organisation.
731
+ *
732
+ * Le seul déplacement de coffre qu'on s'autorise, et il ne va que dans ce
733
+ * sens : le projet quitte `<cwd>/.runeya` pour
734
+ * `~/.runeya/orgs/<org>/projects/<id>/`, en gardant son identifiant. Il n'y a
735
+ * donc jamais deux copies à réconcilier — c'est ce qui rendait « publier »
736
+ * dangereux tant que le projet restait aussi local.
737
+ *
738
+ * Refusé si l'organisation porte déjà ce projet : la copie d'ici écraserait
739
+ * celle du cloud, et ces deux histoires ne sont pas à nous de les fusionner.
740
+ *
741
+ * Ce qui suit le projet est écrit par les stores eux-mêmes : `save()` réécrit
742
+ * chaque coffre — le fichier local disparaît, celui de l'org apparaît — puis
743
+ * les contenus sont relogés depuis le registre, qui pointe désormais ailleurs.
744
+ */
745
+ async moveToOrg(projectId, orgId) {
746
+ await this.load();
747
+ const local = this.projects.get(projectKey({ id: projectId, orgId: null }));
748
+ if (!local) throw new Error("PROJECT_NOT_LOCAL");
749
+ if (this.projects.get(projectKey({ id: projectId, orgId }))) {
750
+ throw new Error("PROJECT_ALREADY_IN_ORG");
751
+ }
752
+ const localRoot = vaultFor(local).root;
753
+ const localDir = localRoot === localVaultRoot() ? null : dirname(localRoot);
754
+ const moved = { ...local, orgId };
755
+ this.projects.delete(projectKey(local));
756
+ forgetProject(local);
757
+ forgetLocalProjectRoot(local.id);
758
+ this.projects.set(projectKey(moved), moved);
759
+ setProjectVault(moved, vaultFor(moved).root);
760
+ setProjectServices(moved, moved.serviceIds ?? []);
761
+ await this.save();
762
+ if (localDir && !await projectPathStore.get(moved)) {
763
+ await projectPathStore.set(moved, localDir);
764
+ }
765
+ await serviceStore.reassignOrg(moved.serviceIds ?? [], orgId);
766
+ await environmentStore.relocateAll();
767
+ return moved;
768
+ }
769
+ async delete(target) {
770
+ await this.load();
771
+ const found = this.find(target);
772
+ if (!found) return false;
773
+ const { key, project } = found;
774
+ this.projects.delete(key);
775
+ await this.save();
776
+ await this.discardVault(project);
777
+ await projectPathStore.clear(project);
778
+ await conversationSectionStore.forgetProject(project.id);
779
+ return true;
780
+ }
781
+ /**
782
+ * Remove what a deleted cloud project left behind.
783
+ *
784
+ * Its vault is a directory of its own, so dropping the project means dropping
785
+ * the directory — otherwise `readCloudVaults` would resurrect it on the next
786
+ * load. Local projects share their vault with the directory's own files and
787
+ * are only removed from the list.
788
+ */
789
+ async discardVault(project) {
790
+ forgetProject(project);
791
+ if (!project.orgId) forgetLocalProjectRoot(project.id);
792
+ if (!project.orgId) return;
793
+ await rm(vaultFor(project).root, { recursive: true, force: true }).catch(() => {
794
+ });
795
+ }
796
+ /**
797
+ * Stop holding a project on this machine, leaving the doc alone.
798
+ *
799
+ * Memory only: the caller removes the vault itself. Its contents are dropped
800
+ * too, because a service or environment surviving in memory would be written
801
+ * back on the next save — and, its project being unknown by then, into the
802
+ * local vault.
803
+ */
804
+ async forgetLocally(target) {
805
+ await this.load();
806
+ const found = this.find(target);
807
+ if (!found) return;
808
+ const { key, project } = found;
809
+ this.projects.delete(key);
810
+ forgetProject(project);
811
+ await serviceStore.forgetLocally(project.serviceIds ?? []);
812
+ await environmentStore.forgetByProject(project.id);
813
+ await projectPathStore.clear(project);
814
+ }
815
+ /**
816
+ * Le projet qui porte un service, ou `null` s'il n'est rattaché à aucun.
817
+ *
818
+ * Un service n'appartient qu'à un projet : la recherche s'arrête au premier.
819
+ */
820
+ async ofService(serviceId) {
821
+ await this.load();
822
+ for (const project of this.projects.values()) {
823
+ if (project.serviceIds.includes(serviceId)) return project;
824
+ }
825
+ return null;
826
+ }
827
+ /** All projects linked to a given organization (for the sync bridge). */
828
+ async listByOrg(orgId) {
829
+ await this.load();
830
+ return Array.from(this.projects.values()).filter((p) => p.orgId === orgId);
831
+ }
832
+ /** Projects of an organization in their on-disk form (what the cloud mirrors). */
833
+ async listDiskFormByOrg(orgId) {
834
+ return (await this.listByOrg(orgId)).map(toDiskForm);
835
+ }
836
+ /**
837
+ * Apply a project received from the cloud, in its on-disk form.
838
+ *
839
+ * Validated rather than trusted: a peer on another version — or a tampered
840
+ * doc — must not be able to write arbitrary shapes into `projects.json`.
841
+ * `activeEnvironmentId` is runtime-only and stays whatever it is locally.
842
+ * Diff-guarded: an identical project neither saves nor emits (loop-safe).
843
+ * Returns whether a new local project was created, so the caller can
844
+ * bootstrap project-local state (e.g. a default environment).
845
+ */
846
+ async applyRemoteUpsert(id, fields) {
847
+ await this.load();
848
+ const parsed = ProjectSchema.safeParse({ ...fields, id });
849
+ if (!parsed.success) {
850
+ console.error("[project-store] Rejected remote project %s:", id, parsed.error.message);
851
+ return null;
852
+ }
853
+ const incoming = toDiskForm(parsed.data);
854
+ const key = projectKey(parsed.data);
855
+ const existing = this.projects.get(key);
856
+ if (existing) {
857
+ if (JSON.stringify(toDiskForm(existing)) === JSON.stringify(incoming)) {
858
+ return { project: existing, created: false };
859
+ }
860
+ const updated = { ...incoming, activeEnvironmentId: existing.activeEnvironmentId };
861
+ this.projects.set(key, updated);
862
+ setProjectVault(updated, vaultFor(updated).root);
863
+ setProjectServices(updated, updated.serviceIds);
864
+ await this.save();
865
+ return { project: updated, created: false };
866
+ }
867
+ const project = { ...incoming, activeEnvironmentId: void 0 };
868
+ this.projects.set(key, project);
869
+ setProjectVault(project, vaultFor(project).root);
870
+ setProjectServices(project, project.serviceIds);
871
+ await this.save();
872
+ return { project, created: true };
873
+ }
874
+ /** Delete a project that was removed on another peer. */
875
+ async applyRemoteDelete(target) {
876
+ await this.load();
877
+ const found = this.find(target);
878
+ if (!found) return false;
879
+ const { key, project } = found;
880
+ this.projects.delete(key);
881
+ await this.save();
882
+ await this.discardVault(project);
883
+ await projectPathStore.clear(project);
884
+ return true;
885
+ }
886
+ async addService(target, serviceId) {
887
+ await this.load();
888
+ const found = this.find(target);
889
+ if (!found) return null;
890
+ const { key, project } = found;
891
+ if (project.serviceIds.includes(serviceId)) return project;
892
+ const updated = { ...project, serviceIds: [...project.serviceIds, serviceId] };
893
+ this.projects.set(key, updated);
894
+ setProjectServices(updated, updated.serviceIds);
895
+ await this.save();
896
+ return updated;
897
+ }
898
+ async removeService(target, serviceId) {
899
+ await this.load();
900
+ const found = this.find(target);
901
+ if (!found) return null;
902
+ const { key, project } = found;
903
+ const updated = { ...project, serviceIds: project.serviceIds.filter((id) => id !== serviceId) };
904
+ recordDetached(project.id, "serviceIds", project.serviceIds, updated.serviceIds);
905
+ this.projects.set(key, updated);
906
+ setProjectServices(updated, updated.serviceIds);
907
+ await this.save();
908
+ return updated;
909
+ }
910
+ async addEnvironment(target, environmentId) {
911
+ await this.load();
912
+ const found = this.find(target);
913
+ if (!found) return null;
914
+ const { key, project } = found;
915
+ if (project.environmentIds?.includes(environmentId)) return project;
916
+ const updated = {
917
+ ...project,
918
+ environmentIds: [...project.environmentIds ?? [], environmentId]
919
+ };
920
+ this.projects.set(key, updated);
921
+ await this.save();
922
+ return updated;
923
+ }
924
+ async removeEnvironment(target, environmentId) {
925
+ await this.load();
926
+ const found = this.find(target);
927
+ if (!found) return null;
928
+ const { key, project } = found;
929
+ const updated = {
930
+ ...project,
931
+ environmentIds: (project.environmentIds ?? []).filter((id) => id !== environmentId),
932
+ // Clear activeEnvironmentId if it was the removed environment
933
+ ...project.activeEnvironmentId === environmentId && { activeEnvironmentId: void 0 }
934
+ };
935
+ recordDetached(project.id, "environmentIds", project.environmentIds ?? [], updated.environmentIds);
936
+ this.projects.set(key, updated);
937
+ await this.save();
938
+ return updated;
939
+ }
940
+ async setActiveEnvironment(target, environmentId) {
941
+ await this.load();
942
+ const found = this.find(target);
943
+ if (!found) return null;
944
+ const { key, project } = found;
945
+ const updated = {
946
+ ...project,
947
+ activeEnvironmentId: environmentId
948
+ };
949
+ this.projects.set(key, updated);
950
+ this.unsettled.delete(key);
951
+ return updated;
952
+ }
953
+ };
954
+ var projectStore = new ProjectStore();
955
+ registerProjectLoader(() => projectStore.list().then(() => void 0));
956
+
957
+ export {
958
+ ensureDataGitignore,
959
+ conversationSectionStore,
960
+ holdsRuneyaProject,
961
+ projectPathStore,
962
+ LocalProjectRefusedError,
963
+ LocalProjectExistsError,
964
+ LocalProjectInMachineRootError,
965
+ LocalProjectCloudLinkedError,
966
+ LocalProjectNotFoundError,
967
+ projectStore
968
+ };
969
+ //# sourceMappingURL=chunk-7QXP4FXD.js.map