@wpmoo/toolkit 0.9.9 → 0.9.10

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
@@ -144,6 +144,10 @@ Every cockpit action maps to a direct command, so the same workflow can be used
144
144
  ./moo restore-snapshot --dry-run before-update devel
145
145
  ```
146
146
 
147
+ In `WPMOO_ENV=prod`, `install`, `update`, and `test` require `WPMOO_ALLOW_PROD_LIFECYCLE=1`.
148
+ `resetdb` and real `restore-snapshot` require `WPMOO_ALLOW_DESTRUCTIVE=1` in `stage` and `prod`.
149
+ `restore-snapshot --dry-run` remains allowed for preview.
150
+
147
151
  Module source actions also have direct commands. Default is `private`; pass `--source-type oca` or `--source-type external` for non-private source repositories:
148
152
 
149
153
  ```bash
@@ -166,9 +170,11 @@ npx @wpmoo/toolkit doctor --json --postgres
166
170
 
167
171
  JSON output is optional; human-readable output remains the default.
168
172
  `doctor --postgres` adds read-only PostgreSQL health and performance diagnostics
169
- such as database size, active connections, slow-query readiness, extension
170
- visibility, and settings.
173
+ such as database size, sessions currently running queries with
174
+ `pg_stat_activity.state = 'active'`, slow-query readiness, extension visibility,
175
+ and settings.
171
176
  `doctor --json --postgres` includes a structured `postgres` object for automation.
177
+ Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics.
172
178
 
173
179
  ## Documentation
174
180
 
@@ -155,9 +155,15 @@ function isDestructiveCommand(command, args) {
155
155
  return true;
156
156
  return command === 'restore-snapshot' && args[0] !== '--dry-run';
157
157
  }
158
+ function isProductionLifecycleCommand(command) {
159
+ return command === 'install' || command === 'update' || command === 'test';
160
+ }
158
161
  function destructiveCommandError(command, envName) {
159
162
  return `Refusing destructive command '${command}' in WPMOO_ENV=${envName}. Set WPMOO_ALLOW_DESTRUCTIVE=1 to run it intentionally.`;
160
163
  }
164
+ function productionLifecycleCommandError(command) {
165
+ return `Refusing production lifecycle command '${command}' in WPMOO_ENV=prod. Set WPMOO_ALLOW_PROD_LIFECYCLE=1 to run it intentionally.`;
166
+ }
161
167
  async function assertDestructiveCommandAllowed(command, args, cwd) {
162
168
  if (!isDestructiveCommand(command, args)) {
163
169
  return;
@@ -172,6 +178,20 @@ async function assertDestructiveCommandAllowed(command, args, cwd) {
172
178
  throw new Error(destructiveCommandError(command, envName));
173
179
  }
174
180
  }
181
+ async function assertProductionLifecycleCommandAllowed(command, cwd) {
182
+ if (!isProductionLifecycleCommand(command)) {
183
+ return;
184
+ }
185
+ const env = await readEnvFile(cwd);
186
+ const envName = process.env.WPMOO_ENV?.trim() || selectedComposeEnvironment(env);
187
+ if (envName !== 'prod') {
188
+ return;
189
+ }
190
+ const allowProdLifecycle = process.env.WPMOO_ALLOW_PROD_LIFECYCLE?.trim() || env?.get('WPMOO_ALLOW_PROD_LIFECYCLE')?.trim();
191
+ if (allowProdLifecycle !== '1') {
192
+ throw new Error(productionLifecycleCommandError(command));
193
+ }
194
+ }
175
195
  async function assertEnvironmentRoot(cwd) {
176
196
  try {
177
197
  await access(join(cwd, markerPath));
@@ -194,6 +214,7 @@ export async function dailyActionPlan(command, argv, cwd = process.cwd()) {
194
214
  await assertEnvironmentRoot(cwd);
195
215
  const scriptPath = await assertScriptExists(cwd, dailyActionScripts[command]);
196
216
  const args = scriptArgs(command, argv);
217
+ await assertProductionLifecycleCommandAllowed(command, cwd);
197
218
  await assertDestructiveCommandAllowed(command, args, cwd);
198
219
  return {
199
220
  cwd,
package/dist/doctor.js CHANGED
@@ -55,6 +55,7 @@ WITH metrics(metric, value) AS (
55
55
  SELECT 'active_connections', count(*)::text
56
56
  FROM pg_stat_activity
57
57
  WHERE datname IS NOT NULL
58
+ AND state = 'active'
58
59
  UNION ALL
59
60
  SELECT 'total_database_size_bytes', COALESCE(sum(pg_database_size(datname)), 0)::text
60
61
  FROM pg_database
@@ -132,12 +133,28 @@ function renderPostgresDiagnostics(diagnostics) {
132
133
  });
133
134
  return parts.length > 0 ? `OK PostgreSQL diagnostics ${parts.join(' ')}` : undefined;
134
135
  }
136
+ function missingPostgresDiagnosticKeys(diagnostics) {
137
+ return postgresDiagnosticKeys.filter((key) => !diagnostics[key]);
138
+ }
139
+ function unavailablePostgresDiagnosticsWarning(diagnostics, missingKeys) {
140
+ return Object.keys(diagnostics).length === 0
141
+ ? 'no diagnostic rows returned'
142
+ : `incomplete diagnostic rows: missing ${missingKeys.join(', ')}`;
143
+ }
135
144
  function integerDiagnostic(value) {
136
145
  if (!value || !/^\d+$/u.test(value)) {
137
146
  return undefined;
138
147
  }
139
148
  return Number.parseInt(value, 10);
140
149
  }
150
+ function malformedPostgresDiagnosticKeys(diagnostics) {
151
+ const numericKeys = [
152
+ 'database_count',
153
+ 'active_connections',
154
+ 'total_database_size_bytes',
155
+ ];
156
+ return numericKeys.filter((key) => diagnostics[key] !== undefined && integerDiagnostic(diagnostics[key]) === undefined);
157
+ }
141
158
  function structuredPostgresDiagnostics(diagnostics) {
142
159
  const structured = {};
143
160
  const databaseCount = integerDiagnostic(diagnostics.database_count);
@@ -555,9 +572,13 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
555
572
  if (actualOptions.postgres) {
556
573
  try {
557
574
  const postgresDiagnostics = await readPostgresDiagnostics(target, actualRunner);
558
- const renderedPostgresDiagnostics = renderPostgresDiagnostics(postgresDiagnostics);
559
- if (renderedPostgresDiagnostics) {
560
- checks.push(renderedPostgresDiagnostics);
575
+ const missingKeys = missingPostgresDiagnosticKeys(postgresDiagnostics);
576
+ const malformedKeys = malformedPostgresDiagnosticKeys(postgresDiagnostics);
577
+ if (missingKeys.length === 0 && malformedKeys.length === 0) {
578
+ const renderedPostgresDiagnostics = renderPostgresDiagnostics(postgresDiagnostics);
579
+ if (renderedPostgresDiagnostics) {
580
+ checks.push(renderedPostgresDiagnostics);
581
+ }
561
582
  report.postgres = {
562
583
  requested: true,
563
584
  available: true,
@@ -565,12 +586,14 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
565
586
  };
566
587
  }
567
588
  else {
568
- const warning = 'no diagnostic rows returned';
589
+ const warning = malformedKeys.length > 0
590
+ ? `malformed diagnostic values: ${malformedKeys.join(', ')}`
591
+ : unavailablePostgresDiagnosticsWarning(postgresDiagnostics, missingKeys);
569
592
  warnings.push(`PostgreSQL diagnostics unavailable: ${warning}`);
570
593
  report.postgres = {
571
594
  requested: true,
572
595
  available: false,
573
- diagnostics: {},
596
+ diagnostics: structuredPostgresDiagnostics(postgresDiagnostics),
574
597
  warning,
575
598
  };
576
599
  }
@@ -43,6 +43,8 @@ export function renderComposeEnvExample(options) {
43
43
  '# Required only when intentionally running destructive database actions',
44
44
  '# such as resetdb or restore-snapshot with WPMOO_ENV=stage or WPMOO_ENV=prod.',
45
45
  '# WPMOO_ALLOW_DESTRUCTIVE=1',
46
+ '# Required only when intentionally running install/update/test in WPMOO_ENV=prod.',
47
+ '# WPMOO_ALLOW_PROD_LIFECYCLE=1',
46
48
  '',
47
49
  ].join('\n');
48
50
  }
package/dist/help.js CHANGED
@@ -85,6 +85,11 @@ Daily actions:
85
85
  Generated environments also include ./moo for local compose commands such as ./moo start.
86
86
  Use ./moo or npx @wpmoo/toolkit with the same daily action arguments.
87
87
 
88
+ Production command guards:
89
+ In WPMOO_ENV=prod, install/update/test require WPMOO_ALLOW_PROD_LIFECYCLE=1.
90
+ resetdb and real restore-snapshot require WPMOO_ALLOW_DESTRUCTIVE=1 in stage/prod.
91
+ restore-snapshot --dry-run remains allowed for preview.
92
+
88
93
  Cockpit:
89
94
  Run npx @wpmoo/toolkit inside a generated environment to open the cockpit.
90
95
  Use Command palette / to search slash commands across services, modules, database,
@@ -104,7 +109,9 @@ Status and doctor:
104
109
  doctor: deeper health check. May check Docker CLI access and GitHub workflows.
105
110
  doctor --fix: applies safe file-level repairs. Runs doctor again after fixes.
106
111
  doctor --postgres: adds read-only PostgreSQL diagnostics such as database size,
107
- active connections, slow-query readiness, extension visibility, and settings.
112
+ sessions currently running queries with pg_stat_activity.state = 'active',
113
+ slow-query readiness, extension visibility, and settings.
114
+ Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics.
108
115
 
109
116
  Task recipes:
110
117
  Create environment:
@@ -148,6 +155,7 @@ Machine-readable JSON output:
148
155
  npx @wpmoo/toolkit source sync --json
149
156
  npx @wpmoo/toolkit doctor --json
150
157
  doctor --json --postgres includes a structured postgres object for automation.
158
+ Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics.
151
159
 
152
160
  Example:
153
161
  npx @wpmoo/toolkit create \\
package/dist/templates.js CHANGED
@@ -208,6 +208,10 @@ resources/odoo/entrypoint.sh
208
208
 
209
209
  Development uses compose.yaml plus compose/dev.yaml by default.
210
210
  Set WPMOO_ENV=stage or WPMOO_ENV=prod only after providing production-grade secrets and volumes.
211
+ In WPMOO_ENV=prod, module lifecycle commands such as install, update, and test
212
+ require WPMOO_ALLOW_PROD_LIFECYCLE=1. Destructive database commands such as
213
+ resetdb and real restore-snapshot require WPMOO_ALLOW_DESTRUCTIVE=1 in stage
214
+ and prod. restore-snapshot --dry-run remains available for preview.
211
215
 
212
216
  If copied from the standalone resource, additional compose notes are in
213
217
  \`docs/compose.md\`.
@@ -568,6 +572,23 @@ require_destructive_allowed() {
568
572
  fi
569
573
  }
570
574
 
575
+ allow_prod_lifecycle() {
576
+ local value="\${WPMOO_ALLOW_PROD_LIFECYCLE:-$(env_file_value WPMOO_ALLOW_PROD_LIFECYCLE)}"
577
+ [[ "$value" == "1" ]]
578
+ }
579
+
580
+ require_prod_lifecycle_allowed() {
581
+ local command="$1"
582
+ local env_name
583
+ env_name="$(selected_env)"
584
+ if [[ "$env_name" == "prod" ]]; then
585
+ if ! allow_prod_lifecycle; then
586
+ echo "Refusing production lifecycle command '$command' in WPMOO_ENV=prod. Set WPMOO_ALLOW_PROD_LIFECYCLE=1 to run it intentionally." >&2
587
+ exit 1
588
+ fi
589
+ fi
590
+ }
591
+
571
592
  run_script() {
572
593
  local script="$1"
573
594
  shift
@@ -613,16 +634,19 @@ case "$command" in
613
634
  "install")
614
635
  shift
615
636
  require_module_args "$command" "$@"
637
+ require_prod_lifecycle_allowed "$command"
616
638
  run_script ./scripts/install.sh "$@"
617
639
  ;;
618
640
  "update")
619
641
  shift
620
642
  require_module_args "$command" "$@"
643
+ require_prod_lifecycle_allowed "$command"
621
644
  run_script ./scripts/update.sh "$@"
622
645
  ;;
623
646
  "test")
624
647
  shift
625
648
  validate_test_args "$@"
649
+ require_prod_lifecycle_allowed "$command"
626
650
  run_script ./scripts/test.sh "$@"
627
651
  ;;
628
652
  "resetdb")
@@ -23,7 +23,7 @@ not validate staging or production deployments.
23
23
  | Doctor checks | Metadata, compose files, scripts, source repo paths, and local tooling checks behave as expected. | `npx @wpmoo/toolkit doctor` or `./moo doctor` |
24
24
  | Doctor safe fixes | Safe file-level fixes are applied only with `--fix`, then doctor runs again and reports any remaining manual issues. | `npx @wpmoo/toolkit doctor --fix` |
25
25
  | Generated Postgres checks | For PostgreSQL 18 environments, doctor validates db mount targets avoid old PG image-specific paths and can normalize safe targets with `--fix`. | `npx @wpmoo/toolkit doctor`, `npx @wpmoo/toolkit doctor --fix` |
26
- | PostgreSQL diagnostics | Optional read-only database health/performance diagnostics report database count, active connections, total database size, slow-query readiness, extension visibility, and selected settings without failing doctor when the database is unavailable. | `npx @wpmoo/toolkit doctor --postgres`, `npx @wpmoo/toolkit doctor --json --postgres` |
26
+ | PostgreSQL diagnostics | Optional read-only database health/performance diagnostics report database count, sessions currently running queries with `pg_stat_activity.state = 'active'`, total database size, slow-query readiness, extension visibility, and selected settings without failing doctor when the database is unavailable. | `npx @wpmoo/toolkit doctor --postgres`, `npx @wpmoo/toolkit doctor --json --postgres` |
27
27
  | Source repo add/remove | Source repository registration and submodule lifecycle behave correctly. | `npx @wpmoo/toolkit add-repo ...`, `npx @wpmoo/toolkit remove-repo ...` |
28
28
  | Source manifest sync | Source repo metadata, `.gitmodules`, and `odoo/custom/manifests/sources.yaml` stay aligned. | `npx @wpmoo/toolkit source list`, `npx @wpmoo/toolkit source sync` |
29
29
  | Module add/remove | Module skeleton files include manifest, model, access CSV, explicit view XML, action/menu XML, post-install test scaffold, and selected source repo registration. Existing scaffold files are not overwritten. | `npx @wpmoo/toolkit add-module ...`, `npx @wpmoo/toolkit remove-module ...` |
@@ -53,6 +53,12 @@ scripts unless `.env` or the process environment explicitly sets
53
53
  `WPMOO_ALLOW_DESTRUCTIVE=1`. `restore-snapshot --dry-run` remains allowed for
54
54
  safe preview.
55
55
 
56
+ When `WPMOO_ENV=prod`, WPMoo also refuses module lifecycle commands that mutate
57
+ or exercise the Odoo database (`install`, `update`, and `test`) unless `.env` or
58
+ the process environment explicitly sets `WPMOO_ALLOW_PROD_LIFECYCLE=1`.
59
+ Staging keeps these commands available for release rehearsal while still
60
+ enforcing the destructive database guard above.
61
+
56
62
  For PostgreSQL 18 environments (including `POSTGRES_IMAGE=postgres:18`), ensure db
57
63
  volume and tmpfs mount targets use `/var/lib/postgresql` directly:
58
64
 
@@ -70,12 +76,16 @@ is involved, use PostgreSQL upgrade tooling first.
70
76
 
71
77
  Use `doctor --postgres` when the database container is running and you want
72
78
  read-only PostgreSQL diagnostics. The check uses fixed diagnostic queries for
73
- database count, active connections, aggregate database size, slow-query logging
74
- readiness, `pg_stat_statements` availability, and `shared_buffers`. If the
75
- database is unavailable, doctor reports a warning instead of failing the whole
76
- environment check.
79
+ database count, sessions currently running queries where
80
+ `pg_stat_activity.state` is `active`, aggregate database size, slow-query
81
+ logging readiness,
82
+ `pg_stat_statements` availability, and `shared_buffers`. If the database is
83
+ unavailable, doctor reports a warning instead of failing the whole environment
84
+ check.
77
85
  JSON output preserves `checks` and `warnings` while adding a structured
78
86
  `postgres` object when `--postgres` is requested.
87
+ Incomplete or malformed PostgreSQL metric rows are reported as unavailable
88
+ diagnostics.
79
89
 
80
90
  ## Safe reset policy
81
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/toolkit",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
4
4
  "description": "WPMoo Toolkit for development, staging, and production lifecycle workflows.",
5
5
  "type": "module",
6
6
  "repository": {