@y4wee/nupo 0.1.0

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 (63) hide show
  1. package/dist/App.d.ts +8 -0
  2. package/dist/App.js +109 -0
  3. package/dist/__tests__/checks.test.d.ts +1 -0
  4. package/dist/__tests__/checks.test.js +68 -0
  5. package/dist/__tests__/config.test.d.ts +1 -0
  6. package/dist/__tests__/config.test.js +61 -0
  7. package/dist/components/ConfirmExit.d.ts +9 -0
  8. package/dist/components/ConfirmExit.js +12 -0
  9. package/dist/components/ErrorPanel.d.ts +7 -0
  10. package/dist/components/ErrorPanel.js +12 -0
  11. package/dist/components/Header.d.ts +10 -0
  12. package/dist/components/Header.js +17 -0
  13. package/dist/components/LeftPanel.d.ts +9 -0
  14. package/dist/components/LeftPanel.js +31 -0
  15. package/dist/components/OptionsPanel.d.ts +11 -0
  16. package/dist/components/OptionsPanel.js +18 -0
  17. package/dist/components/PathInput.d.ts +10 -0
  18. package/dist/components/PathInput.js +133 -0
  19. package/dist/components/ProgressBar.d.ts +5 -0
  20. package/dist/components/ProgressBar.js +21 -0
  21. package/dist/components/StepsPanel.d.ts +8 -0
  22. package/dist/components/StepsPanel.js +24 -0
  23. package/dist/hooks/useConfig.d.ts +8 -0
  24. package/dist/hooks/useConfig.js +26 -0
  25. package/dist/hooks/useTerminalSize.d.ts +4 -0
  26. package/dist/hooks/useTerminalSize.js +18 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +169 -0
  29. package/dist/screens/ConfigScreen.d.ts +10 -0
  30. package/dist/screens/ConfigScreen.js +182 -0
  31. package/dist/screens/ConfigureServiceScreen.d.ts +11 -0
  32. package/dist/screens/ConfigureServiceScreen.js +499 -0
  33. package/dist/screens/HomeScreen.d.ts +14 -0
  34. package/dist/screens/HomeScreen.js +24 -0
  35. package/dist/screens/IdeScreen.d.ts +9 -0
  36. package/dist/screens/IdeScreen.js +101 -0
  37. package/dist/screens/InitScreen.d.ts +9 -0
  38. package/dist/screens/InitScreen.js +182 -0
  39. package/dist/screens/InstallVersionScreen.d.ts +10 -0
  40. package/dist/screens/InstallVersionScreen.js +495 -0
  41. package/dist/screens/OdooScreen.d.ts +13 -0
  42. package/dist/screens/OdooScreen.js +76 -0
  43. package/dist/screens/OdooServiceScreen.d.ts +10 -0
  44. package/dist/screens/OdooServiceScreen.js +51 -0
  45. package/dist/screens/StartServiceScreen.d.ts +12 -0
  46. package/dist/screens/StartServiceScreen.js +386 -0
  47. package/dist/screens/UpgradeVersionScreen.d.ts +9 -0
  48. package/dist/screens/UpgradeVersionScreen.js +259 -0
  49. package/dist/services/checks.d.ts +8 -0
  50. package/dist/services/checks.js +48 -0
  51. package/dist/services/config.d.ts +11 -0
  52. package/dist/services/config.js +146 -0
  53. package/dist/services/git.d.ts +35 -0
  54. package/dist/services/git.js +173 -0
  55. package/dist/services/ide.d.ts +10 -0
  56. package/dist/services/ide.js +126 -0
  57. package/dist/services/python.d.ts +14 -0
  58. package/dist/services/python.js +81 -0
  59. package/dist/services/system.d.ts +2 -0
  60. package/dist/services/system.js +22 -0
  61. package/dist/types/index.d.ts +82 -0
  62. package/dist/types/index.js +26 -0
  63. package/package.json +37 -0
@@ -0,0 +1,495 @@
1
+ import React, { useReducer, useEffect, useState, useCallback, useRef } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { mkdir, stat } from 'fs/promises';
5
+ import { join } from 'path';
6
+ import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor, } from '../types/index.js';
7
+ import { checkBranch, cloneRepo, ODOO_COMMUNITY_URL, ODOO_ENTERPRISE_URL, } from '../services/git.js';
8
+ import { createVenv, installRequirements } from '../services/python.js';
9
+ import { readConfig, writeConfig } from '../services/config.js';
10
+ import { LeftPanel } from '../components/LeftPanel.js';
11
+ import { StepsPanel } from '../components/StepsPanel.js';
12
+ import { ErrorPanel } from '../components/ErrorPanel.js';
13
+ import { ProgressBar } from '../components/ProgressBar.js';
14
+ const STEP_DEFS = [
15
+ { id: 'branch_input', label: 'Saisie de la version' },
16
+ { id: 'check_community', label: 'Vérification branche community' },
17
+ { id: 'check_enterprise', label: 'Vérification branche enterprise' },
18
+ { id: 'create_dir', label: 'Création du dossier' },
19
+ { id: 'clone_community', label: 'Téléchargement Odoo community' },
20
+ { id: 'clone_enterprise', label: 'Téléchargement Odoo enterprise' },
21
+ { id: 'create_venv', label: 'Création de l\'environnement virtuel Python' },
22
+ { id: 'install_requirements', label: 'Installation des dépendances Python' },
23
+ { id: 'create_extras', label: 'Création dossiers custom et config' },
24
+ ];
25
+ function reducer(state, action) {
26
+ if (action.type === 'RESET')
27
+ return action.steps;
28
+ return state.map(s => s.id === action.id ? { ...s, status: action.status, errorMessage: action.errorMessage } : s);
29
+ }
30
+ function buildInitialSteps() {
31
+ return STEP_DEFS.map((def, i) => ({
32
+ ...def,
33
+ status: (i === 0 ? 'running' : 'pending'),
34
+ }));
35
+ }
36
+ /** Builds steps for a resume: steps up to lastCompletedStep are success, rest pending. */
37
+ function buildResumedSteps(lastCompletedStep) {
38
+ const lastIdx = lastCompletedStep
39
+ ? STEP_DEFS.findIndex(s => s.id === lastCompletedStep)
40
+ : -1;
41
+ return STEP_DEFS.map((def, i) => ({
42
+ ...def,
43
+ status: (i <= lastIdx ? 'success' : 'pending'),
44
+ }));
45
+ }
46
+ async function dirExists(p) {
47
+ try {
48
+ return (await stat(p)).isDirectory();
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ const PHASE_LABEL = {
55
+ receiving: 'Receiving objects',
56
+ resolving: 'Resolving deltas ',
57
+ };
58
+ // ── Screen ──────────────────────────────────────────────────────────────────
59
+ export function InstallVersionScreen({ config, leftWidth, onComplete, onBack, }) {
60
+ const textColor = getTextColor(config);
61
+ const cursorColor = getCursorColor(config);
62
+ const [steps, dispatch] = useReducer(reducer, buildInitialSteps());
63
+ const [currentStepIndex, setCurrentStepIndex] = useState(0);
64
+ const [branchInput, setBranchInput] = useState('');
65
+ const [inputError, setInputError] = useState(null);
66
+ const [branchName, setBranchName] = useState('');
67
+ const [versionPath, setVersionPath] = useState('');
68
+ const [done, setDone] = useState(false);
69
+ const [cloneProgress, setCloneProgress] = useState(null);
70
+ const [pipOutput, setPipOutput] = useState('');
71
+ const [retryCount, setRetryCount] = useState(0);
72
+ const [errorAction, setErrorAction] = useState(0); // 0 = Relancer, 1 = Retour
73
+ const [focus, setFocus] = useState('input');
74
+ const [pendingSelected, setPendingSelected] = useState(0);
75
+ const [installedSelected, setInstalledSelected] = useState(0);
76
+ // Refs for use inside async callbacks
77
+ const dispatchRef = useRef(dispatch);
78
+ dispatchRef.current = dispatch;
79
+ const onCompleteRef = useRef(onComplete);
80
+ onCompleteRef.current = onComplete;
81
+ const branchNameRef = useRef('');
82
+ const versionPathRef = useRef('');
83
+ const pendingInstalls = Object.values(config.pending_installs ?? {});
84
+ const installedVersions = Object.values(config.odoo_versions ?? {});
85
+ // ── Handlers defined before useInput ──────────────────────────────────────
86
+ const handleRetry = () => {
87
+ const step = STEP_DEFS[currentStepIndex];
88
+ if (!step)
89
+ return;
90
+ dispatch({ type: 'SET_STATUS', id: step.id, status: 'pending', errorMessage: undefined });
91
+ setRetryCount(c => c + 1);
92
+ };
93
+ const handleReinstall = (version) => {
94
+ branchNameRef.current = version.branch;
95
+ versionPathRef.current = version.path;
96
+ setBranchName(version.branch);
97
+ setVersionPath(version.path);
98
+ dispatch({ type: 'RESET', steps: buildResumedSteps('branch_input') });
99
+ setCurrentStepIndex(1);
100
+ };
101
+ const handleResume = (pending) => {
102
+ branchNameRef.current = pending.branch;
103
+ versionPathRef.current = pending.path;
104
+ setBranchName(pending.branch);
105
+ setVersionPath(pending.path);
106
+ const resumedSteps = buildResumedSteps(pending.lastCompletedStep);
107
+ dispatch({ type: 'RESET', steps: resumedSteps });
108
+ const lastIdx = pending.lastCompletedStep
109
+ ? STEP_DEFS.findIndex(s => s.id === pending.lastCompletedStep)
110
+ : -1;
111
+ setCurrentStepIndex(lastIdx + 1);
112
+ };
113
+ // ── Input hooks ────────────────────────────────────────────────────────────
114
+ // Input field: Escape = back, ↓ = switch to pending or installed list
115
+ useInput((_char, key) => {
116
+ if (key.escape) {
117
+ onBack();
118
+ return;
119
+ }
120
+ if (key.downArrow) {
121
+ if (pendingInstalls.length > 0) {
122
+ setFocus('pending');
123
+ setPendingSelected(0);
124
+ }
125
+ else if (installedVersions.length > 0) {
126
+ setFocus('installed');
127
+ setInstalledSelected(0);
128
+ }
129
+ }
130
+ }, { isActive: currentStepIndex === 0 && focus === 'input' });
131
+ // Pending list navigation
132
+ useInput((_char, key) => {
133
+ if (key.escape || (key.upArrow && pendingSelected === 0)) {
134
+ setFocus('input');
135
+ return;
136
+ }
137
+ if (key.upArrow)
138
+ setPendingSelected(p => p - 1);
139
+ if (key.downArrow) {
140
+ if (pendingSelected < pendingInstalls.length - 1)
141
+ setPendingSelected(p => p + 1);
142
+ else if (installedVersions.length > 0) {
143
+ setFocus('installed');
144
+ setInstalledSelected(0);
145
+ }
146
+ }
147
+ if (key.return && pendingInstalls[pendingSelected]) {
148
+ handleResume(pendingInstalls[pendingSelected]);
149
+ }
150
+ }, { isActive: currentStepIndex === 0 && focus === 'pending' });
151
+ // Installed versions navigation
152
+ useInput((_char, key) => {
153
+ if (key.escape || (key.upArrow && installedSelected === 0)) {
154
+ if (pendingInstalls.length > 0) {
155
+ setFocus('pending');
156
+ setPendingSelected(pendingInstalls.length - 1);
157
+ }
158
+ else
159
+ setFocus('input');
160
+ return;
161
+ }
162
+ if (key.upArrow)
163
+ setInstalledSelected(p => p - 1);
164
+ if (key.downArrow)
165
+ setInstalledSelected(p => Math.min(p + 1, installedVersions.length - 1));
166
+ if (key.return && installedVersions[installedSelected]) {
167
+ handleReinstall(installedVersions[installedSelected]);
168
+ }
169
+ }, { isActive: currentStepIndex === 0 && focus === 'installed' });
170
+ // Error recovery: Relancer / Retour
171
+ useInput((_char, key) => {
172
+ if (key.leftArrow)
173
+ setErrorAction(0);
174
+ if (key.rightArrow)
175
+ setErrorAction(1);
176
+ if (key.escape)
177
+ onBack();
178
+ if (key.return)
179
+ errorAction === 0 ? handleRetry() : onBack();
180
+ }, { isActive: !!steps.find(s => s.status === 'error') && currentStepIndex > 0 });
181
+ // Done: wait for Escape before navigating back
182
+ useInput((_char, key) => { if (key.escape)
183
+ onCompleteRef.current(); }, { isActive: done });
184
+ // ── Save progress helper ───────────────────────────────────────────────────
185
+ const saveProgress = useCallback(async (lastCompletedStep) => {
186
+ try {
187
+ const current = await readConfig();
188
+ await writeConfig({
189
+ ...current,
190
+ pending_installs: {
191
+ ...(current.pending_installs ?? {}),
192
+ [branchNameRef.current]: {
193
+ branch: branchNameRef.current,
194
+ path: versionPathRef.current,
195
+ lastCompletedStep,
196
+ },
197
+ },
198
+ });
199
+ }
200
+ catch { /* non-critical — installation continues regardless */ }
201
+ }, []);
202
+ const saveProgressRef = useRef(saveProgress);
203
+ saveProgressRef.current = saveProgress;
204
+ // ── Branch input submit ────────────────────────────────────────────────────
205
+ const handleBranchSubmit = async (value) => {
206
+ const name = value.trim();
207
+ if (!name) {
208
+ setInputError('Le nom de la branche ne peut pas être vide.');
209
+ return;
210
+ }
211
+ if (config.odoo_versions[name]) {
212
+ const existing = config.odoo_versions[name];
213
+ if (await dirExists(existing.path)) {
214
+ setInputError(`La version "${name}" est déjà installée dans ${existing.path}`);
215
+ return;
216
+ }
217
+ // Dossier supprimé manuellement → on nettoie la config et on réinstalle
218
+ const current = await readConfig();
219
+ const { [name]: _removed, ...restVersions } = current.odoo_versions;
220
+ await writeConfig({ ...current, odoo_versions: restVersions });
221
+ }
222
+ const path = join(config.odoo_path_repo, name);
223
+ branchNameRef.current = name;
224
+ versionPathRef.current = path;
225
+ setBranchName(name);
226
+ setVersionPath(path);
227
+ dispatch({ type: 'SET_STATUS', id: 'branch_input', status: 'success', errorMessage: name });
228
+ void saveProgressRef.current('branch_input');
229
+ setCurrentStepIndex(1);
230
+ };
231
+ // ── Auto steps ──────────────────────────────────────────────────────────
232
+ const runCheckCommunity = useCallback(async () => {
233
+ dispatchRef.current({ type: 'SET_STATUS', id: 'check_community', status: 'running' });
234
+ const r = await checkBranch(ODOO_COMMUNITY_URL, branchNameRef.current);
235
+ if (r.ok) {
236
+ dispatchRef.current({ type: 'SET_STATUS', id: 'check_community', status: 'success' });
237
+ void saveProgressRef.current('check_community');
238
+ setCurrentStepIndex(2);
239
+ }
240
+ else {
241
+ dispatchRef.current({ type: 'SET_STATUS', id: 'check_community', status: 'error', errorMessage: r.error });
242
+ }
243
+ }, []);
244
+ const runCheckEnterprise = useCallback(async () => {
245
+ dispatchRef.current({ type: 'SET_STATUS', id: 'check_enterprise', status: 'running' });
246
+ const r = await checkBranch(ODOO_ENTERPRISE_URL, branchNameRef.current);
247
+ if (r.ok) {
248
+ dispatchRef.current({ type: 'SET_STATUS', id: 'check_enterprise', status: 'success' });
249
+ void saveProgressRef.current('check_enterprise');
250
+ setCurrentStepIndex(3);
251
+ }
252
+ else {
253
+ dispatchRef.current({ type: 'SET_STATUS', id: 'check_enterprise', status: 'error', errorMessage: r.error });
254
+ }
255
+ }, []);
256
+ const runCreateDir = useCallback(async () => {
257
+ dispatchRef.current({ type: 'SET_STATUS', id: 'create_dir', status: 'running' });
258
+ try {
259
+ await mkdir(versionPathRef.current, { recursive: true });
260
+ dispatchRef.current({
261
+ type: 'SET_STATUS', id: 'create_dir', status: 'success',
262
+ errorMessage: versionPathRef.current,
263
+ });
264
+ void saveProgressRef.current('create_dir');
265
+ setCurrentStepIndex(4);
266
+ }
267
+ catch (err) {
268
+ dispatchRef.current({
269
+ type: 'SET_STATUS', id: 'create_dir', status: 'error',
270
+ errorMessage: err.message,
271
+ });
272
+ }
273
+ }, []);
274
+ const runCloneCommunity = useCallback(async () => {
275
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_community', status: 'running' });
276
+ setCloneProgress(null);
277
+ const dest = join(versionPathRef.current, 'community');
278
+ if (await dirExists(dest)) {
279
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_community', status: 'success', errorMessage: `${dest} (déjà présent)` });
280
+ void saveProgressRef.current('clone_community');
281
+ setCurrentStepIndex(5);
282
+ return;
283
+ }
284
+ let lastUpdate = 0;
285
+ const r = await cloneRepo(ODOO_COMMUNITY_URL, dest, branchNameRef.current, progress => {
286
+ const now = Date.now();
287
+ if (now - lastUpdate >= 80) { // ~12 fps max
288
+ lastUpdate = now;
289
+ setCloneProgress(progress);
290
+ }
291
+ });
292
+ setCloneProgress(null);
293
+ if (r.ok) {
294
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_community', status: 'success', errorMessage: dest });
295
+ void saveProgressRef.current('clone_community');
296
+ setCurrentStepIndex(5);
297
+ }
298
+ else {
299
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_community', status: 'error', errorMessage: r.error });
300
+ }
301
+ }, []);
302
+ const runCloneEnterprise = useCallback(async () => {
303
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_enterprise', status: 'running' });
304
+ setCloneProgress(null);
305
+ const dest = join(versionPathRef.current, 'enterprise');
306
+ if (await dirExists(dest)) {
307
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_enterprise', status: 'success', errorMessage: `${dest} (déjà présent)` });
308
+ void saveProgressRef.current('clone_enterprise');
309
+ setCurrentStepIndex(6);
310
+ return;
311
+ }
312
+ let lastUpdate = 0;
313
+ const r = await cloneRepo(ODOO_ENTERPRISE_URL, dest, branchNameRef.current, progress => {
314
+ const now = Date.now();
315
+ if (now - lastUpdate >= 80) {
316
+ lastUpdate = now;
317
+ setCloneProgress(progress);
318
+ }
319
+ });
320
+ setCloneProgress(null);
321
+ if (r.ok) {
322
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_enterprise', status: 'success', errorMessage: dest });
323
+ void saveProgressRef.current('clone_enterprise');
324
+ setCurrentStepIndex(6);
325
+ }
326
+ else {
327
+ dispatchRef.current({ type: 'SET_STATUS', id: 'clone_enterprise', status: 'error', errorMessage: r.error });
328
+ }
329
+ }, []);
330
+ const runCreateVenv = useCallback(async () => {
331
+ dispatchRef.current({ type: 'SET_STATUS', id: 'create_venv', status: 'running' });
332
+ const venvPath = join(versionPathRef.current, '.venv');
333
+ const r = await createVenv(venvPath);
334
+ if (r.ok) {
335
+ dispatchRef.current({ type: 'SET_STATUS', id: 'create_venv', status: 'success', errorMessage: venvPath });
336
+ void saveProgressRef.current('create_venv');
337
+ setCurrentStepIndex(7);
338
+ }
339
+ else {
340
+ dispatchRef.current({ type: 'SET_STATUS', id: 'create_venv', status: 'error', errorMessage: r.error });
341
+ }
342
+ }, []);
343
+ const runInstallRequirements = useCallback(async () => {
344
+ dispatchRef.current({ type: 'SET_STATUS', id: 'install_requirements', status: 'running' });
345
+ setPipOutput('');
346
+ const pipPath = join(versionPathRef.current, '.venv', 'bin', 'pip');
347
+ const requirementsPath = join(versionPathRef.current, 'community', 'requirements.txt');
348
+ const r = await installRequirements(pipPath, requirementsPath, line => {
349
+ setPipOutput(line);
350
+ });
351
+ if (r.ok) {
352
+ dispatchRef.current({ type: 'SET_STATUS', id: 'install_requirements', status: 'success' });
353
+ void saveProgressRef.current('install_requirements');
354
+ setCurrentStepIndex(8);
355
+ }
356
+ else {
357
+ dispatchRef.current({ type: 'SET_STATUS', id: 'install_requirements', status: 'error', errorMessage: r.error });
358
+ }
359
+ }, []);
360
+ const runCreateExtras = useCallback(async () => {
361
+ dispatchRef.current({ type: 'SET_STATUS', id: 'create_extras', status: 'running' });
362
+ const base = versionPathRef.current;
363
+ const branch = branchNameRef.current;
364
+ try {
365
+ await mkdir(join(base, 'custom'), { recursive: true });
366
+ await mkdir(join(base, 'config'), { recursive: true });
367
+ const current = await readConfig();
368
+ // Remove from pending_installs and add to odoo_versions atomically
369
+ const { [branch]: _removed, ...restPending } = current.pending_installs ?? {};
370
+ await writeConfig({
371
+ ...current,
372
+ odoo_versions: { ...current.odoo_versions, [branch]: { branch, path: base } },
373
+ pending_installs: restPending,
374
+ });
375
+ dispatchRef.current({ type: 'SET_STATUS', id: 'create_extras', status: 'success' });
376
+ setDone(true);
377
+ }
378
+ catch (err) {
379
+ dispatchRef.current({
380
+ type: 'SET_STATUS', id: 'create_extras', status: 'error',
381
+ errorMessage: err.message,
382
+ });
383
+ }
384
+ }, []);
385
+ useEffect(() => {
386
+ if (currentStepIndex === 0)
387
+ return;
388
+ switch (currentStepIndex) {
389
+ case 1:
390
+ void runCheckCommunity();
391
+ break;
392
+ case 2:
393
+ void runCheckEnterprise();
394
+ break;
395
+ case 3:
396
+ void runCreateDir();
397
+ break;
398
+ case 4:
399
+ void runCloneCommunity();
400
+ break;
401
+ case 5:
402
+ void runCloneEnterprise();
403
+ break;
404
+ case 6:
405
+ void runCreateVenv();
406
+ break;
407
+ case 7:
408
+ void runInstallRequirements();
409
+ break;
410
+ case 8:
411
+ void runCreateExtras();
412
+ break;
413
+ }
414
+ // retryCount in deps: re-triggers the current step when user retries
415
+ // eslint-disable-next-line react-hooks/exhaustive-deps
416
+ }, [currentStepIndex, retryCount, runCheckCommunity, runCheckEnterprise, runCreateDir, runCloneCommunity, runCloneEnterprise, runCreateVenv, runInstallRequirements, runCreateExtras]);
417
+ const isCloneStep = currentStepIndex === 4 || currentStepIndex === 5;
418
+ const isPipStep = currentStepIndex === 7;
419
+ const errorStep = steps.find(s => s.status === 'error');
420
+ const cloneLabel = currentStepIndex === 4 ? 'community' : 'enterprise';
421
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
422
+ React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
423
+ React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
424
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
425
+ React.createElement(Text, { color: getSecondaryColor(config), bold: true }, "Installer une version"),
426
+ currentStepIndex === 0 && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
427
+ focus === 'input' ? (React.createElement(React.Fragment, null,
428
+ React.createElement(Text, { color: "white" }, "Nom de la branche Odoo (ex : 17.0, 16.0) :"),
429
+ React.createElement(Box, null,
430
+ React.createElement(Text, { color: textColor, dimColor: true }, '› '),
431
+ React.createElement(TextInput, { value: branchInput, onChange: v => { setBranchInput(v); setInputError(null); }, onSubmit: handleBranchSubmit, placeholder: "17.0" })),
432
+ inputError && React.createElement(Text, { color: "red" }, inputError),
433
+ React.createElement(Text, { color: textColor, dimColor: true },
434
+ '↵ valider · Échap retour',
435
+ pendingInstalls.length > 0 ? ' · ↓ reprises' : '',
436
+ installedVersions.length > 0 ? ' · ↓ installées' : ''))) : (React.createElement(Text, { color: textColor, dimColor: true }, "Nom de la branche Odoo (ex : 17.0, 16.0)")),
437
+ pendingInstalls.length > 0 && (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
438
+ React.createElement(Text, { color: "yellow", bold: true }, "Installations en cours :"),
439
+ pendingInstalls.map((p, i) => {
440
+ const isSelected = focus === 'pending' && i === pendingSelected;
441
+ const lastIdx = p.lastCompletedStep
442
+ ? STEP_DEFS.findIndex(s => s.id === p.lastCompletedStep)
443
+ : -1;
444
+ const nextStep = STEP_DEFS[lastIdx + 1];
445
+ const resumeLabel = nextStep?.label ?? '–';
446
+ return (React.createElement(Text, { key: p.branch, color: isSelected ? 'black' : 'white', backgroundColor: isSelected ? 'yellow' : undefined, bold: isSelected },
447
+ ` ${isSelected ? '▶' : ' '} ${p.branch} `,
448
+ React.createElement(Text, { color: isSelected ? 'black' : 'gray', dimColor: !isSelected }, `reprendre depuis : ${resumeLabel}`)));
449
+ }),
450
+ focus === 'pending' && (React.createElement(Text, { color: textColor, dimColor: true }, '↑↓ naviguer · ↵ reprendre · Échap retour')))),
451
+ installedVersions.length > 0 && (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
452
+ React.createElement(Text, { color: "white", bold: true }, "Versions install\u00E9es :"),
453
+ installedVersions.map((v, i) => {
454
+ const isSelected = focus === 'installed' && i === installedSelected;
455
+ return (React.createElement(Text, { key: v.branch, color: isSelected ? 'black' : 'white', backgroundColor: isSelected ? cursorColor : undefined, bold: isSelected },
456
+ ` ${isSelected ? '▶' : ' '} ${v.branch} `,
457
+ React.createElement(Text, { color: isSelected ? 'black' : 'gray', dimColor: !isSelected }, v.path)));
458
+ }),
459
+ focus === 'installed' && (React.createElement(Text, { color: textColor, dimColor: true }, '↑↓ naviguer · ↵ relancer · Échap retour')))))),
460
+ currentStepIndex > 0 && !isCloneStep && !done && !errorStep && (React.createElement(Box, { marginTop: 1, flexDirection: "column", gap: 0 },
461
+ React.createElement(Text, { color: textColor },
462
+ 'Installation de ',
463
+ React.createElement(Text, { color: getPrimaryColor(config), bold: true }, branchName),
464
+ '…'),
465
+ isPipStep && pipOutput !== '' && (React.createElement(Text, { color: textColor, dimColor: true }, pipOutput)))),
466
+ errorStep && currentStepIndex > 0 && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
467
+ React.createElement(Box, { flexDirection: "row", gap: 2 },
468
+ React.createElement(Text, { color: errorAction === 0 ? 'black' : 'white', backgroundColor: errorAction === 0 ? cursorColor : undefined, bold: errorAction === 0 }, ' ↺ Relancer '),
469
+ React.createElement(Text, { color: errorAction === 1 ? 'black' : 'white', backgroundColor: errorAction === 1 ? 'gray' : undefined, bold: errorAction === 1 }, ' ← Retour ')),
470
+ React.createElement(Text, { color: textColor, dimColor: true }, '◀▶ choisir · ↵ confirmer · Échap retour'))),
471
+ isCloneStep && !errorStep && (React.createElement(Box, { flexDirection: "column", marginTop: 1, gap: 0 },
472
+ React.createElement(Text, { color: textColor },
473
+ 'Clonage ',
474
+ React.createElement(Text, { color: getPrimaryColor(config), bold: true }, cloneLabel),
475
+ ` → ${join(versionPath, cloneLabel)}`),
476
+ React.createElement(Box, { marginTop: 1, flexDirection: "column", gap: 0 }, cloneProgress ? (React.createElement(React.Fragment, null,
477
+ React.createElement(ProgressBar, { percent: cloneProgress.percent, textColor: textColor }),
478
+ React.createElement(Box, { gap: 2, marginTop: 0 },
479
+ React.createElement(Text, { color: textColor, dimColor: true }, PHASE_LABEL[cloneProgress.phase]),
480
+ cloneProgress.speed && (React.createElement(Text, { color: getPrimaryColor(config), dimColor: true }, cloneProgress.speed))))) : (React.createElement(Text, { color: textColor, dimColor: true }, "\u27F3 Connexion au d\u00E9p\u00F4t\u2026"))))),
481
+ done && (React.createElement(Box, { flexDirection: "column", marginTop: 1, gap: 1 },
482
+ React.createElement(Text, { color: "green" },
483
+ '✓ ',
484
+ React.createElement(Text, { bold: true }, branchName),
485
+ ' installé avec succès.'),
486
+ React.createElement(Box, { flexDirection: "column", gap: 0 },
487
+ React.createElement(Text, { color: textColor, dimColor: true }, ` community/ → ${join(versionPath, 'community')}`),
488
+ React.createElement(Text, { color: textColor, dimColor: true }, ` enterprise/ → ${join(versionPath, 'enterprise')}`),
489
+ React.createElement(Text, { color: textColor, dimColor: true }, ` .venv/ → ${join(versionPath, '.venv')}`),
490
+ React.createElement(Text, { color: textColor, dimColor: true }, ` custom/`),
491
+ React.createElement(Text, { color: textColor, dimColor: true }, ` config/`)),
492
+ React.createElement(Text, { color: "white" }, 'Échap retour'))))),
493
+ React.createElement(StepsPanel, { steps: steps, textColor: textColor }),
494
+ React.createElement(ErrorPanel, { steps: steps })));
495
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { NupoConfig, OdooServiceConfig, CliStartArgs } from '../types/index.js';
3
+ interface OdooScreenProps {
4
+ leftWidth: number;
5
+ config: NupoConfig;
6
+ onBack: () => void;
7
+ onConfigChange: () => void;
8
+ onServiceRunning: (service: OdooServiceConfig) => void;
9
+ onServiceStopped: () => void;
10
+ autoStart?: CliStartArgs;
11
+ }
12
+ export declare function OdooScreen({ leftWidth, config, onBack, onConfigChange, onServiceRunning, onServiceStopped, autoStart }: OdooScreenProps): React.JSX.Element;
13
+ export {};
@@ -0,0 +1,76 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
4
+ import { LeftPanel } from '../components/LeftPanel.js';
5
+ import { InstallVersionScreen } from './InstallVersionScreen.js';
6
+ import { UpgradeVersionScreen } from './UpgradeVersionScreen.js';
7
+ import { OdooServiceScreen } from './OdooServiceScreen.js';
8
+ import { StartServiceScreen } from './StartServiceScreen.js';
9
+ const ODOO_OPTIONS = [
10
+ {
11
+ id: 'install',
12
+ label: 'Installer une version',
13
+ description: "Installer une nouvelle version d'Odoo : télécharge community et enterprise avec --depth 1.",
14
+ },
15
+ {
16
+ id: 'upgrade',
17
+ label: 'Mise à niveau',
18
+ description: "Mettre à jour une version Odoo installée : récupère les derniers commits de community et enterprise.",
19
+ },
20
+ {
21
+ id: 'service',
22
+ label: 'Configurer Service Odoo',
23
+ description: "Créer ou modifier un fichier de configuration .conf pour démarrer un service Odoo.",
24
+ },
25
+ {
26
+ id: 'start',
27
+ label: 'Démarrer Service Odoo',
28
+ description: "Lancer un service Odoo configuré avec des arguments supplémentaires optionnels.",
29
+ },
30
+ ];
31
+ export function OdooScreen({ leftWidth, config, onBack, onConfigChange, onServiceRunning, onServiceStopped, autoStart }) {
32
+ const [subScreen, setSubScreen] = useState(null);
33
+ const [selected, setSelected] = useState(0);
34
+ const textColor = getTextColor(config);
35
+ const cursorColor = getCursorColor(config);
36
+ // Auto-navigate to start screen when CLI args are present
37
+ useEffect(() => {
38
+ if (autoStart)
39
+ setSubScreen('start');
40
+ }, []);
41
+ useInput((_char, key) => {
42
+ if (key.upArrow)
43
+ setSelected(prev => (prev - 1 + ODOO_OPTIONS.length) % ODOO_OPTIONS.length);
44
+ if (key.downArrow)
45
+ setSelected(prev => (prev + 1) % ODOO_OPTIONS.length);
46
+ if (key.return)
47
+ setSubScreen(ODOO_OPTIONS[selected].id);
48
+ if (key.escape)
49
+ onBack();
50
+ }, { isActive: subScreen === null });
51
+ if (subScreen === 'install') {
52
+ return (React.createElement(InstallVersionScreen, { config: config, leftWidth: leftWidth, onComplete: () => { onConfigChange(); setSubScreen(null); }, onBack: () => setSubScreen(null) }));
53
+ }
54
+ if (subScreen === 'upgrade') {
55
+ return (React.createElement(UpgradeVersionScreen, { config: config, leftWidth: leftWidth, onBack: () => setSubScreen(null) }));
56
+ }
57
+ if (subScreen === 'service') {
58
+ return (React.createElement(OdooServiceScreen, { config: config, leftWidth: leftWidth, onBack: () => setSubScreen(null), onConfigChange: () => { onConfigChange(); setSubScreen(null); } }));
59
+ }
60
+ if (subScreen === 'start') {
61
+ return (React.createElement(StartServiceScreen, { config: config, leftWidth: leftWidth, onBack: () => setSubScreen(null), onServiceRunning: onServiceRunning, onServiceStopped: onServiceStopped, autoStart: autoStart }));
62
+ }
63
+ const current = ODOO_OPTIONS[selected];
64
+ return (React.createElement(Box, { flexDirection: "row" },
65
+ React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
66
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
67
+ React.createElement(Text, { color: getSecondaryColor(config), bold: true }, "Odoo"),
68
+ React.createElement(Box, { flexDirection: "column", gap: 0 }, ODOO_OPTIONS.map((opt, i) => {
69
+ const isSelected = i === selected;
70
+ return (React.createElement(Text, { key: opt.id, color: isSelected ? 'black' : 'white', backgroundColor: isSelected ? cursorColor : undefined, bold: isSelected }, ` ${isSelected ? '▶' : ' '} ${opt.label}`));
71
+ })),
72
+ React.createElement(Box, { borderStyle: "round", borderColor: textColor, paddingX: 1, paddingY: 0 },
73
+ React.createElement(Text, { color: textColor, wrap: "wrap" }, current.description)),
74
+ React.createElement(Box, null,
75
+ React.createElement(Text, { color: textColor, dimColor: true }, '↑↓ naviguer · ↵ sélectionner · Échap retour')))));
76
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { NupoConfig } from '../types/index.js';
3
+ interface OdooServiceScreenProps {
4
+ config: NupoConfig;
5
+ leftWidth: number;
6
+ onBack: () => void;
7
+ onConfigChange: () => void;
8
+ }
9
+ export declare function OdooServiceScreen({ config, leftWidth, onBack, onConfigChange }: OdooServiceScreenProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,51 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
4
+ import { LeftPanel } from '../components/LeftPanel.js';
5
+ import { ConfigureServiceScreen } from './ConfigureServiceScreen.js';
6
+ export function OdooServiceScreen({ config, leftWidth, onBack, onConfigChange }) {
7
+ const services = Object.values(config.odoo_services ?? {});
8
+ const textColor = getTextColor(config);
9
+ const cursorColor = getCursorColor(config);
10
+ const itemCount = 1 + services.length; // 0 = nouveau, 1..n = services
11
+ const [selected, setSelected] = useState(0);
12
+ const [subScreen, setSubScreen] = useState(null);
13
+ useInput((_char, key) => {
14
+ if (key.escape) {
15
+ onBack();
16
+ return;
17
+ }
18
+ if (key.upArrow)
19
+ setSelected(p => (p - 1 + itemCount) % itemCount);
20
+ if (key.downArrow)
21
+ setSelected(p => (p + 1) % itemCount);
22
+ if (key.return) {
23
+ if (selected === 0)
24
+ setSubScreen('new');
25
+ else
26
+ setSubScreen(services[selected - 1]);
27
+ }
28
+ }, { isActive: subScreen === null });
29
+ if (subScreen !== null) {
30
+ return (React.createElement(ConfigureServiceScreen, { config: config, leftWidth: leftWidth, initialService: subScreen === 'new' ? undefined : subScreen, onComplete: () => { onConfigChange(); setSubScreen(null); }, onBack: () => setSubScreen(null) }));
31
+ }
32
+ return (React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
33
+ React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
34
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
35
+ React.createElement(Text, { color: getSecondaryColor(config), bold: true }, "Configurer Service Odoo"),
36
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, gap: 0 },
37
+ React.createElement(Text, { color: selected === 0 ? 'black' : 'cyan', backgroundColor: selected === 0 ? cursorColor : undefined, bold: selected === 0 }, ` ${selected === 0 ? '▶' : ' '} + Nouveau service`),
38
+ services.length > 0 && (React.createElement(Text, { color: textColor, dimColor: true }, ' ─────────────────')),
39
+ services.length === 0 && (React.createElement(Text, { color: textColor, dimColor: true }, ' Aucun service configuré')),
40
+ services.map((s, i) => {
41
+ const isSel = i + 1 === selected;
42
+ return (React.createElement(Text, { key: s.name, color: isSel ? 'black' : 'white', backgroundColor: isSel ? cursorColor : undefined, bold: isSel },
43
+ ` ${isSel ? '▶' : ' '} ${s.name} `,
44
+ React.createElement(Text, { color: isSel ? 'black' : 'gray', dimColor: !isSel },
45
+ s.branch,
46
+ s.useEnterprise ? ' · Enterprise' : '',
47
+ s.customFolders.length > 0 ? ` · ${s.customFolders.length} module(s)` : '')));
48
+ })),
49
+ React.createElement(Box, { marginTop: 1 },
50
+ React.createElement(Text, { color: textColor, dimColor: true }, '↑↓ naviguer · ↵ sélectionner · Échap retour')))));
51
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { NupoConfig, OdooServiceConfig, CliStartArgs } from '../types/index.js';
3
+ interface StartServiceScreenProps {
4
+ config: NupoConfig;
5
+ leftWidth: number;
6
+ onBack: () => void;
7
+ onServiceRunning: (service: OdooServiceConfig) => void;
8
+ onServiceStopped: () => void;
9
+ autoStart?: CliStartArgs;
10
+ }
11
+ export declare function StartServiceScreen({ config, leftWidth, onBack, onServiceRunning, onServiceStopped, autoStart, }: StartServiceScreenProps): React.JSX.Element;
12
+ export {};