@wpmoo/odoo 0.8.60 → 0.8.61

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 CHANGED
@@ -139,10 +139,12 @@ npx @wpmoo/odoo --version
139
139
 
140
140
  npx @wpmoo/odoo status
141
141
  npx @wpmoo/odoo doctor
142
+ npx @wpmoo/odoo doctor --fix
142
143
  npx @wpmoo/odoo add-repo --repo-url https://github.com/example-org/odoo_sample_module_reports.git
143
144
  npx @wpmoo/odoo remove-repo --repo odoo_sample_module_reports
144
145
  npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base
145
146
  npx @wpmoo/odoo remove-module --repo odoo_sample_module --module odoo_sample_module_base
147
+ npx @wpmoo/odoo reset --dry-run
146
148
  npx @wpmoo/odoo reset
147
149
 
148
150
  npx @wpmoo/odoo start
@@ -346,9 +348,15 @@ Docker Compose access, GitHub CLI authentication when available, and PostgreSQL
346
348
  18 compatibility in compose mount targets (for mounts to
347
349
  `/var/lib/postgresql/data` or `/var/lib/postgresql/18/docker`).
348
350
 
351
+ Use `doctor --fix` for safe file-level repairs. It can normalize PostgreSQL 18
352
+ mount targets and regenerate `odoo/custom/manifests/sources.yaml` from
353
+ metadata plus `.gitmodules`, then it runs doctor again and reports any remaining
354
+ manual issues.
355
+
349
356
  Safe reset refreshes generated environment files without deleting product source code:
350
357
 
351
358
  ```bash
359
+ npx @wpmoo/odoo reset --dry-run
352
360
  npx @wpmoo/odoo reset
353
361
  ```
354
362
 
@@ -357,6 +365,9 @@ Safe reset updates generated files such as `.wpmoo/odoo.json`, `moo`,
357
365
  Agent Skills. Compose overlays like `compose.yaml` and `compose/dev.yaml` are
358
366
  also refreshed from the current compose template source.
359
367
 
368
+ Use `reset --dry-run` first when you want a deterministic preview of refreshed
369
+ files and cleanup warnings without writing to the environment.
370
+
360
371
  It does not touch source repo folders under
361
372
  `odoo/custom/src/private`, module source code, Git history, remotes, or
362
373
  branches. It also preserves local runtime artifacts and custom source layout
@@ -373,8 +384,9 @@ Recommended recovery pattern:
373
384
 
374
385
  ```bash
375
386
  ./moo snapshot devel before-reset
387
+ npx @wpmoo/odoo reset --dry-run
376
388
  npx @wpmoo/odoo reset
377
- npx @wpmoo/odoo doctor
389
+ npx @wpmoo/odoo doctor --fix
378
390
  ./moo restore-snapshot before-reset devel
379
391
  ```
380
392
 
package/dist/cli.js CHANGED
@@ -464,11 +464,25 @@ function removeRepoOptionsFromArgs(argv) {
464
464
  stage: booleanOption(values, 'stage', true),
465
465
  };
466
466
  }
467
- function resetOptionsFromArgs(argv) {
467
+ function resetCommandOptionsFromArgs(argv) {
468
468
  const { values } = parseArgs(argv);
469
469
  return {
470
470
  target: resolve(stringOption(values, 'target') ?? process.cwd()),
471
471
  stage: booleanOption(values, 'stage', true),
472
+ dryRun: booleanOption(values, 'dryRun', false),
473
+ };
474
+ }
475
+ function doctorOptionsFromArgs(argv) {
476
+ if (argv.length === 0) {
477
+ return {};
478
+ }
479
+ const { values } = parseArgs(argv);
480
+ const keys = Object.keys(values);
481
+ if (keys.length !== 1 || !Object.hasOwn(values, 'fix')) {
482
+ throw new Error('Usage: wpmoo doctor');
483
+ }
484
+ return {
485
+ fix: booleanOption(values, 'fix', false),
472
486
  };
473
487
  }
474
488
  function sourceUsage() {
@@ -859,17 +873,20 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
859
873
  }
860
874
  if (route.command === 'reset') {
861
875
  console.log(renderBanner());
862
- const options = resetOptionsFromArgs(route.argv);
863
- await safeResetEnvironment(options);
876
+ const options = resetCommandOptionsFromArgs(route.argv);
877
+ if (options.dryRun) {
878
+ console.log(renderSafeResetPreview(options.target, options.stage));
879
+ return;
880
+ }
881
+ const resetOptions = { target: options.target, stage: options.stage };
882
+ await safeResetEnvironment(resetOptions);
864
883
  outro(`Safe reset refreshed generated environment files in ${options.target}.`);
865
884
  return;
866
885
  }
867
886
  if (route.command === 'doctor') {
868
- if (route.argv.length > 0) {
869
- throw new Error('Usage: wpmoo doctor');
870
- }
887
+ const options = doctorOptionsFromArgs(route.argv);
871
888
  console.log(renderBanner());
872
- console.log(await runDoctor(cwd));
889
+ console.log(options.fix === undefined ? await runDoctor(cwd) : await runDoctor(cwd, options));
873
890
  return;
874
891
  }
875
892
  if (route.command === 'status') {
package/dist/doctor.js CHANGED
@@ -1,11 +1,11 @@
1
- import { access, readFile } from 'node:fs/promises';
1
+ import { access, readFile, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { execa } from 'execa';
4
4
  import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './compose-layout.js';
5
5
  import { dailyActionScripts } from './daily-actions.js';
6
6
  import { defaultPostgresVersion } from './external-templates.js';
7
- import { defaultOdooVersion, markerPath } from './environment.js';
8
- import { listGitmoduleSources, readSourceManifest, sourceManifestPath, } from './source-manifest.js';
7
+ import { defaultOdooVersion, markerPath, replaceSourceRepos } from './environment.js';
8
+ import { listGitmoduleSources, readSourceManifest, sourceReposFromManifest, sourceManifestPath, syncManifestFromMetadataAndGitmodules, writeSourceManifest, } from './source-manifest.js';
9
9
  const realCommandRunner = async (command, args, options) => {
10
10
  const result = await execa(command, args, { cwd: options.cwd });
11
11
  return { stdout: result.stdout, stderr: result.stderr };
@@ -37,6 +37,9 @@ function commandErrorText(error) {
37
37
  function isRecord(value) {
38
38
  return typeof value === 'object' && value !== null && !Array.isArray(value);
39
39
  }
40
+ function isDoctorOptions(value) {
41
+ return isRecord(value);
42
+ }
40
43
  const incompatiblePostgres18MountTargets = ['/var/lib/postgresql/data', '/var/lib/postgresql/18/docker'];
41
44
  function parsePostgresMajorFromValue(value) {
42
45
  if (!value)
@@ -63,6 +66,48 @@ function hasInvalidPostgres18Mount(line, mountTarget) {
63
66
  ];
64
67
  return shortPatterns.some((pattern) => pattern.test(line));
65
68
  }
69
+ function isNonAmbiguousLineForMountFix(line, mountTarget) {
70
+ return hasInvalidPostgres18Mount(line, mountTarget);
71
+ }
72
+ function replaceMountTargetInLine(line, from, to) {
73
+ return line.split(from).join(to);
74
+ }
75
+ function normalizePostgres18MountTargetsInComposeContent(content) {
76
+ const fixedTargets = [];
77
+ const fixed = [];
78
+ const hasTrailingNewline = content.endsWith('\n');
79
+ const comparableContent = hasTrailingNewline ? content.slice(0, -1) : content;
80
+ const lines = comparableContent.split(/\r?\n/);
81
+ const nextLines = [];
82
+ for (const line of lines) {
83
+ const commentIndex = line.indexOf('#');
84
+ const comment = commentIndex === -1 ? '' : line.slice(commentIndex);
85
+ const body = commentIndex === -1 ? line : line.slice(0, commentIndex);
86
+ let nextBody = body;
87
+ let lineFixed = false;
88
+ for (const target of incompatiblePostgres18MountTargets) {
89
+ if (!isNonAmbiguousLineForMountFix(body, target))
90
+ continue;
91
+ nextBody = replaceMountTargetInLine(nextBody, target, '/var/lib/postgresql');
92
+ if (!fixedTargets.includes(target)) {
93
+ fixedTargets.push(target);
94
+ }
95
+ lineFixed = true;
96
+ }
97
+ if (lineFixed) {
98
+ fixed.push(line);
99
+ nextLines.push(`${nextBody}${comment}`);
100
+ }
101
+ else {
102
+ nextLines.push(line);
103
+ }
104
+ }
105
+ return {
106
+ content: `${nextLines.join('\n')}${hasTrailingNewline ? '\n' : ''}`,
107
+ fixed,
108
+ fixedTargets,
109
+ };
110
+ }
66
111
  function invalidPostgres18MountTargetsInCompose(content) {
67
112
  const badTargets = new Set();
68
113
  for (const rawLine of content.split(/\r?\n/)) {
@@ -199,7 +244,7 @@ function formatKeyForPath(key) {
199
244
  const [sourceType, ...pathParts] = key.split(':');
200
245
  return sourceRepoPath(sourceType, pathParts.join(':'));
201
246
  }
202
- function checkSourceConsistency(sourceRepos, manifestEntries, gitmoduleSources, manifestExists) {
247
+ function checkSourceConsistency(sourceRepos, manifestEntries, gitmoduleSources, manifestExists, gitmodulesExists) {
203
248
  if (!manifestExists) {
204
249
  return [];
205
250
  }
@@ -224,13 +269,21 @@ function checkSourceConsistency(sourceRepos, manifestEntries, gitmoduleSources,
224
269
  if (!metadataEntries.has(key)) {
225
270
  errors.push(`Manifest source entry missing in metadata: ${formatKeyForPath(key)}`);
226
271
  }
227
- if (!gitmoduleSet.has(key)) {
272
+ if (gitmodulesExists && !gitmoduleSet.has(key)) {
228
273
  errors.push(`Manifest source path missing in .gitmodules: ${formatKeyForPath(key)}`);
229
274
  }
230
275
  }
231
276
  return errors;
232
277
  }
233
- export async function runDoctor(target = process.cwd(), runner = realCommandRunner) {
278
+ async function repairSourceManifestFromDiscoveredState(target, sourceRepos, fallbackBranch, gitmoduleSources) {
279
+ const entries = syncManifestFromMetadataAndGitmodules(sourceRepos, fallbackBranch, gitmoduleSources);
280
+ await writeSourceManifest(target, entries);
281
+ await replaceSourceRepos(target, sourceReposFromManifest(entries));
282
+ }
283
+ export async function runDoctor(target = process.cwd(), runnerOrOptions = realCommandRunner, options = {}) {
284
+ const actualRunner = isDoctorOptions(runnerOrOptions) ? realCommandRunner : runnerOrOptions;
285
+ const actualOptions = isDoctorOptions(runnerOrOptions) ? runnerOrOptions : options;
286
+ const appliedFixes = [];
234
287
  const lines = ['WPMoo doctor'];
235
288
  const errors = [];
236
289
  const warnings = [];
@@ -272,6 +325,16 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
272
325
  errors.push(`Cannot read compose file for compatibility check: ${file}: ${errorMessage(error)}`);
273
326
  continue;
274
327
  }
328
+ if (actualOptions.fix) {
329
+ const normalization = normalizePostgres18MountTargetsInComposeContent(content);
330
+ if (normalization.fixed.length > 0) {
331
+ await writeFile(composePath, normalization.content, 'utf8');
332
+ for (const target of normalization.fixedTargets) {
333
+ appliedFixes.push(`Normalized PostgreSQL 18 mount target in '${file}': replaced '${target}' -> '/var/lib/postgresql'`);
334
+ }
335
+ continue;
336
+ }
337
+ }
275
338
  const badMounts = invalidPostgres18MountTargetsInCompose(content);
276
339
  for (const badMount of badMounts) {
277
340
  errors.push(`PostgreSQL 18 compatibility issue in '${file}': mount target '${badMount}' is invalid; recommend using '/var/lib/postgresql'`);
@@ -301,17 +364,43 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
301
364
  const manifestPath = join(target, sourceManifestPath);
302
365
  const hasManifest = await exists(manifestPath);
303
366
  let manifestEntries = [];
367
+ let manifestReadError;
304
368
  if (hasManifest) {
305
369
  try {
306
370
  manifestEntries = (await readSourceManifest(target)).sources;
307
371
  }
308
372
  catch (error) {
309
- errors.push(`Failed to read source manifest ${sourceManifestPath}: ${errorMessage(error)}`);
373
+ manifestReadError = `Failed to read source manifest ${sourceManifestPath}: ${errorMessage(error)}`;
374
+ if (!actualOptions.fix) {
375
+ errors.push(manifestReadError);
376
+ }
310
377
  }
311
378
  }
312
379
  const gitmoduleSources = await listGitmoduleSources(target);
313
- if (errors.length === 0 || manifestEntries.length > 0) {
314
- errors.push(...checkSourceConsistency(sourceRepos, manifestEntries, gitmoduleSources, hasManifest));
380
+ const hasGitmodules = await exists(join(target, '.gitmodules'));
381
+ const sourceConsistencyIssues = !manifestReadError
382
+ ? checkSourceConsistency(sourceRepos, manifestEntries, gitmoduleSources, hasManifest, hasGitmodules)
383
+ : [];
384
+ const shouldSyncSources = actualOptions.fix &&
385
+ (manifestReadError || sourceConsistencyIssues.length > 0 || (!hasManifest && (sourceRepos.length > 0 || gitmoduleSources.length > 0)));
386
+ if (sourceConsistencyIssues.length > 0) {
387
+ if (actualOptions.fix) {
388
+ const uniqueIssues = [...new Set(sourceConsistencyIssues)];
389
+ appliedFixes.push(...uniqueIssues.map((issue) => `Will regenerate source manifest and metadata to fix: ${issue}`));
390
+ }
391
+ else {
392
+ errors.push(...sourceConsistencyIssues);
393
+ }
394
+ }
395
+ else if (manifestReadError) {
396
+ appliedFixes.push('Will regenerate source manifest and metadata after repairing source manifest read failure.');
397
+ }
398
+ else if (shouldSyncSources) {
399
+ appliedFixes.push('Will create missing source manifest from metadata and .gitmodules state.');
400
+ }
401
+ if (shouldSyncSources && actualOptions.fix) {
402
+ await repairSourceManifestFromDiscoveredState(target, sourceRepos, odooVersion, gitmoduleSources);
403
+ appliedFixes.push('Synced source manifest and metadata with current metadata/.gitmodules state.');
315
404
  }
316
405
  if (env) {
317
406
  const httpPort = validatePort('HTTP_PORT', env, errors);
@@ -324,14 +413,14 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
324
413
  }
325
414
  }
326
415
  try {
327
- await runner('docker', ['version'], { cwd: target });
416
+ await actualRunner('docker', ['version'], { cwd: target });
328
417
  lines.push('OK docker CLI');
329
418
  }
330
419
  catch (error) {
331
420
  errors.push(`Docker CLI check failed: ${errorMessage(error)}`);
332
421
  }
333
422
  try {
334
- await runner('docker', ['compose', 'version'], { cwd: target });
423
+ await actualRunner('docker', ['compose', 'version'], { cwd: target });
335
424
  lines.push('OK docker compose');
336
425
  }
337
426
  catch (error) {
@@ -339,7 +428,7 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
339
428
  }
340
429
  if (sourceRepos.length > 0) {
341
430
  try {
342
- const result = await runner('git', ['submodule', 'status', '--recursive'], { cwd: target });
431
+ const result = await actualRunner('git', ['submodule', 'status', '--recursive'], { cwd: target });
343
432
  const submoduleErrors = sourceSubmoduleStatusErrors(result.stdout, sourceRepos);
344
433
  errors.push(...submoduleErrors);
345
434
  if (submoduleErrors.length === 0) {
@@ -356,16 +445,33 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
356
445
  }
357
446
  }
358
447
  try {
359
- await runner('gh', ['auth', 'status'], { cwd: target });
448
+ await actualRunner('gh', ['auth', 'status'], { cwd: target });
360
449
  lines.push('OK GitHub CLI auth');
361
450
  }
362
451
  catch (error) {
363
452
  warnings.push(`WARN GitHub CLI auth: ${errorMessage(error)}`);
364
453
  }
365
454
  if (errors.length > 0) {
455
+ if (actualOptions.fix && appliedFixes.length > 0) {
456
+ return [
457
+ 'Doctor auto-fixes were not enough to satisfy all checks.',
458
+ ...appliedFixes.map((fix) => `- ${fix}`),
459
+ renderFailure(errors),
460
+ ].join('\n');
461
+ }
366
462
  throw new Error(renderFailure(errors));
367
463
  }
368
464
  lines.push(...warnings);
369
465
  lines.push('Doctor checks passed.');
370
- return lines.join('\n');
466
+ const report = lines.join('\n');
467
+ if (actualOptions.fix && appliedFixes.length > 0) {
468
+ const postFixReport = await runDoctor(target, actualRunner, { ...actualOptions, fix: false });
469
+ return [
470
+ 'Applied safe doctor fixes:',
471
+ ...appliedFixes.map((fix) => `- ${fix}`),
472
+ '',
473
+ postFixReport,
474
+ ].join('\n');
475
+ }
476
+ return report;
371
477
  }
package/dist/help.js CHANGED
@@ -15,8 +15,8 @@ Usage:
15
15
  npx @wpmoo/odoo source remove --repo <name> [--source-type private|oca|external]
16
16
  npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
17
17
  npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name>
18
- npx @wpmoo/odoo reset
19
- npx @wpmoo/odoo doctor
18
+ npx @wpmoo/odoo reset [--dry-run]
19
+ npx @wpmoo/odoo doctor [--fix]
20
20
  npx @wpmoo/odoo start
21
21
  npx @wpmoo/odoo stop
22
22
  npx @wpmoo/odoo logs [service]
@@ -87,6 +87,7 @@ Wizard local-only path:
87
87
  Status and doctor:
88
88
  status: fast and offline. Reads local environment metadata and files only.
89
89
  doctor: deeper health check. May check Docker CLI access and GitHub workflows.
90
+ doctor --fix: applies safe file-level repairs. Runs doctor again after fixes.
90
91
 
91
92
  Task recipes:
92
93
  Create environment:
@@ -105,11 +106,13 @@ Task recipes:
105
106
  npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
106
107
  Safe reset and recover:
107
108
  npx @wpmoo/odoo snapshot [db] [snapshot-name]
109
+ npx @wpmoo/odoo reset --dry-run
108
110
  npx @wpmoo/odoo reset
109
111
  npx @wpmoo/odoo restore-snapshot <snapshot-name> [db]
110
112
  Daily command checks:
111
113
  npx @wpmoo/odoo status
112
114
  npx @wpmoo/odoo doctor
115
+ npx @wpmoo/odoo doctor --fix
113
116
  npx @wpmoo/odoo logs [service]
114
117
  npx @wpmoo/odoo restart
115
118
 
@@ -21,11 +21,12 @@ not validate staging or production deployments.
21
21
  | Compose resource files | Compact compose layout is present (`compose.yaml` + environment overlays under `compose/`), plus config/resources/scripts. | `npx @wpmoo/odoo create ...` |
22
22
  | `./moo` delegation | `./moo` dispatches fixed daily actions to the matching script and preserves argument pass-through. | `./moo <action> ...` |
23
23
  | Doctor checks | Metadata, compose files, scripts, source repo paths, and local tooling checks behave as expected. | `npx @wpmoo/odoo doctor` or `./moo doctor` |
24
- | Generated Postgres checks | For PostgreSQL 18 environments, doctor validates db mount targets avoid old PG image-specific paths. | `npx @wpmoo/odoo doctor` |
24
+ | Doctor safe fixes | Safe file-level fixes are applied only with `--fix`, then doctor runs again and reports any remaining manual issues. | `npx @wpmoo/odoo doctor --fix` |
25
+ | Generated Postgres checks | For PostgreSQL 18 environments, doctor validates db mount targets avoid old PG image-specific paths and can normalize safe targets with `--fix`. | `npx @wpmoo/odoo doctor`, `npx @wpmoo/odoo doctor --fix` |
25
26
  | Source repo add/remove | Source repository registration and submodule lifecycle behave correctly. | `npx @wpmoo/odoo add-repo ...`, `npx @wpmoo/odoo remove-repo ...` |
26
27
  | Source manifest sync | Source repo metadata, `.gitmodules`, and `odoo/custom/manifests/sources.yaml` stay aligned. | `npx @wpmoo/odoo source list`, `npx @wpmoo/odoo source sync` |
27
28
  | Module add/remove | Module registration changes are applied to the selected source repo config. | `npx @wpmoo/odoo add-module ...`, `npx @wpmoo/odoo remove-module ...` |
28
- | Safe reset | Generated files are refreshed (including `compose.yaml` overlays and env example) without deleting source module code. Local runtime/data directories and custom source layout content are preserved; legacy user-editable paths from older templates may remain and are reported for manual cleanup. | `npx @wpmoo/odoo reset` |
29
+ | Safe reset | Generated files are refreshed (including `compose.yaml` overlays and env example) without deleting source module code. Local runtime/data directories and custom source layout content are preserved; legacy user-editable paths from older templates may remain and are reported for manual cleanup. | `npx @wpmoo/odoo reset --dry-run`, `npx @wpmoo/odoo reset` |
29
30
  | Snapshot/restore and lint/pot | These actions are delegated by `./moo` to compose scripts without extra package-side logic. | `./moo snapshot ...`, `./moo restore-snapshot ...`, `./moo lint`, `./moo pot ...` |
30
31
 
31
32
  ## Compact compose checks
@@ -56,6 +57,10 @@ volume and tmpfs mount targets use `/var/lib/postgresql` directly:
56
57
  Paths such as `/var/lib/postgresql/data` and `/var/lib/postgresql/18/docker` are
57
58
  no longer accepted by the package `doctor` check.
58
59
 
60
+ `doctor --fix` may rewrite these safe mount targets to `/var/lib/postgresql`.
61
+ It does not upgrade existing database data; if a real PostgreSQL major upgrade
62
+ is involved, use PostgreSQL upgrade tooling first.
63
+
59
64
  ## Safe reset policy
60
65
 
61
66
  Safe reset intentionally avoids deleting user-editable legacy paths from old
@@ -80,6 +85,9 @@ odoo/custom/patches/
80
85
  odoo/custom/manifests/
81
86
  ```
82
87
 
88
+ Run `npx @wpmoo/odoo reset --dry-run` before writing changes when you need to
89
+ review the generated file refresh plan.
90
+
83
91
  ## Source manifest checks
84
92
 
85
93
  Generated environments include `odoo/custom/manifests/sources.yaml`. The manifest
@@ -99,8 +107,10 @@ manifest and normalize `.wpmoo/odoo.json` source entries:
99
107
  npx @wpmoo/odoo source sync
100
108
  ```
101
109
 
102
- `doctor` fails when manifest entries, metadata entries, and source submodule
103
- paths diverge.
110
+ `doctor` fails when manifest entries, metadata entries, and registered source
111
+ submodule paths diverge. `doctor --fix` can regenerate
112
+ `odoo/custom/manifests/sources.yaml` from metadata plus `.gitmodules` when the
113
+ manifest is missing, unreadable, or stale.
104
114
 
105
115
  ## Local verification commands
106
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.60",
3
+ "version": "0.8.61",
4
4
  "description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
5
5
  "type": "module",
6
6
  "repository": {