@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 +22 -1
- package/dist/cli.js +24 -7
- package/dist/daily-actions.js +13 -2
- package/dist/doctor.js +120 -14
- package/dist/external-templates.js +6 -0
- package/dist/help.js +7 -3
- package/dist/templates.js +10 -3
- package/docs/generated-environment-verification.md +31 -5
- package/package.json +1 -1
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
|
|
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 =
|
|
863
|
-
|
|
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
|
-
|
|
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/daily-actions.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|