@wpmoo/odoo 0.8.55 → 0.8.57

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
@@ -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`. The copied Compose resource must include the matching `docker-compose_<version>.yml` file for the selected branch.
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
- |-- docker-compose_19.0.yml
180
- |-- etc/
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`, `.gitignore`, `.env.example`, generated docs, compose assets, and optional Agent Skills. It does not touch source repo folders under `odoo/custom/src/private`, module source code, Git history, remotes, or branches.
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 readEnv(target);
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
- for (const version of composeVersions) {
161
- const composeFile = `docker-compose_${version}.yml`;
162
- if (await exists(join(target, composeFile))) {
163
- lines.push(`OK compose ${composeFile}`);
164
- }
165
- else {
166
- errors.push(`Missing compose file: ${composeFile}`);
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;
@@ -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 sourcePath = options.sourceSubdir ? join(checkedOut.root, options.sourceSubdir) : checkedOut.root;
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 readmePath = join(checkedOut.root, 'README.md');
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
- exclude: ['README.md', 'README-template.md', '.gitignore', 'LICENSE', 'package.json', 'package-lock.json'],
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
  }
@@ -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/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
- return missing;
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 || status.invalidSourceRepoPaths.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
@@ -23,20 +23,37 @@ function hasSourceRepos(options) {
23
23
  function repositoryLayout(options) {
24
24
  const sourceRepoRows = hasSourceRepos(options)
25
25
  ? options.sourceRepos.map((repo) => `│ ├── ${repo.path}/`).join('\n')
26
- : '│ └── README.md';
26
+ : '│ └── (add repos with ./moo add-repo)';
27
27
  return `${options.devRepo}/
28
- ├── docker-compose_17.0.yml
29
- ├── docker-compose_18.0.yml
30
- ├── docker-compose_19.0.yml
28
+ ├── compose.yaml
29
+ ├── compose/
30
+ ├── dev.yaml
31
+ │ ├── debug.yaml
32
+ │ ├── test.yaml
33
+ │ ├── stage.yaml
34
+ │ ├── prod.yaml
35
+ │ ├── proxy.yaml
36
+ │ └── tools.yaml
37
+ ├── config/
38
+ │ ├── odoo/
39
+ │ │ ├── odoo.conf
40
+ │ │ └── requirements.txt
41
+ │ └── logrotate/
42
+ │ └── odoo
43
+ ├── resources/
44
+ │ └── odoo/
45
+ │ └── entrypoint.sh
31
46
  ├── moo
32
47
  ├── scripts/
33
- ├── etc/
34
48
  ├── odoo/
35
49
  │ └── custom/
36
50
  │ └── src/
37
51
  │ └── private/
38
52
  ${sourceRepoRows}
39
53
  ├── docs/
54
+ │ ├── appstore-release.md
55
+ │ └── compose.md
56
+ ├── .env.example
40
57
  ├── README.md
41
58
  └── AGENTS.md`;
42
59
  }
@@ -146,17 +163,22 @@ function verificationCommand(options) {
146
163
  function environmentUsageDocs(options) {
147
164
  return `## Docker Compose Notes
148
165
 
149
- This environment uses the standalone WPMoo Odoo Compose resource. Compose files
150
- are version-specific and static:
166
+ This environment uses the compact WPMoo Compose layout:
151
167
 
152
168
  \`\`\`text
153
- docker-compose_17.0.yml
154
- docker-compose_18.0.yml
155
- docker-compose_19.0.yml
169
+ compose.yaml
170
+ compose/dev.yaml
171
+ compose/stage.yaml
172
+ compose/prod.yaml
173
+ config/odoo/odoo.conf
174
+ resources/odoo/entrypoint.sh
156
175
  \`\`\`
157
176
 
158
- If copied from the standalone resource, additional compose documentation is kept
159
- in \`docs/compose.md\`.
177
+ Development uses compose.yaml plus compose/dev.yaml by default.
178
+ Set WPMOO_ENV=stage or WPMOO_ENV=prod only after providing production-grade secrets and volumes.
179
+
180
+ If copied from the standalone resource, additional compose notes are in
181
+ \`docs/compose.md\`.
160
182
 
161
183
  Source repositories stay under \`odoo/custom/src/private\` when configured. At
162
184
  container startup, \`entrypoint.sh\` scans those repositories for addons and
@@ -280,9 +302,12 @@ __pycache__/
280
302
 
281
303
  # Local generated files
282
304
  *.code-workspace
305
+ addons/
283
306
  auto/
307
+ backups/
284
308
  data/
285
309
  filestore/
310
+ postgresql/
286
311
  sessions/
287
312
  odoo/custom/auto/
288
313
  odoo/custom/src/*/.git-aggregate-cache/
@@ -15,21 +15,33 @@ gh:wpmoo-org/odoo-skills
15
15
 
16
16
  ## Compose resource
17
17
 
18
- `wpmoo-org/odoo-docker-compose` uses static version-specific files:
18
+ `wpmoo-org/odoo-docker-compose` now exposes a compact generated-environment payload
19
+ under `resources/generated-env/`:
19
20
 
20
21
  ```text
21
- docker-compose_17.0.yml
22
- docker-compose_18.0.yml
23
- docker-compose_19.0.yml
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
- Standalone usage:
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
- cp .env.example .env
32
- docker compose -f docker-compose_19.0.yml up -d
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 | Required compose/version files, compose docs, and script set are present. | `npx @wpmoo/odoo create ...` |
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.55",
3
+ "version": "0.8.57",
4
4
  "description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
5
5
  "type": "module",
6
6
  "repository": {