@wpmoo/odoo 0.8.60 → 0.8.62

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
@@ -160,6 +162,7 @@ npx @wpmoo/odoo pot sale devel i18n/sale.pot
160
162
 
161
163
  npx @wpmoo/odoo resetdb devel sale
162
164
  npx @wpmoo/odoo snapshot devel before-update
165
+ npx @wpmoo/odoo restore-snapshot --dry-run before-update devel
163
166
  npx @wpmoo/odoo restore-snapshot before-update devel
164
167
  ```
165
168
 
@@ -227,10 +230,17 @@ cp .env.example .env
227
230
  ./moo pot sale devel i18n/sale.pot
228
231
 
229
232
  ./moo snapshot devel before-update
233
+ ./moo restore-snapshot --dry-run before-update devel
230
234
  ./moo restore-snapshot before-update devel
231
235
  ./moo resetdb devel sale
232
236
  ```
233
237
 
238
+ `restore-snapshot --dry-run` validates the selected snapshot and prints the
239
+ restore plan without changing the database or filestore. Generated environments
240
+ also support `WPMOO_SNAPSHOT_RETENTION_COUNT` for pruning old snapshot files.
241
+ When `WPMOO_ENV=stage` or `WPMOO_ENV=prod`, destructive database actions such
242
+ as `resetdb` and real `restore-snapshot` require `WPMOO_ALLOW_DESTRUCTIVE=1`.
243
+
234
244
  Use `npx @wpmoo/odoo ...` for package/operator commands such as `create`, `add-repo`, `remove-repo`, `add-module`, `remove-module`, `status`, `doctor`, and `reset`. Use `./moo ...` inside a generated environment for local daily Compose commands.
235
245
 
236
246
  ## Repository and Module Management
@@ -346,9 +356,15 @@ Docker Compose access, GitHub CLI authentication when available, and PostgreSQL
346
356
  18 compatibility in compose mount targets (for mounts to
347
357
  `/var/lib/postgresql/data` or `/var/lib/postgresql/18/docker`).
348
358
 
359
+ Use `doctor --fix` for safe file-level repairs. It can normalize PostgreSQL 18
360
+ mount targets and regenerate `odoo/custom/manifests/sources.yaml` from
361
+ metadata plus `.gitmodules`, then it runs doctor again and reports any remaining
362
+ manual issues.
363
+
349
364
  Safe reset refreshes generated environment files without deleting product source code:
350
365
 
351
366
  ```bash
367
+ npx @wpmoo/odoo reset --dry-run
352
368
  npx @wpmoo/odoo reset
353
369
  ```
354
370
 
@@ -357,6 +373,9 @@ Safe reset updates generated files such as `.wpmoo/odoo.json`, `moo`,
357
373
  Agent Skills. Compose overlays like `compose.yaml` and `compose/dev.yaml` are
358
374
  also refreshed from the current compose template source.
359
375
 
376
+ Use `reset --dry-run` first when you want a deterministic preview of refreshed
377
+ files and cleanup warnings without writing to the environment.
378
+
360
379
  It does not touch source repo folders under
361
380
  `odoo/custom/src/private`, module source code, Git history, remotes, or
362
381
  branches. It also preserves local runtime artifacts and custom source layout
@@ -373,8 +392,10 @@ Recommended recovery pattern:
373
392
 
374
393
  ```bash
375
394
  ./moo snapshot devel before-reset
395
+ npx @wpmoo/odoo reset --dry-run
376
396
  npx @wpmoo/odoo reset
377
- npx @wpmoo/odoo doctor
397
+ npx @wpmoo/odoo doctor --fix
398
+ ./moo restore-snapshot --dry-run before-reset devel
378
399
  ./moo restore-snapshot before-reset devel
379
400
  ```
380
401
 
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') {
@@ -62,7 +62,7 @@ function usage(command) {
62
62
  if (command === 'snapshot')
63
63
  return 'Usage: wpmoo snapshot [db] [snapshot-name]';
64
64
  if (command === 'restore-snapshot')
65
- return 'Usage: wpmoo restore-snapshot <snapshot-name> [db]';
65
+ return 'Usage: wpmoo restore-snapshot [--dry-run] <snapshot-name> [db]';
66
66
  if (command === 'lint')
67
67
  return 'Usage: wpmoo lint';
68
68
  return 'Usage: wpmoo pot <module[,module]> [db] [output]';
@@ -89,6 +89,17 @@ function positionalArgs(command, argv, min, max) {
89
89
  }
90
90
  return argv;
91
91
  }
92
+ function restoreSnapshotArgs(argv) {
93
+ const args = [...argv];
94
+ const dryRun = args[0] === '--dry-run';
95
+ if (dryRun) {
96
+ args.shift();
97
+ }
98
+ if (args.length < 1 || args.length > 2 || args.some((arg) => arg.startsWith('-'))) {
99
+ throw new Error(usage('restore-snapshot'));
100
+ }
101
+ return dryRun ? ['--dry-run', ...args] : args;
102
+ }
92
103
  function testArgs(argv) {
93
104
  const [modules, ...rest] = argv;
94
105
  if (!modules || modules.startsWith('-'))
@@ -129,7 +140,7 @@ function scriptArgs(command, argv) {
129
140
  if (command === 'snapshot')
130
141
  return positionalArgs(command, argv, 0, 2);
131
142
  if (command === 'restore-snapshot')
132
- return positionalArgs(command, argv, 1, 2);
143
+ return restoreSnapshotArgs(argv);
133
144
  if (command === 'lint')
134
145
  return ensureNoArgs(command, argv);
135
146
  return positionalArgs(command, argv, 1, 3);
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
  }
@@ -37,6 +37,12 @@ export function renderComposeEnvExample(options) {
37
37
  'POSTGRES_PASSWORD=odoo',
38
38
  'ODOO_MASTER_PASSWORD=admin',
39
39
  `ODOO_TEST_MODULE=${defaultTestModule(options)}`,
40
+ 'WPMOO_ENV=dev',
41
+ 'WPMOO_SNAPSHOT_RETENTION_COUNT=0',
42
+ '',
43
+ '# Required only when intentionally running destructive database actions',
44
+ '# such as resetdb or restore-snapshot with WPMOO_ENV=stage or WPMOO_ENV=prod.',
45
+ '# WPMOO_ALLOW_DESTRUCTIVE=1',
40
46
  '',
41
47
  ].join('\n');
42
48
  }
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]
@@ -28,7 +28,7 @@ Usage:
28
28
  npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
29
29
  npx @wpmoo/odoo resetdb [db] [module[,module]]
30
30
  npx @wpmoo/odoo snapshot [db] [snapshot-name]
31
- npx @wpmoo/odoo restore-snapshot <snapshot-name> [db]
31
+ npx @wpmoo/odoo restore-snapshot [--dry-run] <snapshot-name> [db]
32
32
  npx @wpmoo/odoo lint
33
33
  npx @wpmoo/odoo pot <module[,module]> [db] [output]
34
34
 
@@ -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,14 @@ 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
111
+ npx @wpmoo/odoo restore-snapshot --dry-run <snapshot-name> [db]
109
112
  npx @wpmoo/odoo restore-snapshot <snapshot-name> [db]
110
113
  Daily command checks:
111
114
  npx @wpmoo/odoo status
112
115
  npx @wpmoo/odoo doctor
116
+ npx @wpmoo/odoo doctor --fix
113
117
  npx @wpmoo/odoo logs [service]
114
118
  npx @wpmoo/odoo restart
115
119
 
package/dist/templates.js CHANGED
@@ -243,6 +243,7 @@ cp .env.example .env
243
243
 
244
244
  \`\`\`bash
245
245
  ./moo snapshot devel before-update
246
+ ./moo restore-snapshot --dry-run before-update devel
246
247
  ./moo restore-snapshot before-update devel
247
248
  \`\`\`
248
249
 
@@ -371,7 +372,7 @@ usage() {
371
372
  "test") echo "Usage: ./moo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]" ;;
372
373
  "resetdb") echo "Usage: ./moo resetdb [db] [module[,module]]" ;;
373
374
  "snapshot") echo "Usage: ./moo snapshot [db] [snapshot-name]" ;;
374
- "restore-snapshot") echo "Usage: ./moo restore-snapshot <snapshot-name> [db]" ;;
375
+ "restore-snapshot") echo "Usage: ./moo restore-snapshot [--dry-run] <snapshot-name> [db]" ;;
375
376
  "lint") echo "Usage: ./moo lint" ;;
376
377
  "pot") echo "Usage: ./moo pot <module[,module]> [db] [output]" ;;
377
378
  esac
@@ -526,8 +527,14 @@ case "$command" in
526
527
  ;;
527
528
  "restore-snapshot")
528
529
  shift
530
+ restore_args=()
531
+ if [[ "\${1:-}" == "--dry-run" ]]; then
532
+ restore_args+=("--dry-run")
533
+ shift
534
+ fi
529
535
  positional_args "$command" 1 2 "$@"
530
- run_script ./scripts/restore-snapshot.sh "$@"
536
+ restore_args+=("$@")
537
+ run_script ./scripts/restore-snapshot.sh "\${restore_args[@]}"
531
538
  ;;
532
539
  "lint")
533
540
  shift
@@ -692,7 +699,7 @@ Useful maintenance commands:
692
699
  ./moo lint
693
700
  ./moo resetdb [db] [module[,module]]
694
701
  ./moo snapshot [db] [snapshot-name]
695
- ./moo restore-snapshot <snapshot-name> [db]
702
+ ./moo restore-snapshot [--dry-run] <snapshot-name> [db]
696
703
  ./moo pot <module[,module]> [db] [output]
697
704
  \`\`\`
698
705
 
@@ -21,12 +21,13 @@ 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
- | 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 ...` |
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` |
30
+ | Snapshot/restore and lint/pot | These actions are delegated by `./moo` to compose scripts. Restore preview, snapshot retention, and stage/prod destructive guards are preserved by the package argument layer. | `./moo snapshot ...`, `./moo restore-snapshot --dry-run ...`, `./moo restore-snapshot ...`, `./moo lint`, `./moo pot ...` |
30
31
 
31
32
  ## Compact compose checks
32
33
 
@@ -45,6 +46,10 @@ Default local development uses `compose.yaml` plus `compose/dev.yaml`.
45
46
  `WPMOO_ENV=stage` or `WPMOO_ENV=prod` must only be used after production-grade
46
47
  secrets and volumes are configured.
47
48
 
49
+ When `WPMOO_ENV=stage` or `WPMOO_ENV=prod`, generated compose scripts refuse
50
+ destructive database actions such as `resetdb` and real `restore-snapshot`
51
+ unless `.env` explicitly sets `WPMOO_ALLOW_DESTRUCTIVE=1`.
52
+
48
53
  For PostgreSQL 18 environments (including `POSTGRES_IMAGE=postgres:18`), ensure db
49
54
  volume and tmpfs mount targets use `/var/lib/postgresql` directly:
50
55
 
@@ -56,6 +61,10 @@ volume and tmpfs mount targets use `/var/lib/postgresql` directly:
56
61
  Paths such as `/var/lib/postgresql/data` and `/var/lib/postgresql/18/docker` are
57
62
  no longer accepted by the package `doctor` check.
58
63
 
64
+ `doctor --fix` may rewrite these safe mount targets to `/var/lib/postgresql`.
65
+ It does not upgrade existing database data; if a real PostgreSQL major upgrade
66
+ is involved, use PostgreSQL upgrade tooling first.
67
+
59
68
  ## Safe reset policy
60
69
 
61
70
  Safe reset intentionally avoids deleting user-editable legacy paths from old
@@ -80,6 +89,21 @@ odoo/custom/patches/
80
89
  odoo/custom/manifests/
81
90
  ```
82
91
 
92
+ Run `npx @wpmoo/odoo reset --dry-run` before writing changes when you need to
93
+ review the generated file refresh plan.
94
+
95
+ ## Snapshot policy
96
+
97
+ Use restore preview before a destructive restore:
98
+
99
+ ```bash
100
+ ./moo restore-snapshot --dry-run <snapshot-name> [db]
101
+ ```
102
+
103
+ `WPMOO_SNAPSHOT_RETENTION_COUNT` may be set to a positive integer to prune old
104
+ snapshot manifests and their matching dump/filestore files after a new snapshot
105
+ is written.
106
+
83
107
  ## Source manifest checks
84
108
 
85
109
  Generated environments include `odoo/custom/manifests/sources.yaml`. The manifest
@@ -99,8 +123,10 @@ manifest and normalize `.wpmoo/odoo.json` source entries:
99
123
  npx @wpmoo/odoo source sync
100
124
  ```
101
125
 
102
- `doctor` fails when manifest entries, metadata entries, and source submodule
103
- paths diverge.
126
+ `doctor` fails when manifest entries, metadata entries, and registered source
127
+ submodule paths diverge. `doctor --fix` can regenerate
128
+ `odoo/custom/manifests/sources.yaml` from metadata plus `.gitmodules` when the
129
+ manifest is missing, unreadable, or stale.
104
130
 
105
131
  ## Local verification commands
106
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.60",
3
+ "version": "0.8.62",
4
4
  "description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
5
5
  "type": "module",
6
6
  "repository": {