@wpmoo/toolkit 0.9.21 → 0.9.23

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
@@ -190,6 +190,17 @@ The unscoped `wpmoo` short alias is optional. If npm returns `E404` or rejects
190
190
  that alias during the publish workflow, the workflow reports a non-blocking
191
191
  warning while keeping the scoped package release valid.
192
192
 
193
+ ### Release candidate rules
194
+
195
+ - **Required release artifacts**: `@wpmoo/toolkit`, `@wpmoo/odoo`, and
196
+ `@wpmoo/odoo-dev` must be publishable at package version `$VERSION` for tag
197
+ `v$VERSION` to count as valid.
198
+ - **Optional artifact**: `wpmoo` at package version `$VERSION` is best-effort only. If npm
199
+ rejects it, the release still succeeds as long as the three required scoped
200
+ packages are valid.
201
+ - **Smoke expectation**: run `npm run smoke:published -- "$VERSION"` after the
202
+ release tag workflow completes.
203
+
193
204
  ## Documentation
194
205
 
195
206
  - [External Resources](docs/external-resources.md)
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, se
38
38
  import { renderBanner } from './templates.js';
39
39
  import { checkForUpdate, isUpdateCheckSkipped, restartCli } from './update-check.js';
40
40
  import { packageName, packageVersion, renderVersion, renderVersionTag } from './version.js';
41
- import { environmentStatusJson, getEnvironmentStatus, renderEnvironmentStatusForTarget, renderEnvironmentStatusSummary, } from './status.js';
41
+ import { environmentStatusJson, getEnvironmentStatus, environmentBannerSummaryLine, renderEnvironmentStatusForTarget, } from './status.js';
42
42
  import { getGitHubAccounts, getGitHubRepositoryStatus, githubRepositoryUrl, realGitHub, createGitHubRepository, } from './github.js';
43
43
  import { environmentGitHubOwner } from './environment-context.js';
44
44
  import { handlePromptCancel, handleUnavailableMenuChoice, installPromptCancelKeyTracker, isMenuBackSignal, MenuBackSignal, menuIntroTitle, menuPromptMessage, } from './menu-navigation.js';
@@ -184,27 +184,16 @@ function validateRepoName(value) {
184
184
  function startupVersionLine(latestVersion) {
185
185
  return `v${packageVersion()}${latestVersion ? ` -> v${latestVersion} available` : ''}`;
186
186
  }
187
- function pluralize(count, singular, plural) {
188
- return `${count} ${count === 1 ? singular : plural}`;
189
- }
190
- function renderStartupEnvironmentLine(status) {
191
- if (status.kind !== 'environment') {
192
- return `Environment: ${renderEnvironmentStatusSummary(status)}`;
193
- }
194
- const issueCount = status.composeErrors.length + status.invalidSourceRepoPaths.length + status.missingCoreFiles.length;
195
- const issueSuffix = issueCount > 0 ? ` · ${pluralize(issueCount, 'issue', 'issues')}` : '';
196
- return [
197
- `Environment: Odoo ${status.odooVersion}`,
198
- pluralize(status.sourceRepoCount, 'repo', 'repos'),
199
- pluralize(status.moduleCandidateCount, 'module', 'modules'),
200
- ].join(' · ') + issueSuffix;
201
- }
202
187
  function renderStartupBanner(details, latestVersion) {
203
188
  const versionLine = startupVersionLine(latestVersion);
204
189
  return renderBanner(details?.(versionLine), details ? { version: versionLine } : undefined);
205
190
  }
206
191
  function renderCockpitStatusLines(status, serviceStatus, lastStatus) {
207
- return [renderStartupEnvironmentLine(status), renderServiceRuntimeStatusLine(serviceStatus), lastStatus];
192
+ return [
193
+ environmentBannerSummaryLine(status),
194
+ renderServiceRuntimeStatusLine(serviceStatus),
195
+ lastStatus,
196
+ ];
208
197
  }
209
198
  function renderLastCommandStatus(command) {
210
199
  return `Last: ${command.label} ✓ completed`;
@@ -29,41 +29,40 @@ function internalCommand(id, category, label, description, aliases = []) {
29
29
  };
30
30
  }
31
31
  export const cockpitCommands = [
32
- dailyCommand('start', 'services', 'Start services', 'Start the Odoo development services.', ['up', 'compose up']),
33
- dailyCommand('stop', 'services', 'Stop services', 'Stop the Odoo development services.', ['down', 'compose down']),
34
- dailyCommand('restart', 'services', 'Restart services', 'Restart the Odoo development services.', ['reload']),
35
- dailyCommand('logs', 'services', 'View logs', 'Stream logs for an Odoo environment service.', ['log', 'tail']),
36
- dailyCommand('shell', 'services', 'Open shell', 'Open a shell inside the Odoo service container.', ['bash', 'terminal']),
32
+ dailyCommand('start', 'services', 'Start services', 'Start Odoo services.', ['up', 'compose up']),
33
+ dailyCommand('stop', 'services', 'Stop services', 'Stop Odoo services.', ['down', 'compose down']),
34
+ dailyCommand('restart', 'services', 'Restart services', 'Restart Odoo services.', ['reload']),
35
+ dailyCommand('logs', 'services', 'View logs', 'Tail service logs.', ['log', 'tail']),
36
+ dailyCommand('shell', 'services', 'Open shell', 'Open a service shell.', ['bash', 'terminal']),
37
37
  internalCommand('list-modules', 'modules', 'List modules', 'Browse detected Odoo modules by source category.', [
38
38
  'modules list',
39
39
  'browse modules',
40
+ '/module',
41
+ 'module',
40
42
  ]),
41
- dailyCommand('install', 'modules', 'Install module', 'Install one or more Odoo modules into a database.', ['install module']),
42
- dailyCommand('update', 'modules', 'Update module', 'Update one or more Odoo modules in a database.', ['upgrade']),
43
- dailyCommand('test', 'modules', 'Run tests', 'Run Odoo tests for one or more modules.', ['tests', 'pytest']),
44
- dailyCommand('lint', 'modules', 'Run environment lint', 'Run the configured environment lint checks.', ['check', 'quality']),
45
- dailyCommand('pot', 'modules', 'Generate POT', 'Generate translation template files for a module.', ['translation', 'i18n']),
46
- dailyCommand('psql', 'database', 'Open psql', 'Open a PostgreSQL prompt for an environment database.', ['postgres', 'sql']),
47
- dailyCommand('snapshot', 'database', 'Create snapshot', 'Create a database snapshot.', ['backup', 'dump']),
48
- dailyCommand('restore-snapshot', 'database', 'Restore snapshot', 'Restore a database from a named snapshot.', ['restore', 'snapshot restore']),
49
- dailyCommand('resetdb', 'database', 'Reset database', 'Reset an environment database.', ['reset db', 'database reset']),
43
+ dailyCommand('install', 'modules', 'Install module', 'Install modules in the database.', ['install module', 'module']),
44
+ dailyCommand('update', 'modules', 'Update module', 'Update modules in the database.', ['upgrade', 'module']),
45
+ dailyCommand('test', 'modules', 'Run tests', 'Run tests for selected modules.', ['tests', 'pytest', 'module']),
46
+ dailyCommand('lint', 'modules', 'Run environment lint', 'Run environment lint checks.', ['check', 'quality']),
47
+ dailyCommand('pot', 'modules', 'Generate POT', 'Generate module translation templates.', ['translation', 'i18n']),
48
+ dailyCommand('psql', 'database', 'Open psql', 'Open PostgreSQL prompt.', ['postgres', 'sql', '/db']),
49
+ dailyCommand('snapshot', 'database', 'Create snapshot', 'Create a database snapshot.', ['backup', 'dump', '/snapshot']),
50
+ dailyCommand('restore-snapshot', 'database', 'Restore snapshot', 'Restore a named snapshot.', ['restore', 'snapshot restore']),
51
+ dailyCommand('resetdb', 'database', 'Reset database', 'Reset the environment database.', ['reset db', 'database reset']),
50
52
  internalCommand('status', 'diagnostics', 'Environment status', 'Show a summary of the current environment state.', [
51
53
  'state',
52
54
  'summary',
53
55
  ]),
54
- internalCommand('doctor', 'diagnostics', 'Run doctor', 'Run environment diagnostics and report actionable issues.', [
55
- 'diagnose',
56
- 'health',
57
- ]),
58
- internalCommand('add-repo', 'repositories', 'Add source repo', 'Add a source repository as an environment submodule.', [
56
+ internalCommand('doctor', 'diagnostics', 'Run doctor', 'Run environment diagnostics.', ['diagnose', 'health']),
57
+ internalCommand('add-repo', 'repositories', 'Add source repo', 'Add a source repository.', [
59
58
  'repository add',
60
59
  'source add',
61
60
  ]),
62
- internalCommand('remove-repo', 'repositories', 'Remove source repo', 'Remove a source repository from the environment.', ['repository remove', 'source remove']),
63
- internalCommand('add-module', 'modules', 'Add module', 'Add a module folder to a source repository.', ['module add']),
64
- internalCommand('remove-module', 'modules', 'Remove module', 'Remove a module folder from a source repository.', ['module remove']),
65
- internalCommand('safe-reset', 'maintenance', 'Safe reset environment', 'Refresh generated environment files while preserving source repositories.', ['reset', 'refresh']),
66
- internalCommand('exit', 'maintenance', 'Exit', 'Leave the command palette.', ['quit', 'back']),
61
+ internalCommand('remove-repo', 'repositories', 'Remove source repo', 'Remove a source repository.', ['repository remove', 'source remove']),
62
+ internalCommand('add-module', 'modules', 'Add module', 'Add a module to a source repository.', ['module add']),
63
+ internalCommand('remove-module', 'modules', 'Remove module', 'Remove a module from a source repository.', ['module remove']),
64
+ internalCommand('safe-reset', 'maintenance', 'Safe reset environment', 'Refresh generated files only.', ['reset', 'refresh', '/safe']),
65
+ internalCommand('exit', 'maintenance', 'Exit', 'Close the command palette.', ['quit', 'back']),
67
66
  ];
68
67
  const defaultCommandIds = new Set(['start', 'logs', 'test', 'status', 'doctor', 'exit']);
69
68
  export function normalizeCockpitSearchTerm(term) {
@@ -33,6 +33,21 @@ function categoryHeading(category) {
33
33
  function commandName(command) {
34
34
  return `${rgb(226, 184, 96, ` ${command.label.padEnd(topLevelCommandLabelWidth)}`)}${dim(` ${command.description}`)}`;
35
35
  }
36
+ const disabledReasonNextStep = {
37
+ 'No modules found.': 'Next: choose "Add module" first.',
38
+ 'Services stopped.': 'Next: choose "Start services" first.',
39
+ 'Already running.': 'Next: choose "Stop services" or "Restart services".',
40
+ 'Docker not running.': 'Next: start Docker, then choose "Start services".',
41
+ 'No source repos found.': 'Next: choose "Add source repo" first.',
42
+ };
43
+ function disabledError(reason) {
44
+ const base = 'This option is disabled and cannot be selected.';
45
+ if (!reason) {
46
+ return base;
47
+ }
48
+ const nextStep = disabledReasonNextStep[reason];
49
+ return nextStep ? `${base}\nReason: ${reason}\n${nextStep}` : `${base}\nReason: ${reason}`;
50
+ }
36
51
  function serviceDisabledReason(command, serviceStatus) {
37
52
  if (command.category !== 'services' || !serviceStatus)
38
53
  return undefined;
@@ -56,9 +71,6 @@ function disabledReason(command, serviceStatus, moduleCount, sourceRepoCount) {
56
71
  moduleDisabledReason(command, moduleCount) ??
57
72
  sourceRepoDisabledReason(command, sourceRepoCount));
58
73
  }
59
- function disabledError() {
60
- return 'This option is disabled and cannot be selected.';
61
- }
62
74
  function commandDisabledValue(reason) {
63
75
  if (!reason) {
64
76
  return undefined;
@@ -131,7 +143,7 @@ export async function selectCockpitTopLevelMenu(options = {}) {
131
143
  pageSize: topLevelPageSize(choices.length),
132
144
  loop: false,
133
145
  hideMessage: true,
134
- disabledError: disabledError(),
146
+ disabledError,
135
147
  navigationWarning: options.navigationWarning,
136
148
  escapeBehavior: 'ignore',
137
149
  });
@@ -171,10 +171,13 @@ function hiddenSelectTheme(disabledError, navigationHelp = 'exit', navigationWar
171
171
  };
172
172
  }
173
173
  function disabledErrorI18n(disabledError, activeReason) {
174
- const i18n = { disabledError };
174
+ const i18n = typeof disabledError === 'string' ? { disabledError } : { disabledError: disabledError(undefined) };
175
175
  Object.defineProperty(i18n, 'disabledError', {
176
176
  get: () => {
177
177
  const reason = activeReason();
178
+ if (typeof disabledError === 'function') {
179
+ return disabledError(reason);
180
+ }
178
181
  return reason ? `${disabledError}\nReason: ${reason}` : disabledError;
179
182
  },
180
183
  });
package/dist/status.js CHANGED
@@ -5,6 +5,10 @@ import { defaultOdooVersion, markerPath } from './environment.js';
5
5
  import { emptyModuleQualitySummary, mergeModuleQualitySummaries, scanModuleQuality, } from './module-quality.js';
6
6
  import { isValidPathSegment, validateRepoPath } from './path-validation.js';
7
7
  const validSourceTypes = ['private', 'oca', 'external'];
8
+ const summarySeparator = ' \u00B7 ';
9
+ function pluralize(count, singular, plural) {
10
+ return `${count} ${count === 1 ? singular : plural}`;
11
+ }
8
12
  function normalizeSourceType(sourceType) {
9
13
  if (typeof sourceType === 'string' && validSourceTypes.includes(sourceType)) {
10
14
  return sourceType;
@@ -85,6 +89,18 @@ async function missingCoreFiles(target, odooVersion) {
85
89
  missing.push(...composeLayout.missingFiles);
86
90
  return { missing, composeFiles: composeLayout.files, composeErrors: composeLayout.errors };
87
91
  }
92
+ export function environmentBannerSummaryLine(status) {
93
+ if (status.kind !== 'environment') {
94
+ return `Environment: ${summaryText(status)}`;
95
+ }
96
+ const issueCount = status.composeErrors.length + status.invalidSourceRepoPaths.length + status.missingCoreFiles.length;
97
+ const issueSuffix = issueCount > 0 ? `${summarySeparator}${pluralize(issueCount, 'issue', 'issues')}` : '';
98
+ return [
99
+ `Environment: Odoo ${status.odooVersion}`,
100
+ pluralize(status.sourceRepoCount, 'repo', 'repos'),
101
+ pluralize(status.moduleCandidateCount, 'module', 'modules'),
102
+ ].join(summarySeparator) + issueSuffix;
103
+ }
88
104
  function summaryText(status) {
89
105
  if (status.kind === 'no_environment')
90
106
  return 'No WPMoo environment detected.';
@@ -160,3 +160,29 @@ npm test
160
160
  npm run test:coverage
161
161
  npm run build
162
162
  ```
163
+
164
+ ## Coverage watchlist (risk monitoring)
165
+
166
+ The following list is a risk watchlist for Train 2 verification, not a hard gate.
167
+ It uses the full `npm run test:coverage` suite to highlight where changes in
168
+ high-impact runtime files should be reviewed with extra care:
169
+
170
+ - `src/cli.ts`: **watch**: 83.74% line coverage (1458/1741), function coverage
171
+ 92.47% (86/93), branch coverage 80.61% (420/521). This file remains the
172
+ highest-risk surface because it owns direct commands, cockpit dispatch, JSON
173
+ routes, and release-facing error behavior.
174
+ - `src/doctor.ts`: **observe**: 94.48% line coverage (702/743), function
175
+ coverage 95.56% (43/45), branch coverage 86.42% (229/265).
176
+ - `src/module-actions.ts`: **observe**: 96.83% line coverage (519/536),
177
+ function coverage 97.22% (35/36), branch coverage 88.97% (129/145).
178
+ - `src/templates.ts`: **observe**: 99.24% line coverage (262/264), function
179
+ coverage 100.00% (38/38), branch coverage 90.08% (109/121).
180
+ - `src/prompts/index.ts`: **observe**: 95.15% line coverage (294/309),
181
+ function coverage 100.00% (36/36), branch coverage 92.31% (96/104).
182
+
183
+ Train 2 full-suite coverage baseline:
184
+
185
+ - Statements: 92.65% (7304/7883)
186
+ - Branches: 88.24% (2432/2756)
187
+ - Functions: 96.27% (595/618)
188
+ - Lines: 92.65% (7304/7883)
package/docs/handoff.md CHANGED
@@ -42,7 +42,23 @@ npm view "@wpmoo/odoo-dev@$VERSION" version
42
42
  ```
43
43
 
44
44
  `npm view "wpmoo@$VERSION" version` is optional and may report that the short
45
- alias is absent.
45
+ alias is absent. A release is valid when all required scoped packages verify:
46
+
47
+ - `npm view "@wpmoo/toolkit@$VERSION" version`
48
+ - `npm view "@wpmoo/odoo@$VERSION" version`
49
+ - `npm view "@wpmoo/odoo-dev@$VERSION" version`
50
+
51
+ Optional short alias rule:
52
+
53
+ - `wpmoo` may be reported as missing or fail publish without invalidating the
54
+ release candidate. Scoped packages are the supported release artifacts and
55
+ are sufficient to mark the release valid.
56
+
57
+ Suggested smoke check:
58
+
59
+ ```bash
60
+ npm run smoke:published -- "$VERSION"
61
+ ```
46
62
 
47
63
  Current command standard:
48
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/toolkit",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "WPMoo Toolkit for development, staging, and production lifecycle workflows.",
5
5
  "type": "module",
6
6
  "repository": {