@wpmoo/odoo 0.8.45 → 0.8.47

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
@@ -178,10 +178,19 @@ npx @wpmoo/odoo remove-module \
178
178
 
179
179
  Check that a generated environment is structurally ready:
180
180
 
181
+ ```bash
182
+ npx @wpmoo/odoo status
183
+ ```
184
+
185
+ Run the deeper environment health check:
186
+
181
187
  ```bash
182
188
  npx @wpmoo/odoo doctor
183
189
  ```
184
190
 
191
+ `status` is fast and offline, and reads local metadata/files only.
192
+ `doctor` is a deeper health check and may check Docker CLI access and GitHub workflows.
193
+
185
194
  Refresh generated environment files without deleting module source code:
186
195
 
187
196
  ```bash
@@ -216,10 +225,38 @@ Daily actions require `.wpmoo/odoo.json` in the current directory and delegate t
216
225
  fixed scripts under `./scripts`; they do not search parent directories or accept
217
226
  arbitrary script names.
218
227
 
228
+ Task-oriented quick recipes:
229
+
230
+ ```bash
231
+ # create environment
232
+ npx @wpmoo/odoo create --product odoo_sample_module --dev-repo-url <dev-repo-url> --source-repo-url <source-repo-url>
233
+
234
+ # add source repo
235
+ npx @wpmoo/odoo add-repo --repo-url https://github.com/example-org/odoo_sample_module_reports.git
236
+
237
+ # add module
238
+ npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base
239
+
240
+ # run tests
241
+ npx @wpmoo/odoo test sale --db devel --mode update --tags /sale
242
+
243
+ # safe reset / recover
244
+ npx @wpmoo/odoo snapshot devel before-reset
245
+ npx @wpmoo/odoo reset
246
+ npx @wpmoo/odoo restore-snapshot before-reset devel
247
+
248
+ # daily checks
249
+ npx @wpmoo/odoo status
250
+ npx @wpmoo/odoo doctor
251
+ ./moo logs odoo
252
+ ```
253
+
219
254
  Use `npx @wpmoo/odoo ...` for package/operator commands such as create,
220
255
  add/remove repo, add/remove module, `doctor`, and `reset`. Generated
221
256
  environments include `./moo` for local daily commands; it also falls back to
222
257
  `npx @wpmoo/odoo@latest` for package commands such as `./moo doctor`.
258
+ For the operator-facing verification matrix, see
259
+ [`docs/generated-environment-verification.md`](docs/generated-environment-verification.md).
223
260
 
224
261
  ## Defaults
225
262
 
package/dist/args.js CHANGED
@@ -6,6 +6,7 @@ import { dailyActionCommands } from './daily-actions.js';
6
6
  import { validateAddonName, validateRepoPath } from './path-validation.js';
7
7
  import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-url.js';
8
8
  const commandNames = new Set([
9
+ 'status',
9
10
  'create',
10
11
  'add-repo',
11
12
  'remove-repo',
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts';
3
+ import { realpathSync } from 'node:fs';
3
4
  import { resolve } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { commandFromArgs, defaultTargetForProduct, isHelpRequested, isVersionRequested, optionsFromArgs, parseArgs, stripInternalFlags, } from './args.js';
6
7
  import { detectDevelopmentEnvironment } from './environment.js';
7
8
  import { commandOdooVersion } from './environment-version.js';
@@ -22,6 +23,7 @@ import { scaffold } from './scaffold.js';
22
23
  import { renderBanner } from './templates.js';
23
24
  import { checkForUpdate, installLatestPackage, isUpdateCheckSkipped, restartCli } from './update-check.js';
24
25
  import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
26
+ import { getEnvironmentStatus, renderEnvironmentStatusForTarget, renderEnvironmentStatusSummary, } from './status.js';
25
27
  import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, realGitHub, createGitHubRepository, } from './github.js';
26
28
  import { environmentGitHubOwner } from './environment-context.js';
27
29
  import { handlePromptCancel, handleUnavailableMenuChoice, installPromptCancelKeyTracker, isMenuBackSignal, MenuBackSignal, menuIntroTitle, menuPromptMessage, } from './menu-navigation.js';
@@ -321,7 +323,7 @@ async function selectSourceRepo(target, cancelAction = 'exit') {
321
323
  const repos = await listModuleRepos(target);
322
324
  if (repos.length === 0) {
323
325
  if (cancelAction === 'back') {
324
- note(`No source repos found under ${target}/odoo/custom/src/private.`, 'Nothing to select');
326
+ note(`No source repos found under ${target}/odoo/custom/src/private.\nNext: choose "Add source repo" first.`, 'Nothing to select');
325
327
  handleUnavailableMenuChoice(cancelAction);
326
328
  }
327
329
  throw new Error(`No source repos found under ${target}/odoo/custom/src/private`);
@@ -409,7 +411,7 @@ async function removeRepoOptionsFromPrompts(argv, showIntro = true, cancelAction
409
411
  const repos = await listModuleRepos(target);
410
412
  if (repos.length === 0) {
411
413
  if (cancelAction === 'back') {
412
- note(`No module submodules found under ${target}/odoo/custom/src/private.`, 'Nothing to remove');
414
+ note(`No module submodules found under ${target}/odoo/custom/src/private.\nNext: choose "Add source repo" first.`, 'Nothing to remove');
413
415
  handleUnavailableMenuChoice(cancelAction);
414
416
  }
415
417
  throw new Error(`No module submodules found under ${target}/odoo/custom/src/private`);
@@ -448,7 +450,7 @@ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = '
448
450
  const modules = await listModulesInSourceRepo(target, repoPath);
449
451
  if (modules.length === 0) {
450
452
  if (cancelAction === 'back') {
451
- note(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}.`, 'Nothing to remove');
453
+ note(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
452
454
  handleUnavailableMenuChoice(cancelAction);
453
455
  }
454
456
  throw new Error(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}`);
@@ -571,6 +573,8 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
571
573
  }
572
574
  while (true) {
573
575
  try {
576
+ const status = await getEnvironmentStatus(cwd);
577
+ note(renderEnvironmentStatusSummary(status), 'Environment status');
574
578
  const action = await selectEnvironmentActionFromMenu();
575
579
  if (action === 'exit') {
576
580
  return;
@@ -686,6 +690,14 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
686
690
  console.log(await runDoctor(cwd));
687
691
  return;
688
692
  }
693
+ if (route.command === 'status') {
694
+ if (route.argv.length > 0) {
695
+ throw new Error('Usage: wpmoo status');
696
+ }
697
+ console.log(renderBanner());
698
+ console.log(await renderEnvironmentStatusForTarget(cwd));
699
+ return;
700
+ }
689
701
  if (isDailyActionCommand(route.command)) {
690
702
  console.log(renderBanner());
691
703
  await runDailyAction(route.command, route.argv, cwd);
@@ -712,7 +724,19 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
712
724
  }
713
725
  outro(`Created Odoo dev overlay in ${resolvedOptions.target}. Review staged changes, then commit.`);
714
726
  }
715
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
727
+ export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
728
+ if (!argvPath)
729
+ return false;
730
+ try {
731
+ const entrypointUrl = pathToFileURL(realpathSync(fileURLToPath(metaUrl))).href;
732
+ const argvUrl = pathToFileURL(realpathSync(argvPath)).href;
733
+ return entrypointUrl === argvUrl;
734
+ }
735
+ catch {
736
+ return metaUrl === pathToFileURL(argvPath).href;
737
+ }
738
+ }
739
+ if (isCliEntrypoint(import.meta.url)) {
716
740
  runCli().catch((error) => {
717
741
  const message = error instanceof Error ? error.message : String(error);
718
742
  console.error(message);
package/dist/help.js CHANGED
@@ -6,6 +6,7 @@ WPMoo Odoo lifecycle tooling.
6
6
  Usage:
7
7
  npx @wpmoo/odoo
8
8
  npx @wpmoo/odoo create --product <slug> --dev-repo-url <url> --source-repo-url <url>
9
+ npx @wpmoo/odoo status
9
10
  npx @wpmoo/odoo add-repo --repo-url <url>
10
11
  npx @wpmoo/odoo remove-repo --repo <name>
11
12
  npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
@@ -66,9 +67,28 @@ Daily actions:
66
67
  Generated environments also include ./moo for local compose commands such as ./moo start.
67
68
  Use ./moo or npx @wpmoo/odoo with the same daily action arguments.
68
69
 
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.
70
+ Status and doctor:
71
+ status: fast and offline. Reads local environment metadata and files only.
72
+ doctor: deeper health check. May check Docker CLI access and GitHub workflows.
73
+
74
+ Task recipes:
75
+ Create environment:
76
+ npx @wpmoo/odoo create --product <slug> --dev-repo-url <url> --source-repo-url <url>
77
+ Add source repo:
78
+ npx @wpmoo/odoo add-repo --repo-url <url>
79
+ Add module:
80
+ npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
81
+ Run tests:
82
+ npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
83
+ Safe reset and recover:
84
+ npx @wpmoo/odoo snapshot [db] [snapshot-name]
85
+ npx @wpmoo/odoo reset
86
+ npx @wpmoo/odoo restore-snapshot <snapshot-name> [db]
87
+ Daily command checks:
88
+ npx @wpmoo/odoo status
89
+ npx @wpmoo/odoo doctor
90
+ npx @wpmoo/odoo logs [service]
91
+ npx @wpmoo/odoo restart
72
92
 
73
93
  Example:
74
94
  npx @wpmoo/odoo create \\
package/dist/status.js ADDED
@@ -0,0 +1,193 @@
1
+ import { access, readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { defaultOdooVersion, markerPath } from './environment.js';
4
+ import { isValidPathSegment, validateRepoPath } from './path-validation.js';
5
+ async function pathExists(path) {
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function errorMessage(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ function parseMetadata(content) {
21
+ const parsed = JSON.parse(content);
22
+ if (!isRecord(parsed)) {
23
+ throw new Error('metadata is not an object');
24
+ }
25
+ return parsed;
26
+ }
27
+ function sourceRepoPathsFromMetadata(metadata) {
28
+ const sourceRepoPaths = [];
29
+ const invalidSourceRepoPaths = [];
30
+ if (!Array.isArray(metadata.sourceRepos))
31
+ return { sourceRepoPaths, invalidSourceRepoPaths };
32
+ for (const repo of metadata.sourceRepos) {
33
+ const path = repo && typeof repo.path === 'string' ? repo.path.trim() : '';
34
+ if (!path)
35
+ continue;
36
+ if (!isValidPathSegment(path)) {
37
+ invalidSourceRepoPaths.push(path);
38
+ continue;
39
+ }
40
+ sourceRepoPaths.push(validateRepoPath(path));
41
+ }
42
+ return { sourceRepoPaths, invalidSourceRepoPaths };
43
+ }
44
+ async function missingCoreFiles(target, odooVersion) {
45
+ const missing = [];
46
+ const composeFile = `docker-compose_${odooVersion}.yml`;
47
+ const checks = [
48
+ { label: 'moo', path: join(target, 'moo') },
49
+ { label: 'README.md', path: join(target, 'README.md') },
50
+ { label: 'AGENTS.md', path: join(target, 'AGENTS.md') },
51
+ { label: composeFile, path: join(target, composeFile) },
52
+ { label: 'scripts/', path: join(target, 'scripts'), mustBeDirectory: true },
53
+ ];
54
+ for (const check of checks) {
55
+ if (!(await pathExists(check.path))) {
56
+ missing.push(check.label);
57
+ continue;
58
+ }
59
+ if (check.mustBeDirectory) {
60
+ const fileStat = await stat(check.path);
61
+ if (!fileStat.isDirectory())
62
+ missing.push(check.label);
63
+ }
64
+ }
65
+ return missing;
66
+ }
67
+ async function countModuleCandidatesInRepoPath(path) {
68
+ if (!(await pathExists(path)))
69
+ return 0;
70
+ const rootStat = await stat(path);
71
+ if (!rootStat.isDirectory())
72
+ return 0;
73
+ let count = 0;
74
+ const stack = [path];
75
+ while (stack.length > 0) {
76
+ const current = stack.pop();
77
+ if (!current)
78
+ continue;
79
+ const entries = await readdir(current, { withFileTypes: true });
80
+ let hasManifest = false;
81
+ for (const entry of entries) {
82
+ if (entry.isFile() && entry.name === '__manifest__.py') {
83
+ hasManifest = true;
84
+ }
85
+ else if (entry.isDirectory()) {
86
+ stack.push(join(current, entry.name));
87
+ }
88
+ }
89
+ if (hasManifest)
90
+ count += 1;
91
+ }
92
+ return count;
93
+ }
94
+ function summaryText(status) {
95
+ if (status.kind === 'no_environment')
96
+ return 'No WPMoo environment detected.';
97
+ if (status.kind === 'invalid_metadata')
98
+ return 'Environment metadata is invalid.';
99
+ const prefix = status.missingCoreFiles.length > 0 || status.invalidSourceRepoPaths.length > 0
100
+ ? 'Environment needs attention'
101
+ : 'Environment ready';
102
+ return `${prefix}: Odoo ${status.odooVersion}, source repos ${status.sourceRepoCount}, module candidates ${status.moduleCandidateCount}.`;
103
+ }
104
+ export async function getEnvironmentStatus(target) {
105
+ const metadataFullPath = join(target, markerPath);
106
+ if (!(await pathExists(metadataFullPath))) {
107
+ return {
108
+ kind: 'no_environment',
109
+ target,
110
+ metadataPath: markerPath,
111
+ recommendedNextAction: 'Run npx @wpmoo/odoo create ...',
112
+ };
113
+ }
114
+ let metadata;
115
+ try {
116
+ const content = await readFile(metadataFullPath, 'utf8');
117
+ metadata = parseMetadata(content);
118
+ }
119
+ catch (error) {
120
+ return {
121
+ kind: 'invalid_metadata',
122
+ target,
123
+ metadataPath: markerPath,
124
+ metadataError: errorMessage(error),
125
+ recommendedNextAction: 'Fix .wpmoo/odoo.json or run npx @wpmoo/odoo reset from a valid environment.',
126
+ };
127
+ }
128
+ const odooVersion = typeof metadata.odooVersion === 'string' && metadata.odooVersion.trim()
129
+ ? metadata.odooVersion.trim()
130
+ : defaultOdooVersion;
131
+ const { sourceRepoPaths, invalidSourceRepoPaths } = sourceRepoPathsFromMetadata(metadata);
132
+ const repoRoots = sourceRepoPaths.map((path) => join(target, 'odoo/custom/src/private', path));
133
+ let moduleCandidateCount = 0;
134
+ for (const repoRoot of repoRoots) {
135
+ moduleCandidateCount += await countModuleCandidatesInRepoPath(repoRoot);
136
+ }
137
+ const missingFiles = await missingCoreFiles(target, odooVersion);
138
+ let recommendedNextAction = 'Run npx @wpmoo/odoo doctor for deep checks or ./moo start.';
139
+ if (invalidSourceRepoPaths.length > 0) {
140
+ recommendedNextAction =
141
+ 'Fix invalid source repo paths in .wpmoo/odoo.json, then run npx @wpmoo/odoo doctor.';
142
+ }
143
+ else if (missingFiles.length > 0) {
144
+ recommendedNextAction = 'Run npx @wpmoo/odoo reset, then npx @wpmoo/odoo doctor.';
145
+ }
146
+ else if (sourceRepoPaths.length === 0) {
147
+ recommendedNextAction = 'Run npx @wpmoo/odoo add-repo ...';
148
+ }
149
+ return {
150
+ kind: 'environment',
151
+ target,
152
+ metadataPath: markerPath,
153
+ odooVersion,
154
+ sourceRepoCount: sourceRepoPaths.length,
155
+ sourceRepoPaths,
156
+ invalidSourceRepoPaths,
157
+ moduleCandidateCount,
158
+ missingCoreFiles: missingFiles,
159
+ recommendedNextAction,
160
+ };
161
+ }
162
+ export function renderEnvironmentStatusSummary(status) {
163
+ return summaryText(status);
164
+ }
165
+ export function renderEnvironmentStatus(status) {
166
+ const lines = [`Status: ${summaryText(status)}`];
167
+ if (status.kind === 'no_environment') {
168
+ lines.push(`Metadata: missing ${status.metadataPath}`);
169
+ lines.push(`Next: ${status.recommendedNextAction}`);
170
+ return lines.join('\n');
171
+ }
172
+ if (status.kind === 'invalid_metadata') {
173
+ lines.push(`Metadata: invalid ${status.metadataPath}`);
174
+ lines.push(`Error: ${status.metadataError}`);
175
+ lines.push(`Next: ${status.recommendedNextAction}`);
176
+ return lines.join('\n');
177
+ }
178
+ lines.push(`Metadata: ${status.metadataPath}`);
179
+ lines.push(`Odoo: ${status.odooVersion}`);
180
+ lines.push(`Source repos: ${status.sourceRepoCount}`);
181
+ lines.push(`Source repo paths: ${status.sourceRepoPaths.length > 0 ? status.sourceRepoPaths.join(', ') : '(none configured)'}`);
182
+ if (status.invalidSourceRepoPaths.length > 0) {
183
+ lines.push(`Invalid source repo paths: ${status.invalidSourceRepoPaths.join(', ')}`);
184
+ }
185
+ lines.push(`Module candidates: ${status.moduleCandidateCount}`);
186
+ lines.push(`Missing core files: ${status.missingCoreFiles.length > 0 ? status.missingCoreFiles.join(', ') : '(none)'}`);
187
+ lines.push(`Next: ${status.recommendedNextAction}`);
188
+ return lines.join('\n');
189
+ }
190
+ export async function renderEnvironmentStatusForTarget(target) {
191
+ const status = await getEnvironmentStatus(target);
192
+ return renderEnvironmentStatus(status);
193
+ }
package/dist/templates.js CHANGED
@@ -122,26 +122,57 @@ Source repositories stay under \`odoo/custom/src/private\`. At container startup
122
122
  \`entrypoint.sh\` scans those repositories for addons and exposes them through
123
123
  \`/mnt/wpmoo-addons\`.
124
124
 
125
- ## Common Commands
125
+ ## Daily Command Hub (\`./moo\`)
126
+
127
+ \`./moo\` routes day-to-day service and module workflows to local scripts in
128
+ \`./scripts/\` (for example \`start\`, \`logs\`, \`update\`, \`test\`, \`snapshot\`).
129
+ \`./moo status\` and \`./moo doctor\` are package fallback commands that run via
130
+ \`npx --yes @wpmoo/odoo@latest ...\`.
131
+
132
+ ### Start And Inspect Services
126
133
 
127
134
  \`\`\`bash
128
135
  cp .env.example .env
129
136
  ./moo start
130
- ./moo logs
137
+ ./moo logs odoo
131
138
  ./moo shell
139
+ ./moo psql postgres
132
140
  ./moo stop
133
- ./moo doctor
134
- ./moo resetdb devel sale
141
+ \`\`\`
142
+
143
+ ### Run, Update, And Test Modules
144
+
145
+ \`\`\`bash
146
+ ./moo install ${allAddons(options)[0] ?? options.product}
147
+ ./moo update ${allAddons(options)[0] ?? options.product}
148
+ ./moo test ${allAddons(options)[0] ?? options.product}
149
+ \`\`\`
150
+
151
+ ### Snapshot And Restore
152
+
153
+ \`\`\`bash
135
154
  ./moo snapshot devel before-update
136
155
  ./moo restore-snapshot before-update devel
156
+ \`\`\`
157
+
158
+ ### Lint
159
+
160
+ \`\`\`bash
137
161
  ./moo lint
138
- ./moo pot sale devel i18n/sale.pot
139
162
  \`\`\`
140
163
 
141
- Run tests for one planned product addon:
164
+ ### Export Translations
142
165
 
143
166
  \`\`\`bash
144
- ./moo test ${allAddons(options)[0] ?? options.product}
167
+ ./moo pot ${allAddons(options)[0] ?? options.product} devel i18n/${allAddons(options)[0] ?? options.product}.pot
168
+ \`\`\`
169
+
170
+ ### Recover / Reset
171
+
172
+ \`\`\`bash
173
+ ./moo doctor
174
+ ./moo status
175
+ ./moo resetdb devel ${allAddons(options)[0] ?? options.product}
145
176
  \`\`\`
146
177
  `;
147
178
  }
@@ -576,6 +607,10 @@ Useful maintenance commands:
576
607
  ./moo pot <module[,module]> [db] [output]
577
608
  \`\`\`
578
609
 
610
+ Daily script delegation vs package fallback:
611
+ - \`./moo start\`, \`logs\`, \`install\`, \`update\`, \`test\`, \`snapshot\`, and related runtime tasks delegate to local \`./scripts/*.sh\`.
612
+ - \`./moo status\` and \`./moo doctor\` are package fallback commands routed to \`npx --yes @wpmoo/odoo@latest ...\`.
613
+
579
614
  Only report completion after the relevant update/test/lint command exits cleanly.
580
615
  `;
581
616
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.45",
3
+ "version": "0.8.47",
4
4
  "description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
5
5
  "type": "module",
6
6
  "repository": {