@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,386 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { spawn } from 'child_process';
5
+ import { join } from 'path';
6
+ import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
7
+ import { LeftPanel } from '../components/LeftPanel.js';
8
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
9
+ const ARGS_ITEMS = [
10
+ { key: 'shell', label: 'shell', type: 'toggle' },
11
+ { key: 'db', label: '-d <database>', type: 'input' },
12
+ { key: 'module', label: '-u <module>', type: 'input' },
13
+ { key: 'install', label: '-i <module>', type: 'input' },
14
+ { key: 'stop_after_init', label: '--stop-after-init', type: 'toggle' },
15
+ { key: 'launch', label: 'Lancer →', type: 'action' },
16
+ ];
17
+ // ── Helpers ──────────────────────────────────────────────────────────────────
18
+ function httpPortForBranch(branch) {
19
+ const m = branch.match(/(\d+)\./);
20
+ return m ? 8000 + parseInt(m[1], 10) : 8069;
21
+ }
22
+ function buildAddonsPaths(service) {
23
+ const paths = [join(service.versionPath, 'community', 'addons')];
24
+ if (service.useEnterprise)
25
+ paths.push(join(service.versionPath, 'enterprise'));
26
+ for (const f of service.customFolders)
27
+ paths.push(join(service.versionPath, 'custom', f));
28
+ return paths;
29
+ }
30
+ function buildLaunchCmd(service, opts) {
31
+ const python = join(service.versionPath, '.venv', 'bin', 'python3');
32
+ const odooBin = join(service.versionPath, 'community', 'odoo-bin');
33
+ const args = [odooBin];
34
+ if (opts.shell)
35
+ args.push('shell');
36
+ args.push('-c', service.confPath);
37
+ args.push('--addons-path', buildAddonsPaths(service).join(','));
38
+ if (opts.db)
39
+ args.push('-d', opts.db);
40
+ if (opts.module)
41
+ args.push('-u', opts.module);
42
+ if (opts.install)
43
+ args.push('-i', opts.install);
44
+ if (opts.stopAfterInit)
45
+ args.push('--stop-after-init');
46
+ return { cmd: python, args };
47
+ }
48
+ const LEVEL_COLORS = {
49
+ INFO: 'green',
50
+ WARNING: 'yellow',
51
+ ERROR: 'red',
52
+ CRITICAL: 'red',
53
+ DEBUG: 'gray',
54
+ };
55
+ function LogLine({ line, idx }) {
56
+ const match = line.match(/\b(INFO|WARNING|ERROR|CRITICAL|DEBUG)\b/);
57
+ if (!match || match.index === undefined) {
58
+ return React.createElement(Text, { key: idx, color: "white", wrap: "wrap" }, line);
59
+ }
60
+ const level = match[0];
61
+ const before = line.slice(0, match.index);
62
+ const after = line.slice(match.index + level.length);
63
+ return (React.createElement(Text, { key: idx, color: "white", wrap: "wrap" },
64
+ before,
65
+ React.createElement(Text, { color: LEVEL_COLORS[level] }, level),
66
+ after));
67
+ }
68
+ // ── Component ────────────────────────────────────────────────────────────────
69
+ export function StartServiceScreen({ config, leftWidth, onBack, onServiceRunning, onServiceStopped, autoStart, }) {
70
+ const { rows } = useTerminalSize();
71
+ const services = Object.values(config.odoo_services ?? {});
72
+ const textColor = getTextColor(config);
73
+ const cursorColor = getCursorColor(config);
74
+ const [step, setStep] = useState('select');
75
+ const [selected, setSelected] = useState(0);
76
+ const [argsCursor, setArgsCursor] = useState(0);
77
+ const [useShell, setUseShell] = useState(false);
78
+ const [dbName, setDbName] = useState('');
79
+ const [moduleName, setModuleName] = useState('');
80
+ const [installName, setInstallName] = useState('');
81
+ const [stopAfterInit, setStopAfterInit] = useState(false);
82
+ const [inputValue, setInputValue] = useState('');
83
+ const [logs, setLogs] = useState([]);
84
+ const [exitCode, setExitCode] = useState(null);
85
+ const [scrollOffset, setScrollOffset] = useState(0);
86
+ const [filterText, setFilterText] = useState('');
87
+ const [filterMode, setFilterMode] = useState(false);
88
+ const [autoStartError, setAutoStartError] = useState(null);
89
+ const childRef = useRef(null);
90
+ const mountedRef = useRef(true);
91
+ const activeServiceRef = useRef(null);
92
+ const userStoppedRef = useRef(false);
93
+ const maxScrollRef = useRef(0);
94
+ useEffect(() => {
95
+ return () => {
96
+ mountedRef.current = false;
97
+ childRef.current?.kill('SIGTERM');
98
+ };
99
+ }, []);
100
+ const service = services[selected];
101
+ const warnNoDb = !!(moduleName || installName) && !dbName;
102
+ // Fixed rows consumed outside the log box:
103
+ // App borders(2) + Header(2) + running view padY(2) + gaps(4)
104
+ // + header row(1) + urls box(4) + filter box(3) + controls(1) + log borders(2) = 21
105
+ const logBoxHeight = Math.max(5, rows - 19); // outer height incl. borders
106
+ const visibleLines = Math.max(3, logBoxHeight - 2); // inner content lines
107
+ // ── Launch ────────────────────────────────────────────────────────────────
108
+ const launchService = (svc, overrideOpts) => {
109
+ const target = svc ?? service;
110
+ if (!target)
111
+ return;
112
+ const opts = overrideOpts ?? { shell: useShell, db: dbName, module: moduleName, install: installName, stopAfterInit };
113
+ activeServiceRef.current = target;
114
+ setLogs([]);
115
+ setExitCode(null);
116
+ setScrollOffset(0);
117
+ setStep('running');
118
+ onServiceRunning(target);
119
+ const { cmd, args } = buildLaunchCmd(target, opts);
120
+ const proc = spawn(cmd, args);
121
+ childRef.current = proc;
122
+ const appendChunk = (chunk) => {
123
+ if (!mountedRef.current)
124
+ return;
125
+ const lines = chunk.toString().split('\n').filter(l => l.length > 0);
126
+ setLogs(prev => [...prev, ...lines].slice(-(config.log_buffer_size ?? 500)));
127
+ };
128
+ proc.stdout?.on('data', appendChunk);
129
+ proc.stderr?.on('data', appendChunk);
130
+ proc.on('close', code => {
131
+ childRef.current = null;
132
+ if (!mountedRef.current)
133
+ return;
134
+ if (userStoppedRef.current) {
135
+ userStoppedRef.current = false;
136
+ setLogs([]);
137
+ setExitCode(null);
138
+ setScrollOffset(0);
139
+ setStep('args_list');
140
+ }
141
+ else {
142
+ setExitCode(code ?? -1);
143
+ }
144
+ onServiceStopped();
145
+ });
146
+ };
147
+ // ── Auto-start from CLI ────────────────────────────────────────────────────
148
+ useEffect(() => {
149
+ if (!autoStart)
150
+ return;
151
+ const svc = services.find(s => s.name === autoStart.serviceName);
152
+ if (!svc) {
153
+ setAutoStartError(`Service introuvable : "${autoStart.serviceName}"`);
154
+ return;
155
+ }
156
+ launchService(svc, {
157
+ shell: autoStart.shell,
158
+ db: autoStart.db ?? '',
159
+ module: autoStart.module ?? '',
160
+ install: autoStart.install ?? '',
161
+ stopAfterInit: autoStart.stopAfterInit,
162
+ });
163
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
164
+ // ── Input hooks ───────────────────────────────────────────────────────────
165
+ useInput((_char, key) => {
166
+ if (key.escape) {
167
+ onBack();
168
+ return;
169
+ }
170
+ if (autoStartError)
171
+ return;
172
+ if (services.length === 0)
173
+ return;
174
+ if (key.upArrow)
175
+ setSelected(p => Math.max(0, p - 1));
176
+ if (key.downArrow)
177
+ setSelected(p => Math.min(services.length - 1, p + 1));
178
+ if (key.return && service)
179
+ setStep('args_list');
180
+ }, { isActive: step === 'select' });
181
+ useInput((char, key) => {
182
+ if (key.escape) {
183
+ setStep('select');
184
+ return;
185
+ }
186
+ if (key.upArrow)
187
+ setArgsCursor(p => Math.max(0, p - 1));
188
+ if (key.downArrow)
189
+ setArgsCursor(p => Math.min(ARGS_ITEMS.length - 1, p + 1));
190
+ if (key.return || char === ' ') {
191
+ const item = ARGS_ITEMS[argsCursor];
192
+ switch (item.key) {
193
+ case 'shell':
194
+ setUseShell(p => !p);
195
+ break;
196
+ case 'stop_after_init':
197
+ setStopAfterInit(p => !p);
198
+ break;
199
+ case 'db':
200
+ setInputValue(dbName);
201
+ setStep('input_db');
202
+ break;
203
+ case 'module':
204
+ setInputValue(moduleName);
205
+ setStep('input_module');
206
+ break;
207
+ case 'install':
208
+ setInputValue(installName);
209
+ setStep('input_install');
210
+ break;
211
+ case 'launch':
212
+ launchService();
213
+ break;
214
+ }
215
+ }
216
+ }, { isActive: step === 'args_list' });
217
+ useInput((_char, key) => {
218
+ if (key.escape)
219
+ setStep('args_list');
220
+ }, { isActive: step === 'input_db' });
221
+ useInput((_char, key) => {
222
+ if (key.escape)
223
+ setStep('args_list');
224
+ }, { isActive: step === 'input_module' });
225
+ useInput((_char, key) => {
226
+ if (key.escape)
227
+ setStep('args_list');
228
+ }, { isActive: step === 'input_install' });
229
+ useInput((char, key) => {
230
+ if (exitCode !== null) {
231
+ if (key.escape) {
232
+ setStep('args_list');
233
+ setLogs([]);
234
+ setExitCode(null);
235
+ setScrollOffset(0);
236
+ }
237
+ return;
238
+ }
239
+ if (key.ctrl && char === 'c') {
240
+ userStoppedRef.current = true;
241
+ childRef.current?.kill('SIGTERM');
242
+ return;
243
+ }
244
+ if (char === '/') {
245
+ setFilterMode(true);
246
+ }
247
+ }, { isActive: step === 'running' && !filterMode });
248
+ // Filter mode input — Escape exits filter mode
249
+ useInput((_char, key) => {
250
+ if (key.escape) {
251
+ setFilterMode(false);
252
+ }
253
+ }, { isActive: step === 'running' && filterMode });
254
+ // Mouse wheel scroll — active only while running and not in filter mode
255
+ useEffect(() => {
256
+ if (step !== 'running' || filterMode)
257
+ return;
258
+ // Enable mouse reporting (basic + SGR extended mode)
259
+ process.stdout.write('\x1B[?1000h\x1B[?1006h');
260
+ const handleData = (data) => {
261
+ const str = data.toString();
262
+ // SGR format: ESC[<64;x;yM = scroll up, ESC[<65;x;yM = scroll down
263
+ if (/\x1B\[<6[45];/.test(str)) {
264
+ if (str.includes('\x1B[<64;'))
265
+ setScrollOffset(p => Math.min(maxScrollRef.current, p + 1));
266
+ if (str.includes('\x1B[<65;'))
267
+ setScrollOffset(p => Math.max(0, p - 1));
268
+ return;
269
+ }
270
+ // X10 fallback: ESC[M + 3 bytes, button byte 96=scroll up, 97=scroll down
271
+ if (str.startsWith('\x1B[M') && str.length >= 6) {
272
+ const btn = str.charCodeAt(3) - 32;
273
+ if (btn === 64)
274
+ setScrollOffset(p => Math.min(maxScrollRef.current, p + 1));
275
+ if (btn === 65)
276
+ setScrollOffset(p => Math.max(0, p - 1));
277
+ }
278
+ };
279
+ process.stdin.on('data', handleData);
280
+ return () => {
281
+ process.stdout.write('\x1B[?1000l\x1B[?1006l');
282
+ process.stdin.off('data', handleData);
283
+ };
284
+ }, [step, filterMode]);
285
+ // Reset scroll when filter changes
286
+ useEffect(() => { setScrollOffset(0); }, [filterText]);
287
+ // ── Log window ────────────────────────────────────────────────────────────
288
+ const filteredLogs = filterText
289
+ ? logs.filter(l => l.toLowerCase().includes(filterText.toLowerCase()))
290
+ : logs;
291
+ const maxScroll = Math.max(0, filteredLogs.length - visibleLines);
292
+ maxScrollRef.current = maxScroll;
293
+ const offset = Math.min(scrollOffset, maxScroll);
294
+ const end = filteredLogs.length - offset;
295
+ const start = Math.max(0, end - visibleLines);
296
+ const visibleLogs = filteredLogs.slice(start, end);
297
+ // ── Render helpers ────────────────────────────────────────────────────────
298
+ const argIsSet = {
299
+ shell: useShell,
300
+ db: !!dbName,
301
+ module: !!moduleName,
302
+ install: !!installName,
303
+ stop_after_init: stopAfterInit,
304
+ };
305
+ const argDisplay = { db: dbName, module: moduleName, install: installName };
306
+ const activeService = activeServiceRef.current;
307
+ // ── JSX ───────────────────────────────────────────────────────────────────
308
+ if (step === 'running' && activeService) {
309
+ return (React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 2, paddingY: 1, gap: 1 },
310
+ React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 0, flexDirection: "column" },
311
+ React.createElement(Text, { color: "yellow" }, `http://localhost:${httpPortForBranch(activeService.branch)}`),
312
+ React.createElement(Text, { color: "yellow" }, `http://localhost:${httpPortForBranch(activeService.branch)}/web/database/manager`)),
313
+ React.createElement(Box, { borderStyle: "round", borderColor: filterMode ? 'cyan' : 'gray', paddingX: 2, paddingY: 0, flexDirection: "row", gap: 1 },
314
+ React.createElement(Text, { color: textColor, dimColor: true }, 'filtre ›'),
315
+ filterMode ? (React.createElement(TextInput, { value: filterText, onChange: setFilterText, onSubmit: () => setFilterMode(false), placeholder: "rechercher dans les logs\u2026" })) : (React.createElement(Text, { color: filterText ? 'white' : 'gray', dimColor: !filterText }, filterText || 'appuyer sur / pour filtrer')),
316
+ filterText !== '' && (React.createElement(Text, { color: textColor, dimColor: true },
317
+ "(",
318
+ filteredLogs.length,
319
+ ")"))),
320
+ React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", height: logBoxHeight, overflow: "hidden" }, visibleLogs.length === 0 ? (React.createElement(Text, { color: textColor, dimColor: true }, "En attente des logs\u2026")) : (visibleLogs.map((line, i) => React.createElement(LogLine, { key: start + i, line: line, idx: start + i })))),
321
+ React.createElement(Box, null, filterMode ? (React.createElement(Text, { color: textColor, dimColor: true }, "taper pour filtrer \u00B7 \u21B5 valider \u00B7 \u00C9chap quitter filtre")) : exitCode === null ? (React.createElement(Text, { color: textColor, dimColor: true }, "scroll d\u00E9filer \u00B7 / filtrer \u00B7 Ctrl+C arr\u00EAter")) : (React.createElement(Text, { color: textColor, dimColor: true }, "scroll d\u00E9filer \u00B7 / filtrer \u00B7 \u00C9chap retour")))));
322
+ }
323
+ return (React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
324
+ React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
325
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
326
+ React.createElement(Text, { color: getSecondaryColor(config), bold: true }, "D\u00E9marrer Service Odoo"),
327
+ autoStartError && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
328
+ React.createElement(Text, { color: "red" }, autoStartError),
329
+ React.createElement(Text, { color: textColor, dimColor: true },
330
+ "Services disponibles :",
331
+ ' ',
332
+ services.length === 0
333
+ ? 'aucun'
334
+ : services.map(s => s.name).join(', ')),
335
+ React.createElement(Box, { marginTop: 1 },
336
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour")))),
337
+ !autoStartError && step === 'select' && services.length === 0 && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
338
+ React.createElement(Text, { color: "yellow" }, "Aucun service configur\u00E9."),
339
+ React.createElement(Text, { color: textColor, dimColor: true }, "Cr\u00E9ez d'abord un service via \u00AB Configurer Service Odoo \u00BB."),
340
+ React.createElement(Box, { marginTop: 1 },
341
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour")))),
342
+ !autoStartError && step === 'select' && services.length > 0 && (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
343
+ services.map((svc, i) => {
344
+ const isSel = i === selected;
345
+ return (React.createElement(Text, { key: svc.name, color: isSel ? 'black' : 'white', backgroundColor: isSel ? cursorColor : undefined, bold: isSel },
346
+ ` ${isSel ? '▶' : ' '} ${svc.name} `,
347
+ React.createElement(Text, { color: isSel ? 'black' : 'gray', dimColor: !isSel },
348
+ svc.branch,
349
+ svc.useEnterprise ? ' · Enterprise' : '')));
350
+ }),
351
+ React.createElement(Box, { marginTop: 1 },
352
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 \u21B5 s\u00E9lectionner \u00B7 \u00C9chap retour")))),
353
+ step === 'args_list' && (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
354
+ React.createElement(Text, { color: "white" }, "Arguments suppl\u00E9mentaires :"),
355
+ React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 }, ARGS_ITEMS.map((item, i) => {
356
+ const isSel = i === argsCursor;
357
+ const isAction = item.type === 'action';
358
+ const isSet = argIsSet[item.key] ?? false;
359
+ const display = argDisplay[item.key] ?? '';
360
+ return (React.createElement(Box, { key: item.key, flexDirection: "row", gap: 1 }, isAction ? (React.createElement(Text, { color: isSel ? 'black' : 'green', backgroundColor: isSel ? 'green' : undefined, bold: true }, ` ${isSel ? '▶' : ' '} ${item.label}`)) : (React.createElement(React.Fragment, null,
361
+ React.createElement(Text, { color: isSel ? 'black' : 'white', backgroundColor: isSel ? cursorColor : undefined, bold: isSel }, ` ${isSel ? '▶' : ' '} ${isSet ? '[✓]' : '[ ]'} ${item.label}`),
362
+ display && React.createElement(Text, { color: getPrimaryColor(config) }, display)))));
363
+ })),
364
+ warnNoDb && (React.createElement(Box, { marginTop: 1 },
365
+ React.createElement(Text, { color: "yellow" }, "\u26A0 -u/-i n\u00E9cessite un -d (base de donn\u00E9es non d\u00E9finie)"))),
366
+ React.createElement(Box, { marginTop: 1 },
367
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 \u21B5/Espace basculer \u00B7 \u00C9chap retour")))),
368
+ step === 'input_db' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
369
+ React.createElement(Text, { color: "white" }, "Base de donn\u00E9es (-d) :"),
370
+ React.createElement(Box, null,
371
+ React.createElement(Text, { color: textColor, dimColor: true }, '› '),
372
+ React.createElement(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: v => { setDbName(v.trim()); setStep('args_list'); }, placeholder: "ma_base" })),
373
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u21B5 valider \u00B7 \u00C9chap retour"))),
374
+ step === 'input_module' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
375
+ React.createElement(Text, { color: "white" }, "Module \u00E0 mettre \u00E0 jour (-u) :"),
376
+ React.createElement(Box, null,
377
+ React.createElement(Text, { color: textColor, dimColor: true }, '› '),
378
+ React.createElement(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: v => { setModuleName(v.trim()); setStep('args_list'); }, placeholder: "mon_module" })),
379
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u21B5 valider \u00B7 \u00C9chap retour"))),
380
+ step === 'input_install' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
381
+ React.createElement(Text, { color: "white" }, "Module \u00E0 installer (-i) :"),
382
+ React.createElement(Box, null,
383
+ React.createElement(Text, { color: textColor, dimColor: true }, '› '),
384
+ React.createElement(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: v => { setInstallName(v.trim()); setStep('args_list'); }, placeholder: "mon_module" })),
385
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u21B5 valider \u00B7 \u00C9chap retour"))))));
386
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { NupoConfig } from '../types/index.js';
3
+ interface UpgradeVersionScreenProps {
4
+ config: NupoConfig;
5
+ leftWidth: number;
6
+ onBack: () => void;
7
+ }
8
+ export declare function UpgradeVersionScreen({ config, leftWidth, onBack }: UpgradeVersionScreenProps): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,259 @@
1
+ import React, { useState, useEffect, useCallback, useRef, useReducer } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { stat } from 'fs/promises';
4
+ import { join } from 'path';
5
+ import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
6
+ import { getLocalCommit, getRemoteCommit, updateRepo, ODOO_COMMUNITY_URL, } from '../services/git.js';
7
+ import { LeftPanel } from '../components/LeftPanel.js';
8
+ import { StepsPanel } from '../components/StepsPanel.js';
9
+ import { ErrorPanel } from '../components/ErrorPanel.js';
10
+ import { ProgressBar } from '../components/ProgressBar.js';
11
+ function stepReducer(state, action) {
12
+ if (action.type === 'RESET')
13
+ return action.steps;
14
+ return state.map(s => s.id === action.id ? { ...s, status: action.status, errorMessage: action.errorMessage } : s);
15
+ }
16
+ async function dirExists(p) {
17
+ try {
18
+ return (await stat(p)).isDirectory();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ export function UpgradeVersionScreen({ config, leftWidth, onBack }) {
25
+ const versions = Object.values(config.odoo_versions);
26
+ const textColor = getTextColor(config);
27
+ const cursorColor = getCursorColor(config);
28
+ const [phase, setPhase] = useState('list');
29
+ const [selected, setSelected] = useState(0);
30
+ const [statusMap, setStatusMap] = useState({});
31
+ const [confirmAction, setConfirmAction] = useState(0);
32
+ const [steps, dispatch] = useReducer(stepReducer, []);
33
+ const [currentStepIdx, setCurrentStepIdx] = useState(0);
34
+ const [retryCount, setRetryCount] = useState(0);
35
+ const [errorAction, setErrorAction] = useState(0);
36
+ const [fetchProgress, setFetchProgress] = useState(null);
37
+ const selectedVersionRef = useRef(null);
38
+ const stepsRef = useRef(steps);
39
+ const dispatchRef = useRef(dispatch);
40
+ stepsRef.current = steps;
41
+ dispatchRef.current = dispatch;
42
+ // ── Vérification du statut de chaque version au montage ──────────────────
43
+ useEffect(() => {
44
+ if (versions.length === 0)
45
+ return;
46
+ const initial = {};
47
+ for (const v of versions)
48
+ initial[v.branch] = { checking: true, upToDate: null };
49
+ setStatusMap(initial);
50
+ for (const v of versions) {
51
+ const communityPath = join(v.path, 'community');
52
+ void Promise.all([
53
+ getLocalCommit(communityPath),
54
+ getRemoteCommit(ODOO_COMMUNITY_URL, v.branch),
55
+ ]).then(([local, remote]) => {
56
+ const upToDate = local !== null && remote !== null ? local === remote : null;
57
+ setStatusMap(prev => ({ ...prev, [v.branch]: { checking: false, upToDate } }));
58
+ });
59
+ }
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, []);
62
+ // ── Input : liste ─────────────────────────────────────────────────────────
63
+ useInput((_char, key) => {
64
+ if (key.escape) {
65
+ onBack();
66
+ return;
67
+ }
68
+ if (key.upArrow)
69
+ setSelected(p => (p - 1 + versions.length) % versions.length);
70
+ if (key.downArrow)
71
+ setSelected(p => (p + 1) % versions.length);
72
+ if (key.return && versions[selected]) {
73
+ selectedVersionRef.current = versions[selected];
74
+ setConfirmAction(0);
75
+ setPhase('confirm');
76
+ }
77
+ }, { isActive: phase === 'list' && versions.length > 0 });
78
+ useInput((_char, key) => { if (key.escape)
79
+ onBack(); }, { isActive: phase === 'list' && versions.length === 0 });
80
+ // ── Input : confirmation ──────────────────────────────────────────────────
81
+ useInput((_char, key) => {
82
+ if (key.escape) {
83
+ setPhase('list');
84
+ return;
85
+ }
86
+ if (key.leftArrow)
87
+ setConfirmAction(0);
88
+ if (key.rightArrow)
89
+ setConfirmAction(1);
90
+ if (key.return) {
91
+ if (confirmAction === 1) {
92
+ setPhase('list');
93
+ return;
94
+ }
95
+ void startUpgrade();
96
+ }
97
+ }, { isActive: phase === 'confirm' });
98
+ // ── Input : erreur / retry ────────────────────────────────────────────────
99
+ const errorStep = steps.find(s => s.status === 'error');
100
+ useInput((_char, key) => {
101
+ if (key.escape) {
102
+ setPhase('list');
103
+ return;
104
+ }
105
+ if (key.leftArrow)
106
+ setErrorAction(0);
107
+ if (key.rightArrow)
108
+ setErrorAction(1);
109
+ if (key.return) {
110
+ if (errorAction === 0) {
111
+ const step = stepsRef.current[currentStepIdx];
112
+ if (step) {
113
+ dispatchRef.current({ type: 'SET_STATUS', id: step.id, status: 'pending', errorMessage: undefined });
114
+ setRetryCount(c => c + 1);
115
+ }
116
+ }
117
+ else {
118
+ setPhase('list');
119
+ }
120
+ }
121
+ }, { isActive: phase === 'upgrading' && !!errorStep });
122
+ // ── Input : terminé ───────────────────────────────────────────────────────
123
+ useInput((_char, key) => { if (key.escape)
124
+ onBack(); }, { isActive: phase === 'done' });
125
+ // ── Démarrage de la mise à jour ───────────────────────────────────────────
126
+ const startUpgrade = useCallback(async () => {
127
+ const version = selectedVersionRef.current;
128
+ if (!version)
129
+ return;
130
+ const enterprisePath = join(version.path, 'enterprise');
131
+ const hasEnterprise = await dirExists(enterprisePath);
132
+ const initialSteps = [
133
+ { id: 'update_community', label: 'Mise à jour community', status: 'pending' },
134
+ ...(hasEnterprise
135
+ ? [{ id: 'update_enterprise', label: 'Mise à jour enterprise', status: 'pending' }]
136
+ : []),
137
+ ];
138
+ dispatchRef.current({ type: 'RESET', steps: initialSteps });
139
+ setCurrentStepIdx(0);
140
+ setRetryCount(0);
141
+ setErrorAction(0);
142
+ setPhase('upgrading');
143
+ }, []);
144
+ // ── Exécution des étapes ──────────────────────────────────────────────────
145
+ useEffect(() => {
146
+ if (phase !== 'upgrading')
147
+ return;
148
+ const version = selectedVersionRef.current;
149
+ if (!version)
150
+ return;
151
+ const step = stepsRef.current[currentStepIdx];
152
+ if (!step)
153
+ return;
154
+ void (async () => {
155
+ dispatchRef.current({ type: 'SET_STATUS', id: step.id, status: 'running', errorMessage: undefined });
156
+ setFetchProgress(null);
157
+ const repoPath = step.id === 'update_community'
158
+ ? join(version.path, 'community')
159
+ : join(version.path, 'enterprise');
160
+ const r = await updateRepo(repoPath, version.branch, progress => {
161
+ setFetchProgress(progress);
162
+ });
163
+ setFetchProgress(null);
164
+ if (r.ok) {
165
+ dispatchRef.current({ type: 'SET_STATUS', id: step.id, status: 'success', errorMessage: repoPath });
166
+ const nextIdx = currentStepIdx + 1;
167
+ if (nextIdx < stepsRef.current.length) {
168
+ setCurrentStepIdx(nextIdx);
169
+ }
170
+ else {
171
+ setPhase('done');
172
+ // Re-vérifier le statut de la version mise à jour
173
+ const communityPath = join(version.path, 'community');
174
+ void Promise.all([
175
+ getLocalCommit(communityPath),
176
+ getRemoteCommit(ODOO_COMMUNITY_URL, version.branch),
177
+ ]).then(([local, remote]) => {
178
+ const upToDate = local !== null && remote !== null ? local === remote : null;
179
+ setStatusMap(prev => ({ ...prev, [version.branch]: { checking: false, upToDate } }));
180
+ });
181
+ }
182
+ }
183
+ else {
184
+ dispatchRef.current({ type: 'SET_STATUS', id: step.id, status: 'error', errorMessage: r.error });
185
+ }
186
+ })();
187
+ // eslint-disable-next-line react-hooks/exhaustive-deps
188
+ }, [phase, currentStepIdx, retryCount]);
189
+ // ── Render ────────────────────────────────────────────────────────────────
190
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
191
+ React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
192
+ React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
193
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
194
+ React.createElement(Text, { color: getSecondaryColor(config), bold: true }, "Mise \u00E0 niveau"),
195
+ versions.length === 0 && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
196
+ React.createElement(Text, { color: textColor }, "Aucune version Odoo install\u00E9e."),
197
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour"))),
198
+ phase === 'list' && versions.length > 0 && (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
199
+ versions.map((v, i) => {
200
+ const status = statusMap[v.branch];
201
+ const isSelected = i === selected;
202
+ let icon = '⟳';
203
+ let iconColor = 'gray';
204
+ if (status && !status.checking) {
205
+ if (status.upToDate === true) {
206
+ icon = '✓';
207
+ iconColor = 'green';
208
+ }
209
+ else if (status.upToDate === false) {
210
+ icon = '!';
211
+ iconColor = 'yellow';
212
+ }
213
+ else {
214
+ icon = '?';
215
+ iconColor = 'red';
216
+ }
217
+ }
218
+ return (React.createElement(Box, { key: v.branch, flexDirection: "row", gap: 1, alignItems: "center" },
219
+ React.createElement(Text, { color: isSelected ? 'black' : 'white', backgroundColor: isSelected ? cursorColor : undefined, bold: isSelected },
220
+ ` ${isSelected ? '▶' : ' '} ${v.branch} `,
221
+ React.createElement(Text, { color: isSelected ? 'black' : 'gray', dimColor: !isSelected }, v.path)),
222
+ React.createElement(Text, { color: iconColor }, icon)));
223
+ }),
224
+ React.createElement(Box, { marginTop: 1 },
225
+ React.createElement(Text, { color: textColor, dimColor: true }, '↑↓ naviguer · ↵ sélectionner · Échap retour')))),
226
+ phase === 'confirm' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
227
+ React.createElement(Text, { color: "white" },
228
+ 'Mettre à jour ',
229
+ React.createElement(Text, { color: getPrimaryColor(config), bold: true }, selectedVersionRef.current?.branch),
230
+ ' ?'),
231
+ React.createElement(Box, { flexDirection: "row", gap: 2 },
232
+ React.createElement(Text, { color: confirmAction === 0 ? 'black' : 'white', backgroundColor: confirmAction === 0 ? cursorColor : undefined, bold: confirmAction === 0 }, ' ✓ Oui '),
233
+ React.createElement(Text, { color: confirmAction === 1 ? 'black' : 'white', backgroundColor: confirmAction === 1 ? 'gray' : undefined, bold: confirmAction === 1 }, ' ✗ Non ')),
234
+ React.createElement(Text, { color: textColor, dimColor: true }, '◀▶ choisir · ↵ confirmer · Échap retour'))),
235
+ phase === 'upgrading' && !errorStep && (React.createElement(Box, { flexDirection: "column", marginTop: 1, gap: 0 },
236
+ React.createElement(Text, { color: textColor },
237
+ 'Mise à jour de ',
238
+ React.createElement(Text, { color: getPrimaryColor(config), bold: true }, selectedVersionRef.current?.branch),
239
+ '…'),
240
+ fetchProgress && (React.createElement(Box, { flexDirection: "column", marginTop: 1, gap: 0 },
241
+ React.createElement(ProgressBar, { percent: fetchProgress.percent, textColor: textColor }),
242
+ React.createElement(Text, { color: textColor, dimColor: true },
243
+ fetchProgress.phase === 'receiving' ? 'Receiving objects' : 'Resolving deltas ',
244
+ fetchProgress.speed ? ` ${fetchProgress.speed}` : ''))),
245
+ !fetchProgress && phase === 'upgrading' && (React.createElement(Text, { color: textColor, dimColor: true }, "\u27F3 Connexion au d\u00E9p\u00F4t\u2026")))),
246
+ phase === 'upgrading' && errorStep && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
247
+ React.createElement(Box, { flexDirection: "row", gap: 2 },
248
+ React.createElement(Text, { color: errorAction === 0 ? 'black' : 'white', backgroundColor: errorAction === 0 ? cursorColor : undefined, bold: errorAction === 0 }, ' ↺ Relancer '),
249
+ React.createElement(Text, { color: errorAction === 1 ? 'black' : 'white', backgroundColor: errorAction === 1 ? 'gray' : undefined, bold: errorAction === 1 }, ' ← Retour ')),
250
+ React.createElement(Text, { color: textColor, dimColor: true }, '◀▶ choisir · ↵ confirmer · Échap retour'))),
251
+ phase === 'done' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
252
+ React.createElement(Text, { color: "green" },
253
+ '✓ ',
254
+ React.createElement(Text, { bold: true }, selectedVersionRef.current?.branch),
255
+ ' mis à jour avec succès.'),
256
+ React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour"))))),
257
+ phase === 'upgrading' && React.createElement(StepsPanel, { steps: steps, textColor: textColor }),
258
+ phase === 'upgrading' && React.createElement(ErrorPanel, { steps: steps })));
259
+ }