@runeya/runeya 2.0.144 → 2.0.146

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 (148) hide show
  1. package/agent/index.js +33 -22
  2. package/agent/index.js.map +1 -1
  3. package/agent-manager-R7BPQO6D.js +11 -0
  4. package/chat-LPML22J5.js +24 -0
  5. package/{chunk-7L6AQUBR.js → chunk-4LJQIQMN.js} +6 -2
  6. package/{chunk-7L6AQUBR.js.map → chunk-4LJQIQMN.js.map} +1 -1
  7. package/{chunk-7HIPFSOA.js → chunk-57P45FNY.js} +11 -3
  8. package/chunk-57P45FNY.js.map +1 -0
  9. package/chunk-6IBPK6UE.js +1063 -0
  10. package/chunk-6IBPK6UE.js.map +1 -0
  11. package/{chunk-GXE5TLLJ.js → chunk-KV4GJKXU.js} +17 -17
  12. package/{chunk-XAFOP677.js → chunk-MZSDTA4A.js} +480 -233
  13. package/chunk-MZSDTA4A.js.map +1 -0
  14. package/{chunk-UR3HL373.js → chunk-NPMSBYXC.js} +8 -8
  15. package/{chunk-P73PTVII.js → chunk-SEPAURTQ.js} +369 -1282
  16. package/chunk-SEPAURTQ.js.map +1 -0
  17. package/{chunk-NSS47H26.js → chunk-UD42LACH.js} +348 -78
  18. package/chunk-UD42LACH.js.map +1 -0
  19. package/{chunk-IXO5KIO5.js → chunk-VPSQJOSN.js} +23 -2
  20. package/{chunk-IXO5KIO5.js.map → chunk-VPSQJOSN.js.map} +1 -1
  21. package/{companion-2LHKU577.js → companion-W3UMZWGH.js} +11 -7
  22. package/{desktop-installer-page-6JDSXK2E.js → desktop-installer-page-I5FHFDES.js} +11 -8
  23. package/desktop-installer-page-I5FHFDES.js.map +1 -0
  24. package/{desktop-launcher-YPNRQMIS.js → desktop-launcher-GD4QPM66.js} +3 -3
  25. package/external-NMKQPROK.js +387 -0
  26. package/external-NMKQPROK.js.map +1 -0
  27. package/index.js +34 -12
  28. package/index.js.map +1 -1
  29. package/{install-gate-2NNH7RM2.js → install-gate-PVMGJDWE.js} +2 -2
  30. package/mcp-server/external.d.ts +44 -0
  31. package/mcp-server/external.js +2679 -0
  32. package/mcp-server/external.js.map +1 -0
  33. package/mcp-server/index.js +142 -40
  34. package/mcp-server/index.js.map +1 -1
  35. package/mcp-server/launch.d.ts +19 -1
  36. package/mcp-server/launch.js +20 -0
  37. package/mcp-server/launch.js.map +1 -1
  38. package/package.json +1 -1
  39. package/settings-manager-XPLBO35P.js +10 -0
  40. package/src-DCCEUWE6.js +385 -0
  41. package/{src-5TEXF3SU.js → src-LEDGW5KP.js} +1029 -536
  42. package/src-LEDGW5KP.js.map +1 -0
  43. package/{vault-resolver-VK6W5J6W.js → vault-resolver-E7KPKA5C.js} +3 -2
  44. package/vault-resolver-E7KPKA5C.js.map +1 -0
  45. package/web/assets/{AgentScalarChatInterface.vue-B-SjOsdj.js → AgentScalarChatInterface.vue-BYszptaB.js} +1 -1
  46. package/web/assets/{AgentTerminalPage-JzC8DUgu.js → AgentTerminalPage-C3TJ1NL9.js} +1 -1
  47. package/web/assets/{AiChatModals-D1928AxG.js → AiChatModals-D3scRE3S.js} +1 -1
  48. package/web/assets/AppLayout-BMQUjPKe.js +1 -0
  49. package/web/assets/{AppLayout-8sBPr3od.css → AppLayout-tsw5Ow0w.css} +1 -1
  50. package/web/assets/{AuditPage-BZIJ3_Kr.js → AuditPage-CKcExBws.js} +1 -1
  51. package/web/assets/{CloudAccountPage-DBv470sT.js → CloudAccountPage-B6oPg0BZ.js} +1 -1
  52. package/web/assets/{CloudMessagingPage-qCI8-UDL.js → CloudMessagingPage-C5fwVe-F.js} +1 -1
  53. package/web/assets/DashboardPage-9RV7sx3e.js +1 -0
  54. package/web/assets/{DashboardPage-D9-PpC3F.css → DashboardPage-FvNuC_e4.css} +1 -1
  55. package/web/assets/{DirectoryPickerDialog-B5DvDfQP.js → DirectoryPickerDialog-C06BXyfJ.js} +1 -1
  56. package/web/assets/{EnvironmentForm-Bw8Djnqm.js → EnvironmentForm-CAvDoira.js} +1 -1
  57. package/web/assets/{EnvironmentFormPage-B59uiYtp.js → EnvironmentFormPage-BmAxhN-m.js} +1 -1
  58. package/web/assets/{EnvironmentListPage-W9zLDZAg.js → EnvironmentListPage-B8uATPBI.js} +1 -1
  59. package/web/assets/KanbanBoardPage-CQsOkRUf.js +4 -0
  60. package/web/assets/KanbanBoardPage-eHmsrFGA.css +1 -0
  61. package/web/assets/{KanbanListPage-Bn9k8Rpc.js → KanbanListPage-CWZqkjD6.js} +1 -1
  62. package/web/assets/{LaunchProjectPage-Dc7v1UjQ.js → LaunchProjectPage-saSmC7Pr.js} +1 -1
  63. package/web/assets/LaunchServicesPage-BkKyLMEz.js +1 -0
  64. package/web/assets/LaunchServicesPage-DoLDPqQq.css +1 -0
  65. package/web/assets/{LaunchShell-LonTJpb3.js → LaunchShell-DUrSunB4.js} +1 -1
  66. package/web/assets/{LogViewer-DkuqWYkQ.js → LogViewer-CZBYkH9J.js} +1 -1
  67. package/web/assets/{OrgKeyPrompt-By1VjlSN.js → OrgKeyPrompt-DzQ7pwbs.js} +1 -1
  68. package/web/assets/{ProcessControls-C0skzKGL.js → ProcessControls-BEeoNlNV.js} +1 -1
  69. package/web/assets/{ProcessDetailPage-BvPX6z_X.js → ProcessDetailPage-CQgu5uKm.js} +1 -1
  70. package/web/assets/{ProjectCreateDialog-C4o9u15L.js → ProjectCreateDialog-BolrhRD-.js} +1 -1
  71. package/web/assets/{ProjectLocationGate-BJ1CSQt0.js → ProjectLocationGate-DhMVdIjN.js} +1 -1
  72. package/web/assets/{ProjectPublishDialog-Cp-zXdGm.js → ProjectPublishDialog-DsZNWrfz.js} +1 -1
  73. package/web/assets/{RandomJobTestPage-DF4a1U13.js → RandomJobTestPage-BUS2Ty6-.js} +1 -1
  74. package/web/assets/ScenarioEditorPage-MIAGh2us.js +14 -0
  75. package/web/assets/{SendToChatDialog-Dg2SHkVD.js → SendToChatDialog-BxWsyv29.js} +1 -1
  76. package/web/assets/{ServiceControlsBar-q2VHWaxK.js → ServiceControlsBar-DqDPAZt7.js} +1 -1
  77. package/web/assets/ServiceDetailPage-hkSAzZ-l.js +2 -0
  78. package/web/assets/{ServiceFormPage-tT_n7WUy.js → ServiceFormPage-DXqhqXg8.js} +1 -1
  79. package/web/assets/SettingsPage-4pKUgKPm.css +1 -0
  80. package/web/assets/SettingsPage-DS2YhMIM.js +2086 -0
  81. package/web/assets/{UnlockPage-BKAdMJkb.js → UnlockPage-s8SgVcKk.js} +1 -1
  82. package/web/assets/{VaultMigrationPage-D9KUTTss.js → VaultMigrationPage-s0b44MDM.js} +1 -1
  83. package/web/assets/{VersionBadge-moKP5bAv.js → VersionBadge-BmArk24p.js} +1 -1
  84. package/web/assets/{WikiPage-hLUndDxo.js → WikiPage-DADu8WUA.js} +1 -1
  85. package/web/assets/{WikiView-rrjXRR72.js → WikiView-DtGwIt_Q.js} +1 -1
  86. package/web/assets/{add-scalar-classes-CBfRr8B2.js → add-scalar-classes-dWEWPlFD.js} +2 -2
  87. package/web/assets/{agent.store-B3dVemmT.js → agent.store-CKLAkF9a.js} +1 -1
  88. package/web/assets/app-mode-BVpTuh0i.js +1 -0
  89. package/web/assets/auth-Cg9BWTfK.js +2 -0
  90. package/web/assets/{cloud-chat.store-Wg1poFem.js → cloud-chat.store-b-BHGz7I.js} +1 -1
  91. package/web/assets/{cloud-client-DQlUaoAw.js → cloud-client-D_zCiW0s.js} +2 -2
  92. package/web/assets/{cloud-session-DRUqcSGP.js → cloud-session-DOaDpZTy.js} +2 -2
  93. package/web/assets/dist-N4nrnbnA.js +1 -0
  94. package/web/assets/{editor.store-SoH48ux2.js → editor.store-DCAZSeVG.js} +1 -1
  95. package/web/assets/i18n-D0afQEGs.js +1 -0
  96. package/web/assets/{index-D_x2s8Ut.js → index-D8VRbyoP.js} +101 -101
  97. package/web/assets/index-DsezNbSM.css +1 -0
  98. package/web/assets/{modal-CpsVK_dA.js → modal-qYgTMkYb.js} +1 -1
  99. package/web/assets/{project-launch.store-C3fZeRGb.js → project-launch.store-y7JBMNiE.js} +1 -1
  100. package/web/assets/{project.store-BEk4fciH.js → project.store-DGJr0Qr9.js} +1 -1
  101. package/web/assets/{scenario.store-D-JrRiVO.js → scenario.store-YWHL2_wu.js} +3 -3
  102. package/web/assets/{service.store-B5uYZtyM.js → service.store-5jWsJxPx.js} +1 -1
  103. package/web/assets/{settings-scope-Blnwj3rR.js → settings-scope-Bv7QGPrt.js} +1 -1
  104. package/web/assets/{settings.store-Bo3FYs1o.js → settings.store-bEsB7oKb.js} +1 -1
  105. package/web/assets/{src-CAwAaB5j.js → src-C-D_y7ou.js} +1 -1
  106. package/web/assets/{src-DValZNJU.js → src-Dnwk6NpV.js} +5 -5
  107. package/web/assets/src-g1DRo8xZ.css +1 -0
  108. package/web/assets/{trpc-DbsE5GvQ.js → trpc-Drabzwpd.js} +3 -3
  109. package/web/assets/{useCloudOrg-DygUBG1a.js → useCloudOrg-BAQEtFPl.js} +1 -1
  110. package/web/assets/{useEnvVarAutocomplete-DmYNOXgd.js → useEnvVarAutocomplete-BqGTODUv.js} +1 -1
  111. package/web/assets/{useProjectCatalog-D2mHB7dg.js → useProjectCatalog-yocjIKt6.js} +1 -1
  112. package/web/assets/{useProjectCloudOrg-avTCVold.js → useProjectCloudOrg-CEN9-_OA.js} +1 -1
  113. package/web/assets/{useServiceUrlHost-Q3fzGox3.js → useServiceUrlHost-BuoPEXZX.js} +1 -1
  114. package/web/assets/{vue-draggable-plus-WgrMXXmE.js → vue-draggable-plus-DX4rTBiu.js} +1 -1
  115. package/web/index.html +19 -18
  116. package/agent-manager-M4NA5YFD.js +0 -11
  117. package/chat-X3X527EX.js +0 -23
  118. package/chunk-7HIPFSOA.js.map +0 -1
  119. package/chunk-NSS47H26.js.map +0 -1
  120. package/chunk-P73PTVII.js.map +0 -1
  121. package/chunk-XAFOP677.js.map +0 -1
  122. package/desktop-installer-page-6JDSXK2E.js.map +0 -1
  123. package/settings-manager-32SVMAFN.js +0 -10
  124. package/src-5TEXF3SU.js.map +0 -1
  125. package/web/assets/AppLayout-CVQIBYqg.js +0 -1
  126. package/web/assets/DashboardPage-D0UZM0mr.js +0 -1
  127. package/web/assets/KanbanBoardPage-DRACMk_D.css +0 -1
  128. package/web/assets/KanbanBoardPage-DwPqoJDJ.js +0 -4
  129. package/web/assets/LaunchServicesPage-CXUS9O3y.js +0 -1
  130. package/web/assets/LaunchServicesPage-DJDIN8HU.css +0 -1
  131. package/web/assets/ScenarioEditorPage-Dl-gEkJI.js +0 -14
  132. package/web/assets/ServiceDetailPage-CJ8WaFod.js +0 -2
  133. package/web/assets/SettingsPage-CWakkv0V.js +0 -2086
  134. package/web/assets/SettingsPage-aGSxZw1n.css +0 -1
  135. package/web/assets/auth-BCrvnP1U.js +0 -2
  136. package/web/assets/dist-DoChtT9t.js +0 -1
  137. package/web/assets/i18n-BszFg0ko.js +0 -1
  138. package/web/assets/index-C_JV7fYy.css +0 -1
  139. package/web/assets/src-DuBXN1hu.css +0 -1
  140. /package/{agent-manager-M4NA5YFD.js.map → agent-manager-R7BPQO6D.js.map} +0 -0
  141. /package/{chat-X3X527EX.js.map → chat-LPML22J5.js.map} +0 -0
  142. /package/{chunk-GXE5TLLJ.js.map → chunk-KV4GJKXU.js.map} +0 -0
  143. /package/{chunk-UR3HL373.js.map → chunk-NPMSBYXC.js.map} +0 -0
  144. /package/{companion-2LHKU577.js.map → companion-W3UMZWGH.js.map} +0 -0
  145. /package/{desktop-launcher-YPNRQMIS.js.map → desktop-launcher-GD4QPM66.js.map} +0 -0
  146. /package/{install-gate-2NNH7RM2.js.map → install-gate-PVMGJDWE.js.map} +0 -0
  147. /package/{settings-manager-32SVMAFN.js.map → settings-manager-XPLBO35P.js.map} +0 -0
  148. /package/{vault-resolver-VK6W5J6W.js.map → src-DCCEUWE6.js.map} +0 -0
@@ -1,3 +1,7 @@
1
+ import {
2
+ LAUNCHER_PROJECT_ID_PATTERN
3
+ } from "./chunk-MZSDTA4A.js";
4
+
1
5
  // ../server/src/services/vault-resolver.ts
2
6
  import { join, sep, resolve } from "path";
3
7
  import { homedir } from "os";
@@ -31,6 +35,12 @@ var EnvSchema = z.object({
31
35
  // terminal. The web app surfaces it so the user knows which Runeya they are
32
36
  // looking at when several are running.
33
37
  RUNEYA_FROM_DESKTOP: z.preprocess((v) => v === "true" || v === "1", z.boolean()).default(false),
38
+ // `--project <id>` : l'instance n'ouvre que ce projet. L'interface va droit
39
+ // au choix des services et ne propose ni la liste ni le changement de projet.
40
+ RUNEYA_PROJECT_ID: z.preprocess(
41
+ (v) => typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0,
42
+ z.string().regex(LAUNCHER_PROJECT_ID_PATTERN, "RUNEYA_PROJECT_ID: expected a project id").nullable().default(null)
43
+ ),
34
44
  // Label shown for this machine in the cloud's "linked machines" screen.
35
45
  // Defaults to the hostname (see `getDeviceName`); set it explicitly when a
36
46
  // single host runs several instances and you want to tell them apart.
@@ -43,6 +53,15 @@ var EnvSchema = z.object({
43
53
  RUNEYA_JWT_TTL: z.preprocess(
44
54
  (v) => typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0,
45
55
  z.string().regex(/^[1-9]\d{0,5}(s|m|h|d)$/, "RUNEYA_JWT_TTL: expected e.g. 4s, 30m, 8h, 1d").default("8h")
56
+ ),
57
+ // La commande écrite dans la config des outils IA pour lancer le MCP, à la
58
+ // place de `runeya` (voir `services/ai-client-mcp.ts`). Pour le dev : faire
59
+ // pointer ces outils vers le CLI du dépôt sans toucher au `runeya` du PATH,
60
+ // par exemple `node /chemin/vers/apps/cli/dist/index.js`. Découpée sur les
61
+ // espaces : un chemin qui en contient n'est pas pris en charge.
62
+ RUNEYA_MCP_COMMAND: z.preprocess(
63
+ (v) => typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0,
64
+ z.string().max(4096).optional()
46
65
  )
47
66
  }).transform((data) => ({
48
67
  ...data,
@@ -66,7 +85,9 @@ var EnvSchema = z.object({
66
85
  AGENT_BINARY_PATH: data.RUNEYA_AGENT_BINARY_PATH,
67
86
  DEVICE_NAME: data.RUNEYA_DEVICE_NAME,
68
87
  FROM_DESKTOP: data.RUNEYA_FROM_DESKTOP,
69
- JWT_TTL: data.RUNEYA_JWT_TTL
88
+ PROJECT_ID: data.RUNEYA_PROJECT_ID,
89
+ JWT_TTL: data.RUNEYA_JWT_TTL,
90
+ MCP_COMMAND: data.RUNEYA_MCP_COMMAND
70
91
  }));
71
92
  var env = EnvSchema.parse(process.env);
72
93
 
@@ -414,4 +435,4 @@ export {
414
435
  legacyVaultRoot,
415
436
  resetKeyRoot
416
437
  };
417
- //# sourceMappingURL=chunk-IXO5KIO5.js.map
438
+ //# sourceMappingURL=chunk-VPSQJOSN.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../server/src/services/vault-resolver.ts","../../server/src/config/env.ts"],"sourcesContent":["import { join, sep, resolve } from 'node:path';\nimport { homedir } from 'node:os';\nimport { readdir, mkdir, access, readFile, writeFile, unlink, rename } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { env } from '../config/env.js';\n\n/**\n * Where a project's files live, and which key protects them.\n *\n * Runeya holds two kinds of projects, and they do not belong in the same place.\n * A local project *is* a directory on this machine: its data belongs next to\n * the code, in `<cwd>/.runeya`. A cloud project belongs to an organization and\n * is materialized from the doc; tying it to whichever directory happened to\n * launch the server is what made the same project look \"already here\" in one\n * folder and \"to download\" in another. Cloud projects therefore live under the\n * user's home, prefixed by organization and by project.\n *\n * A vault holds a project's own data — project.json, services, environments.\n * It does NOT hold the key that protects it: one key answers for the whole\n * machine (see `keyRoot` at the bottom of this file), which is what the cloud's\n * org-wide canary already assumes.\n */\n\nexport interface Vault {\n /** Directory holding this project's own files. */\n root: string;\n}\n\n/** Identifies a project well enough to place it — the stored shape, or the doc's. */\nexport interface VaultTarget {\n id: string;\n orgId?: string | null;\n}\n\n/**\n * Ids reach us from the cloud, and they end up in filesystem paths. Anything\n * outside this alphabet — a separator, a `..`, a NUL — could walk out of the\n * home directory, so it is rejected rather than sanitized: a mangled id would\n * silently point at the wrong vault.\n */\nconst SAFE_ID = /^[A-Za-z0-9._-]{1,128}$/;\n\nfunction assertSafeId(kind: 'organization' | 'project', id: string): void {\n if (!SAFE_ID.test(id) || id === '.' || id === '..') {\n throw new Error(`Invalid ${kind} id for a vault path: ${JSON.stringify(id)}`);\n }\n}\n\n/**\n * Everything that belongs to this machine rather than to a project.\n *\n * Read from `process.env` rather than the parsed config: tests that mock\n * `config/env.js` would otherwise lose the override and write cloud vaults into\n * the developer's real home directory.\n */\nexport function machineRoot(): string {\n const override = process.env['RUNEYA_HOME'];\n return join(override && override.length > 0 ? override : homedir(), '.runeya');\n}\n\n/**\n * The local vault: the `.runeya` of the directory the server was launched in.\n *\n * Kept on `env.DATA_DIR` so `--data-dir` and the tests keep working. That value\n * is already absolute, resolved once at boot — re-resolving it here would make\n * the vault follow the process's cwd, which is exactly what it must not do.\n */\nexport function localVaultRoot(): string {\n return env.DATA_DIR;\n}\n\n/**\n * Le coffre local se confond-il avec la racine machine, ou se range-t-il dedans ?\n *\n * Runeya lancée depuis le dossier qui abrite son propre `.runeya` — le home,\n * typiquement — y verrait son coffre local : les fichiers d'un projet local\n * iraient alors se mêler aux coffres d'organisation, aux passphrases d'agent et\n * au reste de ce qui appartient à la machine, dans un seul répertoire. Deux\n * natures de données que tout sépare partageraient leur rangement, et supprimer\n * l'une emporterait l'autre.\n */\nexport function localVaultCollidesWithMachineRoot(): boolean {\n return collidesWithMachineRoot(localVaultRoot());\n}\n\n/** The vault of an organization: its projects, and its lock. */\nexport function orgVaultRoot(orgId: string): string {\n assertSafeId('organization', orgId);\n return join(machineRoot(), 'orgs', orgId);\n}\n\n/** The vault of one cloud project, under its organization. */\nexport function projectVaultRoot(orgId: string, projectId: string): string {\n assertSafeId('project', projectId);\n return join(orgVaultRoot(orgId), 'projects', projectId);\n}\n\n/**\n * The organization a vault path belongs to, or null for the local vault.\n *\n * The layout is the authority on ownership — `~/.runeya/orgs/<org>/projects/<id>`\n * says both which project and which organization — so an entity read from a\n * vault carries its couple in the path it came from, even though its own file\n * only ever names its project.\n */\nexport function orgIdOfRoot(root: string): string | null {\n const marker = join(machineRoot(), 'orgs') + sep;\n if (!root.startsWith(marker)) return null;\n const [orgId] = root.slice(marker.length).split(sep);\n return orgId && SAFE_ID.test(orgId) ? orgId : null;\n}\n\n/**\n * La clé sous laquelle une donnée de ce coffre est chiffrée.\n *\n * L'identifiant d'organisation quand le coffre lui appartient, `undefined` pour\n * la clé machine. Le rangement décide donc du chiffrement, sans qu'aucune\n * entité ait à porter un champ de plus : le coffre où on l'écrit dit déjà à qui\n * elle est.\n */\nexport function vaultIdOfRoot(root: string): string | undefined {\n return orgIdOfRoot(root) ?? undefined;\n}\n\n// ─── Projets locaux ─────────────────────────────────────────────────────────\n\n/**\n * Les dossiers des projets locaux de cette machine.\n *\n * Un projet local vit dans le `.runeya` de son dossier. Il y en a un par\n * dossier, et autant de dossiers qu'on en importe ou crée depuis l'interface.\n * Ce fichier de la racine machine en garde la liste, pour que chaque instance\n * de Runeya les voie, quel que soit le dossier d'où elle est lancée.\n */\nconst LOCAL_PROJECTS_FILE = 'local-projects.json';\n\nfunction localProjectsFilePath(): string {\n return join(machineRoot(), LOCAL_PROJECTS_FILE);\n}\n\n/** Le coffre d'un projet local : le `.runeya` de son dossier. */\nexport function localProjectVaultRoot(dir: string): string {\n return join(resolve(dir), '.runeya');\n}\n\n/** Les dossiers enregistrés, absolus, sans doublon. */\nexport async function readLocalProjectDirs(): Promise<string[]> {\n try {\n const parsed = JSON.parse(await readFile(localProjectsFilePath(), 'utf-8')) as { dirs?: unknown };\n const dirs = Array.isArray(parsed.dirs) ? parsed.dirs.filter((d): d is string => typeof d === 'string') : [];\n return [...new Set(dirs.map((d) => resolve(d)))];\n } catch {\n return [];\n }\n}\n\nasync function writeLocalProjectDirs(dirs: string[]): Promise<void> {\n await mkdir(machineRoot(), { recursive: true });\n const path = localProjectsFilePath();\n await writeFile(`${path}.tmp`, JSON.stringify({ dirs }, null, 2), 'utf-8');\n await rename(`${path}.tmp`, path);\n}\n\n/** Ajouter un dossier à la liste. Sans effet s'il y est déjà. */\nexport async function registerLocalProjectDir(dir: string): Promise<void> {\n const target = resolve(dir);\n const dirs = await readLocalProjectDirs();\n if (dirs.includes(target)) return;\n await writeLocalProjectDirs([...dirs, target]);\n}\n\n/** Retirer un dossier de la liste. Son `.runeya` reste sur le disque. */\nexport async function unregisterLocalProjectDir(dir: string): Promise<void> {\n const target = resolve(dir);\n const dirs = await readLocalProjectDirs();\n if (!dirs.includes(target)) return;\n await writeLocalProjectDirs(dirs.filter((d) => d !== target));\n}\n\n/** Un dossier de projet local dont le coffre serait la racine machine, ou dedans. */\nexport function collidesWithMachineRoot(vaultRoot: string): boolean {\n const machine = machineRoot();\n return vaultRoot === machine || vaultRoot.startsWith(machine + sep);\n}\n\n/**\n * Le coffre de chaque projet local connu, par id.\n *\n * Tenu par le store des projets, qui lit les dossiers enregistrés. Sans entrée,\n * un projet local retombe sur le coffre du dossier de lancement, comme avant.\n */\nconst localProjectRoots = new Map<string, string>();\n\nexport function setLocalProjectRoot(projectId: string, root: string): void {\n localProjectRoots.set(projectId, root);\n}\n\nexport function forgetLocalProjectRoot(projectId: string): void {\n localProjectRoots.delete(projectId);\n}\n\n/** Where to read and write a given project. */\nexport function vaultFor(target: VaultTarget): Vault {\n if (!target.orgId) return { root: localProjectRoots.get(target.id) ?? localVaultRoot() };\n return { root: projectVaultRoot(target.orgId, target.id) };\n}\n\n/**\n * Which cloud instance an organization's vault came from.\n *\n * `~/.runeya/orgs/` is machine-global, so a machine that has ever synced against\n * two clouds (a self-hosted or local one and the hosted one) ends up with org\n * ids from both, indistinguishable. Stamping the instance next to the vault is\n * what lets a reader tell \"an org I am not a member of\" from \"an org of a cloud\n * I am simply not signed into\" — and what keeps the sync from offering one\n * cloud's organizations to another.\n */\nconst ORG_CLOUD_FILE = 'cloud.json';\n\nexport async function stampOrgCloud(\n orgId: string,\n cloudUrl: string,\n name?: string,\n accountId?: string,\n): Promise<void> {\n const root = orgVaultRoot(orgId);\n // Not best-effort any more: the stamp is now what tells a later boot which\n // cloud an org's vault came from, and syncing follows those vaults. A stamp\n // silently skipped would leave the org to be claimed by whatever cloud is\n // configured then — so a failure here has to stop the pull it precedes.\n await mkdir(root, { recursive: true });\n // Le nom rejoint le tampon plutôt qu'un fichier à lui : il se connaît au même\n // instant, et une organisation sans nom local n'a plus qu'un hash à montrer\n // dès que la session cloud est éteinte.\n const known = name ?? (await readOrgNames())[orgId];\n // Le compte rejoint le tampon pour la même raison que le cloud : il n'y en a\n // plus un seul sur le poste, et sans lui une organisation serait interrogée\n // avec le token du dernier utilisateur connecté — au mieux un 403, au pire\n // les données d'un homonyme. Un `accountId` omis laisse en place celui qui y\n // était : les appelants qui ne savent pas de quel compte ils parlent ne\n // doivent pas effacer ce que la liaison, elle, savait.\n const kept = accountId ?? (await readOrgAccounts())[orgId];\n const stamp = {\n cloudUrl,\n ...(known ? { name: known } : {}),\n ...(kept ? { accountId: kept } : {}),\n };\n await writeFile(join(root, ORG_CLOUD_FILE), JSON.stringify(stamp, null, 2), 'utf-8');\n}\n\n/**\n * Retenir le compte auquel une organisation appartient.\n *\n * Appelé avec ce que le cloud vient de répondre : une organisation appartient\n * au compte qui la LISTE, et c'est la seule source qui le sache. Le poste, lui,\n * ne peut que deviner — il l'a fait, et il a rangé sous un compte les\n * organisations d'un autre, y compris celles d'un tout autre cloud.\n *\n * Écrase donc le compte déjà tamponné : cette réponse-ci fait autorité, la\n * précédente pouvait être une supposition.\n */\nexport async function rememberOrgAccount(orgId: string, accountId: string): Promise<void> {\n const root = orgVaultRoot(orgId);\n // Aucun coffre : cette organisation n'a rien ici, et lui en créer un pour un\n // simple rattachement rangerait un répertoire vide sous un compte.\n if (!existsSync(root)) return;\n const file = join(root, ORG_CLOUD_FILE);\n let stamp: Record<string, unknown> = {};\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n // Pas de tampon : le rattachement en fonde un.\n }\n if (stamp['accountId'] === accountId) return;\n await writeFile(file, JSON.stringify({ ...stamp, accountId }, null, 2), 'utf-8');\n}\n\n/**\n * Compléter un tampon qui connaît son compte mais pas son cloud.\n *\n * `rememberOrgAccount` n'écrit que le compte : un coffre passé par lui garde\n * donc un tampon sans adresse, et la synchronisation l'écartait dès qu'il y\n * avait plus d'un compte lié — faute de savoir vers quel cloud frapper, alors\n * que le compte tamponné le disait. La déduction seule ne suffit pas : refaite\n * à chaque démarrage, elle laisse le disque faux, et le moindre chemin qui lit\n * l'adresse sans passer par elle retombe dans le trou. On la recopie donc.\n *\n * N'écrase jamais une adresse en place — elle peut désigner un autre cloud que\n * celui du compte, et c'est le tampon qui fait foi.\n */\nexport async function backfillOrgCloud(orgId: string, cloudUrl: string): Promise<boolean> {\n const root = orgVaultRoot(orgId);\n // Aucun coffre : rien à compléter, et en fonder un rangerait un répertoire\n // vide sous un cloud.\n if (!existsSync(root)) return false;\n const file = join(root, ORG_CLOUD_FILE);\n let stamp: Record<string, unknown>;\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n // Pas de tampon du tout : ce n'est pas un rattrapage, c'est une liaison —\n // elle passe par `stampOrgCloud`, qui sait aussi poser nom et compte.\n return false;\n }\n const current = stamp['cloudUrl'];\n if (typeof current === 'string' && current) return false;\n await writeFile(file, JSON.stringify({ ...stamp, cloudUrl }, null, 2), 'utf-8');\n return true;\n}\n\n/**\n * Suivre un compte qui change d'identifiant.\n *\n * Un compte lié avant le multicompte n'a pas d'identité : son identifiant est\n * provisoire, fondé sur son seul cloud, et devient définitif au premier appel\n * qui apprend qui il est. Les tampons posés entre-temps nomment l'ancien — et\n * un tampon qui désigne un compte inexistant rend son organisation muette dès\n * qu'il y a plus d'un compte, c'est-à-dire exactement quand on en a besoin.\n */\nexport async function renameOrgAccount(oldId: string, newId: string): Promise<void> {\n if (oldId === newId) return;\n for (const orgId of await listOrgVaults()) {\n const file = join(orgVaultRoot(orgId), ORG_CLOUD_FILE);\n let stamp: Record<string, unknown>;\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n continue;\n }\n if (stamp['accountId'] !== oldId) continue;\n await writeFile(file, JSON.stringify({ ...stamp, accountId: newId }, null, 2), 'utf-8');\n }\n}\n\n/**\n * Le compte dont chaque organisation relève, lu sur le disque.\n *\n * Pendant exact de `readOrgClouds` : le cloud dit *où* frapper, le compte dit\n * *au nom de qui*. Une organisation sans compte tamponné est celle d'un coffre\n * écrit avant le multicompte ; elle retombe sur le compte unique du poste, ce\n * qui est précisément la situation qu'elle décrivait.\n */\nexport async function readOrgAccounts(): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const orgId of await listOrgVaults()) {\n try {\n const raw = await readFile(join(orgVaultRoot(orgId), ORG_CLOUD_FILE), 'utf-8');\n const id = (JSON.parse(raw) as { accountId?: unknown }).accountId;\n if (typeof id === 'string' && id) out[orgId] = id;\n } catch {\n // Pas de tampon, ou pas de compte : le poste tranchera avec ce qu'il a.\n }\n }\n return out;\n}\n\n/**\n * Retenir le nom d'une organisation déjà présente ici.\n *\n * Appelé quand le cloud vient de le dire, pour qu'il survive à la déconnexion.\n * Aucun COFFRE n'est créé : une organisation dont ce poste n'a rien n'a pas à\n * recevoir un répertoire pour un simple nom.\n *\n * Le tampon, lui, est écrit même s'il manque — c'est la nuance qui manquait.\n * Tous les coffres ne naissent pas d'une synchronisation : un simple\n * `settingsManager.getSettings(orgId)` crée le répertoire sans passer par\n * `stampOrgCloud`. Renoncer faute de fichier condamnait ces coffres-là à\n * n'afficher qu'un hash pour toujours, quel que soit le nombre de fois où le\n * cloud redit leur nom.\n *\n * Le tampon posé ici ne porte pas de `cloudUrl` : personne ici ne sait de quel\n * cloud vient le coffre, et l'inventer rangerait l'organisation sous le mauvais.\n * Un tampon sans cloud se lit exactement comme l'absence de tampon.\n */\nexport async function rememberOrgName(orgId: string, name: string): Promise<void> {\n const root = orgVaultRoot(orgId);\n if (!existsSync(root)) return;\n const file = join(root, ORG_CLOUD_FILE);\n let stamp: Record<string, unknown> = {};\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n // Pas de tampon, ou illisible : le nom en fonde un.\n }\n if (stamp['name'] === name) return;\n await writeFile(file, JSON.stringify({ ...stamp, name }, null, 2), 'utf-8');\n}\n\n/** Le nom de chaque organisation dont le tampon en porte un. */\nexport async function readOrgNames(): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const orgId of await listOrgVaults()) {\n try {\n const raw = await readFile(join(orgVaultRoot(orgId), ORG_CLOUD_FILE), 'utf-8');\n const name = (JSON.parse(raw) as { name?: unknown }).name;\n if (typeof name === 'string' && name) out[orgId] = name;\n } catch {\n // Pas de tampon, ou pas encore de nom : l'id fera l'affaire.\n }\n }\n return out;\n}\n\n/**\n * Le logo d'une organisation, gardé à côté de son nom.\n *\n * Même raison d'être que le nom recopié dans le tampon : ce que le poste sait\n * de ses organisations ne doit dépendre ni d'une session cloud ni du réseau.\n * Le nom tient dans le tampon ; les octets, eux, méritent leur propre fichier —\n * `cloud.json` est relu et réécrit à chaque pull, et y mêler 40 Ko de base64\n * ferait payer l'image à des lectures qui n'en veulent pas.\n */\nconst ORG_LOGO_FILE = 'logo.json';\n\ninterface StoredLogo {\n contentType: string;\n base64: string;\n fetchedAt: string;\n}\n\n/** Formats matriciels inertes, la même liste que celle acceptée par le cloud. */\nconst LOGO_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);\n\n/** Garder (ou remplacer) le logo d'une organisation présente ici. */\nexport async function rememberOrgLogo(orgId: string, contentType: string, base64: string): Promise<void> {\n if (!LOGO_TYPES.has(contentType)) return;\n const root = orgVaultRoot(orgId);\n const stored: StoredLogo = { contentType, base64, fetchedAt: new Date().toISOString() };\n await mkdir(root, { recursive: true });\n await writeFile(join(root, ORG_LOGO_FILE), JSON.stringify(stored), 'utf-8');\n}\n\n/** Oublier le logo d'une organisation — elle n'en a plus. */\nexport async function forgetOrgLogo(orgId: string): Promise<void> {\n try {\n await unlink(join(orgVaultRoot(orgId), ORG_LOGO_FILE));\n } catch {\n // Il n'y en avait pas : c'est le résultat voulu.\n }\n}\n\n/**\n * Le logo retenu d'une organisation : ses octets et leur type.\n *\n * Le type est revalidé à la lecture — un fichier trafiqué sur le disque ne doit\n * pas pouvoir faire servir un `image/svg+xml`, donc du script, à l'interface.\n */\nexport async function readOrgLogo(orgId: string): Promise<{ contentType: string; bytes: Uint8Array } | null> {\n try {\n const raw = await readFile(join(orgVaultRoot(orgId), ORG_LOGO_FILE), 'utf-8');\n const { contentType, base64 } = JSON.parse(raw) as Partial<StoredLogo>;\n if (typeof contentType !== 'string' || !LOGO_TYPES.has(contentType)) return null;\n if (typeof base64 !== 'string' || !base64) return null;\n return { contentType, bytes: new Uint8Array(Buffer.from(base64, 'base64')) };\n } catch {\n // Pas de logo retenu, ou fichier illisible.\n return null;\n }\n}\n\n/**\n * Les logos retenus, en data URL, prêts pour un `src`.\n *\n * Le type est revalidé à la lecture : un fichier trafiqué sur le disque ne doit\n * pas pouvoir faire servir un `image/svg+xml` — donc du script — à l'interface.\n */\nexport async function readOrgLogos(): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const orgId of await listOrgVaults()) {\n const logo = await readOrgLogo(orgId);\n // Pas de logo retenu : l'initiale fera l'affaire.\n if (logo) out[orgId] = `data:${logo.contentType};base64,${Buffer.from(logo.bytes).toString('base64')}`;\n }\n return out;\n}\n\n/** The cloud instance of every org vault that carries a stamp. */\nexport async function readOrgClouds(): Promise<Record<string, string>> {\n const orgsDir = join(machineRoot(), 'orgs');\n const out: Record<string, string> = {};\n let orgIds: string[];\n try {\n orgIds = (await readdir(orgsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory() && SAFE_ID.test(e.name))\n .map((e) => e.name);\n } catch {\n return out;\n }\n for (const orgId of orgIds) {\n try {\n const raw = await readFile(join(orgsDir, orgId, ORG_CLOUD_FILE), 'utf-8');\n const url = (JSON.parse(raw) as { cloudUrl?: unknown }).cloudUrl;\n if (typeof url === 'string' && url) out[orgId] = url;\n } catch {\n // No stamp (older vault, or never linked here) — simply unknown.\n }\n }\n return out;\n}\n\n/**\n * Les organisations présentes sur ce disque, projets ou non.\n *\n * Distinct de `listCloudVaults`, qui ne voit une organisation qu'à travers ses\n * projets matérialisés : une organisation peut porter des données à elle —\n * agents, clés d'API — sans qu'aucun projet n'ait encore été ouvert ici, et\n * l'ignorer alors reviendrait à les perdre au prochain démarrage.\n */\nexport async function listOrgVaults(): Promise<string[]> {\n const orgsDir = join(machineRoot(), 'orgs');\n try {\n return (await readdir(orgsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory() && SAFE_ID.test(e.name))\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\n/** One cloud project vault found on disk. */\nexport interface CloudVault {\n orgId: string;\n projectId: string;\n root: string;\n}\n\n/**\n * Every cloud project vault present on this machine, by walking\n * `~/.runeya/orgs/<orgId>/projects/<projectId>`.\n *\n * Discovery reads the disk rather than an index on purpose: an index is one\n * more thing that can disagree with what is actually there, and a project\n * present on disk but missing from it is precisely the \"already here, still\n * offered as a download\" bug. Unreadable or foreign directories are skipped in\n * silence — a half-written pull must not take a whole load down.\n */\nexport async function listCloudVaults(): Promise<CloudVault[]> {\n const orgsDir = join(machineRoot(), 'orgs');\n const found: CloudVault[] = [];\n\n let orgIds: string[];\n try {\n orgIds = (await readdir(orgsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory() && SAFE_ID.test(e.name))\n .map((e) => e.name);\n } catch {\n return found;\n }\n\n for (const orgId of orgIds) {\n const projectsDir = join(orgsDir, orgId, 'projects');\n try {\n for (const entry of await readdir(projectsDir, { withFileTypes: true })) {\n if (!entry.isDirectory() || !SAFE_ID.test(entry.name)) continue;\n const root = join(projectsDir, entry.name);\n // Un répertoire n'est pas un projet : il faut qu'il en porte la fiche.\n //\n // Tout ce qui traînait ici était pris pour un coffre — une copie de\n // sauvegarde, un dossier oublié — et les stores le lisaient, le\n // comptaient, pouvaient le réécrire. Une sauvegarde déposée à côté\n // d'un projet s'est ainsi retrouvée annoncée comme dix-neuf\n // conversations illisibles appartenant à l'organisation.\n //\n // `project.json` est déjà ce qui fait foi ailleurs (`isMaterialized`\n // dans org-sync-scope) : la même règle vaut ici, pour tout le monde.\n if (!(await access(join(root, 'project.json')).then(() => true, () => false))) continue;\n found.push({ orgId, projectId: entry.name, root });\n }\n } catch {\n // Org directory without materialized projects yet.\n }\n }\n return found;\n}\n\n/**\n * Every vault holding data, local first — what stores must read to see it all.\n *\n * Derived from the disk, so it needs no store to have loaded beforehand: a\n * content store can resolve where to read without waiting on the project store.\n */\nexport async function allVaultRoots(): Promise<string[]> {\n const cloud = await listCloudVaults();\n const locals = (await readLocalProjectDirs()).map(localProjectVaultRoot);\n return [...new Set([localVaultRoot(), ...locals, ...cloud.map((v) => v.root)])];\n}\n\n/**\n * A file belonging to the machine rather than to any project.\n *\n * Settings, the audit log, caches: none of them describe a project, and keeping\n * them in the launch directory forced a `.runeya` to exist for someone working\n * only on cloud projects — the very thing vaults remove.\n *\n * For anything ENCRYPTED, use `keyedFilePath` instead.\n */\nexport function machineFilePath(filename: string): string {\n return join(machineRoot(), filename);\n}\n\n/**\n * A machine file whose contents are encrypted.\n *\n * It lives beside the key that opens it, never merely beside the machine. The\n * two are usually the same directory — but a launch directory that owns its own\n * key keeps it (see `keyRoot`), and a file encrypted under that key read from\n * elsewhere would decrypt to blanks, then be saved back blank. Tying the file to\n * its key is what makes that impossible.\n */\nexport async function keyedFilePath(filename: string): Promise<string> {\n return join(await keyRoot(), filename);\n}\n\n/**\n * Where such a file was kept by earlier versions: in the launch directory.\n *\n * Read as a fallback and never written, so a directory shared with an older\n * Runeya keeps working.\n */\nexport function legacyMachineFilePath(filename: string): string {\n return join(localVaultRoot(), filename);\n}\n\n/**\n * Create the local vault, on the way to writing a local project into it.\n *\n * The directory is created here and nowhere else. Runeya no longer makes a\n * `.runeya` just because it was started somewhere: a machine used only for\n * cloud projects should leave the directories it visits untouched, and a\n * `.runeya` appearing in a repository should mean \"there is a local project\n * here\" rather than \"Runeya ran here once\".\n */\nexport async function ensureLocalVault(): Promise<string> {\n const root = localVaultRoot();\n await mkdir(root, { recursive: true });\n return root;\n}\n\n/** Files that mark a directory as already holding an encryption key. */\nconst KEY_FILES = ['.encryption.key.enc', '.runeya-verify'];\n\nlet keyRootPromise: Promise<string> | null = null;\n\n/** True when a directory already holds an encryption key of its own. */\nasync function holdsKey(root: string): Promise<boolean> {\n for (const filename of KEY_FILES) {\n try {\n await access(join(root, filename));\n return true;\n } catch {\n // Not here — keep looking.\n }\n }\n return false;\n}\n\n/**\n * Where this machine's encryption key and its canary live.\n *\n * The key protects everything on the machine — agents, sessions, and the\n * environments of every vault — so there is one, not one per vault, and the\n * machine root is its home.\n *\n * Earlier versions let a launch directory keep a key of its own, which is how a\n * machine ended up with several master passwords and several cloud identities.\n * Such a directory is no longer promoted: the machine key answers as soon as it\n * exists, and the leftover local key is surfaced by `legacyVaultRoot()` so it\n * can be migrated rather than silently obeyed. A machine that has never had a\n * machine key still falls back to the local one, so an un-migrated install\n * keeps opening exactly as before.\n *\n * Resolved once per process: the answer must not change under a running server,\n * and a key written mid-session would otherwise move its own home.\n */\nexport function keyRoot(): Promise<string> {\n keyRootPromise ??= (async () => {\n if (await holdsKey(machineRoot())) return machineRoot();\n return (await holdsKey(localVaultRoot())) ? localVaultRoot() : machineRoot();\n })();\n return keyRootPromise;\n}\n\n/**\n * The launch directory's own key, when the machine key is what we opened.\n *\n * Its presence is the whole trigger for the migration page: this directory\n * holds data encrypted under a key nothing reads any more. Null once migrated,\n * and null too when the local key IS the one in use — there is nothing to\n * migrate then.\n */\nexport async function legacyVaultRoot(): Promise<string | null> {\n const local = localVaultRoot();\n if ((await keyRoot()) === local) return null;\n return (await holdsKey(local)) ? local : null;\n}\n\n/** Test seam: forget the resolved key home. */\nexport function resetKeyRoot(): void {\n keyRootPromise = null;\n}\n","import { z } from 'zod';\nimport { resolve as resolvePath } from 'node:path';\n\n/**\n * Every variable Runeya reads from the process environment is prefixed with\n * `RUNEYA_`. Generic names (PORT, HOST, MODE, DATA_DIR…) are deliberately NOT\n * read: Runeya loads the `.env` of the stack it manages, where those names very\n * often belong to the user's own services.\n *\n * The resolved object keeps short field names (`env.PORT`, `env.HOST`, …) —\n * only the environment keys are prefixed.\n */\n\n// Server port, used to derive the default agent port\nconst serverPort = z.coerce.number().int().min(1).max(65535).default(4000).parse(process.env.RUNEYA_HTTP_PORT);\n\nconst defaultAgentPort = process.env.RUNEYA_AGENT_PORT\n ? z.coerce.number().int().min(1).max(65535).parse(process.env.RUNEYA_AGENT_PORT)\n : serverPort + 1;\n\nconst EnvSchema = z.object({\n RUNEYA_HTTP_PORT: z.coerce.number().int().min(1).max(65535).default(4000),\n RUNEYA_HOST: z.string().default('127.0.0.1'),\n RUNEYA_AGENT_PORT: z.coerce.number().int().min(1).max(65535).default(defaultAgentPort),\n RUNEYA_AGENT_HOST: z.string().default('127.0.0.1'),\n RUNEYA_AGENT_BINARY_PATH: z.string().nullable().default(null),\n RUNEYA_DATA_DIR: z.preprocess((v) => (typeof v === 'string' && v.length > 0 ? v : undefined), z.string().default('.runeya')),\n // Home holding the cloud vaults (`<home>/.runeya/orgs/...`). Defaults to the\n // user's home directory; overridden by tests so they never write to the real\n // one, and usable to relocate the vaults on a shared machine.\n RUNEYA_HOME: z.preprocess((v) => (typeof v === 'string' && v.length > 0 ? v : undefined), z.string().nullable().default(null)),\n RUNEYA_CLOUD_URL: z.string().url().nullable().default(null),\n // Cloud front-end (landing) URL — used by the web app for the registration /\n // onboarding flow. Configured on the server at launch; the web fetches it.\n RUNEYA_CLOUD_APP_URL: z.string().url().nullable().default(null),\n RUNEYA_MODE: z.enum(['local-simple', 'cloud']).default('local-simple'),\n RUNEYA_CORS_ORIGIN: z.string().nullable().default(null),\n RUNEYA_LAN_PROXY: z.preprocess((v) => v === 'true' || v === '1', z.boolean()).default(false),\n // Set by the CLI when it was started from a desktop launcher rather than a\n // terminal. The web app surfaces it so the user knows which Runeya they are\n // looking at when several are running.\n RUNEYA_FROM_DESKTOP: z.preprocess((v) => v === 'true' || v === '1', z.boolean()).default(false),\n // Label shown for this machine in the cloud's \"linked machines\" screen.\n // Defaults to the hostname (see `getDeviceName`); set it explicitly when a\n // single host runs several instances and you want to tell them apart.\n RUNEYA_DEVICE_NAME: z.preprocess(\n (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined),\n z.string().max(64).nullable().default(null),\n ),\n // Lifetime of the JWTs signed by the server (UI session and AI agents).\n // Meant for reproducing an expiry locally, e.g. RUNEYA_JWT_TTL=4s.\n RUNEYA_JWT_TTL: z.preprocess(\n (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined),\n z.string().regex(/^[1-9]\\d{0,5}(s|m|h|d)$/, 'RUNEYA_JWT_TTL: expected e.g. 4s, 30m, 8h, 1d').default('8h'),\n ),\n}).transform((data) => ({\n ...data,\n // Short aliases used throughout the server code\n PORT: data.RUNEYA_HTTP_PORT,\n HOST: data.RUNEYA_HOST,\n MODE: data.RUNEYA_MODE,\n /**\n * Absolute from the start, resolved once against the launch directory.\n *\n * It defaults to a relative `.runeya`, and every caller used to re-resolve it\n * against whatever the process's cwd happened to be. A vault that moves when\n * the process changes directory is a vault that can be silently written to the\n * wrong place — so the answer is fixed here, at boot, and never recomputed.\n */\n DATA_DIR: resolvePath(data.RUNEYA_DATA_DIR),\n HOME_DIR: data.RUNEYA_HOME,\n CLOUD_URL: data.RUNEYA_CLOUD_URL,\n CLOUD_APP_URL: data.RUNEYA_CLOUD_APP_URL,\n CORS_ORIGIN: data.RUNEYA_CORS_ORIGIN,\n AGENT_BINARY_PATH: data.RUNEYA_AGENT_BINARY_PATH,\n DEVICE_NAME: data.RUNEYA_DEVICE_NAME,\n FROM_DESKTOP: data.RUNEYA_FROM_DESKTOP,\n JWT_TTL: data.RUNEYA_JWT_TTL,\n}));\n\nexport type Env = z.infer<typeof EnvSchema>;\n\nexport const env = EnvSchema.parse(process.env);\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../server/src/services/vault-resolver.ts","../../server/src/config/env.ts"],"sourcesContent":["import { join, sep, resolve } from 'node:path';\nimport { homedir } from 'node:os';\nimport { readdir, mkdir, access, readFile, writeFile, unlink, rename } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { env } from '../config/env.js';\n\n/**\n * Where a project's files live, and which key protects them.\n *\n * Runeya holds two kinds of projects, and they do not belong in the same place.\n * A local project *is* a directory on this machine: its data belongs next to\n * the code, in `<cwd>/.runeya`. A cloud project belongs to an organization and\n * is materialized from the doc; tying it to whichever directory happened to\n * launch the server is what made the same project look \"already here\" in one\n * folder and \"to download\" in another. Cloud projects therefore live under the\n * user's home, prefixed by organization and by project.\n *\n * A vault holds a project's own data — project.json, services, environments.\n * It does NOT hold the key that protects it: one key answers for the whole\n * machine (see `keyRoot` at the bottom of this file), which is what the cloud's\n * org-wide canary already assumes.\n */\n\nexport interface Vault {\n /** Directory holding this project's own files. */\n root: string;\n}\n\n/** Identifies a project well enough to place it — the stored shape, or the doc's. */\nexport interface VaultTarget {\n id: string;\n orgId?: string | null;\n}\n\n/**\n * Ids reach us from the cloud, and they end up in filesystem paths. Anything\n * outside this alphabet — a separator, a `..`, a NUL — could walk out of the\n * home directory, so it is rejected rather than sanitized: a mangled id would\n * silently point at the wrong vault.\n */\nconst SAFE_ID = /^[A-Za-z0-9._-]{1,128}$/;\n\nfunction assertSafeId(kind: 'organization' | 'project', id: string): void {\n if (!SAFE_ID.test(id) || id === '.' || id === '..') {\n throw new Error(`Invalid ${kind} id for a vault path: ${JSON.stringify(id)}`);\n }\n}\n\n/**\n * Everything that belongs to this machine rather than to a project.\n *\n * Read from `process.env` rather than the parsed config: tests that mock\n * `config/env.js` would otherwise lose the override and write cloud vaults into\n * the developer's real home directory.\n */\nexport function machineRoot(): string {\n const override = process.env['RUNEYA_HOME'];\n return join(override && override.length > 0 ? override : homedir(), '.runeya');\n}\n\n/**\n * The local vault: the `.runeya` of the directory the server was launched in.\n *\n * Kept on `env.DATA_DIR` so `--data-dir` and the tests keep working. That value\n * is already absolute, resolved once at boot — re-resolving it here would make\n * the vault follow the process's cwd, which is exactly what it must not do.\n */\nexport function localVaultRoot(): string {\n return env.DATA_DIR;\n}\n\n/**\n * Le coffre local se confond-il avec la racine machine, ou se range-t-il dedans ?\n *\n * Runeya lancée depuis le dossier qui abrite son propre `.runeya` — le home,\n * typiquement — y verrait son coffre local : les fichiers d'un projet local\n * iraient alors se mêler aux coffres d'organisation, aux passphrases d'agent et\n * au reste de ce qui appartient à la machine, dans un seul répertoire. Deux\n * natures de données que tout sépare partageraient leur rangement, et supprimer\n * l'une emporterait l'autre.\n */\nexport function localVaultCollidesWithMachineRoot(): boolean {\n return collidesWithMachineRoot(localVaultRoot());\n}\n\n/** The vault of an organization: its projects, and its lock. */\nexport function orgVaultRoot(orgId: string): string {\n assertSafeId('organization', orgId);\n return join(machineRoot(), 'orgs', orgId);\n}\n\n/** The vault of one cloud project, under its organization. */\nexport function projectVaultRoot(orgId: string, projectId: string): string {\n assertSafeId('project', projectId);\n return join(orgVaultRoot(orgId), 'projects', projectId);\n}\n\n/**\n * The organization a vault path belongs to, or null for the local vault.\n *\n * The layout is the authority on ownership — `~/.runeya/orgs/<org>/projects/<id>`\n * says both which project and which organization — so an entity read from a\n * vault carries its couple in the path it came from, even though its own file\n * only ever names its project.\n */\nexport function orgIdOfRoot(root: string): string | null {\n const marker = join(machineRoot(), 'orgs') + sep;\n if (!root.startsWith(marker)) return null;\n const [orgId] = root.slice(marker.length).split(sep);\n return orgId && SAFE_ID.test(orgId) ? orgId : null;\n}\n\n/**\n * La clé sous laquelle une donnée de ce coffre est chiffrée.\n *\n * L'identifiant d'organisation quand le coffre lui appartient, `undefined` pour\n * la clé machine. Le rangement décide donc du chiffrement, sans qu'aucune\n * entité ait à porter un champ de plus : le coffre où on l'écrit dit déjà à qui\n * elle est.\n */\nexport function vaultIdOfRoot(root: string): string | undefined {\n return orgIdOfRoot(root) ?? undefined;\n}\n\n// ─── Projets locaux ─────────────────────────────────────────────────────────\n\n/**\n * Les dossiers des projets locaux de cette machine.\n *\n * Un projet local vit dans le `.runeya` de son dossier. Il y en a un par\n * dossier, et autant de dossiers qu'on en importe ou crée depuis l'interface.\n * Ce fichier de la racine machine en garde la liste, pour que chaque instance\n * de Runeya les voie, quel que soit le dossier d'où elle est lancée.\n */\nconst LOCAL_PROJECTS_FILE = 'local-projects.json';\n\nfunction localProjectsFilePath(): string {\n return join(machineRoot(), LOCAL_PROJECTS_FILE);\n}\n\n/** Le coffre d'un projet local : le `.runeya` de son dossier. */\nexport function localProjectVaultRoot(dir: string): string {\n return join(resolve(dir), '.runeya');\n}\n\n/** Les dossiers enregistrés, absolus, sans doublon. */\nexport async function readLocalProjectDirs(): Promise<string[]> {\n try {\n const parsed = JSON.parse(await readFile(localProjectsFilePath(), 'utf-8')) as { dirs?: unknown };\n const dirs = Array.isArray(parsed.dirs) ? parsed.dirs.filter((d): d is string => typeof d === 'string') : [];\n return [...new Set(dirs.map((d) => resolve(d)))];\n } catch {\n return [];\n }\n}\n\nasync function writeLocalProjectDirs(dirs: string[]): Promise<void> {\n await mkdir(machineRoot(), { recursive: true });\n const path = localProjectsFilePath();\n await writeFile(`${path}.tmp`, JSON.stringify({ dirs }, null, 2), 'utf-8');\n await rename(`${path}.tmp`, path);\n}\n\n/** Ajouter un dossier à la liste. Sans effet s'il y est déjà. */\nexport async function registerLocalProjectDir(dir: string): Promise<void> {\n const target = resolve(dir);\n const dirs = await readLocalProjectDirs();\n if (dirs.includes(target)) return;\n await writeLocalProjectDirs([...dirs, target]);\n}\n\n/** Retirer un dossier de la liste. Son `.runeya` reste sur le disque. */\nexport async function unregisterLocalProjectDir(dir: string): Promise<void> {\n const target = resolve(dir);\n const dirs = await readLocalProjectDirs();\n if (!dirs.includes(target)) return;\n await writeLocalProjectDirs(dirs.filter((d) => d !== target));\n}\n\n/** Un dossier de projet local dont le coffre serait la racine machine, ou dedans. */\nexport function collidesWithMachineRoot(vaultRoot: string): boolean {\n const machine = machineRoot();\n return vaultRoot === machine || vaultRoot.startsWith(machine + sep);\n}\n\n/**\n * Le coffre de chaque projet local connu, par id.\n *\n * Tenu par le store des projets, qui lit les dossiers enregistrés. Sans entrée,\n * un projet local retombe sur le coffre du dossier de lancement, comme avant.\n */\nconst localProjectRoots = new Map<string, string>();\n\nexport function setLocalProjectRoot(projectId: string, root: string): void {\n localProjectRoots.set(projectId, root);\n}\n\nexport function forgetLocalProjectRoot(projectId: string): void {\n localProjectRoots.delete(projectId);\n}\n\n/** Where to read and write a given project. */\nexport function vaultFor(target: VaultTarget): Vault {\n if (!target.orgId) return { root: localProjectRoots.get(target.id) ?? localVaultRoot() };\n return { root: projectVaultRoot(target.orgId, target.id) };\n}\n\n/**\n * Which cloud instance an organization's vault came from.\n *\n * `~/.runeya/orgs/` is machine-global, so a machine that has ever synced against\n * two clouds (a self-hosted or local one and the hosted one) ends up with org\n * ids from both, indistinguishable. Stamping the instance next to the vault is\n * what lets a reader tell \"an org I am not a member of\" from \"an org of a cloud\n * I am simply not signed into\" — and what keeps the sync from offering one\n * cloud's organizations to another.\n */\nconst ORG_CLOUD_FILE = 'cloud.json';\n\nexport async function stampOrgCloud(\n orgId: string,\n cloudUrl: string,\n name?: string,\n accountId?: string,\n): Promise<void> {\n const root = orgVaultRoot(orgId);\n // Not best-effort any more: the stamp is now what tells a later boot which\n // cloud an org's vault came from, and syncing follows those vaults. A stamp\n // silently skipped would leave the org to be claimed by whatever cloud is\n // configured then — so a failure here has to stop the pull it precedes.\n await mkdir(root, { recursive: true });\n // Le nom rejoint le tampon plutôt qu'un fichier à lui : il se connaît au même\n // instant, et une organisation sans nom local n'a plus qu'un hash à montrer\n // dès que la session cloud est éteinte.\n const known = name ?? (await readOrgNames())[orgId];\n // Le compte rejoint le tampon pour la même raison que le cloud : il n'y en a\n // plus un seul sur le poste, et sans lui une organisation serait interrogée\n // avec le token du dernier utilisateur connecté — au mieux un 403, au pire\n // les données d'un homonyme. Un `accountId` omis laisse en place celui qui y\n // était : les appelants qui ne savent pas de quel compte ils parlent ne\n // doivent pas effacer ce que la liaison, elle, savait.\n const kept = accountId ?? (await readOrgAccounts())[orgId];\n const stamp = {\n cloudUrl,\n ...(known ? { name: known } : {}),\n ...(kept ? { accountId: kept } : {}),\n };\n await writeFile(join(root, ORG_CLOUD_FILE), JSON.stringify(stamp, null, 2), 'utf-8');\n}\n\n/**\n * Retenir le compte auquel une organisation appartient.\n *\n * Appelé avec ce que le cloud vient de répondre : une organisation appartient\n * au compte qui la LISTE, et c'est la seule source qui le sache. Le poste, lui,\n * ne peut que deviner — il l'a fait, et il a rangé sous un compte les\n * organisations d'un autre, y compris celles d'un tout autre cloud.\n *\n * Écrase donc le compte déjà tamponné : cette réponse-ci fait autorité, la\n * précédente pouvait être une supposition.\n */\nexport async function rememberOrgAccount(orgId: string, accountId: string): Promise<void> {\n const root = orgVaultRoot(orgId);\n // Aucun coffre : cette organisation n'a rien ici, et lui en créer un pour un\n // simple rattachement rangerait un répertoire vide sous un compte.\n if (!existsSync(root)) return;\n const file = join(root, ORG_CLOUD_FILE);\n let stamp: Record<string, unknown> = {};\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n // Pas de tampon : le rattachement en fonde un.\n }\n if (stamp['accountId'] === accountId) return;\n await writeFile(file, JSON.stringify({ ...stamp, accountId }, null, 2), 'utf-8');\n}\n\n/**\n * Compléter un tampon qui connaît son compte mais pas son cloud.\n *\n * `rememberOrgAccount` n'écrit que le compte : un coffre passé par lui garde\n * donc un tampon sans adresse, et la synchronisation l'écartait dès qu'il y\n * avait plus d'un compte lié — faute de savoir vers quel cloud frapper, alors\n * que le compte tamponné le disait. La déduction seule ne suffit pas : refaite\n * à chaque démarrage, elle laisse le disque faux, et le moindre chemin qui lit\n * l'adresse sans passer par elle retombe dans le trou. On la recopie donc.\n *\n * N'écrase jamais une adresse en place — elle peut désigner un autre cloud que\n * celui du compte, et c'est le tampon qui fait foi.\n */\nexport async function backfillOrgCloud(orgId: string, cloudUrl: string): Promise<boolean> {\n const root = orgVaultRoot(orgId);\n // Aucun coffre : rien à compléter, et en fonder un rangerait un répertoire\n // vide sous un cloud.\n if (!existsSync(root)) return false;\n const file = join(root, ORG_CLOUD_FILE);\n let stamp: Record<string, unknown>;\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n // Pas de tampon du tout : ce n'est pas un rattrapage, c'est une liaison —\n // elle passe par `stampOrgCloud`, qui sait aussi poser nom et compte.\n return false;\n }\n const current = stamp['cloudUrl'];\n if (typeof current === 'string' && current) return false;\n await writeFile(file, JSON.stringify({ ...stamp, cloudUrl }, null, 2), 'utf-8');\n return true;\n}\n\n/**\n * Suivre un compte qui change d'identifiant.\n *\n * Un compte lié avant le multicompte n'a pas d'identité : son identifiant est\n * provisoire, fondé sur son seul cloud, et devient définitif au premier appel\n * qui apprend qui il est. Les tampons posés entre-temps nomment l'ancien — et\n * un tampon qui désigne un compte inexistant rend son organisation muette dès\n * qu'il y a plus d'un compte, c'est-à-dire exactement quand on en a besoin.\n */\nexport async function renameOrgAccount(oldId: string, newId: string): Promise<void> {\n if (oldId === newId) return;\n for (const orgId of await listOrgVaults()) {\n const file = join(orgVaultRoot(orgId), ORG_CLOUD_FILE);\n let stamp: Record<string, unknown>;\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n continue;\n }\n if (stamp['accountId'] !== oldId) continue;\n await writeFile(file, JSON.stringify({ ...stamp, accountId: newId }, null, 2), 'utf-8');\n }\n}\n\n/**\n * Le compte dont chaque organisation relève, lu sur le disque.\n *\n * Pendant exact de `readOrgClouds` : le cloud dit *où* frapper, le compte dit\n * *au nom de qui*. Une organisation sans compte tamponné est celle d'un coffre\n * écrit avant le multicompte ; elle retombe sur le compte unique du poste, ce\n * qui est précisément la situation qu'elle décrivait.\n */\nexport async function readOrgAccounts(): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const orgId of await listOrgVaults()) {\n try {\n const raw = await readFile(join(orgVaultRoot(orgId), ORG_CLOUD_FILE), 'utf-8');\n const id = (JSON.parse(raw) as { accountId?: unknown }).accountId;\n if (typeof id === 'string' && id) out[orgId] = id;\n } catch {\n // Pas de tampon, ou pas de compte : le poste tranchera avec ce qu'il a.\n }\n }\n return out;\n}\n\n/**\n * Retenir le nom d'une organisation déjà présente ici.\n *\n * Appelé quand le cloud vient de le dire, pour qu'il survive à la déconnexion.\n * Aucun COFFRE n'est créé : une organisation dont ce poste n'a rien n'a pas à\n * recevoir un répertoire pour un simple nom.\n *\n * Le tampon, lui, est écrit même s'il manque — c'est la nuance qui manquait.\n * Tous les coffres ne naissent pas d'une synchronisation : un simple\n * `settingsManager.getSettings(orgId)` crée le répertoire sans passer par\n * `stampOrgCloud`. Renoncer faute de fichier condamnait ces coffres-là à\n * n'afficher qu'un hash pour toujours, quel que soit le nombre de fois où le\n * cloud redit leur nom.\n *\n * Le tampon posé ici ne porte pas de `cloudUrl` : personne ici ne sait de quel\n * cloud vient le coffre, et l'inventer rangerait l'organisation sous le mauvais.\n * Un tampon sans cloud se lit exactement comme l'absence de tampon.\n */\nexport async function rememberOrgName(orgId: string, name: string): Promise<void> {\n const root = orgVaultRoot(orgId);\n if (!existsSync(root)) return;\n const file = join(root, ORG_CLOUD_FILE);\n let stamp: Record<string, unknown> = {};\n try {\n stamp = JSON.parse(await readFile(file, 'utf-8')) as Record<string, unknown>;\n } catch {\n // Pas de tampon, ou illisible : le nom en fonde un.\n }\n if (stamp['name'] === name) return;\n await writeFile(file, JSON.stringify({ ...stamp, name }, null, 2), 'utf-8');\n}\n\n/** Le nom de chaque organisation dont le tampon en porte un. */\nexport async function readOrgNames(): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const orgId of await listOrgVaults()) {\n try {\n const raw = await readFile(join(orgVaultRoot(orgId), ORG_CLOUD_FILE), 'utf-8');\n const name = (JSON.parse(raw) as { name?: unknown }).name;\n if (typeof name === 'string' && name) out[orgId] = name;\n } catch {\n // Pas de tampon, ou pas encore de nom : l'id fera l'affaire.\n }\n }\n return out;\n}\n\n/**\n * Le logo d'une organisation, gardé à côté de son nom.\n *\n * Même raison d'être que le nom recopié dans le tampon : ce que le poste sait\n * de ses organisations ne doit dépendre ni d'une session cloud ni du réseau.\n * Le nom tient dans le tampon ; les octets, eux, méritent leur propre fichier —\n * `cloud.json` est relu et réécrit à chaque pull, et y mêler 40 Ko de base64\n * ferait payer l'image à des lectures qui n'en veulent pas.\n */\nconst ORG_LOGO_FILE = 'logo.json';\n\ninterface StoredLogo {\n contentType: string;\n base64: string;\n fetchedAt: string;\n}\n\n/** Formats matriciels inertes, la même liste que celle acceptée par le cloud. */\nconst LOGO_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);\n\n/** Garder (ou remplacer) le logo d'une organisation présente ici. */\nexport async function rememberOrgLogo(orgId: string, contentType: string, base64: string): Promise<void> {\n if (!LOGO_TYPES.has(contentType)) return;\n const root = orgVaultRoot(orgId);\n const stored: StoredLogo = { contentType, base64, fetchedAt: new Date().toISOString() };\n await mkdir(root, { recursive: true });\n await writeFile(join(root, ORG_LOGO_FILE), JSON.stringify(stored), 'utf-8');\n}\n\n/** Oublier le logo d'une organisation — elle n'en a plus. */\nexport async function forgetOrgLogo(orgId: string): Promise<void> {\n try {\n await unlink(join(orgVaultRoot(orgId), ORG_LOGO_FILE));\n } catch {\n // Il n'y en avait pas : c'est le résultat voulu.\n }\n}\n\n/**\n * Le logo retenu d'une organisation : ses octets et leur type.\n *\n * Le type est revalidé à la lecture — un fichier trafiqué sur le disque ne doit\n * pas pouvoir faire servir un `image/svg+xml`, donc du script, à l'interface.\n */\nexport async function readOrgLogo(orgId: string): Promise<{ contentType: string; bytes: Uint8Array } | null> {\n try {\n const raw = await readFile(join(orgVaultRoot(orgId), ORG_LOGO_FILE), 'utf-8');\n const { contentType, base64 } = JSON.parse(raw) as Partial<StoredLogo>;\n if (typeof contentType !== 'string' || !LOGO_TYPES.has(contentType)) return null;\n if (typeof base64 !== 'string' || !base64) return null;\n return { contentType, bytes: new Uint8Array(Buffer.from(base64, 'base64')) };\n } catch {\n // Pas de logo retenu, ou fichier illisible.\n return null;\n }\n}\n\n/**\n * Les logos retenus, en data URL, prêts pour un `src`.\n *\n * Le type est revalidé à la lecture : un fichier trafiqué sur le disque ne doit\n * pas pouvoir faire servir un `image/svg+xml` — donc du script — à l'interface.\n */\nexport async function readOrgLogos(): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const orgId of await listOrgVaults()) {\n const logo = await readOrgLogo(orgId);\n // Pas de logo retenu : l'initiale fera l'affaire.\n if (logo) out[orgId] = `data:${logo.contentType};base64,${Buffer.from(logo.bytes).toString('base64')}`;\n }\n return out;\n}\n\n/** The cloud instance of every org vault that carries a stamp. */\nexport async function readOrgClouds(): Promise<Record<string, string>> {\n const orgsDir = join(machineRoot(), 'orgs');\n const out: Record<string, string> = {};\n let orgIds: string[];\n try {\n orgIds = (await readdir(orgsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory() && SAFE_ID.test(e.name))\n .map((e) => e.name);\n } catch {\n return out;\n }\n for (const orgId of orgIds) {\n try {\n const raw = await readFile(join(orgsDir, orgId, ORG_CLOUD_FILE), 'utf-8');\n const url = (JSON.parse(raw) as { cloudUrl?: unknown }).cloudUrl;\n if (typeof url === 'string' && url) out[orgId] = url;\n } catch {\n // No stamp (older vault, or never linked here) — simply unknown.\n }\n }\n return out;\n}\n\n/**\n * Les organisations présentes sur ce disque, projets ou non.\n *\n * Distinct de `listCloudVaults`, qui ne voit une organisation qu'à travers ses\n * projets matérialisés : une organisation peut porter des données à elle —\n * agents, clés d'API — sans qu'aucun projet n'ait encore été ouvert ici, et\n * l'ignorer alors reviendrait à les perdre au prochain démarrage.\n */\nexport async function listOrgVaults(): Promise<string[]> {\n const orgsDir = join(machineRoot(), 'orgs');\n try {\n return (await readdir(orgsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory() && SAFE_ID.test(e.name))\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\n/** One cloud project vault found on disk. */\nexport interface CloudVault {\n orgId: string;\n projectId: string;\n root: string;\n}\n\n/**\n * Every cloud project vault present on this machine, by walking\n * `~/.runeya/orgs/<orgId>/projects/<projectId>`.\n *\n * Discovery reads the disk rather than an index on purpose: an index is one\n * more thing that can disagree with what is actually there, and a project\n * present on disk but missing from it is precisely the \"already here, still\n * offered as a download\" bug. Unreadable or foreign directories are skipped in\n * silence — a half-written pull must not take a whole load down.\n */\nexport async function listCloudVaults(): Promise<CloudVault[]> {\n const orgsDir = join(machineRoot(), 'orgs');\n const found: CloudVault[] = [];\n\n let orgIds: string[];\n try {\n orgIds = (await readdir(orgsDir, { withFileTypes: true }))\n .filter((e) => e.isDirectory() && SAFE_ID.test(e.name))\n .map((e) => e.name);\n } catch {\n return found;\n }\n\n for (const orgId of orgIds) {\n const projectsDir = join(orgsDir, orgId, 'projects');\n try {\n for (const entry of await readdir(projectsDir, { withFileTypes: true })) {\n if (!entry.isDirectory() || !SAFE_ID.test(entry.name)) continue;\n const root = join(projectsDir, entry.name);\n // Un répertoire n'est pas un projet : il faut qu'il en porte la fiche.\n //\n // Tout ce qui traînait ici était pris pour un coffre — une copie de\n // sauvegarde, un dossier oublié — et les stores le lisaient, le\n // comptaient, pouvaient le réécrire. Une sauvegarde déposée à côté\n // d'un projet s'est ainsi retrouvée annoncée comme dix-neuf\n // conversations illisibles appartenant à l'organisation.\n //\n // `project.json` est déjà ce qui fait foi ailleurs (`isMaterialized`\n // dans org-sync-scope) : la même règle vaut ici, pour tout le monde.\n if (!(await access(join(root, 'project.json')).then(() => true, () => false))) continue;\n found.push({ orgId, projectId: entry.name, root });\n }\n } catch {\n // Org directory without materialized projects yet.\n }\n }\n return found;\n}\n\n/**\n * Every vault holding data, local first — what stores must read to see it all.\n *\n * Derived from the disk, so it needs no store to have loaded beforehand: a\n * content store can resolve where to read without waiting on the project store.\n */\nexport async function allVaultRoots(): Promise<string[]> {\n const cloud = await listCloudVaults();\n const locals = (await readLocalProjectDirs()).map(localProjectVaultRoot);\n return [...new Set([localVaultRoot(), ...locals, ...cloud.map((v) => v.root)])];\n}\n\n/**\n * A file belonging to the machine rather than to any project.\n *\n * Settings, the audit log, caches: none of them describe a project, and keeping\n * them in the launch directory forced a `.runeya` to exist for someone working\n * only on cloud projects — the very thing vaults remove.\n *\n * For anything ENCRYPTED, use `keyedFilePath` instead.\n */\nexport function machineFilePath(filename: string): string {\n return join(machineRoot(), filename);\n}\n\n/**\n * A machine file whose contents are encrypted.\n *\n * It lives beside the key that opens it, never merely beside the machine. The\n * two are usually the same directory — but a launch directory that owns its own\n * key keeps it (see `keyRoot`), and a file encrypted under that key read from\n * elsewhere would decrypt to blanks, then be saved back blank. Tying the file to\n * its key is what makes that impossible.\n */\nexport async function keyedFilePath(filename: string): Promise<string> {\n return join(await keyRoot(), filename);\n}\n\n/**\n * Where such a file was kept by earlier versions: in the launch directory.\n *\n * Read as a fallback and never written, so a directory shared with an older\n * Runeya keeps working.\n */\nexport function legacyMachineFilePath(filename: string): string {\n return join(localVaultRoot(), filename);\n}\n\n/**\n * Create the local vault, on the way to writing a local project into it.\n *\n * The directory is created here and nowhere else. Runeya no longer makes a\n * `.runeya` just because it was started somewhere: a machine used only for\n * cloud projects should leave the directories it visits untouched, and a\n * `.runeya` appearing in a repository should mean \"there is a local project\n * here\" rather than \"Runeya ran here once\".\n */\nexport async function ensureLocalVault(): Promise<string> {\n const root = localVaultRoot();\n await mkdir(root, { recursive: true });\n return root;\n}\n\n/** Files that mark a directory as already holding an encryption key. */\nconst KEY_FILES = ['.encryption.key.enc', '.runeya-verify'];\n\nlet keyRootPromise: Promise<string> | null = null;\n\n/** True when a directory already holds an encryption key of its own. */\nasync function holdsKey(root: string): Promise<boolean> {\n for (const filename of KEY_FILES) {\n try {\n await access(join(root, filename));\n return true;\n } catch {\n // Not here — keep looking.\n }\n }\n return false;\n}\n\n/**\n * Where this machine's encryption key and its canary live.\n *\n * The key protects everything on the machine — agents, sessions, and the\n * environments of every vault — so there is one, not one per vault, and the\n * machine root is its home.\n *\n * Earlier versions let a launch directory keep a key of its own, which is how a\n * machine ended up with several master passwords and several cloud identities.\n * Such a directory is no longer promoted: the machine key answers as soon as it\n * exists, and the leftover local key is surfaced by `legacyVaultRoot()` so it\n * can be migrated rather than silently obeyed. A machine that has never had a\n * machine key still falls back to the local one, so an un-migrated install\n * keeps opening exactly as before.\n *\n * Resolved once per process: the answer must not change under a running server,\n * and a key written mid-session would otherwise move its own home.\n */\nexport function keyRoot(): Promise<string> {\n keyRootPromise ??= (async () => {\n if (await holdsKey(machineRoot())) return machineRoot();\n return (await holdsKey(localVaultRoot())) ? localVaultRoot() : machineRoot();\n })();\n return keyRootPromise;\n}\n\n/**\n * The launch directory's own key, when the machine key is what we opened.\n *\n * Its presence is the whole trigger for the migration page: this directory\n * holds data encrypted under a key nothing reads any more. Null once migrated,\n * and null too when the local key IS the one in use — there is nothing to\n * migrate then.\n */\nexport async function legacyVaultRoot(): Promise<string | null> {\n const local = localVaultRoot();\n if ((await keyRoot()) === local) return null;\n return (await holdsKey(local)) ? local : null;\n}\n\n/** Test seam: forget the resolved key home. */\nexport function resetKeyRoot(): void {\n keyRootPromise = null;\n}\n","import { z } from 'zod';\nimport { resolve as resolvePath } from 'node:path';\nimport { LAUNCHER_PROJECT_ID_PATTERN } from '@runeya/packages-shared';\n\n/**\n * Every variable Runeya reads from the process environment is prefixed with\n * `RUNEYA_`. Generic names (PORT, HOST, MODE, DATA_DIR…) are deliberately NOT\n * read: Runeya loads the `.env` of the stack it manages, where those names very\n * often belong to the user's own services.\n *\n * The resolved object keeps short field names (`env.PORT`, `env.HOST`, …) —\n * only the environment keys are prefixed.\n */\n\n// Server port, used to derive the default agent port\nconst serverPort = z.coerce.number().int().min(1).max(65535).default(4000).parse(process.env.RUNEYA_HTTP_PORT);\n\nconst defaultAgentPort = process.env.RUNEYA_AGENT_PORT\n ? z.coerce.number().int().min(1).max(65535).parse(process.env.RUNEYA_AGENT_PORT)\n : serverPort + 1;\n\nconst EnvSchema = z.object({\n RUNEYA_HTTP_PORT: z.coerce.number().int().min(1).max(65535).default(4000),\n RUNEYA_HOST: z.string().default('127.0.0.1'),\n RUNEYA_AGENT_PORT: z.coerce.number().int().min(1).max(65535).default(defaultAgentPort),\n RUNEYA_AGENT_HOST: z.string().default('127.0.0.1'),\n RUNEYA_AGENT_BINARY_PATH: z.string().nullable().default(null),\n RUNEYA_DATA_DIR: z.preprocess((v) => (typeof v === 'string' && v.length > 0 ? v : undefined), z.string().default('.runeya')),\n // Home holding the cloud vaults (`<home>/.runeya/orgs/...`). Defaults to the\n // user's home directory; overridden by tests so they never write to the real\n // one, and usable to relocate the vaults on a shared machine.\n RUNEYA_HOME: z.preprocess((v) => (typeof v === 'string' && v.length > 0 ? v : undefined), z.string().nullable().default(null)),\n RUNEYA_CLOUD_URL: z.string().url().nullable().default(null),\n // Cloud front-end (landing) URL — used by the web app for the registration /\n // onboarding flow. Configured on the server at launch; the web fetches it.\n RUNEYA_CLOUD_APP_URL: z.string().url().nullable().default(null),\n RUNEYA_MODE: z.enum(['local-simple', 'cloud']).default('local-simple'),\n RUNEYA_CORS_ORIGIN: z.string().nullable().default(null),\n RUNEYA_LAN_PROXY: z.preprocess((v) => v === 'true' || v === '1', z.boolean()).default(false),\n // Set by the CLI when it was started from a desktop launcher rather than a\n // terminal. The web app surfaces it so the user knows which Runeya they are\n // looking at when several are running.\n RUNEYA_FROM_DESKTOP: z.preprocess((v) => v === 'true' || v === '1', z.boolean()).default(false),\n // `--project <id>` : l'instance n'ouvre que ce projet. L'interface va droit\n // au choix des services et ne propose ni la liste ni le changement de projet.\n RUNEYA_PROJECT_ID: z.preprocess(\n (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined),\n z.string().regex(LAUNCHER_PROJECT_ID_PATTERN, 'RUNEYA_PROJECT_ID: expected a project id').nullable().default(null),\n ),\n // Label shown for this machine in the cloud's \"linked machines\" screen.\n // Defaults to the hostname (see `getDeviceName`); set it explicitly when a\n // single host runs several instances and you want to tell them apart.\n RUNEYA_DEVICE_NAME: z.preprocess(\n (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined),\n z.string().max(64).nullable().default(null),\n ),\n // Lifetime of the JWTs signed by the server (UI session and AI agents).\n // Meant for reproducing an expiry locally, e.g. RUNEYA_JWT_TTL=4s.\n RUNEYA_JWT_TTL: z.preprocess(\n (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined),\n z.string().regex(/^[1-9]\\d{0,5}(s|m|h|d)$/, 'RUNEYA_JWT_TTL: expected e.g. 4s, 30m, 8h, 1d').default('8h'),\n ),\n // La commande écrite dans la config des outils IA pour lancer le MCP, à la\n // place de `runeya` (voir `services/ai-client-mcp.ts`). Pour le dev : faire\n // pointer ces outils vers le CLI du dépôt sans toucher au `runeya` du PATH,\n // par exemple `node /chemin/vers/apps/cli/dist/index.js`. Découpée sur les\n // espaces : un chemin qui en contient n'est pas pris en charge.\n RUNEYA_MCP_COMMAND: z.preprocess(\n (v) => (typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined),\n z.string().max(4096).optional(),\n ),\n}).transform((data) => ({\n ...data,\n // Short aliases used throughout the server code\n PORT: data.RUNEYA_HTTP_PORT,\n HOST: data.RUNEYA_HOST,\n MODE: data.RUNEYA_MODE,\n /**\n * Absolute from the start, resolved once against the launch directory.\n *\n * It defaults to a relative `.runeya`, and every caller used to re-resolve it\n * against whatever the process's cwd happened to be. A vault that moves when\n * the process changes directory is a vault that can be silently written to the\n * wrong place — so the answer is fixed here, at boot, and never recomputed.\n */\n DATA_DIR: resolvePath(data.RUNEYA_DATA_DIR),\n HOME_DIR: data.RUNEYA_HOME,\n CLOUD_URL: data.RUNEYA_CLOUD_URL,\n CLOUD_APP_URL: data.RUNEYA_CLOUD_APP_URL,\n CORS_ORIGIN: data.RUNEYA_CORS_ORIGIN,\n AGENT_BINARY_PATH: data.RUNEYA_AGENT_BINARY_PATH,\n DEVICE_NAME: data.RUNEYA_DEVICE_NAME,\n FROM_DESKTOP: data.RUNEYA_FROM_DESKTOP,\n PROJECT_ID: data.RUNEYA_PROJECT_ID,\n JWT_TTL: data.RUNEYA_JWT_TTL,\n MCP_COMMAND: data.RUNEYA_MCP_COMMAND,\n}));\n\nexport type Env = z.infer<typeof EnvSchema>;\n\nexport const env = EnvSchema.parse(process.env);\n"],"mappings":"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","names":[]}
@@ -6,7 +6,9 @@ import {
6
6
  buildCompanionPrompt,
7
7
  buildRelaunchMessage,
8
8
  companionAgentId,
9
+ companionCreateDefaults,
9
10
  companionDefaultPrompt,
11
+ companionInstructions,
10
12
  companionSettings,
11
13
  companionSkips,
12
14
  companionSystemNote,
@@ -29,13 +31,13 @@ import {
29
31
  runCompanionNow,
30
32
  stopCompanion,
31
33
  takeRelaunchSlot
32
- } from "./chunk-NSS47H26.js";
33
- import "./chunk-GXE5TLLJ.js";
34
- import "./chunk-7L6AQUBR.js";
35
- import "./chunk-MD3VW4HQ.js";
36
- import "./chunk-XAFOP677.js";
37
- import "./chunk-IXO5KIO5.js";
34
+ } from "./chunk-UD42LACH.js";
35
+ import "./chunk-KV4GJKXU.js";
38
36
  import "./chunk-OZBSOSAS.js";
37
+ import "./chunk-4LJQIQMN.js";
38
+ import "./chunk-MD3VW4HQ.js";
39
+ import "./chunk-VPSQJOSN.js";
40
+ import "./chunk-MZSDTA4A.js";
39
41
  export {
40
42
  COMPANION_SIGNATURE,
41
43
  MAX_CONSECUTIVE_RELAUNCHES,
@@ -44,7 +46,9 @@ export {
44
46
  buildCompanionPrompt,
45
47
  buildRelaunchMessage,
46
48
  companionAgentId,
49
+ companionCreateDefaults,
47
50
  companionDefaultPrompt,
51
+ companionInstructions,
48
52
  companionSettings,
49
53
  companionSkips,
50
54
  companionSystemNote,
@@ -68,4 +72,4 @@ export {
68
72
  stopCompanion,
69
73
  takeRelaunchSlot
70
74
  };
71
- //# sourceMappingURL=companion-2LHKU577.js.map
75
+ //# sourceMappingURL=companion-W3UMZWGH.js.map
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  launcherProblems
3
- } from "./chunk-7HIPFSOA.js";
4
- import {
5
- deriveLauncherIdentity
6
- } from "./chunk-XAFOP677.js";
3
+ } from "./chunk-57P45FNY.js";
7
4
  import {
8
5
  shellArg,
9
6
  whereToFind
@@ -12,6 +9,9 @@ import {
12
9
  findFreePortPair,
13
10
  isPortFree
14
11
  } from "./chunk-C2BGNVBT.js";
12
+ import {
13
+ deriveLauncherIdentity
14
+ } from "./chunk-MZSDTA4A.js";
15
15
 
16
16
  // src/desktop-installer-page.ts
17
17
  import { createServer } from "http";
@@ -236,7 +236,7 @@ const T = {
236
236
  portsBusy: (a, b) => 'Les ports ' + a + ' et ' + b + ' sont d\xE9j\xE0 utilis\xE9s sur cette machine',
237
237
  cancel: 'Fermer', install: 'Cr\xE9er le raccourci', installing: 'Cr\xE9ation\u2026',
238
238
  existingTitle: 'Raccourcis existants', global: 'Global (dossier personnel)',
239
- portsOf: (p, a) => 'Ports ' + p + ' \xB7 ' + a, defaultPorts: 'Ports par d\xE9faut',
239
+ portsOf: (p, a) => 'Ports ' + p + ' \xB7 ' + a, defaultPorts: 'Ports par d\xE9faut', projectOf: (id) => 'Ouvre le projet ' + id,
240
240
  outdated: 'Vise un autre Node ou une autre installation de Runeya',
241
241
  recreate: 'Recr\xE9er', removeAction: 'Supprimer', confirmRemove: 'Confirmer',
242
242
  working: '\u2026',
@@ -272,7 +272,7 @@ const T = {
272
272
  portsBusy: (a, b) => 'Ports ' + a + ' and ' + b + ' are already in use on this machine',
273
273
  cancel: 'Close', install: 'Create shortcut', installing: 'Creating\u2026',
274
274
  existingTitle: 'Existing shortcuts', global: 'Global (home folder)',
275
- portsOf: (p, a) => 'Ports ' + p + ' \xB7 ' + a, defaultPorts: 'Default ports',
275
+ portsOf: (p, a) => 'Ports ' + p + ' \xB7 ' + a, defaultPorts: 'Default ports', projectOf: (id) => 'Opens project ' + id,
276
276
  outdated: 'Points to another Node or another Runeya install',
277
277
  recreate: 'Recreate', removeAction: 'Remove', confirmRemove: 'Confirm',
278
278
  working: '\u2026',
@@ -524,6 +524,7 @@ function renderLauncher(l) {
524
524
  body.append(el('strong', '', l.name));
525
525
  body.append(el('span', '', l.workingDir ?? t.global));
526
526
  body.append(el('span', '', l.port === null ? t.defaultPorts : t.portsOf(l.port, l.agentPort)));
527
+ if (l.projectId) body.append(el('span', '', t.projectOf(l.projectId)));
527
528
  for (const p of l.problems) body.append(el('span', 'problem', p));
528
529
  if (l.outdated && !l.problems.length) body.append(el('span', 'stale', t.outdated));
529
530
  row.append(body);
@@ -623,6 +624,7 @@ async function toView(l, current) {
623
624
  workingDir: l.workingDir,
624
625
  port: l.port,
625
626
  agentPort: l.agentPort,
627
+ projectId: l.projectId,
626
628
  paths: l.paths,
627
629
  problems: await launcherProblems(l),
628
630
  outdated: l.nodePath !== current.nodePath || l.cliEntry !== current.cliEntry
@@ -713,7 +715,8 @@ async function runInstallerPage(opts) {
713
715
  name: launcher.name,
714
716
  workingDir: launcher.workingDir,
715
717
  port: launcher.port,
716
- agentPort: launcher.agentPort
718
+ agentPort: launcher.agentPort,
719
+ projectId: launcher.projectId
717
720
  });
718
721
  return send(res, 200, { paths: result.paths, warnings: result.warnings });
719
722
  } catch (err) {
@@ -788,4 +791,4 @@ export {
788
791
  parseInstallRequest,
789
792
  runInstallerPage
790
793
  };
791
- //# sourceMappingURL=desktop-installer-page-6JDSXK2E.js.map
794
+ //# sourceMappingURL=desktop-installer-page-I5FHFDES.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/desktop-installer-page.ts","../src/desktop-installer-html.ts"],"sourcesContent":["/**\n * La page d'installation du raccourci de bureau, servie par `runeya install-desktop`.\n *\n * Runeya n'est PAS démarrée : ni agent, ni répertoire de données. Un serveur\n * HTTP éphémère, sur la boucle locale et un port tiré par le système, sert une\n * page dédiée où l'on choisit le nom et les ports. Le raccourci créé ici est\n * toujours global ; celui d'un projet se crée depuis l'app, dans les réglages. Le raccourci\n * écrit, il s'arrête et rend la main au terminal.\n *\n * Deux gardes, parce qu'un port local est joignable par n'importe quel onglet :\n * l'en-tête `Host` doit nommer ce serveur (pas de rebinding DNS), et une\n * écriture doit venir de sa propre origine, en JSON — ce qu'un formulaire d'un\n * autre site ne peut pas envoyer sans pré-vol.\n */\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { isAbsolute } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { deriveIdentity, launcherProblems, type InstalledLauncher, type LauncherResult } from './desktop-launcher.js';\nimport { findFreePortPair, isPortFree } from './free-port.js';\nimport { shellArg, whereToFind } from './desktop-wizard.js';\nimport { installerHtml } from './desktop-installer-html.js';\n\n/** Le port de Runeya, prérempli quand aucune paire libre n'a été trouvée. */\nconst DEFAULT_PORT = 4000;\n\n/** Au-delà, une requête n'est pas un formulaire de quatre champs. */\nconst MAX_BODY_BYTES = 16 * 1024;\n\nexport type InstallerAnswers = {\n workingDir: string | null;\n name: string;\n port: number | null;\n agentPort: number | null;\n /** Projet ouvert directement : repris d'un raccourci recréé, jamais saisi dans la page. */\n projectId?: string | null;\n};\n\n/** Ce que la page affiche avant tout choix. */\nexport type InstallerState = {\n /** Le nom proposé : celui d'un raccourci global. */\n name: string;\n /** Une paire libre tirée au hasard, `null` si aucune n'a été trouvée. */\n suggestedPort: number | null;\n defaultPort: number;\n /** Ouverte par un `runeya` lancé hors raccourci : la page l'explique. */\n required: boolean;\n /** Runeya est-il installé globalement ? Sinon, la page propose de le faire avant le raccourci. */\n global: GlobalInstallState;\n};\n\nexport type GlobalInstallState = {\n installed: boolean;\n /** Le préfixe où Runeya sera installé, `null` si npm ne répond pas. */\n prefix: string | null;\n /** Version d'une installation plus ancienne que la copie qui tourne : l'étape 1 est une mise à jour. */\n installedVersion: string | null;\n};\n\n/** Ce que la page sait faire de l'installation globale. */\nexport type GlobalInstaller = {\n state: () => Promise<GlobalInstallState>;\n install: () => Promise<{ ok: boolean; output: string }>;\n};\n\nfunction isPort(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65_535;\n}\n\n/**\n * Valider le corps d'une installation. Rejette plutôt que de corriger : un nom\n * trop long ou un port hors borne est une erreur à montrer, pas à raboter.\n */\nexport function parseInstallRequest(body: unknown): InstallerAnswers | string {\n if (typeof body !== 'object' || body === null) return 'Requête invalide.';\n const b = body as Record<string, unknown>;\n\n if (typeof b['name'] !== 'string') return 'Nom manquant.';\n const name = b['name'].trim();\n if (name.length === 0) return 'Le nom ne peut pas être vide.';\n if (name.length > 120) return 'Le nom ne doit pas dépasser 120 caractères.';\n\n const workingDir = b['workingDir'];\n if (workingDir !== null && (typeof workingDir !== 'string' || workingDir.length > 4096 || !isAbsolute(workingDir))) {\n return 'Le dossier de travail doit être un chemin absolu.';\n }\n\n const port = b['port'];\n const agentPort = b['agentPort'];\n if (port !== null && !isPort(port)) return 'Port de Runeya invalide.';\n if (agentPort !== null && !isPort(agentPort)) return 'Port de l’agent invalide.';\n if ((port === null) !== (agentPort === null)) return 'Les deux ports vont ensemble.';\n if (port !== null && port === agentPort) return 'Runeya et l’agent ne peuvent pas partager un port.';\n\n return { name, workingDir: workingDir as string | null, port: port as number | null, agentPort: agentPort as number | null };\n}\n\n/** L'en-tête `Host` nomme-t-il ce serveur ? */\nexport function isOwnHost(host: string | undefined, port: number): boolean {\n return host === `127.0.0.1:${port}` || host === `localhost:${port}`;\n}\n\nasync function readJson(req: IncomingMessage): Promise<unknown> {\n let size = 0;\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n size += (chunk as Buffer).length;\n if (size > MAX_BODY_BYTES) throw new Error('too-large');\n chunks.push(chunk as Buffer);\n }\n return JSON.parse(Buffer.concat(chunks).toString('utf8'));\n}\n\nfunction send(res: ServerResponse, status: number, body: unknown, type = 'application/json; charset=utf-8'): void {\n res.writeHead(status, {\n 'Content-Type': type,\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n 'Referrer-Policy': 'no-referrer',\n 'Content-Security-Policy': \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src data:\",\n });\n res.end(typeof body === 'string' ? body : JSON.stringify(body));\n}\n\nexport type InstallerOutcome = { answers: InstallerAnswers; result: LauncherResult };\n\n/** Un raccourci existant, tel que la page le montre. */\nexport type LauncherView = {\n id: string;\n name: string;\n workingDir: string | null;\n port: number | null;\n agentPort: number | null;\n projectId: string | null;\n paths: string[];\n /** Ce qui l'empêcherait de démarrer. */\n problems: string[];\n /** Il vise un autre Node ou un autre Runeya que celui-ci : le recréer le repointe. */\n outdated: boolean;\n};\n\nasync function toView(l: InstalledLauncher, current: { nodePath: string; cliEntry: string }): Promise<LauncherView> {\n return {\n id: l.id,\n name: l.name,\n workingDir: l.workingDir,\n port: l.port,\n agentPort: l.agentPort,\n projectId: l.projectId,\n paths: l.paths,\n problems: await launcherProblems(l),\n outdated: l.nodePath !== current.nodePath || l.cliEntry !== current.cliEntry,\n };\n}\n\n/** Le corps d'une action sur un raccourci existant : son id, rien d'autre. */\nfunction parseLauncherRef(body: unknown): string | null {\n const id = (body as { id?: unknown } | null)?.id;\n return typeof id === 'string' && id.length > 0 && id.length <= 200 ? id : null;\n}\n\n/**\n * Servir la page et attendre qu'on y installe — ou qu'on y renonce.\n *\n * `install` écrit le raccourci ; il est appelé ici, dans la requête, pour que la\n * page affiche le résultat réel et non une promesse. Rend `null` si l'on annule\n * depuis la page ou par Ctrl+C.\n */\nexport async function runInstallerPage(opts: {\n openUrl: (url: string) => Promise<unknown>;\n install: (answers: InstallerAnswers) => Promise<LauncherResult>;\n /** Les raccourcis déjà posés, relus à chaque demande. */\n list: () => Promise<InstalledLauncher[]>;\n uninstall: (launcher: InstalledLauncher) => Promise<LauncherResult>;\n /**\n * Le Node et l'entrée du CLI que viseront les raccourcis. Relu à chaque\n * requête : l'installation globale le fait pointer vers la copie installée.\n */\n current: { nodePath: string; cliEntry: string };\n global: GlobalInstaller;\n required?: boolean;\n}): Promise<InstallerOutcome | null> {\n const suggestedPort = await findFreePortPair();\n const state: InstallerState = {\n name: deriveIdentity(undefined, null).name,\n suggestedPort,\n defaultPort: DEFAULT_PORT,\n required: opts.required ?? false,\n global: await opts.global.state(),\n };\n\n return new Promise((resolvePromise) => {\n let settled = false;\n let installing = false;\n\n const server = createServer((req, res) => {\n void handle(req, res).catch(() => send(res, 500, { error: 'Erreur interne.' }));\n });\n\n function finish(outcome: InstallerOutcome | null): void {\n if (settled) return;\n settled = true;\n process.off('SIGINT', onSigint);\n // Laisser partir la réponse en cours avant de couper les connexions.\n setImmediate(() => {\n server.close();\n server.closeAllConnections();\n resolvePromise(outcome);\n });\n }\n\n function onSigint(): void {\n console.log('\\nAnnulé.');\n finish(null);\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const { port } = server.address() as AddressInfo;\n if (!isOwnHost(req.headers.host, port)) return send(res, 421, { error: 'Hôte inattendu.' });\n\n const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);\n\n if (req.method === 'GET' && url.pathname === '/') {\n return send(res, 200, installerHtml(), 'text/html; charset=utf-8');\n }\n if (req.method === 'GET' && url.pathname === '/api/state') {\n return send(res, 200, state);\n }\n if (req.method === 'GET' && url.pathname === '/api/ports') {\n // Vérifiés pendant la saisie : chaque port demandé, libre ou non.\n const ports = url.searchParams.getAll('port').map(Number).filter(isPort).slice(0, 2);\n const free = await Promise.all(ports.map((p) => isPortFree(p)));\n return send(res, 200, Object.fromEntries(ports.map((p, i) => [p, free[i]])));\n }\n if (req.method === 'GET' && url.pathname === '/api/launchers') {\n const launchers = await opts.list();\n return send(res, 200, await Promise.all(launchers.map((l) => toView(l, opts.current))));\n }\n\n if (req.method === 'POST') {\n const origin = req.headers.origin;\n if (!origin || !isOwnHost(origin.replace(/^http:\\/\\//, ''), port)) {\n return send(res, 403, { error: 'Origine refusée.' });\n }\n if (!req.headers['content-type']?.startsWith('application/json')) {\n return send(res, 415, { error: 'JSON attendu.' });\n }\n\n if (url.pathname === '/api/cancel') {\n send(res, 200, { ok: true });\n console.log('\\nAnnulé depuis la page.');\n return finish(null);\n }\n\n if (url.pathname === '/api/uninstall' || url.pathname === '/api/reinstall') {\n if (installing || settled) return send(res, 409, { error: 'Une opération est déjà en cours.' });\n let body: unknown;\n try {\n body = await readJson(req);\n } catch {\n return send(res, 400, { error: 'Requête invalide.' });\n }\n const id = parseLauncherRef(body);\n // Seul un raccourci relu sur le disque peut être visé : un nom reçu\n // tel quel deviendrait un chemin à supprimer.\n const launcher = id === null ? undefined : (await opts.list()).find((l) => l.id === id);\n if (!launcher) return send(res, 404, { error: 'Raccourci introuvable.' });\n if (url.pathname === '/api/reinstall' && !state.global.installed) {\n return send(res, 409, { error: 'Installez d’abord Runeya globalement.' });\n }\n\n installing = true;\n try {\n const result = url.pathname === '/api/uninstall'\n ? await opts.uninstall(launcher)\n : await opts.install({\n name: launcher.name,\n workingDir: launcher.workingDir,\n port: launcher.port,\n agentPort: launcher.agentPort,\n projectId: launcher.projectId,\n });\n return send(res, 200, { paths: result.paths, warnings: result.warnings });\n } catch (err) {\n return send(res, 500, { error: (err as Error).message });\n } finally {\n installing = false;\n }\n }\n\n if (url.pathname === '/api/install-global') {\n if (installing || settled) return send(res, 409, { error: 'Une opération est déjà en cours.' });\n installing = true;\n try {\n const result = await opts.global.install();\n if (result.ok) state.global = await opts.global.state();\n return send(res, result.ok ? 200 : 500, { ok: result.ok, output: result.output, global: state.global });\n } catch (err) {\n return send(res, 500, { error: (err as Error).message });\n } finally {\n installing = false;\n }\n }\n\n if (url.pathname === '/api/install') {\n if (installing || settled) return send(res, 409, { error: 'Installation déjà en cours.' });\n // Un raccourci vers une copie éphémère (npx, clone) cesserait de\n // marcher sans prévenir : l'installation globale passe d'abord.\n if (!state.global.installed) return send(res, 409, { error: 'Installez d’abord Runeya globalement.' });\n let body: unknown;\n try {\n body = await readJson(req);\n } catch {\n return send(res, 400, { error: 'Requête invalide.' });\n }\n const parsed = parseInstallRequest(body);\n if (typeof parsed === 'string') return send(res, 400, { error: parsed });\n // Revérifié ici : un port libre à la saisie a pu être pris depuis.\n for (const p of [parsed.port, parsed.agentPort]) {\n if (p !== null && !(await isPortFree(p))) return send(res, 409, { error: `Le port ${p} est déjà utilisé sur cette machine.` });\n }\n // Toujours global d'ici : un raccourci vers un projet se crée depuis l'app.\n const answers: InstallerAnswers = { ...parsed, workingDir: null };\n\n installing = true;\n try {\n const result = await opts.install(answers);\n send(res, 200, {\n name: answers.name,\n paths: result.paths,\n warnings: result.warnings,\n whereToFind: whereToFind(),\n uninstall: `runeya uninstall-desktop --name ${shellArg(answers.name)}`,\n });\n return finish({ answers, result });\n } catch (err) {\n // L'échec reste sur la page : corriger un champ et réessayer.\n return send(res, 500, { error: (err as Error).message });\n } finally {\n installing = false;\n }\n }\n }\n\n send(res, 404, { error: 'Introuvable.' });\n }\n\n process.on('SIGINT', onSigint);\n server.listen(0, '127.0.0.1', () => {\n const { port } = server.address() as AddressInfo;\n const url = `http://127.0.0.1:${port}/`;\n console.log(`\\nInstallateur du raccourci Runeya : ${url}`);\n console.log('Choisissez les réglages dans la page. Ctrl+C pour annuler.');\n opts.openUrl(url).catch(() => {\n console.log('Impossible d’ouvrir le navigateur — ouvrez l’adresse ci-dessus.');\n });\n });\n });\n}\n","/**\n * La page de l'installateur de raccourci, autonome : ni police, ni script, ni\n * feuille externes — le serveur qui la sert refuse tout le reste (CSP), et elle\n * doit s'afficher hors ligne.\n *\n * Deux langues, choisies d'après le navigateur ; le français par défaut, comme\n * les messages du CLI.\n */\nexport function installerHtml(): string {\n return `<!doctype html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Installer Runeya</title>\n<link rel=\"icon\" href=\"data:image/svg+xml,${encodeURIComponent(MARK)}\">\n<style>\n:root {\n --bg: #f6f6f9; --surface: #ffffff; --border: #e3e3ea; --text: #1d1d27;\n --muted: #6b6b7b; --primary: #6366f1; --primary-text: #ffffff;\n --danger: #dc2626; --ok: #16a34a; --warn: #b45309;\n --ease: cubic-bezier(0.23, 1, 0.32, 1);\n color-scheme: light;\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --bg: #111118; --surface: #1a1a24; --border: #2c2c3a; --text: #ececf2;\n --muted: #9a9aab; --primary: #818cf8; --primary-text: #111118;\n --danger: #f87171; --ok: #4ade80; --warn: #fbbf24;\n color-scheme: dark;\n }\n}\n* { box-sizing: border-box; }\nbody {\n margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;\n padding: 3rem 1rem; background: var(--bg); color: var(--text);\n font: 15px/1.5 system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n}\nmain { width: 100%; max-width: 1600px; display: flex; flex-direction: column; gap: 1.5rem;\n animation: enter 400ms var(--ease) both; }\nheader { display: flex; flex-direction: column; align-items: center; text-align: center; gap: .5rem; }\nheader svg { width: 48px; height: 48px; }\nh1 { margin: .25rem 0 0; font-size: 1.375rem; font-weight: 650; letter-spacing: -.01em; }\n.lead { margin: 0; color: var(--muted); font-size: .875rem; }\n.card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 1.25rem;\n display: flex; flex-direction: column; gap: 1.25rem; box-shadow: 0 1px 2px rgb(0 0 0 / .04); }\nfieldset { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }\nlegend, label.title { font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;\n color: var(--muted); margin-bottom: .5rem; padding: 0; }\n.choice { display: flex; gap: .75rem; align-items: flex-start; padding: .75rem .875rem; border: 1px solid var(--border);\n border-radius: 10px; cursor: pointer; transition: border-color 150ms ease, box-shadow 150ms ease; }\n.choice:has(input:checked) { border-color: var(--primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); }\n.choice input { margin: .2rem 0 0; accent-color: var(--primary); }\n.choice strong { display: block; font-weight: 600; font-size: .9375rem; }\n.choice span { display: block; color: var(--muted); font-size: .8125rem; overflow-wrap: anywhere; }\n.choice .warn { color: var(--danger); }\ninput[type=text], input[type=number] { width: 100%; padding: .625rem .75rem; border: 1px solid var(--border);\n border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }\ninput:focus-visible, button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }\n.ports { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }\n.ports label { font-size: .8125rem; color: var(--muted); display: flex; flex-direction: column; gap: .25rem; }\n[hidden] { display: none !important; }\n.actions { display: flex; gap: .75rem; }\nbutton { flex: 1; padding: .7rem 1rem; border-radius: 10px; border: 1px solid transparent; font: inherit;\n font-weight: 600; cursor: pointer; transition: transform 160ms var(--ease), opacity 150ms ease; }\nbutton:active { transform: scale(.97); }\nbutton:disabled { opacity: .6; cursor: default; }\n.primary { background: var(--primary); color: var(--primary-text); }\n.secondary { flex: 0 0 auto; background: transparent; color: var(--muted); border-color: var(--border); }\n.error { margin: 0; color: var(--danger); font-size: .875rem; }\n.done { text-align: center; align-items: center; width: 100%; max-width: 520px; align-self: center; }\n.done .check { width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center;\n background: color-mix(in srgb, var(--ok) 15%, var(--surface)); color: var(--ok); font-size: 1.4rem; }\n.done h2 { margin: 0; font-size: 1.125rem; }\n.done dl { width: 100%; margin: 0; text-align: left; display: flex; flex-direction: column; gap: .75rem; }\n.done dt { font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }\n.done dd { margin: .25rem 0 0; font-size: .875rem; overflow-wrap: anywhere; }\ncode { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8125rem;\n background: color-mix(in srgb, var(--text) 7%, var(--surface)); padding: .15rem .4rem; border-radius: 6px; }\n.steps { list-style: none; margin: 0 auto; padding: 0; display: flex; gap: 1.5rem; justify-content: center;\n font-size: .8125rem; color: var(--muted); }\n.steps li { display: flex; align-items: center; gap: .5rem; white-space: nowrap; }\n.steps .num { flex-shrink: 0; width: 1.5rem; height: 1.5rem; border-radius: 50%; display: grid; place-items: center; font-weight: 600;\n border: 1px solid var(--border); transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease; }\n.steps li[aria-current=step] { color: var(--text); font-weight: 600; }\n.steps li[aria-current=step] .num { background: var(--primary); border-color: var(--primary); color: var(--primary-text); }\n.steps li.done .num { border-color: var(--ok); color: var(--ok); }\n.ports-status { grid-column: 1 / -1; margin: 0; font-size: .8125rem; }\n.ports-status.ok { color: var(--ok); }\n.ports-status.busy { color: var(--danger); }\n.global-step { width: 100%; max-width: 520px; align-self: center; gap: .75rem; }\n.global-step h2 { margin: 0; font-size: 1rem; }\n.global-step p { margin: 0; color: var(--muted); font-size: .875rem; }\n.global-step pre { margin: 0; max-height: 14rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;\n font: .75rem/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; padding: .625rem .75rem; border-radius: 8px;\n background: color-mix(in srgb, var(--text) 7%, var(--surface)); }\n.close { color: var(--muted); font-size: .8125rem; margin: 0; }\n.hint { margin: .375rem 0 0; color: var(--muted); font-size: .8125rem; }\n.split { display: grid; grid-template-columns: minmax(0, 520px); justify-content: center; gap: 1.5rem; align-items: start; }\n/*\n * Des raccourcis à gérer : la création reste au centre, à sa largeur ; les\n * existants prennent la colonne de droite, la gauche restant vide pour que le\n * centre ne bouge pas. En dessous de 1200 px, ils passent sous la création.\n */\n.split.has-existing { grid-template-columns: minmax(0, 1fr) minmax(0, 520px) minmax(0, 1fr); }\n.split.has-existing #form { grid-column: 2; grid-row: 1; }\n.split.has-existing .existing { grid-column: 3; grid-row: 1; }\n@media (max-width: 1200px) {\n .split.has-existing { grid-template-columns: minmax(0, 520px); }\n .split.has-existing #form, .split.has-existing .existing { grid-column: 1; grid-row: auto; }\n}\n.existing { gap: .75rem; order: 2; }\n.existing h2 { margin: 0; font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }\n.launcher-list { display: flex; flex-direction: column; gap: .5rem; }\n.launcher { display: flex; gap: .75rem; align-items: center; padding: .75rem .875rem; border: 1px solid var(--border); border-radius: 10px; }\n.launcher__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .125rem; }\n.launcher__body strong { font-weight: 600; font-size: .9375rem; }\n.launcher__body span { color: var(--muted); font-size: .8125rem; overflow-wrap: anywhere; }\n.launcher__body .problem { color: var(--danger); }\n.launcher__body .stale { color: var(--warn); }\n.launcher__actions { display: flex; gap: .375rem; flex-shrink: 0; }\n.launcher__actions button { flex: 0 0 auto; padding: .4rem .7rem; font-size: .8125rem; background: transparent;\n color: var(--text); border-color: var(--border); }\n.launcher__actions .danger { color: var(--danger); }\n.launcher__actions .confirm { background: var(--danger); border-color: var(--danger); color: var(--primary-text); }\n@media (max-width: 480px) { .launcher { flex-direction: column; align-items: stretch; } }\n@keyframes enter { from { opacity: 0; transform: translateY(8px); } }\n@media (prefers-reduced-motion: reduce) { main { animation: none; } button:active { transform: none; } }\n@media (max-width: 480px) { .ports { grid-template-columns: 1fr; } }\n</style>\n</head>\n<body>\n<main>\n <header>\n ${MARK}\n <h1 data-t=\"title\"></h1>\n <p class=\"lead\" data-t=\"lead\"></p>\n </header>\n\n <ol class=\"steps\" id=\"steps\" hidden>\n <li id=\"step-1\"><span class=\"num\" aria-hidden=\"true\">1</span><span data-t=\"step1\"></span></li>\n <li id=\"step-2\"><span class=\"num\" aria-hidden=\"true\">2</span><span data-t=\"step2\"></span></li>\n </ol>\n\n <section class=\"card global-step\" id=\"global-step\" hidden>\n <h2 data-t=\"globalTitle\"></h2>\n <p id=\"global-text\"></p>\n <pre id=\"global-output\" hidden></pre>\n <p class=\"error\" id=\"global-error\" role=\"alert\" hidden></p>\n <div class=\"actions\">\n <button type=\"button\" class=\"secondary\" id=\"global-cancel\" data-t=\"cancel\"></button>\n <button type=\"button\" class=\"primary\" id=\"global-install\" data-t=\"globalInstall\"></button>\n </div>\n </section>\n\n <div class=\"split\" id=\"split\">\n <section class=\"card existing\" id=\"existing\" hidden>\n <h2 data-t=\"existingTitle\"></h2>\n <div id=\"launchers\" class=\"launcher-list\"></div>\n <p class=\"error\" id=\"list-error\" role=\"alert\" hidden></p>\n </section>\n\n <form id=\"form\" class=\"card\" novalidate>\n\n <div>\n <label class=\"title\" for=\"name\" data-t=\"name\"></label>\n <input id=\"name\" type=\"text\" maxlength=\"120\" autocomplete=\"off\" required>\n <p class=\"hint\" data-t=\"globalHint\"></p>\n </div>\n\n <fieldset>\n <legend data-t=\"ports\"></legend>\n <label class=\"choice\" id=\"ports-dedicated-row\"><input type=\"radio\" name=\"ports\" value=\"dedicated\">\n <div><strong data-t=\"portsDedicated\"></strong><span id=\"ports-dedicated\"></span></div></label>\n <label class=\"choice\"><input type=\"radio\" name=\"ports\" value=\"custom\">\n <div><strong data-t=\"portsCustom\"></strong><span data-t=\"portsCustomHint\"></span></div></label>\n <div class=\"ports\" id=\"custom\" hidden>\n <label><span data-t=\"portRuneya\"></span><input id=\"port\" type=\"number\" min=\"1\" max=\"65535\"></label>\n <label><span data-t=\"portAgent\"></span><input id=\"agent-port\" type=\"number\" min=\"1\" max=\"65535\"></label>\n <p class=\"ports-status\" id=\"ports-status\" aria-live=\"polite\" hidden></p>\n </div>\n </fieldset>\n\n <p class=\"error\" id=\"error\" role=\"alert\" hidden></p>\n\n <div class=\"actions\">\n <button type=\"button\" class=\"secondary\" id=\"cancel\" data-t=\"cancel\"></button>\n <button type=\"submit\" class=\"primary\" id=\"submit\" data-t=\"install\"></button>\n </div>\n </form>\n </div>\n\n <section class=\"card done\" id=\"done\" hidden aria-live=\"polite\">\n <div class=\"check\" aria-hidden=\"true\">✓</div>\n <h2 id=\"done-title\"></h2>\n <dl>\n <div><dt data-t=\"where\"></dt><dd id=\"where\"></dd></div>\n <div><dt data-t=\"files\"></dt><dd id=\"files\"></dd></div>\n <div id=\"warnings-row\" hidden><dt data-t=\"warnings\"></dt><dd id=\"warnings\"></dd></div>\n <div><dt data-t=\"remove\"></dt><dd><code id=\"uninstall\"></code></dd></div>\n </dl>\n <p class=\"close\" data-t=\"closeTab\"></p>\n </section>\n\n <section class=\"card done\" id=\"cancelled\" hidden>\n <h2 data-t=\"cancelledTitle\"></h2>\n <p class=\"close\" data-t=\"closeTab\"></p>\n </section>\n</main>\n<script>\nconst T = {\n fr: {\n title: 'Installer Runeya',\n lead: 'Un lanceur dans le menu de votre bureau, pour démarrer Runeya sans terminal.',\n globalHint: 'Le raccourci démarre Runeya dans votre dossier personnel et range les données dans ~/.runeya. Pour un raccourci vers un projet, passez par les réglages de Runeya.',\n name: 'Nom du raccourci', ports: 'Ports',\n portsDedicated: 'Ports dédiés (recommandé)',\n portsCustom: 'Choisir moi-même', portsCustomHint: 'Saisir le port de Runeya, puis celui de l’agent',\n portRuneya: 'Runeya', portAgent: 'Agent',\n dedicatedHint: (p) => p + ' et ' + (p + 1) + ', choisis parmi les ports libres',\n portsChecking: 'Vérification…', portsFree: 'Ports libres sur cette machine',\n portBusy: (p) => 'Le port ' + p + ' est déjà utilisé sur cette machine',\n portsBusy: (a, b) => 'Les ports ' + a + ' et ' + b + ' sont déjà utilisés sur cette machine',\n cancel: 'Fermer', install: 'Créer le raccourci', installing: 'Création…',\n existingTitle: 'Raccourcis existants', global: 'Global (dossier personnel)',\n portsOf: (p, a) => 'Ports ' + p + ' · ' + a, defaultPorts: 'Ports par défaut', projectOf: (id) => 'Ouvre le projet ' + id,\n outdated: 'Vise un autre Node ou une autre installation de Runeya',\n recreate: 'Recréer', removeAction: 'Supprimer', confirmRemove: 'Confirmer',\n working: '…',\n created: (n) => 'Raccourci « ' + n + ' » créé',\n where: 'Où le trouver', files: 'Fichiers écrits', warnings: 'Avertissements', remove: 'Pour le supprimer',\n closeTab: 'Vous pouvez fermer cet onglet.',\n cancelledTitle: 'Installateur fermé',\n nameRequired: 'Donnez un nom au raccourci.',\n portsInvalid: 'Deux ports différents entre 1 et 65535.',\n unreachableCmd: (cmd) => 'L’installateur ne répond plus. Relancez « ' + cmd + ' ».',\n leadRequired: 'Runeya se lance depuis un raccourci de bureau. Créez-le ici, puis démarrez Runeya depuis le menu de votre bureau.',\n globalTitle: 'Installer Runeya sur cette machine',\n step1: 'Installer Runeya', step2: 'Créer le raccourci',\n updateTitle: 'Mettre à jour Runeya sur cette machine', update: 'Mettre à jour Runeya',\n updateText: (v, prefix) => 'Runeya ' + v + ' est installé dans ' + prefix + ', une version plus ancienne que celle-ci. Le raccourci utilisera la version à jour.',\n globalText: (prefix) => 'Le raccourci a besoin d’une installation globale. Runeya sera installé avec npm dans ' + prefix + '.',\n globalNoNpm: 'npm ne répond pas. Installez Runeya avec « npm install -g @runeya/runeya », puis relancez « runeya ».',\n globalInstall: 'Installer Runeya', globalInstalling: 'Installation… (cela peut prendre une minute)',\n globalFailed: 'L’installation a échoué. La sortie de npm est affichée ci-dessus.',\n globalFirst: 'Installez d’abord Runeya.',\n },\n en: {\n title: 'Install Runeya',\n lead: 'A launcher in your desktop menu, to start Runeya without a terminal.',\n globalHint: 'The shortcut starts Runeya in your home folder and stores data in ~/.runeya. For a shortcut to a project, use the Runeya settings.',\n name: 'Shortcut name', ports: 'Ports',\n portsDedicated: 'Dedicated ports (recommended)',\n portsCustom: 'Choose myself', portsCustomHint: 'Enter the Runeya port, then the agent’s',\n portRuneya: 'Runeya', portAgent: 'Agent',\n dedicatedHint: (p) => p + ' and ' + (p + 1) + ', chosen among free ports',\n portsChecking: 'Checking…', portsFree: 'Ports free on this machine',\n portBusy: (p) => 'Port ' + p + ' is already in use on this machine',\n portsBusy: (a, b) => 'Ports ' + a + ' and ' + b + ' are already in use on this machine',\n cancel: 'Close', install: 'Create shortcut', installing: 'Creating…',\n existingTitle: 'Existing shortcuts', global: 'Global (home folder)',\n portsOf: (p, a) => 'Ports ' + p + ' · ' + a, defaultPorts: 'Default ports', projectOf: (id) => 'Opens project ' + id,\n outdated: 'Points to another Node or another Runeya install',\n recreate: 'Recreate', removeAction: 'Remove', confirmRemove: 'Confirm',\n working: '…',\n created: (n) => 'Shortcut “' + n + '” created',\n where: 'Where to find it', files: 'Files written', warnings: 'Warnings', remove: 'To remove it',\n closeTab: 'You can close this tab.',\n cancelledTitle: 'Installer closed',\n nameRequired: 'Give the shortcut a name.',\n portsInvalid: 'Two different ports between 1 and 65535.',\n unreachableCmd: (cmd) => 'The installer no longer answers. Run “' + cmd + '” again.',\n leadRequired: 'Runeya starts from a desktop shortcut. Create it here, then start Runeya from your desktop menu.',\n globalTitle: 'Install Runeya on this machine',\n step1: 'Install Runeya', step2: 'Create the shortcut',\n updateTitle: 'Update Runeya on this machine', update: 'Update Runeya',\n updateText: (v, prefix) => 'Runeya ' + v + ' is installed in ' + prefix + ', older than this one. The shortcut will use the up-to-date version.',\n globalText: (prefix) => 'The shortcut needs a global install. Runeya will be installed with npm into ' + prefix + '.',\n globalNoNpm: 'npm does not answer. Install Runeya with “npm install -g @runeya/runeya”, then run “runeya” again.',\n globalInstall: 'Install Runeya', globalInstalling: 'Installing… (this can take a minute)',\n globalFailed: 'The install failed. The npm output is shown above.',\n globalFirst: 'Install Runeya first.',\n },\n};\nconst lang = (navigator.language || 'fr').toLowerCase().startsWith('fr') ? 'fr' : 'en';\nconst t = T[lang];\ndocument.documentElement.lang = lang;\ndocument.title = t.title;\ndocument.querySelectorAll('[data-t]').forEach((el) => { el.textContent = t[el.dataset.t]; });\n\nconst $ = (id) => document.getElementById(id);\n/** La commande qui a ouvert la page : c'est elle qu'il faut relancer. */\nconst unreachable = () => t.unreachableCmd(state && state.required ? 'runeya' : 'runeya install-desktop');\nconst form = $('form');\nlet state = null;\n\nasync function post(path, body) {\n const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });\n const data = await res.json().catch(() => ({}));\n if (!res.ok) throw new Error(data.error || res.statusText);\n return data;\n}\n\nfunction showError(message) {\n $('error').textContent = message;\n $('error').hidden = !message;\n}\n\nfunction choosePorts(value) {\n form.querySelector('input[name=ports][value=' + value + ']').checked = true;\n}\n\nasync function load() {\n state = await (await fetch('/api/state')).json();\n $('port').value = state.suggestedPort ?? state.defaultPort;\n $('agent-port').value = (state.suggestedPort ?? state.defaultPort) + 1;\n if (state.suggestedPort === null) {\n // Aucune paire libre trouvée : il reste à les choisir soi-même.\n $('ports-dedicated-row').hidden = true;\n choosePorts('custom');\n $('custom').hidden = false;\n checkPorts();\n } else {\n $('ports-dedicated').textContent = t.dedicatedHint(state.suggestedPort);\n choosePorts('dedicated');\n }\n $('name').value = state.name;\n if (state.required) document.querySelector('[data-t=lead]').textContent = t.leadRequired;\n renderGlobal();\n if (state.global.installed) $('name').focus();\n}\n\n/**\n * Deux étapes quand Runeya n'est pas encore installé globalement : l'installer,\n * puis créer le raccourci. Une seule est visible à la fois.\n */\nlet twoSteps = false;\nfunction renderGlobal() {\n const pending = !state.global.installed;\n if (pending) twoSteps = true;\n $('steps').hidden = !twoSteps;\n $('step-1').toggleAttribute('aria-current', false);\n $('step-2').toggleAttribute('aria-current', false);\n $(pending ? 'step-1' : 'step-2').setAttribute('aria-current', 'step');\n $('step-1').classList.toggle('done', !pending);\n $('step-1').querySelector('.num').textContent = pending ? '1' : '✓';\n $('global-step').hidden = !pending;\n $('split').hidden = pending;\n $('submit').disabled = pending;\n if (!pending) return;\n const outdated = state.global.installedVersion;\n document.querySelector('[data-t=globalTitle]').textContent = outdated ? t.updateTitle : t.globalTitle;\n document.querySelector('[data-t=step1]').textContent = outdated ? t.update : t.step1;\n $('global-install').textContent = outdated ? t.update : t.globalInstall;\n $('global-text').textContent = !state.global.prefix ? t.globalNoNpm\n : outdated ? t.updateText(outdated, state.global.prefix) : t.globalText(state.global.prefix);\n $('global-install').hidden = !state.global.prefix;\n}\n\n$('global-install').addEventListener('click', async () => {\n const button = $('global-install');\n button.disabled = true; $('global-cancel').disabled = true; button.textContent = t.globalInstalling;\n $('global-error').hidden = true;\n $('global-output').hidden = true;\n try {\n const res = await fetch('/api/install-global', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });\n const data = await res.json().catch(() => ({}));\n if (data.output) { $('global-output').textContent = data.output; $('global-output').hidden = false; }\n if (!res.ok) throw new Error(data.output ? t.globalFailed : (data.error || res.statusText));\n state.global = data.global;\n renderGlobal();\n $('name').focus();\n } catch (err) {\n $('global-error').textContent = err instanceof TypeError ? unreachable() : err.message;\n $('global-error').hidden = false;\n } finally {\n button.disabled = false; $('global-cancel').disabled = false;\n button.textContent = state.global.installedVersion ? t.update : t.globalInstall;\n }\n});\n\n/** Le choix de ports coché : dedicated, default ou custom. */\nfunction portsChoice() {\n const checked = form.querySelector('input[name=ports]:checked');\n return checked ? checked.value : null;\n}\n\nform.addEventListener('change', (e) => {\n if (e.target.name !== 'ports') return;\n $('custom').hidden = portsChoice() !== 'custom';\n if (portsChoice() === 'custom') checkPorts();\n});\n\n/**\n * Vérification des ports pendant la saisie. portsBusy bloque l'envoi ; le\n * serveur revérifie de toute façon à l'installation. Seule la dernière réponse\n * compte : une réponse lente ne doit pas écraser celle d'une saisie plus récente.\n */\nlet portsBusy = false;\nlet checkSeq = 0;\nlet checkTimer = null;\n\nfunction customPorts() {\n return [Number($('port').value), Number($('agent-port').value)];\n}\n\nfunction portsStatus(text, kind) {\n $('ports-status').textContent = text;\n $('ports-status').className = 'ports-status' + (kind ? ' ' + kind : '');\n $('ports-status').hidden = !text;\n}\n\nfunction checkPorts() {\n clearTimeout(checkTimer);\n const [port, agentPort] = customPorts();\n const valid = [port, agentPort].every((p) => Number.isInteger(p) && p >= 1 && p <= 65535) && port !== agentPort;\n if (!valid) { portsBusy = true; portsStatus(t.portsInvalid, 'busy'); return; }\n portsBusy = true;\n portsStatus(t.portsChecking, '');\n const seq = ++checkSeq;\n checkTimer = setTimeout(async () => {\n try {\n const free = await (await fetch('/api/ports?port=' + port + '&port=' + agentPort)).json();\n if (seq !== checkSeq) return;\n const busy = [port, agentPort].filter((p) => free[p] === false);\n portsBusy = busy.length > 0;\n if (busy.length === 2) portsStatus(t.portsBusy(busy[0], busy[1]), 'busy');\n else if (busy.length === 1) portsStatus(t.portBusy(busy[0]), 'busy');\n else portsStatus(t.portsFree, 'ok');\n } catch {\n if (seq === checkSeq) portsStatus(unreachable(), 'busy');\n }\n }, 250);\n}\n\n$('port').addEventListener('input', checkPorts);\n$('agent-port').addEventListener('input', checkPorts);\n\nform.addEventListener('submit', async (e) => {\n e.preventDefault();\n showError('');\n if (!state.global.installed) { showError(t.globalFirst); return; }\n const name = $('name').value.trim();\n if (!name) { showError(t.nameRequired); $('name').focus(); return; }\n\n let port = null, agentPort = null;\n if (portsChoice() === 'dedicated') { port = state.suggestedPort; agentPort = port + 1; }\n if (portsChoice() === 'custom') {\n [port, agentPort] = customPorts();\n if (portsBusy) { showError($('ports-status').textContent || t.portsInvalid); return; }\n }\n\n const submit = $('submit');\n submit.disabled = true; $('cancel').disabled = true;\n submit.textContent = t.installing;\n try {\n const r = await post('/api/install', { name, workingDir: null, port, agentPort });\n $('done-title').textContent = t.created(r.name);\n $('where').textContent = r.whereToFind;\n $('files').replaceChildren();\n for (const p of r.paths) { const d = document.createElement('div'); d.textContent = p; $('files').append(d); }\n if (r.warnings.length) {\n $('warnings-row').hidden = false;\n $('warnings').textContent = r.warnings.join(' · ');\n }\n $('uninstall').textContent = r.uninstall;\n $('split').hidden = true; $('steps').hidden = true; $('done').hidden = false;\n } catch (err) {\n showError(err instanceof TypeError ? unreachable() : err.message);\n submit.disabled = !state.global.installed; $('cancel').disabled = false;\n submit.textContent = t.install;\n }\n});\n\nasync function cancel() {\n try { await post('/api/cancel', {}); } catch { /* déjà fermé */ }\n $('split').hidden = true; $('global-step').hidden = true; $('steps').hidden = true; $('cancelled').hidden = false;\n}\n$('cancel').addEventListener('click', cancel);\n$('global-cancel').addEventListener('click', cancel);\n\nfunction el(tag, className, text) {\n const node = document.createElement(tag);\n if (className) node.className = className;\n if (text !== undefined) node.textContent = text;\n return node;\n}\n\nfunction listError(message) {\n $('list-error').textContent = message;\n $('list-error').hidden = !message;\n}\n\n/** Une action sur un raccourci existant ; la liste est relue ensuite. */\nasync function act(path, launcher, button) {\n listError('');\n const label = button.textContent;\n button.disabled = true; button.textContent = t.working;\n try {\n await post(path, { id: launcher.id });\n await loadLaunchers();\n } catch (err) {\n listError(err instanceof TypeError ? unreachable() : err.message);\n button.disabled = false; button.textContent = label;\n }\n}\n\nfunction renderLauncher(l) {\n const row = el('div', 'launcher');\n const body = el('div', 'launcher__body');\n body.append(el('strong', '', l.name));\n body.append(el('span', '', l.workingDir ?? t.global));\n body.append(el('span', '', l.port === null ? t.defaultPorts : t.portsOf(l.port, l.agentPort)));\n if (l.projectId) body.append(el('span', '', t.projectOf(l.projectId)));\n for (const p of l.problems) body.append(el('span', 'problem', p));\n if (l.outdated && !l.problems.length) body.append(el('span', 'stale', t.outdated));\n row.append(body);\n\n const actions = el('div', 'launcher__actions');\n // Recréer n'a de sens que si le raccourci ne vise plus ce Runeya-ci.\n if (l.outdated || l.problems.length) {\n const recreate = el('button', '', t.recreate);\n recreate.type = 'button';\n recreate.addEventListener('click', () => act('/api/reinstall', l, recreate));\n actions.append(recreate);\n }\n // Deux temps, sans boîte de dialogue : le premier clic arme, le second supprime.\n const remove = el('button', 'danger', t.removeAction);\n remove.type = 'button';\n let armed = false;\n remove.addEventListener('click', () => {\n if (!armed) {\n armed = true; remove.textContent = t.confirmRemove; remove.className = 'confirm';\n setTimeout(() => { if (armed && !remove.disabled) { armed = false; remove.textContent = t.removeAction; remove.className = 'danger'; } }, 3000);\n return;\n }\n act('/api/uninstall', l, remove);\n });\n actions.append(remove);\n row.append(actions);\n return row;\n}\n\nasync function loadLaunchers() {\n const list = await (await fetch('/api/launchers')).json();\n $('launchers').replaceChildren(...list.map(renderLauncher));\n $('existing').hidden = list.length === 0;\n $('split').classList.toggle('has-existing', list.length > 0);\n}\n\nload().catch(() => showError(unreachable()));\nloadLaunchers().catch(() => listError(unreachable()));\n</script>\n</body>\n</html>`;\n}\n\n/** Le logo, tel que dans `assets/icon.svg`. */\nconst MARK = '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 28 28\" aria-hidden=\"true\"><rect width=\"28\" height=\"28\" rx=\"8\" fill=\"#6366f1\"/><path d=\"M7 20 L14 8 L21 20\" stroke=\"white\" stroke-width=\"2.5\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M10 16 L18 16\" stroke=\"white\" stroke-width=\"2.5\" stroke-linecap=\"round\"/></svg>';\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,SAAS,oBAA+D;AACxE,SAAS,kBAAkB;;;ACPpB,SAAS,gBAAwB;AACtC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4CAMmC,mBAAmB,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAwH9D,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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","names":[]}
@@ -14,12 +14,12 @@ import {
14
14
  uninstallDesktopLauncher,
15
15
  vbsQuote,
16
16
  windowsVbsScript
17
- } from "./chunk-7HIPFSOA.js";
17
+ } from "./chunk-57P45FNY.js";
18
18
  import {
19
19
  deriveLauncherIdentity,
20
20
  launcherIdFromName,
21
21
  slugifyLauncherName
22
- } from "./chunk-XAFOP677.js";
22
+ } from "./chunk-MZSDTA4A.js";
23
23
  export {
24
24
  deriveLauncherIdentity as deriveIdentity,
25
25
  desktopExecQuote,
@@ -40,4 +40,4 @@ export {
40
40
  vbsQuote,
41
41
  windowsVbsScript
42
42
  };
43
- //# sourceMappingURL=desktop-launcher-YPNRQMIS.js.map
43
+ //# sourceMappingURL=desktop-launcher-GD4QPM66.js.map