@wpmoo/odoo 0.8.34 → 0.8.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/args.js +1 -0
- package/dist/cli.js +9 -0
- package/dist/daily-actions.js +61 -5
- package/dist/doctor.js +168 -0
- package/dist/help.js +14 -0
- package/dist/safe-reset.js +21 -1
- package/dist/templates.js +216 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,6 +171,12 @@ npx @wpmoo/odoo remove-module \
|
|
|
171
171
|
--module odoo_sample_module_base
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
Check that a generated environment is structurally ready:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx @wpmoo/odoo doctor
|
|
178
|
+
```
|
|
179
|
+
|
|
174
180
|
Refresh generated environment files without deleting module source code:
|
|
175
181
|
|
|
176
182
|
```bash
|
|
@@ -180,19 +186,35 @@ npx @wpmoo/odoo reset
|
|
|
180
186
|
Run daily local development actions from a generated environment root:
|
|
181
187
|
|
|
182
188
|
```bash
|
|
189
|
+
npx @wpmoo/odoo start
|
|
183
190
|
npx @wpmoo/odoo logs odoo
|
|
184
191
|
npx @wpmoo/odoo restart
|
|
192
|
+
npx @wpmoo/odoo stop
|
|
185
193
|
npx @wpmoo/odoo shell
|
|
186
194
|
npx @wpmoo/odoo psql devel
|
|
187
195
|
npx @wpmoo/odoo install sale devel
|
|
188
196
|
npx @wpmoo/odoo update sale devel
|
|
189
197
|
npx @wpmoo/odoo test sale --db devel --mode update --tags /sale
|
|
198
|
+
npx @wpmoo/odoo resetdb devel sale
|
|
199
|
+
npx @wpmoo/odoo snapshot devel before-update
|
|
200
|
+
npx @wpmoo/odoo restore-snapshot before-update devel
|
|
201
|
+
npx @wpmoo/odoo lint
|
|
202
|
+
npx @wpmoo/odoo pot sale devel i18n/sale.pot
|
|
190
203
|
```
|
|
191
204
|
|
|
205
|
+
The doctor command must be run from a generated environment root containing
|
|
206
|
+
`.wpmoo/odoo.json`. It checks metadata, selected compose files, daily scripts,
|
|
207
|
+
source repo paths, `.env` ports, and Docker CLI access.
|
|
208
|
+
|
|
192
209
|
Daily actions require `.wpmoo/odoo.json` in the current directory and delegate to
|
|
193
210
|
fixed scripts under `./scripts`; they do not search parent directories or accept
|
|
194
211
|
arbitrary script names.
|
|
195
212
|
|
|
213
|
+
Generated environments also include a local `./moo` shortcut for local compose
|
|
214
|
+
daily commands such as `./moo start`, `./moo restart`, and `./moo stop`. The
|
|
215
|
+
shortcut supports the same daily action arguments as `npx @wpmoo/odoo`. It also
|
|
216
|
+
falls back to `npx @wpmoo/odoo@latest doctor` for `./moo doctor`.
|
|
217
|
+
|
|
196
218
|
## Defaults
|
|
197
219
|
|
|
198
220
|
Each source repo can contain one or many Odoo modules. For example:
|
package/dist/args.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { detectDevelopmentEnvironment } from './environment.js';
|
|
|
6
6
|
import { commandOdooVersion } from './environment-version.js';
|
|
7
7
|
import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
|
|
8
8
|
import { isDailyActionCommand, runDailyAction } from './daily-actions.js';
|
|
9
|
+
import { runDoctor } from './doctor.js';
|
|
9
10
|
import { getOriginUrl, realGit } from './git.js';
|
|
10
11
|
import { renderHelp } from './help.js';
|
|
11
12
|
import { addModuleToSourceRepo, listModulesInSourceRepo, removeModuleFromSourceRepo, } from './module-actions.js';
|
|
@@ -676,6 +677,14 @@ async function main() {
|
|
|
676
677
|
outro(`Safe reset refreshed generated environment files in ${options.target}.`);
|
|
677
678
|
return;
|
|
678
679
|
}
|
|
680
|
+
if (route.command === 'doctor') {
|
|
681
|
+
if (route.argv.length > 0) {
|
|
682
|
+
throw new Error('Usage: wpmoo doctor');
|
|
683
|
+
}
|
|
684
|
+
console.log(renderBanner());
|
|
685
|
+
console.log(await runDoctor(process.cwd()));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
679
688
|
if (isDailyActionCommand(route.command)) {
|
|
680
689
|
console.log(renderBanner());
|
|
681
690
|
await runDailyAction(route.command, route.argv);
|
package/dist/daily-actions.js
CHANGED
|
@@ -2,9 +2,26 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { access } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { markerPath } from './environment.js';
|
|
5
|
-
export const dailyActionCommands = [
|
|
5
|
+
export const dailyActionCommands = [
|
|
6
|
+
'start',
|
|
7
|
+
'stop',
|
|
8
|
+
'logs',
|
|
9
|
+
'restart',
|
|
10
|
+
'shell',
|
|
11
|
+
'psql',
|
|
12
|
+
'install',
|
|
13
|
+
'update',
|
|
14
|
+
'test',
|
|
15
|
+
'resetdb',
|
|
16
|
+
'snapshot',
|
|
17
|
+
'restore-snapshot',
|
|
18
|
+
'lint',
|
|
19
|
+
'pot',
|
|
20
|
+
];
|
|
6
21
|
const dailyActionCommandSet = new Set(dailyActionCommands);
|
|
7
|
-
const
|
|
22
|
+
export const dailyActionScripts = {
|
|
23
|
+
start: 'up.sh',
|
|
24
|
+
stop: 'down.sh',
|
|
8
25
|
logs: 'logs.sh',
|
|
9
26
|
restart: 'restart.sh',
|
|
10
27
|
shell: 'shell.sh',
|
|
@@ -12,11 +29,20 @@ const scripts = {
|
|
|
12
29
|
install: 'install.sh',
|
|
13
30
|
update: 'update.sh',
|
|
14
31
|
test: 'test.sh',
|
|
32
|
+
resetdb: 'resetdb.sh',
|
|
33
|
+
snapshot: 'snapshot.sh',
|
|
34
|
+
'restore-snapshot': 'restore-snapshot.sh',
|
|
35
|
+
lint: 'lint.sh',
|
|
36
|
+
pot: 'pot.sh',
|
|
15
37
|
};
|
|
16
38
|
export function isDailyActionCommand(command) {
|
|
17
39
|
return dailyActionCommandSet.has(command);
|
|
18
40
|
}
|
|
19
41
|
function usage(command) {
|
|
42
|
+
if (command === 'start')
|
|
43
|
+
return 'Usage: wpmoo start';
|
|
44
|
+
if (command === 'stop')
|
|
45
|
+
return 'Usage: wpmoo stop';
|
|
20
46
|
if (command === 'logs')
|
|
21
47
|
return 'Usage: wpmoo logs [service]';
|
|
22
48
|
if (command === 'restart')
|
|
@@ -29,7 +55,17 @@ function usage(command) {
|
|
|
29
55
|
return 'Usage: wpmoo install <module[,module]> [db]';
|
|
30
56
|
if (command === 'update')
|
|
31
57
|
return 'Usage: wpmoo update <module[,module]> [db]';
|
|
32
|
-
|
|
58
|
+
if (command === 'test')
|
|
59
|
+
return 'Usage: wpmoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]';
|
|
60
|
+
if (command === 'resetdb')
|
|
61
|
+
return 'Usage: wpmoo resetdb [db] [module[,module]]';
|
|
62
|
+
if (command === 'snapshot')
|
|
63
|
+
return 'Usage: wpmoo snapshot [db] [snapshot-name]';
|
|
64
|
+
if (command === 'restore-snapshot')
|
|
65
|
+
return 'Usage: wpmoo restore-snapshot <snapshot-name> [db]';
|
|
66
|
+
if (command === 'lint')
|
|
67
|
+
return 'Usage: wpmoo lint';
|
|
68
|
+
return 'Usage: wpmoo pot <module[,module]> [db] [output]';
|
|
33
69
|
}
|
|
34
70
|
function ensureNoArgs(command, argv) {
|
|
35
71
|
if (argv.length > 0)
|
|
@@ -47,6 +83,12 @@ function moduleArgs(command, argv) {
|
|
|
47
83
|
throw new Error(usage(command));
|
|
48
84
|
return db ? [modules, db] : [modules];
|
|
49
85
|
}
|
|
86
|
+
function positionalArgs(command, argv, min, max) {
|
|
87
|
+
if (argv.length < min || argv.length > max || argv.some((arg) => arg.startsWith('-'))) {
|
|
88
|
+
throw new Error(usage(command));
|
|
89
|
+
}
|
|
90
|
+
return argv;
|
|
91
|
+
}
|
|
50
92
|
function testArgs(argv) {
|
|
51
93
|
const [modules, ...rest] = argv;
|
|
52
94
|
if (!modules || modules.startsWith('-'))
|
|
@@ -66,6 +108,10 @@ function testArgs(argv) {
|
|
|
66
108
|
return argv;
|
|
67
109
|
}
|
|
68
110
|
function scriptArgs(command, argv) {
|
|
111
|
+
if (command === 'start')
|
|
112
|
+
return ensureNoArgs(command, argv);
|
|
113
|
+
if (command === 'stop')
|
|
114
|
+
return ensureNoArgs(command, argv);
|
|
69
115
|
if (command === 'logs')
|
|
70
116
|
return optionalSingleArg(command, argv, 'odoo');
|
|
71
117
|
if (command === 'restart')
|
|
@@ -76,7 +122,17 @@ function scriptArgs(command, argv) {
|
|
|
76
122
|
return optionalSingleArg(command, argv, 'postgres');
|
|
77
123
|
if (command === 'install' || command === 'update')
|
|
78
124
|
return moduleArgs(command, argv);
|
|
79
|
-
|
|
125
|
+
if (command === 'test')
|
|
126
|
+
return testArgs(argv);
|
|
127
|
+
if (command === 'resetdb')
|
|
128
|
+
return positionalArgs(command, argv, 0, 2);
|
|
129
|
+
if (command === 'snapshot')
|
|
130
|
+
return positionalArgs(command, argv, 0, 2);
|
|
131
|
+
if (command === 'restore-snapshot')
|
|
132
|
+
return positionalArgs(command, argv, 1, 2);
|
|
133
|
+
if (command === 'lint')
|
|
134
|
+
return ensureNoArgs(command, argv);
|
|
135
|
+
return positionalArgs(command, argv, 1, 3);
|
|
80
136
|
}
|
|
81
137
|
async function assertEnvironmentRoot(cwd) {
|
|
82
138
|
try {
|
|
@@ -98,7 +154,7 @@ async function assertScriptExists(cwd, script) {
|
|
|
98
154
|
}
|
|
99
155
|
export async function dailyActionPlan(command, argv, cwd = process.cwd()) {
|
|
100
156
|
await assertEnvironmentRoot(cwd);
|
|
101
|
-
const scriptPath = await assertScriptExists(cwd,
|
|
157
|
+
const scriptPath = await assertScriptExists(cwd, dailyActionScripts[command]);
|
|
102
158
|
return {
|
|
103
159
|
cwd,
|
|
104
160
|
scriptPath,
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { dailyActionScripts } from './daily-actions.js';
|
|
5
|
+
import { defaultOdooVersion, markerPath } from './environment.js';
|
|
6
|
+
const realCommandRunner = async (command, args, options) => {
|
|
7
|
+
const result = await execa(command, args, { cwd: options.cwd });
|
|
8
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
9
|
+
};
|
|
10
|
+
async function exists(path) {
|
|
11
|
+
try {
|
|
12
|
+
await access(path);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function errorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function sourceReposFromMetadata(metadata) {
|
|
26
|
+
const sourceRepos = metadata.sourceRepos;
|
|
27
|
+
if (!Array.isArray(sourceRepos))
|
|
28
|
+
return [];
|
|
29
|
+
return sourceRepos.map((repo, index) => {
|
|
30
|
+
if (!isRecord(repo) || typeof repo.path !== 'string' || !repo.path.trim()) {
|
|
31
|
+
throw new Error(`Invalid sourceRepos entry in .wpmoo/odoo.json at index ${index}`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
url: typeof repo.url === 'string' ? repo.url : '',
|
|
35
|
+
path: repo.path.trim(),
|
|
36
|
+
addons: Array.isArray(repo.addons) ? repo.addons.filter((addon) => typeof addon === 'string') : [],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function readMetadata(target) {
|
|
41
|
+
let content;
|
|
42
|
+
try {
|
|
43
|
+
content = await readFile(join(target, markerPath), 'utf8');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`Missing metadata file: ${markerPath}`);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(content);
|
|
50
|
+
if (!isRecord(parsed)) {
|
|
51
|
+
throw new Error('metadata is not an object');
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new Error(`Invalid metadata JSON in ${markerPath}: ${errorMessage(error)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function metadataString(metadata, key) {
|
|
60
|
+
const value = metadata[key];
|
|
61
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
62
|
+
}
|
|
63
|
+
function parseEnv(content) {
|
|
64
|
+
const values = new Map();
|
|
65
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
66
|
+
const line = rawLine.trim();
|
|
67
|
+
if (!line || line.startsWith('#'))
|
|
68
|
+
continue;
|
|
69
|
+
const separator = line.indexOf('=');
|
|
70
|
+
if (separator === -1)
|
|
71
|
+
continue;
|
|
72
|
+
const key = line.slice(0, separator).trim();
|
|
73
|
+
let value = line.slice(separator + 1).trim();
|
|
74
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
75
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
76
|
+
value = value.slice(1, -1);
|
|
77
|
+
}
|
|
78
|
+
values.set(key, value);
|
|
79
|
+
}
|
|
80
|
+
return values;
|
|
81
|
+
}
|
|
82
|
+
async function readEnv(target) {
|
|
83
|
+
const path = join(target, '.env');
|
|
84
|
+
if (!(await exists(path)))
|
|
85
|
+
return undefined;
|
|
86
|
+
return parseEnv(await readFile(path, 'utf8'));
|
|
87
|
+
}
|
|
88
|
+
function validatePort(name, env, errors) {
|
|
89
|
+
const value = env.get(name)?.trim() ?? '';
|
|
90
|
+
if (!/^\d+$/.test(value)) {
|
|
91
|
+
errors.push(`Invalid ${name} in .env: expected a non-empty numeric value`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function renderFailure(errors) {
|
|
96
|
+
return ['WPMoo doctor failed:', ...errors.map((error) => `- ${error}`)].join('\n');
|
|
97
|
+
}
|
|
98
|
+
export async function runDoctor(target = process.cwd(), runner = realCommandRunner) {
|
|
99
|
+
const lines = ['WPMoo doctor'];
|
|
100
|
+
const errors = [];
|
|
101
|
+
const metadata = await readMetadata(target);
|
|
102
|
+
lines.push(`OK metadata ${markerPath}`);
|
|
103
|
+
const engine = metadataString(metadata, 'engine') ?? 'compose';
|
|
104
|
+
if (engine !== 'compose') {
|
|
105
|
+
errors.push(`Unsupported environment engine: ${engine}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
lines.push('OK engine compose');
|
|
109
|
+
}
|
|
110
|
+
const odooVersion = metadataString(metadata, 'odooVersion') ?? defaultOdooVersion;
|
|
111
|
+
lines.push(`OK Odoo version ${odooVersion}`);
|
|
112
|
+
const env = await readEnv(target);
|
|
113
|
+
const composeVersions = new Set([odooVersion]);
|
|
114
|
+
const envOdooVersion = env?.get('ODOO_VERSION')?.trim();
|
|
115
|
+
if (envOdooVersion) {
|
|
116
|
+
composeVersions.add(envOdooVersion);
|
|
117
|
+
}
|
|
118
|
+
for (const version of composeVersions) {
|
|
119
|
+
const composeFile = `docker-compose_${version}.yml`;
|
|
120
|
+
if (await exists(join(target, composeFile))) {
|
|
121
|
+
lines.push(`OK compose ${composeFile}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
errors.push(`Missing compose file: ${composeFile}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const scriptNames = Object.values(dailyActionScripts);
|
|
128
|
+
const scriptErrorCount = errors.length;
|
|
129
|
+
for (const script of scriptNames) {
|
|
130
|
+
const relativePath = `scripts/${script}`;
|
|
131
|
+
if (!(await exists(join(target, relativePath)))) {
|
|
132
|
+
errors.push(`Missing daily action script: ${relativePath}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (errors.length === scriptErrorCount) {
|
|
136
|
+
lines.push(`OK scripts ${scriptNames.length} checked`);
|
|
137
|
+
}
|
|
138
|
+
const sourceRepos = sourceReposFromMetadata(metadata);
|
|
139
|
+
for (const repo of sourceRepos) {
|
|
140
|
+
const relativePath = `odoo/custom/src/private/${repo.path}`;
|
|
141
|
+
if (!(await exists(join(target, relativePath)))) {
|
|
142
|
+
errors.push(`Missing source repo path: ${relativePath}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push(`OK source repos ${sourceRepos.length} checked`);
|
|
146
|
+
if (env) {
|
|
147
|
+
const httpPort = validatePort('HTTP_PORT', env, errors);
|
|
148
|
+
const geventPort = validatePort('GEVENT_PORT', env, errors);
|
|
149
|
+
if (httpPort && geventPort && httpPort === geventPort) {
|
|
150
|
+
errors.push('HTTP_PORT and GEVENT_PORT in .env must not be equal');
|
|
151
|
+
}
|
|
152
|
+
if (/^\d+$/.test(httpPort) && /^\d+$/.test(geventPort) && httpPort !== geventPort) {
|
|
153
|
+
lines.push(`OK .env ports HTTP_PORT=${httpPort} GEVENT_PORT=${geventPort}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await runner('docker', ['version'], { cwd: target });
|
|
158
|
+
lines.push('OK docker CLI');
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
errors.push(`Docker CLI check failed: ${errorMessage(error)}`);
|
|
162
|
+
}
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
throw new Error(renderFailure(errors));
|
|
165
|
+
}
|
|
166
|
+
lines.push('Doctor checks passed.');
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -11,6 +11,9 @@ Usage:
|
|
|
11
11
|
npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
|
|
12
12
|
npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name>
|
|
13
13
|
npx @wpmoo/odoo reset
|
|
14
|
+
npx @wpmoo/odoo doctor
|
|
15
|
+
npx @wpmoo/odoo start
|
|
16
|
+
npx @wpmoo/odoo stop
|
|
14
17
|
npx @wpmoo/odoo logs [service]
|
|
15
18
|
npx @wpmoo/odoo restart
|
|
16
19
|
npx @wpmoo/odoo shell
|
|
@@ -18,6 +21,11 @@ Usage:
|
|
|
18
21
|
npx @wpmoo/odoo install <module[,module]> [db]
|
|
19
22
|
npx @wpmoo/odoo update <module[,module]> [db]
|
|
20
23
|
npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
|
|
24
|
+
npx @wpmoo/odoo resetdb [db] [module[,module]]
|
|
25
|
+
npx @wpmoo/odoo snapshot [db] [snapshot-name]
|
|
26
|
+
npx @wpmoo/odoo restore-snapshot <snapshot-name> [db]
|
|
27
|
+
npx @wpmoo/odoo lint
|
|
28
|
+
npx @wpmoo/odoo pot <module[,module]> [db] [output]
|
|
21
29
|
|
|
22
30
|
Options:
|
|
23
31
|
--product <slug> Product slug, for example my_odoo_module.
|
|
@@ -55,6 +63,12 @@ Options:
|
|
|
55
63
|
Daily actions:
|
|
56
64
|
Daily actions must be run from a generated environment root containing .wpmoo/odoo.json.
|
|
57
65
|
They delegate to the fixed scripts copied from the compose resource under ./scripts.
|
|
66
|
+
Generated environments also include ./moo for local compose commands such as ./moo start.
|
|
67
|
+
Use ./moo or npx @wpmoo/odoo with the same daily action arguments.
|
|
68
|
+
|
|
69
|
+
Doctor:
|
|
70
|
+
Run npx @wpmoo/odoo doctor from a generated environment root to check metadata,
|
|
71
|
+
compose files, daily scripts, source repo paths, .env ports, and Docker CLI access.
|
|
58
72
|
|
|
59
73
|
Example:
|
|
60
74
|
npx @wpmoo/odoo create \\
|
package/dist/safe-reset.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import { readEnvironmentMetadata } from './environment.js';
|
|
4
|
+
import { applyExternalAsset, writeTextFile } from './external-assets.js';
|
|
5
|
+
import { plannedExternalAssetOptions, renderComposeEnvExample } from './external-templates.js';
|
|
4
6
|
import { realGit, stageAll } from './git.js';
|
|
5
7
|
import { isValidPathSegment, validateAddonName, validateRepoPath } from './path-validation.js';
|
|
6
8
|
import { listModuleRepos, readAddonsYaml } from './repo-actions.js';
|
|
@@ -16,10 +18,12 @@ export function renderSafeResetPreview(target, stage) {
|
|
|
16
18
|
'- .wpmoo/odoo.json',
|
|
17
19
|
'- moo',
|
|
18
20
|
'- .gitignore',
|
|
21
|
+
'- .env.example',
|
|
19
22
|
'- README.md',
|
|
20
23
|
'- AGENTS.md',
|
|
21
24
|
'- docs/appstore-release.md',
|
|
22
|
-
'-
|
|
25
|
+
'- External compose template assets',
|
|
26
|
+
'- External agent skill assets when configured',
|
|
23
27
|
'',
|
|
24
28
|
'Will not touch:',
|
|
25
29
|
'- source repo folders under odoo/custom/src/private',
|
|
@@ -32,6 +36,17 @@ export function renderSafeResetPreview(target, stage) {
|
|
|
32
36
|
function titleFromTarget(target) {
|
|
33
37
|
return basename(target).replace(/_dev$/, '') || 'odoo_sample_module';
|
|
34
38
|
}
|
|
39
|
+
function safeResetExternalAssetOptions(options) {
|
|
40
|
+
return plannedExternalAssetOptions(options).map((assetOptions) => ({
|
|
41
|
+
...assetOptions,
|
|
42
|
+
exclude: [
|
|
43
|
+
...(assetOptions.exclude ?? []),
|
|
44
|
+
'.env',
|
|
45
|
+
'.gitmodules',
|
|
46
|
+
'odoo/custom/src/private',
|
|
47
|
+
],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
35
50
|
function parseAddonsForRepo(addonsYaml, repoPath) {
|
|
36
51
|
const safeRepoPath = validateRepoPath(repoPath);
|
|
37
52
|
const lines = addonsYaml.split('\n');
|
|
@@ -112,6 +127,7 @@ async function inferOptions(target) {
|
|
|
112
127
|
export async function safeResetEnvironment(options, git = realGit) {
|
|
113
128
|
const scaffoldOptions = await inferOptions(options.target);
|
|
114
129
|
const files = generatedFiles(scaffoldOptions);
|
|
130
|
+
const externalAssets = safeResetExternalAssetOptions(scaffoldOptions);
|
|
115
131
|
for (const file of files) {
|
|
116
132
|
if (file.path === 'odoo/custom/src/addons.yaml') {
|
|
117
133
|
continue;
|
|
@@ -123,6 +139,10 @@ export async function safeResetEnvironment(options, git = realGit) {
|
|
|
123
139
|
await chmod(destination, file.mode);
|
|
124
140
|
}
|
|
125
141
|
}
|
|
142
|
+
for (const assetOptions of externalAssets) {
|
|
143
|
+
await applyExternalAsset(assetOptions, git);
|
|
144
|
+
}
|
|
145
|
+
await writeTextFile(join(options.target, '.env.example'), renderComposeEnvExample(scaffoldOptions));
|
|
126
146
|
if (options.stage) {
|
|
127
147
|
await stageAll(git, options.target);
|
|
128
148
|
}
|
package/dist/templates.js
CHANGED
|
@@ -22,6 +22,7 @@ function repositoryLayout(options) {
|
|
|
22
22
|
├── docker-compose_17.0.yml
|
|
23
23
|
├── docker-compose_18.0.yml
|
|
24
24
|
├── docker-compose_19.0.yml
|
|
25
|
+
├── moo
|
|
25
26
|
├── scripts/
|
|
26
27
|
├── etc/
|
|
27
28
|
├── odoo/
|
|
@@ -100,7 +101,7 @@ function repoDuplicationNote() {
|
|
|
100
101
|
}
|
|
101
102
|
function verificationCommand(options) {
|
|
102
103
|
const firstAddon = allAddons(options)[0] ?? options.product;
|
|
103
|
-
return `./
|
|
104
|
+
return `./moo test ${firstAddon}`;
|
|
104
105
|
}
|
|
105
106
|
function environmentUsageDocs(options) {
|
|
106
107
|
return `## Docker Compose Notes
|
|
@@ -125,16 +126,22 @@ Source repositories stay under \`odoo/custom/src/private\`. At container startup
|
|
|
125
126
|
|
|
126
127
|
\`\`\`bash
|
|
127
128
|
cp .env.example .env
|
|
128
|
-
./
|
|
129
|
-
./
|
|
130
|
-
./
|
|
131
|
-
./
|
|
129
|
+
./moo start
|
|
130
|
+
./moo logs
|
|
131
|
+
./moo shell
|
|
132
|
+
./moo stop
|
|
133
|
+
./moo doctor
|
|
134
|
+
./moo resetdb devel sale
|
|
135
|
+
./moo snapshot devel before-update
|
|
136
|
+
./moo restore-snapshot before-update devel
|
|
137
|
+
./moo lint
|
|
138
|
+
./moo pot sale devel i18n/sale.pot
|
|
132
139
|
\`\`\`
|
|
133
140
|
|
|
134
141
|
Run tests for one planned product addon:
|
|
135
142
|
|
|
136
143
|
\`\`\`bash
|
|
137
|
-
./
|
|
144
|
+
./moo test ${allAddons(options)[0] ?? options.product}
|
|
138
145
|
\`\`\`
|
|
139
146
|
`;
|
|
140
147
|
}
|
|
@@ -226,7 +233,191 @@ set -euo pipefail
|
|
|
226
233
|
script_dir="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")" && pwd)"
|
|
227
234
|
cd "$script_dir"
|
|
228
235
|
|
|
229
|
-
|
|
236
|
+
usage() {
|
|
237
|
+
case "$1" in
|
|
238
|
+
"start") echo "Usage: ./moo start" ;;
|
|
239
|
+
"stop") echo "Usage: ./moo stop" ;;
|
|
240
|
+
"logs") echo "Usage: ./moo logs [service]" ;;
|
|
241
|
+
"restart") echo "Usage: ./moo restart" ;;
|
|
242
|
+
"shell") echo "Usage: ./moo shell" ;;
|
|
243
|
+
"psql") echo "Usage: ./moo psql [db]" ;;
|
|
244
|
+
"install") echo "Usage: ./moo install <module[,module]> [db]" ;;
|
|
245
|
+
"update") echo "Usage: ./moo update <module[,module]> [db]" ;;
|
|
246
|
+
"test") echo "Usage: ./moo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]" ;;
|
|
247
|
+
"resetdb") echo "Usage: ./moo resetdb [db] [module[,module]]" ;;
|
|
248
|
+
"snapshot") echo "Usage: ./moo snapshot [db] [snapshot-name]" ;;
|
|
249
|
+
"restore-snapshot") echo "Usage: ./moo restore-snapshot <snapshot-name> [db]" ;;
|
|
250
|
+
"lint") echo "Usage: ./moo lint" ;;
|
|
251
|
+
"pot") echo "Usage: ./moo pot <module[,module]> [db] [output]" ;;
|
|
252
|
+
esac
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fail_usage() {
|
|
256
|
+
usage "$1" >&2
|
|
257
|
+
exit 2
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
require_no_args() {
|
|
261
|
+
local command="$1"
|
|
262
|
+
shift
|
|
263
|
+
if [[ "$#" -ne 0 ]]; then
|
|
264
|
+
fail_usage "$command"
|
|
265
|
+
fi
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
optional_single_arg() {
|
|
269
|
+
local command="$1"
|
|
270
|
+
local fallback="$2"
|
|
271
|
+
shift 2
|
|
272
|
+
if [[ "$#" -gt 1 ]]; then
|
|
273
|
+
fail_usage "$command"
|
|
274
|
+
fi
|
|
275
|
+
printf '%s\\n' "\${1:-$fallback}"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
require_module_args() {
|
|
279
|
+
local command="$1"
|
|
280
|
+
shift
|
|
281
|
+
if [[ "$#" -lt 1 || "\${1:-}" == -* || "$#" -gt 2 ]]; then
|
|
282
|
+
fail_usage "$command"
|
|
283
|
+
fi
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
positional_args() {
|
|
287
|
+
local command="$1"
|
|
288
|
+
local min="$2"
|
|
289
|
+
local max="$3"
|
|
290
|
+
shift 3
|
|
291
|
+
if [[ "$#" -lt "$min" || "$#" -gt "$max" ]]; then
|
|
292
|
+
fail_usage "$command"
|
|
293
|
+
fi
|
|
294
|
+
for arg in "$@"; do
|
|
295
|
+
if [[ "$arg" == -* ]]; then
|
|
296
|
+
fail_usage "$command"
|
|
297
|
+
fi
|
|
298
|
+
done
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
validate_test_args() {
|
|
302
|
+
if [[ "$#" -lt 1 || "\${1:-}" == -* ]]; then
|
|
303
|
+
fail_usage "test"
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
shift
|
|
307
|
+
while [[ "$#" -gt 0 ]]; do
|
|
308
|
+
case "$1" in
|
|
309
|
+
"--db"|"--tags")
|
|
310
|
+
if [[ "$#" -lt 2 || "\${2:-}" == --* ]]; then
|
|
311
|
+
echo "Missing value for $1" >&2
|
|
312
|
+
exit 2
|
|
313
|
+
fi
|
|
314
|
+
shift 2
|
|
315
|
+
;;
|
|
316
|
+
"--mode")
|
|
317
|
+
if [[ "$#" -lt 2 || "\${2:-}" == --* ]]; then
|
|
318
|
+
echo "Missing value for --mode" >&2
|
|
319
|
+
exit 2
|
|
320
|
+
fi
|
|
321
|
+
if [[ "$2" != "init" && "$2" != "update" ]]; then
|
|
322
|
+
echo "Invalid value for --mode: expected init or update" >&2
|
|
323
|
+
exit 2
|
|
324
|
+
fi
|
|
325
|
+
shift 2
|
|
326
|
+
;;
|
|
327
|
+
*)
|
|
328
|
+
echo "Unknown option for ./moo test: $1" >&2
|
|
329
|
+
exit 2
|
|
330
|
+
;;
|
|
331
|
+
esac
|
|
332
|
+
done
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
run_script() {
|
|
336
|
+
local script="$1"
|
|
337
|
+
shift
|
|
338
|
+
if [[ ! -x "$script" ]]; then
|
|
339
|
+
echo "Missing daily action script: \${script#./}" >&2
|
|
340
|
+
exit 1
|
|
341
|
+
fi
|
|
342
|
+
exec "$script" "$@"
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
command="\${1:-}"
|
|
346
|
+
case "$command" in
|
|
347
|
+
"start")
|
|
348
|
+
shift
|
|
349
|
+
require_no_args "$command" "$@"
|
|
350
|
+
run_script ./scripts/up.sh
|
|
351
|
+
;;
|
|
352
|
+
"stop")
|
|
353
|
+
shift
|
|
354
|
+
require_no_args "$command" "$@"
|
|
355
|
+
run_script ./scripts/down.sh
|
|
356
|
+
;;
|
|
357
|
+
"logs")
|
|
358
|
+
shift
|
|
359
|
+
service="$(optional_single_arg "$command" "odoo" "$@")"
|
|
360
|
+
run_script ./scripts/logs.sh "$service"
|
|
361
|
+
;;
|
|
362
|
+
"restart")
|
|
363
|
+
shift
|
|
364
|
+
require_no_args "$command" "$@"
|
|
365
|
+
run_script ./scripts/restart.sh
|
|
366
|
+
;;
|
|
367
|
+
"shell")
|
|
368
|
+
shift
|
|
369
|
+
require_no_args "$command" "$@"
|
|
370
|
+
run_script ./scripts/shell.sh
|
|
371
|
+
;;
|
|
372
|
+
"psql")
|
|
373
|
+
shift
|
|
374
|
+
db="$(optional_single_arg "$command" "postgres" "$@")"
|
|
375
|
+
run_script ./scripts/psql.sh "$db"
|
|
376
|
+
;;
|
|
377
|
+
"install")
|
|
378
|
+
shift
|
|
379
|
+
require_module_args "$command" "$@"
|
|
380
|
+
run_script ./scripts/install.sh "$@"
|
|
381
|
+
;;
|
|
382
|
+
"update")
|
|
383
|
+
shift
|
|
384
|
+
require_module_args "$command" "$@"
|
|
385
|
+
run_script ./scripts/update.sh "$@"
|
|
386
|
+
;;
|
|
387
|
+
"test")
|
|
388
|
+
shift
|
|
389
|
+
validate_test_args "$@"
|
|
390
|
+
run_script ./scripts/test.sh "$@"
|
|
391
|
+
;;
|
|
392
|
+
"resetdb")
|
|
393
|
+
shift
|
|
394
|
+
positional_args "$command" 0 2 "$@"
|
|
395
|
+
run_script ./scripts/resetdb.sh "$@"
|
|
396
|
+
;;
|
|
397
|
+
"snapshot")
|
|
398
|
+
shift
|
|
399
|
+
positional_args "$command" 0 2 "$@"
|
|
400
|
+
run_script ./scripts/snapshot.sh "$@"
|
|
401
|
+
;;
|
|
402
|
+
"restore-snapshot")
|
|
403
|
+
shift
|
|
404
|
+
positional_args "$command" 1 2 "$@"
|
|
405
|
+
run_script ./scripts/restore-snapshot.sh "$@"
|
|
406
|
+
;;
|
|
407
|
+
"lint")
|
|
408
|
+
shift
|
|
409
|
+
require_no_args "$command" "$@"
|
|
410
|
+
run_script ./scripts/lint.sh
|
|
411
|
+
;;
|
|
412
|
+
"pot")
|
|
413
|
+
shift
|
|
414
|
+
positional_args "$command" 1 3 "$@"
|
|
415
|
+
run_script ./scripts/pot.sh "$@"
|
|
416
|
+
;;
|
|
417
|
+
*)
|
|
418
|
+
exec npx --yes @wpmoo/odoo@latest "$@"
|
|
419
|
+
;;
|
|
420
|
+
esac
|
|
230
421
|
`;
|
|
231
422
|
}
|
|
232
423
|
export function renderAddonsYaml(options) {
|
|
@@ -291,16 +482,20 @@ git submodule update --init --recursive
|
|
|
291
482
|
|
|
292
483
|
## WPMoo CLI Shortcut
|
|
293
484
|
|
|
294
|
-
This environment includes a local \`moo\`
|
|
485
|
+
This environment includes a local \`moo\` shortcut script. From the repository
|
|
295
486
|
root:
|
|
296
487
|
|
|
297
488
|
\`\`\`bash
|
|
298
489
|
./moo
|
|
490
|
+
./moo start
|
|
491
|
+
./moo stop
|
|
492
|
+
./moo restart
|
|
493
|
+
./moo doctor
|
|
299
494
|
./moo add-module
|
|
300
495
|
\`\`\`
|
|
301
496
|
|
|
302
|
-
|
|
303
|
-
anywhere and the script will
|
|
497
|
+
Optionally, if this repository root is on your \`PATH\`, you can run \`moo ...\`
|
|
498
|
+
from anywhere and the script will return to this environment root first.
|
|
304
499
|
${optionalAgentSkillsReadme(options)}
|
|
305
500
|
## Source Repositories
|
|
306
501
|
|
|
@@ -371,7 +566,17 @@ Use the environment's addon test/update command:
|
|
|
371
566
|
${verificationCommand(options)}
|
|
372
567
|
\`\`\`
|
|
373
568
|
|
|
374
|
-
|
|
569
|
+
Useful maintenance commands:
|
|
570
|
+
|
|
571
|
+
\`\`\`bash
|
|
572
|
+
./moo lint
|
|
573
|
+
./moo resetdb [db] [module[,module]]
|
|
574
|
+
./moo snapshot [db] [snapshot-name]
|
|
575
|
+
./moo restore-snapshot <snapshot-name> [db]
|
|
576
|
+
./moo pot <module[,module]> [db] [output]
|
|
577
|
+
\`\`\`
|
|
578
|
+
|
|
579
|
+
Only report completion after the relevant update/test/lint command exits cleanly.
|
|
375
580
|
`;
|
|
376
581
|
}
|
|
377
582
|
export function renderAppstoreRelease(options) {
|