@wpmoo/odoo 0.8.67 → 0.8.69
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 +9 -7
- package/dist/cli.js +69 -75
- package/dist/cockpit/command-palette.js +7 -3
- package/dist/cockpit/daily-prompts.js +5 -5
- package/dist/cockpit/menu.js +56 -48
- package/dist/cockpit/safety.js +3 -3
- package/dist/daily-actions.js +1 -1
- package/dist/help.js +1 -1
- package/dist/prompt-repositories.js +5 -5
- package/dist/prompts/index.js +149 -0
- package/dist/templates.js +4 -9
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-

|
|
2
2
|
|
|
3
|
-
[](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://codecov.io/gh/wpmoo-org/wpmoo-odoo) [](LICENSE) [](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://codecov.io/gh/wpmoo-org/wpmoo-odoo) [](LICENSE) [](https://github.com/wpmoo-org/wpmoo-odoo) [](https://www.buymeacoffee.com/cangir) [](https://www.patreon.com/wpmoo)
|
|
4
4
|
|
|
5
|
-
# WPMoo
|
|
5
|
+
# WPMoo Tool
|
|
6
6
|
|
|
7
|
-
WPMoo
|
|
7
|
+
WPMoo Tool is a development-first CLI for creating and operating Docker Compose based environments for Odoo, with source repositories managed as Git submodules.
|
|
8
8
|
|
|
9
9
|
It gives Odoo teams a repeatable environment layout, a guided cockpit for daily work, direct commands for automation, and recovery tools that refresh generated files without touching product source code.
|
|
10
10
|
|
|
11
|
+
WPMoo Tool is an independent project and is not affiliated with, endorsed by, or sponsored by Odoo S.A. Odoo is a trademark of Odoo S.A.
|
|
12
|
+
|
|
11
13
|
## Development Status
|
|
12
14
|
|
|
13
15
|
> [!IMPORTANT]
|
|
14
|
-
> **Pre-1.0 active development:** WPMoo
|
|
16
|
+
> **Pre-1.0 active development:** WPMoo Tool has not reached `1.0.0` yet. Until the `1.0.0` release, use it as a preview tool for evaluation, local trials, and feedback rather than a dependency for critical production workflows. Setup conventions and command behavior may still change between pre-1.0 releases.
|
|
15
17
|
|
|
16
|
-
## Why WPMoo
|
|
18
|
+
## Why WPMoo Tool
|
|
17
19
|
|
|
18
20
|
- Create a local Odoo development environment from a dev repository and one or more source repositories.
|
|
19
21
|
- Keep product source repositories under `odoo/custom/src/private`, `odoo/custom/src/oca`, or `odoo/custom/src/external` as Git submodules pinned to the selected Odoo branch.
|
|
@@ -423,7 +425,7 @@ npx @wpmoo/odoo doctor --fix
|
|
|
423
425
|
|
|
424
426
|
## External Resources
|
|
425
427
|
|
|
426
|
-
WPMoo
|
|
428
|
+
WPMoo Tool keeps the package small by copying external resources into generated environments:
|
|
427
429
|
|
|
428
430
|
```text
|
|
429
431
|
gh:wpmoo-org/odoo-docker-compose
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts';
|
|
3
2
|
import { realpathSync } from 'node:fs';
|
|
4
3
|
import { basename, relative, resolve } from 'node:path';
|
|
5
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
5
|
import { commandFromArgs, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
|
|
7
|
-
import { selectCockpitCommandFromPalette } from './cockpit/command-palette.js';
|
|
8
6
|
import { collectDailyActionArgs } from './cockpit/daily-prompts.js';
|
|
9
|
-
import {
|
|
7
|
+
import { selectCockpitTopLevelMenu } from './cockpit/menu.js';
|
|
10
8
|
import { confirmCockpitCommandRisk } from './cockpit/safety.js';
|
|
11
9
|
import { detectDevelopmentEnvironment } from './environment.js';
|
|
12
10
|
import { commandOdooVersion } from './environment-version.js';
|
|
@@ -25,6 +23,7 @@ import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
|
|
|
25
23
|
import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
|
|
26
24
|
import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, repositoryPreflightAvailable, } from './repository-preflight.js';
|
|
27
25
|
import { scaffold } from './scaffold.js';
|
|
26
|
+
import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, selectPrompt, textPrompt } from './prompts/index.js';
|
|
28
27
|
import { renderBanner } from './templates.js';
|
|
29
28
|
import { checkForUpdate, installLatestPackage, isUpdateCheckSkipped, restartCli } from './update-check.js';
|
|
30
29
|
import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
|
|
@@ -33,11 +32,11 @@ import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, real
|
|
|
33
32
|
import { environmentGitHubOwner } from './environment-context.js';
|
|
34
33
|
import { handlePromptCancel, handleUnavailableMenuChoice, installPromptCancelKeyTracker, isMenuBackSignal, MenuBackSignal, menuIntroTitle, menuPromptMessage, } from './menu-navigation.js';
|
|
35
34
|
function handleCancel(value, action) {
|
|
36
|
-
handlePromptCancel(
|
|
35
|
+
handlePromptCancel(isPromptCancel(value), action);
|
|
37
36
|
}
|
|
38
37
|
function showSubmenuIntro(title, showIntro, cancelAction) {
|
|
39
38
|
if (showIntro) {
|
|
40
|
-
|
|
39
|
+
introPrompt(menuIntroTitle(title, cancelAction));
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
function asString(value, fallback, cancelAction = 'exit') {
|
|
@@ -59,7 +58,7 @@ async function selectDefaultGitHubOwner(cancelAction = 'exit', preferredOwner) {
|
|
|
59
58
|
const initialValue = accounts.some((account) => account.login === preferredOwner)
|
|
60
59
|
? preferredOwner
|
|
61
60
|
: accounts[0].login;
|
|
62
|
-
const selectedOwner = await
|
|
61
|
+
const selectedOwner = await selectPrompt({
|
|
63
62
|
message: 'GitHub account/organization',
|
|
64
63
|
options: accounts.map((account) => ({
|
|
65
64
|
value: account.login,
|
|
@@ -155,7 +154,7 @@ async function showStartup(argv, skipUpdateCheck) {
|
|
|
155
154
|
const updateCheck = await checkForUpdate(packageName(), packageVersion());
|
|
156
155
|
console.log(renderVersionTag(updateCheck.status === 'update-available' ? updateCheck.latestVersion : undefined));
|
|
157
156
|
if (updateCheck.status === 'update-available') {
|
|
158
|
-
const shouldUpdate = await
|
|
157
|
+
const shouldUpdate = await confirmPrompt({
|
|
159
158
|
message: `Update to v.${updateCheck.latestVersion}? (Y/n)`,
|
|
160
159
|
active: 'Y',
|
|
161
160
|
inactive: 'n',
|
|
@@ -184,28 +183,25 @@ async function selectCockpitCommandFromMenu() {
|
|
|
184
183
|
if (selection.kind === 'exit') {
|
|
185
184
|
return 'exit';
|
|
186
185
|
}
|
|
187
|
-
|
|
188
|
-
return selectCockpitCommandFromPalette();
|
|
189
|
-
}
|
|
190
|
-
return selectCockpitCategoryCommand(selection.category);
|
|
186
|
+
return selection.command;
|
|
191
187
|
}
|
|
192
188
|
async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
193
189
|
if (showIntro) {
|
|
194
|
-
|
|
190
|
+
introPrompt('Create Odoo dev environment');
|
|
195
191
|
}
|
|
196
|
-
const product = asString(await
|
|
192
|
+
const product = asString(await textPrompt({
|
|
197
193
|
message: 'Product slug',
|
|
198
194
|
placeholder: 'odoo_sample_module',
|
|
199
195
|
validate: (value) => (value.trim() ? undefined : 'Enter a product/module slug.'),
|
|
200
196
|
}), 'odoo_sample_module', cancelAction);
|
|
201
197
|
const defaultTarget = `./${product}_dev`;
|
|
202
|
-
const target = resolve(asString(await
|
|
198
|
+
const target = resolve(asString(await textPrompt({
|
|
203
199
|
message: 'Environment folder',
|
|
204
200
|
placeholder: defaultTarget,
|
|
205
201
|
defaultValue: defaultTarget,
|
|
206
202
|
initialValue: defaultTarget,
|
|
207
203
|
}), defaultTarget, cancelAction));
|
|
208
|
-
const connectGitHub = await
|
|
204
|
+
const connectGitHub = await selectPrompt({
|
|
209
205
|
message: 'Connect this environment to Git/GitHub now?',
|
|
210
206
|
options: [
|
|
211
207
|
{ value: true, label: 'Yes, connect Git/GitHub repositories' },
|
|
@@ -216,10 +212,10 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
216
212
|
handleCancel(connectGitHub, cancelAction);
|
|
217
213
|
let selectedGitHubOwner;
|
|
218
214
|
if (connectGitHub) {
|
|
219
|
-
|
|
215
|
+
notePrompt(renderRepositorySetupNote(product), 'Repository setup');
|
|
220
216
|
selectedGitHubOwner = await selectDefaultGitHubOwner(cancelAction);
|
|
221
217
|
}
|
|
222
|
-
const selectedVersion = await
|
|
218
|
+
const selectedVersion = await selectPrompt({
|
|
223
219
|
message: menuPromptMessage('Odoo version', cancelAction),
|
|
224
220
|
options: supportedOdooVersions.map((version) => ({ value: version, label: version })),
|
|
225
221
|
initialValue: supportedOdooVersions[0],
|
|
@@ -227,7 +223,7 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
227
223
|
handleCancel(selectedVersion, cancelAction);
|
|
228
224
|
const odooVersion = String(selectedVersion);
|
|
229
225
|
async function promptInstallAgentSkills() {
|
|
230
|
-
const installAgentSkills = await
|
|
226
|
+
const installAgentSkills = await selectPrompt({
|
|
231
227
|
message: 'Install project-local Odoo Agent Skills?',
|
|
232
228
|
options: [
|
|
233
229
|
{ value: true, label: 'Yes, install latest default skills' },
|
|
@@ -287,7 +283,7 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
287
283
|
path: sourcePath,
|
|
288
284
|
addons: [sourcePath],
|
|
289
285
|
});
|
|
290
|
-
const shouldAddAnother = await
|
|
286
|
+
const shouldAddAnother = await selectPrompt({
|
|
291
287
|
message: 'Add another source repo?',
|
|
292
288
|
options: [
|
|
293
289
|
{ value: false, label: 'No' },
|
|
@@ -299,7 +295,7 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
|
299
295
|
addAnother = Boolean(shouldAddAnother);
|
|
300
296
|
}
|
|
301
297
|
const installAgentSkills = await promptInstallAgentSkills();
|
|
302
|
-
const initEmpty = await
|
|
298
|
+
const initEmpty = await selectPrompt({
|
|
303
299
|
message: 'Initialize repositories that exist but have no commits?',
|
|
304
300
|
options: [
|
|
305
301
|
{ value: true, label: 'Yes, create the selected Odoo branch' },
|
|
@@ -348,12 +344,12 @@ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit'
|
|
|
348
344
|
const preferredOwner = await environmentGitHubOwner(target);
|
|
349
345
|
const selectedOwner = await selectDefaultGitHubOwner(cancelAction, preferredOwner);
|
|
350
346
|
const owner = selectedOwner ??
|
|
351
|
-
asString(await
|
|
347
|
+
asString(await textPrompt({
|
|
352
348
|
message: menuPromptMessage('GitHub owner/organization', cancelAction),
|
|
353
349
|
placeholder: 'example-org',
|
|
354
350
|
validate: (value) => (value.trim() ? undefined : 'Enter a GitHub owner or organization.'),
|
|
355
351
|
}), 'example-org', cancelAction);
|
|
356
|
-
const repoName = asString(await
|
|
352
|
+
const repoName = asString(await textPrompt({
|
|
357
353
|
message: menuPromptMessage('Source repo name', cancelAction),
|
|
358
354
|
placeholder: 'odoo_sample_module_repo',
|
|
359
355
|
validate: validateRepoName,
|
|
@@ -370,7 +366,7 @@ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit'
|
|
|
370
366
|
}
|
|
371
367
|
async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
|
|
372
368
|
if (!(await repositoryPreflightAvailable())) {
|
|
373
|
-
|
|
369
|
+
notePrompt([
|
|
374
370
|
'GitHub CLI (`gh`) is not available or not authenticated.',
|
|
375
371
|
'The source repo will be used as-is. If it does not exist, create it first or authenticate gh.',
|
|
376
372
|
].join('\n'), 'Repository check skipped');
|
|
@@ -380,8 +376,8 @@ async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
|
|
|
380
376
|
if (status.status !== 'inaccessible') {
|
|
381
377
|
return;
|
|
382
378
|
}
|
|
383
|
-
|
|
384
|
-
const shouldCreate = await
|
|
379
|
+
notePrompt(`Source repo is not accessible: ${status.slug}`, 'Repository check');
|
|
380
|
+
const shouldCreate = await selectPrompt({
|
|
385
381
|
message: 'Create this source repository with GitHub CLI?',
|
|
386
382
|
options: [
|
|
387
383
|
{ value: true, label: 'Yes, create it' },
|
|
@@ -393,7 +389,7 @@ async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
|
|
|
393
389
|
if (!shouldCreate) {
|
|
394
390
|
throw new Error(`Source repository is not accessible: ${status.slug}`);
|
|
395
391
|
}
|
|
396
|
-
const visibility = await
|
|
392
|
+
const visibility = await selectPrompt({
|
|
397
393
|
message: 'Visibility for new repository',
|
|
398
394
|
options: [
|
|
399
395
|
{ value: 'private', label: 'Private' },
|
|
@@ -417,12 +413,12 @@ async function selectSourceRepo(target, cancelAction = 'exit') {
|
|
|
417
413
|
}));
|
|
418
414
|
if (repoOptions.length === 0) {
|
|
419
415
|
if (cancelAction === 'back') {
|
|
420
|
-
|
|
416
|
+
notePrompt(`No source repos found under ${target}/odoo/custom/src.\nNext: choose "Add source repo" first.`, 'Nothing to select');
|
|
421
417
|
handleUnavailableMenuChoice(cancelAction);
|
|
422
418
|
}
|
|
423
419
|
throw new Error(`No source repos found under ${target}/odoo/custom/src`);
|
|
424
420
|
}
|
|
425
|
-
const selected = await
|
|
421
|
+
const selected = await selectPrompt({
|
|
426
422
|
message: menuPromptMessage('Source repo', cancelAction),
|
|
427
423
|
options: repoOptions,
|
|
428
424
|
initialValue: repoOptions[0].value,
|
|
@@ -463,7 +459,7 @@ async function addModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exi
|
|
|
463
459
|
showSubmenuIntro('Add module to source repo', showIntro, cancelAction);
|
|
464
460
|
const target = process.cwd();
|
|
465
461
|
const sourceRepo = await selectSourceRepo(target, cancelAction);
|
|
466
|
-
const moduleName = asString(await
|
|
462
|
+
const moduleName = asString(await textPrompt({
|
|
467
463
|
message: menuPromptMessage('Module name', cancelAction),
|
|
468
464
|
placeholder: suggestedModuleName(sourceRepo.repoPath),
|
|
469
465
|
validate: (value) => (value.trim() ? undefined : 'Enter the module technical name.'),
|
|
@@ -555,7 +551,7 @@ async function runSourceCommand(argv) {
|
|
|
555
551
|
return;
|
|
556
552
|
}
|
|
557
553
|
console.log(renderBanner());
|
|
558
|
-
|
|
554
|
+
outroPrompt(`Synced source manifest in ${options.target}.`);
|
|
559
555
|
return;
|
|
560
556
|
}
|
|
561
557
|
if (subcommand === 'add') {
|
|
@@ -565,7 +561,7 @@ async function runSourceCommand(argv) {
|
|
|
565
561
|
}
|
|
566
562
|
console.log(renderBanner());
|
|
567
563
|
await addModuleRepo(options);
|
|
568
|
-
|
|
564
|
+
outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
|
|
569
565
|
return;
|
|
570
566
|
}
|
|
571
567
|
if (subcommand === 'remove') {
|
|
@@ -575,14 +571,14 @@ async function runSourceCommand(argv) {
|
|
|
575
571
|
}
|
|
576
572
|
console.log(renderBanner());
|
|
577
573
|
await removeModuleRepo(options);
|
|
578
|
-
|
|
574
|
+
outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
|
|
579
575
|
return;
|
|
580
576
|
}
|
|
581
577
|
throw new Error(sourceUsage());
|
|
582
578
|
}
|
|
583
579
|
async function confirmSafeResetFromMenu(options) {
|
|
584
|
-
|
|
585
|
-
const confirmed = await
|
|
580
|
+
notePrompt(renderSafeResetPreview(options.target, options.stage), 'Safe reset preview');
|
|
581
|
+
const confirmed = await confirmPrompt({
|
|
586
582
|
message: menuPromptMessage('Continue with safe reset?', 'back'),
|
|
587
583
|
active: 'Yes',
|
|
588
584
|
inactive: 'No',
|
|
@@ -600,12 +596,12 @@ async function removeRepoOptionsFromPrompts(argv, showIntro = true, cancelAction
|
|
|
600
596
|
const repos = await listModuleRepos(target);
|
|
601
597
|
if (repos.length === 0) {
|
|
602
598
|
if (cancelAction === 'back') {
|
|
603
|
-
|
|
599
|
+
notePrompt(`No module submodules found under ${target}/odoo/custom/src/private.\nNext: choose "Add source repo" first.`, 'Nothing to remove');
|
|
604
600
|
handleUnavailableMenuChoice(cancelAction);
|
|
605
601
|
}
|
|
606
602
|
throw new Error(`No module submodules found under ${target}/odoo/custom/src/private`);
|
|
607
603
|
}
|
|
608
|
-
const repoPath = await
|
|
604
|
+
const repoPath = await selectPrompt({
|
|
609
605
|
message: menuPromptMessage('Repo to remove', cancelAction),
|
|
610
606
|
options: repos.map((repo) => ({ value: repo, label: repo })),
|
|
611
607
|
initialValue: repos[0],
|
|
@@ -640,18 +636,18 @@ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = '
|
|
|
640
636
|
const modules = await listModulesInSourceRepo(target, sourceRepo.repoPath, sourceRepo.sourceType);
|
|
641
637
|
if (modules.length === 0) {
|
|
642
638
|
if (cancelAction === 'back') {
|
|
643
|
-
|
|
639
|
+
notePrompt(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
|
|
644
640
|
handleUnavailableMenuChoice(cancelAction);
|
|
645
641
|
}
|
|
646
642
|
throw new Error(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}`);
|
|
647
643
|
}
|
|
648
|
-
const moduleName = await
|
|
644
|
+
const moduleName = await selectPrompt({
|
|
649
645
|
message: menuPromptMessage('Module to remove', cancelAction),
|
|
650
646
|
options: modules.map((module) => ({ value: module, label: module })),
|
|
651
647
|
initialValue: modules[0],
|
|
652
648
|
});
|
|
653
649
|
handleCancel(moduleName, cancelAction);
|
|
654
|
-
const deleteFiles = await
|
|
650
|
+
const deleteFiles = await confirmPrompt({
|
|
655
651
|
message: menuPromptMessage('Delete module files too? (y/N)', cancelAction),
|
|
656
652
|
active: 'Y',
|
|
657
653
|
inactive: 'n',
|
|
@@ -689,13 +685,13 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
689
685
|
throw new Error(message);
|
|
690
686
|
}
|
|
691
687
|
if (interactive) {
|
|
692
|
-
|
|
688
|
+
notePrompt(message, 'Repository check skipped');
|
|
693
689
|
}
|
|
694
690
|
return;
|
|
695
691
|
}
|
|
696
692
|
const { accessible, inaccessible: missing } = await checkGitHubRepositories(options);
|
|
697
693
|
if (interactive && accessible.length > 0) {
|
|
698
|
-
|
|
694
|
+
notePrompt([
|
|
699
695
|
'These GitHub repositories already exist and are accessible:',
|
|
700
696
|
'',
|
|
701
697
|
...accessible.map((repository) => `- ${repository.label}: ${repository.slug}`),
|
|
@@ -711,12 +707,12 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
711
707
|
await createGitHubRepositories(missing, options.repoVisibility ?? 'private');
|
|
712
708
|
return;
|
|
713
709
|
}
|
|
714
|
-
|
|
710
|
+
notePrompt([
|
|
715
711
|
'These GitHub repositories are not accessible. They may not exist, or your account may not have access:',
|
|
716
712
|
'',
|
|
717
713
|
missingList,
|
|
718
714
|
].join('\n'), 'Repository check');
|
|
719
|
-
const shouldCreate = await
|
|
715
|
+
const shouldCreate = await selectPrompt({
|
|
720
716
|
message: 'Create the inaccessible repositories with GitHub CLI?',
|
|
721
717
|
options: [
|
|
722
718
|
{ value: true, label: 'Yes, create them' },
|
|
@@ -724,12 +720,11 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
724
720
|
],
|
|
725
721
|
initialValue: true,
|
|
726
722
|
});
|
|
727
|
-
|
|
728
|
-
process.exit(1);
|
|
723
|
+
handleCancel(shouldCreate, 'exit');
|
|
729
724
|
if (!shouldCreate) {
|
|
730
725
|
throw new Error(['Required repositories are not accessible. Create them first:', '', ...manualCreateCommands(missing)].join('\n'));
|
|
731
726
|
}
|
|
732
|
-
const visibility = await
|
|
727
|
+
const visibility = await selectPrompt({
|
|
733
728
|
message: 'Visibility for new repositories',
|
|
734
729
|
options: [
|
|
735
730
|
{ value: 'private', label: 'Private' },
|
|
@@ -737,8 +732,7 @@ async function ensureGitHubRepositories(options, interactive) {
|
|
|
737
732
|
],
|
|
738
733
|
initialValue: 'private',
|
|
739
734
|
});
|
|
740
|
-
|
|
741
|
-
process.exit(1);
|
|
735
|
+
handleCancel(visibility, 'exit');
|
|
742
736
|
await createGitHubRepositories(missing, visibility);
|
|
743
737
|
}
|
|
744
738
|
async function runCockpitCommand(command, cwd) {
|
|
@@ -748,62 +742,62 @@ async function runCockpitCommand(command, cwd) {
|
|
|
748
742
|
if (command.target.kind === 'daily') {
|
|
749
743
|
const argv = await collectDailyActionArgs(command.target.command, cwd);
|
|
750
744
|
if (!(await confirmCockpitCommandRisk(command))) {
|
|
751
|
-
|
|
745
|
+
notePrompt(`${command.slashAlias} was not run.`, 'Action skipped');
|
|
752
746
|
return 'continue';
|
|
753
747
|
}
|
|
754
748
|
await runDailyAction(command.target.command, argv, cwd);
|
|
755
|
-
|
|
749
|
+
notePrompt(`${command.slashAlias} completed.`, 'Done');
|
|
756
750
|
return 'continue';
|
|
757
751
|
}
|
|
758
752
|
if (command.id === 'status') {
|
|
759
|
-
|
|
753
|
+
notePrompt(await renderEnvironmentStatusForTarget(cwd), 'Environment status');
|
|
760
754
|
return 'continue';
|
|
761
755
|
}
|
|
762
756
|
if (command.id === 'doctor') {
|
|
763
|
-
|
|
757
|
+
notePrompt(await runDoctor(cwd), 'Doctor');
|
|
764
758
|
return 'continue';
|
|
765
759
|
}
|
|
766
760
|
if (command.id === 'add-repo') {
|
|
767
761
|
const options = await addRepoOptionsFromPrompts(false, 'back');
|
|
768
762
|
await ensureAddRepoGitHubRepository(options, 'back');
|
|
769
763
|
await addModuleRepo(options);
|
|
770
|
-
|
|
764
|
+
notePrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private')}.`, 'Done');
|
|
771
765
|
return 'continue';
|
|
772
766
|
}
|
|
773
767
|
if (command.id === 'remove-repo') {
|
|
774
768
|
const options = await removeRepoOptionsFromPrompts([], false, 'back');
|
|
775
769
|
if (!(await confirmCockpitCommandRisk(command))) {
|
|
776
|
-
|
|
770
|
+
notePrompt(`Source repo ${options.repoPath} was not removed.`, 'Action skipped');
|
|
777
771
|
return 'continue';
|
|
778
772
|
}
|
|
779
773
|
await removeModuleRepo(options);
|
|
780
|
-
|
|
774
|
+
notePrompt(`Removed source repo ${options.repoPath} from ${options.target}.`, 'Done');
|
|
781
775
|
return 'continue';
|
|
782
776
|
}
|
|
783
777
|
if (command.id === 'add-module') {
|
|
784
778
|
const options = await addModuleOptionsFromPrompts(false, 'back');
|
|
785
779
|
await addModuleToSourceRepo(options);
|
|
786
|
-
|
|
780
|
+
notePrompt(`Added module ${options.moduleName} under source repo ${options.repoPath}.`, 'Done');
|
|
787
781
|
return 'continue';
|
|
788
782
|
}
|
|
789
783
|
if (command.id === 'remove-module') {
|
|
790
784
|
const options = await removeModuleOptionsFromPrompts(false, 'back');
|
|
791
785
|
if (!(await confirmCockpitCommandRisk(command))) {
|
|
792
|
-
|
|
786
|
+
notePrompt(`Module ${options.moduleName} was not removed.`, 'Action skipped');
|
|
793
787
|
return 'continue';
|
|
794
788
|
}
|
|
795
789
|
await removeModuleFromSourceRepo(options);
|
|
796
|
-
|
|
790
|
+
notePrompt(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`, 'Done');
|
|
797
791
|
return 'continue';
|
|
798
792
|
}
|
|
799
793
|
if (command.id === 'safe-reset') {
|
|
800
794
|
const options = { target: cwd, stage: true };
|
|
801
795
|
await confirmSafeResetFromMenu(options);
|
|
802
796
|
await safeResetEnvironment(options);
|
|
803
|
-
|
|
797
|
+
notePrompt(`Safe reset refreshed generated environment files in ${cwd}.`, 'Done');
|
|
804
798
|
return 'continue';
|
|
805
799
|
}
|
|
806
|
-
|
|
800
|
+
notePrompt(`Unknown cockpit command: ${command.slashAlias}`, 'No action');
|
|
807
801
|
return 'continue';
|
|
808
802
|
}
|
|
809
803
|
export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd()) {
|
|
@@ -827,15 +821,15 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
827
821
|
const resolvedOptions = await optionsFromPrompts();
|
|
828
822
|
await ensureGitHubRepositories(resolvedOptions, true);
|
|
829
823
|
await scaffold(resolvedOptions);
|
|
830
|
-
|
|
831
|
-
|
|
824
|
+
notePrompt(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
|
|
825
|
+
outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
832
826
|
return;
|
|
833
827
|
}
|
|
834
|
-
|
|
828
|
+
introPrompt('WPMoo Tool');
|
|
835
829
|
while (true) {
|
|
836
830
|
try {
|
|
837
831
|
const status = await getEnvironmentStatus(cwd);
|
|
838
|
-
|
|
832
|
+
notePrompt(renderEnvironmentStatusSummary(status), 'Environment status');
|
|
839
833
|
const command = await selectCockpitCommandFromMenu();
|
|
840
834
|
if (command === 'exit') {
|
|
841
835
|
return;
|
|
@@ -858,14 +852,14 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
858
852
|
if (options) {
|
|
859
853
|
console.log(renderBanner());
|
|
860
854
|
await addModuleRepo(options);
|
|
861
|
-
|
|
855
|
+
outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
|
|
862
856
|
return;
|
|
863
857
|
}
|
|
864
858
|
await showStartup(argv, skipUpdateCheck);
|
|
865
859
|
const promptedOptions = await addRepoOptionsFromPrompts();
|
|
866
860
|
await ensureAddRepoGitHubRepository(promptedOptions);
|
|
867
861
|
await addModuleRepo(promptedOptions);
|
|
868
|
-
|
|
862
|
+
outroPrompt(`Added source repo under ${promptedOptions.target}/odoo/custom/src/private.`);
|
|
869
863
|
return;
|
|
870
864
|
}
|
|
871
865
|
if (route.command === 'remove-repo') {
|
|
@@ -873,13 +867,13 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
873
867
|
if (options) {
|
|
874
868
|
console.log(renderBanner());
|
|
875
869
|
await removeModuleRepo(options);
|
|
876
|
-
|
|
870
|
+
outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
|
|
877
871
|
return;
|
|
878
872
|
}
|
|
879
873
|
await showStartup(argv, skipUpdateCheck);
|
|
880
874
|
const promptedOptions = await removeRepoOptionsFromPrompts(route.argv);
|
|
881
875
|
await removeModuleRepo(promptedOptions);
|
|
882
|
-
|
|
876
|
+
outroPrompt(`Removed source repo ${promptedOptions.repoPath} from ${promptedOptions.target}.`);
|
|
883
877
|
return;
|
|
884
878
|
}
|
|
885
879
|
if (route.command === 'source') {
|
|
@@ -891,13 +885,13 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
891
885
|
if (options) {
|
|
892
886
|
console.log(renderBanner());
|
|
893
887
|
await addModuleToSourceRepo(options);
|
|
894
|
-
|
|
888
|
+
outroPrompt(`Added module ${options.moduleName} under source repo ${options.repoPath}.`);
|
|
895
889
|
return;
|
|
896
890
|
}
|
|
897
891
|
await showStartup(argv, skipUpdateCheck);
|
|
898
892
|
const promptedOptions = await addModuleOptionsFromPrompts();
|
|
899
893
|
await addModuleToSourceRepo(promptedOptions);
|
|
900
|
-
|
|
894
|
+
outroPrompt(`Added module ${promptedOptions.moduleName} under source repo ${promptedOptions.repoPath}.`);
|
|
901
895
|
return;
|
|
902
896
|
}
|
|
903
897
|
if (route.command === 'remove-module') {
|
|
@@ -905,13 +899,13 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
905
899
|
if (options) {
|
|
906
900
|
console.log(renderBanner());
|
|
907
901
|
await removeModuleFromSourceRepo(options);
|
|
908
|
-
|
|
902
|
+
outroPrompt(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
|
|
909
903
|
return;
|
|
910
904
|
}
|
|
911
905
|
await showStartup(argv, skipUpdateCheck);
|
|
912
906
|
const promptedOptions = await removeModuleOptionsFromPrompts();
|
|
913
907
|
await removeModuleFromSourceRepo(promptedOptions);
|
|
914
|
-
|
|
908
|
+
outroPrompt(`Removed module ${promptedOptions.moduleName} from source repo ${promptedOptions.repoPath}.`);
|
|
915
909
|
return;
|
|
916
910
|
}
|
|
917
911
|
if (route.command === 'reset') {
|
|
@@ -923,7 +917,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
923
917
|
}
|
|
924
918
|
const resetOptions = { target: options.target, stage: options.stage };
|
|
925
919
|
await safeResetEnvironment(resetOptions);
|
|
926
|
-
|
|
920
|
+
outroPrompt(`Safe reset refreshed generated environment files in ${options.target}.`);
|
|
927
921
|
return;
|
|
928
922
|
}
|
|
929
923
|
if (route.command === 'doctor') {
|
|
@@ -978,8 +972,8 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
978
972
|
console.log(`- ${command}`);
|
|
979
973
|
return;
|
|
980
974
|
}
|
|
981
|
-
|
|
982
|
-
|
|
975
|
+
notePrompt(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
|
|
976
|
+
outroPrompt(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
|
|
983
977
|
}
|
|
984
978
|
export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
|
|
985
979
|
if (!argvPath)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isPromptCancel, searchPrompt } from '../prompts/index.js';
|
|
2
2
|
import { searchCockpitCommands } from './command-registry.js';
|
|
3
|
-
const defaultSearchPrompt = (config) =>
|
|
3
|
+
const defaultSearchPrompt = (config) => searchPrompt(config);
|
|
4
4
|
function commandChoice(command) {
|
|
5
5
|
return {
|
|
6
6
|
value: command,
|
|
@@ -11,9 +11,13 @@ function commandChoice(command) {
|
|
|
11
11
|
}
|
|
12
12
|
export async function selectCockpitCommandFromPalette(options = {}) {
|
|
13
13
|
const prompt = options.prompt ?? defaultSearchPrompt;
|
|
14
|
-
|
|
14
|
+
const selected = await prompt({
|
|
15
15
|
message: 'Search commands',
|
|
16
16
|
pageSize: 10,
|
|
17
17
|
source: (term) => searchCockpitCommands(term).map(commandChoice),
|
|
18
18
|
});
|
|
19
|
+
if (isPromptCancel(selected)) {
|
|
20
|
+
throw new Error('Prompt was canceled.');
|
|
21
|
+
}
|
|
22
|
+
return selected;
|
|
19
23
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { isCancel, select, text } from '@clack/prompts';
|
|
2
1
|
import { listModulesInSourceRepo } from '../module-actions.js';
|
|
3
2
|
import { listModuleRepos } from '../repo-actions.js';
|
|
4
3
|
import { listSources } from '../source-actions.js';
|
|
5
4
|
import { handlePromptCancel, menuPromptMessage, } from '../menu-navigation.js';
|
|
5
|
+
import { isPromptCancel, selectPrompt, textPrompt } from '../prompts/index.js';
|
|
6
6
|
const manualModuleValue = '__wpmoo_manual_module_entry__';
|
|
7
7
|
function defaultCancelHandler(value, action) {
|
|
8
|
-
handlePromptCancel(
|
|
8
|
+
handlePromptCancel(isPromptCancel(value), action);
|
|
9
9
|
}
|
|
10
10
|
function promptDeps(deps = {}) {
|
|
11
11
|
return {
|
|
12
|
-
select: deps.select ?? ((options) =>
|
|
13
|
-
text: deps.text ?? ((options) =>
|
|
14
|
-
list: deps.list ?? ((options) =>
|
|
12
|
+
select: deps.select ?? ((options) => selectPrompt(options)),
|
|
13
|
+
text: deps.text ?? ((options) => textPrompt(options)),
|
|
14
|
+
list: deps.list ?? ((options) => selectPrompt(options)),
|
|
15
15
|
handleCancel: deps.handleCancel ?? defaultCancelHandler,
|
|
16
16
|
};
|
|
17
17
|
}
|
package/dist/cockpit/menu.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
2
|
import { cockpitCommands, } from './command-registry.js';
|
|
3
|
-
import { handlePromptCancel
|
|
4
|
-
|
|
3
|
+
import { handlePromptCancel } from '../menu-navigation.js';
|
|
4
|
+
import { isPromptCancel, promptSeparator, selectPrompt, } from '../prompts/index.js';
|
|
5
5
|
const categoryLabels = {
|
|
6
6
|
services: 'Services',
|
|
7
7
|
modules: 'Modules',
|
|
@@ -10,29 +10,59 @@ const categoryLabels = {
|
|
|
10
10
|
repositories: 'Repositories',
|
|
11
11
|
maintenance: 'Maintenance',
|
|
12
12
|
};
|
|
13
|
-
const
|
|
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([
|
|
13
|
+
const topLevelCategoryOrder = [
|
|
24
14
|
'services',
|
|
25
15
|
'modules',
|
|
26
16
|
'database',
|
|
27
17
|
'diagnostics',
|
|
28
18
|
'repositories',
|
|
29
19
|
'maintenance',
|
|
30
|
-
]
|
|
20
|
+
];
|
|
21
|
+
const topLevelCommands = topLevelCategoryOrder.flatMap((category) => cockpitCommands.filter((command) => command.category === category && command.id !== 'exit'));
|
|
22
|
+
const topLevelCommandLabelWidth = Math.max(...topLevelCommands.map((command) => command.label.length));
|
|
23
|
+
function color(format, value) {
|
|
24
|
+
return styleText(format, value, { validateStream: false });
|
|
25
|
+
}
|
|
26
|
+
function categoryHeading(category) {
|
|
27
|
+
return color('white', categoryLabels[category]);
|
|
28
|
+
}
|
|
29
|
+
function commandName(command) {
|
|
30
|
+
return `${color('yellow', ` ${command.label.padEnd(topLevelCommandLabelWidth)}`)}${color('dim', ` ${command.description}`)}`;
|
|
31
|
+
}
|
|
32
|
+
function categoryChoices(category, index) {
|
|
33
|
+
const choices = [
|
|
34
|
+
promptSeparator(categoryHeading(category)),
|
|
35
|
+
...topLevelCommands
|
|
36
|
+
.filter((command) => command.category === category)
|
|
37
|
+
.map((command) => ({
|
|
38
|
+
value: command,
|
|
39
|
+
name: commandName(command),
|
|
40
|
+
short: command.label,
|
|
41
|
+
})),
|
|
42
|
+
];
|
|
43
|
+
if (index < topLevelCategoryOrder.length - 1) {
|
|
44
|
+
choices.push(promptSeparator(' '));
|
|
45
|
+
}
|
|
46
|
+
return choices;
|
|
47
|
+
}
|
|
48
|
+
const topLevelChoices = [
|
|
49
|
+
...topLevelCategoryOrder.flatMap(categoryChoices),
|
|
50
|
+
{ value: 'exit', name: 'Exit', short: 'Exit' },
|
|
51
|
+
];
|
|
52
|
+
const minimumTopLevelPageSize = 8;
|
|
53
|
+
const startupViewportReservedRows = 23;
|
|
54
|
+
function topLevelPageSize(choiceCount) {
|
|
55
|
+
const terminalRows = process.stdout.rows;
|
|
56
|
+
if (!terminalRows || terminalRows <= 0) {
|
|
57
|
+
return Math.min(choiceCount, 12);
|
|
58
|
+
}
|
|
59
|
+
return Math.min(choiceCount, Math.max(minimumTopLevelPageSize, terminalRows - startupViewportReservedRows));
|
|
60
|
+
}
|
|
31
61
|
function defaultSelect(options) {
|
|
32
|
-
return
|
|
62
|
+
return selectPrompt(options);
|
|
33
63
|
}
|
|
34
64
|
function defaultCancelHandler(value, action) {
|
|
35
|
-
handlePromptCancel(
|
|
65
|
+
handlePromptCancel(isPromptCancel(value), action);
|
|
36
66
|
}
|
|
37
67
|
function menuDeps(deps = {}) {
|
|
38
68
|
return {
|
|
@@ -40,49 +70,27 @@ function menuDeps(deps = {}) {
|
|
|
40
70
|
handleCancel: deps.handleCancel ?? defaultCancelHandler,
|
|
41
71
|
};
|
|
42
72
|
}
|
|
43
|
-
function
|
|
44
|
-
return typeof value === '
|
|
73
|
+
function isCockpitCommand(value) {
|
|
74
|
+
return typeof value === 'object' && value !== null && 'id' in value && 'slashAlias' in value;
|
|
45
75
|
}
|
|
46
76
|
export async function selectCockpitTopLevelMenu(options = {}) {
|
|
47
77
|
const deps = menuDeps(options);
|
|
48
78
|
const selected = await deps.select({
|
|
49
79
|
message: 'What do you want to do?',
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
choices: [...topLevelChoices],
|
|
81
|
+
default: topLevelCommands[0],
|
|
82
|
+
pageSize: topLevelPageSize(topLevelChoices.length),
|
|
83
|
+
loop: false,
|
|
52
84
|
});
|
|
53
85
|
deps.handleCancel(selected, 'exit');
|
|
54
|
-
if (selected === 'command-palette') {
|
|
55
|
-
return { kind: 'command-palette' };
|
|
56
|
-
}
|
|
57
86
|
if (selected === 'exit') {
|
|
58
87
|
return { kind: 'exit' };
|
|
59
88
|
}
|
|
60
|
-
if (
|
|
89
|
+
if (isCockpitCommand(selected)) {
|
|
61
90
|
return {
|
|
62
|
-
kind: '
|
|
63
|
-
|
|
91
|
+
kind: 'command',
|
|
92
|
+
command: selected,
|
|
64
93
|
};
|
|
65
94
|
}
|
|
66
95
|
return { kind: 'exit' };
|
|
67
96
|
}
|
|
68
|
-
export async function selectCockpitCategoryCommand(category, options = {}) {
|
|
69
|
-
const deps = menuDeps(options);
|
|
70
|
-
const commands = cockpitCommands.filter((command) => command.category === category && command.id !== 'exit');
|
|
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
|
-
}
|
package/dist/cockpit/safety.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { confirm, isCancel } from '@clack/prompts';
|
|
2
1
|
import { handlePromptCancel, menuPromptMessage } from '../menu-navigation.js';
|
|
2
|
+
import { confirmPrompt, isPromptCancel } from '../prompts/index.js';
|
|
3
3
|
function defaultHandleCancel(value, action) {
|
|
4
|
-
handlePromptCancel(
|
|
4
|
+
handlePromptCancel(isPromptCancel(value), action);
|
|
5
5
|
}
|
|
6
6
|
function riskConfirmationMessage(command, action) {
|
|
7
7
|
return menuPromptMessage(`Run ${command.slashAlias} ${command.label}? This can change or remove environment state.`, action);
|
|
@@ -10,7 +10,7 @@ export async function confirmCockpitCommandRisk(command, deps = {}) {
|
|
|
10
10
|
if (!command.isRisky) {
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
|
-
const prompt = deps.confirm ??
|
|
13
|
+
const prompt = deps.confirm ?? confirmPrompt;
|
|
14
14
|
const cancelAction = deps.cancelAction ?? 'back';
|
|
15
15
|
const approved = await prompt({
|
|
16
16
|
message: riskConfirmationMessage(command, cancelAction),
|
package/dist/daily-actions.js
CHANGED
|
@@ -150,7 +150,7 @@ async function assertEnvironmentRoot(cwd) {
|
|
|
150
150
|
await access(join(cwd, markerPath));
|
|
151
151
|
}
|
|
152
152
|
catch {
|
|
153
|
-
throw new Error('Daily actions must be run from a WPMoo
|
|
153
|
+
throw new Error('Daily actions must be run from a WPMoo Tool environment root containing .wpmoo/odoo.json.');
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
async function assertScriptExists(cwd, script) {
|
package/dist/help.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confirmPrompt, isPromptCancel, textPrompt, } from './prompts/index.js';
|
|
2
2
|
import { handlePromptCancel, menuPromptMessage } from './menu-navigation.js';
|
|
3
3
|
const defaultPrompt = {
|
|
4
|
-
confirm,
|
|
5
|
-
text,
|
|
4
|
+
confirm: confirmPrompt,
|
|
5
|
+
text: textPrompt,
|
|
6
6
|
};
|
|
7
7
|
export async function promptRepositoryUrl({ label, suggestedUrl, placeholder, prompt = defaultPrompt, cancelAction = 'exit', }) {
|
|
8
8
|
if (suggestedUrl) {
|
|
@@ -12,7 +12,7 @@ export async function promptRepositoryUrl({ label, suggestedUrl, placeholder, pr
|
|
|
12
12
|
inactive: 'n',
|
|
13
13
|
initialValue: true,
|
|
14
14
|
});
|
|
15
|
-
if (
|
|
15
|
+
if (isPromptCancel(useSuggested)) {
|
|
16
16
|
handlePromptCancel(true, cancelAction);
|
|
17
17
|
}
|
|
18
18
|
if (useSuggested) {
|
|
@@ -24,7 +24,7 @@ export async function promptRepositoryUrl({ label, suggestedUrl, placeholder, pr
|
|
|
24
24
|
placeholder,
|
|
25
25
|
validate: (input) => (input.trim() ? undefined : `Enter the ${label.toLowerCase()}.`),
|
|
26
26
|
});
|
|
27
|
-
if (
|
|
27
|
+
if (isPromptCancel(value)) {
|
|
28
28
|
handlePromptCancel(true, cancelAction);
|
|
29
29
|
}
|
|
30
30
|
if (typeof value === 'string' && value.trim()) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
2
|
+
import inquirerSelect, { Separator as InquirerSeparator } from '@inquirer/select';
|
|
3
|
+
import inquirerSearch from '@inquirer/search';
|
|
4
|
+
import { confirm as inquirerConfirm, input as inquirerInput } from '@inquirer/prompts';
|
|
5
|
+
import { recordPromptCancelKey } from '../menu-navigation.js';
|
|
6
|
+
export const promptCancelled = Symbol.for('wpmoo.prompt.cancelled');
|
|
7
|
+
export function promptSeparator(label) {
|
|
8
|
+
return new InquirerSeparator(label);
|
|
9
|
+
}
|
|
10
|
+
function isPromptCancelError(error) {
|
|
11
|
+
if (!(error instanceof Error)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return ['AbortError', 'CancelPromptError', 'AbortPromptError', 'ExitPromptError'].includes(error.name);
|
|
15
|
+
}
|
|
16
|
+
function markPromptCancel(error) {
|
|
17
|
+
if (error instanceof Error && error.name === 'ExitPromptError' && /SIGINT/.test(error.message)) {
|
|
18
|
+
recordPromptCancelKey({ ctrl: true, name: 'c', sequence: '\u0003' });
|
|
19
|
+
return promptCancelled;
|
|
20
|
+
}
|
|
21
|
+
return promptCancelled;
|
|
22
|
+
}
|
|
23
|
+
function mapSearchChoice(choice) {
|
|
24
|
+
if (choice.name !== undefined || choice.description !== undefined || choice.short !== undefined) {
|
|
25
|
+
return {
|
|
26
|
+
value: choice.value,
|
|
27
|
+
name: choice.name,
|
|
28
|
+
description: choice.description,
|
|
29
|
+
short: choice.short,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
value: choice.value,
|
|
34
|
+
name: choice.label,
|
|
35
|
+
description: choice.hint,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function asInquirerSearchConfig(options) {
|
|
39
|
+
return {
|
|
40
|
+
message: options.message,
|
|
41
|
+
source: async (term, signal) => {
|
|
42
|
+
const choices = await options.source(term, signal);
|
|
43
|
+
return choices.map((choice) => mapSearchChoice(choice));
|
|
44
|
+
},
|
|
45
|
+
pageSize: options.pageSize,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function installEscapeAbortController(controller) {
|
|
49
|
+
emitKeypressEvents(process.stdin);
|
|
50
|
+
const listener = (_value, key) => {
|
|
51
|
+
if (key.name !== 'escape' && key.sequence !== '\u001B') {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
recordPromptCancelKey(key);
|
|
55
|
+
if (!controller.signal.aborted) {
|
|
56
|
+
controller.abort();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
process.stdin.on('keypress', listener);
|
|
60
|
+
return () => process.stdin.off('keypress', listener);
|
|
61
|
+
}
|
|
62
|
+
async function withPromptCancelGuard(callback) {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const removeEscapeListener = installEscapeAbortController(controller);
|
|
65
|
+
try {
|
|
66
|
+
return await callback({ signal: controller.signal });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!isPromptCancelError(error)) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
return markPromptCancel(error);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
removeEscapeListener();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isClackSelectOptions(options) {
|
|
79
|
+
return 'options' in options;
|
|
80
|
+
}
|
|
81
|
+
function asInquirerSelectConfig(options) {
|
|
82
|
+
return {
|
|
83
|
+
message: options.message,
|
|
84
|
+
choices: options.options.map((option) => ({
|
|
85
|
+
value: option.value,
|
|
86
|
+
name: option.label,
|
|
87
|
+
description: option.hint,
|
|
88
|
+
})),
|
|
89
|
+
default: options.initialValue,
|
|
90
|
+
pageSize: options.pageSize,
|
|
91
|
+
loop: options.loop,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function asInquirerConfirmConfig(options) {
|
|
95
|
+
const hasChoiceLabels = Boolean(options.active && options.inactive);
|
|
96
|
+
return {
|
|
97
|
+
message: hasChoiceLabels ? `${options.message} (${options.active}/${options.inactive})` : options.message,
|
|
98
|
+
default: options.initialValue,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function asInquirerInputConfig(options) {
|
|
102
|
+
return {
|
|
103
|
+
message: options.message,
|
|
104
|
+
default: options.defaultValue ?? options.initialValue,
|
|
105
|
+
validate: options.validate
|
|
106
|
+
? (value) => {
|
|
107
|
+
const result = options.validate?.(value);
|
|
108
|
+
return result === undefined ? true : result;
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function isPromptCancel(value) {
|
|
114
|
+
return value === promptCancelled;
|
|
115
|
+
}
|
|
116
|
+
export async function selectPrompt(options) {
|
|
117
|
+
if (isClackSelectOptions(options)) {
|
|
118
|
+
return withPromptCancelGuard((context) => inquirerSelect(asInquirerSelectConfig(options), context));
|
|
119
|
+
}
|
|
120
|
+
return withPromptCancelGuard((context) => inquirerSelect(options, context));
|
|
121
|
+
}
|
|
122
|
+
export async function inputPrompt(options) {
|
|
123
|
+
return withPromptCancelGuard((context) => inquirerInput(asInquirerInputConfig(options), context));
|
|
124
|
+
}
|
|
125
|
+
export async function textPrompt(options) {
|
|
126
|
+
return inputPrompt(options);
|
|
127
|
+
}
|
|
128
|
+
export async function confirmPrompt(options) {
|
|
129
|
+
return withPromptCancelGuard((context) => inquirerConfirm(asInquirerConfirmConfig(options), context));
|
|
130
|
+
}
|
|
131
|
+
export async function searchPrompt(options) {
|
|
132
|
+
return withPromptCancelGuard((context) => inquirerSearch(asInquirerSearchConfig(options), context));
|
|
133
|
+
}
|
|
134
|
+
export function introPrompt(title) {
|
|
135
|
+
const rule = '-'.repeat(Math.min(80, Math.max(title.length, 3)));
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(title);
|
|
138
|
+
console.log(rule);
|
|
139
|
+
}
|
|
140
|
+
export function notePrompt(message, title = 'Note') {
|
|
141
|
+
const lines = message.split('\n');
|
|
142
|
+
console.log(`[${title}]`);
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
console.log(` ${line}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function outroPrompt(message) {
|
|
148
|
+
console.log(`Done: ${message}`);
|
|
149
|
+
}
|
package/dist/templates.js
CHANGED
|
@@ -297,15 +297,10 @@ function applyBannerGradient(banner) {
|
|
|
297
297
|
export function renderBanner() {
|
|
298
298
|
const banner = String.raw `
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
░██░██ ░██░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
|
|
305
|
-
░████ ░████ ░██ ░██ ░██ ░██ ░██ ░██ ░██
|
|
306
|
-
░███ ░███ ░██ ░██ ░██ ░███████ ░███████
|
|
307
|
-
|
|
308
|
-
░░░░░░░░░ Workflow Platform - Micro Object Oriented ░░░░░░░░░
|
|
300
|
+
╭────────────────────────────────────────────╮
|
|
301
|
+
│ WPMoo │
|
|
302
|
+
│ Workflow Platform · Micro Object Oriented │
|
|
303
|
+
╰────────────────────────────────────────────╯
|
|
309
304
|
`;
|
|
310
305
|
return `${ANSI_BOLD}${applyBannerGradient(banner)}${ANSI_RESET}`;
|
|
311
306
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wpmoo/odoo",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "WPMoo
|
|
3
|
+
"version": "0.8.69",
|
|
4
|
+
"description": "WPMoo Tool for Odoo development, staging, and production lifecycle workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@
|
|
50
|
+
"@inquirer/prompts": "^8.4.3",
|
|
51
51
|
"@inquirer/search": "^4.1.9",
|
|
52
|
+
"@inquirer/select": "^5.1.5",
|
|
52
53
|
"execa": "^9.6.0"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|