@wpmoo/toolkit 0.9.28 → 0.9.30

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
@@ -64,6 +64,9 @@ Short alias:
64
64
  npx wpmoo
65
65
  ```
66
66
 
67
+ Optional short alias: `npx wpmoo`. Use `npx @wpmoo/toolkit` for documentation,
68
+ scripts, and automation.
69
+
67
70
  Deprecated compatibility aliases:
68
71
 
69
72
  ```bash
@@ -71,7 +74,10 @@ npx @wpmoo/odoo
71
74
  npx @wpmoo/odoo-dev
72
75
  ```
73
76
 
74
- Deprecated package paths `npx @wpmoo/odoo` and `npx @wpmoo/odoo-dev` remain available as compatibility aliases that redirect to `@wpmoo/toolkit`.
77
+ Deprecated package paths `npx @wpmoo/odoo` and `npx @wpmoo/odoo-dev` remain
78
+ available through the 1.x line as compatibility aliases that redirect to
79
+ `@wpmoo/toolkit`. Removing either compatibility alias requires a future major
80
+ release and prior notice.
75
81
 
76
82
  When the current directory is not already a WPMoo environment, the CLI opens the create flow. It asks for a product slug, Odoo version, and environment folder. The default environment folder is `./<product>_dev`.
77
83
 
@@ -102,7 +108,7 @@ The cockpit is the daily workspace. It starts with environment status and then s
102
108
  ```text
103
109
  WPMoo Cockpit
104
110
  |-- Command palette /
105
- | |-- search commands such as /test, /logs, /doctor, /safe-reset
111
+ | |-- search commands such as /test, /modules, /install-module, /doctor, /safe-reset
106
112
  |-- Services
107
113
  | |-- start
108
114
  | |-- stop
@@ -135,12 +141,16 @@ WPMoo Cockpit
135
141
 
136
142
  Every cockpit action maps to a direct command, so the same workflow can be used interactively or scripted:
137
143
 
144
+ When an environment has many module candidates, module selection switches to
145
+ search so names, repositories, and source categories can be filtered quickly.
146
+
138
147
  ```bash
139
148
  ./moo start
140
149
  ./moo logs odoo
141
150
  ./moo update sale
142
151
  ./moo test sale
143
152
  ./moo snapshot devel before-update
153
+ ./moo snapshot --list
144
154
  ./moo restore-snapshot --dry-run before-update devel
145
155
  ```
146
156
 
@@ -148,6 +158,7 @@ In `WPMOO_ENV=stage`, `install` and `update` require `WPMOO_ALLOW_STAGE_LIFECYCL
148
158
  In `WPMOO_ENV=prod`, `install`, `update`, and `test` require `WPMOO_ALLOW_PROD_LIFECYCLE=1`.
149
159
  `resetdb` and real `restore-snapshot` require `WPMOO_ALLOW_DESTRUCTIVE=1` in `stage` and `prod`.
150
160
  `restore-snapshot --dry-run` remains allowed for preview.
161
+ For short-lived local approvals, add JSONL entries to `.wpmoo/approvals.jsonl`; generated `.gitignore` keeps that ledger out of Git.
151
162
 
152
163
  Module source actions also have direct commands. Default is `private`; pass `--source-type oca` or `--source-type external` for non-private source repositories:
153
164
 
@@ -170,6 +181,9 @@ npx @wpmoo/toolkit doctor --json --postgres
170
181
  ```
171
182
 
172
183
  JSON output is optional; human-readable output remains the default.
184
+ Human `doctor` output is grouped into stable sections (`Generated files`,
185
+ `Compose`, `Source repositories`, `PostgreSQL`, and `Host tools`) so terminal
186
+ operators can see which lifecycle layer needs attention first.
173
187
  `doctor --postgres` runs read-only PostgreSQL diagnostics as advisory checks only; it
174
188
  does not perform automatic tuning.
175
189
  Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics
@@ -185,7 +199,10 @@ Current advisory checks include:
185
199
  - optional unused index advisory output when index usage data is available;
186
200
  - WAL and capacity visibility including WAL activity and disk-level pressure context;
187
201
  - slow-query and query-plan readiness checks for common `log_min_duration_statement`
188
- and `pg_stat_statements` prerequisites.
202
+ and `pg_stat_statements` prerequisites;
203
+ - read-only PostgreSQL configuration visibility for `shared_buffers`, `work_mem`,
204
+ `maintenance_work_mem`, `effective_cache_size`, and
205
+ `shared_preload_libraries`.
189
206
 
190
207
  `npx @wpmoo/toolkit doctor --postgres` and
191
208
  `npx @wpmoo/toolkit doctor --json --postgres` use the same checks, and the
@@ -195,6 +212,15 @@ JSON variant exposes a versioned PostgreSQL diagnostics contract.
195
212
  `doctor --json --postgres` keeps the JSON contract stable by versioning the
196
213
  `postgres` payload; individual fields are optional so automation can safely handle
197
214
  environments where PostgreSQL does not expose a metric.
215
+ All `doctor --json` reports also include optional `sections` entries that group
216
+ checks, warnings, and errors without changing the legacy flat arrays.
217
+
218
+ JSON compatibility policy:
219
+
220
+ - Automation should ignore unknown JSON fields.
221
+ - Minor and patch releases may add optional fields without a breaking release.
222
+ - Removing, renaming, or changing the meaning of a documented field requires a
223
+ major release or a `schemaVersion` bump.
198
224
 
199
225
  ## Release Artifacts
200
226
 
@@ -219,6 +245,18 @@ warning while keeping the scoped package release valid.
219
245
  packages are valid.
220
246
  - **Smoke expectation**: run `npm run smoke:published -- "$VERSION"` after the
221
247
  release tag workflow completes.
248
+ - **Deterministic smoke target**: pin the target package explicitly so smoke checks
249
+ are reproducible across reruns:
250
+
251
+ ```bash
252
+ VERSION="$(node -p "require('./package.json').version")"
253
+ WPMOO_PUBLISHED_PACKAGE_SPEC="@wpmoo/toolkit@$VERSION" npm run smoke:published -- "$VERSION"
254
+ ```
255
+
256
+ Use one pinned command for each target artifact you validate; the workflow itself
257
+ remains valid only when required scoped artifacts pass.
258
+ - **1.0 release smoke**: For `1.0.0`, generated-environment acceptance smoke is
259
+ required before the release is considered final.
222
260
 
223
261
  ## Documentation
224
262
 
@@ -0,0 +1,74 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const approvalScopes = [
4
+ 'destructive',
5
+ 'stage-lifecycle',
6
+ 'prod-lifecycle',
7
+ 'no-recent-snapshot',
8
+ 'migration-risk',
9
+ ];
10
+ function isRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ function isApprovalScope(value) {
14
+ return typeof value === 'string' && approvalScopes.includes(value);
15
+ }
16
+ function isEnvironmentKind(value) {
17
+ return value === 'stage' || value === 'prod';
18
+ }
19
+ function isFutureIsoDate(value, now) {
20
+ if (typeof value !== 'string' || !value.trim()) {
21
+ return false;
22
+ }
23
+ const expiresAt = Date.parse(value);
24
+ return Number.isFinite(expiresAt) && expiresAt > now.getTime();
25
+ }
26
+ function activeApprovalFromLine(line, options) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(line);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ if (!isRecord(parsed)) {
35
+ return undefined;
36
+ }
37
+ if (!isApprovalScope(parsed.scope) || !isEnvironmentKind(parsed.environment)) {
38
+ return undefined;
39
+ }
40
+ if (parsed.environment !== options.environment) {
41
+ return undefined;
42
+ }
43
+ const command = typeof parsed.command === 'string' ? parsed.command : undefined;
44
+ if (command && command !== options.command) {
45
+ return undefined;
46
+ }
47
+ if (!isFutureIsoDate(parsed.expiresAt, options.now)) {
48
+ return undefined;
49
+ }
50
+ return {
51
+ scope: parsed.scope,
52
+ environment: parsed.environment,
53
+ ...(command ? { command: command } : {}),
54
+ expiresAt: parsed.expiresAt,
55
+ ...(typeof parsed.reason === 'string' && parsed.reason.trim() ? { reason: parsed.reason.trim() } : {}),
56
+ label: `approval:${parsed.scope}`,
57
+ };
58
+ }
59
+ export async function readActiveApprovals(target, options) {
60
+ let content;
61
+ try {
62
+ content = await readFile(join(target, '.wpmoo/approvals.jsonl'), 'utf8');
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ const resolvedOptions = { ...options, now: options.now ?? new Date() };
68
+ return content
69
+ .split(/\r?\n/)
70
+ .map((line) => line.trim())
71
+ .filter(Boolean)
72
+ .map((line) => activeApprovalFromLine(line, resolvedOptions))
73
+ .filter((approval) => Boolean(approval));
74
+ }
@@ -0,0 +1,20 @@
1
+ import { parseArgs } from '../args.js';
2
+ import { booleanOption, jsonOption } from './options.js';
3
+ export function doctorOptionsFromArgs(argv) {
4
+ const { values } = parseArgs(argv);
5
+ const keys = Object.keys(values);
6
+ const allowedKeys = new Set(['fix', 'json', 'postgres']);
7
+ if (!keys.every((key) => allowedKeys.has(key))) {
8
+ throw new Error('Usage: wpmoo doctor');
9
+ }
10
+ const options = {
11
+ json: jsonOption(values),
12
+ };
13
+ if (Object.hasOwn(values, 'fix')) {
14
+ options.fix = booleanOption(values, 'fix', false);
15
+ }
16
+ if (Object.hasOwn(values, 'postgres')) {
17
+ options.postgres = booleanOption(values, 'postgres', false);
18
+ }
19
+ return options;
20
+ }
@@ -0,0 +1,36 @@
1
+ export function stringOption(values, key) {
2
+ const value = values[key];
3
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
4
+ }
5
+ export function optionalSourceTypeValue(values) {
6
+ const value = stringOption(values, 'sourceType');
7
+ if (value === undefined) {
8
+ return undefined;
9
+ }
10
+ if (value === 'private' || value === 'oca' || value === 'external') {
11
+ return value;
12
+ }
13
+ throw new Error(`Invalid value for --source-type: ${value}`);
14
+ }
15
+ export function sourceTypeValue(values) {
16
+ return optionalSourceTypeValue(values) ?? 'private';
17
+ }
18
+ export function booleanOption(values, key, fallback) {
19
+ const value = values[key];
20
+ if (value === undefined)
21
+ return fallback;
22
+ if (typeof value === 'boolean')
23
+ return value;
24
+ const normalized = value.toLowerCase().trim();
25
+ if (['true', '1', 'yes', 'y'].includes(normalized))
26
+ return true;
27
+ if (['false', '0', 'no', 'n'].includes(normalized))
28
+ return false;
29
+ throw new Error(`Invalid boolean value for --${key}: ${value}`);
30
+ }
31
+ export function jsonOption(values) {
32
+ return booleanOption(values, 'json', false);
33
+ }
34
+ export function printJson(value) {
35
+ console.log(JSON.stringify(value));
36
+ }
@@ -0,0 +1,11 @@
1
+ import { resolve } from 'node:path';
2
+ import { parseArgs } from '../args.js';
3
+ import { booleanOption, stringOption } from './options.js';
4
+ export function resetCommandOptionsFromArgs(argv) {
5
+ const { values } = parseArgs(argv);
6
+ return {
7
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
8
+ stage: booleanOption(values, 'stage', true),
9
+ dryRun: booleanOption(values, 'dryRun', false),
10
+ };
11
+ }
@@ -0,0 +1,112 @@
1
+ import { resolve } from 'node:path';
2
+ import { parseArgs } from '../args.js';
3
+ import { commandOdooVersion } from '../environment-version.js';
4
+ import { outroPrompt } from '../prompts/index.js';
5
+ import { addModuleRepo, removeModuleRepo } from '../repo-actions.js';
6
+ import { normalizeRepositoryUrl } from '../repo-url.js';
7
+ import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from '../source-actions.js';
8
+ import { renderBanner } from '../templates.js';
9
+ import { booleanOption, jsonOption, optionalSourceTypeValue, printJson, sourceTypeValue, stringOption, } from './options.js';
10
+ export function renderedSourceRepoPath(target, sourceType, repoPath) {
11
+ if (repoPath) {
12
+ return `${target}/odoo/custom/src/${sourceType}/${repoPath}`;
13
+ }
14
+ return `${target}/odoo/custom/src/${sourceType}`;
15
+ }
16
+ export async function addRepoOptionsFromArgs(argv) {
17
+ const { values } = parseArgs(argv);
18
+ const repoUrl = stringOption(values, 'repoUrl') ?? stringOption(values, 'sourceRepoUrl');
19
+ if (!repoUrl) {
20
+ return undefined;
21
+ }
22
+ const target = resolve(stringOption(values, 'target') ?? process.cwd());
23
+ return {
24
+ target,
25
+ repoUrl: normalizeRepositoryUrl(repoUrl),
26
+ repoPath: stringOption(values, 'repo') ?? stringOption(values, 'sourcePath'),
27
+ sourceType: sourceTypeValue(values),
28
+ odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
29
+ initEmptyRepos: booleanOption(values, 'initEmptyRepos', false),
30
+ stage: booleanOption(values, 'stage', true),
31
+ };
32
+ }
33
+ export function removeRepoOptionsFromArgs(argv) {
34
+ const { values } = parseArgs(argv);
35
+ const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
36
+ if (!repoPath) {
37
+ return undefined;
38
+ }
39
+ return {
40
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
41
+ repoPath,
42
+ sourceType: optionalSourceTypeValue(values),
43
+ stage: booleanOption(values, 'stage', true),
44
+ };
45
+ }
46
+ export function sourceUsage() {
47
+ return 'Usage: wpmoo source <list|sync|add|remove> [options]';
48
+ }
49
+ export function sourceSyncOptionsFromArgs(argv) {
50
+ const { values } = parseArgs(argv);
51
+ return {
52
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
53
+ stage: booleanOption(values, 'stage', true),
54
+ json: jsonOption(values),
55
+ };
56
+ }
57
+ export function sourceListOptionsFromArgs(argv) {
58
+ const { values } = parseArgs(argv);
59
+ return {
60
+ target: resolve(stringOption(values, 'target') ?? process.cwd()),
61
+ json: jsonOption(values),
62
+ };
63
+ }
64
+ export async function runSourceCommand(argv) {
65
+ const [subcommand, ...subcommandArgv] = argv;
66
+ if (!subcommand) {
67
+ throw new Error(sourceUsage());
68
+ }
69
+ if (subcommand === 'list') {
70
+ const options = sourceListOptionsFromArgs(subcommandArgv);
71
+ const sources = await listSources(options.target);
72
+ if (options.json) {
73
+ printJson(sourceListJson(sources));
74
+ return;
75
+ }
76
+ console.log(renderBanner());
77
+ console.log(renderSourceList(sources));
78
+ return;
79
+ }
80
+ if (subcommand === 'sync') {
81
+ const options = sourceSyncOptionsFromArgs(subcommandArgv);
82
+ const sources = await syncSources({ target: options.target, stage: options.stage });
83
+ if (options.json) {
84
+ printJson(sourceSyncJson(sources, options.target));
85
+ return;
86
+ }
87
+ console.log(renderBanner());
88
+ outroPrompt(`Synced source manifest in ${options.target}.`);
89
+ return;
90
+ }
91
+ if (subcommand === 'add') {
92
+ const options = await addRepoOptionsFromArgs(subcommandArgv);
93
+ if (!options) {
94
+ throw new Error('Usage: wpmoo source add --repo-url <url> [--source-type private|oca|external]');
95
+ }
96
+ console.log(renderBanner());
97
+ await addModuleRepo(options);
98
+ outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
99
+ return;
100
+ }
101
+ if (subcommand === 'remove') {
102
+ const options = removeRepoOptionsFromArgs(subcommandArgv);
103
+ if (!options) {
104
+ throw new Error('Usage: wpmoo source remove --repo <name> [--source-type private|oca|external]');
105
+ }
106
+ console.log(renderBanner());
107
+ await removeModuleRepo(options);
108
+ outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
109
+ return;
110
+ }
111
+ throw new Error(sourceUsage());
112
+ }
package/dist/cli.js CHANGED
@@ -11,10 +11,14 @@ import { selectModuleAction } from './cockpit/module-action-menu.js';
11
11
  import { selectModuleFromBrowser } from './cockpit/module-browser.js';
12
12
  import { selectCockpitTopLevelMenu } from './cockpit/menu.js';
13
13
  import { confirmCockpitCommandRisk } from './cockpit/safety.js';
14
+ import { doctorOptionsFromArgs } from './cli-routes/doctor.js';
15
+ import { booleanOption, jsonOption, optionalSourceTypeValue, printJson, stringOption } from './cli-routes/options.js';
16
+ import { resetCommandOptionsFromArgs } from './cli-routes/reset.js';
17
+ import { addRepoOptionsFromArgs, removeRepoOptionsFromArgs, renderedSourceRepoPath, runSourceCommand, } from './cli-routes/source.js';
14
18
  import { detectDevelopmentEnvironment } from './environment.js';
15
19
  import { commandOdooVersion } from './environment-version.js';
16
20
  import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
17
- import { listEnvironmentDatabases, normalizeDatabaseListResult } from './databases.js';
21
+ import { findDatabaseSnapshots, databaseSnapshotCatalogJson, listEnvironmentDatabases, normalizeDatabaseListResult, renderDatabaseSnapshotCatalog, } from './databases.js';
18
22
  import { isDailyActionCommand, runDailyAction, runDailyActionWithStyledOutput } from './daily-actions.js';
19
23
  import { getDoctorReport, runDoctor } from './doctor.js';
20
24
  import { getOriginUrl, realGit } from './git.js';
@@ -29,7 +33,7 @@ import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-
29
33
  import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
30
34
  import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
31
35
  import { getServiceRuntimeStatus, renderServiceRuntimeStatusLine, } from './service-runtime-status.js';
32
- import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
36
+ import { listSources, } from './source-actions.js';
33
37
  import { backupTargetPath, expectedTargetConfirmation, inspectEnvironmentTarget, renderExistingEnvironmentSummary, renderForeignEnvironmentTargetWarning, } from './environment-target-preflight.js';
34
38
  import { getGitHubPrerequisiteStatus, renderGitHubPrerequisiteGuidance, } from './github-prerequisites.js';
35
39
  import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, } from './repository-preflight.js';
@@ -88,42 +92,6 @@ async function selectDefaultGitHubOwner(cancelAction = 'exit', preferredOwner) {
88
92
  return preferredOwner;
89
93
  }
90
94
  }
91
- function stringOption(values, key) {
92
- const value = values[key];
93
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
94
- }
95
- function optionalSourceTypeValue(values) {
96
- const value = stringOption(values, 'sourceType');
97
- if (value === undefined) {
98
- return undefined;
99
- }
100
- if (value === 'private' || value === 'oca' || value === 'external') {
101
- return value;
102
- }
103
- throw new Error(`Invalid value for --source-type: ${value}`);
104
- }
105
- function sourceTypeValue(values) {
106
- return optionalSourceTypeValue(values) ?? 'private';
107
- }
108
- function booleanOption(values, key, fallback) {
109
- const value = values[key];
110
- if (value === undefined)
111
- return fallback;
112
- if (typeof value === 'boolean')
113
- return value;
114
- const normalized = value.toLowerCase().trim();
115
- if (['true', '1', 'yes', 'y'].includes(normalized))
116
- return true;
117
- if (['false', '0', 'no', 'n'].includes(normalized))
118
- return false;
119
- throw new Error(`Invalid boolean value for --${key}: ${value}`);
120
- }
121
- function jsonOption(values) {
122
- return booleanOption(values, 'json', false);
123
- }
124
- function printJson(value) {
125
- console.log(JSON.stringify(value));
126
- }
127
95
  function supportsAnsi() {
128
96
  return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
129
97
  }
@@ -149,12 +117,6 @@ function shellQuote(value) {
149
117
  return value;
150
118
  return `'${value.replaceAll("'", "'\\''")}'`;
151
119
  }
152
- function renderedSourceRepoPath(target, sourceType, repoPath) {
153
- if (repoPath) {
154
- return `${target}/odoo/custom/src/${sourceType}/${repoPath}`;
155
- }
156
- return `${target}/odoo/custom/src/${sourceType}`;
157
- }
158
120
  function renderPostCreateGuidance(target, cwd) {
159
121
  const relativeTarget = relative(cwd, target) || '.';
160
122
  const cdCommand = `cd ${shellQuote(relativeTarget)}`;
@@ -270,7 +232,7 @@ async function showStartup(argv, skipUpdateCheck, details) {
270
232
  }
271
233
  console.log();
272
234
  }
273
- async function selectCockpitCommandFromMenu(serviceStatus, moduleCount, sourceRepoCount) {
235
+ async function selectCockpitCommandFromMenu(serviceStatus, moduleCount, sourceRepoCount, snapshotCount) {
274
236
  const legacyServiceStatus = serviceStatus.kind === 'services-running' ||
275
237
  serviceStatus.kind === 'db-ready' ||
276
238
  serviceStatus.kind === 'odoo-not-ready' ||
@@ -281,6 +243,7 @@ async function selectCockpitCommandFromMenu(serviceStatus, moduleCount, sourceRe
281
243
  serviceStatus: legacyServiceStatus,
282
244
  moduleCount,
283
245
  sourceRepoCount,
246
+ snapshotCount,
284
247
  });
285
248
  if (selection.kind === 'exit') {
286
249
  return 'exit';
@@ -520,23 +483,6 @@ async function optionsFromPrompts(showIntro = true, cancelAction = 'exit') {
520
483
  },
521
484
  };
522
485
  }
523
- async function addRepoOptionsFromArgs(argv) {
524
- const { values } = parseArgs(argv);
525
- const repoUrl = stringOption(values, 'repoUrl') ?? stringOption(values, 'sourceRepoUrl');
526
- if (!repoUrl) {
527
- return undefined;
528
- }
529
- const target = resolve(stringOption(values, 'target') ?? process.cwd());
530
- return {
531
- target,
532
- repoUrl: normalizeRepositoryUrl(repoUrl),
533
- repoPath: stringOption(values, 'repo') ?? stringOption(values, 'sourcePath'),
534
- sourceType: sourceTypeValue(values),
535
- odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
536
- initEmptyRepos: booleanOption(values, 'initEmptyRepos', false),
537
- stage: booleanOption(values, 'stage', true),
538
- };
539
- }
540
486
  async function addRepoOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
541
487
  showSubmenuIntro('Add source repo as submodule', showIntro, cancelAction);
542
488
  const target = process.cwd();
@@ -680,112 +626,6 @@ async function addModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exi
680
626
  stage: true,
681
627
  };
682
628
  }
683
- function removeRepoOptionsFromArgs(argv) {
684
- const { values } = parseArgs(argv);
685
- const repoPath = stringOption(values, 'repo') ?? stringOption(values, 'sourcePath');
686
- if (!repoPath) {
687
- return undefined;
688
- }
689
- return {
690
- target: resolve(stringOption(values, 'target') ?? process.cwd()),
691
- repoPath,
692
- sourceType: optionalSourceTypeValue(values),
693
- stage: booleanOption(values, 'stage', true),
694
- };
695
- }
696
- function resetCommandOptionsFromArgs(argv) {
697
- const { values } = parseArgs(argv);
698
- return {
699
- target: resolve(stringOption(values, 'target') ?? process.cwd()),
700
- stage: booleanOption(values, 'stage', true),
701
- dryRun: booleanOption(values, 'dryRun', false),
702
- };
703
- }
704
- function doctorOptionsFromArgs(argv) {
705
- const { values } = parseArgs(argv);
706
- const keys = Object.keys(values);
707
- const allowedKeys = new Set(['fix', 'json', 'postgres']);
708
- if (!keys.every((key) => allowedKeys.has(key))) {
709
- throw new Error('Usage: wpmoo doctor');
710
- }
711
- const options = {
712
- json: jsonOption(values),
713
- };
714
- if (Object.hasOwn(values, 'fix')) {
715
- options.fix = booleanOption(values, 'fix', false);
716
- }
717
- if (Object.hasOwn(values, 'postgres')) {
718
- options.postgres = booleanOption(values, 'postgres', false);
719
- }
720
- return options;
721
- }
722
- function sourceUsage() {
723
- return 'Usage: wpmoo source <list|sync|add|remove> [options]';
724
- }
725
- function sourceSyncOptionsFromArgs(argv) {
726
- const { values } = parseArgs(argv);
727
- return {
728
- target: resolve(stringOption(values, 'target') ?? process.cwd()),
729
- stage: booleanOption(values, 'stage', true),
730
- json: jsonOption(values),
731
- };
732
- }
733
- function sourceListOptionsFromArgs(argv) {
734
- const { values } = parseArgs(argv);
735
- return {
736
- target: resolve(stringOption(values, 'target') ?? process.cwd()),
737
- json: jsonOption(values),
738
- };
739
- }
740
- async function runSourceCommand(argv) {
741
- const [subcommand, ...subcommandArgv] = argv;
742
- if (!subcommand) {
743
- throw new Error(sourceUsage());
744
- }
745
- if (subcommand === 'list') {
746
- const options = sourceListOptionsFromArgs(subcommandArgv);
747
- const sources = await listSources(options.target);
748
- if (options.json) {
749
- printJson(sourceListJson(sources));
750
- return;
751
- }
752
- console.log(renderBanner());
753
- console.log(renderSourceList(sources));
754
- return;
755
- }
756
- if (subcommand === 'sync') {
757
- const options = sourceSyncOptionsFromArgs(subcommandArgv);
758
- const sources = await syncSources({ target: options.target, stage: options.stage });
759
- if (options.json) {
760
- printJson(sourceSyncJson(sources, options.target));
761
- return;
762
- }
763
- console.log(renderBanner());
764
- outroPrompt(`Synced source manifest in ${options.target}.`);
765
- return;
766
- }
767
- if (subcommand === 'add') {
768
- const options = await addRepoOptionsFromArgs(subcommandArgv);
769
- if (!options) {
770
- throw new Error('Usage: wpmoo source add --repo-url <url> [--source-type private|oca|external]');
771
- }
772
- console.log(renderBanner());
773
- await addModuleRepo(options);
774
- outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
775
- return;
776
- }
777
- if (subcommand === 'remove') {
778
- const options = removeRepoOptionsFromArgs(subcommandArgv);
779
- if (!options) {
780
- throw new Error('Usage: wpmoo source remove --repo <name> [--source-type private|oca|external]');
781
- }
782
- console.log(renderBanner());
783
- await removeModuleRepo(options);
784
- outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
785
- return;
786
- }
787
- throw new Error(sourceUsage());
788
- }
789
629
  async function confirmSafeResetFromMenu(options) {
790
630
  notePrompt(renderSafeResetPreview(options.target, options.stage), 'Safe reset preview');
791
631
  const confirmed = await confirmPrompt({
@@ -1493,7 +1333,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1493
1333
  };
1494
1334
  while (true) {
1495
1335
  try {
1496
- const command = await selectCockpitCommandFromMenu(serviceStatus, status.kind === 'environment' ? status.moduleCandidateCount : undefined, status.kind === 'environment' ? status.sourceRepoCount : undefined);
1336
+ const command = await selectCockpitCommandFromMenu(serviceStatus, status.kind === 'environment' ? status.moduleCandidateCount : undefined, status.kind === 'environment' ? status.sourceRepoCount : undefined, status.kind === 'environment' ? findDatabaseSnapshots(cwd).snapshots.length : undefined);
1497
1337
  if (command === 'exit') {
1498
1338
  return;
1499
1339
  }
@@ -1634,6 +1474,20 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1634
1474
  console.log(await renderEnvironmentStatusForTarget(cwd));
1635
1475
  return;
1636
1476
  }
1477
+ if (route.command === 'snapshot' && route.argv[0] === '--list') {
1478
+ const { values } = parseArgs(route.argv);
1479
+ const keys = Object.keys(values);
1480
+ if (!keys.every((key) => key === 'list' || key === 'json')) {
1481
+ throw new Error('Usage: wpmoo snapshot [--list] [db] [snapshot-name]');
1482
+ }
1483
+ if (jsonOption(values)) {
1484
+ printJson(databaseSnapshotCatalogJson(cwd));
1485
+ return;
1486
+ }
1487
+ console.log(renderBanner());
1488
+ console.log(renderDatabaseSnapshotCatalog(cwd));
1489
+ return;
1490
+ }
1637
1491
  if (isDailyActionCommand(route.command)) {
1638
1492
  console.log(renderBanner());
1639
1493
  await runDailyAction(route.command, await resolveDailyActionModuleTargets(route.command, route.argv, cwd), cwd);
@@ -38,11 +38,17 @@ export const cockpitCommands = [
38
38
  'modules list',
39
39
  'browse modules',
40
40
  '/module',
41
+ '/modules',
42
+ '/mods',
43
+ 'module',
44
+ ]),
45
+ dailyCommand('install', 'modules', 'Install module', 'Install modules in the database.', [
46
+ 'install module',
47
+ '/install-module',
41
48
  'module',
42
49
  ]),
43
- dailyCommand('install', 'modules', 'Install module', 'Install modules in the database.', ['install module', 'module']),
44
50
  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']),
51
+ dailyCommand('test', 'modules', 'Run tests', 'Run tests for selected modules.', ['/tests', 'tests', 'pytest', 'module']),
46
52
  dailyCommand('lint', 'modules', 'Run environment lint', 'Run environment lint checks.', ['check', 'quality']),
47
53
  dailyCommand('pot', 'modules', 'Generate POT', 'Generate module translation templates.', ['translation', 'i18n']),
48
54
  dailyCommand('psql', 'database', 'Open psql', 'Open PostgreSQL prompt.', ['postgres', 'sql', '/db']),
@@ -60,7 +66,7 @@ export const cockpitCommands = [
60
66
  ]),
61
67
  internalCommand('remove-repo', 'repositories', 'Remove source repo', 'Remove a source repository.', ['repository remove', 'source remove']),
62
68
  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']),
69
+ internalCommand('remove-module', 'modules', 'Remove module', 'Remove a module from a source repository.', ['module remove', '/remove-module', '/rm-module']),
64
70
  internalCommand('safe-reset', 'maintenance', 'Safe reset environment', 'Refresh generated files only.', ['reset', 'refresh', '/safe']),
65
71
  internalCommand('exit', 'maintenance', 'Exit', 'Close the command palette.', ['quit', 'back']),
66
72
  ];