@wpmoo/toolkit 0.9.26 → 0.9.28

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
@@ -170,12 +170,31 @@ npx @wpmoo/toolkit doctor --json --postgres
170
170
  ```
171
171
 
172
172
  JSON output is optional; human-readable output remains the default.
173
- `doctor --postgres` adds read-only PostgreSQL health and performance diagnostics
174
- such as database size, sessions currently running queries with
175
- `pg_stat_activity.state = 'active'`, connection utilization against
176
- `max_connections`, slow-query readiness, extension visibility, and settings.
173
+ `doctor --postgres` runs read-only PostgreSQL diagnostics as advisory checks only; it
174
+ does not perform automatic tuning.
175
+ Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics
176
+ instead of being treated as successful checks.
177
+
178
+ Current advisory checks include:
179
+
180
+ - sessions currently running queries where `pg_stat_activity.state = 'active'`;
181
+ - connection utilization against `max_connections`;
182
+ - long transaction / idle-in-transaction warnings from `pg_stat_activity`;
183
+ - table health visibility (for example table and index bloat signals, index scan
184
+ efficiency, and vacuum-related blockers);
185
+ - optional unused index advisory output when index usage data is available;
186
+ - WAL and capacity visibility including WAL activity and disk-level pressure context;
187
+ - slow-query and query-plan readiness checks for common `log_min_duration_statement`
188
+ and `pg_stat_statements` prerequisites.
189
+
190
+ `npx @wpmoo/toolkit doctor --postgres` and
191
+ `npx @wpmoo/toolkit doctor --json --postgres` use the same checks, and the
192
+ JSON variant exposes a versioned PostgreSQL diagnostics contract.
193
+
177
194
  `doctor --json --postgres` includes a structured `postgres` object for automation.
178
- Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics.
195
+ `doctor --json --postgres` keeps the JSON contract stable by versioning the
196
+ `postgres` payload; individual fields are optional so automation can safely handle
197
+ environments where PostgreSQL does not expose a metric.
179
198
 
180
199
  ## Release Artifacts
181
200
 
@@ -203,8 +222,12 @@ warning while keeping the scoped package release valid.
203
222
 
204
223
  ## Documentation
205
224
 
225
+ - [Command Reference](docs/command-reference.md)
206
226
  - [External Resources](docs/external-resources.md)
207
227
  - [Generated Environment Verification](docs/generated-environment-verification.md)
228
+ - [Lifecycle Recipes](docs/lifecycle-recipes.md)
229
+ - [Troubleshooting](docs/troubleshooting.md)
230
+ - [1.0 Readiness](docs/1-0-readiness.md)
208
231
  - Public documentation site: <https://wpmoo.org>
209
232
 
210
233
  ## License
package/dist/doctor.js CHANGED
@@ -5,6 +5,7 @@ import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './
5
5
  import { dailyActionScripts } from './daily-actions.js';
6
6
  import { defaultPostgresVersion } from './external-templates.js';
7
7
  import { defaultOdooVersion, markerPath, replaceSourceRepos } from './environment.js';
8
+ import { POSTGRES_DIAGNOSTICS_CONTRACT_VERSION, POSTGRES_DIAGNOSTICS_QUERY, malformedPostgresDiagnosticKeys, missingPostgresDiagnosticKeys, parsePostgresDiagnostics, postgresPostgresWarnings, renderPostgresDiagnostics, structuredPostgresDiagnostics, unavailablePostgresDiagnosticsWarning, } from './postgres-diagnostics.js';
8
9
  import { listGitmoduleSources, readSourceManifest, sourceReposFromManifest, sourceManifestPath, syncManifestFromMetadataAndGitmodules, writeSourceManifest, } from './source-manifest.js';
9
10
  const realCommandRunner = async (command, args, options) => {
10
11
  const result = await execa(command, args, { cwd: options.cwd });
@@ -46,128 +47,6 @@ function isMetadataError(message) {
46
47
  message.startsWith('Invalid sourceRepos entry in .wpmoo/odoo.json'));
47
48
  }
48
49
  const incompatiblePostgres18MountTargets = ['/var/lib/postgresql/data', '/var/lib/postgresql/18/docker'];
49
- const postgresDiagnosticQuery = `
50
- WITH metrics(metric, value) AS (
51
- SELECT 'database_count', count(*)::text
52
- FROM pg_database
53
- WHERE datistemplate = false
54
- UNION ALL
55
- SELECT 'active_connections', count(*)::text
56
- FROM pg_stat_activity
57
- WHERE datname IS NOT NULL
58
- AND state = 'active'
59
- UNION ALL
60
- SELECT 'connection_count', count(*)::text
61
- FROM pg_stat_activity
62
- WHERE datname IS NOT NULL
63
- UNION ALL
64
- SELECT 'max_connections', COALESCE(
65
- (SELECT setting FROM pg_settings WHERE name = 'max_connections'),
66
- 'unavailable'
67
- )
68
- UNION ALL
69
- SELECT 'total_database_size_bytes', COALESCE(sum(pg_database_size(datname)), 0)::text
70
- FROM pg_database
71
- WHERE datistemplate = false
72
- UNION ALL
73
- SELECT 'largest_database_name', COALESCE(
74
- (
75
- SELECT datname
76
- FROM pg_database
77
- WHERE datistemplate = false
78
- ORDER BY pg_database_size(datname) DESC, datname
79
- LIMIT 1
80
- ),
81
- 'unavailable'
82
- )
83
- UNION ALL
84
- SELECT 'largest_database_size_bytes', COALESCE(
85
- (
86
- SELECT pg_database_size(datname)::text
87
- FROM pg_database
88
- WHERE datistemplate = false
89
- ORDER BY pg_database_size(datname) DESC, datname
90
- LIMIT 1
91
- ),
92
- '0'
93
- )
94
- UNION ALL
95
- SELECT 'slow_query_logging', COALESCE(
96
- (SELECT setting || unit FROM pg_settings WHERE name = 'log_min_duration_statement'),
97
- 'unavailable'
98
- )
99
- UNION ALL
100
- SELECT 'pg_stat_statements',
101
- CASE
102
- WHEN EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements') THEN 'installed'
103
- WHEN EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pg_stat_statements') THEN 'available'
104
- ELSE 'unavailable'
105
- END
106
- UNION ALL
107
- SELECT 'pg_stat_statements_available_version', COALESCE(
108
- (SELECT default_version FROM pg_available_extensions WHERE name = 'pg_stat_statements'),
109
- 'unavailable'
110
- )
111
- UNION ALL
112
- SELECT 'pg_stat_statements_installed_version', COALESCE(
113
- (SELECT extversion FROM pg_extension WHERE extname = 'pg_stat_statements'),
114
- ''
115
- )
116
- UNION ALL
117
- SELECT 'shared_preload_libraries', COALESCE(
118
- (SELECT setting FROM pg_settings WHERE name = 'shared_preload_libraries'),
119
- 'unavailable'
120
- )
121
- UNION ALL
122
- SELECT 'shared_buffers', COALESCE(
123
- (SELECT setting FROM pg_settings WHERE name = 'shared_buffers'),
124
- 'unavailable'
125
- )
126
- )
127
- SELECT metric || '|' || value
128
- FROM metrics
129
- ORDER BY CASE metric
130
- WHEN 'database_count' THEN 1
131
- WHEN 'active_connections' THEN 2
132
- WHEN 'connection_count' THEN 3
133
- WHEN 'max_connections' THEN 4
134
- WHEN 'total_database_size_bytes' THEN 5
135
- WHEN 'largest_database_name' THEN 6
136
- WHEN 'largest_database_size_bytes' THEN 7
137
- WHEN 'slow_query_logging' THEN 8
138
- WHEN 'pg_stat_statements' THEN 9
139
- WHEN 'pg_stat_statements_available_version' THEN 10
140
- WHEN 'pg_stat_statements_installed_version' THEN 11
141
- WHEN 'shared_preload_libraries' THEN 12
142
- WHEN 'shared_buffers' THEN 13
143
- ELSE 99
144
- END;
145
- `.trim();
146
- const postgresDiagnosticKeys = [
147
- 'database_count',
148
- 'active_connections',
149
- 'connection_count',
150
- 'max_connections',
151
- 'total_database_size_bytes',
152
- 'largest_database_name',
153
- 'largest_database_size_bytes',
154
- 'slow_query_logging',
155
- 'pg_stat_statements',
156
- 'pg_stat_statements_available_version',
157
- 'pg_stat_statements_installed_version',
158
- 'shared_preload_libraries',
159
- 'shared_buffers',
160
- ];
161
- const requiredPostgresDiagnosticKeys = [
162
- 'database_count',
163
- 'active_connections',
164
- 'connection_count',
165
- 'max_connections',
166
- 'total_database_size_bytes',
167
- 'slow_query_logging',
168
- 'pg_stat_statements',
169
- 'shared_buffers',
170
- ];
171
50
  function parsePostgresMajorFromValue(value) {
172
51
  if (!value)
173
52
  return undefined;
@@ -178,139 +57,8 @@ function parsePostgresMajorFromValue(value) {
178
57
  const match = trimmed.match(/postgres:([0-9]{1,3})(?:[-._][A-Za-z0-9._-]+)?(?:@[\w:.-]+)?/i);
179
58
  return match?.[1];
180
59
  }
181
- function parsePostgresDiagnostics(output) {
182
- const diagnostics = {};
183
- const allowedKeys = new Set(postgresDiagnosticKeys);
184
- for (const rawLine of output.split(/\r?\n/u)) {
185
- const line = rawLine.trim();
186
- if (!line)
187
- continue;
188
- const separatorIndex = line.indexOf('|');
189
- if (separatorIndex === -1)
190
- continue;
191
- const key = line.slice(0, separatorIndex).trim();
192
- const value = line.slice(separatorIndex + 1).trim();
193
- if (allowedKeys.has(key) && value) {
194
- diagnostics[key] = value;
195
- }
196
- }
197
- return diagnostics;
198
- }
199
- function renderPostgresDiagnostics(diagnostics) {
200
- const connectionUtilizationPct = postgresConnectionUtilizationPct(diagnostics);
201
- const parts = postgresDiagnosticKeys.flatMap((key) => {
202
- const value = diagnostics[key];
203
- const rendered = value ? [`${key}=${value}`] : [];
204
- if (key === 'max_connections' && connectionUtilizationPct !== undefined) {
205
- rendered.push(`connection_utilization_pct=${connectionUtilizationPct}`);
206
- }
207
- return rendered;
208
- });
209
- return parts.length > 0 ? `OK PostgreSQL diagnostics ${parts.join(' ')}` : undefined;
210
- }
211
- function missingPostgresDiagnosticKeys(diagnostics) {
212
- return requiredPostgresDiagnosticKeys.filter((key) => !diagnostics[key]);
213
- }
214
- function unavailablePostgresDiagnosticsWarning(diagnostics, missingKeys) {
215
- return Object.keys(diagnostics).length === 0
216
- ? 'no diagnostic rows returned'
217
- : `incomplete diagnostic rows: missing ${missingKeys.join(', ')}`;
218
- }
219
- function integerDiagnostic(value) {
220
- if (!value || !/^\d+$/u.test(value)) {
221
- return undefined;
222
- }
223
- return Number.parseInt(value, 10);
224
- }
225
- function malformedPostgresDiagnosticKeys(diagnostics) {
226
- const numericKeys = [
227
- 'database_count',
228
- 'active_connections',
229
- 'connection_count',
230
- 'max_connections',
231
- 'total_database_size_bytes',
232
- 'largest_database_size_bytes',
233
- ];
234
- return numericKeys.filter((key) => diagnostics[key] !== undefined && integerDiagnostic(diagnostics[key]) === undefined);
235
- }
236
- function postgresConnectionUtilizationPct(diagnostics) {
237
- const connectionCount = integerDiagnostic(diagnostics.connection_count);
238
- const maxConnections = integerDiagnostic(diagnostics.max_connections);
239
- if (connectionCount === undefined || maxConnections === undefined || maxConnections <= 0) {
240
- return undefined;
241
- }
242
- return Math.round((connectionCount / maxConnections) * 100);
243
- }
244
- function postgresConnectionUtilizationWarning(diagnostics) {
245
- const connectionCount = integerDiagnostic(diagnostics.connection_count);
246
- const maxConnections = integerDiagnostic(diagnostics.max_connections);
247
- const utilizationPct = postgresConnectionUtilizationPct(diagnostics);
248
- if (connectionCount === undefined ||
249
- maxConnections === undefined ||
250
- utilizationPct === undefined ||
251
- utilizationPct < 80) {
252
- return undefined;
253
- }
254
- return `PostgreSQL connection utilization is high: ${utilizationPct}% of max_connections used (${connectionCount}/${maxConnections}).`;
255
- }
256
- function postgresSlowQueryLoggingWarning(diagnostics) {
257
- const slowQueryLogging = diagnostics.slow_query_logging?.trim();
258
- if (!slowQueryLogging || (!/^-1\s*(?:ms)?$/iu.test(slowQueryLogging) && !/^off$/iu.test(slowQueryLogging))) {
259
- return undefined;
260
- }
261
- return `PostgreSQL slow-query logging is disabled (log_min_duration_statement=${slowQueryLogging}). Enable it before performance triage.`;
262
- }
263
- function postgresExtensionVisibilityWarning(diagnostics) {
264
- if (diagnostics.pg_stat_statements === 'available' &&
265
- diagnostics.pg_stat_statements_available_version &&
266
- !diagnostics.pg_stat_statements_installed_version) {
267
- return 'PostgreSQL pg_stat_statements is available but not installed. Install it before query-level performance triage.';
268
- }
269
- return undefined;
270
- }
271
- function structuredPostgresDiagnostics(diagnostics) {
272
- const structured = {};
273
- const databaseCount = integerDiagnostic(diagnostics.database_count);
274
- const activeConnections = integerDiagnostic(diagnostics.active_connections);
275
- const connectionCount = integerDiagnostic(diagnostics.connection_count);
276
- const maxConnections = integerDiagnostic(diagnostics.max_connections);
277
- const connectionUtilizationPct = postgresConnectionUtilizationPct(diagnostics);
278
- const totalDatabaseSizeBytes = integerDiagnostic(diagnostics.total_database_size_bytes);
279
- const largestDatabaseSizeBytes = integerDiagnostic(diagnostics.largest_database_size_bytes);
280
- if (databaseCount !== undefined)
281
- structured.databaseCount = databaseCount;
282
- if (activeConnections !== undefined)
283
- structured.activeConnections = activeConnections;
284
- if (connectionCount !== undefined)
285
- structured.connectionCount = connectionCount;
286
- if (maxConnections !== undefined)
287
- structured.maxConnections = maxConnections;
288
- if (connectionUtilizationPct !== undefined)
289
- structured.connectionUtilizationPct = connectionUtilizationPct;
290
- if (totalDatabaseSizeBytes !== undefined)
291
- structured.totalDatabaseSizeBytes = totalDatabaseSizeBytes;
292
- if (diagnostics.largest_database_name)
293
- structured.largestDatabaseName = diagnostics.largest_database_name;
294
- if (largestDatabaseSizeBytes !== undefined)
295
- structured.largestDatabaseSizeBytes = largestDatabaseSizeBytes;
296
- if (diagnostics.slow_query_logging)
297
- structured.slowQueryLogging = diagnostics.slow_query_logging;
298
- if (diagnostics.pg_stat_statements)
299
- structured.pgStatStatements = diagnostics.pg_stat_statements;
300
- if (diagnostics.pg_stat_statements_available_version) {
301
- structured.pgStatStatementsAvailableVersion = diagnostics.pg_stat_statements_available_version;
302
- }
303
- if (diagnostics.pg_stat_statements_installed_version) {
304
- structured.pgStatStatementsInstalledVersion = diagnostics.pg_stat_statements_installed_version;
305
- }
306
- if (diagnostics.shared_preload_libraries)
307
- structured.sharedPreloadLibraries = diagnostics.shared_preload_libraries;
308
- if (diagnostics.shared_buffers)
309
- structured.sharedBuffers = diagnostics.shared_buffers;
310
- return structured;
311
- }
312
60
  async function readPostgresDiagnostics(target, runner) {
313
- const queryLiteral = JSON.stringify(postgresDiagnosticQuery);
61
+ const queryLiteral = JSON.stringify(POSTGRES_DIAGNOSTICS_QUERY);
314
62
  const command = [
315
63
  `query=${queryLiteral}`,
316
64
  '. ./scripts/lib.sh >/dev/null',
@@ -716,21 +464,11 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
716
464
  }
717
465
  report.postgres = {
718
466
  requested: true,
467
+ contractVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
719
468
  available: true,
720
469
  diagnostics: structuredPostgresDiagnostics(postgresDiagnostics),
721
470
  };
722
- const connectionUtilizationWarning = postgresConnectionUtilizationWarning(postgresDiagnostics);
723
- if (connectionUtilizationWarning) {
724
- warnings.push(connectionUtilizationWarning);
725
- }
726
- const slowQueryLoggingWarning = postgresSlowQueryLoggingWarning(postgresDiagnostics);
727
- if (slowQueryLoggingWarning) {
728
- warnings.push(slowQueryLoggingWarning);
729
- }
730
- const extensionVisibilityWarning = postgresExtensionVisibilityWarning(postgresDiagnostics);
731
- if (extensionVisibilityWarning) {
732
- warnings.push(extensionVisibilityWarning);
733
- }
471
+ warnings.push(...postgresPostgresWarnings(postgresDiagnostics));
734
472
  }
735
473
  else {
736
474
  const warning = malformedKeys.length > 0
@@ -739,6 +477,7 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
739
477
  warnings.push(`PostgreSQL diagnostics unavailable: ${warning}`);
740
478
  report.postgres = {
741
479
  requested: true,
480
+ contractVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
742
481
  available: false,
743
482
  diagnostics: structuredPostgresDiagnostics(postgresDiagnostics),
744
483
  warning,
@@ -750,6 +489,7 @@ export async function getDoctorReport(target = process.cwd(), runnerOrOptions =
750
489
  warnings.push(`PostgreSQL diagnostics unavailable: ${warning}`);
751
490
  report.postgres = {
752
491
  requested: true,
492
+ contractVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
753
493
  available: false,
754
494
  diagnostics: {},
755
495
  warning,