@runeya/runeya 2.0.140 → 2.0.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/index.js.map +1 -1
- package/agent-manager-M4NA5YFD.js +11 -0
- package/{chat-L7RRHXX5.js → chat-UQIVCG33.js} +9 -7
- package/{chunk-G4TOLDZB.js → chunk-36CC5CZK.js} +123 -57
- package/chunk-36CC5CZK.js.map +1 -0
- package/{chunk-GOMF7NWS.js → chunk-7L6AQUBR.js} +2 -2
- package/{chunk-O2KOX4LU.js → chunk-C2BGNVBT.js} +35 -9
- package/chunk-C2BGNVBT.js.map +1 -0
- package/{chunk-SYCUQDA4.js → chunk-GXE5TLLJ.js} +10 -4
- package/chunk-GXE5TLLJ.js.map +1 -0
- package/{chunk-QWX6R6AW.js → chunk-H2MAALKN.js} +46 -8
- package/chunk-H2MAALKN.js.map +1 -0
- package/{chunk-EV3TBM7E.js → chunk-IXO5KIO5.js} +9 -2
- package/{chunk-EV3TBM7E.js.map → chunk-IXO5KIO5.js.map} +1 -1
- package/{chunk-6HSZPK2W.js → chunk-UR3HL373.js} +3 -3
- package/{companion-LQVLEHU7.js → companion-EVCS2HNC.js} +5 -5
- package/{desktop-installer-page-OKE4WY5Q.js → desktop-installer-page-6JDSXK2E.js} +226 -33
- package/desktop-installer-page-6JDSXK2E.js.map +1 -0
- package/{free-port-565NMFIZ.js → free-port-5UNHDWGE.js} +2 -2
- package/index.js +70 -22
- package/index.js.map +1 -1
- package/install-gate-SFHNMBWT.js +60 -0
- package/install-gate-SFHNMBWT.js.map +1 -0
- package/mcp-server/index.js +84 -10
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/launch.d.ts +5 -4
- package/mcp-server/launch.js.map +1 -1
- package/package.json +1 -1
- package/settings-manager-32SVMAFN.js +10 -0
- package/{src-NVQHFMTQ.js → src-ZSQISY6W.js} +936 -807
- package/src-ZSQISY6W.js.map +1 -0
- package/{vault-resolver-B5P2OUI5.js → vault-resolver-VK6W5J6W.js} +2 -2
- package/web/assets/{AgentScalarChatInterface.vue-Dgobg9wk.js → AgentScalarChatInterface.vue-Df0_SDmK.js} +1 -1
- package/web/assets/{AgentTerminalPage-B43nSPSz.js → AgentTerminalPage-BGA_7YUb.js} +1 -1
- package/web/assets/{AiChatModals-CVZ5ilmt.js → AiChatModals-CaGj0i1I.js} +1 -1
- package/web/assets/{AppLayout-B5yKLUj9.js → AppLayout-CDOSGDLm.js} +1 -1
- package/web/assets/{AuditPage-KR5ydSRG.js → AuditPage-BMYSSho1.js} +1 -1
- package/web/assets/{CloudAccountPage-RNosmi99.js → CloudAccountPage-DJNbXhOH.js} +1 -1
- package/web/assets/{CloudMessagingPage-Dp-FDdoH.js → CloudMessagingPage-JXnnH4aT.js} +1 -1
- package/web/assets/{DashboardPage-Bjj11oP_.js → DashboardPage-BjeN6ZQ3.js} +1 -1
- package/web/assets/{DirectoryPickerDialog-C28UH35_.js → DirectoryPickerDialog-CNIrtkmh.js} +1 -1
- package/web/assets/{EnvironmentForm-Dj2T2DuS.js → EnvironmentForm-DTJsmGg_.js} +1 -1
- package/web/assets/{EnvironmentFormPage-B4Bt1bXS.js → EnvironmentFormPage-HNIgKqaJ.js} +1 -1
- package/web/assets/{EnvironmentListPage-B_V9mEhR.js → EnvironmentListPage-DRI3aFZw.js} +1 -1
- package/web/assets/{KanbanBoardPage-CQLiin5v.js → KanbanBoardPage-DocxhfeF.js} +3 -3
- package/web/assets/{KanbanListPage-ChD0Zj0Y.js → KanbanListPage-DR_DYt46.js} +1 -1
- package/web/assets/{LaunchProjectPage-ByIfq7ka.js → LaunchProjectPage-Dgtz9dX8.js} +1 -1
- package/web/assets/{LaunchServicesPage-BweD0CWu.js → LaunchServicesPage-iM8a_UHk.js} +1 -1
- package/web/assets/{LaunchShell-C9jV0DDh.js → LaunchShell-DccCGeLx.js} +1 -1
- package/web/assets/{LogViewer-Cv024Meq.js → LogViewer-DOcy8D-m.js} +2 -2
- package/web/assets/{OrgKeyPrompt-D95Zm3Tl.js → OrgKeyPrompt-Dx95YhKV.js} +1 -1
- package/web/assets/{ProcessControls-84xtt8ud.js → ProcessControls-BUMbz3Cc.js} +1 -1
- package/web/assets/{ProcessDetailPage-CqMk8XQv.js → ProcessDetailPage-DKVRY4sj.js} +1 -1
- package/web/assets/{ProjectCreateDialog-C4rLTHw0.js → ProjectCreateDialog-CdYIXATf.js} +1 -1
- package/web/assets/{ProjectLocationGate-BtI0T-GU.js → ProjectLocationGate-BpKnR1WI.js} +1 -1
- package/web/assets/{ProjectPublishDialog-v5xz8t4N.js → ProjectPublishDialog-BTZXEmZh.js} +1 -1
- package/web/assets/{RandomJobTestPage-CzT3LSMu.js → RandomJobTestPage-BLR9jVah.js} +1 -1
- package/web/assets/ScenarioEditorPage-ebK_glPw.js +14 -0
- package/web/assets/{SendToChatDialog-BtsCq9hv.js → SendToChatDialog-VwH8FOP7.js} +1 -1
- package/web/assets/{ServiceControlsBar-BFemdioZ.js → ServiceControlsBar-3dlgA6Sg.js} +1 -1
- package/web/assets/{ServiceDetailPage-DsGHPdUc.js → ServiceDetailPage-C9MWIf-1.js} +1 -1
- package/web/assets/{ServiceFormPage-x3jtyV1A.js → ServiceFormPage-DIJMr_Ij.js} +1 -1
- package/web/assets/{SettingsPage-3tVWDaUN.js → SettingsPage-4bd7gsHf.js} +6 -6
- package/web/assets/{UnlockPage-CPcll-ov.js → UnlockPage-F37odjgM.js} +1 -1
- package/web/assets/{VaultMigrationPage-BMS4BcsA.js → VaultMigrationPage-CT6EFAXK.js} +1 -1
- package/web/assets/VersionBadge-4MjBn5ih.js +1 -0
- package/web/assets/VersionBadge-CtFji3sv.css +1 -0
- package/web/assets/{WikiPage-CM1-hJvF.js → WikiPage-Dhil7fWU.js} +1 -1
- package/web/assets/{WikiView-MQLuO7ey.js → WikiView-Ca2kGbvZ.js} +1 -1
- package/web/assets/{add-scalar-classes-BDJ0Zhvc.js → add-scalar-classes-ny-bwITc.js} +2 -2
- package/web/assets/{agent.store-CEmOZ8XE.js → agent.store-BBUsxY3E.js} +1 -1
- package/web/assets/auth-DgcowXmP.js +2 -0
- package/web/assets/{cloud-chat.store-DhCEhagv.js → cloud-chat.store-BT0ikifC.js} +1 -1
- package/web/assets/{cloud-client-CmrptmtM.js → cloud-client-7eaj_DyS.js} +2 -2
- package/web/assets/cloud-session-BI06sY79.js +2 -0
- package/web/assets/{editor.store-Be5P5Eur.js → editor.store-uLBvEAgM.js} +1 -1
- package/web/assets/index-CZJJuyAp.css +1 -0
- package/web/assets/{index-CXnyET_X.js → index-c8l1Vzb5.js} +101 -101
- package/web/assets/{modal-9BC14oRF.js → modal-CNk5Pd2V.js} +1 -1
- package/web/assets/{project-launch.store-cO8Wr508.js → project-launch.store-C0TgNDYm.js} +1 -1
- package/web/assets/{project.store-CpUEwUgF.js → project.store-Dx530iqh.js} +1 -1
- package/web/assets/{scenario.store-B5uv1DSO.js → scenario.store-DWezewHe.js} +1 -1
- package/web/assets/{service.store-ve_P4_2O.js → service.store-CgzQ1QCL.js} +1 -1
- package/web/assets/{settings-scope-CcYu-GYo.js → settings-scope-54SF-1Hf.js} +1 -1
- package/web/assets/{settings.store-CdCOUAsL.js → settings.store-Cz6g0qH2.js} +1 -1
- package/web/assets/{src-RO7PD0gR.js → src-BbRuct-7.js} +1 -1
- package/web/assets/{src-T5uKA02c.js → src-gyxHZ5yB.js} +1 -1
- package/web/assets/{theme-CPaZfI9r.js → theme-BZCAxfJD.js} +1 -1
- package/web/assets/trpc-PGQoddea.js +3 -0
- package/web/assets/{useCloudOrg-DaFUgt9j.js → useCloudOrg-DePshxqA.js} +1 -1
- package/web/assets/{useEnvVarAutocomplete-CXlyG01r.js → useEnvVarAutocomplete-lyIwYncA.js} +1 -1
- package/web/assets/{useProjectCatalog-1vLcCQlh.js → useProjectCatalog-BFmo-l75.js} +1 -1
- package/web/assets/{useProjectCloudOrg-MAfohLuM.js → useProjectCloudOrg-DN7UK1So.js} +1 -1
- package/web/assets/{useServiceUrlHost-D9k9s21s.js → useServiceUrlHost-D31YSsBc.js} +1 -1
- package/web/assets/{vue-draggable-plus-SmOauPHy.js → vue-draggable-plus-DE0HToYu.js} +1 -1
- package/web/favicon.svg +1 -5
- package/web/index.html +17 -17
- package/agent-manager-RZI65MSV.js +0 -11
- package/chunk-G4TOLDZB.js.map +0 -1
- package/chunk-O2KOX4LU.js.map +0 -1
- package/chunk-QWX6R6AW.js.map +0 -1
- package/chunk-SYCUQDA4.js.map +0 -1
- package/desktop-installer-page-OKE4WY5Q.js.map +0 -1
- package/settings-manager-5JHSSKQ2.js +0 -10
- package/src-NVQHFMTQ.js.map +0 -1
- package/web/assets/ScenarioEditorPage-DrWcVOHg.js +0 -14
- package/web/assets/VersionBadge-54viPqlY.css +0 -1
- package/web/assets/VersionBadge-prnLNUGg.js +0 -1
- package/web/assets/auth-BpxI0N26.js +0 -2
- package/web/assets/cloud-session-ByODsQrA.js +0 -2
- package/web/assets/index-Dsh11_J0.css +0 -1
- package/web/assets/trpc-BFA-mSM8.js +0 -3
- /package/{agent-manager-RZI65MSV.js.map → agent-manager-M4NA5YFD.js.map} +0 -0
- /package/{chat-L7RRHXX5.js.map → chat-UQIVCG33.js.map} +0 -0
- /package/{chunk-GOMF7NWS.js.map → chunk-7L6AQUBR.js.map} +0 -0
- /package/{chunk-6HSZPK2W.js.map → chunk-UR3HL373.js.map} +0 -0
- /package/{companion-LQVLEHU7.js.map → companion-EVCS2HNC.js.map} +0 -0
- /package/{free-port-565NMFIZ.js.map → free-port-5UNHDWGE.js.map} +0 -0
- /package/{settings-manager-5JHSSKQ2.js.map → settings-manager-32SVMAFN.js.map} +0 -0
- /package/{vault-resolver-B5P2OUI5.js.map → vault-resolver-VK6W5J6W.js.map} +0 -0
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
findFreePortPair,
|
|
13
13
|
isPortFree
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-C2BGNVBT.js";
|
|
15
15
|
|
|
16
16
|
// src/desktop-installer-page.ts
|
|
17
17
|
import { createServer } from "http";
|
|
@@ -90,6 +90,23 @@ button:disabled { opacity: .6; cursor: default; }
|
|
|
90
90
|
.done dd { margin: .25rem 0 0; font-size: .875rem; overflow-wrap: anywhere; }
|
|
91
91
|
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8125rem;
|
|
92
92
|
background: color-mix(in srgb, var(--text) 7%, var(--surface)); padding: .15rem .4rem; border-radius: 6px; }
|
|
93
|
+
.steps { list-style: none; margin: 0 auto; padding: 0; display: flex; gap: 1.5rem; justify-content: center;
|
|
94
|
+
font-size: .8125rem; color: var(--muted); }
|
|
95
|
+
.steps li { display: flex; align-items: center; gap: .5rem; white-space: nowrap; }
|
|
96
|
+
.steps .num { flex-shrink: 0; width: 1.5rem; height: 1.5rem; border-radius: 50%; display: grid; place-items: center; font-weight: 600;
|
|
97
|
+
border: 1px solid var(--border); transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease; }
|
|
98
|
+
.steps li[aria-current=step] { color: var(--text); font-weight: 600; }
|
|
99
|
+
.steps li[aria-current=step] .num { background: var(--primary); border-color: var(--primary); color: var(--primary-text); }
|
|
100
|
+
.steps li.done .num { border-color: var(--ok); color: var(--ok); }
|
|
101
|
+
.ports-status { grid-column: 1 / -1; margin: 0; font-size: .8125rem; }
|
|
102
|
+
.ports-status.ok { color: var(--ok); }
|
|
103
|
+
.ports-status.busy { color: var(--danger); }
|
|
104
|
+
.global-step { width: 100%; max-width: 520px; align-self: center; gap: .75rem; }
|
|
105
|
+
.global-step h2 { margin: 0; font-size: 1rem; }
|
|
106
|
+
.global-step p { margin: 0; color: var(--muted); font-size: .875rem; }
|
|
107
|
+
.global-step pre { margin: 0; max-height: 14rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
108
|
+
font: .75rem/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; padding: .625rem .75rem; border-radius: 8px;
|
|
109
|
+
background: color-mix(in srgb, var(--text) 7%, var(--surface)); }
|
|
93
110
|
.close { color: var(--muted); font-size: .8125rem; margin: 0; }
|
|
94
111
|
.hint { margin: .375rem 0 0; color: var(--muted); font-size: .8125rem; }
|
|
95
112
|
.split { display: grid; grid-template-columns: minmax(0, 520px); justify-content: center; gap: 1.5rem; align-items: start; }
|
|
@@ -133,6 +150,22 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .
|
|
|
133
150
|
<p class="lead" data-t="lead"></p>
|
|
134
151
|
</header>
|
|
135
152
|
|
|
153
|
+
<ol class="steps" id="steps" hidden>
|
|
154
|
+
<li id="step-1"><span class="num" aria-hidden="true">1</span><span data-t="step1"></span></li>
|
|
155
|
+
<li id="step-2"><span class="num" aria-hidden="true">2</span><span data-t="step2"></span></li>
|
|
156
|
+
</ol>
|
|
157
|
+
|
|
158
|
+
<section class="card global-step" id="global-step" hidden>
|
|
159
|
+
<h2 data-t="globalTitle"></h2>
|
|
160
|
+
<p id="global-text"></p>
|
|
161
|
+
<pre id="global-output" hidden></pre>
|
|
162
|
+
<p class="error" id="global-error" role="alert" hidden></p>
|
|
163
|
+
<div class="actions">
|
|
164
|
+
<button type="button" class="secondary" id="global-cancel" data-t="cancel"></button>
|
|
165
|
+
<button type="button" class="primary" id="global-install" data-t="globalInstall"></button>
|
|
166
|
+
</div>
|
|
167
|
+
</section>
|
|
168
|
+
|
|
136
169
|
<div class="split" id="split">
|
|
137
170
|
<section class="card existing" id="existing" hidden>
|
|
138
171
|
<h2 data-t="existingTitle"></h2>
|
|
@@ -152,13 +185,12 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .
|
|
|
152
185
|
<legend data-t="ports"></legend>
|
|
153
186
|
<label class="choice" id="ports-dedicated-row"><input type="radio" name="ports" value="dedicated">
|
|
154
187
|
<div><strong data-t="portsDedicated"></strong><span id="ports-dedicated"></span></div></label>
|
|
155
|
-
<label class="choice"><input type="radio" name="ports" value="default">
|
|
156
|
-
<div><strong data-t="portsDefault"></strong><span id="ports-default"></span></div></label>
|
|
157
188
|
<label class="choice"><input type="radio" name="ports" value="custom">
|
|
158
189
|
<div><strong data-t="portsCustom"></strong><span data-t="portsCustomHint"></span></div></label>
|
|
159
190
|
<div class="ports" id="custom" hidden>
|
|
160
191
|
<label><span data-t="portRuneya"></span><input id="port" type="number" min="1" max="65535"></label>
|
|
161
192
|
<label><span data-t="portAgent"></span><input id="agent-port" type="number" min="1" max="65535"></label>
|
|
193
|
+
<p class="ports-status" id="ports-status" aria-live="polite" hidden></p>
|
|
162
194
|
</div>
|
|
163
195
|
</fieldset>
|
|
164
196
|
|
|
@@ -195,12 +227,13 @@ const T = {
|
|
|
195
227
|
lead: 'Un lanceur dans le menu de votre bureau, pour d\xE9marrer Runeya sans terminal.',
|
|
196
228
|
globalHint: 'Le raccourci d\xE9marre Runeya dans votre dossier personnel et range les donn\xE9es dans ~/.runeya. Pour un raccourci vers un projet, passez par les r\xE9glages de Runeya.',
|
|
197
229
|
name: 'Nom du raccourci', ports: 'Ports',
|
|
198
|
-
portsDedicated: 'Ports d\xE9di\xE9s (recommand\xE9)',
|
|
230
|
+
portsDedicated: 'Ports d\xE9di\xE9s (recommand\xE9)',
|
|
199
231
|
portsCustom: 'Choisir moi-m\xEAme', portsCustomHint: 'Saisir le port de Runeya, puis celui de l\u2019agent',
|
|
200
232
|
portRuneya: 'Runeya', portAgent: 'Agent',
|
|
201
233
|
dedicatedHint: (p) => p + ' et ' + (p + 1) + ', choisis parmi les ports libres',
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
portsChecking: 'V\xE9rification\u2026', portsFree: 'Ports libres sur cette machine',
|
|
235
|
+
portBusy: (p) => 'Le port ' + p + ' est d\xE9j\xE0 utilis\xE9 sur cette machine',
|
|
236
|
+
portsBusy: (a, b) => 'Les ports ' + a + ' et ' + b + ' sont d\xE9j\xE0 utilis\xE9s sur cette machine',
|
|
204
237
|
cancel: 'Fermer', install: 'Cr\xE9er le raccourci', installing: 'Cr\xE9ation\u2026',
|
|
205
238
|
existingTitle: 'Raccourcis existants', global: 'Global (dossier personnel)',
|
|
206
239
|
portsOf: (p, a) => 'Ports ' + p + ' \xB7 ' + a, defaultPorts: 'Ports par d\xE9faut',
|
|
@@ -213,19 +246,30 @@ const T = {
|
|
|
213
246
|
cancelledTitle: 'Installateur ferm\xE9',
|
|
214
247
|
nameRequired: 'Donnez un nom au raccourci.',
|
|
215
248
|
portsInvalid: 'Deux ports diff\xE9rents entre 1 et 65535.',
|
|
216
|
-
|
|
249
|
+
unreachableCmd: (cmd) => 'L\u2019installateur ne r\xE9pond plus. Relancez \xAB ' + cmd + ' \xBB.',
|
|
250
|
+
leadRequired: 'Runeya se lance depuis un raccourci de bureau. Cr\xE9ez-le ici, puis d\xE9marrez Runeya depuis le menu de votre bureau.',
|
|
251
|
+
globalTitle: 'Installer Runeya sur cette machine',
|
|
252
|
+
step1: 'Installer Runeya', step2: 'Cr\xE9er le raccourci',
|
|
253
|
+
updateTitle: 'Mettre \xE0 jour Runeya sur cette machine', update: 'Mettre \xE0 jour Runeya',
|
|
254
|
+
updateText: (v, prefix) => 'Runeya ' + v + ' est install\xE9 dans ' + prefix + ', une version plus ancienne que celle-ci. Le raccourci utilisera la version \xE0 jour.',
|
|
255
|
+
globalText: (prefix) => 'Le raccourci a besoin d\u2019une installation globale. Runeya sera install\xE9 avec npm dans ' + prefix + '.',
|
|
256
|
+
globalNoNpm: 'npm ne r\xE9pond pas. Installez Runeya avec \xAB npm install -g @runeya/runeya \xBB, puis relancez \xAB runeya \xBB.',
|
|
257
|
+
globalInstall: 'Installer Runeya', globalInstalling: 'Installation\u2026 (cela peut prendre une minute)',
|
|
258
|
+
globalFailed: 'L\u2019installation a \xE9chou\xE9. La sortie de npm est affich\xE9e ci-dessus.',
|
|
259
|
+
globalFirst: 'Installez d\u2019abord Runeya.',
|
|
217
260
|
},
|
|
218
261
|
en: {
|
|
219
262
|
title: 'Install Runeya',
|
|
220
263
|
lead: 'A launcher in your desktop menu, to start Runeya without a terminal.',
|
|
221
264
|
globalHint: 'The shortcut starts Runeya in your home folder and stores data in ~/.runeya. For a shortcut to a project, use the Runeya settings.',
|
|
222
265
|
name: 'Shortcut name', ports: 'Ports',
|
|
223
|
-
portsDedicated: 'Dedicated ports (recommended)',
|
|
266
|
+
portsDedicated: 'Dedicated ports (recommended)',
|
|
224
267
|
portsCustom: 'Choose myself', portsCustomHint: 'Enter the Runeya port, then the agent\u2019s',
|
|
225
268
|
portRuneya: 'Runeya', portAgent: 'Agent',
|
|
226
269
|
dedicatedHint: (p) => p + ' and ' + (p + 1) + ', chosen among free ports',
|
|
227
|
-
|
|
228
|
-
|
|
270
|
+
portsChecking: 'Checking\u2026', portsFree: 'Ports free on this machine',
|
|
271
|
+
portBusy: (p) => 'Port ' + p + ' is already in use on this machine',
|
|
272
|
+
portsBusy: (a, b) => 'Ports ' + a + ' and ' + b + ' are already in use on this machine',
|
|
229
273
|
cancel: 'Close', install: 'Create shortcut', installing: 'Creating\u2026',
|
|
230
274
|
existingTitle: 'Existing shortcuts', global: 'Global (home folder)',
|
|
231
275
|
portsOf: (p, a) => 'Ports ' + p + ' \xB7 ' + a, defaultPorts: 'Default ports',
|
|
@@ -238,7 +282,17 @@ const T = {
|
|
|
238
282
|
cancelledTitle: 'Installer closed',
|
|
239
283
|
nameRequired: 'Give the shortcut a name.',
|
|
240
284
|
portsInvalid: 'Two different ports between 1 and 65535.',
|
|
241
|
-
|
|
285
|
+
unreachableCmd: (cmd) => 'The installer no longer answers. Run \u201C' + cmd + '\u201D again.',
|
|
286
|
+
leadRequired: 'Runeya starts from a desktop shortcut. Create it here, then start Runeya from your desktop menu.',
|
|
287
|
+
globalTitle: 'Install Runeya on this machine',
|
|
288
|
+
step1: 'Install Runeya', step2: 'Create the shortcut',
|
|
289
|
+
updateTitle: 'Update Runeya on this machine', update: 'Update Runeya',
|
|
290
|
+
updateText: (v, prefix) => 'Runeya ' + v + ' is installed in ' + prefix + ', older than this one. The shortcut will use the up-to-date version.',
|
|
291
|
+
globalText: (prefix) => 'The shortcut needs a global install. Runeya will be installed with npm into ' + prefix + '.',
|
|
292
|
+
globalNoNpm: 'npm does not answer. Install Runeya with \u201Cnpm install -g @runeya/runeya\u201D, then run \u201Cruneya\u201D again.',
|
|
293
|
+
globalInstall: 'Install Runeya', globalInstalling: 'Installing\u2026 (this can take a minute)',
|
|
294
|
+
globalFailed: 'The install failed. The npm output is shown above.',
|
|
295
|
+
globalFirst: 'Install Runeya first.',
|
|
242
296
|
},
|
|
243
297
|
};
|
|
244
298
|
const lang = (navigator.language || 'fr').toLowerCase().startsWith('fr') ? 'fr' : 'en';
|
|
@@ -248,6 +302,8 @@ document.title = t.title;
|
|
|
248
302
|
document.querySelectorAll('[data-t]').forEach((el) => { el.textContent = t[el.dataset.t]; });
|
|
249
303
|
|
|
250
304
|
const $ = (id) => document.getElementById(id);
|
|
305
|
+
/** La commande qui a ouvert la page : c'est elle qu'il faut relancer. */
|
|
306
|
+
const unreachable = () => t.unreachableCmd(state && state.required ? 'runeya' : 'runeya install-desktop');
|
|
251
307
|
const form = $('form');
|
|
252
308
|
let state = null;
|
|
253
309
|
|
|
@@ -263,39 +319,148 @@ function showError(message) {
|
|
|
263
319
|
$('error').hidden = !message;
|
|
264
320
|
}
|
|
265
321
|
|
|
322
|
+
function choosePorts(value) {
|
|
323
|
+
form.querySelector('input[name=ports][value=' + value + ']').checked = true;
|
|
324
|
+
}
|
|
325
|
+
|
|
266
326
|
async function load() {
|
|
267
327
|
state = await (await fetch('/api/state')).json();
|
|
328
|
+
$('port').value = state.suggestedPort ?? state.defaultPort;
|
|
329
|
+
$('agent-port').value = (state.suggestedPort ?? state.defaultPort) + 1;
|
|
268
330
|
if (state.suggestedPort === null) {
|
|
331
|
+
// Aucune paire libre trouv\xE9e : il reste \xE0 les choisir soi-m\xEAme.
|
|
269
332
|
$('ports-dedicated-row').hidden = true;
|
|
270
|
-
|
|
333
|
+
choosePorts('custom');
|
|
334
|
+
$('custom').hidden = false;
|
|
335
|
+
checkPorts();
|
|
271
336
|
} else {
|
|
272
337
|
$('ports-dedicated').textContent = t.dedicatedHint(state.suggestedPort);
|
|
273
|
-
|
|
338
|
+
choosePorts('dedicated');
|
|
274
339
|
}
|
|
275
|
-
$('ports-default').textContent = state.defaultsFree ? t.defaultFree(state.defaultPort) : t.defaultTaken(state.defaultPort);
|
|
276
|
-
if (!state.defaultsFree) $('ports-default').classList.add('warn');
|
|
277
|
-
$('port').value = state.suggestedPort ?? state.defaultPort;
|
|
278
|
-
$('agent-port').value = (state.suggestedPort ?? state.defaultPort) + 1;
|
|
279
340
|
$('name').value = state.name;
|
|
280
|
-
|
|
341
|
+
if (state.required) document.querySelector('[data-t=lead]').textContent = t.leadRequired;
|
|
342
|
+
renderGlobal();
|
|
343
|
+
if (state.global.installed) $('name').focus();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Deux \xE9tapes quand Runeya n'est pas encore install\xE9 globalement : l'installer,
|
|
348
|
+
* puis cr\xE9er le raccourci. Une seule est visible \xE0 la fois.
|
|
349
|
+
*/
|
|
350
|
+
let twoSteps = false;
|
|
351
|
+
function renderGlobal() {
|
|
352
|
+
const pending = !state.global.installed;
|
|
353
|
+
if (pending) twoSteps = true;
|
|
354
|
+
$('steps').hidden = !twoSteps;
|
|
355
|
+
$('step-1').toggleAttribute('aria-current', false);
|
|
356
|
+
$('step-2').toggleAttribute('aria-current', false);
|
|
357
|
+
$(pending ? 'step-1' : 'step-2').setAttribute('aria-current', 'step');
|
|
358
|
+
$('step-1').classList.toggle('done', !pending);
|
|
359
|
+
$('step-1').querySelector('.num').textContent = pending ? '1' : '\u2713';
|
|
360
|
+
$('global-step').hidden = !pending;
|
|
361
|
+
$('split').hidden = pending;
|
|
362
|
+
$('submit').disabled = pending;
|
|
363
|
+
if (!pending) return;
|
|
364
|
+
const outdated = state.global.installedVersion;
|
|
365
|
+
document.querySelector('[data-t=globalTitle]').textContent = outdated ? t.updateTitle : t.globalTitle;
|
|
366
|
+
document.querySelector('[data-t=step1]').textContent = outdated ? t.update : t.step1;
|
|
367
|
+
$('global-install').textContent = outdated ? t.update : t.globalInstall;
|
|
368
|
+
$('global-text').textContent = !state.global.prefix ? t.globalNoNpm
|
|
369
|
+
: outdated ? t.updateText(outdated, state.global.prefix) : t.globalText(state.global.prefix);
|
|
370
|
+
$('global-install').hidden = !state.global.prefix;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
$('global-install').addEventListener('click', async () => {
|
|
374
|
+
const button = $('global-install');
|
|
375
|
+
button.disabled = true; $('global-cancel').disabled = true; button.textContent = t.globalInstalling;
|
|
376
|
+
$('global-error').hidden = true;
|
|
377
|
+
$('global-output').hidden = true;
|
|
378
|
+
try {
|
|
379
|
+
const res = await fetch('/api/install-global', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
380
|
+
const data = await res.json().catch(() => ({}));
|
|
381
|
+
if (data.output) { $('global-output').textContent = data.output; $('global-output').hidden = false; }
|
|
382
|
+
if (!res.ok) throw new Error(data.output ? t.globalFailed : (data.error || res.statusText));
|
|
383
|
+
state.global = data.global;
|
|
384
|
+
renderGlobal();
|
|
385
|
+
$('name').focus();
|
|
386
|
+
} catch (err) {
|
|
387
|
+
$('global-error').textContent = err instanceof TypeError ? unreachable() : err.message;
|
|
388
|
+
$('global-error').hidden = false;
|
|
389
|
+
} finally {
|
|
390
|
+
button.disabled = false; $('global-cancel').disabled = false;
|
|
391
|
+
button.textContent = state.global.installedVersion ? t.update : t.globalInstall;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
/** Le choix de ports coch\xE9 : dedicated, default ou custom. */
|
|
396
|
+
function portsChoice() {
|
|
397
|
+
const checked = form.querySelector('input[name=ports]:checked');
|
|
398
|
+
return checked ? checked.value : null;
|
|
281
399
|
}
|
|
282
400
|
|
|
283
401
|
form.addEventListener('change', (e) => {
|
|
284
|
-
if (e.target.name
|
|
402
|
+
if (e.target.name !== 'ports') return;
|
|
403
|
+
$('custom').hidden = portsChoice() !== 'custom';
|
|
404
|
+
if (portsChoice() === 'custom') checkPorts();
|
|
285
405
|
});
|
|
286
406
|
|
|
407
|
+
/**
|
|
408
|
+
* V\xE9rification des ports pendant la saisie. portsBusy bloque l'envoi ; le
|
|
409
|
+
* serveur rev\xE9rifie de toute fa\xE7on \xE0 l'installation. Seule la derni\xE8re r\xE9ponse
|
|
410
|
+
* compte : une r\xE9ponse lente ne doit pas \xE9craser celle d'une saisie plus r\xE9cente.
|
|
411
|
+
*/
|
|
412
|
+
let portsBusy = false;
|
|
413
|
+
let checkSeq = 0;
|
|
414
|
+
let checkTimer = null;
|
|
415
|
+
|
|
416
|
+
function customPorts() {
|
|
417
|
+
return [Number($('port').value), Number($('agent-port').value)];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function portsStatus(text, kind) {
|
|
421
|
+
$('ports-status').textContent = text;
|
|
422
|
+
$('ports-status').className = 'ports-status' + (kind ? ' ' + kind : '');
|
|
423
|
+
$('ports-status').hidden = !text;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function checkPorts() {
|
|
427
|
+
clearTimeout(checkTimer);
|
|
428
|
+
const [port, agentPort] = customPorts();
|
|
429
|
+
const valid = [port, agentPort].every((p) => Number.isInteger(p) && p >= 1 && p <= 65535) && port !== agentPort;
|
|
430
|
+
if (!valid) { portsBusy = true; portsStatus(t.portsInvalid, 'busy'); return; }
|
|
431
|
+
portsBusy = true;
|
|
432
|
+
portsStatus(t.portsChecking, '');
|
|
433
|
+
const seq = ++checkSeq;
|
|
434
|
+
checkTimer = setTimeout(async () => {
|
|
435
|
+
try {
|
|
436
|
+
const free = await (await fetch('/api/ports?port=' + port + '&port=' + agentPort)).json();
|
|
437
|
+
if (seq !== checkSeq) return;
|
|
438
|
+
const busy = [port, agentPort].filter((p) => free[p] === false);
|
|
439
|
+
portsBusy = busy.length > 0;
|
|
440
|
+
if (busy.length === 2) portsStatus(t.portsBusy(busy[0], busy[1]), 'busy');
|
|
441
|
+
else if (busy.length === 1) portsStatus(t.portBusy(busy[0]), 'busy');
|
|
442
|
+
else portsStatus(t.portsFree, 'ok');
|
|
443
|
+
} catch {
|
|
444
|
+
if (seq === checkSeq) portsStatus(unreachable(), 'busy');
|
|
445
|
+
}
|
|
446
|
+
}, 250);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
$('port').addEventListener('input', checkPorts);
|
|
450
|
+
$('agent-port').addEventListener('input', checkPorts);
|
|
451
|
+
|
|
287
452
|
form.addEventListener('submit', async (e) => {
|
|
288
453
|
e.preventDefault();
|
|
289
454
|
showError('');
|
|
455
|
+
if (!state.global.installed) { showError(t.globalFirst); return; }
|
|
290
456
|
const name = $('name').value.trim();
|
|
291
457
|
if (!name) { showError(t.nameRequired); $('name').focus(); return; }
|
|
292
458
|
|
|
293
459
|
let port = null, agentPort = null;
|
|
294
460
|
if (portsChoice() === 'dedicated') { port = state.suggestedPort; agentPort = port + 1; }
|
|
295
461
|
if (portsChoice() === 'custom') {
|
|
296
|
-
port
|
|
297
|
-
|
|
298
|
-
if (!ok) { showError(t.portsInvalid); return; }
|
|
462
|
+
[port, agentPort] = customPorts();
|
|
463
|
+
if (portsBusy) { showError($('ports-status').textContent || t.portsInvalid); return; }
|
|
299
464
|
}
|
|
300
465
|
|
|
301
466
|
const submit = $('submit');
|
|
@@ -312,18 +477,20 @@ form.addEventListener('submit', async (e) => {
|
|
|
312
477
|
$('warnings').textContent = r.warnings.join(' \xB7 ');
|
|
313
478
|
}
|
|
314
479
|
$('uninstall').textContent = r.uninstall;
|
|
315
|
-
$('split').hidden = true; $('done').hidden = false;
|
|
480
|
+
$('split').hidden = true; $('steps').hidden = true; $('done').hidden = false;
|
|
316
481
|
} catch (err) {
|
|
317
|
-
showError(err instanceof TypeError ?
|
|
318
|
-
submit.disabled =
|
|
482
|
+
showError(err instanceof TypeError ? unreachable() : err.message);
|
|
483
|
+
submit.disabled = !state.global.installed; $('cancel').disabled = false;
|
|
319
484
|
submit.textContent = t.install;
|
|
320
485
|
}
|
|
321
486
|
});
|
|
322
487
|
|
|
323
|
-
|
|
488
|
+
async function cancel() {
|
|
324
489
|
try { await post('/api/cancel', {}); } catch { /* d\xE9j\xE0 ferm\xE9 */ }
|
|
325
|
-
$('split').hidden = true; $('cancelled').hidden = false;
|
|
326
|
-
}
|
|
490
|
+
$('split').hidden = true; $('global-step').hidden = true; $('steps').hidden = true; $('cancelled').hidden = false;
|
|
491
|
+
}
|
|
492
|
+
$('cancel').addEventListener('click', cancel);
|
|
493
|
+
$('global-cancel').addEventListener('click', cancel);
|
|
327
494
|
|
|
328
495
|
function el(tag, className, text) {
|
|
329
496
|
const node = document.createElement(tag);
|
|
@@ -346,7 +513,7 @@ async function act(path, launcher, button) {
|
|
|
346
513
|
await post(path, { id: launcher.id });
|
|
347
514
|
await loadLaunchers();
|
|
348
515
|
} catch (err) {
|
|
349
|
-
listError(err instanceof TypeError ?
|
|
516
|
+
listError(err instanceof TypeError ? unreachable() : err.message);
|
|
350
517
|
button.disabled = false; button.textContent = label;
|
|
351
518
|
}
|
|
352
519
|
}
|
|
@@ -393,8 +560,8 @@ async function loadLaunchers() {
|
|
|
393
560
|
$('split').classList.toggle('has-existing', list.length > 0);
|
|
394
561
|
}
|
|
395
562
|
|
|
396
|
-
load().catch(() => showError(
|
|
397
|
-
loadLaunchers().catch(() => listError(
|
|
563
|
+
load().catch(() => showError(unreachable()));
|
|
564
|
+
loadLaunchers().catch(() => listError(unreachable()));
|
|
398
565
|
</script>
|
|
399
566
|
</body>
|
|
400
567
|
</html>`;
|
|
@@ -471,7 +638,8 @@ async function runInstallerPage(opts) {
|
|
|
471
638
|
name: deriveLauncherIdentity(void 0, null).name,
|
|
472
639
|
suggestedPort,
|
|
473
640
|
defaultPort: DEFAULT_PORT,
|
|
474
|
-
|
|
641
|
+
required: opts.required ?? false,
|
|
642
|
+
global: await opts.global.state()
|
|
475
643
|
};
|
|
476
644
|
return new Promise((resolvePromise) => {
|
|
477
645
|
let settled = false;
|
|
@@ -503,6 +671,11 @@ async function runInstallerPage(opts) {
|
|
|
503
671
|
if (req.method === "GET" && url.pathname === "/api/state") {
|
|
504
672
|
return send(res, 200, state);
|
|
505
673
|
}
|
|
674
|
+
if (req.method === "GET" && url.pathname === "/api/ports") {
|
|
675
|
+
const ports = url.searchParams.getAll("port").map(Number).filter(isPort).slice(0, 2);
|
|
676
|
+
const free = await Promise.all(ports.map((p) => isPortFree(p)));
|
|
677
|
+
return send(res, 200, Object.fromEntries(ports.map((p, i) => [p, free[i]])));
|
|
678
|
+
}
|
|
506
679
|
if (req.method === "GET" && url.pathname === "/api/launchers") {
|
|
507
680
|
const launchers = await opts.list();
|
|
508
681
|
return send(res, 200, await Promise.all(launchers.map((l) => toView(l, opts.current))));
|
|
@@ -531,6 +704,9 @@ async function runInstallerPage(opts) {
|
|
|
531
704
|
const id = parseLauncherRef(body);
|
|
532
705
|
const launcher = id === null ? void 0 : (await opts.list()).find((l) => l.id === id);
|
|
533
706
|
if (!launcher) return send(res, 404, { error: "Raccourci introuvable." });
|
|
707
|
+
if (url.pathname === "/api/reinstall" && !state.global.installed) {
|
|
708
|
+
return send(res, 409, { error: "Installez d\u2019abord Runeya globalement." });
|
|
709
|
+
}
|
|
534
710
|
installing = true;
|
|
535
711
|
try {
|
|
536
712
|
const result = url.pathname === "/api/uninstall" ? await opts.uninstall(launcher) : await opts.install({
|
|
@@ -546,8 +722,22 @@ async function runInstallerPage(opts) {
|
|
|
546
722
|
installing = false;
|
|
547
723
|
}
|
|
548
724
|
}
|
|
725
|
+
if (url.pathname === "/api/install-global") {
|
|
726
|
+
if (installing || settled) return send(res, 409, { error: "Une op\xE9ration est d\xE9j\xE0 en cours." });
|
|
727
|
+
installing = true;
|
|
728
|
+
try {
|
|
729
|
+
const result = await opts.global.install();
|
|
730
|
+
if (result.ok) state.global = await opts.global.state();
|
|
731
|
+
return send(res, result.ok ? 200 : 500, { ok: result.ok, output: result.output, global: state.global });
|
|
732
|
+
} catch (err) {
|
|
733
|
+
return send(res, 500, { error: err.message });
|
|
734
|
+
} finally {
|
|
735
|
+
installing = false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
549
738
|
if (url.pathname === "/api/install") {
|
|
550
739
|
if (installing || settled) return send(res, 409, { error: "Installation d\xE9j\xE0 en cours." });
|
|
740
|
+
if (!state.global.installed) return send(res, 409, { error: "Installez d\u2019abord Runeya globalement." });
|
|
551
741
|
let body;
|
|
552
742
|
try {
|
|
553
743
|
body = await readJson(req);
|
|
@@ -556,6 +746,9 @@ async function runInstallerPage(opts) {
|
|
|
556
746
|
}
|
|
557
747
|
const parsed = parseInstallRequest(body);
|
|
558
748
|
if (typeof parsed === "string") return send(res, 400, { error: parsed });
|
|
749
|
+
for (const p of [parsed.port, parsed.agentPort]) {
|
|
750
|
+
if (p !== null && !await isPortFree(p)) return send(res, 409, { error: `Le port ${p} est d\xE9j\xE0 utilis\xE9 sur cette machine.` });
|
|
751
|
+
}
|
|
559
752
|
const answers = { ...parsed, workingDir: null };
|
|
560
753
|
installing = true;
|
|
561
754
|
try {
|
|
@@ -595,4 +788,4 @@ export {
|
|
|
595
788
|
parseInstallRequest,
|
|
596
789
|
runInstallerPage
|
|
597
790
|
};
|
|
598
|
-
//# sourceMappingURL=desktop-installer-page-
|
|
791
|
+
//# sourceMappingURL=desktop-installer-page-6JDSXK2E.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};\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 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 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 });\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',\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',\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 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":[]}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
isPortFree,
|
|
4
4
|
parsePort,
|
|
5
5
|
probeRuneya
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-C2BGNVBT.js";
|
|
7
7
|
export {
|
|
8
8
|
findFreePortPair,
|
|
9
9
|
isPortFree,
|
|
10
10
|
parsePort,
|
|
11
11
|
probeRuneya
|
|
12
12
|
};
|
|
13
|
-
//# sourceMappingURL=free-port-
|
|
13
|
+
//# sourceMappingURL=free-port-5UNHDWGE.js.map
|