@y4wee/nupo 0.1.1 → 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/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', () => {
@@ -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
+ }
@@ -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.1",
3
+ "version": "0.2.1",
4
4
  "description": "nupo CLI - Odoo development environment manager",
5
5
  "type": "module",
6
6
  "bin": {