@wpmoo/odoo 0.8.68 → 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/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 { selectCockpitCategoryCommand, selectCockpitTopLevelMenu } from './cockpit/menu.js';
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(isCancel(value), action);
35
+ handlePromptCancel(isPromptCancel(value), action);
37
36
  }
38
37
  function showSubmenuIntro(title, showIntro, cancelAction) {
39
38
  if (showIntro) {
40
- intro(menuIntroTitle(title, cancelAction));
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 select({
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 confirm({
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
- if (selection.kind === 'command-palette') {
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
- intro('Create Odoo dev environment');
190
+ introPrompt('Create Odoo dev environment');
195
191
  }
196
- const product = asString(await text({
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 text({
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 select({
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
- note(renderRepositorySetupNote(product), 'Repository setup');
215
+ notePrompt(renderRepositorySetupNote(product), 'Repository setup');
220
216
  selectedGitHubOwner = await selectDefaultGitHubOwner(cancelAction);
221
217
  }
222
- const selectedVersion = await select({
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 select({
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 select({
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 select({
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 text({
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 text({
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
- note([
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
- note(`Source repo is not accessible: ${status.slug}`, 'Repository check');
384
- const shouldCreate = await select({
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 select({
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
- note(`No source repos found under ${target}/odoo/custom/src.\nNext: choose "Add source repo" first.`, 'Nothing to select');
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 select({
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 text({
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
- outro(`Synced source manifest in ${options.target}.`);
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
- outro(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
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
- outro(`Removed source repo ${options.repoPath} from ${options.target}.`);
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
- note(renderSafeResetPreview(options.target, options.stage), 'Safe reset preview');
585
- const confirmed = await confirm({
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
- note(`No module submodules found under ${target}/odoo/custom/src/private.\nNext: choose "Add source repo" first.`, 'Nothing to remove');
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 select({
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
- note(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
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 select({
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 confirm({
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
- note(message, 'Repository check skipped');
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
- note([
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
- note([
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 select({
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
- if (isCancel(shouldCreate))
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 select({
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
- if (isCancel(visibility))
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
- note(`${command.slashAlias} was not run.`, 'Action skipped');
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
- note(`${command.slashAlias} completed.`, 'Done');
749
+ notePrompt(`${command.slashAlias} completed.`, 'Done');
756
750
  return 'continue';
757
751
  }
758
752
  if (command.id === 'status') {
759
- note(await renderEnvironmentStatusForTarget(cwd), 'Environment status');
753
+ notePrompt(await renderEnvironmentStatusForTarget(cwd), 'Environment status');
760
754
  return 'continue';
761
755
  }
762
756
  if (command.id === 'doctor') {
763
- note(await runDoctor(cwd), 'Doctor');
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
- note(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private')}.`, 'Done');
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
- note(`Source repo ${options.repoPath} was not removed.`, 'Action skipped');
770
+ notePrompt(`Source repo ${options.repoPath} was not removed.`, 'Action skipped');
777
771
  return 'continue';
778
772
  }
779
773
  await removeModuleRepo(options);
780
- note(`Removed source repo ${options.repoPath} from ${options.target}.`, 'Done');
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
- note(`Added module ${options.moduleName} under source repo ${options.repoPath}.`, 'Done');
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
- note(`Module ${options.moduleName} was not removed.`, 'Action skipped');
786
+ notePrompt(`Module ${options.moduleName} was not removed.`, 'Action skipped');
793
787
  return 'continue';
794
788
  }
795
789
  await removeModuleFromSourceRepo(options);
796
- note(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`, 'Done');
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
- note(`Safe reset refreshed generated environment files in ${cwd}.`, 'Done');
797
+ notePrompt(`Safe reset refreshed generated environment files in ${cwd}.`, 'Done');
804
798
  return 'continue';
805
799
  }
806
- note(`Unknown cockpit command: ${command.slashAlias}`, 'No action');
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
- note(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
831
- outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
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
- intro('WPMoo Tool');
828
+ introPrompt('WPMoo Tool');
835
829
  while (true) {
836
830
  try {
837
831
  const status = await getEnvironmentStatus(cwd);
838
- note(renderEnvironmentStatusSummary(status), 'Environment status');
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
- outro(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
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
- outro(`Added source repo under ${promptedOptions.target}/odoo/custom/src/private.`);
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
- outro(`Removed source repo ${options.repoPath} from ${options.target}.`);
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
- outro(`Removed source repo ${promptedOptions.repoPath} from ${promptedOptions.target}.`);
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
- outro(`Added module ${options.moduleName} under source repo ${options.repoPath}.`);
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
- outro(`Added module ${promptedOptions.moduleName} under source repo ${promptedOptions.repoPath}.`);
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
- outro(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
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
- outro(`Removed module ${promptedOptions.moduleName} from source repo ${promptedOptions.repoPath}.`);
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
- outro(`Safe reset refreshed generated environment files in ${options.target}.`);
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
- note(renderPostCreateGuidance(resolvedOptions.target, cwd), 'Next steps');
982
- outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
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 search from '@inquirer/search';
1
+ import { isPromptCancel, searchPrompt } from '../prompts/index.js';
2
2
  import { searchCockpitCommands } from './command-registry.js';
3
- const defaultSearchPrompt = (config) => search(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
- return prompt({
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(isCancel(value), action);
8
+ handlePromptCancel(isPromptCancel(value), action);
9
9
  }
10
10
  function promptDeps(deps = {}) {
11
11
  return {
12
- select: deps.select ?? ((options) => select(options)),
13
- text: deps.text ?? ((options) => text(options)),
14
- list: deps.list ?? ((options) => select(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
  }
@@ -1,7 +1,7 @@
1
- import { isCancel, select } from '@clack/prompts';
1
+ import { styleText } from 'node:util';
2
2
  import { cockpitCommands, } from './command-registry.js';
3
- import { handlePromptCancel, menuPromptMessage, MenuBackSignal } from '../menu-navigation.js';
4
- export const cockpitMenuBackValue = '__wpmoo_cockpit_menu_back__';
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 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([
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 select(options);
62
+ return selectPrompt(options);
33
63
  }
34
64
  function defaultCancelHandler(value, action) {
35
- handlePromptCancel(isCancel(value), action);
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 isCockpitCommandCategory(value) {
44
- return typeof value === 'string' && categories.has(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
- options: [...topLevelOptions],
51
- initialValue: 'command-palette',
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 (isCockpitCommandCategory(selected)) {
89
+ if (isCockpitCommand(selected)) {
61
90
  return {
62
- kind: 'category',
63
- category: selected,
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
- }
@@ -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(isCancel(value), action);
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 ?? 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),
@@ -1,8 +1,8 @@
1
- import { confirm, isCancel, text } from '@clack/prompts';
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 (isCancel(useSuggested)) {
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 (isCancel(value)) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.68",
3
+ "version": "0.8.69",
4
4
  "description": "WPMoo Tool for Odoo development, staging, and production lifecycle workflows.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -47,8 +47,9 @@
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit"
48
48
  },
49
49
  "dependencies": {
50
- "@clack/prompts": "^0.11.0",
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": {