@wpmoo/toolkit 0.9.29 → 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 +25 -2
- 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 +6 -4
- 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 +34 -10
- package/docs/command-reference.md +19 -1
- 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,12 +141,16 @@ 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
|
|
|
@@ -154,6 +158,7 @@ In `WPMOO_ENV=stage`, `install` and `update` require `WPMOO_ALLOW_STAGE_LIFECYCL
|
|
|
154
158
|
In `WPMOO_ENV=prod`, `install`, `update`, and `test` 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,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
|
];
|