@wpmoo/odoo 0.8.35 → 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 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
@@ -189,14 +195,25 @@ npx @wpmoo/odoo psql devel
189
195
  npx @wpmoo/odoo install sale devel
190
196
  npx @wpmoo/odoo update sale devel
191
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
192
203
  ```
193
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
+
194
209
  Daily actions require `.wpmoo/odoo.json` in the current directory and delegate to
195
210
  fixed scripts under `./scripts`; they do not search parent directories or accept
196
211
  arbitrary script names.
197
212
 
198
- Generated environments also include a local `./moo` shortcut for Doodba-style
199
- daily commands such as `./moo start`, `./moo restart`, and `./moo stop`.
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`.
200
217
 
201
218
  ## Defaults
202
219
 
package/dist/args.js CHANGED
@@ -12,6 +12,7 @@ const commandNames = new Set([
12
12
  'add-module',
13
13
  'remove-module',
14
14
  'reset',
15
+ 'doctor',
15
16
  ...dailyActionCommands,
16
17
  ]);
17
18
  const internalFlags = new Set(['--no-update-check']);
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);
@@ -2,9 +2,24 @@ 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 = ['start', 'stop', 'logs', 'restart', 'shell', 'psql', 'install', 'update', 'test'];
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 scripts = {
22
+ export const dailyActionScripts = {
8
23
  start: 'up.sh',
9
24
  stop: 'down.sh',
10
25
  logs: 'logs.sh',
@@ -14,6 +29,11 @@ const scripts = {
14
29
  install: 'install.sh',
15
30
  update: 'update.sh',
16
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',
17
37
  };
18
38
  export function isDailyActionCommand(command) {
19
39
  return dailyActionCommandSet.has(command);
@@ -35,7 +55,17 @@ function usage(command) {
35
55
  return 'Usage: wpmoo install <module[,module]> [db]';
36
56
  if (command === 'update')
37
57
  return 'Usage: wpmoo update <module[,module]> [db]';
38
- return 'Usage: wpmoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]';
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]';
39
69
  }
40
70
  function ensureNoArgs(command, argv) {
41
71
  if (argv.length > 0)
@@ -53,6 +83,12 @@ function moduleArgs(command, argv) {
53
83
  throw new Error(usage(command));
54
84
  return db ? [modules, db] : [modules];
55
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
+ }
56
92
  function testArgs(argv) {
57
93
  const [modules, ...rest] = argv;
58
94
  if (!modules || modules.startsWith('-'))
@@ -86,7 +122,17 @@ function scriptArgs(command, argv) {
86
122
  return optionalSingleArg(command, argv, 'postgres');
87
123
  if (command === 'install' || command === 'update')
88
124
  return moduleArgs(command, argv);
89
- return testArgs(argv);
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);
90
136
  }
91
137
  async function assertEnvironmentRoot(cwd) {
92
138
  try {
@@ -108,7 +154,7 @@ async function assertScriptExists(cwd, script) {
108
154
  }
109
155
  export async function dailyActionPlan(command, argv, cwd = process.cwd()) {
110
156
  await assertEnvironmentRoot(cwd);
111
- const scriptPath = await assertScriptExists(cwd, scripts[command]);
157
+ const scriptPath = await assertScriptExists(cwd, dailyActionScripts[command]);
112
158
  return {
113
159
  cwd,
114
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,7 @@ 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
14
15
  npx @wpmoo/odoo start
15
16
  npx @wpmoo/odoo stop
16
17
  npx @wpmoo/odoo logs [service]
@@ -20,6 +21,11 @@ Usage:
20
21
  npx @wpmoo/odoo install <module[,module]> [db]
21
22
  npx @wpmoo/odoo update <module[,module]> [db]
22
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]
23
29
 
24
30
  Options:
25
31
  --product <slug> Product slug, for example my_odoo_module.
@@ -57,7 +63,12 @@ Options:
57
63
  Daily actions:
58
64
  Daily actions must be run from a generated environment root containing .wpmoo/odoo.json.
59
65
  They delegate to the fixed scripts copied from the compose resource under ./scripts.
60
- Generated environments also include ./moo for Doodba-style local commands such as ./moo start.
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.
61
72
 
62
73
  Example:
63
74
  npx @wpmoo/odoo create \\
@@ -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
- '- Compose generated files',
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
@@ -130,6 +130,12 @@ cp .env.example .env
130
130
  ./moo logs
131
131
  ./moo shell
132
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
133
139
  \`\`\`
134
140
 
135
141
  Run tests for one planned product addon:
@@ -238,6 +244,11 @@ usage() {
238
244
  "install") echo "Usage: ./moo install <module[,module]> [db]" ;;
239
245
  "update") echo "Usage: ./moo update <module[,module]> [db]" ;;
240
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]" ;;
241
252
  esac
242
253
  }
243
254
 
@@ -272,6 +283,21 @@ require_module_args() {
272
283
  fi
273
284
  }
274
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
+
275
301
  validate_test_args() {
276
302
  if [[ "$#" -lt 1 || "\${1:-}" == -* ]]; then
277
303
  fail_usage "test"
@@ -363,6 +389,31 @@ case "$command" in
363
389
  validate_test_args "$@"
364
390
  run_script ./scripts/test.sh "$@"
365
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
+ ;;
366
417
  *)
367
418
  exec npx --yes @wpmoo/odoo@latest "$@"
368
419
  ;;
@@ -439,6 +490,7 @@ root:
439
490
  ./moo start
440
491
  ./moo stop
441
492
  ./moo restart
493
+ ./moo doctor
442
494
  ./moo add-module
443
495
  \`\`\`
444
496
 
@@ -514,7 +566,17 @@ Use the environment's addon test/update command:
514
566
  ${verificationCommand(options)}
515
567
  \`\`\`
516
568
 
517
- Only report completion after the relevant update/test command exits cleanly.
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.
518
580
  `;
519
581
  }
520
582
  export function renderAppstoreRelease(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.35",
3
+ "version": "0.8.36",
4
4
  "description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
5
5
  "type": "module",
6
6
  "repository": {