@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.
- package/dist/App.d.ts +8 -0
- package/dist/App.js +109 -0
- package/dist/__tests__/checks.test.d.ts +1 -0
- package/dist/__tests__/checks.test.js +68 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +61 -0
- package/dist/components/ConfirmExit.d.ts +9 -0
- package/dist/components/ConfirmExit.js +12 -0
- package/dist/components/ErrorPanel.d.ts +7 -0
- package/dist/components/ErrorPanel.js +12 -0
- package/dist/components/Header.d.ts +10 -0
- package/dist/components/Header.js +17 -0
- package/dist/components/LeftPanel.d.ts +9 -0
- package/dist/components/LeftPanel.js +31 -0
- package/dist/components/OptionsPanel.d.ts +11 -0
- package/dist/components/OptionsPanel.js +18 -0
- package/dist/components/PathInput.d.ts +10 -0
- package/dist/components/PathInput.js +133 -0
- package/dist/components/ProgressBar.d.ts +5 -0
- package/dist/components/ProgressBar.js +21 -0
- package/dist/components/StepsPanel.d.ts +8 -0
- package/dist/components/StepsPanel.js +24 -0
- package/dist/hooks/useConfig.d.ts +8 -0
- package/dist/hooks/useConfig.js +26 -0
- package/dist/hooks/useTerminalSize.d.ts +4 -0
- package/dist/hooks/useTerminalSize.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -0
- package/dist/screens/ConfigScreen.d.ts +10 -0
- package/dist/screens/ConfigScreen.js +182 -0
- package/dist/screens/ConfigureServiceScreen.d.ts +11 -0
- package/dist/screens/ConfigureServiceScreen.js +499 -0
- package/dist/screens/HomeScreen.d.ts +14 -0
- package/dist/screens/HomeScreen.js +24 -0
- package/dist/screens/IdeScreen.d.ts +9 -0
- package/dist/screens/IdeScreen.js +101 -0
- package/dist/screens/InitScreen.d.ts +9 -0
- package/dist/screens/InitScreen.js +182 -0
- package/dist/screens/InstallVersionScreen.d.ts +10 -0
- package/dist/screens/InstallVersionScreen.js +495 -0
- package/dist/screens/OdooScreen.d.ts +13 -0
- package/dist/screens/OdooScreen.js +76 -0
- package/dist/screens/OdooServiceScreen.d.ts +10 -0
- package/dist/screens/OdooServiceScreen.js +51 -0
- package/dist/screens/StartServiceScreen.d.ts +12 -0
- package/dist/screens/StartServiceScreen.js +386 -0
- package/dist/screens/UpgradeVersionScreen.d.ts +9 -0
- package/dist/screens/UpgradeVersionScreen.js +259 -0
- package/dist/services/checks.d.ts +8 -0
- package/dist/services/checks.js +48 -0
- package/dist/services/config.d.ts +11 -0
- package/dist/services/config.js +146 -0
- package/dist/services/git.d.ts +35 -0
- package/dist/services/git.js +173 -0
- package/dist/services/ide.d.ts +10 -0
- package/dist/services/ide.js +126 -0
- package/dist/services/python.d.ts +14 -0
- package/dist/services/python.js +81 -0
- package/dist/services/system.d.ts +2 -0
- package/dist/services/system.js +22 -0
- package/dist/types/index.d.ts +82 -0
- package/dist/types/index.js +26 -0
- package/package.json +37 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { readdir, stat, mkdir, writeFile, unlink } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
|
|
7
|
+
import { readConfig, writeConfig, readBaseConf } from '../services/config.js';
|
|
8
|
+
import { openInEditor } from '../services/system.js';
|
|
9
|
+
import { LeftPanel } from '../components/LeftPanel.js';
|
|
10
|
+
const EDIT_PARAMS = [
|
|
11
|
+
{ key: 'name', label: 'Nom' },
|
|
12
|
+
{ key: 'version', label: 'Version' },
|
|
13
|
+
{ key: 'enterprise', label: 'Enterprise' },
|
|
14
|
+
{ key: 'custom_folders', label: 'Dossiers custom' },
|
|
15
|
+
{ key: 'open_conf', label: 'Modifier odoo.conf' },
|
|
16
|
+
{ key: 'delete', label: 'Supprimer' },
|
|
17
|
+
];
|
|
18
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
19
|
+
async function dirExists(p) {
|
|
20
|
+
try {
|
|
21
|
+
return (await stat(p)).isDirectory();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function loadCustomFolders(versionPath) {
|
|
28
|
+
try {
|
|
29
|
+
const entries = await readdir(join(versionPath, 'custom'), { withFileTypes: true });
|
|
30
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function httpPortForBranch(branch) {
|
|
37
|
+
const m = branch.match(/(\d+)\./);
|
|
38
|
+
return m ? 8000 + parseInt(m[1], 10) : 8069;
|
|
39
|
+
}
|
|
40
|
+
function injectHttpPort(conf, branch) {
|
|
41
|
+
const line = `http_port = ${httpPortForBranch(branch)}`;
|
|
42
|
+
const lines = conf.split('\n');
|
|
43
|
+
const idx = lines.findIndex(l => l.trimStart().startsWith('http_port'));
|
|
44
|
+
if (idx >= 0)
|
|
45
|
+
lines[idx] = line;
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
async function generateConfContent(branch) {
|
|
49
|
+
const baseConf = await readBaseConf();
|
|
50
|
+
return injectHttpPort(baseConf, branch);
|
|
51
|
+
}
|
|
52
|
+
// ── Component ────────────────────────────────────────────────────────────────
|
|
53
|
+
export function ConfigureServiceScreen({ config, leftWidth, initialService, onComplete, onBack, }) {
|
|
54
|
+
const isEditing = !!initialService;
|
|
55
|
+
const versions = Object.values(config.odoo_versions);
|
|
56
|
+
const textColor = getTextColor(config);
|
|
57
|
+
const cursorColor = getCursorColor(config);
|
|
58
|
+
// ── Shared state ─────────────────────────────────────────────────────────
|
|
59
|
+
const [step, setStep] = useState(isEditing ? 'edit_list' : 'name');
|
|
60
|
+
const [editParamCursor, setEditParamCursor] = useState(0);
|
|
61
|
+
const [editSaving, setEditSaving] = useState(false);
|
|
62
|
+
const [nameInput, setNameInput] = useState(initialService?.name ?? '');
|
|
63
|
+
const [nameError, setNameError] = useState(null);
|
|
64
|
+
const [confirmedName, setConfirmedName] = useState(initialService?.name ?? '');
|
|
65
|
+
// Track the name currently persisted in config so we can rename correctly
|
|
66
|
+
const lastSavedName = useRef(initialService?.name ?? '');
|
|
67
|
+
const [selectedVersionIdx, setSelectedVersionIdx] = useState(() => {
|
|
68
|
+
if (initialService) {
|
|
69
|
+
const idx = versions.findIndex(v => v.branch === initialService.branch);
|
|
70
|
+
return idx >= 0 ? idx : 0;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
});
|
|
74
|
+
const [transitioning, setTransitioning] = useState(false);
|
|
75
|
+
const [hasEnterprise, setHasEnterprise] = useState(false);
|
|
76
|
+
const [enterpriseAction, setEnterpriseAction] = useState(initialService ? (initialService.useEnterprise ? 0 : 1) : 0);
|
|
77
|
+
const [customFoldersList, setCustomFoldersList] = useState([]);
|
|
78
|
+
const [selectedFolders, setSelectedFolders] = useState(new Set(initialService?.customFolders ?? []));
|
|
79
|
+
const [folderCursor, setFolderCursor] = useState(0);
|
|
80
|
+
const [saveError, setSaveError] = useState(null);
|
|
81
|
+
// Pre-load enterprise / folders on mount in edit mode
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!initialService)
|
|
84
|
+
return;
|
|
85
|
+
const version = versions.find(v => v.branch === initialService.branch);
|
|
86
|
+
if (!version)
|
|
87
|
+
return;
|
|
88
|
+
void dirExists(join(version.path, 'enterprise')).then(setHasEnterprise);
|
|
89
|
+
void loadCustomFolders(version.path).then(setCustomFoldersList);
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, []);
|
|
92
|
+
// ── Core save ─────────────────────────────────────────────────────────────
|
|
93
|
+
const buildAndSave = useCallback(async (opts) => {
|
|
94
|
+
const confPath = join(opts.version.path, 'config', `${opts.name}.conf`);
|
|
95
|
+
const service = {
|
|
96
|
+
name: opts.name,
|
|
97
|
+
branch: opts.version.branch,
|
|
98
|
+
versionPath: opts.version.path,
|
|
99
|
+
useEnterprise: opts.useEnterprise,
|
|
100
|
+
customFolders: opts.folders,
|
|
101
|
+
confPath,
|
|
102
|
+
};
|
|
103
|
+
await mkdir(join(opts.version.path, 'config'), { recursive: true });
|
|
104
|
+
await writeFile(confPath, await generateConfContent(opts.version.branch), 'utf-8');
|
|
105
|
+
const current = await readConfig();
|
|
106
|
+
const services = { ...(current.odoo_services ?? {}) };
|
|
107
|
+
// Rename: delete old entry if name changed
|
|
108
|
+
if (lastSavedName.current && lastSavedName.current !== opts.name) {
|
|
109
|
+
delete services[lastSavedName.current];
|
|
110
|
+
}
|
|
111
|
+
services[opts.name] = service;
|
|
112
|
+
await writeConfig({ ...current, odoo_services: services });
|
|
113
|
+
lastSavedName.current = opts.name;
|
|
114
|
+
}, []);
|
|
115
|
+
// ── Create mode: version → enterprise → custom_folders → save ────────────
|
|
116
|
+
const handleVersionSelectCreate = useCallback(async () => {
|
|
117
|
+
if (transitioning)
|
|
118
|
+
return;
|
|
119
|
+
const version = versions[selectedVersionIdx];
|
|
120
|
+
if (!version)
|
|
121
|
+
return;
|
|
122
|
+
setTransitioning(true);
|
|
123
|
+
const enterprise = await dirExists(join(version.path, 'enterprise'));
|
|
124
|
+
const folders = await loadCustomFolders(version.path);
|
|
125
|
+
setHasEnterprise(enterprise);
|
|
126
|
+
setCustomFoldersList(folders);
|
|
127
|
+
setSelectedFolders(new Set()); // reset only in create mode
|
|
128
|
+
setFolderCursor(0);
|
|
129
|
+
setTransitioning(false);
|
|
130
|
+
setStep(enterprise ? 'enterprise' : 'custom_folders');
|
|
131
|
+
}, [transitioning, selectedVersionIdx, versions]);
|
|
132
|
+
const saveCreate = useCallback(async () => {
|
|
133
|
+
const version = versions[selectedVersionIdx];
|
|
134
|
+
if (!version)
|
|
135
|
+
return;
|
|
136
|
+
setStep('saving');
|
|
137
|
+
setSaveError(null);
|
|
138
|
+
try {
|
|
139
|
+
await buildAndSave({
|
|
140
|
+
name: confirmedName,
|
|
141
|
+
version,
|
|
142
|
+
useEnterprise: hasEnterprise && enterpriseAction === 0,
|
|
143
|
+
folders: [...selectedFolders].sort(),
|
|
144
|
+
});
|
|
145
|
+
setStep('done');
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
setSaveError(err.message);
|
|
149
|
+
setStep('name');
|
|
150
|
+
}
|
|
151
|
+
}, [versions, selectedVersionIdx, confirmedName, hasEnterprise, enterpriseAction, selectedFolders, buildAndSave]);
|
|
152
|
+
// ── Edit mode: save a single changed param and return to edit_list ────────
|
|
153
|
+
const saveEditParam = useCallback(async (updates) => {
|
|
154
|
+
const vIdx = updates.versionIdx ?? selectedVersionIdx;
|
|
155
|
+
const version = versions[vIdx];
|
|
156
|
+
if (!version)
|
|
157
|
+
return;
|
|
158
|
+
const newName = updates.name ?? confirmedName;
|
|
159
|
+
const newUseEnterprise = updates.useEnterprise ?? (hasEnterprise && enterpriseAction === 0);
|
|
160
|
+
const newFolders = updates.folders ?? [...selectedFolders].sort();
|
|
161
|
+
setEditSaving(true);
|
|
162
|
+
setSaveError(null);
|
|
163
|
+
try {
|
|
164
|
+
await buildAndSave({ name: newName, version, useEnterprise: newUseEnterprise, folders: newFolders });
|
|
165
|
+
// Commit new values to state
|
|
166
|
+
if (updates.name !== undefined) {
|
|
167
|
+
setConfirmedName(updates.name);
|
|
168
|
+
setNameInput(updates.name);
|
|
169
|
+
}
|
|
170
|
+
if (updates.versionIdx !== undefined)
|
|
171
|
+
setSelectedVersionIdx(updates.versionIdx);
|
|
172
|
+
if (updates.useEnterprise !== undefined)
|
|
173
|
+
setEnterpriseAction(updates.useEnterprise ? 0 : 1);
|
|
174
|
+
if (updates.folders !== undefined)
|
|
175
|
+
setSelectedFolders(new Set(updates.folders));
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
setSaveError(err.message);
|
|
179
|
+
}
|
|
180
|
+
setEditSaving(false);
|
|
181
|
+
setStep('edit_list');
|
|
182
|
+
}, [
|
|
183
|
+
selectedVersionIdx, versions, confirmedName, hasEnterprise,
|
|
184
|
+
enterpriseAction, selectedFolders, buildAndSave,
|
|
185
|
+
]);
|
|
186
|
+
// Edit mode: version change → auto-adapt enterprise & folders, then save
|
|
187
|
+
const handleVersionSelectEdit = useCallback(async () => {
|
|
188
|
+
if (transitioning)
|
|
189
|
+
return;
|
|
190
|
+
const version = versions[selectedVersionIdx];
|
|
191
|
+
if (!version)
|
|
192
|
+
return;
|
|
193
|
+
setTransitioning(true);
|
|
194
|
+
const enterprise = await dirExists(join(version.path, 'enterprise'));
|
|
195
|
+
const folders = await loadCustomFolders(version.path);
|
|
196
|
+
const validFolders = [...selectedFolders].filter(f => folders.includes(f));
|
|
197
|
+
setHasEnterprise(enterprise);
|
|
198
|
+
setCustomFoldersList(folders);
|
|
199
|
+
if (!enterprise)
|
|
200
|
+
setEnterpriseAction(1);
|
|
201
|
+
setTransitioning(false);
|
|
202
|
+
await saveEditParam({
|
|
203
|
+
versionIdx: selectedVersionIdx,
|
|
204
|
+
useEnterprise: enterprise && enterpriseAction === 0,
|
|
205
|
+
folders: validFolders.sort(),
|
|
206
|
+
});
|
|
207
|
+
}, [transitioning, selectedVersionIdx, versions, selectedFolders, enterpriseAction, saveEditParam]);
|
|
208
|
+
// Edit mode: open enterprise param (need to check availability first)
|
|
209
|
+
const openEnterpriseEdit = useCallback(async () => {
|
|
210
|
+
const version = versions[selectedVersionIdx];
|
|
211
|
+
if (!version)
|
|
212
|
+
return;
|
|
213
|
+
setTransitioning(true);
|
|
214
|
+
const enterprise = await dirExists(join(version.path, 'enterprise'));
|
|
215
|
+
setHasEnterprise(enterprise);
|
|
216
|
+
setTransitioning(false);
|
|
217
|
+
setStep('enterprise');
|
|
218
|
+
}, [selectedVersionIdx, versions]);
|
|
219
|
+
// Edit mode: open custom_folders param (reload list, keep selection)
|
|
220
|
+
const openCustomFoldersEdit = useCallback(async () => {
|
|
221
|
+
const version = versions[selectedVersionIdx];
|
|
222
|
+
if (!version)
|
|
223
|
+
return;
|
|
224
|
+
setTransitioning(true);
|
|
225
|
+
const folders = await loadCustomFolders(version.path);
|
|
226
|
+
setCustomFoldersList(folders);
|
|
227
|
+
setFolderCursor(0);
|
|
228
|
+
setTransitioning(false);
|
|
229
|
+
setStep('custom_folders');
|
|
230
|
+
}, [selectedVersionIdx, versions]);
|
|
231
|
+
// ── Esc helper: depends on mode ───────────────────────────────────────────
|
|
232
|
+
const escapeFrom = useCallback((s) => {
|
|
233
|
+
if (isEditing) {
|
|
234
|
+
setStep('edit_list');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
switch (s) {
|
|
238
|
+
case 'name':
|
|
239
|
+
onBack();
|
|
240
|
+
break;
|
|
241
|
+
case 'version':
|
|
242
|
+
setStep('name');
|
|
243
|
+
break;
|
|
244
|
+
case 'enterprise':
|
|
245
|
+
setStep('version');
|
|
246
|
+
break;
|
|
247
|
+
case 'custom_folders':
|
|
248
|
+
setStep(hasEnterprise ? 'enterprise' : 'version');
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}, [isEditing, hasEnterprise, onBack]);
|
|
252
|
+
// ── Input hooks ───────────────────────────────────────────────────────────
|
|
253
|
+
// edit_list
|
|
254
|
+
useInput((_char, key) => {
|
|
255
|
+
if (key.escape) {
|
|
256
|
+
onBack();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (key.upArrow)
|
|
260
|
+
setEditParamCursor(p => (p - 1 + EDIT_PARAMS.length) % EDIT_PARAMS.length);
|
|
261
|
+
if (key.downArrow)
|
|
262
|
+
setEditParamCursor(p => (p + 1) % EDIT_PARAMS.length);
|
|
263
|
+
if (key.return && !editSaving && !transitioning) {
|
|
264
|
+
const param = EDIT_PARAMS[editParamCursor].key;
|
|
265
|
+
switch (param) {
|
|
266
|
+
case 'name':
|
|
267
|
+
setStep('name');
|
|
268
|
+
break;
|
|
269
|
+
case 'version':
|
|
270
|
+
setStep('version');
|
|
271
|
+
break;
|
|
272
|
+
case 'enterprise':
|
|
273
|
+
void openEnterpriseEdit();
|
|
274
|
+
break;
|
|
275
|
+
case 'custom_folders':
|
|
276
|
+
void openCustomFoldersEdit();
|
|
277
|
+
break;
|
|
278
|
+
case 'open_conf':
|
|
279
|
+
openInEditor(initialService.confPath);
|
|
280
|
+
break;
|
|
281
|
+
case 'delete':
|
|
282
|
+
setStep('confirm_delete');
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}, { isActive: step === 'edit_list' });
|
|
287
|
+
// name: Esc
|
|
288
|
+
useInput((_char, key) => { if (key.escape)
|
|
289
|
+
escapeFrom('name'); }, { isActive: step === 'name' });
|
|
290
|
+
// version
|
|
291
|
+
useInput((_char, key) => {
|
|
292
|
+
if (key.escape) {
|
|
293
|
+
escapeFrom('version');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (key.upArrow)
|
|
297
|
+
setSelectedVersionIdx(p => (p - 1 + versions.length) % versions.length);
|
|
298
|
+
if (key.downArrow)
|
|
299
|
+
setSelectedVersionIdx(p => (p + 1) % versions.length);
|
|
300
|
+
if (key.return) {
|
|
301
|
+
if (isEditing)
|
|
302
|
+
void handleVersionSelectEdit();
|
|
303
|
+
else
|
|
304
|
+
void handleVersionSelectCreate();
|
|
305
|
+
}
|
|
306
|
+
}, { isActive: step === 'version' && !transitioning });
|
|
307
|
+
// enterprise
|
|
308
|
+
useInput((_char, key) => {
|
|
309
|
+
if (key.escape) {
|
|
310
|
+
escapeFrom('enterprise');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (key.leftArrow)
|
|
314
|
+
setEnterpriseAction(0);
|
|
315
|
+
if (key.rightArrow)
|
|
316
|
+
setEnterpriseAction(1);
|
|
317
|
+
if (key.return) {
|
|
318
|
+
if (isEditing)
|
|
319
|
+
void saveEditParam({ useEnterprise: enterpriseAction === 0 });
|
|
320
|
+
else
|
|
321
|
+
setStep('custom_folders');
|
|
322
|
+
}
|
|
323
|
+
}, { isActive: step === 'enterprise' });
|
|
324
|
+
// custom_folders
|
|
325
|
+
useInput((char, key) => {
|
|
326
|
+
if (key.escape) {
|
|
327
|
+
escapeFrom('custom_folders');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (key.upArrow)
|
|
331
|
+
setFolderCursor(p => Math.max(0, p - 1));
|
|
332
|
+
if (key.downArrow)
|
|
333
|
+
setFolderCursor(p => Math.min(customFoldersList.length - 1, p + 1));
|
|
334
|
+
if (char === ' ' && customFoldersList[folderCursor]) {
|
|
335
|
+
const folder = customFoldersList[folderCursor];
|
|
336
|
+
setSelectedFolders(prev => {
|
|
337
|
+
const next = new Set(prev);
|
|
338
|
+
if (next.has(folder))
|
|
339
|
+
next.delete(folder);
|
|
340
|
+
else
|
|
341
|
+
next.add(folder);
|
|
342
|
+
return next;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (key.return) {
|
|
346
|
+
if (isEditing)
|
|
347
|
+
void saveEditParam({ folders: [...selectedFolders].sort() });
|
|
348
|
+
else
|
|
349
|
+
void saveCreate();
|
|
350
|
+
}
|
|
351
|
+
}, { isActive: step === 'custom_folders' });
|
|
352
|
+
// done
|
|
353
|
+
useInput((_char, key) => { if (key.escape)
|
|
354
|
+
onComplete(); }, { isActive: step === 'done' });
|
|
355
|
+
// confirm_delete
|
|
356
|
+
useInput((_char, key) => {
|
|
357
|
+
if (key.escape) {
|
|
358
|
+
setStep('edit_list');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (key.return)
|
|
362
|
+
void handleDelete();
|
|
363
|
+
}, { isActive: step === 'confirm_delete' });
|
|
364
|
+
// ── Delete ────────────────────────────────────────────────────────────────
|
|
365
|
+
const handleDelete = useCallback(async () => {
|
|
366
|
+
try {
|
|
367
|
+
await unlink(initialService.confPath);
|
|
368
|
+
}
|
|
369
|
+
catch { /* already gone */ }
|
|
370
|
+
const current = await readConfig();
|
|
371
|
+
const services = { ...(current.odoo_services ?? {}) };
|
|
372
|
+
delete services[initialService.name];
|
|
373
|
+
await writeConfig({ ...current, odoo_services: services });
|
|
374
|
+
onComplete();
|
|
375
|
+
}, [initialService, onComplete]);
|
|
376
|
+
// ── Name submit (shared between create & edit) ────────────────────────────
|
|
377
|
+
const handleNameSubmit = (value) => {
|
|
378
|
+
const name = value.trim();
|
|
379
|
+
if (!name) {
|
|
380
|
+
setNameError('Le nom ne peut pas être vide.');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const existing = config.odoo_services ?? {};
|
|
384
|
+
if (existing[name] && name !== lastSavedName.current) {
|
|
385
|
+
setNameError(`Un service "${name}" existe déjà.`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
setNameError(null);
|
|
389
|
+
if (isEditing) {
|
|
390
|
+
void saveEditParam({ name });
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
if (versions.length === 0) {
|
|
394
|
+
setNameError('Aucune version Odoo installée.');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
setConfirmedName(name);
|
|
398
|
+
setStep('version');
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
// ── Render helpers ────────────────────────────────────────────────────────
|
|
402
|
+
const selectedVersion = versions[selectedVersionIdx];
|
|
403
|
+
const editParamValues = {
|
|
404
|
+
name: confirmedName || '—',
|
|
405
|
+
version: selectedVersion?.branch ?? '—',
|
|
406
|
+
enterprise: hasEnterprise ? (enterpriseAction === 0 ? 'Oui' : 'Non') : 'Non disponible',
|
|
407
|
+
custom_folders: selectedFolders.size > 0 ? [...selectedFolders].join(', ') : 'Aucun',
|
|
408
|
+
open_conf: '↵ ouvrir dans $EDITOR',
|
|
409
|
+
delete: '',
|
|
410
|
+
};
|
|
411
|
+
// ── JSX ───────────────────────────────────────────────────────────────────
|
|
412
|
+
return (React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
|
|
413
|
+
React.createElement(LeftPanel, { width: leftWidth, primaryColor: getPrimaryColor(config), textColor: textColor }),
|
|
414
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
|
|
415
|
+
React.createElement(Text, { color: getSecondaryColor(config), bold: true }, isEditing ? `Modifier : ${initialService.name}` : 'Nouveau service'),
|
|
416
|
+
step === 'edit_list' && (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
|
|
417
|
+
EDIT_PARAMS.map((p, i) => {
|
|
418
|
+
const isSel = i === editParamCursor;
|
|
419
|
+
const isDel = p.key === 'delete';
|
|
420
|
+
return (React.createElement(Box, { key: p.key, flexDirection: "row", gap: 1 },
|
|
421
|
+
React.createElement(Text, { color: isSel ? 'black' : (isDel ? 'red' : 'white'), backgroundColor: isSel ? (isDel ? 'red' : cursorColor) : undefined, bold: isSel || isDel }, ` ${isSel ? '▶' : ' '} ${p.label.padEnd(16)}`),
|
|
422
|
+
React.createElement(Text, { color: isSel ? 'cyan' : 'gray', dimColor: !isSel }, editParamValues[p.key])));
|
|
423
|
+
}),
|
|
424
|
+
saveError && React.createElement(Box, { marginTop: 1 },
|
|
425
|
+
React.createElement(Text, { color: "red" }, saveError)),
|
|
426
|
+
(editSaving || transitioning) && React.createElement(Text, { color: textColor, dimColor: true }, "\u27F3 Sauvegarde\u2026"),
|
|
427
|
+
React.createElement(Box, { marginTop: 1 },
|
|
428
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 \u21B5 modifier \u00B7 \u00C9chap retour")))),
|
|
429
|
+
step === 'name' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
430
|
+
React.createElement(Text, { color: "white" }, "Nom du service :"),
|
|
431
|
+
React.createElement(Box, null,
|
|
432
|
+
React.createElement(Text, { color: textColor, dimColor: true }, '› '),
|
|
433
|
+
React.createElement(TextInput, { value: nameInput, onChange: v => { setNameInput(v); setNameError(null); }, onSubmit: handleNameSubmit, placeholder: "odoo-17-prod" })),
|
|
434
|
+
nameError && React.createElement(Text, { color: "red" }, nameError),
|
|
435
|
+
saveError && React.createElement(Text, { color: "red" },
|
|
436
|
+
"Erreur : ",
|
|
437
|
+
saveError),
|
|
438
|
+
React.createElement(Text, { color: textColor, dimColor: true },
|
|
439
|
+
"\u21B5 valider \u00B7 \u00C9chap ",
|
|
440
|
+
isEditing ? 'retour' : 'annuler'))),
|
|
441
|
+
step === 'version' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
442
|
+
!isEditing && (React.createElement(Text, { color: "white" },
|
|
443
|
+
'Service : ',
|
|
444
|
+
React.createElement(Text, { color: getPrimaryColor(config), bold: true }, confirmedName))),
|
|
445
|
+
React.createElement(Text, { color: "white" }, "Version Odoo :"),
|
|
446
|
+
React.createElement(Box, { flexDirection: "column", gap: 0 }, versions.map((v, i) => {
|
|
447
|
+
const isSel = i === selectedVersionIdx;
|
|
448
|
+
return (React.createElement(Text, { key: v.branch, color: isSel ? 'black' : 'white', backgroundColor: isSel ? cursorColor : undefined, bold: isSel },
|
|
449
|
+
` ${isSel ? '▶' : ' '} ${v.branch} `,
|
|
450
|
+
React.createElement(Text, { color: isSel ? 'black' : 'gray', dimColor: !isSel }, v.path)));
|
|
451
|
+
})),
|
|
452
|
+
transitioning && React.createElement(Text, { color: textColor, dimColor: true }, "\u27F3 V\u00E9rification\u2026"),
|
|
453
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 \u21B5 s\u00E9lectionner \u00B7 \u00C9chap retour"))),
|
|
454
|
+
step === 'enterprise' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
455
|
+
React.createElement(Text, { color: "white" },
|
|
456
|
+
'Version : ',
|
|
457
|
+
React.createElement(Text, { color: getPrimaryColor(config) }, selectedVersion?.branch)),
|
|
458
|
+
!hasEnterprise ? (React.createElement(React.Fragment, null,
|
|
459
|
+
React.createElement(Text, { color: "yellow" }, "Enterprise non disponible pour cette version."),
|
|
460
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour"))) : (React.createElement(React.Fragment, null,
|
|
461
|
+
React.createElement(Text, { color: "white" }, "Utiliser Enterprise ?"),
|
|
462
|
+
React.createElement(Box, { flexDirection: "row", gap: 2 },
|
|
463
|
+
React.createElement(Text, { color: enterpriseAction === 0 ? 'black' : 'white', backgroundColor: enterpriseAction === 0 ? cursorColor : undefined, bold: enterpriseAction === 0 }, ' ✓ Oui '),
|
|
464
|
+
React.createElement(Text, { color: enterpriseAction === 1 ? 'black' : 'white', backgroundColor: enterpriseAction === 1 ? 'gray' : undefined, bold: enterpriseAction === 1 }, ' ✗ Non ')),
|
|
465
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u25C0\u25B6 choisir \u00B7 \u21B5 confirmer \u00B7 \u00C9chap retour"))))),
|
|
466
|
+
step === 'custom_folders' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
467
|
+
React.createElement(Text, { color: "white" }, "Dossiers custom \u00E0 inclure :"),
|
|
468
|
+
customFoldersList.length === 0 ? (React.createElement(Box, { flexDirection: "column", gap: 0 },
|
|
469
|
+
React.createElement(Text, { color: textColor, dimColor: true }, " Aucun module dans custom/"),
|
|
470
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u21B5 continuer \u00B7 \u00C9chap retour"))) : (React.createElement(Box, { flexDirection: "column", gap: 0 },
|
|
471
|
+
customFoldersList.map((folder, i) => {
|
|
472
|
+
const isCursor = i === folderCursor;
|
|
473
|
+
const isChecked = selectedFolders.has(folder);
|
|
474
|
+
return (React.createElement(Text, { key: folder, color: isCursor ? 'black' : 'white', backgroundColor: isCursor ? cursorColor : undefined, bold: isCursor }, ` ${isChecked ? '[✓]' : '[ ]'} ${folder}`));
|
|
475
|
+
}),
|
|
476
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column", gap: 0 },
|
|
477
|
+
selectedFolders.size > 0 && (React.createElement(Text, { color: "yellow", dimColor: true }, ` ${selectedFolders.size} sélectionné(s) : ${[...selectedFolders].join(', ')}`)),
|
|
478
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 Espace s\u00E9lectionner \u00B7 \u21B5 confirmer \u00B7 \u00C9chap retour")))))),
|
|
479
|
+
step === 'confirm_delete' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
480
|
+
React.createElement(Text, { color: "red", bold: true },
|
|
481
|
+
"Supprimer le service \u00AB",
|
|
482
|
+
initialService.name,
|
|
483
|
+
"\u00BB ?"),
|
|
484
|
+
React.createElement(Text, { color: textColor, dimColor: true }, " Le fichier .conf sera \u00E9galement supprim\u00E9 :"),
|
|
485
|
+
React.createElement(Text, { color: textColor, dimColor: true },
|
|
486
|
+
" ",
|
|
487
|
+
initialService.confPath),
|
|
488
|
+
React.createElement(Box, { marginTop: 1 },
|
|
489
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u21B5 confirmer \u00B7 \u00C9chap annuler")))),
|
|
490
|
+
step === 'saving' && (React.createElement(Box, { marginTop: 1 },
|
|
491
|
+
React.createElement(Text, { color: textColor }, "\u27F3 Enregistrement du service\u2026"))),
|
|
492
|
+
step === 'done' && (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
493
|
+
React.createElement(Text, { color: "green" },
|
|
494
|
+
'✓ Service ',
|
|
495
|
+
React.createElement(Text, { bold: true }, confirmedName),
|
|
496
|
+
' créé.'),
|
|
497
|
+
React.createElement(Text, { color: textColor, dimColor: true }, ` conf → ${join(selectedVersion?.path ?? '', 'config', `${confirmedName}.conf`)}`),
|
|
498
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour"))))));
|
|
499
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MenuOption, Screen } from '../types/index.js';
|
|
3
|
+
interface HomeScreenProps {
|
|
4
|
+
leftWidth: number;
|
|
5
|
+
options: MenuOption[];
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
primaryColor?: string;
|
|
8
|
+
secondaryColor?: string;
|
|
9
|
+
textColor?: string;
|
|
10
|
+
cursorColor?: string;
|
|
11
|
+
onNavigate: (screen: Screen) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function HomeScreen({ leftWidth, options, isActive, primaryColor, secondaryColor, textColor, cursorColor, onNavigate }: HomeScreenProps): React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, useInput } from 'ink';
|
|
3
|
+
import { LeftPanel } from '../components/LeftPanel.js';
|
|
4
|
+
import { OptionsPanel } from '../components/OptionsPanel.js';
|
|
5
|
+
export function HomeScreen({ leftWidth, options, isActive, primaryColor, secondaryColor, textColor, cursorColor, onNavigate }) {
|
|
6
|
+
const [selected, setSelected] = useState(0);
|
|
7
|
+
useInput((_char, key) => {
|
|
8
|
+
if (key.upArrow) {
|
|
9
|
+
setSelected(prev => (prev - 1 + options.length) % Math.max(options.length, 1));
|
|
10
|
+
}
|
|
11
|
+
if (key.downArrow) {
|
|
12
|
+
setSelected(prev => (prev + 1) % Math.max(options.length, 1));
|
|
13
|
+
}
|
|
14
|
+
if (key.return && options.length > 0) {
|
|
15
|
+
const opt = options[selected];
|
|
16
|
+
if (opt)
|
|
17
|
+
onNavigate(opt.screen);
|
|
18
|
+
}
|
|
19
|
+
}, { isActive });
|
|
20
|
+
const safeSelected = Math.min(selected, Math.max(0, options.length - 1));
|
|
21
|
+
return (React.createElement(Box, { flexDirection: "row" },
|
|
22
|
+
React.createElement(LeftPanel, { width: leftWidth, primaryColor: primaryColor }),
|
|
23
|
+
React.createElement(OptionsPanel, { options: options, selected: safeSelected, secondaryColor: secondaryColor, textColor: textColor, cursorColor: cursorColor })));
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { NupoConfig } from '../types/index.js';
|
|
3
|
+
interface IdeScreenProps {
|
|
4
|
+
config: NupoConfig;
|
|
5
|
+
leftWidth: number;
|
|
6
|
+
onBack: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function IdeScreen({ config, leftWidth, onBack }: IdeScreenProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { getPrimaryColor, getSecondaryColor, getTextColor, getCursorColor } from '../types/index.js';
|
|
4
|
+
import { LeftPanel } from '../components/LeftPanel.js';
|
|
5
|
+
import { setupVsCode } from '../services/ide.js';
|
|
6
|
+
const INITIAL_STEPS = [
|
|
7
|
+
{ id: 'vscode_dir', label: 'Dossier .vscode', status: 'pending' },
|
|
8
|
+
{ id: 'settings_json', label: 'settings.json', status: 'pending' },
|
|
9
|
+
{ id: 'launch_json', label: 'launch.json', status: 'pending' },
|
|
10
|
+
{ id: 'open_vscode', label: 'Ouvrir VS Code', status: 'pending' },
|
|
11
|
+
];
|
|
12
|
+
const STATUS_ICONS = {
|
|
13
|
+
pending: '○',
|
|
14
|
+
running: '◌',
|
|
15
|
+
success: '✓',
|
|
16
|
+
error: '✗',
|
|
17
|
+
};
|
|
18
|
+
const STATUS_COLORS = {
|
|
19
|
+
pending: 'gray',
|
|
20
|
+
running: 'cyan',
|
|
21
|
+
success: 'green',
|
|
22
|
+
error: 'red',
|
|
23
|
+
};
|
|
24
|
+
export function IdeScreen({ config, leftWidth, onBack }) {
|
|
25
|
+
const versions = Object.values(config.odoo_versions ?? {});
|
|
26
|
+
const services = Object.values(config.odoo_services ?? {});
|
|
27
|
+
const [view, setView] = useState('select');
|
|
28
|
+
const [selected, setSelected] = useState(0);
|
|
29
|
+
const [steps, setSteps] = useState(INITIAL_STEPS);
|
|
30
|
+
const [done, setDone] = useState(false);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const primaryColor = getPrimaryColor(config);
|
|
33
|
+
const secondaryColor = getSecondaryColor(config);
|
|
34
|
+
const textColor = getTextColor(config);
|
|
35
|
+
const cursorColor = getCursorColor(config);
|
|
36
|
+
const patchStep = (id, patch) => setSteps(prev => prev.map(s => s.id === id ? { ...s, ...patch } : s));
|
|
37
|
+
async function runSetup(version) {
|
|
38
|
+
setView('setup');
|
|
39
|
+
setSteps(INITIAL_STEPS.map(s => ({ ...s })));
|
|
40
|
+
setDone(false);
|
|
41
|
+
setError(null);
|
|
42
|
+
const ok = await setupVsCode(version, services, (id, status, detail) => {
|
|
43
|
+
patchStep(id, { status: status, detail });
|
|
44
|
+
if (status === 'error')
|
|
45
|
+
setError(detail ?? 'Erreur inconnue');
|
|
46
|
+
});
|
|
47
|
+
if (ok)
|
|
48
|
+
setDone(true);
|
|
49
|
+
}
|
|
50
|
+
// ── Input ──────────────────────────────────────────────────────────────────
|
|
51
|
+
useInput((_char, key) => {
|
|
52
|
+
if (key.escape) {
|
|
53
|
+
onBack();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.upArrow)
|
|
57
|
+
setSelected(p => Math.max(0, p - 1));
|
|
58
|
+
if (key.downArrow)
|
|
59
|
+
setSelected(p => Math.min(versions.length - 1, p + 1));
|
|
60
|
+
if (key.return && versions[selected])
|
|
61
|
+
void runSetup(versions[selected]);
|
|
62
|
+
}, { isActive: view === 'select' });
|
|
63
|
+
useInput((_char, key) => {
|
|
64
|
+
if (key.escape || key.return)
|
|
65
|
+
onBack();
|
|
66
|
+
}, { isActive: view === 'setup' && (done || error !== null) });
|
|
67
|
+
// ── Render: select ─────────────────────────────────────────────────────────
|
|
68
|
+
if (view === 'select') {
|
|
69
|
+
return (React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
|
|
70
|
+
React.createElement(LeftPanel, { width: leftWidth, primaryColor: primaryColor, textColor: textColor }),
|
|
71
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
|
|
72
|
+
React.createElement(Text, { color: secondaryColor, bold: true }, "IDE"),
|
|
73
|
+
versions.length === 0 ? (React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 },
|
|
74
|
+
React.createElement(Text, { color: "yellow" }, "Aucune version Odoo install\u00E9e."),
|
|
75
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "Installez une version via \u00AB Odoo \u2192 Installer une version \u00BB."),
|
|
76
|
+
React.createElement(Box, { marginTop: 1 },
|
|
77
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u00C9chap retour")))) : (React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 },
|
|
78
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "S\u00E9lectionnez une version \u00E0 ouvrir dans VS Code :"),
|
|
79
|
+
React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 }, versions.map((v, i) => {
|
|
80
|
+
const isSel = i === selected;
|
|
81
|
+
return (React.createElement(Text, { key: v.branch, color: isSel ? 'black' : 'white', backgroundColor: isSel ? cursorColor : undefined, bold: isSel },
|
|
82
|
+
` ${isSel ? '▶' : ' '} ${v.branch} `,
|
|
83
|
+
React.createElement(Text, { color: isSel ? 'black' : 'gray', dimColor: !isSel }, v.path)));
|
|
84
|
+
})),
|
|
85
|
+
React.createElement(Box, { marginTop: 1 },
|
|
86
|
+
React.createElement(Text, { color: textColor, dimColor: true }, "\u2191\u2193 naviguer \u00B7 \u21B5 ouvrir \u00B7 \u00C9chap retour")))))));
|
|
87
|
+
}
|
|
88
|
+
// ── Render: setup ──────────────────────────────────────────────────────────
|
|
89
|
+
return (React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
|
|
90
|
+
React.createElement(LeftPanel, { width: leftWidth, primaryColor: primaryColor, textColor: textColor }),
|
|
91
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", paddingX: 3, paddingY: 2, gap: 1 },
|
|
92
|
+
React.createElement(Text, { color: secondaryColor, bold: true }, "IDE \u2014 Configuration VS Code"),
|
|
93
|
+
React.createElement(Box, { flexDirection: "column", gap: 0, marginTop: 1 }, steps.map(s => (React.createElement(Box, { key: s.id, flexDirection: "row", gap: 1 },
|
|
94
|
+
React.createElement(Text, { color: STATUS_COLORS[s.status] }, STATUS_ICONS[s.status]),
|
|
95
|
+
React.createElement(Text, { color: s.status === 'error' ? 'red' : 'white' }, s.label),
|
|
96
|
+
s.detail && React.createElement(Text, { color: textColor, dimColor: true }, s.detail))))),
|
|
97
|
+
error && (React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1 },
|
|
98
|
+
React.createElement(Text, { color: "red" }, error))),
|
|
99
|
+
(done || error) && (React.createElement(Box, { marginTop: 1 },
|
|
100
|
+
React.createElement(Text, { color: textColor, dimColor: true }, done ? '↵/Échap retour' : 'Échap retour'))))));
|
|
101
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { NupoConfig } from '../types/index.js';
|
|
3
|
+
interface InitScreenProps {
|
|
4
|
+
config: NupoConfig | null;
|
|
5
|
+
leftWidth: number;
|
|
6
|
+
onComplete: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function InitScreen({ config, leftWidth, onComplete }: InitScreenProps): React.JSX.Element;
|
|
9
|
+
export {};
|