@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,4 @@
1
+ export declare function useTerminalSize(): {
2
+ columns: number;
3
+ rows: number;
4
+ };
@@ -0,0 +1,18 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useStdout } from 'ink';
3
+ export function useTerminalSize() {
4
+ const { stdout } = useStdout();
5
+ const [columns, setColumns] = useState(stdout?.columns ?? 80);
6
+ const [rows, setRows] = useState(stdout?.rows ?? 24);
7
+ useEffect(() => {
8
+ if (!stdout)
9
+ return;
10
+ const handler = () => {
11
+ setColumns(stdout.columns);
12
+ setRows(stdout.rows);
13
+ };
14
+ stdout.on('resize', handler);
15
+ return () => { stdout.off('resize', handler); };
16
+ }, [stdout]);
17
+ return { columns, rows };
18
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import { App } from './App.js';
5
+ import { configExists, readConfig } from './services/config.js';
6
+ import { setupVsCode } from './services/ide.js';
7
+ // ── Help ─────────────────────────────────────────────────────────────────────
8
+ const rawArgs = process.argv.slice(2);
9
+ if (rawArgs[0] === '--help' || rawArgs[0] === '-h') {
10
+ process.stdout.write(`
11
+ nupO — gestionnaire d'environnements Odoo
12
+
13
+ USAGE
14
+ nupo Lance l'interface interactive
15
+ nupo start <service> [opts] Lance directement un service Odoo
16
+ nupo code <branche> Configure .vscode et ouvre VS Code
17
+
18
+ COMMANDES
19
+ start <service> Démarre le service nommé <service>
20
+ code <branche> Configure et ouvre la version Odoo dans VS Code
21
+
22
+ OPTIONS DE START
23
+ -d <base> Base de données (--database)
24
+ -u <module> Module à mettre à jour (--update)
25
+ -i <module> Module à installer (--init)
26
+ --stop-after-init Arrête Odoo après l'initialisation
27
+ --shell Lance en mode shell interactif
28
+
29
+ EXEMPLES
30
+ nupo
31
+ nupo start mon_service
32
+ nupo start mon_service -d ma_base -u mon_module
33
+ nupo start mon_service -d ma_base -i mon_module --stop-after-init
34
+ nupo start mon_service --shell
35
+ nupo code 18.0
36
+ nupo code 17.0
37
+
38
+ `);
39
+ process.exit(0);
40
+ }
41
+ // ── CLI argument parsing ──────────────────────────────────────────────────────
42
+ function parseCliArgs() {
43
+ const args = rawArgs;
44
+ if (args[0] !== 'start' || !args[1])
45
+ return null;
46
+ const result = { serviceName: args[1], stopAfterInit: false, shell: false };
47
+ for (let i = 2; i < args.length; i++) {
48
+ switch (args[i]) {
49
+ case '-d':
50
+ if (args[i + 1])
51
+ result.db = args[++i];
52
+ break;
53
+ case '-u':
54
+ if (args[i + 1])
55
+ result.module = args[++i];
56
+ break;
57
+ case '-i':
58
+ if (args[i + 1])
59
+ result.install = args[++i];
60
+ break;
61
+ case '--stop-after-init':
62
+ result.stopAfterInit = true;
63
+ break;
64
+ case '--shell':
65
+ result.shell = true;
66
+ break;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ const startupArgs = parseCliArgs();
72
+ // ── nupo code <branch> ────────────────────────────────────────────────────────
73
+ if (rawArgs[0] === 'code') {
74
+ const branch = rawArgs[1];
75
+ if (!branch) {
76
+ process.stderr.write(`nupo: usage : nupo code <branche>\n`);
77
+ process.exit(1);
78
+ }
79
+ const exists = await configExists();
80
+ if (!exists) {
81
+ process.stderr.write(`nupo: aucune configuration trouvée. Lancez "nupo" pour initialiser.\n`);
82
+ process.exit(1);
83
+ }
84
+ const cfg = await readConfig();
85
+ if (!cfg.initiated) {
86
+ process.stderr.write(`nupo: nupo n'est pas initialisé. Lancez "nupo" pour configurer.\n`);
87
+ process.exit(1);
88
+ }
89
+ const version = (cfg.odoo_versions ?? {})[branch];
90
+ if (!version) {
91
+ const names = Object.keys(cfg.odoo_versions ?? {});
92
+ const list = names.length > 0 ? names.join(', ') : 'aucune';
93
+ process.stderr.write(`nupo: version introuvable : "${branch}"\nVersions disponibles : ${list}\n`);
94
+ process.exit(1);
95
+ }
96
+ const LABELS = {
97
+ vscode_dir: 'Dossier .vscode',
98
+ settings_json: 'settings.json ',
99
+ launch_json: 'launch.json ',
100
+ open_vscode: 'Ouvrir VS Code ',
101
+ };
102
+ const services = Object.values(cfg.odoo_services ?? {});
103
+ const ok = await setupVsCode(version, services, (id, status, detail) => {
104
+ const icon = status === 'running' ? '…' : status === 'success' ? '✓' : '✗';
105
+ const label = LABELS[id] ?? id;
106
+ const info = detail ? ` ${detail}` : '';
107
+ if (status !== 'running')
108
+ process.stdout.write(`${icon} ${label}${info}\n`);
109
+ });
110
+ process.exit(ok ? 0 : 1);
111
+ }
112
+ // ── Pre-flight validation (before alternate screen) ───────────────────────────
113
+ if (startupArgs) {
114
+ const exists = await configExists();
115
+ if (!exists) {
116
+ process.stderr.write(`nupo: aucune configuration trouvée. Lancez "nupo" pour initialiser.\n`);
117
+ process.exit(1);
118
+ }
119
+ const cfg = await readConfig();
120
+ if (!cfg.initiated) {
121
+ process.stderr.write(`nupo: nupo n'est pas initialisé. Lancez "nupo" pour configurer.\n`);
122
+ process.exit(1);
123
+ }
124
+ const services = cfg.odoo_services ?? {};
125
+ if (!services[startupArgs.serviceName]) {
126
+ const names = Object.keys(services);
127
+ const list = names.length > 0 ? names.join(', ') : 'aucun';
128
+ process.stderr.write(`nupo: service introuvable : "${startupArgs.serviceName}"\nServices disponibles : ${list}\n`);
129
+ process.exit(1);
130
+ }
131
+ }
132
+ // ── Alternate screen buffer ───────────────────────────────────────────────────
133
+ // Enter alternate screen + hide cursor before rendering anything
134
+ process.stdout.write('\x1B[?1049h\x1B[?25l');
135
+ let cleanedUp = false;
136
+ function cleanup() {
137
+ if (cleanedUp)
138
+ return;
139
+ cleanedUp = true;
140
+ // Restore main screen buffer + show cursor
141
+ process.stdout.write('\x1B[?1049l\x1B[?25h');
142
+ }
143
+ // Guarantee cleanup on every possible exit path
144
+ process.on('exit', cleanup);
145
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
146
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
147
+ process.on('uncaughtException', err => {
148
+ cleanup();
149
+ process.stderr.write(`\nnupo: erreur non gérée : ${err.message}\n${err.stack ?? ''}\n`);
150
+ process.exit(1);
151
+ });
152
+ process.on('unhandledRejection', reason => {
153
+ cleanup();
154
+ process.stderr.write(`\nnupo: promesse rejetée : ${String(reason)}\n`);
155
+ process.exit(1);
156
+ });
157
+ // ── Render ────────────────────────────────────────────────────────────────────
158
+ let instance;
159
+ function handleExit() {
160
+ instance.clear();
161
+ instance.unmount();
162
+ cleanup();
163
+ }
164
+ instance = render(React.createElement(App, { onExit: handleExit, startupArgs: startupArgs ?? undefined }), {
165
+ exitOnCtrlC: false,
166
+ });
167
+ process.stdout.on('resize', () => {
168
+ process.stdout.write('\x1B[2J\x1B[H');
169
+ });
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { NupoConfig } from '../types/index.js';
3
+ interface ConfigScreenProps {
4
+ config: NupoConfig;
5
+ leftWidth: number;
6
+ onBack: () => void;
7
+ onSaved: () => void;
8
+ }
9
+ export declare function ConfigScreen({ config, leftWidth, onBack, onSaved }: ConfigScreenProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,182 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { PathInput } from '../components/PathInput.js';
4
+ import { access } from 'fs/promises';
5
+ import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
6
+ import { patchConfig, ensureBaseConf, getBaseConfPath } from '../services/config.js';
7
+ import { openInEditor } from '../services/system.js';
8
+ import { LeftPanel } from '../components/LeftPanel.js';
9
+ const ITEMS = [
10
+ {
11
+ type: 'config',
12
+ key: 'odoo_path_repo',
13
+ label: 'Chemin du dépôt Odoo',
14
+ description: 'Chemin absolu vers le dépôt Odoo sur ce système.',
15
+ validate: async (value) => {
16
+ const v = value.trim();
17
+ if (!v)
18
+ return 'Le chemin ne peut pas être vide.';
19
+ try {
20
+ await access(v);
21
+ return null;
22
+ }
23
+ catch {
24
+ return `Chemin introuvable : ${v}`;
25
+ }
26
+ },
27
+ },
28
+ {
29
+ type: 'config',
30
+ key: 'log_buffer_size',
31
+ label: 'Buffer de logs',
32
+ description: 'Nombre de lignes de logs conservées en mémoire lors de l\'exécution d\'un service Odoo. Valeur recommandée : 500–5000.',
33
+ validate: async (value) => {
34
+ const n = parseInt(value.trim(), 10);
35
+ if (isNaN(n) || n < 100)
36
+ return 'Doit être un entier ≥ 100.';
37
+ return null;
38
+ },
39
+ transform: (value) => parseInt(value.trim(), 10),
40
+ },
41
+ {
42
+ type: 'config',
43
+ key: 'primary_color',
44
+ label: 'Couleur principale',
45
+ description: 'Couleur principale de l\'interface nupo (logo, accents). Format hexadécimal : #RRGGBB.',
46
+ validate: async (value) => {
47
+ if (!/^#[0-9a-fA-F]{6}$/.test(value.trim()))
48
+ return 'Format invalide. Exemple : #9F0C58';
49
+ return null;
50
+ },
51
+ },
52
+ {
53
+ type: 'config',
54
+ key: 'secondary_color',
55
+ label: 'Couleur secondaire',
56
+ description: 'Couleur secondaire de l\'interface nupo (titres des écrans). Format hexadécimal : #RRGGBB.',
57
+ validate: async (value) => {
58
+ if (!/^#[0-9a-fA-F]{6}$/.test(value.trim()))
59
+ return 'Format invalide. Exemple : #E79439';
60
+ return null;
61
+ },
62
+ },
63
+ {
64
+ type: 'config',
65
+ key: 'cursor_color',
66
+ label: 'Couleur du curseur',
67
+ description: 'Couleur de surlignage des éléments sélectionnés dans les listes. Format hexadécimal ou nom CSS : #RRGGBB ou cyan.',
68
+ validate: async (value) => {
69
+ if (!value.trim())
70
+ return 'La valeur ne peut pas être vide.';
71
+ return null;
72
+ },
73
+ },
74
+ {
75
+ type: 'config',
76
+ key: 'text_color',
77
+ label: 'Couleur des textes',
78
+ description: 'Couleur des textes secondaires de l\'interface nupo (hints, valeurs, labels). Format hexadécimal : #RRGGBB.',
79
+ validate: async (value) => {
80
+ if (!/^#[0-9a-fA-F]{6}$/.test(value.trim()))
81
+ return 'Format invalide. Exemple : #848484';
82
+ return null;
83
+ },
84
+ },
85
+ {
86
+ type: 'action',
87
+ id: 'open_base_conf',
88
+ label: 'Conf Odoo de base',
89
+ description: `Template de configuration utilisé lors de la création des services Odoo.\nFichier : ${getBaseConfPath()}`,
90
+ },
91
+ ];
92
+ export function ConfigScreen({ config, leftWidth, onBack, onSaved }) {
93
+ const textColor = getTextColor(config);
94
+ const cursorColor = getCursorColor(config);
95
+ const [selected, setSelected] = useState(0);
96
+ const [edit, setEdit] = useState({ active: false });
97
+ useInput((_char, key) => {
98
+ if (edit.active) {
99
+ if (key.escape)
100
+ setEdit({ active: false });
101
+ return;
102
+ }
103
+ if (key.upArrow)
104
+ setSelected(prev => (prev - 1 + ITEMS.length) % ITEMS.length);
105
+ if (key.downArrow)
106
+ setSelected(prev => (prev + 1) % ITEMS.length);
107
+ if (key.return) {
108
+ const item = ITEMS[selected];
109
+ if (item.type === 'config') {
110
+ setEdit({
111
+ active: true,
112
+ itemIndex: selected,
113
+ value: String(config[item.key] ?? ''),
114
+ error: null,
115
+ saving: false,
116
+ });
117
+ }
118
+ else if (item.type === 'action' && item.id === 'open_base_conf') {
119
+ void ensureBaseConf().then(() => {
120
+ openInEditor(getBaseConfPath());
121
+ });
122
+ }
123
+ }
124
+ if (key.escape)
125
+ onBack();
126
+ }, { isActive: !(edit.active && edit.saving) });
127
+ const handleSubmit = async (inputValue) => {
128
+ if (!edit.active)
129
+ return;
130
+ const item = ITEMS[edit.itemIndex];
131
+ if (item.type !== 'config')
132
+ return;
133
+ const trimmed = inputValue.trim();
134
+ setEdit(prev => (prev.active ? { ...prev, saving: true, error: null } : prev));
135
+ const error = item.validate ? await item.validate(trimmed) : null;
136
+ if (error) {
137
+ setEdit(prev => (prev.active ? { ...prev, saving: false, error } : prev));
138
+ return;
139
+ }
140
+ const value = item.transform ? item.transform(trimmed) : trimmed;
141
+ await patchConfig({ [item.key]: value });
142
+ setEdit({ active: false });
143
+ onSaved();
144
+ };
145
+ const currentItem = ITEMS[selected];
146
+ return (React.createElement(Box, { flexDirection: "row" },
147
+ React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
148
+ React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
149
+ React.createElement(Text, { color: getSecondaryColor(config), bold: true }, "Param\u00E8tres"),
150
+ !edit.active && (React.createElement(Box, { flexDirection: "column", marginTop: 1, gap: 0 }, ITEMS.map((item, i) => {
151
+ const isSel = i === selected;
152
+ const value = item.type === 'config'
153
+ ? (String(config[item.key] ?? '') || '(non défini)')
154
+ : '↵ ouvrir dans $EDITOR';
155
+ return (React.createElement(Box, { key: item.type === 'config' ? item.key : item.id, flexDirection: "row", gap: 1 },
156
+ React.createElement(Text, { color: isSel ? 'black' : 'white', backgroundColor: isSel ? cursorColor : undefined, bold: isSel }, ` ${isSel ? '▶' : ' '} ${item.label}`),
157
+ React.createElement(Text, { color: textColor, dimColor: true }, value),
158
+ item.type === 'config' && item.key.endsWith('_color') && config[item.key] && (React.createElement(Text, { color: String(config[item.key]) }, "\u25CF"))));
159
+ }))),
160
+ edit.active && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
161
+ React.createElement(Text, { color: "white" },
162
+ ITEMS[edit.itemIndex].label,
163
+ " :"),
164
+ React.createElement(Box, null,
165
+ React.createElement(Text, { color: textColor, dimColor: true }, '› '),
166
+ React.createElement(PathInput, { value: edit.value, onChange: value => setEdit(prev => (prev.active ? { ...prev, value, error: null } : prev)), onSubmit: val => void handleSubmit(val), focus: !edit.saving, textColor: textColor })),
167
+ (() => {
168
+ const editItem = ITEMS[edit.itemIndex];
169
+ const val = edit.value.trim();
170
+ const isColorKey = editItem?.key?.endsWith('_color');
171
+ const isHex = /^#[0-9a-fA-F]{6}$/.test(val);
172
+ const isCursorKey = editItem?.key === 'cursor_color';
173
+ return isColorKey && (isHex || (isCursorKey && val.length > 0)) ? (React.createElement(Text, null,
174
+ React.createElement(Text, { color: val }, "\u25CF "),
175
+ React.createElement(Text, { color: textColor, dimColor: true }, val))) : null;
176
+ })(),
177
+ edit.error && React.createElement(Text, { color: "red" }, edit.error),
178
+ edit.saving && React.createElement(Text, { color: "yellow", dimColor: true }, "Sauvegarde\u2026"))),
179
+ React.createElement(Box, { marginTop: 1 }, edit.active ? (React.createElement(Text, { color: textColor, dimColor: true }, "\u21B5 sauvegarder \u00B7 \u00C9chap annuler")) : (React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 \u21B5 modifier \u00B7 \u00C9chap retour"))),
180
+ !edit.active && (React.createElement(Box, { borderStyle: "round", borderColor: textColor, paddingX: 1, paddingY: 0 },
181
+ React.createElement(Text, { color: textColor, wrap: "wrap" }, currentItem.description))))));
182
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { NupoConfig, OdooServiceConfig } from '../types/index.js';
3
+ interface ConfigureServiceScreenProps {
4
+ config: NupoConfig;
5
+ leftWidth: number;
6
+ initialService?: OdooServiceConfig;
7
+ onComplete: () => void;
8
+ onBack: () => void;
9
+ }
10
+ export declare function ConfigureServiceScreen({ config, leftWidth, initialService, onComplete, onBack, }: ConfigureServiceScreenProps): React.JSX.Element;
11
+ export {};