@wpmoo/odoo 0.8.64 → 0.8.66
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 +18 -0
- package/dist/cli.js +52 -17
- package/dist/doctor.js +71 -29
- package/dist/external-assets.js +29 -1
- package/dist/help.js +12 -0
- package/dist/source-actions.js +23 -0
- package/dist/status.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,8 +138,11 @@ 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
148
|
npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
|
|
@@ -319,12 +322,14 @@ Inspect configured sources:
|
|
|
319
322
|
|
|
320
323
|
```bash
|
|
321
324
|
npx @wpmoo/odoo source list
|
|
325
|
+
npx @wpmoo/odoo source list --json
|
|
322
326
|
```
|
|
323
327
|
|
|
324
328
|
Regenerate the manifest and metadata from the current metadata/gitmodule state:
|
|
325
329
|
|
|
326
330
|
```bash
|
|
327
331
|
npx @wpmoo/odoo source sync
|
|
332
|
+
npx @wpmoo/odoo source sync --json
|
|
328
333
|
```
|
|
329
334
|
|
|
330
335
|
`source add` and `source remove` are direct aliases for the same repository
|
|
@@ -344,10 +349,23 @@ npx @wpmoo/odoo source remove --repo server-tools --source-type oca
|
|
|
344
349
|
|
|
345
350
|
```bash
|
|
346
351
|
npx @wpmoo/odoo status
|
|
352
|
+
npx @wpmoo/odoo status --json
|
|
347
353
|
```
|
|
348
354
|
|
|
349
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.
|
|
350
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
|
+
|
|
351
369
|
`doctor` performs deeper checks:
|
|
352
370
|
|
|
353
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;
|
|
@@ -493,17 +499,19 @@ function resetCommandOptionsFromArgs(argv) {
|
|
|
493
499
|
};
|
|
494
500
|
}
|
|
495
501
|
function doctorOptionsFromArgs(argv) {
|
|
496
|
-
if (argv.length === 0) {
|
|
497
|
-
return {};
|
|
498
|
-
}
|
|
499
502
|
const { values } = parseArgs(argv);
|
|
500
503
|
const keys = Object.keys(values);
|
|
501
|
-
|
|
504
|
+
const allowedKeys = new Set(['fix', 'json']);
|
|
505
|
+
if (!keys.every((key) => allowedKeys.has(key))) {
|
|
502
506
|
throw new Error('Usage: wpmoo doctor');
|
|
503
507
|
}
|
|
504
|
-
|
|
505
|
-
|
|
508
|
+
const options = {
|
|
509
|
+
json: jsonOption(values),
|
|
506
510
|
};
|
|
511
|
+
if (Object.hasOwn(values, 'fix')) {
|
|
512
|
+
options.fix = booleanOption(values, 'fix', false);
|
|
513
|
+
}
|
|
514
|
+
return options;
|
|
507
515
|
}
|
|
508
516
|
function sourceUsage() {
|
|
509
517
|
return 'Usage: wpmoo source <list|sync|add|remove> [options]';
|
|
@@ -513,11 +521,15 @@ function sourceSyncOptionsFromArgs(argv) {
|
|
|
513
521
|
return {
|
|
514
522
|
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
515
523
|
stage: booleanOption(values, 'stage', true),
|
|
524
|
+
json: jsonOption(values),
|
|
516
525
|
};
|
|
517
526
|
}
|
|
518
|
-
function
|
|
527
|
+
function sourceListOptionsFromArgs(argv) {
|
|
519
528
|
const { values } = parseArgs(argv);
|
|
520
|
-
return
|
|
529
|
+
return {
|
|
530
|
+
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
531
|
+
json: jsonOption(values),
|
|
532
|
+
};
|
|
521
533
|
}
|
|
522
534
|
async function runSourceCommand(argv) {
|
|
523
535
|
const [subcommand, ...subcommandArgv] = argv;
|
|
@@ -525,15 +537,24 @@ async function runSourceCommand(argv) {
|
|
|
525
537
|
throw new Error(sourceUsage());
|
|
526
538
|
}
|
|
527
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
|
+
}
|
|
528
546
|
console.log(renderBanner());
|
|
529
|
-
|
|
530
|
-
console.log(renderSourceList(await listSources(target)));
|
|
547
|
+
console.log(renderSourceList(sources));
|
|
531
548
|
return;
|
|
532
549
|
}
|
|
533
550
|
if (subcommand === 'sync') {
|
|
534
|
-
console.log(renderBanner());
|
|
535
551
|
const options = sourceSyncOptionsFromArgs(subcommandArgv);
|
|
536
|
-
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());
|
|
537
558
|
outro(`Synced source manifest in ${options.target}.`);
|
|
538
559
|
return;
|
|
539
560
|
}
|
|
@@ -907,14 +928,28 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
907
928
|
}
|
|
908
929
|
if (route.command === 'doctor') {
|
|
909
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
|
+
}
|
|
910
939
|
console.log(renderBanner());
|
|
911
|
-
console.log(options.fix === undefined ? await runDoctor(cwd) : await runDoctor(cwd,
|
|
940
|
+
console.log(options.fix === undefined ? await runDoctor(cwd) : await runDoctor(cwd, doctorOptions));
|
|
912
941
|
return;
|
|
913
942
|
}
|
|
914
943
|
if (route.command === 'status') {
|
|
915
|
-
|
|
944
|
+
const { values } = parseArgs(route.argv);
|
|
945
|
+
const keys = Object.keys(values);
|
|
946
|
+
if (!keys.every((key) => key === 'json')) {
|
|
916
947
|
throw new Error('Usage: wpmoo status');
|
|
917
948
|
}
|
|
949
|
+
if (jsonOption(values)) {
|
|
950
|
+
printJson(environmentStatusJson(await getEnvironmentStatus(cwd)));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
918
953
|
console.log(renderBanner());
|
|
919
954
|
console.log(await renderEnvironmentStatusForTarget(cwd));
|
|
920
955
|
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/external-assets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cp, mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { cp, mkdir, mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { realGit } from './git.js';
|
|
@@ -12,6 +12,14 @@ async function pathExists(path) {
|
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
async function statIfExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
return await stat(path);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
15
23
|
function expandHome(path) {
|
|
16
24
|
if (path === '~')
|
|
17
25
|
return process.env.HOME ?? path;
|
|
@@ -61,6 +69,25 @@ function isExcluded(relativePath, excludes) {
|
|
|
61
69
|
const normalized = relativePath.split('\\').join('/');
|
|
62
70
|
return excludes.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
|
|
63
71
|
}
|
|
72
|
+
async function removeDestinationTypeConflicts(sourcePath, destinationPath, excludes) {
|
|
73
|
+
async function visit(source) {
|
|
74
|
+
const rel = relative(sourcePath, source);
|
|
75
|
+
if (rel && isExcluded(rel, excludes))
|
|
76
|
+
return;
|
|
77
|
+
const sourceStat = await stat(source);
|
|
78
|
+
const destination = rel ? join(destinationPath, rel) : destinationPath;
|
|
79
|
+
const destinationStat = await statIfExists(destination);
|
|
80
|
+
if (destinationStat && sourceStat.isDirectory() !== destinationStat.isDirectory()) {
|
|
81
|
+
await rm(destination, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
if (!sourceStat.isDirectory()) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const entries = await readdir(source);
|
|
87
|
+
await Promise.all(entries.map((entry) => visit(join(source, entry))));
|
|
88
|
+
}
|
|
89
|
+
await visit(sourcePath);
|
|
90
|
+
}
|
|
64
91
|
async function copyDirectory(options, checkedOut) {
|
|
65
92
|
const selectedSourceSubdir = await selectSourceSubdir(options, checkedOut.root);
|
|
66
93
|
const sourcePath = selectedSourceSubdir ? join(checkedOut.root, selectedSourceSubdir) : checkedOut.root;
|
|
@@ -72,6 +99,7 @@ async function copyDirectory(options, checkedOut) {
|
|
|
72
99
|
}
|
|
73
100
|
const excludes = [...defaultExcludes, ...(options.exclude ?? [])];
|
|
74
101
|
await mkdir(destinationPath, { recursive: true });
|
|
102
|
+
await removeDestinationTypeConflicts(sourcePath, destinationPath, excludes);
|
|
75
103
|
await cp(sourcePath, destinationPath, {
|
|
76
104
|
recursive: true,
|
|
77
105
|
force: true,
|
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
19
|
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> [--source-type <category>]
|
|
17
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,6 +52,7 @@ 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
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.
|
|
@@ -121,6 +126,13 @@ Task recipes:
|
|
|
121
126
|
npx @wpmoo/odoo logs [service]
|
|
122
127
|
npx @wpmoo/odoo restart
|
|
123
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
|
+
|
|
124
136
|
Example:
|
|
125
137
|
npx @wpmoo/odoo create \\
|
|
126
138
|
--product odoo_sample_module \\
|
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
|
@@ -123,6 +123,21 @@ function summaryText(status) {
|
|
|
123
123
|
: 'Environment ready';
|
|
124
124
|
return `${prefix}: Odoo ${status.odooVersion}, source repos ${status.sourceRepoCount}, module candidates ${status.moduleCandidateCount}.`;
|
|
125
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
|
+
}
|
|
126
141
|
export async function getEnvironmentStatus(target) {
|
|
127
142
|
const metadataFullPath = join(target, markerPath);
|
|
128
143
|
if (!(await pathExists(metadataFullPath))) {
|