@wpmoo/odoo 0.8.57 → 0.8.59
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 +32 -6
- package/dist/cli.js +24 -2
- package/dist/doctor.js +71 -0
- package/dist/environment.js +72 -4
- package/dist/help.js +3 -2
- package/dist/repo-actions.js +63 -22
- package/dist/safe-reset.js +56 -5
- package/dist/scaffold.js +31 -4
- package/dist/templates.js +60 -38
- package/docs/generated-environment-verification.md +26 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ It gives Odoo teams a repeatable environment layout, a guided cockpit for daily
|
|
|
16
16
|
## Why WPMoo Odoo
|
|
17
17
|
|
|
18
18
|
- Create a local Odoo development environment from a dev repository and one or more source repositories.
|
|
19
|
-
- Keep product source repositories under `odoo/custom/src/private` as Git submodules pinned to the selected Odoo branch.
|
|
19
|
+
- Keep product source repositories under `odoo/custom/src/private`, `odoo/custom/src/oca`, or `odoo/custom/src/external` as Git submodules pinned to the selected Odoo branch.
|
|
20
20
|
- Copy Docker Compose resources from the standalone `wpmoo-org/odoo-docker-compose` resource instead of embedding large runtime assets in the TypeScript package.
|
|
21
21
|
- Optionally copy project-local Agent Skills from `wpmoo-org/odoo-skills` into generated environments.
|
|
22
22
|
- Use either a guided terminal cockpit or direct CLI commands for the same lifecycle tasks.
|
|
@@ -194,8 +194,9 @@ odoo_sample_module_dev/
|
|
|
194
194
|
|-- odoo/
|
|
195
195
|
| `-- custom/
|
|
196
196
|
| `-- src/
|
|
197
|
-
|
|
|
198
|
-
|
|
|
197
|
+
| |-- private/
|
|
198
|
+
| |-- oca/
|
|
199
|
+
| `-- external/
|
|
199
200
|
`-- scripts/
|
|
200
201
|
```
|
|
201
202
|
|
|
@@ -242,6 +243,18 @@ npx @wpmoo/odoo add-repo \
|
|
|
242
243
|
--init-empty-repos
|
|
243
244
|
```
|
|
244
245
|
|
|
246
|
+
Pin source repositories to dedicated source directories:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npx @wpmoo/odoo add-repo \
|
|
250
|
+
--repo-url https://github.com/OCA/sale-workflow.git \
|
|
251
|
+
--source-type oca
|
|
252
|
+
|
|
253
|
+
npx @wpmoo/odoo add-repo \
|
|
254
|
+
--repo-url https://github.com/example-org/odoo_external_tool.git \
|
|
255
|
+
--source-type external
|
|
256
|
+
```
|
|
257
|
+
|
|
245
258
|
GitHub CLI is optional for repository setup. When it is available and authenticated, the interactive flow can:
|
|
246
259
|
|
|
247
260
|
- detect the owner or organization from the current environment;
|
|
@@ -299,7 +312,10 @@ It reports whether the environment is detected, which Odoo version is selected,
|
|
|
299
312
|
npx @wpmoo/odoo doctor
|
|
300
313
|
```
|
|
301
314
|
|
|
302
|
-
It validates metadata, engine support, selected compose files,
|
|
315
|
+
It validates metadata, engine support, selected compose files, source repo paths,
|
|
316
|
+
daily scripts, `.env` settings, Docker CLI access, Docker Compose access, GitHub CLI
|
|
317
|
+
authentication when available, and PostgreSQL 18 compatibility in compose mount
|
|
318
|
+
targets (for mounts to `/var/lib/postgresql/data` or `/var/lib/postgresql/18/docker`).
|
|
303
319
|
|
|
304
320
|
Safe reset refreshes generated environment files without deleting product source code:
|
|
305
321
|
|
|
@@ -309,9 +325,19 @@ npx @wpmoo/odoo reset
|
|
|
309
325
|
|
|
310
326
|
Safe reset updates generated files such as `.wpmoo/odoo.json`, `moo`,
|
|
311
327
|
`.gitignore`, `.env.example`, generated docs, compose assets, and optional
|
|
312
|
-
Agent Skills.
|
|
328
|
+
Agent Skills. Compose overlays like `compose.yaml` and `compose/dev.yaml` are
|
|
329
|
+
also refreshed from the current compose template source.
|
|
330
|
+
|
|
331
|
+
It does not touch source repo folders under
|
|
313
332
|
`odoo/custom/src/private`, module source code, Git history, remotes, or
|
|
314
|
-
branches.
|
|
333
|
+
branches. It also preserves local runtime artifacts and custom source layout
|
|
334
|
+
content:
|
|
335
|
+
|
|
336
|
+
- `.env`, `data`, and `backups`
|
|
337
|
+
- `odoo/custom/src/oca`, `odoo/custom/src/external`, `odoo/custom/patches`,
|
|
338
|
+
`odoo/custom/manifests`, and their existing contents
|
|
339
|
+
|
|
340
|
+
Legacy compose template paths from older scaffolds can remain
|
|
315
341
|
(`docs/assets/`, `test/`, `.github/`) until you remove them manually.
|
|
316
342
|
|
|
317
343
|
Recommended recovery pattern:
|
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,19 @@ function stringOption(values, key) {
|
|
|
79
79
|
const value = values[key];
|
|
80
80
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
81
81
|
}
|
|
82
|
+
function optionalSourceTypeValue(values) {
|
|
83
|
+
const value = stringOption(values, 'sourceType');
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (value === 'private' || value === 'oca' || value === 'external') {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Invalid value for --source-type: ${value}`);
|
|
91
|
+
}
|
|
92
|
+
function sourceTypeValue(values) {
|
|
93
|
+
return optionalSourceTypeValue(values) ?? 'private';
|
|
94
|
+
}
|
|
82
95
|
function booleanOption(values, key, fallback) {
|
|
83
96
|
const value = values[key];
|
|
84
97
|
if (value === undefined)
|
|
@@ -102,6 +115,12 @@ function shellQuote(value) {
|
|
|
102
115
|
return value;
|
|
103
116
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
104
117
|
}
|
|
118
|
+
function renderedSourceRepoPath(target, sourceType, repoPath) {
|
|
119
|
+
if (repoPath) {
|
|
120
|
+
return `${target}/odoo/custom/src/${sourceType}/${repoPath}`;
|
|
121
|
+
}
|
|
122
|
+
return `${target}/odoo/custom/src/${sourceType}`;
|
|
123
|
+
}
|
|
105
124
|
function renderPostCreateGuidance(target, cwd) {
|
|
106
125
|
const relativeTarget = relative(cwd, target) || '.';
|
|
107
126
|
return yellow([
|
|
@@ -309,6 +328,7 @@ async function addRepoOptionsFromArgs(argv) {
|
|
|
309
328
|
target,
|
|
310
329
|
repoUrl: normalizeRepositoryUrl(repoUrl),
|
|
311
330
|
repoPath: stringOption(values, 'repo') ?? stringOption(values, 'sourcePath'),
|
|
331
|
+
sourceType: sourceTypeValue(values),
|
|
312
332
|
odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
|
|
313
333
|
initEmptyRepos: booleanOption(values, 'initEmptyRepos', false),
|
|
314
334
|
stage: booleanOption(values, 'stage', true),
|
|
@@ -335,6 +355,7 @@ async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit'
|
|
|
335
355
|
return {
|
|
336
356
|
target,
|
|
337
357
|
repoUrl,
|
|
358
|
+
sourceType: 'private',
|
|
338
359
|
odooVersion,
|
|
339
360
|
initEmptyRepos: true,
|
|
340
361
|
stage: true,
|
|
@@ -438,6 +459,7 @@ function removeRepoOptionsFromArgs(argv) {
|
|
|
438
459
|
return {
|
|
439
460
|
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
440
461
|
repoPath,
|
|
462
|
+
sourceType: optionalSourceTypeValue(values),
|
|
441
463
|
stage: booleanOption(values, 'stage', true),
|
|
442
464
|
};
|
|
443
465
|
}
|
|
@@ -633,7 +655,7 @@ async function runCockpitCommand(command, cwd) {
|
|
|
633
655
|
const options = await addRepoOptionsFromPrompts(false, 'back');
|
|
634
656
|
await ensureAddRepoGitHubRepository(options, 'back');
|
|
635
657
|
await addModuleRepo(options);
|
|
636
|
-
note(`Added source repo under ${options.target}
|
|
658
|
+
note(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private')}.`, 'Done');
|
|
637
659
|
return 'continue';
|
|
638
660
|
}
|
|
639
661
|
if (command.id === 'remove-repo') {
|
|
@@ -724,7 +746,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
724
746
|
if (options) {
|
|
725
747
|
console.log(renderBanner());
|
|
726
748
|
await addModuleRepo(options);
|
|
727
|
-
outro(`Added source repo under ${options.target}
|
|
749
|
+
outro(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
|
|
728
750
|
return;
|
|
729
751
|
}
|
|
730
752
|
await showStartup(argv, skipUpdateCheck);
|
package/dist/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ 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
|
+
import { defaultPostgresVersion } from './external-templates.js';
|
|
6
7
|
import { defaultOdooVersion, markerPath } from './environment.js';
|
|
7
8
|
const realCommandRunner = async (command, args, options) => {
|
|
8
9
|
const result = await execa(command, args, { cwd: options.cwd });
|
|
@@ -35,6 +36,58 @@ function commandErrorText(error) {
|
|
|
35
36
|
function isRecord(value) {
|
|
36
37
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
38
|
}
|
|
39
|
+
const incompatiblePostgres18MountTargets = ['/var/lib/postgresql/data', '/var/lib/postgresql/18/docker'];
|
|
40
|
+
function parsePostgresMajorFromValue(value) {
|
|
41
|
+
if (!value)
|
|
42
|
+
return undefined;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
if (/^\d{1,3}$/.test(trimmed)) {
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
const match = trimmed.match(/postgres:([0-9]{1,3})(?:[-._][A-Za-z0-9._-]+)?(?:@[\w:.-]+)?/i);
|
|
48
|
+
return match?.[1];
|
|
49
|
+
}
|
|
50
|
+
function stripInlineComment(line) {
|
|
51
|
+
const hashIndex = line.indexOf('#');
|
|
52
|
+
if (hashIndex === -1)
|
|
53
|
+
return line;
|
|
54
|
+
return line.slice(0, hashIndex);
|
|
55
|
+
}
|
|
56
|
+
function hasInvalidPostgres18Mount(line, mountTarget) {
|
|
57
|
+
const escaped = mountTarget.replaceAll('.', '\\.').replaceAll('/', '\\/');
|
|
58
|
+
const shortPatterns = [
|
|
59
|
+
new RegExp(`^\\s*-\\s+.+:\\s*['"]?${escaped}['"]?(?:\\s|:|$)`),
|
|
60
|
+
new RegExp(`^\\s*-\\s*['"]?${escaped}['"]?(?:\\s|$)`),
|
|
61
|
+
new RegExp(`^\\s*target:\\s*['"]?${escaped}['"]?(?:\\s|$)`),
|
|
62
|
+
];
|
|
63
|
+
return shortPatterns.some((pattern) => pattern.test(line));
|
|
64
|
+
}
|
|
65
|
+
function invalidPostgres18MountTargetsInCompose(content) {
|
|
66
|
+
const badTargets = new Set();
|
|
67
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
68
|
+
const line = stripInlineComment(rawLine).trim();
|
|
69
|
+
if (!line)
|
|
70
|
+
continue;
|
|
71
|
+
for (const target of incompatiblePostgres18MountTargets) {
|
|
72
|
+
if (hasInvalidPostgres18Mount(line, target)) {
|
|
73
|
+
badTargets.add(target);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...badTargets];
|
|
78
|
+
}
|
|
79
|
+
function inferPostgresVersion(metadata, odooVersion, env) {
|
|
80
|
+
const envPostgresImage = env?.get('POSTGRES_IMAGE')?.trim();
|
|
81
|
+
const envPostgresMajor = parsePostgresMajorFromValue(envPostgresImage);
|
|
82
|
+
if (envPostgresMajor) {
|
|
83
|
+
return envPostgresMajor;
|
|
84
|
+
}
|
|
85
|
+
const explicitPostgres = parsePostgresMajorFromValue(metadataString(metadata, 'postgresVersion'));
|
|
86
|
+
if (explicitPostgres) {
|
|
87
|
+
return explicitPostgres;
|
|
88
|
+
}
|
|
89
|
+
return defaultPostgresVersion(odooVersion);
|
|
90
|
+
}
|
|
38
91
|
function sourceReposFromMetadata(metadata) {
|
|
39
92
|
const sourceRepos = metadata.sourceRepos;
|
|
40
93
|
if (!Array.isArray(sourceRepos))
|
|
@@ -142,6 +195,24 @@ export async function runDoctor(target = process.cwd(), runner = realCommandRunn
|
|
|
142
195
|
}
|
|
143
196
|
else {
|
|
144
197
|
lines.push(`OK compose files ${composeLayout.files.join(', ')}`);
|
|
198
|
+
const postgresVersion = inferPostgresVersion(metadata, odooVersion, env);
|
|
199
|
+
if (postgresVersion === '18') {
|
|
200
|
+
for (const file of composeLayout.files) {
|
|
201
|
+
const composePath = join(target, file);
|
|
202
|
+
let content;
|
|
203
|
+
try {
|
|
204
|
+
content = await readFile(composePath, 'utf8');
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
errors.push(`Cannot read compose file for compatibility check: ${file}: ${errorMessage(error)}`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const badMounts = invalidPostgres18MountTargetsInCompose(content);
|
|
211
|
+
for (const badMount of badMounts) {
|
|
212
|
+
errors.push(`PostgreSQL 18 compatibility issue in '${file}': mount target '${badMount}' is invalid; recommend using '/var/lib/postgresql'`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
145
216
|
}
|
|
146
217
|
const scriptNames = Object.values(dailyActionScripts);
|
|
147
218
|
const scriptErrorCount = errors.length;
|
package/dist/environment.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { packageName, packageVersion } from './version.js';
|
|
4
|
+
const validSourceTypes = ['private', 'oca', 'external'];
|
|
4
5
|
export const markerPath = '.wpmoo/odoo.json';
|
|
5
6
|
export const defaultOdooVersion = '19.0';
|
|
6
7
|
async function exists(path) {
|
|
@@ -34,23 +35,90 @@ export function environmentMetadata(options) {
|
|
|
34
35
|
export function renderEnvironmentMetadata(options) {
|
|
35
36
|
return `${JSON.stringify(environmentMetadata(options), null, 2)}\n`;
|
|
36
37
|
}
|
|
38
|
+
function normalizeSourceType(sourceType) {
|
|
39
|
+
const normalized = sourceType ?? 'private';
|
|
40
|
+
return validSourceTypes.includes(normalized) ? normalized : 'private';
|
|
41
|
+
}
|
|
42
|
+
function normalizeMetadataSourceRepo(repo) {
|
|
43
|
+
if (!repo || typeof repo !== 'object') {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const candidate = repo;
|
|
47
|
+
const path = typeof candidate.path === 'string' ? candidate.path : '';
|
|
48
|
+
const url = typeof candidate.url === 'string' ? candidate.url : '';
|
|
49
|
+
const addons = Array.isArray(candidate.addons) ? candidate.addons.filter((item) => typeof item === 'string') : [];
|
|
50
|
+
const sourceType = normalizeSourceType(typeof candidate.sourceType === 'string' ? candidate.sourceType : undefined);
|
|
51
|
+
if (!path || !url) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return { ...candidate, path, url, addons, sourceType };
|
|
55
|
+
}
|
|
56
|
+
function sourceRepoWithType(repo) {
|
|
57
|
+
return {
|
|
58
|
+
...repo,
|
|
59
|
+
sourceType: normalizeSourceType(repo.sourceType),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function withoutPathDuplicates(repos) {
|
|
63
|
+
const byPath = new Map();
|
|
64
|
+
repos.forEach((repo) => {
|
|
65
|
+
const normalized = sourceRepoWithType(repo);
|
|
66
|
+
byPath.set(`${normalized.sourceType}:${normalized.path}`, normalized);
|
|
67
|
+
});
|
|
68
|
+
return Array.from(byPath.values());
|
|
69
|
+
}
|
|
37
70
|
export async function readEnvironmentMetadata(target) {
|
|
38
71
|
try {
|
|
39
72
|
const content = await readFile(join(target, markerPath), 'utf8');
|
|
40
|
-
|
|
73
|
+
const metadata = JSON.parse(content);
|
|
74
|
+
if (!metadata?.sourceRepos || !Array.isArray(metadata.sourceRepos)) {
|
|
75
|
+
return metadata;
|
|
76
|
+
}
|
|
77
|
+
metadata.sourceRepos = metadata.sourceRepos
|
|
78
|
+
.map(normalizeMetadataSourceRepo)
|
|
79
|
+
.filter((repo) => Boolean(repo));
|
|
80
|
+
metadata.sourceRepos = withoutPathDuplicates(metadata.sourceRepos);
|
|
81
|
+
return metadata;
|
|
41
82
|
}
|
|
42
83
|
catch {
|
|
43
84
|
return undefined;
|
|
44
85
|
}
|
|
45
86
|
}
|
|
87
|
+
async function writeEnvironmentMetadata(target, metadata) {
|
|
88
|
+
const content = `${JSON.stringify({
|
|
89
|
+
...metadata,
|
|
90
|
+
sourceRepos: metadata.sourceRepos.map(sourceRepoWithType),
|
|
91
|
+
}, null, 2)}\n`;
|
|
92
|
+
await writeFile(join(target, markerPath), content, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
export async function upsertSourceRepoMetadata(target, sourceRepo) {
|
|
95
|
+
const metadata = await readEnvironmentMetadata(target);
|
|
96
|
+
if (!metadata)
|
|
97
|
+
return;
|
|
98
|
+
const normalizedRepo = sourceRepoWithType(sourceRepo);
|
|
99
|
+
const sources = metadata.sourceRepos.filter((repo) => !(repo.path === normalizedRepo.path && normalizeSourceType(repo.sourceType) === normalizedRepo.sourceType));
|
|
100
|
+
sources.push(normalizedRepo);
|
|
101
|
+
metadata.sourceRepos = withoutPathDuplicates(sources);
|
|
102
|
+
await writeEnvironmentMetadata(target, metadata);
|
|
103
|
+
}
|
|
104
|
+
export async function removeSourceRepoMetadata(target, repoPath, sourceType) {
|
|
105
|
+
const metadata = await readEnvironmentMetadata(target);
|
|
106
|
+
if (!metadata)
|
|
107
|
+
return;
|
|
108
|
+
const normalizedType = normalizeSourceType(sourceType);
|
|
109
|
+
metadata.sourceRepos = metadata.sourceRepos.filter((repo) => !(repo.path === repoPath && normalizeSourceType(repo.sourceType) === normalizedType));
|
|
110
|
+
await writeEnvironmentMetadata(target, metadata);
|
|
111
|
+
}
|
|
46
112
|
export async function detectDevelopmentEnvironment(target) {
|
|
47
113
|
if (await readEnvironmentMetadata(target)) {
|
|
48
114
|
return { isEnvironment: true, source: 'marker' };
|
|
49
115
|
}
|
|
50
116
|
const hasAddonsYaml = await exists(join(target, 'odoo/custom/src/addons.yaml'));
|
|
51
117
|
const hasReposYaml = await exists(join(target, 'odoo/custom/src/repos.yaml'));
|
|
52
|
-
const
|
|
53
|
-
|
|
118
|
+
const hasSourceDir = (await exists(join(target, 'odoo/custom/src/private'))) ||
|
|
119
|
+
(await exists(join(target, 'odoo/custom/src/oca'))) ||
|
|
120
|
+
(await exists(join(target, 'odoo/custom/src/external')));
|
|
121
|
+
if (hasAddonsYaml && hasReposYaml && hasSourceDir) {
|
|
54
122
|
return { isEnvironment: true, source: 'layout' };
|
|
55
123
|
}
|
|
56
124
|
return { isEnvironment: false, source: 'none' };
|
package/dist/help.js
CHANGED
|
@@ -7,7 +7,7 @@ Usage:
|
|
|
7
7
|
npx @wpmoo/odoo
|
|
8
8
|
npx @wpmoo/odoo create --product <slug> [--target <path>] --dev-repo-url <url> --source-repo-url <url>
|
|
9
9
|
npx @wpmoo/odoo status
|
|
10
|
-
npx @wpmoo/odoo add-repo --repo-url <url>
|
|
10
|
+
npx @wpmoo/odoo add-repo --repo-url <url> [--source-type private|oca|external]
|
|
11
11
|
npx @wpmoo/odoo remove-repo --repo <name>
|
|
12
12
|
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
|
|
13
13
|
npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name>
|
|
@@ -45,6 +45,7 @@ Options:
|
|
|
45
45
|
--http-port <port> Host HTTP port written to .env.example.
|
|
46
46
|
--gevent-port <port> Host gevent/live chat port written to .env.example.
|
|
47
47
|
--repo-url <url> Source repo URL for add-repo.
|
|
48
|
+
--source-type <category> Source repo category for add-repo/remove-repo. One of private, oca, external. Default: private.
|
|
48
49
|
--repo <name> Source repo folder name for repo/module actions.
|
|
49
50
|
--module <name> Odoo module technical name for module actions.
|
|
50
51
|
--delete-files Also delete module files in remove-module. Default: false.
|
|
@@ -90,7 +91,7 @@ Task recipes:
|
|
|
90
91
|
Create local-only environment:
|
|
91
92
|
npx @wpmoo/odoo
|
|
92
93
|
Add source repo:
|
|
93
|
-
npx @wpmoo/odoo add-repo --repo-url <url>
|
|
94
|
+
npx @wpmoo/odoo add-repo --repo-url <url> --source-type oca
|
|
94
95
|
Add module:
|
|
95
96
|
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
|
|
96
97
|
Run tests:
|
package/dist/repo-actions.js
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { addSourceRepoToAddonsYaml, removeSourceRepoFromAddonsYaml } from './addons-yaml.js';
|
|
4
|
-
import { readEnvironmentMetadata } from './environment.js';
|
|
4
|
+
import { readEnvironmentMetadata, removeSourceRepoMetadata, upsertSourceRepoMetadata } from './environment.js';
|
|
5
5
|
import { ensureRemoteHasBranch, ensureSubmodule, hasUncommittedChanges, realGit, removeSubmodule, stageAll, } from './git.js';
|
|
6
6
|
import { isValidPathSegment, validateRepoPath } from './path-validation.js';
|
|
7
7
|
import { inferRepoPath } from './repo-url.js';
|
|
8
8
|
export const addonsYamlHeader = `# Addons activated from source submodules.
|
|
9
9
|
#
|
|
10
|
-
# Source repos are managed as Git submodules under odoo/custom/src/private.
|
|
10
|
+
# Source repos are managed as Git submodules under odoo/custom/src/private (product code).
|
|
11
|
+
# OCA/external source repos can be placed under odoo/custom/src/oca and odoo/custom/src/external.
|
|
11
12
|
# Do not duplicate these same repos in repos.yaml.
|
|
12
13
|
`;
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const validSourceTypes = ['private', 'oca', 'external'];
|
|
15
|
+
function normalizeSourceType(value) {
|
|
16
|
+
return validSourceTypes.includes(value) ? value : 'private';
|
|
17
|
+
}
|
|
18
|
+
function sourceSubmodulePath(sourceType, repoPath) {
|
|
19
|
+
return `odoo/custom/src/${sourceType}/${validateRepoPath(repoPath)}`;
|
|
20
|
+
}
|
|
21
|
+
function resolveSourceTypeFromSubmodulePath(submodulePath) {
|
|
22
|
+
const match = /^odoo\/custom\/src\/(private|oca|external)\//.exec(submodulePath);
|
|
23
|
+
if (!match)
|
|
24
|
+
return undefined;
|
|
25
|
+
return match[1];
|
|
26
|
+
}
|
|
27
|
+
async function listGitmoduleRepos(target) {
|
|
28
|
+
try {
|
|
29
|
+
const gitmodules = await readFile(join(target, '.gitmodules'), 'utf8');
|
|
30
|
+
return [...gitmodules.matchAll(/^\s*path\s*=\s*odoo\/custom\/src\/(private|oca|external)\/(.+)$/gm)]
|
|
31
|
+
.map((match) => ({ sourceType: match[1], path: match[2].trim() }))
|
|
32
|
+
.filter((entry) => isValidPathSegment(entry.path));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function resolveSubmodulePathFromConfig(target, repoPath, sourceType) {
|
|
39
|
+
if (sourceType) {
|
|
40
|
+
return sourceSubmodulePath(sourceType, validateRepoPath(repoPath));
|
|
41
|
+
}
|
|
42
|
+
const repoMatches = (await listGitmoduleRepos(target)).filter((repo) => repo.path === repoPath);
|
|
43
|
+
if (repoMatches.length === 1) {
|
|
44
|
+
return sourceSubmodulePath(repoMatches[0].sourceType, repoPath);
|
|
45
|
+
}
|
|
46
|
+
if (repoMatches.length > 1) {
|
|
47
|
+
const sorted = repoMatches.map((repo) => repo.sourceType).sort();
|
|
48
|
+
throw new Error(`Source repo ${repoPath} exists in multiple source directories: ${sorted.join(', ')}. Provide --source-type to disambiguate.`);
|
|
49
|
+
}
|
|
50
|
+
return sourceSubmodulePath('private', repoPath);
|
|
15
51
|
}
|
|
16
52
|
export async function readAddonsYaml(target) {
|
|
17
53
|
try {
|
|
@@ -55,20 +91,29 @@ export async function syncComposeOdooConfAddonsPath(target) {
|
|
|
55
91
|
}
|
|
56
92
|
export async function addModuleRepo(options, git = realGit) {
|
|
57
93
|
const repoPath = validateRepoPath(options.repoPath?.trim() || inferRepoPath(options.repoUrl));
|
|
58
|
-
const
|
|
94
|
+
const sourceType = normalizeSourceType(options.sourceType);
|
|
95
|
+
const submodulePath = sourceSubmodulePath(sourceType, repoPath);
|
|
59
96
|
await ensureRemoteHasBranch(git, options.target, options.repoUrl, options.odooVersion, options.initEmptyRepos);
|
|
60
|
-
await mkdir(join(options.target, 'odoo/custom/src
|
|
97
|
+
await mkdir(join(options.target, 'odoo/custom/src', sourceType), { recursive: true });
|
|
61
98
|
await ensureSubmodule(git, options.target, options.repoUrl, options.odooVersion, submodulePath);
|
|
62
99
|
const listedRepos = await listModuleRepos(options.target);
|
|
63
100
|
if (!listedRepos.includes(repoPath)) {
|
|
64
101
|
throw new Error(`Source repo was added but is not registered in .gitmodules: ${repoPath}`);
|
|
65
102
|
}
|
|
103
|
+
await upsertSourceRepoMetadata(options.target, {
|
|
104
|
+
url: options.repoUrl,
|
|
105
|
+
path: repoPath,
|
|
106
|
+
addons: [repoPath],
|
|
107
|
+
sourceType,
|
|
108
|
+
});
|
|
66
109
|
if (!(await isComposeEnvironment(options.target))) {
|
|
67
110
|
const addonsYaml = await readAddonsYaml(options.target);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
if (sourceType === 'private') {
|
|
112
|
+
await writeAddonsYaml(options.target, addSourceRepoToAddonsYaml(addonsYaml, {
|
|
113
|
+
path: repoPath,
|
|
114
|
+
addons: [repoPath],
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
72
117
|
}
|
|
73
118
|
await syncComposeOdooConfAddonsPath(options.target);
|
|
74
119
|
if (options.stage) {
|
|
@@ -76,28 +121,24 @@ export async function addModuleRepo(options, git = realGit) {
|
|
|
76
121
|
}
|
|
77
122
|
}
|
|
78
123
|
export async function listModuleRepos(target) {
|
|
79
|
-
|
|
80
|
-
const gitmodules = await readFile(join(target, '.gitmodules'), 'utf8');
|
|
81
|
-
return [...gitmodules.matchAll(/^\s*path\s*=\s*odoo\/custom\/src\/private\/(.+)$/gm)]
|
|
82
|
-
.map((match) => match[1].trim())
|
|
83
|
-
.filter((repoPath) => repoPath && isValidPathSegment(repoPath))
|
|
84
|
-
.sort();
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
124
|
+
return (await listGitmoduleRepos(target)).map((repo) => repo.path).sort();
|
|
89
125
|
}
|
|
90
126
|
export async function removeModuleRepo(options, git = realGit) {
|
|
91
127
|
const repoPath = validateRepoPath(options.repoPath);
|
|
92
|
-
const
|
|
128
|
+
const sourceType = options.sourceType ? normalizeSourceType(options.sourceType) : undefined;
|
|
129
|
+
const submodulePath = await resolveSubmodulePathFromConfig(options.target, repoPath, sourceType);
|
|
93
130
|
const fullSubmodulePath = join(options.target, submodulePath);
|
|
131
|
+
const resolvedSourceType = sourceType ?? resolveSourceTypeFromSubmodulePath(submodulePath);
|
|
94
132
|
if (await hasUncommittedChanges(git, fullSubmodulePath)) {
|
|
95
133
|
throw new Error(`Cannot remove ${repoPath}: submodule has uncommitted changes.`);
|
|
96
134
|
}
|
|
97
135
|
await removeSubmodule(git, options.target, submodulePath);
|
|
136
|
+
await removeSourceRepoMetadata(options.target, repoPath, resolvedSourceType);
|
|
98
137
|
if (!(await isComposeEnvironment(options.target))) {
|
|
99
138
|
const addonsYaml = await readAddonsYaml(options.target);
|
|
100
|
-
|
|
139
|
+
if (resolvedSourceType === 'private') {
|
|
140
|
+
await writeAddonsYaml(options.target, removeSourceRepoFromAddonsYaml(addonsYaml, repoPath));
|
|
141
|
+
}
|
|
101
142
|
}
|
|
102
143
|
await syncComposeOdooConfAddonsPath(options.target);
|
|
103
144
|
if (options.stage) {
|
package/dist/safe-reset.js
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
|
-
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
|
-
import { readEnvironmentMetadata } from './environment.js';
|
|
3
|
+
import { environmentMetadata, readEnvironmentMetadata } from './environment.js';
|
|
4
4
|
import { applyExternalAsset, writeTextFile } from './external-assets.js';
|
|
5
5
|
import { plannedExternalAssetOptions, renderComposeEnvExample } from './external-templates.js';
|
|
6
6
|
import { realGit, stageAll } from './git.js';
|
|
7
7
|
import { isValidPathSegment, validateAddonName, validateRepoPath } from './path-validation.js';
|
|
8
8
|
import { listModuleRepos, readAddonsYaml } from './repo-actions.js';
|
|
9
9
|
import { generatedFiles } from './scaffold.js';
|
|
10
|
+
const safeResetProtectedPaths = [
|
|
11
|
+
'data',
|
|
12
|
+
'backups',
|
|
13
|
+
'.env',
|
|
14
|
+
'.gitmodules',
|
|
15
|
+
'odoo/custom/src/private',
|
|
16
|
+
'odoo/custom/src/oca',
|
|
17
|
+
'odoo/custom/src/external',
|
|
18
|
+
'odoo/custom/patches',
|
|
19
|
+
'odoo/custom/manifests',
|
|
20
|
+
].map((path) => path.replace(/\/$/, ''));
|
|
21
|
+
const safeResetProtectedGeneratedReadmes = new Set([
|
|
22
|
+
'odoo/custom/src/private/README.md',
|
|
23
|
+
'odoo/custom/src/oca/README.md',
|
|
24
|
+
'odoo/custom/src/external/README.md',
|
|
25
|
+
'odoo/custom/patches/README.md',
|
|
26
|
+
'odoo/custom/manifests/README.md',
|
|
27
|
+
]);
|
|
28
|
+
function isProtectedGeneratedFile(filePath) {
|
|
29
|
+
return safeResetProtectedGeneratedReadmes.has(filePath);
|
|
30
|
+
}
|
|
31
|
+
function mergeEnvironmentMetadata(target, options) {
|
|
32
|
+
const generated = environmentMetadata(options);
|
|
33
|
+
return readFile(join(target, '.wpmoo/odoo.json'), 'utf8')
|
|
34
|
+
.then((content) => JSON.parse(content))
|
|
35
|
+
.then((existing) => {
|
|
36
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
37
|
+
return `${JSON.stringify(generated, null, 2)}\n`;
|
|
38
|
+
}
|
|
39
|
+
return `${JSON.stringify({ ...existing, ...generated, sourceRepos: generated.sourceRepos }, null, 2)}\n`;
|
|
40
|
+
})
|
|
41
|
+
.catch(() => `${JSON.stringify(generated, null, 2)}\n`);
|
|
42
|
+
}
|
|
10
43
|
export function renderSafeResetPreview(target, stage) {
|
|
11
44
|
return [
|
|
12
45
|
'Safe reset will refresh generated WPMoo environment files.',
|
|
@@ -29,8 +62,12 @@ export function renderSafeResetPreview(target, stage) {
|
|
|
29
62
|
'- source repo folders under odoo/custom/src/private',
|
|
30
63
|
'- module source code',
|
|
31
64
|
'- Git history, remotes, or branches',
|
|
65
|
+
'- .env, data, and backups',
|
|
66
|
+
'- custom source layout directories (oca, external, patches, manifests)',
|
|
32
67
|
'- Legacy compose template files may remain until manually removed: docs/assets/, test/, .github/',
|
|
33
68
|
'',
|
|
69
|
+
'Preview-only output; files are not changed until reset is executed.',
|
|
70
|
+
'',
|
|
34
71
|
stage ? 'Generated changes will be staged with git add .' : 'Generated changes will not be staged.',
|
|
35
72
|
].join('\n');
|
|
36
73
|
}
|
|
@@ -42,9 +79,7 @@ function safeResetExternalAssetOptions(options) {
|
|
|
42
79
|
...assetOptions,
|
|
43
80
|
exclude: [
|
|
44
81
|
...(assetOptions.exclude ?? []),
|
|
45
|
-
|
|
46
|
-
'.gitmodules',
|
|
47
|
-
'odoo/custom/src/private',
|
|
82
|
+
...safeResetProtectedPaths,
|
|
48
83
|
],
|
|
49
84
|
}));
|
|
50
85
|
}
|
|
@@ -88,6 +123,15 @@ async function readSubmoduleUrl(target, repoPath) {
|
|
|
88
123
|
return `odoo/custom/src/private/${safeRepoPath}`;
|
|
89
124
|
}
|
|
90
125
|
}
|
|
126
|
+
async function pathExists(path) {
|
|
127
|
+
try {
|
|
128
|
+
await stat(path);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
91
135
|
async function inferOptions(target) {
|
|
92
136
|
const metadata = await readEnvironmentMetadata(target);
|
|
93
137
|
const addonsYaml = await readAddonsYaml(target);
|
|
@@ -130,6 +174,12 @@ export async function safeResetEnvironment(options, git = realGit) {
|
|
|
130
174
|
const files = generatedFiles(scaffoldOptions);
|
|
131
175
|
const externalAssets = safeResetExternalAssetOptions(scaffoldOptions);
|
|
132
176
|
for (const file of files) {
|
|
177
|
+
if (file.path === '.wpmoo/odoo.json') {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (isProtectedGeneratedFile(file.path) && (await pathExists(join(options.target, file.path)))) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
133
183
|
if (file.path === 'odoo/custom/src/addons.yaml') {
|
|
134
184
|
continue;
|
|
135
185
|
}
|
|
@@ -143,6 +193,7 @@ export async function safeResetEnvironment(options, git = realGit) {
|
|
|
143
193
|
for (const assetOptions of externalAssets) {
|
|
144
194
|
await applyExternalAsset(assetOptions, git);
|
|
145
195
|
}
|
|
196
|
+
await writeTextFile(join(options.target, '.wpmoo/odoo.json'), await mergeEnvironmentMetadata(options.target, scaffoldOptions));
|
|
146
197
|
await writeTextFile(join(options.target, '.env.example'), renderComposeEnvExample(scaffoldOptions));
|
|
147
198
|
if (options.stage) {
|
|
148
199
|
await stageAll(git, options.target);
|
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/templates.js
CHANGED
|
@@ -22,46 +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
|
-
├──
|
|
38
|
-
│ ├──
|
|
39
|
-
│
|
|
40
|
-
│
|
|
41
|
-
│ └──
|
|
42
|
-
|
|
43
|
-
├──
|
|
44
|
-
│
|
|
45
|
-
│
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
├──
|
|
49
|
-
│ └──
|
|
50
|
-
│ └──
|
|
51
|
-
|
|
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
|
|
52
57
|
${sourceRepoRows}
|
|
53
|
-
├──
|
|
54
|
-
│
|
|
55
|
-
│
|
|
56
|
-
|
|
57
|
-
├──
|
|
58
|
-
|
|
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`;
|
|
59
68
|
}
|
|
60
69
|
function sourceRepoDocs(options) {
|
|
61
70
|
if (!hasSourceRepos(options)) {
|
|
62
71
|
return `This environment was scaffolded without source repository submodules.
|
|
63
72
|
Add source repositories later from the cockpit or with \`npx @wpmoo/odoo add-repo\`.
|
|
64
|
-
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.`;
|
|
65
81
|
}
|
|
66
82
|
return options.sourceRepos
|
|
67
83
|
.map((repo) => `### ${repo.path}
|
|
@@ -78,6 +94,9 @@ Submodule path:
|
|
|
78
94
|
odoo/custom/src/private/${repo.path}
|
|
79
95
|
\`\`\`
|
|
80
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
|
+
|
|
81
100
|
Expected addon layout:
|
|
82
101
|
|
|
83
102
|
\`\`\`text
|
|
@@ -154,7 +173,7 @@ function environmentKind() {
|
|
|
154
173
|
return 'Docker Compose';
|
|
155
174
|
}
|
|
156
175
|
function repoDuplicationNote() {
|
|
157
|
-
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`.';
|
|
158
177
|
}
|
|
159
178
|
function verificationCommand(options) {
|
|
160
179
|
const firstAddon = allAddons(options)[0] ?? options.product;
|
|
@@ -180,7 +199,8 @@ Set WPMOO_ENV=stage or WPMOO_ENV=prod only after providing production-grade secr
|
|
|
180
199
|
If copied from the standalone resource, additional compose notes are in
|
|
181
200
|
\`docs/compose.md\`.
|
|
182
201
|
|
|
183
|
-
Source repositories stay under \`odoo/custom/src/private\` when
|
|
202
|
+
Source repositories stay under \`odoo/custom/src/{private,oca,external}\` when
|
|
203
|
+
configured. At
|
|
184
204
|
container startup, \`entrypoint.sh\` scans those repositories for addons and
|
|
185
205
|
exposes them through \`/mnt/wpmoo-addons\`.
|
|
186
206
|
|
|
@@ -553,7 +573,8 @@ export function renderReadme(options) {
|
|
|
553
573
|
Private ${environmentKind()} development environment for the ${title} product.
|
|
554
574
|
|
|
555
575
|
This folder owns the development environment only. Product source code lives
|
|
556
|
-
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
|
|
557
578
|
repositories are connected.
|
|
558
579
|
|
|
559
580
|
## Repository Layout
|
|
@@ -603,7 +624,8 @@ export function renderAgents(options) {
|
|
|
603
624
|
? options.sourceRepos.map((repo) => `- \`${repo.path}\`: \`${repo.url}\``).join('\n')
|
|
604
625
|
: '- No source repositories are configured yet.';
|
|
605
626
|
const sourceLayout = hasSourceRepos(options)
|
|
606
|
-
? `Product repositories are Git submodules
|
|
627
|
+
? `Product repositories are Git submodules. They are listed under the private
|
|
628
|
+
source directory below for this environment:
|
|
607
629
|
|
|
608
630
|
\`\`\`text
|
|
609
631
|
${options.sourceRepos.map((repo) => `odoo/custom/src/private/${repo.path}`).join('\n')}
|
|
@@ -21,9 +21,10 @@ 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
25
|
| Source repo add/remove | Source repository registration and submodule lifecycle behave correctly. | `npx @wpmoo/odoo add-repo ...`, `npx @wpmoo/odoo remove-repo ...` |
|
|
25
26
|
| 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.
|
|
27
|
+
| 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` |
|
|
27
28
|
| 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
29
|
|
|
29
30
|
## Compact compose checks
|
|
@@ -43,6 +44,17 @@ Default local development uses `compose.yaml` plus `compose/dev.yaml`.
|
|
|
43
44
|
`WPMOO_ENV=stage` or `WPMOO_ENV=prod` must only be used after production-grade
|
|
44
45
|
secrets and volumes are configured.
|
|
45
46
|
|
|
47
|
+
For PostgreSQL 18 environments (including `POSTGRES_IMAGE=postgres:18`), ensure db
|
|
48
|
+
volume and tmpfs mount targets use `/var/lib/postgresql` directly:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
- volumes:
|
|
52
|
+
- db_data:/var/lib/postgresql
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Paths such as `/var/lib/postgresql/data` and `/var/lib/postgresql/18/docker` are
|
|
56
|
+
no longer accepted by the package `doctor` check.
|
|
57
|
+
|
|
46
58
|
## Safe reset policy
|
|
47
59
|
|
|
48
60
|
Safe reset intentionally avoids deleting user-editable legacy paths from old
|
|
@@ -54,6 +66,19 @@ test/
|
|
|
54
66
|
.github/
|
|
55
67
|
```
|
|
56
68
|
|
|
69
|
+
In addition, safe reset preserves local runtime and source-data state while refreshing
|
|
70
|
+
generated and compose assets:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
.env
|
|
74
|
+
data/
|
|
75
|
+
backups/
|
|
76
|
+
odoo/custom/src/oca/
|
|
77
|
+
odoo/custom/src/external/
|
|
78
|
+
odoo/custom/patches/
|
|
79
|
+
odoo/custom/manifests/
|
|
80
|
+
```
|
|
81
|
+
|
|
57
82
|
## Local verification commands
|
|
58
83
|
|
|
59
84
|
Run from the `wpmoo-odoo` repository root:
|