@wpmoo/odoo 0.8.56 → 0.8.58
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 +25 -4
- package/dist/compose-layout.js +118 -0
- package/dist/doctor.js +11 -34
- package/dist/external-assets.js +14 -2
- package/dist/external-templates.js +12 -1
- package/dist/safe-reset.js +1 -0
- package/dist/scaffold.js +31 -4
- package/dist/status.js +21 -5
- package/dist/templates.js +72 -28
- package/docs/external-resources.md +19 -7
- package/docs/generated-environment-verification.md +30 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,10 @@ It gives Odoo teams a repeatable environment layout, a guided cockpit for daily
|
|
|
28
28
|
- Docker and Docker Compose for generated environment runtime commands
|
|
29
29
|
- GitHub CLI (`gh`) is optional. Use it for repository discovery, repository creation, and deeper diagnostics.
|
|
30
30
|
|
|
31
|
-
The wizard currently offers Odoo `19.0`, `18.0`, `17.0`, and `16.0`.
|
|
31
|
+
The wizard currently offers Odoo `19.0`, `18.0`, `17.0`, and `16.0`. Generated
|
|
32
|
+
environments now use the compact compose layout (`compose.yaml` with
|
|
33
|
+
`compose/<env>.yaml` overlays). Legacy root-level
|
|
34
|
+
`docker-compose_<version>.yml` layouts are still supported for compatibility.
|
|
32
35
|
|
|
33
36
|
Set up GitHub CLI only when you want WPMoo to discover your personal account and organizations or create missing repositories from the interactive wizard:
|
|
34
37
|
|
|
@@ -173,11 +176,20 @@ odoo_sample_module_dev/
|
|
|
173
176
|
|-- .env.example
|
|
174
177
|
|-- AGENTS.md
|
|
175
178
|
|-- README.md
|
|
179
|
+
|-- compose.yaml
|
|
180
|
+
|-- compose/
|
|
181
|
+
| |-- dev.yaml
|
|
182
|
+
| |-- stage.yaml
|
|
183
|
+
| `-- prod.yaml
|
|
184
|
+
|-- config/
|
|
185
|
+
| `-- odoo/
|
|
186
|
+
| `-- odoo.conf
|
|
176
187
|
|-- docs/
|
|
177
188
|
| |-- appstore-release.md
|
|
178
189
|
| `-- compose.md
|
|
179
|
-
|--
|
|
180
|
-
|
|
190
|
+
|-- resources/
|
|
191
|
+
| `-- odoo/
|
|
192
|
+
| `-- entrypoint.sh
|
|
181
193
|
|-- moo
|
|
182
194
|
|-- odoo/
|
|
183
195
|
| `-- custom/
|
|
@@ -187,6 +199,10 @@ odoo_sample_module_dev/
|
|
|
187
199
|
`-- scripts/
|
|
188
200
|
```
|
|
189
201
|
|
|
202
|
+
Development uses `compose.yaml` plus `compose/dev.yaml` by default. Set
|
|
203
|
+
`WPMOO_ENV=stage` or `WPMOO_ENV=prod` only after providing production-grade
|
|
204
|
+
secrets and volumes.
|
|
205
|
+
|
|
190
206
|
The metadata file `.wpmoo/odoo.json` records the product slug, selected Odoo version, dev repo URL, source repos, engine, external resource refs, ports, and template configuration. Status, doctor, daily actions, and safe reset use that metadata instead of guessing from the filesystem.
|
|
191
207
|
|
|
192
208
|
## Daily `./moo` Commands
|
|
@@ -291,7 +307,12 @@ Safe reset refreshes generated environment files without deleting product source
|
|
|
291
307
|
npx @wpmoo/odoo reset
|
|
292
308
|
```
|
|
293
309
|
|
|
294
|
-
Safe reset updates generated files such as `.wpmoo/odoo.json`, `moo`,
|
|
310
|
+
Safe reset updates generated files such as `.wpmoo/odoo.json`, `moo`,
|
|
311
|
+
`.gitignore`, `.env.example`, generated docs, compose assets, and optional
|
|
312
|
+
Agent Skills. It does not touch source repo folders under
|
|
313
|
+
`odoo/custom/src/private`, module source code, Git history, remotes, or
|
|
314
|
+
branches. Legacy compose template paths from older scaffolds can remain
|
|
315
|
+
(`docs/assets/`, `test/`, `.github/`) until you remove them manually.
|
|
295
316
|
|
|
296
317
|
Recommended recovery pattern:
|
|
297
318
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
async function exists(path) {
|
|
4
|
+
try {
|
|
5
|
+
await access(path);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function parseEnvContent(content) {
|
|
13
|
+
const values = new Map();
|
|
14
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
15
|
+
const line = rawLine.trim();
|
|
16
|
+
if (!line || line.startsWith('#'))
|
|
17
|
+
continue;
|
|
18
|
+
const separator = line.indexOf('=');
|
|
19
|
+
if (separator === -1)
|
|
20
|
+
continue;
|
|
21
|
+
const key = line.slice(0, separator).trim();
|
|
22
|
+
let value = line.slice(separator + 1).trim();
|
|
23
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
24
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
25
|
+
value = value.slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
values.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
return values;
|
|
30
|
+
}
|
|
31
|
+
export async function readEnvFile(target) {
|
|
32
|
+
const path = join(target, '.env');
|
|
33
|
+
if (!(await exists(path)))
|
|
34
|
+
return undefined;
|
|
35
|
+
return parseEnvContent(await readFile(path, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
export function selectedComposeEnvironment(env) {
|
|
38
|
+
return env?.get('WPMOO_ENV')?.trim() || 'dev';
|
|
39
|
+
}
|
|
40
|
+
function uniqueStrings(values) {
|
|
41
|
+
return [...new Set(values.filter((value) => value.trim()).map((value) => value.trim()))];
|
|
42
|
+
}
|
|
43
|
+
function isValidComposeEnvironmentName(value) {
|
|
44
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
45
|
+
}
|
|
46
|
+
function isValidOdooVersion(value) {
|
|
47
|
+
return /^\d+\.\d+$/.test(value);
|
|
48
|
+
}
|
|
49
|
+
function compactOverlayError(envName, overlayFile) {
|
|
50
|
+
if (envName === 'dev')
|
|
51
|
+
return `Missing compact compose overlay: ${overlayFile}`;
|
|
52
|
+
return `Missing compact compose overlay for WPMOO_ENV=${envName}: ${overlayFile}`;
|
|
53
|
+
}
|
|
54
|
+
export async function detectComposeLayout(target, options) {
|
|
55
|
+
const envName = options.envName?.trim() || 'dev';
|
|
56
|
+
if (!isValidComposeEnvironmentName(envName)) {
|
|
57
|
+
return {
|
|
58
|
+
kind: 'missing',
|
|
59
|
+
files: [],
|
|
60
|
+
missingFiles: [],
|
|
61
|
+
errors: [`Invalid WPMOO_ENV in .env: expected a simple compose overlay name, got ${envName}`],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const compactBase = 'compose.yaml';
|
|
65
|
+
const compactOverlay = `compose/${envName}.yaml`;
|
|
66
|
+
const hasCompactBase = await exists(join(target, compactBase));
|
|
67
|
+
const hasCompactOverlay = await exists(join(target, compactOverlay));
|
|
68
|
+
if (hasCompactBase && hasCompactOverlay) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'compact',
|
|
71
|
+
envName,
|
|
72
|
+
files: [compactBase, compactOverlay],
|
|
73
|
+
missingFiles: [],
|
|
74
|
+
errors: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (hasCompactBase || hasCompactOverlay) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
const missingFiles = [];
|
|
80
|
+
if (!hasCompactBase) {
|
|
81
|
+
missingFiles.push(compactBase);
|
|
82
|
+
errors.push(`Missing compact compose base: ${compactBase}`);
|
|
83
|
+
}
|
|
84
|
+
if (!hasCompactOverlay) {
|
|
85
|
+
missingFiles.push(compactOverlay);
|
|
86
|
+
errors.push(compactOverlayError(envName, compactOverlay));
|
|
87
|
+
}
|
|
88
|
+
return { kind: 'missing', files: [], missingFiles, errors };
|
|
89
|
+
}
|
|
90
|
+
const odooVersions = uniqueStrings(options.odooVersions);
|
|
91
|
+
const invalidOdooVersions = odooVersions.filter((version) => !isValidOdooVersion(version));
|
|
92
|
+
if (invalidOdooVersions.length > 0) {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'missing',
|
|
95
|
+
files: [],
|
|
96
|
+
missingFiles: [],
|
|
97
|
+
errors: invalidOdooVersions.map((version) => `Invalid Odoo version for compose file: ${version}`),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const legacyFiles = odooVersions.map((version) => `docker-compose_${version}.yml`);
|
|
101
|
+
const missingLegacyFiles = [];
|
|
102
|
+
for (const file of legacyFiles) {
|
|
103
|
+
if (!(await exists(join(target, file)))) {
|
|
104
|
+
missingLegacyFiles.push(file);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (legacyFiles.length > 0 && missingLegacyFiles.length === 0) {
|
|
108
|
+
return { kind: 'legacy', files: legacyFiles, missingFiles: [], errors: [] };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
kind: 'missing',
|
|
112
|
+
files: [],
|
|
113
|
+
missingFiles: missingLegacyFiles,
|
|
114
|
+
errors: legacyFiles.length > 0
|
|
115
|
+
? missingLegacyFiles.map((file) => `Missing compose file: ${file}`)
|
|
116
|
+
: ['Missing compose layout: expected compose.yaml with compose/dev.yaml or a versioned docker-compose file'],
|
|
117
|
+
};
|
|
118
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
|
+
import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './compose-layout.js';
|
|
4
5
|
import { dailyActionScripts } from './daily-actions.js';
|
|
5
6
|
import { defaultOdooVersion, markerPath } from './environment.js';
|
|
6
7
|
const realCommandRunner = async (command, args, options) => {
|
|
@@ -72,31 +73,6 @@ function metadataString(metadata, key) {
|
|
|
72
73
|
const value = metadata[key];
|
|
73
74
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
74
75
|
}
|
|
75
|
-
function parseEnv(content) {
|
|
76
|
-
const values = new Map();
|
|
77
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
78
|
-
const line = rawLine.trim();
|
|
79
|
-
if (!line || line.startsWith('#'))
|
|
80
|
-
continue;
|
|
81
|
-
const separator = line.indexOf('=');
|
|
82
|
-
if (separator === -1)
|
|
83
|
-
continue;
|
|
84
|
-
const key = line.slice(0, separator).trim();
|
|
85
|
-
let value = line.slice(separator + 1).trim();
|
|
86
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
87
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
88
|
-
value = value.slice(1, -1);
|
|
89
|
-
}
|
|
90
|
-
values.set(key, value);
|
|
91
|
-
}
|
|
92
|
-
return values;
|
|
93
|
-
}
|
|
94
|
-
async function readEnv(target) {
|
|
95
|
-
const path = join(target, '.env');
|
|
96
|
-
if (!(await exists(path)))
|
|
97
|
-
return undefined;
|
|
98
|
-
return parseEnv(await readFile(path, 'utf8'));
|
|
99
|
-
}
|
|
100
76
|
function validatePort(name, env, errors) {
|
|
101
77
|
const value = env.get(name)?.trim() ?? '';
|
|
102
78
|
if (!/^\d+$/.test(value)) {
|
|
@@ -151,20 +127,21 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
|
|
|
151
127
|
}
|
|
152
128
|
const odooVersion = metadataString(metadata, 'odooVersion') ?? defaultOdooVersion;
|
|
153
129
|
lines.push(`OK Odoo version ${odooVersion}`);
|
|
154
|
-
const env = await
|
|
130
|
+
const env = await readEnvFile(target);
|
|
155
131
|
const composeVersions = new Set([odooVersion]);
|
|
156
132
|
const envOdooVersion = env?.get('ODOO_VERSION')?.trim();
|
|
157
133
|
if (envOdooVersion) {
|
|
158
134
|
composeVersions.add(envOdooVersion);
|
|
159
135
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
136
|
+
const composeLayout = await detectComposeLayout(target, {
|
|
137
|
+
odooVersions: [...composeVersions],
|
|
138
|
+
envName: selectedComposeEnvironment(env),
|
|
139
|
+
});
|
|
140
|
+
if (composeLayout.kind === 'missing') {
|
|
141
|
+
errors.push(...composeLayout.errors);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
lines.push(`OK compose files ${composeLayout.files.join(', ')}`);
|
|
168
145
|
}
|
|
169
146
|
const scriptNames = Object.values(dailyActionScripts);
|
|
170
147
|
const scriptErrorCount = errors.length;
|
package/dist/external-assets.js
CHANGED
|
@@ -62,7 +62,8 @@ function isExcluded(relativePath, excludes) {
|
|
|
62
62
|
return excludes.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
|
|
63
63
|
}
|
|
64
64
|
async function copyDirectory(options, checkedOut) {
|
|
65
|
-
const
|
|
65
|
+
const selectedSourceSubdir = await selectSourceSubdir(options, checkedOut.root);
|
|
66
|
+
const sourcePath = selectedSourceSubdir ? join(checkedOut.root, selectedSourceSubdir) : checkedOut.root;
|
|
66
67
|
const destinationPath = options.destinationSubdir
|
|
67
68
|
? join(options.destination, options.destinationSubdir)
|
|
68
69
|
: options.destination;
|
|
@@ -80,7 +81,10 @@ async function copyDirectory(options, checkedOut) {
|
|
|
80
81
|
},
|
|
81
82
|
});
|
|
82
83
|
if (options.readmeDestination) {
|
|
83
|
-
const
|
|
84
|
+
const selectedReadmePath = selectedSourceSubdir ? join(checkedOut.root, selectedSourceSubdir, 'README.md') : undefined;
|
|
85
|
+
const readmePath = selectedReadmePath && (await pathExists(selectedReadmePath))
|
|
86
|
+
? selectedReadmePath
|
|
87
|
+
: join(checkedOut.root, 'README.md');
|
|
84
88
|
if (await pathExists(readmePath)) {
|
|
85
89
|
const destination = join(options.destination, options.readmeDestination);
|
|
86
90
|
await mkdir(dirname(destination), { recursive: true });
|
|
@@ -88,6 +92,14 @@ async function copyDirectory(options, checkedOut) {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
}
|
|
95
|
+
async function selectSourceSubdir(options, root) {
|
|
96
|
+
for (const candidate of options.sourceSubdirCandidates ?? []) {
|
|
97
|
+
if (await pathExists(join(root, candidate))) {
|
|
98
|
+
return candidate;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return options.sourceSubdir;
|
|
102
|
+
}
|
|
91
103
|
export function renderExternalAssetCommand(options) {
|
|
92
104
|
const sourcePath = options.sourceSubdir ? `${options.source}/${options.sourceSubdir}` : options.source;
|
|
93
105
|
const destinationPath = options.destinationSubdir
|
|
@@ -46,7 +46,18 @@ export function composeTemplateOptions(options) {
|
|
|
46
46
|
source: options.composeTemplateUrl ?? defaultComposeTemplateUrl,
|
|
47
47
|
destination: options.target,
|
|
48
48
|
ref: options.composeTemplateRef,
|
|
49
|
-
|
|
49
|
+
sourceSubdirCandidates: ['resources/generated-env'],
|
|
50
|
+
exclude: [
|
|
51
|
+
'.github',
|
|
52
|
+
'docs/assets',
|
|
53
|
+
'test',
|
|
54
|
+
'README.md',
|
|
55
|
+
'README-template.md',
|
|
56
|
+
'.gitignore',
|
|
57
|
+
'LICENSE',
|
|
58
|
+
'package.json',
|
|
59
|
+
'package-lock.json',
|
|
60
|
+
],
|
|
50
61
|
readmeDestination: 'docs/compose.md',
|
|
51
62
|
};
|
|
52
63
|
}
|
package/dist/safe-reset.js
CHANGED
|
@@ -29,6 +29,7 @@ export function renderSafeResetPreview(target, stage) {
|
|
|
29
29
|
'- source repo folders under odoo/custom/src/private',
|
|
30
30
|
'- module source code',
|
|
31
31
|
'- Git history, remotes, or branches',
|
|
32
|
+
'- Legacy compose template files may remain until manually removed: docs/assets/, test/, .github/',
|
|
32
33
|
'',
|
|
33
34
|
stage ? 'Generated changes will be staged with git add .' : 'Generated changes will not be staged.',
|
|
34
35
|
].join('\n');
|
package/dist/scaffold.js
CHANGED
|
@@ -22,6 +22,33 @@ function validateScaffoldOptions(options) {
|
|
|
22
22
|
}
|
|
23
23
|
export function generatedFiles(options) {
|
|
24
24
|
const safeOptions = validateScaffoldOptions(options);
|
|
25
|
+
const sourceDirReadmes = [
|
|
26
|
+
{
|
|
27
|
+
path: 'odoo/custom/src/private/README.md',
|
|
28
|
+
title: 'private',
|
|
29
|
+
body: 'Project-owned/private addon repositories go here.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: 'odoo/custom/src/oca/README.md',
|
|
33
|
+
title: 'oca',
|
|
34
|
+
body: 'OCA repositories go here, for example server-tools, web, queue.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: 'odoo/custom/src/external/README.md',
|
|
38
|
+
title: 'external',
|
|
39
|
+
body: 'Non-OCA third-party, vendor, and community addon repositories go here.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: 'odoo/custom/patches/README.md',
|
|
43
|
+
title: 'patches',
|
|
44
|
+
body: 'Local patches for upstream/vendor/OCA repositories go here.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: 'odoo/custom/manifests/README.md',
|
|
48
|
+
title: 'manifests',
|
|
49
|
+
body: 'Manifest/lock/list files for external sources and pinned revisions go here.',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
25
52
|
const files = [
|
|
26
53
|
{ path: markerPath, content: renderEnvironmentMetadata(safeOptions) },
|
|
27
54
|
{ path: 'moo', content: renderMooDelegationScript(), mode: 0o755 },
|
|
@@ -32,10 +59,10 @@ export function generatedFiles(options) {
|
|
|
32
59
|
];
|
|
33
60
|
return [
|
|
34
61
|
...files,
|
|
35
|
-
{
|
|
36
|
-
path:
|
|
37
|
-
content: renderPlaceholder(
|
|
38
|
-
},
|
|
62
|
+
...sourceDirReadmes.map((readme) => ({
|
|
63
|
+
path: readme.path,
|
|
64
|
+
content: renderPlaceholder(readme.title, readme.body),
|
|
65
|
+
})),
|
|
39
66
|
];
|
|
40
67
|
}
|
|
41
68
|
async function writeGeneratedFiles(target, files) {
|
package/dist/status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './compose-layout.js';
|
|
3
4
|
import { defaultOdooVersion, markerPath } from './environment.js';
|
|
4
5
|
import { isValidPathSegment, validateRepoPath } from './path-validation.js';
|
|
5
6
|
async function pathExists(path) {
|
|
@@ -43,12 +44,10 @@ function sourceRepoPathsFromMetadata(metadata) {
|
|
|
43
44
|
}
|
|
44
45
|
async function missingCoreFiles(target, odooVersion) {
|
|
45
46
|
const missing = [];
|
|
46
|
-
const composeFile = `docker-compose_${odooVersion}.yml`;
|
|
47
47
|
const checks = [
|
|
48
48
|
{ label: 'moo', path: join(target, 'moo') },
|
|
49
49
|
{ label: 'README.md', path: join(target, 'README.md') },
|
|
50
50
|
{ label: 'AGENTS.md', path: join(target, 'AGENTS.md') },
|
|
51
|
-
{ label: composeFile, path: join(target, composeFile) },
|
|
52
51
|
{ label: 'scripts/', path: join(target, 'scripts'), mustBeDirectory: true },
|
|
53
52
|
];
|
|
54
53
|
for (const check of checks) {
|
|
@@ -62,7 +61,13 @@ async function missingCoreFiles(target, odooVersion) {
|
|
|
62
61
|
missing.push(check.label);
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
|
-
|
|
64
|
+
const env = await readEnvFile(target);
|
|
65
|
+
const composeLayout = await detectComposeLayout(target, {
|
|
66
|
+
odooVersions: [odooVersion],
|
|
67
|
+
envName: selectedComposeEnvironment(env),
|
|
68
|
+
});
|
|
69
|
+
missing.push(...composeLayout.missingFiles);
|
|
70
|
+
return { missing, composeFiles: composeLayout.files, composeErrors: composeLayout.errors };
|
|
66
71
|
}
|
|
67
72
|
async function countModuleCandidatesInRepoPath(path) {
|
|
68
73
|
if (!(await pathExists(path)))
|
|
@@ -96,7 +101,9 @@ function summaryText(status) {
|
|
|
96
101
|
return 'No WPMoo environment detected.';
|
|
97
102
|
if (status.kind === 'invalid_metadata')
|
|
98
103
|
return 'Environment metadata is invalid.';
|
|
99
|
-
const prefix = status.missingCoreFiles.length > 0 ||
|
|
104
|
+
const prefix = status.missingCoreFiles.length > 0 ||
|
|
105
|
+
status.invalidSourceRepoPaths.length > 0 ||
|
|
106
|
+
status.composeErrors.length > 0
|
|
100
107
|
? 'Environment needs attention'
|
|
101
108
|
: 'Environment ready';
|
|
102
109
|
return `${prefix}: Odoo ${status.odooVersion}, source repos ${status.sourceRepoCount}, module candidates ${status.moduleCandidateCount}.`;
|
|
@@ -134,7 +141,7 @@ export async function getEnvironmentStatus(target) {
|
|
|
134
141
|
for (const repoRoot of repoRoots) {
|
|
135
142
|
moduleCandidateCount += await countModuleCandidatesInRepoPath(repoRoot);
|
|
136
143
|
}
|
|
137
|
-
const missingFiles = await missingCoreFiles(target, odooVersion);
|
|
144
|
+
const { missing: missingFiles, composeFiles, composeErrors, } = await missingCoreFiles(target, odooVersion);
|
|
138
145
|
let recommendedNextAction = 'Run npx @wpmoo/odoo doctor for deep checks or ./moo start.';
|
|
139
146
|
if (invalidSourceRepoPaths.length > 0) {
|
|
140
147
|
recommendedNextAction =
|
|
@@ -143,6 +150,9 @@ export async function getEnvironmentStatus(target) {
|
|
|
143
150
|
else if (missingFiles.length > 0) {
|
|
144
151
|
recommendedNextAction = 'Run npx @wpmoo/odoo reset, then npx @wpmoo/odoo doctor.';
|
|
145
152
|
}
|
|
153
|
+
else if (composeErrors.length > 0) {
|
|
154
|
+
recommendedNextAction = 'Fix compose layout errors, then run npx @wpmoo/odoo doctor.';
|
|
155
|
+
}
|
|
146
156
|
else if (sourceRepoPaths.length === 0) {
|
|
147
157
|
recommendedNextAction = 'Run npx @wpmoo/odoo add-repo ...';
|
|
148
158
|
}
|
|
@@ -155,6 +165,8 @@ export async function getEnvironmentStatus(target) {
|
|
|
155
165
|
sourceRepoPaths,
|
|
156
166
|
invalidSourceRepoPaths,
|
|
157
167
|
moduleCandidateCount,
|
|
168
|
+
composeFiles,
|
|
169
|
+
composeErrors,
|
|
158
170
|
missingCoreFiles: missingFiles,
|
|
159
171
|
recommendedNextAction,
|
|
160
172
|
};
|
|
@@ -177,6 +189,10 @@ export function renderEnvironmentStatus(status) {
|
|
|
177
189
|
}
|
|
178
190
|
lines.push(`Metadata: ${status.metadataPath}`);
|
|
179
191
|
lines.push(`Odoo: ${status.odooVersion}`);
|
|
192
|
+
lines.push(`Compose files: ${status.composeFiles.length > 0 ? status.composeFiles.join(', ') : '(missing)'}`);
|
|
193
|
+
if (status.composeErrors.length > 0) {
|
|
194
|
+
lines.push(`Compose errors: ${status.composeErrors.join(', ')}`);
|
|
195
|
+
}
|
|
180
196
|
lines.push(`Source repos: ${status.sourceRepoCount}`);
|
|
181
197
|
lines.push(`Source repo paths: ${status.sourceRepoPaths.length > 0 ? status.sourceRepoPaths.join(', ') : '(none configured)'}`);
|
|
182
198
|
if (status.invalidSourceRepoPaths.length > 0) {
|
package/dist/templates.js
CHANGED
|
@@ -22,29 +22,62 @@ function hasSourceRepos(options) {
|
|
|
22
22
|
}
|
|
23
23
|
function repositoryLayout(options) {
|
|
24
24
|
const sourceRepoRows = hasSourceRepos(options)
|
|
25
|
-
? options.sourceRepos
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
├──
|
|
34
|
-
├──
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
25
|
+
? options.sourceRepos
|
|
26
|
+
.map((repo, index) => {
|
|
27
|
+
const connector = index === options.sourceRepos.length - 1 ? '└──' : '├──';
|
|
28
|
+
return `│ │ │ ${connector} ${repo.path}/ # Project-owned addon source repository`;
|
|
29
|
+
})
|
|
30
|
+
.join('\n')
|
|
31
|
+
: '│ │ │ └── (add project-owned repos with ./moo add-repo)';
|
|
32
|
+
return `${options.devRepo}/ # Development environment root
|
|
33
|
+
├── compose.yaml # Base Docker Compose file
|
|
34
|
+
├── compose/ # Compose overlays for each workflow
|
|
35
|
+
│ ├── dev.yaml # Local development services
|
|
36
|
+
│ ├── debug.yaml # Debug tooling and debug-friendly settings
|
|
37
|
+
│ ├── test.yaml # Test runner services and test database setup
|
|
38
|
+
│ ├── stage.yaml # Staging-like deployment overlay
|
|
39
|
+
│ ├── prod.yaml # Production deployment overlay
|
|
40
|
+
│ ├── proxy.yaml # Reverse proxy / edge routing overlay
|
|
41
|
+
│ └── tools.yaml # Optional maintenance and helper tools
|
|
42
|
+
├── config/ # Runtime configuration mounted into containers
|
|
43
|
+
│ ├── odoo/ # Odoo server configuration
|
|
44
|
+
│ │ ├── odoo.conf # Main Odoo configuration file
|
|
45
|
+
│ │ └── requirements.txt # Extra Python dependencies for the Odoo container
|
|
46
|
+
│ └── logrotate/ # Log rotation configuration
|
|
47
|
+
│ └── odoo # Logrotate rules for Odoo logs
|
|
48
|
+
├── resources/ # Container-side helper resources
|
|
49
|
+
│ └── odoo/ # Resources specific to the Odoo service
|
|
50
|
+
│ └── entrypoint.sh # Container startup script that discovers addons
|
|
51
|
+
├── moo # Local command hub shortcut
|
|
52
|
+
├── scripts/ # Shell scripts used by the local command hub
|
|
53
|
+
├── odoo/ # Odoo workspace data and custom source tree
|
|
54
|
+
│ └── custom/ # Custom addon layer for this environment
|
|
55
|
+
│ ├── src/ # Source repository checkout root
|
|
56
|
+
│ │ ├── private/ # Project-owned/private addon repositories
|
|
38
57
|
${sourceRepoRows}
|
|
39
|
-
├──
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
│ │ ├── oca/ # OCA addon repositories
|
|
59
|
+
│ │ └── external/ # Non-OCA third-party addon repositories
|
|
60
|
+
│ ├── patches/ # Local patches for upstream repositories
|
|
61
|
+
│ └── manifests/ # Source manifests, locks, and pinned revisions
|
|
62
|
+
├── docs/ # Project-specific documentation
|
|
63
|
+
│ ├── appstore-release.md # Odoo App Store release checklist and notes
|
|
64
|
+
│ └── compose.md # Compose layout and operations reference
|
|
65
|
+
├── .env.example # Template for local environment variables
|
|
66
|
+
├── README.md # This environment overview
|
|
67
|
+
└── AGENTS.md # Agent instructions for this environment`;
|
|
42
68
|
}
|
|
43
69
|
function sourceRepoDocs(options) {
|
|
44
70
|
if (!hasSourceRepos(options)) {
|
|
45
71
|
return `This environment was scaffolded without source repository submodules.
|
|
46
72
|
Add source repositories later from the cockpit or with \`npx @wpmoo/odoo add-repo\`.
|
|
47
|
-
They
|
|
73
|
+
They can be organized under:
|
|
74
|
+
|
|
75
|
+
\`odoo/custom/src/private\` for project-owned/private addon repositories,
|
|
76
|
+
\`odoo/custom/src/oca\` for OCA repositories, and
|
|
77
|
+
\`odoo/custom/src/external\` for non-OCA third-party repositories.
|
|
78
|
+
|
|
79
|
+
Pinned external manifests and local patches should live under
|
|
80
|
+
\`odoo/custom/manifests\` and \`odoo/custom/patches\` respectively.`;
|
|
48
81
|
}
|
|
49
82
|
return options.sourceRepos
|
|
50
83
|
.map((repo) => `### ${repo.path}
|
|
@@ -61,6 +94,9 @@ Submodule path:
|
|
|
61
94
|
odoo/custom/src/private/${repo.path}
|
|
62
95
|
\`\`\`
|
|
63
96
|
|
|
97
|
+
Note: If this repository is an OCA or third-party source, place it under
|
|
98
|
+
\`odoo/custom/src/oca\` or \`odoo/custom/src/external\` according to your policy.
|
|
99
|
+
|
|
64
100
|
Expected addon layout:
|
|
65
101
|
|
|
66
102
|
\`\`\`text
|
|
@@ -137,7 +173,7 @@ function environmentKind() {
|
|
|
137
173
|
return 'Docker Compose';
|
|
138
174
|
}
|
|
139
175
|
function repoDuplicationNote() {
|
|
140
|
-
return 'Keep
|
|
176
|
+
return 'Keep source repositories under the relevant source directory (`private`, `oca`, or `external`); the Compose entrypoint exposes discovered addons through `/mnt/wpmoo-addons`.';
|
|
141
177
|
}
|
|
142
178
|
function verificationCommand(options) {
|
|
143
179
|
const firstAddon = allAddons(options)[0] ?? options.product;
|
|
@@ -146,19 +182,25 @@ function verificationCommand(options) {
|
|
|
146
182
|
function environmentUsageDocs(options) {
|
|
147
183
|
return `## Docker Compose Notes
|
|
148
184
|
|
|
149
|
-
This environment uses the
|
|
150
|
-
are version-specific and static:
|
|
185
|
+
This environment uses the compact WPMoo Compose layout:
|
|
151
186
|
|
|
152
187
|
\`\`\`text
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
188
|
+
compose.yaml
|
|
189
|
+
compose/dev.yaml
|
|
190
|
+
compose/stage.yaml
|
|
191
|
+
compose/prod.yaml
|
|
192
|
+
config/odoo/odoo.conf
|
|
193
|
+
resources/odoo/entrypoint.sh
|
|
156
194
|
\`\`\`
|
|
157
195
|
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
Development uses compose.yaml plus compose/dev.yaml by default.
|
|
197
|
+
Set WPMOO_ENV=stage or WPMOO_ENV=prod only after providing production-grade secrets and volumes.
|
|
198
|
+
|
|
199
|
+
If copied from the standalone resource, additional compose notes are in
|
|
200
|
+
\`docs/compose.md\`.
|
|
160
201
|
|
|
161
|
-
Source repositories stay under \`odoo/custom/src/private\` when
|
|
202
|
+
Source repositories stay under \`odoo/custom/src/{private,oca,external}\` when
|
|
203
|
+
configured. At
|
|
162
204
|
container startup, \`entrypoint.sh\` scans those repositories for addons and
|
|
163
205
|
exposes them through \`/mnt/wpmoo-addons\`.
|
|
164
206
|
|
|
@@ -531,7 +573,8 @@ export function renderReadme(options) {
|
|
|
531
573
|
Private ${environmentKind()} development environment for the ${title} product.
|
|
532
574
|
|
|
533
575
|
This folder owns the development environment only. Product source code lives
|
|
534
|
-
in source repository submodules under \`odoo/custom/src/private
|
|
576
|
+
in source repository submodules under \`odoo/custom/src/private\`,
|
|
577
|
+
\`odoo/custom/src/oca\`, or \`odoo/custom/src/external\` when source
|
|
535
578
|
repositories are connected.
|
|
536
579
|
|
|
537
580
|
## Repository Layout
|
|
@@ -581,7 +624,8 @@ export function renderAgents(options) {
|
|
|
581
624
|
? options.sourceRepos.map((repo) => `- \`${repo.path}\`: \`${repo.url}\``).join('\n')
|
|
582
625
|
: '- No source repositories are configured yet.';
|
|
583
626
|
const sourceLayout = hasSourceRepos(options)
|
|
584
|
-
? `Product repositories are Git submodules
|
|
627
|
+
? `Product repositories are Git submodules. They are listed under the private
|
|
628
|
+
source directory below for this environment:
|
|
585
629
|
|
|
586
630
|
\`\`\`text
|
|
587
631
|
${options.sourceRepos.map((repo) => `odoo/custom/src/private/${repo.path}`).join('\n')}
|
|
@@ -15,21 +15,33 @@ gh:wpmoo-org/odoo-skills
|
|
|
15
15
|
|
|
16
16
|
## Compose resource
|
|
17
17
|
|
|
18
|
-
`wpmoo-org/odoo-docker-compose`
|
|
18
|
+
`wpmoo-org/odoo-docker-compose` now exposes a compact generated-environment payload
|
|
19
|
+
under `resources/generated-env/`:
|
|
19
20
|
|
|
20
21
|
```text
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
compose.yaml
|
|
23
|
+
compose/dev.yaml
|
|
24
|
+
compose/stage.yaml
|
|
25
|
+
compose/prod.yaml
|
|
26
|
+
config/odoo/odoo.conf
|
|
27
|
+
resources/odoo/entrypoint.sh
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
`@wpmoo/odoo` prefers that compact payload first when copying compose assets.
|
|
31
|
+
For pinned older refs that do not provide `resources/generated-env/`, the CLI
|
|
32
|
+
falls back to the legacy repository-root layout (`docker-compose_<version>.yml`
|
|
33
|
+
and related files) for compatibility.
|
|
34
|
+
|
|
35
|
+
Standalone usage with the compact payload:
|
|
27
36
|
|
|
28
37
|
```bash
|
|
29
38
|
git clone https://github.com/wpmoo-org/odoo-docker-compose
|
|
30
39
|
cd odoo-docker-compose
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
mkdir -p ../my_product_dev
|
|
41
|
+
cp -R resources/generated-env/. ../my_product_dev/
|
|
42
|
+
cp .env.example ../my_product_dev/.env
|
|
43
|
+
cd ../my_product_dev
|
|
44
|
+
./scripts/up.sh
|
|
33
45
|
```
|
|
34
46
|
|
|
35
47
|
WPMoo CLI usage with the default remote source:
|
|
@@ -18,14 +18,42 @@ not validate staging or production deployments.
|
|
|
18
18
|
| Area | Contract | Primary command(s) |
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| Scaffold files and metadata | Generated environment includes expected files and `.wpmoo/odoo.json` metadata. | `npx @wpmoo/odoo create ...` |
|
|
21
|
-
| Compose resource files |
|
|
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
|
| Source repo add/remove | Source repository registration and submodule lifecycle behave correctly. | `npx @wpmoo/odoo add-repo ...`, `npx @wpmoo/odoo remove-repo ...` |
|
|
25
25
|
| Module add/remove | Module registration changes are applied to the selected source repo config. | `npx @wpmoo/odoo add-module ...`, `npx @wpmoo/odoo remove-module ...` |
|
|
26
|
-
| Safe reset | Generated files are refreshed without deleting source module code. | `npx @wpmoo/odoo reset` |
|
|
26
|
+
| Safe reset | Generated files are refreshed without deleting source module code. Legacy user-editable paths from older templates may remain and are reported for manual cleanup. | `npx @wpmoo/odoo reset` |
|
|
27
27
|
| 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 ...` |
|
|
28
28
|
|
|
29
|
+
## Compact compose checks
|
|
30
|
+
|
|
31
|
+
Verify the generated environment includes at least:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
compose.yaml
|
|
35
|
+
compose/dev.yaml
|
|
36
|
+
compose/stage.yaml
|
|
37
|
+
compose/prod.yaml
|
|
38
|
+
config/odoo/odoo.conf
|
|
39
|
+
resources/odoo/entrypoint.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Default local development uses `compose.yaml` plus `compose/dev.yaml`.
|
|
43
|
+
`WPMOO_ENV=stage` or `WPMOO_ENV=prod` must only be used after production-grade
|
|
44
|
+
secrets and volumes are configured.
|
|
45
|
+
|
|
46
|
+
## Safe reset policy
|
|
47
|
+
|
|
48
|
+
Safe reset intentionally avoids deleting user-editable legacy paths from old
|
|
49
|
+
compose templates. Preview output must warn when these paths may remain:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
docs/assets/
|
|
53
|
+
test/
|
|
54
|
+
.github/
|
|
55
|
+
```
|
|
56
|
+
|
|
29
57
|
## Local verification commands
|
|
30
58
|
|
|
31
59
|
Run from the `wpmoo-odoo` repository root:
|