@wpmoo/odoo 0.8.63 → 0.8.65
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 +26 -4
- package/dist/cli.js +92 -35
- package/dist/cockpit/daily-prompts.js +6 -2
- package/dist/doctor.js +71 -29
- package/dist/help.js +20 -4
- package/dist/module-actions.js +20 -13
- package/dist/source-actions.js +23 -0
- package/dist/status.js +36 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,12 +138,15 @@ npx @wpmoo/odoo --help
|
|
|
138
138
|
npx @wpmoo/odoo --version
|
|
139
139
|
|
|
140
140
|
npx @wpmoo/odoo status
|
|
141
|
+
npx @wpmoo/odoo status --json
|
|
141
142
|
npx @wpmoo/odoo doctor
|
|
143
|
+
npx @wpmoo/odoo doctor --json
|
|
142
144
|
npx @wpmoo/odoo doctor --fix
|
|
145
|
+
npx @wpmoo/odoo source list --json
|
|
143
146
|
npx @wpmoo/odoo add-repo --repo-url https://github.com/example-org/odoo_sample_module_reports.git
|
|
144
147
|
npx @wpmoo/odoo remove-repo --repo odoo_sample_module_reports
|
|
145
|
-
npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base
|
|
146
|
-
npx @wpmoo/odoo remove-module --repo odoo_sample_module --module odoo_sample_module_base
|
|
148
|
+
npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
|
|
149
|
+
npx @wpmoo/odoo remove-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
|
|
147
150
|
npx @wpmoo/odoo reset --dry-run
|
|
148
151
|
npx @wpmoo/odoo reset
|
|
149
152
|
|
|
@@ -275,10 +278,13 @@ GitHub CLI is optional for repository setup. When it is available and authentica
|
|
|
275
278
|
|
|
276
279
|
Add a minimal Odoo module skeleton to a source repository:
|
|
277
280
|
|
|
281
|
+
For module actions, `--source-type` selects the source directory (`private`, `oca`, or `external`). Default is `private`.
|
|
282
|
+
|
|
278
283
|
```bash
|
|
279
284
|
npx @wpmoo/odoo add-module \
|
|
280
285
|
--repo odoo_sample_module \
|
|
281
|
-
--module odoo_sample_module_base
|
|
286
|
+
--module odoo_sample_module_base \
|
|
287
|
+
--source-type oca
|
|
282
288
|
```
|
|
283
289
|
|
|
284
290
|
Remove a module registration while keeping files:
|
|
@@ -286,7 +292,8 @@ Remove a module registration while keeping files:
|
|
|
286
292
|
```bash
|
|
287
293
|
npx @wpmoo/odoo remove-module \
|
|
288
294
|
--repo odoo_sample_module \
|
|
289
|
-
--module odoo_sample_module_base
|
|
295
|
+
--module odoo_sample_module_base \
|
|
296
|
+
--source-type oca
|
|
290
297
|
```
|
|
291
298
|
|
|
292
299
|
Delete module files as well:
|
|
@@ -315,12 +322,14 @@ Inspect configured sources:
|
|
|
315
322
|
|
|
316
323
|
```bash
|
|
317
324
|
npx @wpmoo/odoo source list
|
|
325
|
+
npx @wpmoo/odoo source list --json
|
|
318
326
|
```
|
|
319
327
|
|
|
320
328
|
Regenerate the manifest and metadata from the current metadata/gitmodule state:
|
|
321
329
|
|
|
322
330
|
```bash
|
|
323
331
|
npx @wpmoo/odoo source sync
|
|
332
|
+
npx @wpmoo/odoo source sync --json
|
|
324
333
|
```
|
|
325
334
|
|
|
326
335
|
`source add` and `source remove` are direct aliases for the same repository
|
|
@@ -340,10 +349,23 @@ npx @wpmoo/odoo source remove --repo server-tools --source-type oca
|
|
|
340
349
|
|
|
341
350
|
```bash
|
|
342
351
|
npx @wpmoo/odoo status
|
|
352
|
+
npx @wpmoo/odoo status --json
|
|
343
353
|
```
|
|
344
354
|
|
|
345
355
|
It reports whether the environment is detected, which Odoo version is selected, how many source repos are configured, how many module candidates are present, which core files are missing, and the recommended next action.
|
|
346
356
|
|
|
357
|
+
For automation and VS Code cockpit integration, all of these commands also support
|
|
358
|
+
`--json`:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
npx @wpmoo/odoo status --json
|
|
362
|
+
npx @wpmoo/odoo source list --json
|
|
363
|
+
npx @wpmoo/odoo source sync --json
|
|
364
|
+
npx @wpmoo/odoo doctor --json
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
JSON output is optional; human-readable output remains the default.
|
|
368
|
+
|
|
347
369
|
`doctor` performs deeper checks:
|
|
348
370
|
|
|
349
371
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { detectDevelopmentEnvironment } from './environment.js';
|
|
|
12
12
|
import { commandOdooVersion } from './environment-version.js';
|
|
13
13
|
import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
|
|
14
14
|
import { isDailyActionCommand, runDailyAction } from './daily-actions.js';
|
|
15
|
-
import { runDoctor } from './doctor.js';
|
|
15
|
+
import { getDoctorReport, runDoctor } from './doctor.js';
|
|
16
16
|
import { getOriginUrl, realGit } from './git.js';
|
|
17
17
|
import { renderHelp } from './help.js';
|
|
18
18
|
import { addModuleToSourceRepo, listModulesInSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
|
|
@@ -22,13 +22,13 @@ import { promptRepositoryUrl } from './prompt-repositories.js';
|
|
|
22
22
|
import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-url.js';
|
|
23
23
|
import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
|
|
24
24
|
import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
|
|
25
|
-
import { listSources, renderSourceList, syncSources } from './source-actions.js';
|
|
25
|
+
import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
|
|
26
26
|
import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, repositoryPreflightAvailable, } from './repository-preflight.js';
|
|
27
27
|
import { scaffold } from './scaffold.js';
|
|
28
28
|
import { renderBanner } from './templates.js';
|
|
29
29
|
import { checkForUpdate, installLatestPackage, isUpdateCheckSkipped, restartCli } from './update-check.js';
|
|
30
30
|
import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
|
|
31
|
-
import { getEnvironmentStatus, renderEnvironmentStatusForTarget, renderEnvironmentStatusSummary, } from './status.js';
|
|
31
|
+
import { environmentStatusJson, getEnvironmentStatus, renderEnvironmentStatusForTarget, renderEnvironmentStatusSummary, } from './status.js';
|
|
32
32
|
import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, realGitHub, createGitHubRepository, } from './github.js';
|
|
33
33
|
import { environmentGitHubOwner } from './environment-context.js';
|
|
34
34
|
import { handlePromptCancel, handleUnavailableMenuChoice, installPromptCancelKeyTracker, isMenuBackSignal, MenuBackSignal, menuIntroTitle, menuPromptMessage, } from './menu-navigation.js';
|
|
@@ -106,6 +106,12 @@ function booleanOption(values, key, fallback) {
|
|
|
106
106
|
return false;
|
|
107
107
|
throw new Error(`Invalid boolean value for --${key}: ${value}`);
|
|
108
108
|
}
|
|
109
|
+
function jsonOption(values) {
|
|
110
|
+
return booleanOption(values, 'json', false);
|
|
111
|
+
}
|
|
112
|
+
function printJson(value) {
|
|
113
|
+
console.log(JSON.stringify(value));
|
|
114
|
+
}
|
|
109
115
|
function yellow(value) {
|
|
110
116
|
if (!process.stdout.isTTY || process.env.NO_COLOR !== undefined)
|
|
111
117
|
return value;
|
|
@@ -399,21 +405,39 @@ async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
|
|
|
399
405
|
await createGitHubRepository(realGitHub, options.repoUrl, visibility);
|
|
400
406
|
}
|
|
401
407
|
async function selectSourceRepo(target, cancelAction = 'exit') {
|
|
402
|
-
const repos = await
|
|
403
|
-
|
|
408
|
+
const repos = await listSources(target);
|
|
409
|
+
const repoOptions = repos.length > 0
|
|
410
|
+
? repos.map((repo) => ({
|
|
411
|
+
value: { repoPath: repo.path, sourceType: repo.type },
|
|
412
|
+
label: `${repo.type}/${repo.path}`,
|
|
413
|
+
}))
|
|
414
|
+
: (await listModuleRepos(target)).map((repoPath) => ({
|
|
415
|
+
value: { repoPath, sourceType: 'private' },
|
|
416
|
+
label: `private/${repoPath}`,
|
|
417
|
+
}));
|
|
418
|
+
if (repoOptions.length === 0) {
|
|
404
419
|
if (cancelAction === 'back') {
|
|
405
|
-
note(`No source repos found under ${target}/odoo/custom/src
|
|
420
|
+
note(`No source repos found under ${target}/odoo/custom/src.\nNext: choose "Add source repo" first.`, 'Nothing to select');
|
|
406
421
|
handleUnavailableMenuChoice(cancelAction);
|
|
407
422
|
}
|
|
408
|
-
throw new Error(`No source repos found under ${target}/odoo/custom/src
|
|
423
|
+
throw new Error(`No source repos found under ${target}/odoo/custom/src`);
|
|
409
424
|
}
|
|
410
|
-
const
|
|
425
|
+
const selected = await select({
|
|
411
426
|
message: menuPromptMessage('Source repo', cancelAction),
|
|
412
|
-
options:
|
|
413
|
-
initialValue:
|
|
427
|
+
options: repoOptions,
|
|
428
|
+
initialValue: repoOptions[0].value,
|
|
414
429
|
});
|
|
415
|
-
handleCancel(
|
|
416
|
-
|
|
430
|
+
handleCancel(selected, cancelAction);
|
|
431
|
+
if (typeof selected === 'string') {
|
|
432
|
+
return { repoPath: selected, sourceType: 'private' };
|
|
433
|
+
}
|
|
434
|
+
if (typeof selected === 'object' && selected !== null && 'repoPath' in selected && 'sourceType' in selected) {
|
|
435
|
+
return { repoPath: selected.repoPath, sourceType: selected.sourceType };
|
|
436
|
+
}
|
|
437
|
+
return { repoPath: String(selected), sourceType: 'private' };
|
|
438
|
+
}
|
|
439
|
+
function formatSourceRepoPromptPath(target, selected) {
|
|
440
|
+
return renderedSourceRepoPath(target, selected.sourceType, selected.repoPath);
|
|
417
441
|
}
|
|
418
442
|
function suggestedModuleName(repoPath) {
|
|
419
443
|
return 'odoo_sample_module';
|
|
@@ -430,6 +454,7 @@ async function addModuleOptionsFromArgs(argv) {
|
|
|
430
454
|
target,
|
|
431
455
|
repoPath,
|
|
432
456
|
moduleName,
|
|
457
|
+
sourceType: optionalSourceTypeValue(values),
|
|
433
458
|
odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
|
|
434
459
|
stage: booleanOption(values, 'stage', true),
|
|
435
460
|
};
|
|
@@ -437,15 +462,16 @@ async function addModuleOptionsFromArgs(argv) {
|
|
|
437
462
|
async function addModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
438
463
|
showSubmenuIntro('Add module to source repo', showIntro, cancelAction);
|
|
439
464
|
const target = process.cwd();
|
|
440
|
-
const
|
|
465
|
+
const sourceRepo = await selectSourceRepo(target, cancelAction);
|
|
441
466
|
const moduleName = asString(await text({
|
|
442
467
|
message: menuPromptMessage('Module name', cancelAction),
|
|
443
|
-
placeholder: suggestedModuleName(repoPath),
|
|
468
|
+
placeholder: suggestedModuleName(sourceRepo.repoPath),
|
|
444
469
|
validate: (value) => (value.trim() ? undefined : 'Enter the module technical name.'),
|
|
445
|
-
}), suggestedModuleName(repoPath), cancelAction);
|
|
470
|
+
}), suggestedModuleName(sourceRepo.repoPath), cancelAction);
|
|
446
471
|
return {
|
|
447
472
|
target,
|
|
448
|
-
repoPath,
|
|
473
|
+
repoPath: sourceRepo.repoPath,
|
|
474
|
+
sourceType: sourceRepo.sourceType,
|
|
449
475
|
moduleName,
|
|
450
476
|
odooVersion: await commandOdooVersion(target),
|
|
451
477
|
stage: true,
|
|
@@ -473,17 +499,19 @@ function resetCommandOptionsFromArgs(argv) {
|
|
|
473
499
|
};
|
|
474
500
|
}
|
|
475
501
|
function doctorOptionsFromArgs(argv) {
|
|
476
|
-
if (argv.length === 0) {
|
|
477
|
-
return {};
|
|
478
|
-
}
|
|
479
502
|
const { values } = parseArgs(argv);
|
|
480
503
|
const keys = Object.keys(values);
|
|
481
|
-
|
|
504
|
+
const allowedKeys = new Set(['fix', 'json']);
|
|
505
|
+
if (!keys.every((key) => allowedKeys.has(key))) {
|
|
482
506
|
throw new Error('Usage: wpmoo doctor');
|
|
483
507
|
}
|
|
484
|
-
|
|
485
|
-
|
|
508
|
+
const options = {
|
|
509
|
+
json: jsonOption(values),
|
|
486
510
|
};
|
|
511
|
+
if (Object.hasOwn(values, 'fix')) {
|
|
512
|
+
options.fix = booleanOption(values, 'fix', false);
|
|
513
|
+
}
|
|
514
|
+
return options;
|
|
487
515
|
}
|
|
488
516
|
function sourceUsage() {
|
|
489
517
|
return 'Usage: wpmoo source <list|sync|add|remove> [options]';
|
|
@@ -493,11 +521,15 @@ function sourceSyncOptionsFromArgs(argv) {
|
|
|
493
521
|
return {
|
|
494
522
|
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
495
523
|
stage: booleanOption(values, 'stage', true),
|
|
524
|
+
json: jsonOption(values),
|
|
496
525
|
};
|
|
497
526
|
}
|
|
498
|
-
function
|
|
527
|
+
function sourceListOptionsFromArgs(argv) {
|
|
499
528
|
const { values } = parseArgs(argv);
|
|
500
|
-
return
|
|
529
|
+
return {
|
|
530
|
+
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
531
|
+
json: jsonOption(values),
|
|
532
|
+
};
|
|
501
533
|
}
|
|
502
534
|
async function runSourceCommand(argv) {
|
|
503
535
|
const [subcommand, ...subcommandArgv] = argv;
|
|
@@ -505,15 +537,24 @@ async function runSourceCommand(argv) {
|
|
|
505
537
|
throw new Error(sourceUsage());
|
|
506
538
|
}
|
|
507
539
|
if (subcommand === 'list') {
|
|
540
|
+
const options = sourceListOptionsFromArgs(subcommandArgv);
|
|
541
|
+
const sources = await listSources(options.target);
|
|
542
|
+
if (options.json) {
|
|
543
|
+
printJson(sourceListJson(sources));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
508
546
|
console.log(renderBanner());
|
|
509
|
-
|
|
510
|
-
console.log(renderSourceList(await listSources(target)));
|
|
547
|
+
console.log(renderSourceList(sources));
|
|
511
548
|
return;
|
|
512
549
|
}
|
|
513
550
|
if (subcommand === 'sync') {
|
|
514
|
-
console.log(renderBanner());
|
|
515
551
|
const options = sourceSyncOptionsFromArgs(subcommandArgv);
|
|
516
|
-
await syncSources(options);
|
|
552
|
+
const sources = await syncSources({ target: options.target, stage: options.stage });
|
|
553
|
+
if (options.json) {
|
|
554
|
+
printJson(sourceSyncJson(sources, options.target));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
console.log(renderBanner());
|
|
517
558
|
outro(`Synced source manifest in ${options.target}.`);
|
|
518
559
|
return;
|
|
519
560
|
}
|
|
@@ -587,6 +628,7 @@ function removeModuleOptionsFromArgs(argv) {
|
|
|
587
628
|
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
588
629
|
repoPath,
|
|
589
630
|
moduleName,
|
|
631
|
+
sourceType: optionalSourceTypeValue(values),
|
|
590
632
|
deleteFiles: booleanOption(values, 'deleteFiles', false),
|
|
591
633
|
stage: booleanOption(values, 'stage', true),
|
|
592
634
|
};
|
|
@@ -594,14 +636,14 @@ function removeModuleOptionsFromArgs(argv) {
|
|
|
594
636
|
async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
|
|
595
637
|
showSubmenuIntro('Remove module from source repo', showIntro, cancelAction);
|
|
596
638
|
const target = process.cwd();
|
|
597
|
-
const
|
|
598
|
-
const modules = await listModulesInSourceRepo(target, repoPath);
|
|
639
|
+
const sourceRepo = await selectSourceRepo(target, cancelAction);
|
|
640
|
+
const modules = await listModulesInSourceRepo(target, sourceRepo.repoPath, sourceRepo.sourceType);
|
|
599
641
|
if (modules.length === 0) {
|
|
600
642
|
if (cancelAction === 'back') {
|
|
601
|
-
note(`No Odoo modules found under ${target}
|
|
643
|
+
note(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
|
|
602
644
|
handleUnavailableMenuChoice(cancelAction);
|
|
603
645
|
}
|
|
604
|
-
throw new Error(`No Odoo modules found under ${target}
|
|
646
|
+
throw new Error(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}`);
|
|
605
647
|
}
|
|
606
648
|
const moduleName = await select({
|
|
607
649
|
message: menuPromptMessage('Module to remove', cancelAction),
|
|
@@ -618,7 +660,8 @@ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = '
|
|
|
618
660
|
handleCancel(deleteFiles, cancelAction);
|
|
619
661
|
return {
|
|
620
662
|
target,
|
|
621
|
-
repoPath,
|
|
663
|
+
repoPath: sourceRepo.repoPath,
|
|
664
|
+
sourceType: sourceRepo.sourceType,
|
|
622
665
|
moduleName: String(moduleName),
|
|
623
666
|
deleteFiles: Boolean(deleteFiles),
|
|
624
667
|
stage: true,
|
|
@@ -885,14 +928,28 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
885
928
|
}
|
|
886
929
|
if (route.command === 'doctor') {
|
|
887
930
|
const options = doctorOptionsFromArgs(route.argv);
|
|
931
|
+
const doctorOptions = {};
|
|
932
|
+
if (options.fix !== undefined) {
|
|
933
|
+
doctorOptions.fix = options.fix;
|
|
934
|
+
}
|
|
935
|
+
if (options.json) {
|
|
936
|
+
printJson(await getDoctorReport(cwd, doctorOptions));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
888
939
|
console.log(renderBanner());
|
|
889
|
-
console.log(options.fix === undefined ? await runDoctor(cwd) : await runDoctor(cwd,
|
|
940
|
+
console.log(options.fix === undefined ? await runDoctor(cwd) : await runDoctor(cwd, doctorOptions));
|
|
890
941
|
return;
|
|
891
942
|
}
|
|
892
943
|
if (route.command === 'status') {
|
|
893
|
-
|
|
944
|
+
const { values } = parseArgs(route.argv);
|
|
945
|
+
const keys = Object.keys(values);
|
|
946
|
+
if (!keys.every((key) => key === 'json')) {
|
|
894
947
|
throw new Error('Usage: wpmoo status');
|
|
895
948
|
}
|
|
949
|
+
if (jsonOption(values)) {
|
|
950
|
+
printJson(environmentStatusJson(await getEnvironmentStatus(cwd)));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
896
953
|
console.log(renderBanner());
|
|
897
954
|
console.log(await renderEnvironmentStatusForTarget(cwd));
|
|
898
955
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isCancel, select, text } from '@clack/prompts';
|
|
2
2
|
import { listModulesInSourceRepo } from '../module-actions.js';
|
|
3
3
|
import { listModuleRepos } from '../repo-actions.js';
|
|
4
|
+
import { listSources } from '../source-actions.js';
|
|
4
5
|
import { handlePromptCancel, menuPromptMessage, } from '../menu-navigation.js';
|
|
5
6
|
const manualModuleValue = '__wpmoo_manual_module_entry__';
|
|
6
7
|
function defaultCancelHandler(value, action) {
|
|
@@ -27,10 +28,13 @@ function requiredString(value, message, deps) {
|
|
|
27
28
|
}
|
|
28
29
|
async function detectedModules(cwd) {
|
|
29
30
|
try {
|
|
30
|
-
const
|
|
31
|
+
const sources = await listSources(cwd);
|
|
32
|
+
const repos = sources.length > 0
|
|
33
|
+
? sources.map((source) => ({ path: source.path, sourceType: source.type }))
|
|
34
|
+
: (await listModuleRepos(cwd)).map((path) => ({ path, sourceType: 'private' }));
|
|
31
35
|
const modules = await Promise.all(repos.map(async (repo) => {
|
|
32
36
|
try {
|
|
33
|
-
return await listModulesInSourceRepo(cwd, repo);
|
|
37
|
+
return await listModulesInSourceRepo(cwd, repo.path, repo.sourceType);
|
|
34
38
|
}
|
|
35
39
|
catch {
|
|
36
40
|
return [];
|
package/dist/doctor.js
CHANGED
|
@@ -40,6 +40,11 @@ function isRecord(value) {
|
|
|
40
40
|
function isDoctorOptions(value) {
|
|
41
41
|
return isRecord(value);
|
|
42
42
|
}
|
|
43
|
+
function isMetadataError(message) {
|
|
44
|
+
return (message.startsWith('Missing metadata file:') ||
|
|
45
|
+
message.startsWith('Invalid metadata JSON in .wpmoo/odoo.json') ||
|
|
46
|
+
message.startsWith('Invalid sourceRepos entry in .wpmoo/odoo.json'));
|
|
47
|
+
}
|
|
43
48
|
const incompatiblePostgres18MountTargets = ['/var/lib/postgresql/data', '/var/lib/postgresql/18/docker'];
|
|
44
49
|
function parsePostgresMajorFromValue(value) {
|
|
45
50
|
if (!value)
|
|
@@ -280,24 +285,41 @@ async function repairSourceManifestFromDiscoveredState(target, sourceRepos, fall
|
|
|
280
285
|
await writeSourceManifest(target, entries);
|
|
281
286
|
await replaceSourceRepos(target, sourceReposFromManifest(entries));
|
|
282
287
|
}
|
|
283
|
-
export async function
|
|
288
|
+
export async function getDoctorReport(target = process.cwd(), runnerOrOptions = realCommandRunner, options = {}) {
|
|
284
289
|
const actualRunner = isDoctorOptions(runnerOrOptions) ? realCommandRunner : runnerOrOptions;
|
|
285
290
|
const actualOptions = isDoctorOptions(runnerOrOptions) ? runnerOrOptions : options;
|
|
286
|
-
const appliedFixes = [];
|
|
287
|
-
const lines = ['WPMoo doctor'];
|
|
288
291
|
const errors = [];
|
|
289
292
|
const warnings = [];
|
|
290
|
-
const
|
|
291
|
-
|
|
293
|
+
const checks = [];
|
|
294
|
+
const appliedFixes = [];
|
|
295
|
+
const report = {
|
|
296
|
+
schemaVersion: 1,
|
|
297
|
+
command: 'doctor',
|
|
298
|
+
ok: false,
|
|
299
|
+
target,
|
|
300
|
+
checks,
|
|
301
|
+
warnings,
|
|
302
|
+
errors,
|
|
303
|
+
appliedFixes,
|
|
304
|
+
};
|
|
305
|
+
let metadata;
|
|
306
|
+
try {
|
|
307
|
+
metadata = await readMetadata(target);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
errors.push(errorMessage(error));
|
|
311
|
+
return report;
|
|
312
|
+
}
|
|
313
|
+
checks.push(`OK metadata ${markerPath}`);
|
|
292
314
|
const engine = metadataString(metadata, 'engine') ?? 'compose';
|
|
293
315
|
if (engine !== 'compose') {
|
|
294
316
|
errors.push(`Unsupported environment engine: ${engine}`);
|
|
295
317
|
}
|
|
296
318
|
else {
|
|
297
|
-
|
|
319
|
+
checks.push('OK engine compose');
|
|
298
320
|
}
|
|
299
321
|
const odooVersion = metadataString(metadata, 'odooVersion') ?? defaultOdooVersion;
|
|
300
|
-
|
|
322
|
+
checks.push(`OK Odoo version ${odooVersion}`);
|
|
301
323
|
const env = await readEnvFile(target);
|
|
302
324
|
const composeVersions = new Set([odooVersion]);
|
|
303
325
|
const envOdooVersion = env?.get('ODOO_VERSION')?.trim();
|
|
@@ -312,7 +334,7 @@ export async function runDoctor(target = process.cwd(), runnerOrOptions = realCo
|
|
|
312
334
|
errors.push(...composeLayout.errors);
|
|
313
335
|
}
|
|
314
336
|
else {
|
|
315
|
-
|
|
337
|
+
checks.push(`OK compose files ${composeLayout.files.join(', ')}`);
|
|
316
338
|
const postgresVersion = inferPostgresVersion(metadata, odooVersion, env);
|
|
317
339
|
if (postgresVersion === '18') {
|
|
318
340
|
for (const file of composeLayout.files) {
|
|
@@ -351,16 +373,23 @@ export async function runDoctor(target = process.cwd(), runnerOrOptions = realCo
|
|
|
351
373
|
}
|
|
352
374
|
}
|
|
353
375
|
if (errors.length === scriptErrorCount) {
|
|
354
|
-
|
|
376
|
+
checks.push(`OK scripts ${scriptNames.length} checked`);
|
|
377
|
+
}
|
|
378
|
+
let sourceRepos;
|
|
379
|
+
try {
|
|
380
|
+
sourceRepos = sourceReposFromMetadata(metadata);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
errors.push(errorMessage(error));
|
|
384
|
+
return report;
|
|
355
385
|
}
|
|
356
|
-
const sourceRepos = sourceReposFromMetadata(metadata);
|
|
357
386
|
for (const repo of sourceRepos) {
|
|
358
387
|
const relativePath = sourceRepoPath(normalizeSourceType(repo.sourceType), repo.path);
|
|
359
388
|
if (!(await exists(join(target, relativePath))) && repo.path) {
|
|
360
389
|
errors.push(`Missing source repo path: ${relativePath}`);
|
|
361
390
|
}
|
|
362
391
|
}
|
|
363
|
-
|
|
392
|
+
checks.push(`OK source repos ${sourceRepos.length} checked`);
|
|
364
393
|
const manifestPath = join(target, sourceManifestPath);
|
|
365
394
|
const hasManifest = await exists(manifestPath);
|
|
366
395
|
let manifestEntries = [];
|
|
@@ -409,19 +438,19 @@ export async function runDoctor(target = process.cwd(), runnerOrOptions = realCo
|
|
|
409
438
|
errors.push('HTTP_PORT and GEVENT_PORT in .env must not be equal');
|
|
410
439
|
}
|
|
411
440
|
if (/^\d+$/.test(httpPort) && /^\d+$/.test(geventPort) && httpPort !== geventPort) {
|
|
412
|
-
|
|
441
|
+
checks.push(`OK .env ports HTTP_PORT=${httpPort} GEVENT_PORT=${geventPort}`);
|
|
413
442
|
}
|
|
414
443
|
}
|
|
415
444
|
try {
|
|
416
445
|
await actualRunner('docker', ['version'], { cwd: target });
|
|
417
|
-
|
|
446
|
+
checks.push('OK docker CLI');
|
|
418
447
|
}
|
|
419
448
|
catch (error) {
|
|
420
449
|
errors.push(`Docker CLI check failed: ${errorMessage(error)}`);
|
|
421
450
|
}
|
|
422
451
|
try {
|
|
423
452
|
await actualRunner('docker', ['compose', 'version'], { cwd: target });
|
|
424
|
-
|
|
453
|
+
checks.push('OK docker compose');
|
|
425
454
|
}
|
|
426
455
|
catch (error) {
|
|
427
456
|
errors.push(`Docker Compose check failed: ${errorMessage(error)}`);
|
|
@@ -432,12 +461,12 @@ export async function runDoctor(target = process.cwd(), runnerOrOptions = realCo
|
|
|
432
461
|
const submoduleErrors = sourceSubmoduleStatusErrors(result.stdout, sourceRepos);
|
|
433
462
|
errors.push(...submoduleErrors);
|
|
434
463
|
if (submoduleErrors.length === 0) {
|
|
435
|
-
|
|
464
|
+
checks.push(`OK git submodules ${sourceRepos.length} checked`);
|
|
436
465
|
}
|
|
437
466
|
}
|
|
438
467
|
catch (error) {
|
|
439
468
|
if (isNotGitCheckoutError(error)) {
|
|
440
|
-
|
|
469
|
+
checks.push('OK git submodules skipped (not a git checkout)');
|
|
441
470
|
}
|
|
442
471
|
else {
|
|
443
472
|
errors.push(`Git submodule status check failed: ${errorMessage(error)}`);
|
|
@@ -446,32 +475,45 @@ export async function runDoctor(target = process.cwd(), runnerOrOptions = realCo
|
|
|
446
475
|
}
|
|
447
476
|
try {
|
|
448
477
|
await actualRunner('gh', ['auth', 'status'], { cwd: target });
|
|
449
|
-
|
|
478
|
+
checks.push('OK GitHub CLI auth');
|
|
450
479
|
}
|
|
451
480
|
catch (error) {
|
|
452
|
-
warnings.push(`
|
|
481
|
+
warnings.push(`GitHub CLI auth: ${errorMessage(error)}`);
|
|
453
482
|
}
|
|
454
|
-
|
|
455
|
-
|
|
483
|
+
report.ok = errors.length === 0;
|
|
484
|
+
return report;
|
|
485
|
+
}
|
|
486
|
+
export async function runDoctor(target = process.cwd(), runnerOrOptions = realCommandRunner, options = {}) {
|
|
487
|
+
const report = await getDoctorReport(target, runnerOrOptions, options);
|
|
488
|
+
const actualRunner = isDoctorOptions(runnerOrOptions) ? realCommandRunner : runnerOrOptions;
|
|
489
|
+
const actualOptions = isDoctorOptions(runnerOrOptions) ? runnerOrOptions : options;
|
|
490
|
+
if (!report.ok) {
|
|
491
|
+
if (report.errors.some(isMetadataError)) {
|
|
492
|
+
throw new Error(report.errors[0]);
|
|
493
|
+
}
|
|
494
|
+
if (actualOptions.fix && report.appliedFixes.length > 0) {
|
|
456
495
|
return [
|
|
457
496
|
'Doctor auto-fixes were not enough to satisfy all checks.',
|
|
458
|
-
...appliedFixes.map((fix) => `- ${fix}`),
|
|
459
|
-
renderFailure(errors),
|
|
497
|
+
...report.appliedFixes.map((fix) => `- ${fix}`),
|
|
498
|
+
renderFailure(report.errors),
|
|
460
499
|
].join('\n');
|
|
461
500
|
}
|
|
462
|
-
throw new Error(renderFailure(errors));
|
|
501
|
+
throw new Error(renderFailure(report.errors));
|
|
463
502
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
503
|
+
const renderedReport = [
|
|
504
|
+
'WPMoo doctor',
|
|
505
|
+
...report.checks,
|
|
506
|
+
...report.warnings.map((warning) => `WARN ${warning}`),
|
|
507
|
+
'Doctor checks passed.',
|
|
508
|
+
];
|
|
509
|
+
if (report.appliedFixes.length > 0) {
|
|
468
510
|
const postFixReport = await runDoctor(target, actualRunner, { ...actualOptions, fix: false });
|
|
469
511
|
return [
|
|
470
512
|
'Applied safe doctor fixes:',
|
|
471
|
-
...appliedFixes.map((fix) => `- ${fix}`),
|
|
513
|
+
...report.appliedFixes.map((fix) => `- ${fix}`),
|
|
472
514
|
'',
|
|
473
515
|
postFixReport,
|
|
474
516
|
].join('\n');
|
|
475
517
|
}
|
|
476
|
-
return
|
|
518
|
+
return renderedReport.join('\n');
|
|
477
519
|
}
|
package/dist/help.js
CHANGED
|
@@ -7,16 +7,20 @@ Usage:
|
|
|
7
7
|
npx @wpmoo/odoo
|
|
8
8
|
npx @wpmoo/odoo create --product <slug> [--target <path>] --dev-repo-url <url> --source-repo-url <url>
|
|
9
9
|
npx @wpmoo/odoo status
|
|
10
|
+
npx @wpmoo/odoo status --json
|
|
10
11
|
npx @wpmoo/odoo add-repo --repo-url <url> [--source-type private|oca|external]
|
|
11
12
|
npx @wpmoo/odoo remove-repo --repo <name>
|
|
12
13
|
npx @wpmoo/odoo source list
|
|
14
|
+
npx @wpmoo/odoo source list --json
|
|
13
15
|
npx @wpmoo/odoo source sync
|
|
16
|
+
npx @wpmoo/odoo source sync --json
|
|
14
17
|
npx @wpmoo/odoo source add --repo-url <url> [--source-type private|oca|external]
|
|
15
18
|
npx @wpmoo/odoo source remove --repo <name> [--source-type private|oca|external]
|
|
16
|
-
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
|
|
17
|
-
npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name>
|
|
19
|
+
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> [--source-type <category>]
|
|
20
|
+
npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name> [--source-type <category>]
|
|
18
21
|
npx @wpmoo/odoo reset [--dry-run]
|
|
19
22
|
npx @wpmoo/odoo doctor [--fix]
|
|
23
|
+
npx @wpmoo/odoo doctor --json
|
|
20
24
|
npx @wpmoo/odoo start
|
|
21
25
|
npx @wpmoo/odoo stop
|
|
22
26
|
npx @wpmoo/odoo logs [service]
|
|
@@ -48,8 +52,9 @@ Options:
|
|
|
48
52
|
--postgres-version <value> PostgreSQL image version written to compose .env.example.
|
|
49
53
|
--http-port <port> Host HTTP port written to .env.example.
|
|
50
54
|
--gevent-port <port> Host gevent/live chat port written to .env.example.
|
|
55
|
+
--json Emit machine-readable JSON. Human-readable output remains the default.
|
|
51
56
|
--repo-url <url> Source repo URL for add-repo.
|
|
52
|
-
--source-type <category> Source repo category for add-repo/remove-repo. One of private, oca, external. Default: private.
|
|
57
|
+
--source-type <category> Source repo category for add-repo/remove-repo/add-module/remove-module. One of private, oca, external. Default: private.
|
|
53
58
|
--repo <name> Source repo folder name for repo/module actions.
|
|
54
59
|
--module <name> Odoo module technical name for module actions.
|
|
55
60
|
--delete-files Also delete module files in remove-module. Default: false.
|
|
@@ -101,7 +106,11 @@ Task recipes:
|
|
|
101
106
|
npx @wpmoo/odoo source list
|
|
102
107
|
npx @wpmoo/odoo source sync
|
|
103
108
|
Add module:
|
|
104
|
-
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
|
|
109
|
+
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> --source-type private|oca|external
|
|
110
|
+
Remove module:
|
|
111
|
+
npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name> --source-type private|oca|external
|
|
112
|
+
Add OCA module:
|
|
113
|
+
npx @wpmoo/odoo add-module --repo sale-workflow --module sale_order_line_no_discount --source-type oca
|
|
105
114
|
Run tests:
|
|
106
115
|
npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
|
|
107
116
|
Safe reset and recover:
|
|
@@ -117,6 +126,13 @@ Task recipes:
|
|
|
117
126
|
npx @wpmoo/odoo logs [service]
|
|
118
127
|
npx @wpmoo/odoo restart
|
|
119
128
|
|
|
129
|
+
Machine-readable JSON output:
|
|
130
|
+
for automation and VS Code cockpit integration while keeping default human-readable output.
|
|
131
|
+
npx @wpmoo/odoo status --json
|
|
132
|
+
npx @wpmoo/odoo source list --json
|
|
133
|
+
npx @wpmoo/odoo source sync --json
|
|
134
|
+
npx @wpmoo/odoo doctor --json
|
|
135
|
+
|
|
120
136
|
Example:
|
|
121
137
|
npx @wpmoo/odoo create \\
|
|
122
138
|
--product odoo_sample_module \\
|
package/dist/module-actions.js
CHANGED
|
@@ -5,11 +5,15 @@ import { readEnvironmentMetadata } from './environment.js';
|
|
|
5
5
|
import { realGit, stageAll } from './git.js';
|
|
6
6
|
import { pathUnderBase, validateModuleName, validateRepoPath } from './path-validation.js';
|
|
7
7
|
import { readAddonsYaml, writeAddonsYaml } from './repo-actions.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const validSourceTypes = ['private', 'oca', 'external'];
|
|
9
|
+
function normalizeSourceType(value) {
|
|
10
|
+
return validSourceTypes.includes(value) ? value : 'private';
|
|
10
11
|
}
|
|
11
|
-
function
|
|
12
|
-
return pathUnderBase(
|
|
12
|
+
function sourceRepoPath(target, sourceType, repoPath) {
|
|
13
|
+
return pathUnderBase(join(target, `odoo/custom/src/${sourceType}`), repoPath, 'repo path');
|
|
14
|
+
}
|
|
15
|
+
function modulePath(target, sourceType, repoPath, moduleName) {
|
|
16
|
+
return pathUnderBase(sourceRepoPath(target, sourceType, repoPath), moduleName, 'module name');
|
|
13
17
|
}
|
|
14
18
|
function titleizeModule(moduleName) {
|
|
15
19
|
return moduleName
|
|
@@ -49,7 +53,8 @@ async function usesAddonsYaml(target) {
|
|
|
49
53
|
export async function addModuleToSourceRepo(options, git = realGit) {
|
|
50
54
|
const repoPath = validateRepoPath(options.repoPath);
|
|
51
55
|
const moduleName = validateModuleName(options.moduleName);
|
|
52
|
-
const
|
|
56
|
+
const sourceType = normalizeSourceType(options.sourceType);
|
|
57
|
+
const destination = modulePath(options.target, sourceType, repoPath, moduleName);
|
|
53
58
|
await mkdir(join(destination, 'models'), { recursive: true });
|
|
54
59
|
await mkdir(join(destination, 'security'), { recursive: true });
|
|
55
60
|
await mkdir(join(destination, 'views'), { recursive: true });
|
|
@@ -58,24 +63,25 @@ export async function addModuleToSourceRepo(options, git = realGit) {
|
|
|
58
63
|
await writeIfMissing(join(destination, 'models/__init__.py'), '');
|
|
59
64
|
await writeIfMissing(join(destination, 'security/ir.model.access.csv'), 'id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\n');
|
|
60
65
|
await writeIfMissing(join(destination, 'views/.gitkeep'), '');
|
|
61
|
-
if (await usesAddonsYaml(options.target)) {
|
|
66
|
+
if (sourceType === 'private' && (await usesAddonsYaml(options.target))) {
|
|
62
67
|
const addonsYaml = await readAddonsYaml(options.target);
|
|
63
68
|
await writeAddonsYaml(options.target, addModuleToSourceRepoInAddonsYaml(addonsYaml, repoPath, moduleName));
|
|
64
69
|
}
|
|
65
70
|
if (options.stage) {
|
|
66
|
-
await stageAll(git, sourceRepoPath(options.target, repoPath));
|
|
71
|
+
await stageAll(git, sourceRepoPath(options.target, sourceType, repoPath));
|
|
67
72
|
await stageAll(git, options.target);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
|
-
export async function listModulesInSourceRepo(target, repoPath) {
|
|
75
|
+
export async function listModulesInSourceRepo(target, repoPath, sourceType) {
|
|
71
76
|
const safeRepoPath = validateRepoPath(repoPath);
|
|
77
|
+
const resolvedSourceType = normalizeSourceType(sourceType);
|
|
72
78
|
try {
|
|
73
|
-
const entries = await readdir(sourceRepoPath(target, safeRepoPath), { withFileTypes: true });
|
|
79
|
+
const entries = await readdir(sourceRepoPath(target, resolvedSourceType, safeRepoPath), { withFileTypes: true });
|
|
74
80
|
const modules = await Promise.all(entries
|
|
75
81
|
.filter((entry) => entry.isDirectory())
|
|
76
82
|
.map(async (entry) => {
|
|
77
83
|
try {
|
|
78
|
-
await readFile(join(sourceRepoPath(target, safeRepoPath), entry.name, '__manifest__.py'), 'utf8');
|
|
84
|
+
await readFile(join(sourceRepoPath(target, resolvedSourceType, safeRepoPath), entry.name, '__manifest__.py'), 'utf8');
|
|
79
85
|
return entry.name;
|
|
80
86
|
}
|
|
81
87
|
catch {
|
|
@@ -91,16 +97,17 @@ export async function listModulesInSourceRepo(target, repoPath) {
|
|
|
91
97
|
export async function removeModuleFromSourceRepo(options, git = realGit) {
|
|
92
98
|
const repoPath = validateRepoPath(options.repoPath);
|
|
93
99
|
const moduleName = validateModuleName(options.moduleName);
|
|
94
|
-
|
|
100
|
+
const sourceType = normalizeSourceType(options.sourceType);
|
|
101
|
+
if (sourceType === 'private' && (await usesAddonsYaml(options.target))) {
|
|
95
102
|
const addonsYaml = await readAddonsYaml(options.target);
|
|
96
103
|
await writeAddonsYaml(options.target, removeModuleFromSourceRepoInAddonsYaml(addonsYaml, repoPath, moduleName));
|
|
97
104
|
}
|
|
98
105
|
if (options.deleteFiles) {
|
|
99
|
-
await rm(modulePath(options.target, repoPath, moduleName), { recursive: true, force: true });
|
|
106
|
+
await rm(modulePath(options.target, sourceType, repoPath, moduleName), { recursive: true, force: true });
|
|
100
107
|
}
|
|
101
108
|
if (options.stage) {
|
|
102
109
|
if (options.deleteFiles) {
|
|
103
|
-
await stageAll(git, sourceRepoPath(options.target, repoPath));
|
|
110
|
+
await stageAll(git, sourceRepoPath(options.target, sourceType, repoPath));
|
|
104
111
|
}
|
|
105
112
|
await stageAll(git, options.target);
|
|
106
113
|
}
|
package/dist/source-actions.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { defaultOdooVersion, readEnvironmentMetadata, replaceSourceRepos } from './environment.js';
|
|
2
2
|
import { realGit, stageAll } from './git.js';
|
|
3
3
|
import { listGitmoduleSources, readSourceManifest, sourceManifestEntriesFromMetadata, sourceReposFromManifest, syncManifestFromMetadataAndGitmodules, writeSourceManifest, } from './source-manifest.js';
|
|
4
|
+
function cloneSourceEntries(entries) {
|
|
5
|
+
return entries.map((entry) => ({
|
|
6
|
+
...entry,
|
|
7
|
+
addons: [...entry.addons],
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
4
10
|
export async function listSources(target) {
|
|
5
11
|
const metadata = await readEnvironmentMetadata(target);
|
|
6
12
|
const manifest = await readSourceManifest(target);
|
|
@@ -24,6 +30,23 @@ export function renderSourceList(entries) {
|
|
|
24
30
|
})
|
|
25
31
|
.join('\n');
|
|
26
32
|
}
|
|
33
|
+
export function sourceListJson(entries) {
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
command: 'source list',
|
|
37
|
+
ok: true,
|
|
38
|
+
sources: cloneSourceEntries(entries),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function sourceSyncJson(entries, target) {
|
|
42
|
+
return {
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
command: 'source sync',
|
|
45
|
+
ok: true,
|
|
46
|
+
target,
|
|
47
|
+
sources: cloneSourceEntries(entries),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
27
50
|
export async function syncSources(options, git = realGit) {
|
|
28
51
|
const metadata = await readEnvironmentMetadata(options.target);
|
|
29
52
|
const manifest = await readSourceManifest(options.target);
|
package/dist/status.js
CHANGED
|
@@ -3,6 +3,16 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './compose-layout.js';
|
|
4
4
|
import { defaultOdooVersion, markerPath } from './environment.js';
|
|
5
5
|
import { isValidPathSegment, validateRepoPath } from './path-validation.js';
|
|
6
|
+
const validSourceTypes = ['private', 'oca', 'external'];
|
|
7
|
+
function normalizeSourceType(sourceType) {
|
|
8
|
+
if (typeof sourceType === 'string' && validSourceTypes.includes(sourceType)) {
|
|
9
|
+
return sourceType;
|
|
10
|
+
}
|
|
11
|
+
return 'private';
|
|
12
|
+
}
|
|
13
|
+
function sourceRepoPath(target, sourceType, path) {
|
|
14
|
+
return join(target, 'odoo/custom/src', sourceType, path);
|
|
15
|
+
}
|
|
6
16
|
async function pathExists(path) {
|
|
7
17
|
try {
|
|
8
18
|
await access(path);
|
|
@@ -27,9 +37,11 @@ function parseMetadata(content) {
|
|
|
27
37
|
}
|
|
28
38
|
function sourceRepoPathsFromMetadata(metadata) {
|
|
29
39
|
const sourceRepoPaths = [];
|
|
40
|
+
const sourceRepoLocations = [];
|
|
30
41
|
const invalidSourceRepoPaths = [];
|
|
31
|
-
if (!Array.isArray(metadata.sourceRepos))
|
|
32
|
-
return { sourceRepoPaths, invalidSourceRepoPaths };
|
|
42
|
+
if (!Array.isArray(metadata.sourceRepos)) {
|
|
43
|
+
return { sourceRepoPaths, sourceRepoLocations, invalidSourceRepoPaths };
|
|
44
|
+
}
|
|
33
45
|
for (const repo of metadata.sourceRepos) {
|
|
34
46
|
const path = repo && typeof repo.path === 'string' ? repo.path.trim() : '';
|
|
35
47
|
if (!path)
|
|
@@ -38,9 +50,12 @@ function sourceRepoPathsFromMetadata(metadata) {
|
|
|
38
50
|
invalidSourceRepoPaths.push(path);
|
|
39
51
|
continue;
|
|
40
52
|
}
|
|
41
|
-
|
|
53
|
+
const sourceType = normalizeSourceType(typeof repo.sourceType === 'string' ? repo.sourceType : undefined);
|
|
54
|
+
const normalizedPath = validateRepoPath(path);
|
|
55
|
+
sourceRepoPaths.push(normalizedPath);
|
|
56
|
+
sourceRepoLocations.push({ sourceType, path: normalizedPath });
|
|
42
57
|
}
|
|
43
|
-
return { sourceRepoPaths, invalidSourceRepoPaths };
|
|
58
|
+
return { sourceRepoPaths, sourceRepoLocations, invalidSourceRepoPaths };
|
|
44
59
|
}
|
|
45
60
|
async function missingCoreFiles(target, odooVersion) {
|
|
46
61
|
const missing = [];
|
|
@@ -108,6 +123,21 @@ function summaryText(status) {
|
|
|
108
123
|
: 'Environment ready';
|
|
109
124
|
return `${prefix}: Odoo ${status.odooVersion}, source repos ${status.sourceRepoCount}, module candidates ${status.moduleCandidateCount}.`;
|
|
110
125
|
}
|
|
126
|
+
function isStatusHealthy(status) {
|
|
127
|
+
if (status.kind !== 'environment')
|
|
128
|
+
return false;
|
|
129
|
+
return (status.missingCoreFiles.length === 0 &&
|
|
130
|
+
status.invalidSourceRepoPaths.length === 0 &&
|
|
131
|
+
status.composeErrors.length === 0);
|
|
132
|
+
}
|
|
133
|
+
export function environmentStatusJson(status) {
|
|
134
|
+
return {
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
command: 'status',
|
|
137
|
+
ok: isStatusHealthy(status),
|
|
138
|
+
status,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
111
141
|
export async function getEnvironmentStatus(target) {
|
|
112
142
|
const metadataFullPath = join(target, markerPath);
|
|
113
143
|
if (!(await pathExists(metadataFullPath))) {
|
|
@@ -135,8 +165,8 @@ export async function getEnvironmentStatus(target) {
|
|
|
135
165
|
const odooVersion = typeof metadata.odooVersion === 'string' && metadata.odooVersion.trim()
|
|
136
166
|
? metadata.odooVersion.trim()
|
|
137
167
|
: defaultOdooVersion;
|
|
138
|
-
const { sourceRepoPaths, invalidSourceRepoPaths } = sourceRepoPathsFromMetadata(metadata);
|
|
139
|
-
const repoRoots =
|
|
168
|
+
const { sourceRepoPaths, sourceRepoLocations, invalidSourceRepoPaths } = sourceRepoPathsFromMetadata(metadata);
|
|
169
|
+
const repoRoots = sourceRepoLocations.map(({ sourceType, path }) => sourceRepoPath(target, sourceType, path));
|
|
140
170
|
let moduleCandidateCount = 0;
|
|
141
171
|
for (const repoRoot of repoRoots) {
|
|
142
172
|
moduleCandidateCount += await countModuleCandidatesInRepoPath(repoRoot);
|