@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 +2 -1
- package/dist/App.js +68 -3
- package/dist/index.js +15 -2
- package/dist/screens/UpdateScreen.d.ts +11 -0
- package/dist/screens/UpdateScreen.js +62 -0
- package/dist/services/updater.d.ts +4 -0
- package/dist/services/updater.js +58 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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;
|