@wpmoo/toolkit 0.9.5 → 0.9.7

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/cli.js CHANGED
@@ -2,20 +2,25 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { rm, rename } from 'node:fs/promises';
4
4
  import { basename, relative, resolve } from 'node:path';
5
+ import { emitKeypressEvents } from 'node:readline';
5
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
7
  import { commandFromArgs, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
8
+ import { cockpitCommands } from './cockpit/command-registry.js';
7
9
  import { collectDailyActionArgs } from './cockpit/daily-prompts.js';
10
+ import { selectModuleAction } from './cockpit/module-action-menu.js';
11
+ import { selectModuleFromBrowser } from './cockpit/module-browser.js';
8
12
  import { selectCockpitTopLevelMenu } from './cockpit/menu.js';
9
13
  import { confirmCockpitCommandRisk } from './cockpit/safety.js';
10
14
  import { detectDevelopmentEnvironment } from './environment.js';
11
15
  import { commandOdooVersion } from './environment-version.js';
12
16
  import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
13
- import { isDailyActionCommand, runDailyAction } from './daily-actions.js';
17
+ import { listEnvironmentDatabases, normalizeDatabaseListResult } from './databases.js';
18
+ import { isDailyActionCommand, runDailyAction, runDailyActionWithStyledOutput } from './daily-actions.js';
14
19
  import { getDoctorReport, runDoctor } from './doctor.js';
15
20
  import { getOriginUrl, realGit } from './git.js';
16
21
  import { renderHelp } from './help.js';
17
22
  import { runLocalCockpit } from './local-cockpit.js';
18
- import { addModuleToSourceRepo, listModulesInSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
23
+ import { addModuleToSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
19
24
  import { supportedOdooVersions } from './odoo-versions.js';
20
25
  import { renderRepositorySetupNote } from './prompt-copy.js';
21
26
  import { promptRepositoryUrl } from './prompt-repositories.js';
@@ -28,6 +33,7 @@ import { backupTargetPath, expectedTargetConfirmation, inspectEnvironmentTarget,
28
33
  import { getGitHubPrerequisiteStatus, renderGitHubPrerequisiteGuidance, } from './github-prerequisites.js';
29
34
  import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, } from './repository-preflight.js';
30
35
  import { scaffold } from './scaffold.js';
36
+ import { getSystemPrerequisiteStatus, renderSystemPrerequisiteGuidance, } from './system-prerequisites.js';
31
37
  import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, selectPrompt, textPrompt } from './prompts/index.js';
32
38
  import { renderBanner } from './templates.js';
33
39
  import { checkForUpdate, isUpdateCheckSkipped, restartCli } from './update-check.js';
@@ -211,6 +217,29 @@ function clearCockpitScreen() {
211
217
  process.stdout.write('\u001B[2J\u001B[H');
212
218
  }
213
219
  }
220
+ function clearPrerequisiteScreen() {
221
+ if (process.stdout.isTTY) {
222
+ process.stdout.write('\u001B[3J\u001B[2J\u001B[H');
223
+ }
224
+ }
225
+ const ANSI_ACTION = '\u001B[38;2;226;184;96m';
226
+ const ANSI_SUCCESS = '\u001B[32m';
227
+ const ANSI_DEFAULT_FOREGROUND = '\u001B[39m';
228
+ const ANSI_DIM_INFO = '\u001B[2m\u001B[38;2;120;157;181m';
229
+ const ANSI_RESET = '\u001B[0m';
230
+ function renderActionText(value) {
231
+ return ansi(value, ANSI_ACTION, ANSI_DEFAULT_FOREGROUND);
232
+ }
233
+ function renderCompletedText(action) {
234
+ if (!supportsAnsi()) {
235
+ return `✓ ${action} completed.`;
236
+ }
237
+ return `${ANSI_SUCCESS}✓${ANSI_DEFAULT_FOREGROUND} ${action} ${ANSI_SUCCESS}completed${ANSI_DEFAULT_FOREGROUND}.`;
238
+ }
239
+ function renderBackHelp() {
240
+ return ansi('Esc to go back', ANSI_DIM_INFO, ANSI_RESET);
241
+ }
242
+ const manualDatabaseValue = '__wpmoo_manual_database_entry__';
214
243
  async function showStartup(argv, skipUpdateCheck, details) {
215
244
  if (skipUpdateCheck) {
216
245
  console.log(renderStartupBanner(details));
@@ -603,9 +632,6 @@ async function selectSourceRepo(target, cancelAction = 'exit') {
603
632
  }
604
633
  return { repoPath: String(selected), sourceType: 'private' };
605
634
  }
606
- function formatSourceRepoPromptPath(target, selected) {
607
- return renderedSourceRepoPath(target, selected.sourceType, selected.repoPath);
608
- }
609
635
  function suggestedModuleName(repoPath) {
610
636
  return 'odoo_sample_module';
611
637
  }
@@ -801,25 +827,20 @@ function removeModuleOptionsFromArgs(argv) {
801
827
  };
802
828
  }
803
829
  async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
804
- showSubmenuIntro('Remove module from source repo', showIntro, cancelAction);
830
+ if (showIntro) {
831
+ introPrompt('Remove module');
832
+ }
805
833
  const target = process.cwd();
806
- const sourceRepo = await selectSourceRepo(target, cancelAction);
807
- const modules = await listModulesInSourceRepo(target, sourceRepo.repoPath, sourceRepo.sourceType);
808
- if (modules.length === 0) {
834
+ const selectedModule = await selectModuleFromBrowser(target, { cancelAction });
835
+ if (!selectedModule) {
809
836
  if (cancelAction === 'back') {
810
- notePrompt(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
837
+ notePrompt('No Odoo modules found.\nNext: choose "Add module" or "Add source repo" first.', 'Nothing to remove');
811
838
  handleUnavailableMenuChoice(cancelAction);
812
839
  }
813
- throw new Error(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}`);
840
+ throw new Error('No Odoo modules found');
814
841
  }
815
- const moduleName = await selectPrompt({
816
- message: menuPromptMessage('Module to remove', cancelAction),
817
- options: modules.map((module) => ({ value: module, label: module })),
818
- initialValue: modules[0],
819
- });
820
- handleCancel(moduleName, cancelAction);
821
842
  const deleteFiles = await confirmPrompt({
822
- message: menuPromptMessage('Delete module files too? (y/N)', cancelAction),
843
+ message: menuPromptMessage('Delete module files too?', cancelAction),
823
844
  active: 'Y',
824
845
  inactive: 'n',
825
846
  initialValue: false,
@@ -827,9 +848,9 @@ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = '
827
848
  handleCancel(deleteFiles, cancelAction);
828
849
  return {
829
850
  target,
830
- repoPath: sourceRepo.repoPath,
831
- sourceType: sourceRepo.sourceType,
832
- moduleName: String(moduleName),
851
+ repoPath: selectedModule.repoPath,
852
+ sourceType: selectedModule.sourceType,
853
+ moduleName: selectedModule.moduleName,
833
854
  deleteFiles: Boolean(deleteFiles),
834
855
  stage: true,
835
856
  };
@@ -912,6 +933,40 @@ async function ensureGitHubRepositories(options, interactive) {
912
933
  handleCancel(visibility, 'exit');
913
934
  await createGitHubRepositories(missing, visibility);
914
935
  }
936
+ async function ensureSystemPrerequisites(interactive) {
937
+ while (true) {
938
+ const status = await getSystemPrerequisiteStatus();
939
+ if (status.ok) {
940
+ return true;
941
+ }
942
+ const guidance = renderSystemPrerequisiteGuidance(status);
943
+ if (!interactive) {
944
+ throw new Error(guidance);
945
+ }
946
+ clearPrerequisiteScreen();
947
+ console.log(renderStartupBanner());
948
+ console.log(renderVersionTag());
949
+ console.log();
950
+ console.log(guidance);
951
+ console.log();
952
+ const action = await selectPrompt({
953
+ message: 'If you have installed the prerequisites',
954
+ options: [
955
+ {
956
+ value: 'check-again',
957
+ label: `${renderActionText('Check again')}${dim(' (Enter to re-check again)')}`,
958
+ },
959
+ ],
960
+ initialValue: 'check-again',
961
+ loop: false,
962
+ navigationHelp: 'exit',
963
+ });
964
+ handleCancel(action, 'exit');
965
+ if (action === 'check-again') {
966
+ continue;
967
+ }
968
+ }
969
+ }
915
970
  async function ensureNonInteractiveCreateTarget(options) {
916
971
  if (options.dryRun) {
917
972
  return;
@@ -928,7 +983,7 @@ async function ensureNonInteractiveCreateTarget(options) {
928
983
  }
929
984
  throw new Error(renderForeignEnvironmentTargetWarning(state));
930
985
  }
931
- async function finishCreateFlow(result, cwd, interactive) {
986
+ async function finishCreateFlow(result, cwd, interactive, checkSystemPrerequisites = true) {
932
987
  if (result.kind === 'cancelled') {
933
988
  outroPrompt('Create flow cancelled.');
934
989
  return;
@@ -942,6 +997,9 @@ async function finishCreateFlow(result, cwd, interactive) {
942
997
  return;
943
998
  }
944
999
  const { options } = result;
1000
+ if (!options.dryRun && checkSystemPrerequisites && !(await ensureSystemPrerequisites(interactive))) {
1001
+ return;
1002
+ }
945
1003
  await ensureGitHubRepositories(options, interactive);
946
1004
  const scaffoldResult = await scaffold(options);
947
1005
  if (options.dryRun) {
@@ -968,18 +1026,328 @@ async function finishCreateFlow(result, cwd, interactive) {
968
1026
  }
969
1027
  outroPrompt(`Created Odoo dev overlay in ${options.target}. Review staged changes, then commit.`);
970
1028
  }
971
- async function runCockpitCommand(command, cwd) {
972
- if (command.id === 'exit') {
973
- return 'exit';
1029
+ function selectedModuleRemovalOptions(module, cwd, deleteFiles) {
1030
+ return {
1031
+ target: cwd,
1032
+ repoPath: module.repoPath,
1033
+ sourceType: module.sourceType,
1034
+ moduleName: module.moduleName,
1035
+ deleteFiles,
1036
+ stage: true,
1037
+ };
1038
+ }
1039
+ function moduleDailyAction(action) {
1040
+ if (action === 'update')
1041
+ return 'update';
1042
+ if (action === 'test')
1043
+ return 'test';
1044
+ if (action === 'lint')
1045
+ return 'lint';
1046
+ return undefined;
1047
+ }
1048
+ function moduleDailyActionArgs(action, module) {
1049
+ if (action === 'update' || action === 'test') {
1050
+ return [module.moduleName];
974
1051
  }
975
- if (command.target.kind === 'daily') {
976
- const argv = await collectDailyActionArgs(command.target.command, cwd);
1052
+ return [];
1053
+ }
1054
+ function moduleActionTitle(action) {
1055
+ if (action === 'update')
1056
+ return 'Update module';
1057
+ if (action === 'test')
1058
+ return 'Test module';
1059
+ if (action === 'lint')
1060
+ return 'Run environment lint';
1061
+ if (action === 'delete')
1062
+ return 'Delete module';
1063
+ return 'Module action';
1064
+ }
1065
+ function moduleActionCompletedLabel(action) {
1066
+ if (action === 'update')
1067
+ return 'Update';
1068
+ if (action === 'test')
1069
+ return 'Test';
1070
+ if (action === 'lint')
1071
+ return 'Environment lint';
1072
+ return 'Action';
1073
+ }
1074
+ function commandActionTitle(command) {
1075
+ if (command === 'update')
1076
+ return 'Update module';
1077
+ if (command === 'test')
1078
+ return 'Test module';
1079
+ if (command === 'lint')
1080
+ return 'Run environment lint';
1081
+ if (command === 'pot')
1082
+ return 'Generate POT';
1083
+ return command;
1084
+ }
1085
+ function commandCompletedLabel(command) {
1086
+ if (command === 'install')
1087
+ return 'Install';
1088
+ if (command === 'update')
1089
+ return 'Update';
1090
+ if (command === 'test')
1091
+ return 'Test';
1092
+ if (command === 'lint')
1093
+ return 'Environment lint';
1094
+ if (command === 'pot')
1095
+ return 'Generate POT';
1096
+ return command;
1097
+ }
1098
+ function shouldReturnToDailySelection(command) {
1099
+ return ['install', 'update', 'test', 'pot'].includes(command);
1100
+ }
1101
+ function shouldUseModuleBrowserForDailySelection(command) {
1102
+ return ['update', 'test', 'lint', 'pot'].includes(command);
1103
+ }
1104
+ function dailyActionSelectedLabel(command, argv) {
1105
+ if (['install', 'update', 'test', 'pot'].includes(command)) {
1106
+ return argv[0];
1107
+ }
1108
+ return undefined;
1109
+ }
1110
+ async function selectDatabaseArg(cwd, message, fallback, options = {}) {
1111
+ const databaseResult = normalizeDatabaseListResult(await listEnvironmentDatabases(cwd, options));
1112
+ const databases = databaseResult.databases;
1113
+ if (databases.length > 0) {
1114
+ const selected = await selectPrompt({
1115
+ message: menuPromptMessage(message, 'back'),
1116
+ options: [
1117
+ ...databases.map((database) => ({ value: database, label: database })),
1118
+ { value: manualDatabaseValue, label: 'Manual entry' },
1119
+ ],
1120
+ initialValue: databases.includes(fallback) ? fallback : databases[0],
1121
+ });
1122
+ handleCancel(selected, 'back');
1123
+ if (selected !== manualDatabaseValue) {
1124
+ return String(selected);
1125
+ }
1126
+ }
1127
+ return asString(await textPrompt({
1128
+ message: menuPromptMessage(databaseResult.ok ? message : `${message} (database list unavailable; enter manually)`, 'back'),
1129
+ defaultValue: fallback,
1130
+ placeholder: fallback,
1131
+ }), fallback, 'back');
1132
+ }
1133
+ async function collectCockpitModuleDailyActionArgs(command, module, cwd) {
1134
+ const moduleName = module.moduleName;
1135
+ if (command === 'update') {
1136
+ return [moduleName];
1137
+ }
1138
+ if (command === 'test') {
1139
+ return [moduleName];
1140
+ }
1141
+ if (command === 'lint') {
1142
+ return [];
1143
+ }
1144
+ if (command === 'pot') {
1145
+ return [moduleName];
1146
+ }
1147
+ throw new Error(`Unsupported module action command: ${command}`);
1148
+ }
1149
+ async function renderDailyActionResultPageHeader(title, selectedLabel, cwd) {
1150
+ await renderCockpitSubmenuPage(title, cwd);
1151
+ if (selectedLabel) {
1152
+ console.log(renderActionText(selectedLabel));
1153
+ console.log('');
1154
+ }
1155
+ }
1156
+ async function renderCockpitSubmenuPage(title, cwd) {
1157
+ const status = await getEnvironmentStatus(cwd);
1158
+ const serviceStatus = await getServiceRuntimeStatus(cwd, status);
1159
+ clearCockpitScreen();
1160
+ console.log(renderBanner(renderCockpitStatusLines(status, serviceStatus, `Last: ${title}`), { version: startupVersionLine() }));
1161
+ console.log();
1162
+ introPrompt(title);
1163
+ }
1164
+ async function waitForModuleActionBack() {
1165
+ console.log(renderBackHelp());
1166
+ if (!process.stdin.isTTY) {
1167
+ return false;
1168
+ }
1169
+ await new Promise((resolve) => {
1170
+ emitKeypressEvents(process.stdin);
1171
+ const input = process.stdin;
1172
+ const wasRaw = input.isRaw;
1173
+ const listener = (_value, key) => {
1174
+ if (key.ctrl && key.name === 'c') {
1175
+ process.exit(1);
1176
+ }
1177
+ if (key.name === 'escape' || key.sequence === '\u001B') {
1178
+ cleanup();
1179
+ resolve();
1180
+ }
1181
+ };
1182
+ const cleanup = () => {
1183
+ input.off('keypress', listener);
1184
+ if (typeof input.setRawMode === 'function') {
1185
+ input.setRawMode(Boolean(wasRaw));
1186
+ }
1187
+ input.pause();
1188
+ };
1189
+ if (typeof input.setRawMode === 'function') {
1190
+ input.setRawMode(true);
1191
+ }
1192
+ input.resume();
1193
+ input.on('keypress', listener);
1194
+ });
1195
+ return true;
1196
+ }
1197
+ async function runDailyActionResultPage(command, argv, cwd, title = commandActionTitle(command), selectedLabel = dailyActionSelectedLabel(command, argv), completedLabel = commandCompletedLabel(command)) {
1198
+ await renderDailyActionResultPageHeader(title, selectedLabel, cwd);
1199
+ try {
1200
+ await runDailyActionWithStyledOutput(command, argv, cwd);
1201
+ notePrompt(renderCompletedText(completedLabel), 'Done');
1202
+ }
1203
+ catch (error) {
1204
+ const message = error instanceof Error ? error.message : String(error);
1205
+ notePrompt(message, 'Error');
1206
+ await waitForModuleActionBack();
1207
+ throw error;
1208
+ }
1209
+ return waitForModuleActionBack();
1210
+ }
1211
+ async function runSelectedModuleDailyAction(action, module, cwd) {
1212
+ const command = moduleDailyAction(action);
1213
+ if (!command) {
1214
+ return false;
1215
+ }
1216
+ return runDailyActionResultPage(command, moduleDailyActionArgs(action, module), cwd, moduleActionTitle(action), action === 'lint' ? undefined : module.moduleName, moduleActionCompletedLabel(action));
1217
+ }
1218
+ async function runSelectedModuleAction(action, module, cwd) {
1219
+ if (action === 'delete') {
1220
+ const deleteFiles = await confirmPrompt({
1221
+ message: menuPromptMessage('Delete module files too?', 'back'),
1222
+ active: 'Y',
1223
+ inactive: 'n',
1224
+ initialValue: false,
1225
+ });
1226
+ handleCancel(deleteFiles, 'back');
1227
+ const removeCommand = cockpitCommands.find((entry) => entry.id === 'remove-module');
1228
+ if (removeCommand && !(await confirmCockpitCommandRisk(removeCommand))) {
1229
+ notePrompt(`Module ${module.moduleName} was not removed.`, 'Action skipped');
1230
+ return false;
1231
+ }
1232
+ await removeModuleFromSourceRepo(selectedModuleRemovalOptions(module, cwd, Boolean(deleteFiles)));
1233
+ notePrompt(`Removed module ${module.moduleName} from source repo ${module.repoPath}.`, 'Done');
1234
+ return false;
1235
+ }
1236
+ return runSelectedModuleDailyAction(action, module, cwd);
1237
+ }
1238
+ async function runCockpitModuleDailyCommand(command, cwd) {
1239
+ if (command.target.kind !== 'daily') {
1240
+ return;
1241
+ }
1242
+ const dailyCommand = command.target.command;
1243
+ while (true) {
1244
+ let selectedModule;
1245
+ let argv;
1246
+ try {
1247
+ await renderCockpitSubmenuPage(command.label, cwd);
1248
+ const module = await selectModuleFromBrowser(cwd);
1249
+ if (!module) {
1250
+ notePrompt('No Odoo modules found.\nNext: choose "Add module" or "Add source repo" first.', command.label);
1251
+ return;
1252
+ }
1253
+ selectedModule = module;
1254
+ argv = await collectCockpitModuleDailyActionArgs(dailyCommand, selectedModule, cwd);
1255
+ }
1256
+ catch (error) {
1257
+ if (isMenuBackSignal(error)) {
1258
+ return;
1259
+ }
1260
+ throw error;
1261
+ }
977
1262
  if (!(await confirmCockpitCommandRisk(command))) {
978
1263
  notePrompt(`${command.slashAlias} was not run.`, 'Action skipped');
979
- return 'continue';
1264
+ return;
1265
+ }
1266
+ const returnedByBack = await runDailyActionResultPage(dailyCommand, argv, cwd, command.label, selectedModule.moduleName, commandCompletedLabel(dailyCommand));
1267
+ if (!returnedByBack) {
1268
+ return;
1269
+ }
1270
+ }
1271
+ }
1272
+ async function runCockpitDailyCommand(command, cwd) {
1273
+ if (command.target.kind !== 'daily') {
1274
+ return;
1275
+ }
1276
+ const dailyCommand = command.target.command;
1277
+ if (shouldUseModuleBrowserForDailySelection(dailyCommand)) {
1278
+ await runCockpitModuleDailyCommand(command, cwd);
1279
+ return;
1280
+ }
1281
+ if (!shouldReturnToDailySelection(dailyCommand)) {
1282
+ const argv = await collectDailyActionArgs(dailyCommand, cwd);
1283
+ if (!(await confirmCockpitCommandRisk(command))) {
1284
+ notePrompt(`${command.slashAlias} was not run.`, 'Action skipped');
1285
+ return;
980
1286
  }
981
- await runDailyAction(command.target.command, argv, cwd);
1287
+ await runDailyAction(dailyCommand, argv, cwd);
982
1288
  notePrompt(`${command.slashAlias} completed.`, 'Done');
1289
+ return;
1290
+ }
1291
+ while (true) {
1292
+ let argv;
1293
+ try {
1294
+ await renderCockpitSubmenuPage(command.label, cwd);
1295
+ argv = await collectDailyActionArgs(dailyCommand, cwd);
1296
+ }
1297
+ catch (error) {
1298
+ if (isMenuBackSignal(error)) {
1299
+ return;
1300
+ }
1301
+ throw error;
1302
+ }
1303
+ if (!(await confirmCockpitCommandRisk(command))) {
1304
+ notePrompt(`${command.slashAlias} was not run.`, 'Action skipped');
1305
+ return;
1306
+ }
1307
+ const returnedByBack = await runDailyActionResultPage(dailyCommand, argv, cwd, command.label);
1308
+ if (!returnedByBack) {
1309
+ return;
1310
+ }
1311
+ }
1312
+ }
1313
+ async function runListModulesCommand(cwd) {
1314
+ while (true) {
1315
+ await renderCockpitSubmenuPage('List modules', cwd);
1316
+ const selectedModule = await selectModuleFromBrowser(cwd);
1317
+ if (!selectedModule) {
1318
+ notePrompt('No Odoo modules found.\nNext: choose "Add module" or "Add source repo" first.', 'List modules');
1319
+ return;
1320
+ }
1321
+ while (true) {
1322
+ let moduleAction;
1323
+ try {
1324
+ await renderCockpitSubmenuPage('List modules', cwd);
1325
+ console.log(renderActionText(selectedModule.moduleName));
1326
+ console.log('');
1327
+ moduleAction = await selectModuleAction(selectedModule);
1328
+ }
1329
+ catch (error) {
1330
+ if (isMenuBackSignal(error)) {
1331
+ break;
1332
+ }
1333
+ throw error;
1334
+ }
1335
+ if (!moduleAction) {
1336
+ break;
1337
+ }
1338
+ const returnedByBack = await runSelectedModuleAction(moduleAction, selectedModule, cwd);
1339
+ if (!returnedByBack) {
1340
+ return;
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ async function runCockpitCommand(command, cwd) {
1346
+ if (command.id === 'exit') {
1347
+ return 'exit';
1348
+ }
1349
+ if (command.target.kind === 'daily') {
1350
+ await runCockpitDailyCommand(command, cwd);
983
1351
  return 'continue';
984
1352
  }
985
1353
  if (command.id === 'status') {
@@ -990,6 +1358,10 @@ async function runCockpitCommand(command, cwd) {
990
1358
  notePrompt(await runDoctor(cwd), 'Doctor');
991
1359
  return 'continue';
992
1360
  }
1361
+ if (command.id === 'list-modules') {
1362
+ await runListModulesCommand(cwd);
1363
+ return 'continue';
1364
+ }
993
1365
  if (command.id === 'add-repo') {
994
1366
  const options = await addRepoOptionsFromPrompts(false, 'back');
995
1367
  await ensureAddRepoGitHubRepository(options, 'back');
@@ -1014,7 +1386,7 @@ async function runCockpitCommand(command, cwd) {
1014
1386
  return 'continue';
1015
1387
  }
1016
1388
  if (command.id === 'remove-module') {
1017
- const options = await removeModuleOptionsFromPrompts(false, 'back');
1389
+ const options = await removeModuleOptionsFromPrompts(true, 'back');
1018
1390
  if (!(await confirmCockpitCommandRisk(command))) {
1019
1391
  notePrompt(`Module ${options.moduleName} was not removed.`, 'Action skipped');
1020
1392
  return 'continue';
@@ -1051,13 +1423,21 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1051
1423
  const detection = await detectDevelopmentEnvironment(cwd);
1052
1424
  if (!detection.isEnvironment) {
1053
1425
  await showStartup(argv, skipUpdateCheck);
1054
- await finishCreateFlow(await optionsFromPrompts(), cwd, true);
1426
+ if (!(await ensureSystemPrerequisites(true))) {
1427
+ return;
1428
+ }
1429
+ await finishCreateFlow(await optionsFromPrompts(), cwd, true, false);
1055
1430
  return;
1056
1431
  }
1057
1432
  let lastStatus = 'Last: Ready';
1058
1433
  let status = await getEnvironmentStatus(cwd);
1059
1434
  let serviceStatus = await getServiceRuntimeStatus(cwd, status);
1060
1435
  await showStartup(argv, skipUpdateCheck, () => renderCockpitStatusLines(status, serviceStatus, lastStatus));
1436
+ const renderCockpitMenuShell = () => {
1437
+ clearCockpitScreen();
1438
+ console.log(renderBanner(renderCockpitStatusLines(status, serviceStatus, lastStatus), { version: startupVersionLine() }));
1439
+ console.log();
1440
+ };
1061
1441
  while (true) {
1062
1442
  try {
1063
1443
  const command = await selectCockpitCommandFromMenu(serviceStatus);
@@ -1071,6 +1451,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1071
1451
  }
1072
1452
  catch (error) {
1073
1453
  if (isMenuBackSignal(error)) {
1454
+ renderCockpitMenuShell();
1074
1455
  continue;
1075
1456
  }
1076
1457
  commandFailed = true;
@@ -1084,9 +1465,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1084
1465
  }
1085
1466
  status = await getEnvironmentStatus(cwd);
1086
1467
  serviceStatus = await getServiceRuntimeStatus(cwd, status);
1087
- clearCockpitScreen();
1088
- console.log(renderBanner(renderCockpitStatusLines(status, serviceStatus, lastStatus), { version: startupVersionLine() }));
1089
- console.log();
1468
+ renderCockpitMenuShell();
1090
1469
  }
1091
1470
  catch (error) {
1092
1471
  if (isMenuBackSignal(error)) {
@@ -1214,7 +1593,10 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1214
1593
  await finishCreateFlow({ kind: 'create', options }, cwd, false);
1215
1594
  return;
1216
1595
  }
1217
- await finishCreateFlow(await optionsFromPrompts(), cwd, true);
1596
+ if (!(await ensureSystemPrerequisites(true))) {
1597
+ return;
1598
+ }
1599
+ await finishCreateFlow(await optionsFromPrompts(), cwd, true, false);
1218
1600
  }
1219
1601
  export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
1220
1602
  if (!argvPath)
@@ -34,10 +34,14 @@ export const cockpitCommands = [
34
34
  dailyCommand('restart', 'services', 'Restart services', 'Restart the Odoo development services.', ['reload']),
35
35
  dailyCommand('logs', 'services', 'View logs', 'Stream logs for an Odoo environment service.', ['log', 'tail']),
36
36
  dailyCommand('shell', 'services', 'Open shell', 'Open a shell inside the Odoo service container.', ['bash', 'terminal']),
37
+ internalCommand('list-modules', 'modules', 'List modules', 'Browse detected Odoo modules by source category.', [
38
+ 'modules list',
39
+ 'browse modules',
40
+ ]),
37
41
  dailyCommand('install', 'modules', 'Install module', 'Install one or more Odoo modules into a database.', ['install module']),
38
42
  dailyCommand('update', 'modules', 'Update module', 'Update one or more Odoo modules in a database.', ['upgrade']),
39
43
  dailyCommand('test', 'modules', 'Run tests', 'Run Odoo tests for one or more modules.', ['tests', 'pytest']),
40
- dailyCommand('lint', 'modules', 'Run lint', 'Run the configured module lint checks.', ['check', 'quality']),
44
+ dailyCommand('lint', 'modules', 'Run environment lint', 'Run the configured environment lint checks.', ['check', 'quality']),
41
45
  dailyCommand('pot', 'modules', 'Generate POT', 'Generate translation template files for a module.', ['translation', 'i18n']),
42
46
  dailyCommand('psql', 'database', 'Open psql', 'Open a PostgreSQL prompt for an environment database.', ['postgres', 'sql']),
43
47
  dailyCommand('snapshot', 'database', 'Create snapshot', 'Create a database snapshot.', ['backup', 'dump']),