@y4wee/nupo 0.1.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # nupo
2
+
3
+ **nupo** est un gestionnaire d'environnements de développement Odoo en ligne de commande. Il simplifie l'installation, la configuration et le lancement de plusieurs versions d'Odoo en parallèle, directement depuis le terminal.
4
+
5
+ ---
6
+
7
+ ## Fonctionnalités
8
+
9
+ - **Installation de versions Odoo** — clone community + enterprise, création du virtualenv Python, installation des dépendances
10
+ - **Mise à jour des versions** — vérification et mise à jour vers le dernier commit distant
11
+ - **Gestion de services** — créer, modifier, supprimer des configurations de service Odoo (port HTTP, modules custom, conf Odoo)
12
+ - **Lancement de services** — démarrage avec options (-d, -u, -i, --shell, --stop-after-init) et visualiseur de logs en temps réel avec filtrage
13
+ - **Configuration VS Code** — génération automatique de `settings.json` / `launch.json` pour le debug Odoo
14
+ - **Interface themeable** — couleurs entièrement personnalisables (primary, secondary, text, cursor)
15
+ - **Reprise d'installation** — une installation interrompue peut être reprise à l'étape où elle s'est arrêtée
16
+
17
+ ---
18
+
19
+ ## Prérequis
20
+
21
+ - **Node.js** ≥ 18
22
+ - **Python** ≥ 3.8 avec `pip` et `venv`
23
+ - **Git**
24
+ - Un dépôt **Odoo Community** accessible en local ou clonable
25
+
26
+ > Pour Odoo Enterprise : une clé SSH avec accès au dépôt `git@github.com:odoo/enterprise.git` est requise.
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ### Via le script d'installation (Linux & macOS)
33
+
34
+ ```bash
35
+ curl -fsSL https://y4wee.github.io/nupo/install.sh | bash
36
+ ```
37
+
38
+ Le script vérifie automatiquement la présence de Node.js ≥ 18 et l'installe via `nvm` si nécessaire.
39
+
40
+ ### Via npm
41
+
42
+ ```bash
43
+ npm install -g @y4wee/nupo
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Utilisation
49
+
50
+ ### Interface interactive
51
+
52
+ ```bash
53
+ nupo
54
+ ```
55
+
56
+ Lance le menu principal avec navigation au clavier (↑↓ pour naviguer, ↵ pour sélectionner, Échap pour revenir).
57
+
58
+ ### Commandes CLI
59
+
60
+ #### Démarrer un service directement
61
+
62
+ ```bash
63
+ nupo start <nom_du_service> [options]
64
+ ```
65
+
66
+ | Option | Description |
67
+ |---|---|
68
+ | `-d <base>` | Nom de la base de données |
69
+ | `-u <module>` | Module à mettre à jour |
70
+ | `-i <module>` | Module à installer |
71
+ | `--stop-after-init` | Arrêter après l'initialisation |
72
+ | `--shell` | Lancer en mode shell interactif |
73
+
74
+ **Exemples :**
75
+ ```bash
76
+ nupo start mon_service
77
+ nupo start mon_service -d ma_base -u mon_module
78
+ nupo start mon_service --shell
79
+ ```
80
+
81
+ #### Configurer VS Code pour une version
82
+
83
+ ```bash
84
+ nupo code <branche>
85
+ ```
86
+
87
+ **Exemples :**
88
+ ```bash
89
+ nupo code 17.0
90
+ nupo code 18.0
91
+ ```
92
+
93
+ Configure automatiquement `.vscode/settings.json` et `launch.json` pour le debug Odoo, puis ouvre VS Code.
94
+
95
+ ---
96
+
97
+ ## Première utilisation
98
+
99
+ Au premier lancement, nupo guide à travers une étape d'initialisation :
100
+
101
+ 1. Vérification de Python, pip et venv
102
+ 2. Saisie du chemin vers le dépôt Odoo (source des clones)
103
+
104
+ Une fois initialisé, le menu principal donne accès à toutes les fonctionnalités.
105
+
106
+ ---
107
+
108
+ ## Configuration
109
+
110
+ La configuration est stockée dans `~/.nupo/config.json`.
111
+
112
+ Elle est modifiable directement via **nupo → Paramètres** :
113
+
114
+ | Paramètre | Description | Défaut |
115
+ |---|---|---|
116
+ | `odoo_path_repo` | Chemin du dépôt Odoo source | — |
117
+ | `log_buffer_size` | Nombre de lignes de logs conservées | `500` |
118
+ | `primary_color` | Couleur principale de l'interface | `#9F0C58` |
119
+ | `secondary_color` | Couleur des titres d'écran | `#E79439` |
120
+ | `text_color` | Couleur des textes secondaires | `#848484` |
121
+ | `cursor_color` | Couleur de surlignage des sélections | `cyan` |
122
+
123
+ La variable d'environnement `NUPO_CONFIG_DIR` permet de surcharger le répertoire de configuration.
124
+
125
+ ---
126
+
127
+ ## Mise à jour
128
+
129
+ ```bash
130
+ npm update -g @y4wee/nupo
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Publier une nouvelle version (développeurs)
136
+
137
+ ```bash
138
+ npm version patch # ou minor / major
139
+ git push origin master --tags
140
+ npm publish --access=public
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Licence
146
+
147
+ MIT
package/dist/App.d.ts CHANGED
@@ -2,7 +2,8 @@ import React from 'react';
2
2
  import { CliStartArgs } from './types/index.js';
3
3
  interface AppProps {
4
4
  onExit: () => void;
5
+ onUpdate: () => void;
5
6
  startupArgs?: CliStartArgs;
6
7
  }
7
- export declare function App({ onExit, startupArgs }: AppProps): React.JSX.Element;
8
+ export declare function App({ onExit, onUpdate, startupArgs }: AppProps): React.JSX.Element;
8
9
  export {};
package/dist/App.js CHANGED
@@ -10,7 +10,10 @@ import { InitScreen } from './screens/InitScreen.js';
10
10
  import { OdooScreen } from './screens/OdooScreen.js';
11
11
  import { ConfigScreen } from './screens/ConfigScreen.js';
12
12
  import { IdeScreen } from './screens/IdeScreen.js';
13
- export function App({ onExit, startupArgs }) {
13
+ import { UpdateScreen } from './screens/UpdateScreen.js';
14
+ import { checkForUpdate } from './services/updater.js';
15
+ import { patchConfig } from './services/config.js';
16
+ export function App({ onExit, onUpdate, startupArgs }) {
14
17
  const { columns, rows } = useTerminalSize();
15
18
  const { config, loading, refresh } = useConfig();
16
19
  const [currentScreen, setCurrentScreen] = useState('home');
@@ -18,6 +21,10 @@ export function App({ onExit, startupArgs }) {
18
21
  const [confirmSelected, setConfirmSelected] = useState(1);
19
22
  const [serviceRunning, setServiceRunning] = useState(false);
20
23
  const [activeService, setActiveService] = useState(null);
24
+ const [updateBanner, setUpdateBanner] = useState(false);
25
+ const [updateConfirm, setUpdateConfirm] = useState(false);
26
+ const [updateSel, setUpdateSel] = useState(1);
27
+ const [showUpdate, setShowUpdate] = useState(false);
21
28
  // Reposition to top-left whenever the box height changes (service start/stop)
22
29
  useEffect(() => {
23
30
  process.stdout.write('\x1B[2J\x1B[H');
@@ -28,6 +35,21 @@ export function App({ onExit, startupArgs }) {
28
35
  setCurrentScreen('odoo');
29
36
  }
30
37
  }, [loading, config?.initiated, startupArgs]);
38
+ // Update check: show banner immediately if flagged, otherwise check in background
39
+ useEffect(() => {
40
+ if (loading)
41
+ return;
42
+ if (config?.to_update) {
43
+ setUpdateBanner(true);
44
+ return;
45
+ }
46
+ void checkForUpdate().then(hasUpdate => {
47
+ if (!hasUpdate)
48
+ return;
49
+ void patchConfig({ to_update: true });
50
+ setUpdateBanner(true);
51
+ });
52
+ }, [loading]); // eslint-disable-line react-hooks/exhaustive-deps
31
53
  const primaryColor = getPrimaryColor(config);
32
54
  const secondaryColor = getSecondaryColor(config);
33
55
  const textColor = getTextColor(config);
@@ -62,8 +84,33 @@ export function App({ onExit, startupArgs }) {
62
84
  visible: config?.initiated === true,
63
85
  },
64
86
  ].filter(o => o.visible), [config]);
65
- // Global input: handles Ctrl+C and confirm-exit dialog on all screens
87
+ // Global input: handles Ctrl+C, Ctrl+U and confirm dialogs on all screens
66
88
  useInput((char, key) => {
89
+ if (updateConfirm) {
90
+ if (key.leftArrow)
91
+ setUpdateSel(0);
92
+ if (key.rightArrow)
93
+ setUpdateSel(1);
94
+ if (key.return) {
95
+ if (updateSel === 0) {
96
+ setUpdateConfirm(false);
97
+ setShowUpdate(true);
98
+ }
99
+ else {
100
+ setUpdateConfirm(false);
101
+ setUpdateSel(1);
102
+ }
103
+ }
104
+ if (key.escape || char === 'n') {
105
+ setUpdateConfirm(false);
106
+ setUpdateSel(1);
107
+ }
108
+ if (char === 'o' || char === 'y') {
109
+ setUpdateConfirm(false);
110
+ setShowUpdate(true);
111
+ }
112
+ return;
113
+ }
67
114
  if (confirmExit) {
68
115
  if (key.leftArrow)
69
116
  setConfirmSelected(0);
@@ -81,6 +128,11 @@ export function App({ onExit, startupArgs }) {
81
128
  onExit();
82
129
  return;
83
130
  }
131
+ if (key.ctrl && char === 'u' && updateBanner) {
132
+ setUpdateConfirm(true);
133
+ setUpdateSel(1);
134
+ return;
135
+ }
84
136
  if (key.ctrl && char === 'c' && !serviceRunning) {
85
137
  setConfirmExit(true);
86
138
  setConfirmSelected(1);
@@ -98,12 +150,25 @@ export function App({ onExit, startupArgs }) {
98
150
  React.createElement(Box, { paddingX: 3, paddingY: 2 },
99
151
  React.createElement(Text, { color: textColor, dimColor: true }, "Chargement\u2026"))));
100
152
  }
153
+ if (showUpdate) {
154
+ return (React.createElement(UpdateScreen, { termWidth: termWidth, primaryColor: primaryColor, secondaryColor: secondaryColor, textColor: textColor, onComplete: onUpdate, onError: () => setShowUpdate(false) }));
155
+ }
101
156
  return (React.createElement(Box, { borderStyle: "round", borderColor: primaryColor, flexDirection: "column", width: termWidth, height: serviceRunning ? rows : undefined },
102
157
  React.createElement(Header, { activeService: activeService, serviceRunning: serviceRunning, primaryColor: primaryColor, secondaryColor: secondaryColor }),
158
+ updateBanner && (React.createElement(Box, { paddingX: 3, paddingY: 0, borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderColor: secondaryColor },
159
+ React.createElement(Text, { color: secondaryColor }, '⬆ '),
160
+ React.createElement(Text, { color: textColor }, "Mise \u00E0 jour disponible \u2014 "),
161
+ React.createElement(Text, { color: secondaryColor, bold: true }, "Ctrl+U"),
162
+ React.createElement(Text, { color: textColor }, " pour mettre \u00E0 jour"))),
103
163
  currentScreen === 'home' && (React.createElement(HomeScreen, { leftWidth: leftWidth, options: options, isActive: !confirmExit, primaryColor: primaryColor, secondaryColor: secondaryColor, textColor: textColor, cursorColor: cursorColor, onNavigate: screen => setCurrentScreen(screen) })),
104
164
  currentScreen === 'init' && (React.createElement(InitScreen, { config: config, leftWidth: leftWidth, onComplete: handleInitComplete })),
105
165
  currentScreen === 'odoo' && config && (React.createElement(OdooScreen, { leftWidth: leftWidth, config: config, onBack: () => setCurrentScreen('home'), onConfigChange: () => void refresh(), onServiceRunning: svc => { setServiceRunning(true); setActiveService(svc); }, onServiceStopped: () => { setServiceRunning(false); setActiveService(null); }, autoStart: startupArgs })),
106
166
  currentScreen === 'ide' && config && (React.createElement(IdeScreen, { config: config, leftWidth: leftWidth, onBack: () => setCurrentScreen('home') })),
107
167
  currentScreen === 'config' && config && (React.createElement(ConfigScreen, { config: config, leftWidth: leftWidth, onBack: () => setCurrentScreen('home'), onSaved: () => { void refresh(); setCurrentScreen('home'); } })),
108
- React.createElement(ConfirmExit, { visible: confirmExit, selected: confirmSelected, textColor: textColor, cursorColor: cursorColor })));
168
+ React.createElement(ConfirmExit, { visible: confirmExit, selected: confirmSelected, textColor: textColor, cursorColor: cursorColor }),
169
+ updateConfirm && (React.createElement(Box, { paddingX: 3, paddingY: 0, borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: secondaryColor },
170
+ React.createElement(Text, { color: secondaryColor }, "Mettre \u00E0 jour et red\u00E9marrer nupo ? "),
171
+ React.createElement(Text, { color: updateSel === 0 ? 'black' : textColor, backgroundColor: updateSel === 0 ? secondaryColor : undefined, bold: updateSel === 0 }, ' Oui '),
172
+ React.createElement(Text, null, " "),
173
+ React.createElement(Text, { color: updateSel === 1 ? 'black' : textColor, backgroundColor: updateSel === 1 ? cursorColor : undefined, bold: updateSel === 1 }, ' Non ')))));
109
174
  }
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
4
  import { App } from './App.js';
5
- import { configExists, readConfig } from './services/config.js';
5
+ import { configExists, readConfig, patchConfig } from './services/config.js';
6
6
  import { setupVsCode } from './services/ide.js';
7
+ import { spawn } from 'node:child_process';
7
8
  // ── Help ─────────────────────────────────────────────────────────────────────
8
9
  const rawArgs = process.argv.slice(2);
9
10
  if (rawArgs[0] === '--help' || rawArgs[0] === '-h') {
@@ -140,6 +141,18 @@ function cleanup() {
140
141
  // Restore main screen buffer + show cursor
141
142
  process.stdout.write('\x1B[?1049l\x1B[?25h');
142
143
  }
144
+ async function handleUpdate() {
145
+ instance.clear();
146
+ instance.unmount();
147
+ cleanup();
148
+ await patchConfig({ to_update: false });
149
+ // npm install already done by UpdateScreen — just restart
150
+ const child = spawn(process.argv[0], process.argv.slice(1), {
151
+ stdio: 'inherit',
152
+ env: process.env,
153
+ });
154
+ child.on('exit', code => process.exit(code ?? 0));
155
+ }
143
156
  // Guarantee cleanup on every possible exit path
144
157
  process.on('exit', cleanup);
145
158
  process.on('SIGTERM', () => { cleanup(); process.exit(0); });
@@ -161,7 +174,7 @@ function handleExit() {
161
174
  instance.unmount();
162
175
  cleanup();
163
176
  }
164
- instance = render(React.createElement(App, { onExit: handleExit, startupArgs: startupArgs ?? undefined }), {
177
+ instance = render(React.createElement(App, { onExit: handleExit, onUpdate: () => { void handleUpdate(); }, startupArgs: startupArgs ?? undefined }), {
165
178
  exitOnCtrlC: false,
166
179
  });
167
180
  process.stdout.on('resize', () => {
@@ -174,7 +174,10 @@ export function InitScreen({ config, leftWidth, onComplete }) {
174
174
  React.createElement(Text, { color: "red" },
175
175
  "\u00C9tape \u00E9chou\u00E9e : ",
176
176
  errorStep.label),
177
- React.createElement(Text, { color: textColor }, "Corrigez l'erreur et relancez nupo."))),
177
+ errorStep.errorMessage && (React.createElement(Box, { flexDirection: "column", gap: 0 },
178
+ React.createElement(Text, { color: textColor }, "Pour installer :"),
179
+ errorStep.errorMessage.split('\n').map((line, i) => (React.createElement(Text, { key: i, color: "cyan" }, ` ${line}`))))),
180
+ React.createElement(Text, { color: textColor, dimColor: true }, "Relancez nupo une fois corrig\u00E9."))),
178
181
  !waitingInput && !errorStep && !done && (React.createElement(Box, { marginTop: 1 },
179
182
  React.createElement(Text, { color: textColor, dimColor: true }, "\u27F3 V\u00E9rification en cours\u2026"))))),
180
183
  React.createElement(StepsPanel, { steps: steps, textColor: textColor }),
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ interface UpdateScreenProps {
3
+ termWidth: number;
4
+ primaryColor: string;
5
+ secondaryColor: string;
6
+ textColor: string;
7
+ onComplete: () => void;
8
+ onError: (msg: string) => void;
9
+ }
10
+ export declare function UpdateScreen({ termWidth, primaryColor, secondaryColor, textColor, onComplete, onError }: UpdateScreenProps): React.JSX.Element;
11
+ export {};
@@ -0,0 +1,62 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { spawn } from 'node:child_process';
4
+ import { createRequire } from 'module';
5
+ const _require = createRequire(import.meta.url);
6
+ const { name: packageName } = _require('../../package.json');
7
+ const BAR_PADDING = 6; // paddingX * 2 + some margin
8
+ export function UpdateScreen({ termWidth, primaryColor, secondaryColor, textColor, onComplete, onError }) {
9
+ const [progress, setProgress] = useState(0);
10
+ const [done, setDone] = useState(false);
11
+ const doneRef = useRef(false);
12
+ const onCompleteRef = useRef(onComplete);
13
+ const onErrorRef = useRef(onError);
14
+ onCompleteRef.current = onComplete;
15
+ onErrorRef.current = onError;
16
+ const barWidth = Math.max(10, termWidth - BAR_PADDING - 8); // 8 = " XXX% "
17
+ useEffect(() => {
18
+ // Animate progress up to 90% while npm runs
19
+ const timer = setInterval(() => {
20
+ setProgress(prev => {
21
+ if (doneRef.current || prev >= 90)
22
+ return prev;
23
+ // Accelerate early, slow down near 90
24
+ const step = prev < 50 ? 2 : 1;
25
+ return Math.min(prev + step, 90);
26
+ });
27
+ }, 400);
28
+ // Run npm install -g
29
+ const child = spawn('npm', ['install', '-g', packageName], {
30
+ stdio: 'pipe',
31
+ env: process.env,
32
+ });
33
+ child.on('exit', code => {
34
+ clearInterval(timer);
35
+ if (code === 0) {
36
+ doneRef.current = true;
37
+ setProgress(100);
38
+ setDone(true);
39
+ setTimeout(() => onCompleteRef.current(), 800);
40
+ }
41
+ else {
42
+ onErrorRef.current(`npm exited with code ${String(code)}`);
43
+ }
44
+ });
45
+ child.on('error', err => {
46
+ clearInterval(timer);
47
+ onErrorRef.current(err.message);
48
+ });
49
+ return () => clearInterval(timer);
50
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
51
+ const filled = Math.round((progress / 100) * barWidth);
52
+ const empty = barWidth - filled;
53
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: primaryColor, width: termWidth },
54
+ React.createElement(Box, { flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
55
+ React.createElement(Text, { color: secondaryColor, bold: true }, "Mise \u00E0 jour en cours"),
56
+ React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
57
+ React.createElement(Box, null,
58
+ React.createElement(Text, { color: secondaryColor }, '█'.repeat(filled)),
59
+ React.createElement(Text, { color: textColor, dimColor: true }, '░'.repeat(empty)),
60
+ React.createElement(Text, { color: textColor }, ` ${String(progress).padStart(3)}%`)),
61
+ React.createElement(Text, { color: textColor, dimColor: true }, done ? '✓ Installation terminée — redémarrage…' : `Installation de ${packageName}…`)))));
62
+ }
@@ -22,17 +22,35 @@ async function tryCommand(cmd, args) {
22
22
  return { ok: false, error: error.message ?? String(err) };
23
23
  }
24
24
  }
25
+ const isMac = process.platform === 'darwin';
26
+ const HINTS = {
27
+ python: isMac
28
+ ? 'brew install python3 (ou téléchargez depuis https://python.org)'
29
+ : 'sudo apt install python3 # Debian/Ubuntu\nsudo dnf install python3 # Fedora/RHEL',
30
+ pip: isMac
31
+ ? 'brew install python3 (pip3 inclus)\nou : python3 -m ensurepip --upgrade'
32
+ : 'sudo apt install python3-pip # Debian/Ubuntu\nsudo dnf install python3-pip # Fedora/RHEL',
33
+ venv: isMac
34
+ ? 'brew install python3 (venv inclus)'
35
+ : 'sudo apt install python3-venv # Debian/Ubuntu\nsudo dnf install python3-venv # Fedora/RHEL',
36
+ };
25
37
  export async function checkPython() {
26
38
  const result = await tryCommand('python3', ['--version']);
27
39
  if (result.ok)
28
40
  return result;
29
- return tryCommand('python', ['--version']);
41
+ const result2 = await tryCommand('python', ['--version']);
42
+ if (result2.ok)
43
+ return result2;
44
+ return { ok: false, error: HINTS.python };
30
45
  }
31
46
  export async function checkPip() {
32
47
  const result = await tryCommand('pip3', ['--version']);
33
48
  if (result.ok)
34
49
  return result;
35
- return tryCommand('pip', ['--version']);
50
+ const result2 = await tryCommand('pip', ['--version']);
51
+ if (result2.ok)
52
+ return result2;
53
+ return { ok: false, error: HINTS.pip };
36
54
  }
37
55
  export async function checkVenv() {
38
56
  const result = await tryCommand('python3', ['-m', 'venv', '--help']);
@@ -41,8 +59,5 @@ export async function checkVenv() {
41
59
  const result2 = await tryCommand('python', ['-m', 'venv', '--help']);
42
60
  if (result2.ok)
43
61
  return { ok: true };
44
- const hint = process.platform === 'darwin'
45
- ? 'réinstallez Python via python.org ou brew install python3'
46
- : 'sudo apt install python3-venv';
47
- return { ok: false, error: `python venv non disponible (${hint})` };
62
+ return { ok: false, error: HINTS.venv };
48
63
  }
@@ -0,0 +1,4 @@
1
+ /** Vérifie si une version plus récente est disponible sur npm. */
2
+ export declare function checkForUpdate(): Promise<boolean>;
3
+ /** Lance npm install -g puis respawn le process courant. À appeler après cleanup Ink. */
4
+ export declare function performUpdateAndRestart(): void;
@@ -0,0 +1,58 @@
1
+ import { createRequire } from 'module';
2
+ import { get } from 'node:https';
3
+ import { execFileSync, spawn } from 'node:child_process';
4
+ const _require = createRequire(import.meta.url);
5
+ const { version: currentVersion, name: packageName } = _require('../../package.json');
6
+ function fetchLatestVersion() {
7
+ return new Promise(resolve => {
8
+ const req = get(`https://registry.npmjs.org/${packageName}/latest`, { headers: { Accept: 'application/json' } }, res => {
9
+ let data = '';
10
+ res.on('data', (chunk) => { data += chunk; });
11
+ res.on('end', () => {
12
+ try {
13
+ const json = JSON.parse(data);
14
+ resolve(json.version ?? null);
15
+ }
16
+ catch {
17
+ resolve(null);
18
+ }
19
+ });
20
+ });
21
+ req.on('error', () => resolve(null));
22
+ req.setTimeout(3000, () => { req.destroy(); resolve(null); });
23
+ });
24
+ }
25
+ function isNewer(latest, current) {
26
+ const parse = (v) => v.split('.').map(Number);
27
+ const [lMaj, lMin, lPatch] = parse(latest);
28
+ const [cMaj, cMin, cPatch] = parse(current);
29
+ if (lMaj !== cMaj)
30
+ return lMaj > cMaj;
31
+ if (lMin !== cMin)
32
+ return lMin > cMin;
33
+ return lPatch > cPatch;
34
+ }
35
+ /** Vérifie si une version plus récente est disponible sur npm. */
36
+ export async function checkForUpdate() {
37
+ const latest = await fetchLatestVersion();
38
+ if (!latest)
39
+ return false;
40
+ return isNewer(latest, currentVersion);
41
+ }
42
+ /** Lance npm install -g puis respawn le process courant. À appeler après cleanup Ink. */
43
+ export function performUpdateAndRestart() {
44
+ process.stdout.write('\n Mise à jour en cours…\n\n');
45
+ try {
46
+ execFileSync('npm', ['install', '-g', packageName], { stdio: 'inherit' });
47
+ }
48
+ catch {
49
+ process.stdout.write('\n Échec de la mise à jour.\n');
50
+ process.exit(1);
51
+ }
52
+ process.stdout.write('\n Redémarrage…\n\n');
53
+ const child = spawn(process.argv[0], process.argv.slice(1), {
54
+ stdio: 'inherit',
55
+ env: process.env,
56
+ });
57
+ child.on('exit', code => process.exit(code ?? 0));
58
+ }
@@ -29,6 +29,7 @@ export interface NupoConfig {
29
29
  text_color?: string;
30
30
  cursor_color?: string;
31
31
  venv_installed?: boolean;
32
+ to_update?: boolean;
32
33
  }
33
34
  export declare const DEFAULT_CONFIG: NupoConfig;
34
35
  export declare function getPrimaryColor(config?: NupoConfig | null): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y4wee/nupo",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "nupo CLI - Odoo development environment manager",
5
5
  "type": "module",
6
6
  "bin": {