@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 +41 -3
- package/dist/approval-ledger.js +74 -0
- package/dist/cli-routes/doctor.js +20 -0
- package/dist/cli-routes/options.js +36 -0
- package/dist/cli-routes/reset.js +11 -0
- package/dist/cli-routes/source.js +112 -0
- package/dist/cli.js +23 -169
- package/dist/cockpit/command-registry.js +9 -3
- package/dist/cockpit/daily-prompts.js +33 -6
- package/dist/cockpit/menu.js +12 -7
- package/dist/cockpit/module-browser.js +79 -2
- package/dist/daily-actions.js +55 -12
- package/dist/databases.js +223 -36
- package/dist/doctor.js +129 -15
- package/dist/github.js +11 -2
- package/dist/help.js +14 -6
- package/dist/module-actions.js +23 -2
- package/dist/module-manifest.js +6 -0
- package/dist/module-quality.js +98 -0
- package/dist/postgres-diagnostics.js +27 -0
- package/dist/repo-url.js +4 -7
- package/dist/safe-reset.js +21 -12
- package/dist/source-manifest.js +2 -2
- package/dist/templates.js +149 -17
- package/docs/1-0-readiness.md +64 -46
- package/docs/command-reference.md +28 -3
- package/docs/generated-environment-verification.md +23 -2
- package/docs/handoff.md +21 -1
- package/docs/lifecycle-recipes.md +6 -1
- package/docs/troubleshooting.md +29 -1
- package/package.json +1 -1
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
|
|
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, /
|
|
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,
|
|
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
|
];
|