@wpmoo/odoo 0.8.46 → 0.8.48
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/README.md +16 -12
- package/dist/cli.js +98 -48
- package/dist/cockpit/command-palette.js +19 -0
- package/dist/cockpit/command-registry.js +91 -0
- package/dist/cockpit/daily-prompts.js +173 -0
- package/dist/cockpit/menu.js +88 -0
- package/dist/cockpit/safety.js +22 -0
- package/dist/help.js +6 -0
- package/docs/assets/wpmoo-banner.png +0 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
# @wpmoo/odoo
|
|
2
|
-
|
|
3
1
|

|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
[](https://github.com/wpmoo-org/wpmoo-odoo/actions/workflows/ci.yml) [](https://github.com/wpmoo-org/wpmoo-odoo) [](https://www.npmjs.com/package/@wpmoo/odoo) [](https://coveralls.io/github/wpmoo-org/wpmoo-odoo?branch=main) [](LICENSE) [](https://github.com/wpmoo-org/wpmoo-odoo) [](https://www.buymeacoffee.com/cangir)
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
6
|
WPMoo Odoo lifecycle tooling for development, staging, and production workflows.
|
|
13
7
|
|
|
14
8
|
The CLI currently creates Docker Compose based Odoo environments, adds source
|
|
@@ -58,16 +52,26 @@ npx @wpmoo/odoo
|
|
|
58
52
|
The wizard is context-aware. If the current directory is not already a WPMoo
|
|
59
53
|
Odoo development environment, it starts the create flow directly.
|
|
60
54
|
|
|
61
|
-
Inside an existing environment, it
|
|
55
|
+
Inside an existing environment, it opens the cockpit. The cockpit includes
|
|
56
|
+
`Command palette /` for slash-style search across services, modules, database,
|
|
57
|
+
diagnostics, repositories, and maintenance categories, plus guided category
|
|
58
|
+
menus for common workflows.
|
|
62
59
|
|
|
63
60
|
```text
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
Command palette /
|
|
62
|
+
Services
|
|
63
|
+
Modules
|
|
64
|
+
Database
|
|
65
|
+
Diagnostics
|
|
66
|
+
Repositories
|
|
67
|
+
Maintenance
|
|
68
|
+
Exit
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
Direct commands remain available for scripts and repeatable terminal workflows,
|
|
72
|
+
such as `npx @wpmoo/odoo status`, `npx @wpmoo/odoo test sale --db devel`, and
|
|
73
|
+
`npx @wpmoo/odoo logs odoo`.
|
|
74
|
+
|
|
71
75
|
Non-interactive:
|
|
72
76
|
|
|
73
77
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { commandFromArgs, defaultTargetForProduct, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
|
|
7
|
+
import { selectCockpitCommandFromPalette } from './cockpit/command-palette.js';
|
|
8
|
+
import { collectDailyActionArgs } from './cockpit/daily-prompts.js';
|
|
9
|
+
import { selectCockpitCategoryCommand, selectCockpitTopLevelMenu } from './cockpit/menu.js';
|
|
10
|
+
import { confirmCockpitCommandRisk } from './cockpit/safety.js';
|
|
6
11
|
import { detectDevelopmentEnvironment } from './environment.js';
|
|
7
12
|
import { commandOdooVersion } from './environment-version.js';
|
|
8
13
|
import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
|
|
@@ -129,22 +134,15 @@ async function showStartup(argv, skipUpdateCheck) {
|
|
|
129
134
|
}
|
|
130
135
|
console.log();
|
|
131
136
|
}
|
|
132
|
-
async function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{ value: 'reset', label: 'Safe reset environment' },
|
|
142
|
-
{ value: 'exit', label: 'Exit' },
|
|
143
|
-
],
|
|
144
|
-
initialValue: 'add-module',
|
|
145
|
-
});
|
|
146
|
-
handleCancel(action, 'back');
|
|
147
|
-
return action;
|
|
137
|
+
async function selectCockpitCommandFromMenu() {
|
|
138
|
+
const selection = await selectCockpitTopLevelMenu();
|
|
139
|
+
if (selection.kind === 'exit') {
|
|
140
|
+
return 'exit';
|
|
141
|
+
}
|
|
142
|
+
if (selection.kind === 'command-palette') {
|
|
143
|
+
return selectCockpitCommandFromPalette();
|
|
144
|
+
}
|
|
145
|
+
return selectCockpitCategoryCommand(selection.category);
|
|
148
146
|
}
|
|
149
147
|
async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
150
148
|
if (showIntro) {
|
|
@@ -546,6 +544,71 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
546
544
|
process.exit(1);
|
|
547
545
|
await createGitHubRepositories(missing, visibility);
|
|
548
546
|
}
|
|
547
|
+
async function runCockpitCommand(command, cwd) {
|
|
548
|
+
if (command.id === 'exit') {
|
|
549
|
+
return 'exit';
|
|
550
|
+
}
|
|
551
|
+
if (command.target.kind === 'daily') {
|
|
552
|
+
const argv = await collectDailyActionArgs(command.target.command, cwd);
|
|
553
|
+
if (!(await confirmCockpitCommandRisk(command))) {
|
|
554
|
+
note(`${command.slashAlias} was not run.`, 'Action skipped');
|
|
555
|
+
return 'continue';
|
|
556
|
+
}
|
|
557
|
+
await runDailyAction(command.target.command, argv, cwd);
|
|
558
|
+
note(`${command.slashAlias} completed.`, 'Done');
|
|
559
|
+
return 'continue';
|
|
560
|
+
}
|
|
561
|
+
if (command.id === 'status') {
|
|
562
|
+
note(await renderEnvironmentStatusForTarget(cwd), 'Environment status');
|
|
563
|
+
return 'continue';
|
|
564
|
+
}
|
|
565
|
+
if (command.id === 'doctor') {
|
|
566
|
+
note(await runDoctor(cwd), 'Doctor');
|
|
567
|
+
return 'continue';
|
|
568
|
+
}
|
|
569
|
+
if (command.id === 'add-repo') {
|
|
570
|
+
const options = await addRepoOptionsFromPrompts(false, 'back');
|
|
571
|
+
await ensureAddRepoGitHubRepository(options, 'back');
|
|
572
|
+
await addModuleRepo(options);
|
|
573
|
+
note(`Added source repo under ${options.target}/odoo/custom/src/private.`, 'Done');
|
|
574
|
+
return 'continue';
|
|
575
|
+
}
|
|
576
|
+
if (command.id === 'remove-repo') {
|
|
577
|
+
const options = await removeRepoOptionsFromPrompts([], false, 'back');
|
|
578
|
+
if (!(await confirmCockpitCommandRisk(command))) {
|
|
579
|
+
note(`Source repo ${options.repoPath} was not removed.`, 'Action skipped');
|
|
580
|
+
return 'continue';
|
|
581
|
+
}
|
|
582
|
+
await removeModuleRepo(options);
|
|
583
|
+
note(`Removed source repo ${options.repoPath} from ${options.target}.`, 'Done');
|
|
584
|
+
return 'continue';
|
|
585
|
+
}
|
|
586
|
+
if (command.id === 'add-module') {
|
|
587
|
+
const options = await addModuleOptionsFromPrompts(false, 'back');
|
|
588
|
+
await addModuleToSourceRepo(options);
|
|
589
|
+
note(`Added module ${options.moduleName} under source repo ${options.repoPath}.`, 'Done');
|
|
590
|
+
return 'continue';
|
|
591
|
+
}
|
|
592
|
+
if (command.id === 'remove-module') {
|
|
593
|
+
const options = await removeModuleOptionsFromPrompts(false, 'back');
|
|
594
|
+
if (!(await confirmCockpitCommandRisk(command))) {
|
|
595
|
+
note(`Module ${options.moduleName} was not removed.`, 'Action skipped');
|
|
596
|
+
return 'continue';
|
|
597
|
+
}
|
|
598
|
+
await removeModuleFromSourceRepo(options);
|
|
599
|
+
note(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`, 'Done');
|
|
600
|
+
return 'continue';
|
|
601
|
+
}
|
|
602
|
+
if (command.id === 'safe-reset') {
|
|
603
|
+
const options = { target: cwd, stage: true };
|
|
604
|
+
await confirmSafeResetFromMenu(options);
|
|
605
|
+
await safeResetEnvironment(options);
|
|
606
|
+
note(`Safe reset refreshed generated environment files in ${cwd}.`, 'Done');
|
|
607
|
+
return 'continue';
|
|
608
|
+
}
|
|
609
|
+
note(`Unknown cockpit command: ${command.slashAlias}`, 'No action');
|
|
610
|
+
return 'continue';
|
|
611
|
+
}
|
|
549
612
|
export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd()) {
|
|
550
613
|
installPromptCancelKeyTracker();
|
|
551
614
|
const rawArgv = cliArgv;
|
|
@@ -570,44 +633,19 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
570
633
|
outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
571
634
|
return;
|
|
572
635
|
}
|
|
636
|
+
intro('WPMoo Odoo Dev');
|
|
573
637
|
while (true) {
|
|
574
638
|
try {
|
|
575
639
|
const status = await getEnvironmentStatus(cwd);
|
|
576
640
|
note(renderEnvironmentStatusSummary(status), 'Environment status');
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
641
|
+
const command = await selectCockpitCommandFromMenu();
|
|
642
|
+
if (command === 'exit') {
|
|
579
643
|
return;
|
|
580
644
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
await ensureAddRepoGitHubRepository(options, 'back');
|
|
584
|
-
await addModuleRepo(options);
|
|
585
|
-
outro(`Added source repo under ${options.target}/odoo/custom/src/private.`);
|
|
645
|
+
const outcome = await runCockpitCommand(command, cwd);
|
|
646
|
+
if (outcome === 'exit') {
|
|
586
647
|
return;
|
|
587
648
|
}
|
|
588
|
-
if (action === 'remove-repo') {
|
|
589
|
-
const options = await removeRepoOptionsFromPrompts([], false, 'back');
|
|
590
|
-
await removeModuleRepo(options);
|
|
591
|
-
outro(`Removed source repo ${options.repoPath} from ${options.target}.`);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (action === 'add-module') {
|
|
595
|
-
const options = await addModuleOptionsFromPrompts(false, 'back');
|
|
596
|
-
await addModuleToSourceRepo(options);
|
|
597
|
-
outro(`Added module ${options.moduleName} under source repo ${options.repoPath}.`);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
if (action === 'remove-module') {
|
|
601
|
-
const options = await removeModuleOptionsFromPrompts(false, 'back');
|
|
602
|
-
await removeModuleFromSourceRepo(options);
|
|
603
|
-
outro(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
const options = { target: cwd, stage: true };
|
|
607
|
-
await confirmSafeResetFromMenu(options);
|
|
608
|
-
await safeResetEnvironment(options);
|
|
609
|
-
outro(`Safe reset refreshed generated environment files in ${cwd}.`);
|
|
610
|
-
return;
|
|
611
649
|
}
|
|
612
650
|
catch (error) {
|
|
613
651
|
if (isMenuBackSignal(error)) {
|
|
@@ -723,7 +761,19 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
723
761
|
}
|
|
724
762
|
outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
725
763
|
}
|
|
726
|
-
|
|
764
|
+
export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
|
|
765
|
+
if (!argvPath)
|
|
766
|
+
return false;
|
|
767
|
+
try {
|
|
768
|
+
const entrypointUrl = pathToFileURL(realpathSync(fileURLToPath(metaUrl))).href;
|
|
769
|
+
const argvUrl = pathToFileURL(realpathSync(argvPath)).href;
|
|
770
|
+
return entrypointUrl === argvUrl;
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
return metaUrl === pathToFileURL(argvPath).href;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (isCliEntrypoint(import.meta.url)) {
|
|
727
777
|
runCli().catch((error) => {
|
|
728
778
|
const message = error instanceof Error ? error.message : String(error);
|
|
729
779
|
console.error(message);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import search from '@inquirer/search';
|
|
2
|
+
import { searchCockpitCommands } from './command-registry.js';
|
|
3
|
+
const defaultSearchPrompt = (config) => search(config);
|
|
4
|
+
function commandChoice(command) {
|
|
5
|
+
return {
|
|
6
|
+
value: command,
|
|
7
|
+
name: `${command.slashAlias} ${command.label}`,
|
|
8
|
+
description: command.description,
|
|
9
|
+
short: command.id,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function selectCockpitCommandFromPalette(options = {}) {
|
|
13
|
+
const prompt = options.prompt ?? defaultSearchPrompt;
|
|
14
|
+
return prompt({
|
|
15
|
+
message: 'Search commands',
|
|
16
|
+
pageSize: 10,
|
|
17
|
+
source: (term) => searchCockpitCommands(term).map(commandChoice),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const riskyCommandIds = new Set(['stop', 'resetdb', 'restore-snapshot', 'remove-repo', 'remove-module', 'safe-reset']);
|
|
2
|
+
function dailyCommand(command, category, label, description, aliases = []) {
|
|
3
|
+
return {
|
|
4
|
+
id: command,
|
|
5
|
+
slashAlias: `/${command}`,
|
|
6
|
+
category,
|
|
7
|
+
label,
|
|
8
|
+
description,
|
|
9
|
+
isRisky: riskyCommandIds.has(command),
|
|
10
|
+
target: {
|
|
11
|
+
kind: 'daily',
|
|
12
|
+
command,
|
|
13
|
+
},
|
|
14
|
+
aliases,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function internalCommand(id, category, label, description, aliases = []) {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
slashAlias: `/${id}`,
|
|
21
|
+
category,
|
|
22
|
+
label,
|
|
23
|
+
description,
|
|
24
|
+
isRisky: riskyCommandIds.has(id),
|
|
25
|
+
target: {
|
|
26
|
+
kind: 'internal',
|
|
27
|
+
},
|
|
28
|
+
aliases,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export const cockpitCommands = [
|
|
32
|
+
dailyCommand('start', 'services', 'Start services', 'Start the Odoo development services.', ['up', 'compose up']),
|
|
33
|
+
dailyCommand('stop', 'services', 'Stop services', 'Stop the Odoo development services.', ['down', 'compose down']),
|
|
34
|
+
dailyCommand('restart', 'services', 'Restart services', 'Restart the Odoo development services.', ['reload']),
|
|
35
|
+
dailyCommand('logs', 'services', 'View logs', 'Stream logs for an Odoo environment service.', ['log', 'tail']),
|
|
36
|
+
dailyCommand('shell', 'services', 'Open shell', 'Open a shell inside the Odoo service container.', ['bash', 'terminal']),
|
|
37
|
+
dailyCommand('install', 'modules', 'Install module', 'Install one or more Odoo modules into a database.', ['install module']),
|
|
38
|
+
dailyCommand('update', 'modules', 'Update module', 'Update one or more Odoo modules in a database.', ['upgrade']),
|
|
39
|
+
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']),
|
|
41
|
+
dailyCommand('pot', 'modules', 'Generate POT', 'Generate translation template files for a module.', ['translation', 'i18n']),
|
|
42
|
+
dailyCommand('psql', 'database', 'Open psql', 'Open a PostgreSQL prompt for an environment database.', ['postgres', 'sql']),
|
|
43
|
+
dailyCommand('snapshot', 'database', 'Create snapshot', 'Create a database snapshot.', ['backup', 'dump']),
|
|
44
|
+
dailyCommand('restore-snapshot', 'database', 'Restore snapshot', 'Restore a database from a named snapshot.', ['restore', 'snapshot restore']),
|
|
45
|
+
dailyCommand('resetdb', 'database', 'Reset database', 'Reset an environment database.', ['reset db', 'database reset']),
|
|
46
|
+
internalCommand('status', 'diagnostics', 'Environment status', 'Show a summary of the current environment state.', [
|
|
47
|
+
'state',
|
|
48
|
+
'summary',
|
|
49
|
+
]),
|
|
50
|
+
internalCommand('doctor', 'diagnostics', 'Run doctor', 'Run environment diagnostics and report actionable issues.', [
|
|
51
|
+
'diagnose',
|
|
52
|
+
'health',
|
|
53
|
+
]),
|
|
54
|
+
internalCommand('add-repo', 'repositories', 'Add source repo', 'Add a source repository as an environment submodule.', [
|
|
55
|
+
'repository add',
|
|
56
|
+
'source add',
|
|
57
|
+
]),
|
|
58
|
+
internalCommand('remove-repo', 'repositories', 'Remove source repo', 'Remove a source repository from the environment.', ['repository remove', 'source remove']),
|
|
59
|
+
internalCommand('add-module', 'modules', 'Add module', 'Add a module folder to a source repository.', ['module add']),
|
|
60
|
+
internalCommand('remove-module', 'modules', 'Remove module', 'Remove a module folder from a source repository.', ['module remove']),
|
|
61
|
+
internalCommand('safe-reset', 'maintenance', 'Safe reset environment', 'Refresh generated environment files while preserving source repositories.', ['reset', 'refresh']),
|
|
62
|
+
internalCommand('exit', 'maintenance', 'Exit', 'Leave the command palette.', ['quit', 'back']),
|
|
63
|
+
];
|
|
64
|
+
const defaultCommandIds = new Set(['start', 'logs', 'test', 'status', 'doctor', 'exit']);
|
|
65
|
+
export function normalizeCockpitSearchTerm(term) {
|
|
66
|
+
return (term ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
67
|
+
}
|
|
68
|
+
function commandSearchFields(command) {
|
|
69
|
+
return [command.slashAlias, command.id, command.label, command.category, command.description, ...command.aliases].map(normalizeCockpitSearchTerm);
|
|
70
|
+
}
|
|
71
|
+
function exactMatchScore(command, term) {
|
|
72
|
+
const bareTerm = term.startsWith('/') ? term.slice(1) : term;
|
|
73
|
+
if (normalizeCockpitSearchTerm(command.slashAlias) === term)
|
|
74
|
+
return 0;
|
|
75
|
+
if (normalizeCockpitSearchTerm(command.id) === bareTerm)
|
|
76
|
+
return 0;
|
|
77
|
+
if (normalizeCockpitSearchTerm(command.label) === term)
|
|
78
|
+
return 1;
|
|
79
|
+
if (command.aliases.map(normalizeCockpitSearchTerm).includes(term))
|
|
80
|
+
return 2;
|
|
81
|
+
return 10;
|
|
82
|
+
}
|
|
83
|
+
export function searchCockpitCommands(term) {
|
|
84
|
+
const normalizedTerm = normalizeCockpitSearchTerm(term);
|
|
85
|
+
if (!normalizedTerm) {
|
|
86
|
+
return cockpitCommands.filter((command) => defaultCommandIds.has(command.id));
|
|
87
|
+
}
|
|
88
|
+
return cockpitCommands
|
|
89
|
+
.filter((command) => commandSearchFields(command).some((field) => field.includes(normalizedTerm)))
|
|
90
|
+
.sort((left, right) => exactMatchScore(left, normalizedTerm) - exactMatchScore(right, normalizedTerm));
|
|
91
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { isCancel, select, text } from '@clack/prompts';
|
|
2
|
+
import { listModulesInSourceRepo } from '../module-actions.js';
|
|
3
|
+
import { listModuleRepos } from '../repo-actions.js';
|
|
4
|
+
import { handlePromptCancel, menuPromptMessage, } from '../menu-navigation.js';
|
|
5
|
+
const manualModuleValue = '__wpmoo_manual_module_entry__';
|
|
6
|
+
function defaultCancelHandler(value, action) {
|
|
7
|
+
handlePromptCancel(isCancel(value), action);
|
|
8
|
+
}
|
|
9
|
+
function promptDeps(deps = {}) {
|
|
10
|
+
return {
|
|
11
|
+
select: deps.select ?? ((options) => select(options)),
|
|
12
|
+
text: deps.text ?? ((options) => text(options)),
|
|
13
|
+
list: deps.list ?? ((options) => select(options)),
|
|
14
|
+
handleCancel: deps.handleCancel ?? defaultCancelHandler,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function asString(value, fallback, deps) {
|
|
18
|
+
deps.handleCancel(value, 'back');
|
|
19
|
+
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
20
|
+
}
|
|
21
|
+
function requiredString(value, message, deps) {
|
|
22
|
+
deps.handleCancel(value, 'back');
|
|
23
|
+
if (typeof value === 'string' && value.trim()) {
|
|
24
|
+
return value.trim();
|
|
25
|
+
}
|
|
26
|
+
throw new Error(message);
|
|
27
|
+
}
|
|
28
|
+
async function detectedModules(cwd) {
|
|
29
|
+
try {
|
|
30
|
+
const repos = await listModuleRepos(cwd);
|
|
31
|
+
const modules = await Promise.all(repos.map(async (repo) => {
|
|
32
|
+
try {
|
|
33
|
+
return await listModulesInSourceRepo(cwd, repo);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
return [...new Set(modules.flat())].sort();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function moduleArg(cwd, deps, message = 'Module(s)') {
|
|
46
|
+
const modules = await detectedModules(cwd);
|
|
47
|
+
if (modules.length === 0) {
|
|
48
|
+
return requiredString(await deps.text({
|
|
49
|
+
message: menuPromptMessage(message, 'back'),
|
|
50
|
+
placeholder: 'sale,stock',
|
|
51
|
+
validate: (value) => (value.trim() ? undefined : 'Enter one or more module technical names.'),
|
|
52
|
+
}), 'Module is required.', deps);
|
|
53
|
+
}
|
|
54
|
+
const selected = await deps.select({
|
|
55
|
+
message: menuPromptMessage(message, 'back'),
|
|
56
|
+
options: [
|
|
57
|
+
...modules.map((moduleName) => ({ value: moduleName, label: moduleName })),
|
|
58
|
+
{ value: manualModuleValue, label: 'Manual entry' },
|
|
59
|
+
],
|
|
60
|
+
initialValue: modules[0],
|
|
61
|
+
});
|
|
62
|
+
deps.handleCancel(selected, 'back');
|
|
63
|
+
if (selected !== manualModuleValue) {
|
|
64
|
+
return String(selected);
|
|
65
|
+
}
|
|
66
|
+
return requiredString(await deps.text({
|
|
67
|
+
message: menuPromptMessage('Module(s)', 'back'),
|
|
68
|
+
placeholder: modules.join(','),
|
|
69
|
+
validate: (value) => (value.trim() ? undefined : 'Enter one or more module technical names.'),
|
|
70
|
+
}), 'Module is required.', deps);
|
|
71
|
+
}
|
|
72
|
+
async function optionalTextArg(deps, message, fallback) {
|
|
73
|
+
return asString(await deps.text({
|
|
74
|
+
message: menuPromptMessage(message, 'back'),
|
|
75
|
+
defaultValue: fallback,
|
|
76
|
+
placeholder: fallback,
|
|
77
|
+
}), fallback, deps);
|
|
78
|
+
}
|
|
79
|
+
async function optionalModules(cwd, deps) {
|
|
80
|
+
const modules = await detectedModules(cwd);
|
|
81
|
+
if (modules.length === 0) {
|
|
82
|
+
const manualModules = asString(await deps.text({
|
|
83
|
+
message: menuPromptMessage('Module(s) to include (optional)', 'back'),
|
|
84
|
+
placeholder: 'sale,stock',
|
|
85
|
+
}), '', deps);
|
|
86
|
+
return manualModules || undefined;
|
|
87
|
+
}
|
|
88
|
+
const selected = await deps.select({
|
|
89
|
+
message: menuPromptMessage('Module(s) to include (optional)', 'back'),
|
|
90
|
+
options: [
|
|
91
|
+
{ value: '', label: 'All modules' },
|
|
92
|
+
...modules.map((moduleName) => ({ value: moduleName, label: moduleName })),
|
|
93
|
+
{ value: manualModuleValue, label: 'Manual entry' },
|
|
94
|
+
],
|
|
95
|
+
initialValue: '',
|
|
96
|
+
});
|
|
97
|
+
deps.handleCancel(selected, 'back');
|
|
98
|
+
if (selected === '') {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (selected !== manualModuleValue) {
|
|
102
|
+
return String(selected);
|
|
103
|
+
}
|
|
104
|
+
const manualModules = asString(await deps.text({
|
|
105
|
+
message: menuPromptMessage('Module(s) to include', 'back'),
|
|
106
|
+
placeholder: modules.join(','),
|
|
107
|
+
}), '', deps);
|
|
108
|
+
return manualModules || undefined;
|
|
109
|
+
}
|
|
110
|
+
export async function collectDailyActionArgs(command, cwd, promptDepsArg = {}) {
|
|
111
|
+
const deps = promptDeps(promptDepsArg);
|
|
112
|
+
if (['start', 'restart', 'shell', 'lint', 'stop'].includes(command)) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (command === 'logs') {
|
|
116
|
+
return [await optionalTextArg(deps, 'Service', 'odoo')];
|
|
117
|
+
}
|
|
118
|
+
if (command === 'psql') {
|
|
119
|
+
return [await optionalTextArg(deps, 'Database', 'postgres')];
|
|
120
|
+
}
|
|
121
|
+
if (command === 'install' || command === 'update') {
|
|
122
|
+
const modules = await moduleArg(cwd, deps);
|
|
123
|
+
const db = asString(await deps.text({
|
|
124
|
+
message: menuPromptMessage('Database (optional)', 'back'),
|
|
125
|
+
placeholder: 'devel',
|
|
126
|
+
}), '', deps);
|
|
127
|
+
return db ? [modules, db] : [modules];
|
|
128
|
+
}
|
|
129
|
+
if (command === 'test') {
|
|
130
|
+
const modules = await moduleArg(cwd, deps);
|
|
131
|
+
const db = await optionalTextArg(deps, 'Database', 'devel');
|
|
132
|
+
const mode = asString(await deps.list({
|
|
133
|
+
message: menuPromptMessage('Mode', 'back'),
|
|
134
|
+
options: [
|
|
135
|
+
{ value: 'update', label: 'update' },
|
|
136
|
+
{ value: 'init', label: 'init' },
|
|
137
|
+
],
|
|
138
|
+
initialValue: 'update',
|
|
139
|
+
}), 'update', deps);
|
|
140
|
+
const tags = asString(await deps.text({
|
|
141
|
+
message: menuPromptMessage('Tags (optional)', 'back'),
|
|
142
|
+
placeholder: '/sale',
|
|
143
|
+
}), '', deps);
|
|
144
|
+
return tags
|
|
145
|
+
? [modules, '--db', db, '--mode', mode, '--tags', tags]
|
|
146
|
+
: [modules, '--db', db, '--mode', mode];
|
|
147
|
+
}
|
|
148
|
+
if (command === 'pot') {
|
|
149
|
+
const modules = await moduleArg(cwd, deps);
|
|
150
|
+
const db = await optionalTextArg(deps, 'Database', 'devel');
|
|
151
|
+
const output = await optionalTextArg(deps, 'Output file', `i18n/${modules}.pot`);
|
|
152
|
+
return [modules, db, output];
|
|
153
|
+
}
|
|
154
|
+
if (command === 'resetdb') {
|
|
155
|
+
const db = await optionalTextArg(deps, 'Database', 'devel');
|
|
156
|
+
const modules = await optionalModules(cwd, deps);
|
|
157
|
+
return modules ? [db, modules] : [db];
|
|
158
|
+
}
|
|
159
|
+
if (command === 'snapshot') {
|
|
160
|
+
const db = await optionalTextArg(deps, 'Database', 'devel');
|
|
161
|
+
const snapshotName = await optionalTextArg(deps, 'Snapshot name', 'before-update');
|
|
162
|
+
return [db, snapshotName];
|
|
163
|
+
}
|
|
164
|
+
if (command === 'restore-snapshot') {
|
|
165
|
+
const snapshotName = requiredString(await deps.text({
|
|
166
|
+
message: menuPromptMessage('Snapshot name', 'back'),
|
|
167
|
+
validate: (value) => (value.trim() ? undefined : 'Enter the snapshot name.'),
|
|
168
|
+
}), 'Snapshot name is required.', deps);
|
|
169
|
+
const db = await optionalTextArg(deps, 'Database', 'devel');
|
|
170
|
+
return [snapshotName, db];
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isCancel, select } from '@clack/prompts';
|
|
2
|
+
import { cockpitCommands, } from './command-registry.js';
|
|
3
|
+
import { handlePromptCancel, menuPromptMessage, MenuBackSignal } from '../menu-navigation.js';
|
|
4
|
+
export const cockpitMenuBackValue = '__wpmoo_cockpit_menu_back__';
|
|
5
|
+
const categoryLabels = {
|
|
6
|
+
services: 'Services',
|
|
7
|
+
modules: 'Modules',
|
|
8
|
+
database: 'Database',
|
|
9
|
+
diagnostics: 'Diagnostics',
|
|
10
|
+
repositories: 'Repositories',
|
|
11
|
+
maintenance: 'Maintenance',
|
|
12
|
+
};
|
|
13
|
+
const topLevelOptions = [
|
|
14
|
+
{ value: 'command-palette', label: 'Command palette /' },
|
|
15
|
+
{ value: 'services', label: categoryLabels.services },
|
|
16
|
+
{ value: 'modules', label: categoryLabels.modules },
|
|
17
|
+
{ value: 'database', label: categoryLabels.database },
|
|
18
|
+
{ value: 'diagnostics', label: categoryLabels.diagnostics },
|
|
19
|
+
{ value: 'repositories', label: categoryLabels.repositories },
|
|
20
|
+
{ value: 'maintenance', label: categoryLabels.maintenance },
|
|
21
|
+
{ value: 'exit', label: 'Exit' },
|
|
22
|
+
];
|
|
23
|
+
const categories = new Set([
|
|
24
|
+
'services',
|
|
25
|
+
'modules',
|
|
26
|
+
'database',
|
|
27
|
+
'diagnostics',
|
|
28
|
+
'repositories',
|
|
29
|
+
'maintenance',
|
|
30
|
+
]);
|
|
31
|
+
function defaultSelect(options) {
|
|
32
|
+
return select(options);
|
|
33
|
+
}
|
|
34
|
+
function defaultCancelHandler(value, action) {
|
|
35
|
+
handlePromptCancel(isCancel(value), action);
|
|
36
|
+
}
|
|
37
|
+
function menuDeps(deps = {}) {
|
|
38
|
+
return {
|
|
39
|
+
select: deps.select ?? defaultSelect,
|
|
40
|
+
handleCancel: deps.handleCancel ?? defaultCancelHandler,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function isCockpitCommandCategory(value) {
|
|
44
|
+
return typeof value === 'string' && categories.has(value);
|
|
45
|
+
}
|
|
46
|
+
export async function selectCockpitTopLevelMenu(options = {}) {
|
|
47
|
+
const deps = menuDeps(options);
|
|
48
|
+
const selected = await deps.select({
|
|
49
|
+
message: 'What do you want to do?',
|
|
50
|
+
options: [...topLevelOptions],
|
|
51
|
+
initialValue: 'command-palette',
|
|
52
|
+
});
|
|
53
|
+
deps.handleCancel(selected, 'exit');
|
|
54
|
+
if (selected === 'command-palette') {
|
|
55
|
+
return { kind: 'command-palette' };
|
|
56
|
+
}
|
|
57
|
+
if (selected === 'exit') {
|
|
58
|
+
return { kind: 'exit' };
|
|
59
|
+
}
|
|
60
|
+
if (isCockpitCommandCategory(selected)) {
|
|
61
|
+
return {
|
|
62
|
+
kind: 'category',
|
|
63
|
+
category: selected,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { kind: 'exit' };
|
|
67
|
+
}
|
|
68
|
+
export async function selectCockpitCategoryCommand(category, options = {}) {
|
|
69
|
+
const deps = menuDeps(options);
|
|
70
|
+
const commands = cockpitCommands.filter((command) => command.category === category);
|
|
71
|
+
const selected = await deps.select({
|
|
72
|
+
message: menuPromptMessage(categoryLabels[category], 'back'),
|
|
73
|
+
options: [
|
|
74
|
+
...commands.map((command) => ({
|
|
75
|
+
value: command,
|
|
76
|
+
label: command.label,
|
|
77
|
+
hint: command.description,
|
|
78
|
+
})),
|
|
79
|
+
{ value: cockpitMenuBackValue, label: 'Back' },
|
|
80
|
+
],
|
|
81
|
+
initialValue: commands[0],
|
|
82
|
+
});
|
|
83
|
+
deps.handleCancel(selected, 'back');
|
|
84
|
+
if (selected === cockpitMenuBackValue) {
|
|
85
|
+
throw new MenuBackSignal();
|
|
86
|
+
}
|
|
87
|
+
return selected;
|
|
88
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts';
|
|
2
|
+
import { handlePromptCancel, menuPromptMessage } from '../menu-navigation.js';
|
|
3
|
+
function defaultHandleCancel(value, action) {
|
|
4
|
+
handlePromptCancel(isCancel(value), action);
|
|
5
|
+
}
|
|
6
|
+
function riskConfirmationMessage(command, action) {
|
|
7
|
+
return menuPromptMessage(`Run ${command.slashAlias} ${command.label}? This can change or remove environment state.`, action);
|
|
8
|
+
}
|
|
9
|
+
export async function confirmCockpitCommandRisk(command, deps = {}) {
|
|
10
|
+
if (!command.isRisky) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
const prompt = deps.confirm ?? confirm;
|
|
14
|
+
const cancelAction = deps.cancelAction ?? 'back';
|
|
15
|
+
const approved = await prompt({
|
|
16
|
+
message: riskConfirmationMessage(command, cancelAction),
|
|
17
|
+
initialValue: false,
|
|
18
|
+
});
|
|
19
|
+
const handleCancel = deps.handleCancel ?? defaultHandleCancel;
|
|
20
|
+
handleCancel(approved, cancelAction);
|
|
21
|
+
return approved === true;
|
|
22
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -67,6 +67,12 @@ Daily actions:
|
|
|
67
67
|
Generated environments also include ./moo for local compose commands such as ./moo start.
|
|
68
68
|
Use ./moo or npx @wpmoo/odoo with the same daily action arguments.
|
|
69
69
|
|
|
70
|
+
Cockpit:
|
|
71
|
+
Run npx @wpmoo/odoo inside a generated environment to open the cockpit.
|
|
72
|
+
Use Command palette / to search slash commands across services, modules, database,
|
|
73
|
+
diagnostics, repositories, and maintenance categories.
|
|
74
|
+
Direct commands such as npx @wpmoo/odoo status and npx @wpmoo/odoo test remain available.
|
|
75
|
+
|
|
70
76
|
Status and doctor:
|
|
71
77
|
status: fast and offline. Reads local environment metadata and files only.
|
|
72
78
|
doctor: deeper health check. May check Docker CLI access and GitHub workflows.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wpmoo/odoo",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.48",
|
|
4
4
|
"description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"docs/assets"
|
|
35
35
|
],
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=20"
|
|
37
|
+
"node": ">=20.17"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@clack/prompts": "^0.11.0",
|
|
50
|
+
"@inquirer/search": "^4.1.9",
|
|
50
51
|
"execa": "^9.6.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|