@wpmoo/toolkit 0.9.29 → 0.9.31
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 +27 -4
- 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 +123 -0
- package/dist/cli.js +31 -173
- package/dist/cockpit/command-registry.js +9 -3
- package/dist/cockpit/daily-prompts.js +33 -6
- package/dist/cockpit/menu.js +13 -8
- 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 +188 -20
- package/dist/environment-policy.js +6 -6
- package/dist/external-templates.js +7 -1
- package/dist/github.js +11 -2
- package/dist/help.js +8 -6
- package/dist/module-actions.js +61 -16
- package/dist/module-manifest.js +6 -0
- package/dist/module-quality.js +154 -0
- package/dist/postgres-diagnostics.js +48 -10
- package/dist/repo-url.js +4 -7
- package/dist/safe-reset.js +21 -12
- package/dist/source-actions.js +90 -1
- package/dist/source-manifest.js +2 -2
- package/dist/templates.js +257 -19
- package/docs/1-0-readiness.md +37 -12
- package/docs/command-reference.md +22 -3
- package/docs/generated-environment-verification.md +23 -2
- package/docs/handoff.md +14 -2
- package/docs/lifecycle-recipes.md +6 -1
- package/docs/troubleshooting.md +29 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ The cockpit is the daily workspace. It starts with environment status and then s
|
|
|
108
108
|
```text
|
|
109
109
|
WPMoo Cockpit
|
|
110
110
|
|-- Command palette /
|
|
111
|
-
| |-- search commands such as /test, /
|
|
111
|
+
| |-- search commands such as /test, /modules, /install-module, /doctor, /safe-reset
|
|
112
112
|
|-- Services
|
|
113
113
|
| |-- start
|
|
114
114
|
| |-- stop
|
|
@@ -141,19 +141,24 @@ WPMoo Cockpit
|
|
|
141
141
|
|
|
142
142
|
Every cockpit action maps to a direct command, so the same workflow can be used interactively or scripted:
|
|
143
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
|
+
|
|
144
147
|
```bash
|
|
145
148
|
./moo start
|
|
146
149
|
./moo logs odoo
|
|
147
150
|
./moo update sale
|
|
148
151
|
./moo test sale
|
|
149
152
|
./moo snapshot devel before-update
|
|
153
|
+
./moo snapshot --list
|
|
150
154
|
./moo restore-snapshot --dry-run before-update devel
|
|
151
155
|
```
|
|
152
156
|
|
|
153
|
-
In `WPMOO_ENV=stage`, `install` and `
|
|
154
|
-
In `WPMOO_ENV=prod`, `install`, `update`, and `
|
|
157
|
+
In `WPMOO_ENV=stage`, `install`, `update`, `stop`, and `restart` require `WPMOO_ALLOW_STAGE_LIFECYCLE=1`.
|
|
158
|
+
In `WPMOO_ENV=prod`, `install`, `update`, `test`, `stop`, and `restart` require `WPMOO_ALLOW_PROD_LIFECYCLE=1`.
|
|
155
159
|
`resetdb` and real `restore-snapshot` require `WPMOO_ALLOW_DESTRUCTIVE=1` in `stage` and `prod`.
|
|
156
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.
|
|
157
162
|
|
|
158
163
|
Module source actions also have direct commands. Default is `private`; pass `--source-type oca` or `--source-type external` for non-private source repositories:
|
|
159
164
|
|
|
@@ -176,6 +181,9 @@ npx @wpmoo/toolkit doctor --json --postgres
|
|
|
176
181
|
```
|
|
177
182
|
|
|
178
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.
|
|
179
187
|
`doctor --postgres` runs read-only PostgreSQL diagnostics as advisory checks only; it
|
|
180
188
|
does not perform automatic tuning.
|
|
181
189
|
Incomplete or malformed PostgreSQL metric rows are reported as unavailable diagnostics
|
|
@@ -191,7 +199,10 @@ Current advisory checks include:
|
|
|
191
199
|
- optional unused index advisory output when index usage data is available;
|
|
192
200
|
- WAL and capacity visibility including WAL activity and disk-level pressure context;
|
|
193
201
|
- slow-query and query-plan readiness checks for common `log_min_duration_statement`
|
|
194
|
-
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`.
|
|
195
206
|
|
|
196
207
|
`npx @wpmoo/toolkit doctor --postgres` and
|
|
197
208
|
`npx @wpmoo/toolkit doctor --json --postgres` use the same checks, and the
|
|
@@ -201,6 +212,8 @@ JSON variant exposes a versioned PostgreSQL diagnostics contract.
|
|
|
201
212
|
`doctor --json --postgres` keeps the JSON contract stable by versioning the
|
|
202
213
|
`postgres` payload; individual fields are optional so automation can safely handle
|
|
203
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.
|
|
204
217
|
|
|
205
218
|
JSON compatibility policy:
|
|
206
219
|
|
|
@@ -232,6 +245,16 @@ warning while keeping the scoped package release valid.
|
|
|
232
245
|
packages are valid.
|
|
233
246
|
- **Smoke expectation**: run `npm run smoke:published -- "$VERSION"` after the
|
|
234
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.
|
|
235
258
|
- **1.0 release smoke**: For `1.0.0`, generated-environment acceptance smoke is
|
|
236
259
|
required before the release is considered final.
|
|
237
260
|
|
|
@@ -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,123 @@
|
|
|
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, renderSourceSyncPlan, sourceListJson, sourceSyncPlan, sourceSyncPlanJson, 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
|
+
dryRun: booleanOption(values, 'dryRun', false),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function sourceListOptionsFromArgs(argv) {
|
|
59
|
+
const { values } = parseArgs(argv);
|
|
60
|
+
return {
|
|
61
|
+
target: resolve(stringOption(values, 'target') ?? process.cwd()),
|
|
62
|
+
json: jsonOption(values),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function runSourceCommand(argv) {
|
|
66
|
+
const [subcommand, ...subcommandArgv] = argv;
|
|
67
|
+
if (!subcommand) {
|
|
68
|
+
throw new Error(sourceUsage());
|
|
69
|
+
}
|
|
70
|
+
if (subcommand === 'list') {
|
|
71
|
+
const options = sourceListOptionsFromArgs(subcommandArgv);
|
|
72
|
+
const sources = await listSources(options.target);
|
|
73
|
+
if (options.json) {
|
|
74
|
+
printJson(sourceListJson(sources));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(renderBanner());
|
|
78
|
+
console.log(renderSourceList(sources));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (subcommand === 'sync') {
|
|
82
|
+
const options = sourceSyncOptionsFromArgs(subcommandArgv);
|
|
83
|
+
if (options.dryRun) {
|
|
84
|
+
const plan = await sourceSyncPlan(options.target);
|
|
85
|
+
if (options.json) {
|
|
86
|
+
printJson(sourceSyncPlanJson(plan));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(renderBanner());
|
|
90
|
+
console.log(renderSourceSyncPlan(plan));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const sources = await syncSources({ target: options.target, stage: options.stage });
|
|
94
|
+
if (options.json) {
|
|
95
|
+
printJson(sourceSyncJson(sources, options.target));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.log(renderBanner());
|
|
99
|
+
outroPrompt(`Synced source manifest in ${options.target}.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (subcommand === 'add') {
|
|
103
|
+
const options = await addRepoOptionsFromArgs(subcommandArgv);
|
|
104
|
+
if (!options) {
|
|
105
|
+
throw new Error('Usage: wpmoo source add --repo-url <url> [--source-type private|oca|external]');
|
|
106
|
+
}
|
|
107
|
+
console.log(renderBanner());
|
|
108
|
+
await addModuleRepo(options);
|
|
109
|
+
outroPrompt(`Added source repo under ${renderedSourceRepoPath(options.target, options.sourceType ?? 'private', options.repoPath)}.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (subcommand === 'remove') {
|
|
113
|
+
const options = removeRepoOptionsFromArgs(subcommandArgv);
|
|
114
|
+
if (!options) {
|
|
115
|
+
throw new Error('Usage: wpmoo source remove --repo <name> [--source-type private|oca|external]');
|
|
116
|
+
}
|
|
117
|
+
console.log(renderBanner());
|
|
118
|
+
await removeModuleRepo(options);
|
|
119
|
+
outroPrompt(`Removed source repo ${options.repoPath} from ${options.target}.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
throw new Error(sourceUsage());
|
|
123
|
+
}
|
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({
|
|
@@ -836,6 +676,7 @@ function removeModuleOptionsFromArgs(argv) {
|
|
|
836
676
|
moduleName,
|
|
837
677
|
sourceType: optionalSourceTypeValue(values),
|
|
838
678
|
deleteFiles: booleanOption(values, 'deleteFiles', false),
|
|
679
|
+
dryRun: booleanOption(values, 'dryRun', false),
|
|
839
680
|
stage: booleanOption(values, 'stage', true),
|
|
840
681
|
};
|
|
841
682
|
}
|
|
@@ -1493,7 +1334,7 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1493
1334
|
};
|
|
1494
1335
|
while (true) {
|
|
1495
1336
|
try {
|
|
1496
|
-
const command = await selectCockpitCommandFromMenu(serviceStatus, status.kind === 'environment' ? status.moduleCandidateCount : undefined, status.kind === 'environment' ? status.sourceRepoCount : undefined);
|
|
1337
|
+
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
1338
|
if (command === 'exit') {
|
|
1498
1339
|
return;
|
|
1499
1340
|
}
|
|
@@ -1579,14 +1420,14 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1579
1420
|
const options = removeModuleOptionsFromArgs(route.argv);
|
|
1580
1421
|
if (options) {
|
|
1581
1422
|
console.log(renderBanner());
|
|
1582
|
-
await removeModuleFromSourceRepo(options);
|
|
1583
|
-
outroPrompt(`Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
|
|
1423
|
+
const report = await removeModuleFromSourceRepo(options);
|
|
1424
|
+
outroPrompt(report.dryRun ? report.summary : `Removed module ${options.moduleName} from source repo ${options.repoPath}.`);
|
|
1584
1425
|
return;
|
|
1585
1426
|
}
|
|
1586
1427
|
await showStartup(argv, skipUpdateCheck);
|
|
1587
1428
|
const promptedOptions = await removeModuleOptionsFromPrompts();
|
|
1588
|
-
await removeModuleFromSourceRepo(promptedOptions);
|
|
1589
|
-
outroPrompt(`Removed module ${promptedOptions.moduleName} from source repo ${promptedOptions.repoPath}.`);
|
|
1429
|
+
const report = await removeModuleFromSourceRepo(promptedOptions);
|
|
1430
|
+
outroPrompt(report.dryRun ? report.summary : `Removed module ${promptedOptions.moduleName} from source repo ${promptedOptions.repoPath}.`);
|
|
1590
1431
|
return;
|
|
1591
1432
|
}
|
|
1592
1433
|
if (route.command === 'reset') {
|
|
@@ -1611,6 +1452,9 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1611
1452
|
doctorOptions.postgres = options.postgres;
|
|
1612
1453
|
}
|
|
1613
1454
|
if (options.json) {
|
|
1455
|
+
if (doctorOptions.fix) {
|
|
1456
|
+
throw new Error('doctor --json --fix is not supported; run doctor --fix for human-readable auto-fix output, then doctor --json to inspect the post-fix state.');
|
|
1457
|
+
}
|
|
1614
1458
|
printJson(await getDoctorReport(cwd, doctorOptions));
|
|
1615
1459
|
return;
|
|
1616
1460
|
}
|
|
@@ -1634,6 +1478,20 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1634
1478
|
console.log(await renderEnvironmentStatusForTarget(cwd));
|
|
1635
1479
|
return;
|
|
1636
1480
|
}
|
|
1481
|
+
if (route.command === 'snapshot' && route.argv[0] === '--list') {
|
|
1482
|
+
const { values } = parseArgs(route.argv);
|
|
1483
|
+
const keys = Object.keys(values);
|
|
1484
|
+
if (!keys.every((key) => key === 'list' || key === 'json')) {
|
|
1485
|
+
throw new Error('Usage: wpmoo snapshot [--list] [db] [snapshot-name]');
|
|
1486
|
+
}
|
|
1487
|
+
if (jsonOption(values)) {
|
|
1488
|
+
printJson(databaseSnapshotCatalogJson(cwd));
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
console.log(renderBanner());
|
|
1492
|
+
console.log(renderDatabaseSnapshotCatalog(cwd));
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1637
1495
|
if (isDailyActionCommand(route.command)) {
|
|
1638
1496
|
console.log(renderBanner());
|
|
1639
1497
|
await runDailyAction(route.command, await resolveDailyActionModuleTargets(route.command, route.argv, cwd), cwd);
|