@wpmoo/odoo 0.8.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/LICENSE +22 -0
- package/README.md +231 -0
- package/dist/addons-yaml.js +59 -0
- package/dist/args.js +247 -0
- package/dist/cli.js +703 -0
- package/dist/environment-context.js +10 -0
- package/dist/environment-version.js +5 -0
- package/dist/environment.js +61 -0
- package/dist/external-assets.js +113 -0
- package/dist/external-templates.js +69 -0
- package/dist/git.js +98 -0
- package/dist/github.js +87 -0
- package/dist/help.js +62 -0
- package/dist/menu-navigation.js +67 -0
- package/dist/module-actions.js +107 -0
- package/dist/odoo-versions.js +1 -0
- package/dist/path-validation.js +50 -0
- package/dist/prompt-copy.js +8 -0
- package/dist/prompt-repositories.js +34 -0
- package/dist/repo-actions.js +106 -0
- package/dist/repo-url.js +27 -0
- package/dist/repository-preflight.js +46 -0
- package/dist/safe-reset.js +129 -0
- package/dist/scaffold.js +125 -0
- package/dist/templates.js +414 -0
- package/dist/types.js +1 -0
- package/dist/update-check.js +106 -0
- package/dist/version.js +19 -0
- package/docs/assets/wpmoo-banner.png +0 -0
- package/package.json +41 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
export function defaultCommunityAddons(product) {
|
|
2
|
+
return [product];
|
|
3
|
+
}
|
|
4
|
+
export function defaultProAddons(product) {
|
|
5
|
+
return [`${product}_pro`];
|
|
6
|
+
}
|
|
7
|
+
function titleizeProduct(product) {
|
|
8
|
+
return product
|
|
9
|
+
.split(/[_-]+/)
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
12
|
+
.join(' ');
|
|
13
|
+
}
|
|
14
|
+
function yamlList(items) {
|
|
15
|
+
return items.map((item) => ` - ${item}`).join('\n');
|
|
16
|
+
}
|
|
17
|
+
function allAddons(options) {
|
|
18
|
+
return options.sourceRepos.flatMap((repo) => repo.addons);
|
|
19
|
+
}
|
|
20
|
+
function repositoryLayout(options) {
|
|
21
|
+
return `${options.devRepo}/
|
|
22
|
+
├── docker-compose_17.0.yml
|
|
23
|
+
├── docker-compose_18.0.yml
|
|
24
|
+
├── docker-compose_19.0.yml
|
|
25
|
+
├── scripts/
|
|
26
|
+
├── etc/
|
|
27
|
+
├── odoo/
|
|
28
|
+
│ └── custom/
|
|
29
|
+
│ └── src/
|
|
30
|
+
│ └── private/
|
|
31
|
+
${options.sourceRepos.map((repo) => `│ ├── ${repo.path}/`).join('\n')}
|
|
32
|
+
├── docs/
|
|
33
|
+
├── README.md
|
|
34
|
+
└── AGENTS.md`;
|
|
35
|
+
}
|
|
36
|
+
function sourceRepoDocs(options) {
|
|
37
|
+
return options.sourceRepos
|
|
38
|
+
.map((repo) => `### ${repo.path}
|
|
39
|
+
|
|
40
|
+
URL:
|
|
41
|
+
|
|
42
|
+
\`\`\`text
|
|
43
|
+
${repo.url}
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
Submodule path:
|
|
47
|
+
|
|
48
|
+
\`\`\`text
|
|
49
|
+
odoo/custom/src/private/${repo.path}
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
Expected addon layout:
|
|
53
|
+
|
|
54
|
+
\`\`\`text
|
|
55
|
+
${repo.path}/
|
|
56
|
+
${repo.addons.map((addon) => `├── ${addon}/`).join('\n')}
|
|
57
|
+
\`\`\``)
|
|
58
|
+
.join('\n\n');
|
|
59
|
+
}
|
|
60
|
+
function optionalAgentSkillsReadme(options) {
|
|
61
|
+
if (!options.agentSkillsTemplateUrl)
|
|
62
|
+
return '';
|
|
63
|
+
return `
|
|
64
|
+
## Agent Skills
|
|
65
|
+
|
|
66
|
+
This environment is configured to install project-local Agent Skills from:
|
|
67
|
+
|
|
68
|
+
\`\`\`text
|
|
69
|
+
${options.agentSkillsTemplateUrl}${options.agentSkillsTemplateRef ? `#${options.agentSkillsTemplateRef}` : ''}
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
After external resource installation, skills normally live under:
|
|
73
|
+
|
|
74
|
+
\`\`\`text
|
|
75
|
+
.agents/skills/
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
Agents that support the Agent Skills standard can load them on demand.
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
function optionalAgentSkillsAgentsSection(options) {
|
|
82
|
+
if (!options.agentSkillsTemplateUrl)
|
|
83
|
+
return '';
|
|
84
|
+
return `
|
|
85
|
+
## Active Agent Skills
|
|
86
|
+
|
|
87
|
+
When using an agent that supports Agent Skills, prefer the project-local skills
|
|
88
|
+
installed under \`.agents/skills/\`. They are sourced from:
|
|
89
|
+
|
|
90
|
+
\`\`\`text
|
|
91
|
+
${options.agentSkillsTemplateUrl}${options.agentSkillsTemplateRef ? `#${options.agentSkillsTemplateRef}` : ''}
|
|
92
|
+
\`\`\`
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
function environmentKind() {
|
|
96
|
+
return 'Docker Compose';
|
|
97
|
+
}
|
|
98
|
+
function repoDuplicationNote() {
|
|
99
|
+
return 'Keep these repositories under `odoo/custom/src/private`; the Compose entrypoint exposes discovered addons through `/mnt/wpmoo-addons`.';
|
|
100
|
+
}
|
|
101
|
+
function verificationCommand(options) {
|
|
102
|
+
const firstAddon = allAddons(options)[0] ?? options.product;
|
|
103
|
+
return `./scripts/test.sh ${firstAddon}`;
|
|
104
|
+
}
|
|
105
|
+
function environmentUsageDocs(options) {
|
|
106
|
+
return `## Docker Compose Notes
|
|
107
|
+
|
|
108
|
+
This environment uses the standalone WPMoo Odoo Compose resource. Compose files
|
|
109
|
+
are version-specific and static:
|
|
110
|
+
|
|
111
|
+
\`\`\`text
|
|
112
|
+
docker-compose_17.0.yml
|
|
113
|
+
docker-compose_18.0.yml
|
|
114
|
+
docker-compose_19.0.yml
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
If copied from the standalone resource, additional compose documentation is kept
|
|
118
|
+
in \`docs/compose.md\`.
|
|
119
|
+
|
|
120
|
+
Source repositories stay under \`odoo/custom/src/private\`. At container startup,
|
|
121
|
+
\`entrypoint.sh\` scans those repositories for addons and exposes them through
|
|
122
|
+
\`/mnt/wpmoo-addons\`.
|
|
123
|
+
|
|
124
|
+
## Common Commands
|
|
125
|
+
|
|
126
|
+
\`\`\`bash
|
|
127
|
+
cp .env.example .env
|
|
128
|
+
./scripts/up.sh
|
|
129
|
+
./scripts/logs.sh
|
|
130
|
+
./scripts/shell.sh
|
|
131
|
+
./scripts/down.sh
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
Run tests for one planned product addon:
|
|
135
|
+
|
|
136
|
+
\`\`\`bash
|
|
137
|
+
./scripts/test.sh ${allAddons(options)[0] ?? options.product}
|
|
138
|
+
\`\`\`
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
const BANNER_GRADIENT_START = [31, 151, 231];
|
|
142
|
+
const BANNER_GRADIENT_END = [209, 95, 127];
|
|
143
|
+
const ANSI_BOLD = '\u001B[1m';
|
|
144
|
+
const ANSI_RESET = '\u001B[0m';
|
|
145
|
+
function gradientColor(column, width) {
|
|
146
|
+
const ratio = width <= 1 ? 0 : column / (width - 1);
|
|
147
|
+
const [startR, startG, startB] = BANNER_GRADIENT_START;
|
|
148
|
+
const [endR, endG, endB] = BANNER_GRADIENT_END;
|
|
149
|
+
const r = Math.round(startR + (endR - startR) * ratio);
|
|
150
|
+
const g = Math.round(startG + (endG - startG) * ratio);
|
|
151
|
+
const b = Math.round(startB + (endB - startB) * ratio);
|
|
152
|
+
return `\u001B[38;2;${r};${g};${b}m`;
|
|
153
|
+
}
|
|
154
|
+
function applyBannerGradient(banner) {
|
|
155
|
+
const lines = banner.split('\n');
|
|
156
|
+
const width = Math.max(...lines.map((line) => line.length));
|
|
157
|
+
return lines
|
|
158
|
+
.map((line) => Array.from(line)
|
|
159
|
+
.map((character, column) => `${gradientColor(column, width)}${character}`)
|
|
160
|
+
.join(''))
|
|
161
|
+
.join('\n');
|
|
162
|
+
}
|
|
163
|
+
export function renderBanner() {
|
|
164
|
+
const banner = String.raw `
|
|
165
|
+
|
|
166
|
+
░██ ░██ ░█████████ ░███ ░███
|
|
167
|
+
░██ ░██ ░██ ░██ ░████ ░████
|
|
168
|
+
░██ ░██ ░██ ░██ ░██ ░██░██ ░██░██ ░███████ ░███████
|
|
169
|
+
░██ ░████ ░██ ░█████████ ░██ ░████ ░██ ░██ ░██ ░██ ░██
|
|
170
|
+
░██░██ ░██░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
|
|
171
|
+
░████ ░████ ░██ ░██ ░██ ░██ ░██ ░██ ░██
|
|
172
|
+
░███ ░███ ░██ ░██ ░██ ░███████ ░███████
|
|
173
|
+
|
|
174
|
+
░░░░░░░░░ Workflow Platform - Micro Object Oriented ░░░░░░░░░
|
|
175
|
+
`;
|
|
176
|
+
return `${ANSI_BOLD}${applyBannerGradient(banner)}${ANSI_RESET}`;
|
|
177
|
+
}
|
|
178
|
+
export function renderGitignore() {
|
|
179
|
+
return `# macOS/editor noise
|
|
180
|
+
.DS_Store
|
|
181
|
+
.idea/
|
|
182
|
+
.vscode/*.log
|
|
183
|
+
|
|
184
|
+
# Node/local package files
|
|
185
|
+
node_modules/
|
|
186
|
+
dist/
|
|
187
|
+
coverage/
|
|
188
|
+
*.log
|
|
189
|
+
|
|
190
|
+
# Python/cache files
|
|
191
|
+
__pycache__/
|
|
192
|
+
*.py[cod]
|
|
193
|
+
.pytest_cache/
|
|
194
|
+
.mypy_cache/
|
|
195
|
+
.ruff_cache/
|
|
196
|
+
|
|
197
|
+
# Local environment files
|
|
198
|
+
.env
|
|
199
|
+
.env.*
|
|
200
|
+
!.env.example
|
|
201
|
+
*.local
|
|
202
|
+
|
|
203
|
+
# Local generated files
|
|
204
|
+
*.code-workspace
|
|
205
|
+
auto/
|
|
206
|
+
data/
|
|
207
|
+
filestore/
|
|
208
|
+
sessions/
|
|
209
|
+
odoo/custom/auto/
|
|
210
|
+
odoo/custom/src/*/.git-aggregate-cache/
|
|
211
|
+
|
|
212
|
+
# Backups and archives
|
|
213
|
+
*.bak
|
|
214
|
+
*.backup
|
|
215
|
+
*.dump
|
|
216
|
+
*.sql
|
|
217
|
+
*.zip
|
|
218
|
+
*.tar
|
|
219
|
+
*.tar.gz
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
export function renderMooDelegationScript() {
|
|
223
|
+
return `#!/usr/bin/env bash
|
|
224
|
+
set -euo pipefail
|
|
225
|
+
|
|
226
|
+
script_dir="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")" && pwd)"
|
|
227
|
+
cd "$script_dir"
|
|
228
|
+
|
|
229
|
+
exec npx --yes @wpmoo/odoo@latest "$@"
|
|
230
|
+
`;
|
|
231
|
+
}
|
|
232
|
+
export function renderAddonsYaml(options) {
|
|
233
|
+
return `# Addons activated from source submodules.
|
|
234
|
+
#
|
|
235
|
+
# Source repos are managed as Git submodules under odoo/custom/src/private.
|
|
236
|
+
# Do not duplicate these same repos in repos.yaml.
|
|
237
|
+
|
|
238
|
+
${options.sourceRepos.map((repo) => `private/${repo.path}:\n${yamlList(repo.addons)}`).join('\n\n')}
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
export function renderReposYaml(options) {
|
|
242
|
+
return `# git-aggregator repositories.
|
|
243
|
+
#
|
|
244
|
+
# Project source repositories are intentionally not listed here because
|
|
245
|
+
# they are pinned as Git submodules:
|
|
246
|
+
#
|
|
247
|
+
${options.sourceRepos.map((repo) => `# - private/${repo.path}`).join('\n')}
|
|
248
|
+
#
|
|
249
|
+
# Keep this file for upstream/OCA repositories that should be aggregated.
|
|
250
|
+
|
|
251
|
+
odoo:
|
|
252
|
+
defaults:
|
|
253
|
+
depth: $DEPTH_MERGE
|
|
254
|
+
remotes:
|
|
255
|
+
origin: https://github.com/OCA/OCB.git
|
|
256
|
+
odoo: https://github.com/odoo/odoo.git
|
|
257
|
+
target: origin $ODOO_VERSION
|
|
258
|
+
merges:
|
|
259
|
+
- origin $ODOO_VERSION
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
export function renderReadme(options) {
|
|
263
|
+
const title = titleizeProduct(options.product);
|
|
264
|
+
return `# ${title} Development Environment
|
|
265
|
+
|
|
266
|
+
Private ${environmentKind()} development environment for the ${title} product.
|
|
267
|
+
|
|
268
|
+
This repository owns the development environment only. Product source code lives
|
|
269
|
+
in source repository submodules under \`odoo/custom/src/private\`.
|
|
270
|
+
|
|
271
|
+
## Repository Layout
|
|
272
|
+
|
|
273
|
+
\`\`\`text
|
|
274
|
+
${repositoryLayout(options)}
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
## Clone
|
|
278
|
+
|
|
279
|
+
Clone with submodules:
|
|
280
|
+
|
|
281
|
+
\`\`\`bash
|
|
282
|
+
git clone --recurse-submodules ${options.devRepoUrl}
|
|
283
|
+
cd ${options.devRepo}
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
If already cloned:
|
|
287
|
+
|
|
288
|
+
\`\`\`bash
|
|
289
|
+
git submodule update --init --recursive
|
|
290
|
+
\`\`\`
|
|
291
|
+
|
|
292
|
+
## WPMoo CLI Shortcut
|
|
293
|
+
|
|
294
|
+
This environment includes a local \`moo\` delegation script. From the repository
|
|
295
|
+
root:
|
|
296
|
+
|
|
297
|
+
\`\`\`bash
|
|
298
|
+
./moo
|
|
299
|
+
./moo add-module
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
If this repository root is on your \`PATH\`, you can run \`moo ...\` from
|
|
303
|
+
anywhere and the script will delegate back to this environment.
|
|
304
|
+
${optionalAgentSkillsReadme(options)}
|
|
305
|
+
## Source Repositories
|
|
306
|
+
|
|
307
|
+
${sourceRepoDocs(options)}
|
|
308
|
+
|
|
309
|
+
${environmentUsageDocs(options)}
|
|
310
|
+
## Branching
|
|
311
|
+
|
|
312
|
+
Use Odoo major-version branches in source repositories:
|
|
313
|
+
|
|
314
|
+
\`\`\`text
|
|
315
|
+
${options.odooVersion}
|
|
316
|
+
\`\`\`
|
|
317
|
+
|
|
318
|
+
This dev repository can stay on \`main\` and pin exact source commits through
|
|
319
|
+
submodule references.
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
export function renderAgents(options) {
|
|
323
|
+
const repoList = options.sourceRepos
|
|
324
|
+
.map((repo) => `- \`${repo.path}\`: \`${repo.url}\``)
|
|
325
|
+
.join('\n');
|
|
326
|
+
const addonList = options.sourceRepos
|
|
327
|
+
.map((repo) => `\`${repo.path}\` addons:\n${repo.addons.map((addon) => `- \`${addon}\``).join('\n')}`)
|
|
328
|
+
.join('\n\n');
|
|
329
|
+
return `# AGENTS.md
|
|
330
|
+
|
|
331
|
+
## Project
|
|
332
|
+
|
|
333
|
+
Private ${environmentKind()} development environment for the ${titleizeProduct(options.product)} product.
|
|
334
|
+
|
|
335
|
+
## Repository Roles
|
|
336
|
+
|
|
337
|
+
- \`${options.devRepo}\`: environment/config only, private.
|
|
338
|
+
${repoList}
|
|
339
|
+
|
|
340
|
+
## Source Layout
|
|
341
|
+
|
|
342
|
+
Product repositories are Git submodules:
|
|
343
|
+
|
|
344
|
+
\`\`\`text
|
|
345
|
+
${options.sourceRepos.map((repo) => `odoo/custom/src/private/${repo.path}`).join('\n')}
|
|
346
|
+
\`\`\`
|
|
347
|
+
|
|
348
|
+
${repoDuplicationNote()}
|
|
349
|
+
${optionalAgentSkillsAgentsSection(options)}
|
|
350
|
+
## Addon Boundaries
|
|
351
|
+
|
|
352
|
+
${addonList}
|
|
353
|
+
|
|
354
|
+
Public/community addons must not depend on private paid addons. Private paid
|
|
355
|
+
addons may depend on public/community addons.
|
|
356
|
+
|
|
357
|
+
## Odoo 19 Rules
|
|
358
|
+
|
|
359
|
+
- Use \`<list>\` instead of \`<tree>\`.
|
|
360
|
+
- Use direct view expressions such as \`invisible="..."\` instead of \`attrs\`.
|
|
361
|
+
- Use \`models.Constraint\` instead of \`_sql_constraints\`.
|
|
362
|
+
- Use \`@api.ondelete(at_uninstall=False)\` for delete validation.
|
|
363
|
+
- Avoid \`default_*\` field names in \`res.config.settings\`.
|
|
364
|
+
- Keep core/community installable without any pro modules.
|
|
365
|
+
|
|
366
|
+
## Verification
|
|
367
|
+
|
|
368
|
+
Use the environment's addon test/update command:
|
|
369
|
+
|
|
370
|
+
\`\`\`bash
|
|
371
|
+
${verificationCommand(options)}
|
|
372
|
+
\`\`\`
|
|
373
|
+
|
|
374
|
+
Only report completion after the relevant update/test command exits cleanly.
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
export function renderAppstoreRelease(options) {
|
|
378
|
+
return `# Odoo Apps Release Notes
|
|
379
|
+
|
|
380
|
+
Paid addons can live together in a private source repository during development.
|
|
381
|
+
Each paid addon still needs its own App Store metadata.
|
|
382
|
+
|
|
383
|
+
Per addon checklist:
|
|
384
|
+
|
|
385
|
+
- \`__manifest__.py\` has correct \`name\`, \`summary\`, \`version\`, \`depends\`,
|
|
386
|
+
\`license\`, \`price\`, \`currency\`, and \`support\`.
|
|
387
|
+
- \`license\` is appropriate for paid distribution, typically \`OPL-1\`.
|
|
388
|
+
- \`static/description/icon.png\` exists.
|
|
389
|
+
- \`static/description/index.html\` exists.
|
|
390
|
+
- Screenshots are stored under \`static/description/\`.
|
|
391
|
+
- Community dependency versions are compatible with the target Odoo major
|
|
392
|
+
version.
|
|
393
|
+
|
|
394
|
+
Recommended release flow:
|
|
395
|
+
|
|
396
|
+
1. Develop in the relevant private source repository.
|
|
397
|
+
2. Update the addon manifest version.
|
|
398
|
+
3. Run update/test commands in this dev environment.
|
|
399
|
+
4. Tag the source commit.
|
|
400
|
+
5. Prepare an App Store publish package or publish mirror per paid addon.
|
|
401
|
+
6. Trigger Odoo Apps repository scan/update.
|
|
402
|
+
|
|
403
|
+
If App Store scan coupling becomes a problem, create separate private publish
|
|
404
|
+
mirror repositories for each paid addon. Keep development in
|
|
405
|
+
\`odoo/custom/src/private\`; mirrors should be generated artifacts, not
|
|
406
|
+
hand-edited source repositories.
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
export function renderPlaceholder(title, body) {
|
|
410
|
+
return `# ${title}
|
|
411
|
+
|
|
412
|
+
${body}
|
|
413
|
+
`;
|
|
414
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
export const realNpm = {
|
|
4
|
+
async run(args) {
|
|
5
|
+
const result = await execa('npm', args, args[0] === 'view' ? { timeout: 5000 } : {});
|
|
6
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
function truthyEnv(value) {
|
|
10
|
+
return value !== undefined && ['1', 'true', 'yes', 'y'].includes(value.toLowerCase().trim());
|
|
11
|
+
}
|
|
12
|
+
export function isUpdateCheckSkipped(argv, env = process.env) {
|
|
13
|
+
if (argv.includes('--no-update-check'))
|
|
14
|
+
return true;
|
|
15
|
+
if (truthyEnv(env.WPMOO_SKIP_UPDATE_CHECK))
|
|
16
|
+
return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function numericParts(version) {
|
|
20
|
+
return version
|
|
21
|
+
.replace(/^v/, '')
|
|
22
|
+
.split('-', 1)[0]
|
|
23
|
+
.split('.')
|
|
24
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
25
|
+
}
|
|
26
|
+
export function compareVersions(currentVersion, latestVersion) {
|
|
27
|
+
const current = numericParts(currentVersion);
|
|
28
|
+
const latest = numericParts(latestVersion);
|
|
29
|
+
const length = Math.max(current.length, latest.length);
|
|
30
|
+
for (let index = 0; index < length; index += 1) {
|
|
31
|
+
const currentPart = current[index] ?? 0;
|
|
32
|
+
const latestPart = latest[index] ?? 0;
|
|
33
|
+
if (currentPart !== latestPart) {
|
|
34
|
+
return currentPart - latestPart;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
function parseNpmPackageInfo(stdout) {
|
|
40
|
+
const trimmed = stdout.trim();
|
|
41
|
+
if (!trimmed) {
|
|
42
|
+
throw new Error('npm did not return package metadata');
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(trimmed);
|
|
46
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
47
|
+
const record = parsed;
|
|
48
|
+
const version = typeof record.version === 'string' ? record.version.trim() : '';
|
|
49
|
+
const dist = record.dist;
|
|
50
|
+
const nestedTarball = typeof dist === 'object' && dist !== null && typeof dist.tarball === 'string'
|
|
51
|
+
? String(dist.tarball).trim()
|
|
52
|
+
: '';
|
|
53
|
+
const dottedTarball = typeof record['dist.tarball'] === 'string' ? record['dist.tarball'].trim() : '';
|
|
54
|
+
const tarball = nestedTarball || dottedTarball;
|
|
55
|
+
if (version && tarball) {
|
|
56
|
+
return { version, tarball };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Fall through to the validation error below.
|
|
62
|
+
}
|
|
63
|
+
throw new Error('npm did not return a package version and tarball');
|
|
64
|
+
}
|
|
65
|
+
async function viewPackageInfo(packageSpecValue, runner) {
|
|
66
|
+
const result = await runner.run(['view', packageSpecValue, 'version', 'dist.tarball', '--json']);
|
|
67
|
+
return parseNpmPackageInfo(result.stdout);
|
|
68
|
+
}
|
|
69
|
+
export async function checkForUpdate(packageName, currentVersion, runner = realNpm) {
|
|
70
|
+
try {
|
|
71
|
+
const latest = await viewPackageInfo(packageSpec(packageName, 'latest'), runner);
|
|
72
|
+
if (compareVersions(currentVersion, latest.version) < 0) {
|
|
73
|
+
const exact = await viewPackageInfo(packageSpec(packageName, latest.version), runner);
|
|
74
|
+
if (exact.version !== latest.version || !exact.tarball) {
|
|
75
|
+
throw new Error(`npm metadata for ${packageSpec(packageName, latest.version)} did not validate`);
|
|
76
|
+
}
|
|
77
|
+
return { status: 'update-available', currentVersion, latestVersion: exact.version, tarball: exact.tarball };
|
|
78
|
+
}
|
|
79
|
+
return { status: 'current', currentVersion, latestVersion: latest.version };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { status: 'unavailable', currentVersion };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function packageSpec(packageName, version) {
|
|
86
|
+
return `${packageName}@${version}`;
|
|
87
|
+
}
|
|
88
|
+
export async function installLatestPackage(packageName, version, runner = realNpm) {
|
|
89
|
+
await runner.run(['install', '-g', packageSpec(packageName, version)]);
|
|
90
|
+
}
|
|
91
|
+
export function restartArgs(packageName, version, argv) {
|
|
92
|
+
return ['exec', '--yes', '--package', packageSpec(packageName, version), '--', 'wpmoo', ...argv];
|
|
93
|
+
}
|
|
94
|
+
export function restartEnvironment(env = process.env) {
|
|
95
|
+
return { ...env, WPMOO_SKIP_UPDATE_CHECK: '1' };
|
|
96
|
+
}
|
|
97
|
+
export async function restartCli(packageName, version, argv) {
|
|
98
|
+
const child = spawn('npm', restartArgs(packageName, version, argv), {
|
|
99
|
+
env: restartEnvironment(),
|
|
100
|
+
stdio: 'inherit',
|
|
101
|
+
});
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
child.on('error', reject);
|
|
104
|
+
child.on('exit', (code) => resolve(code));
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
function readPackageJson() {
|
|
3
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
|
+
}
|
|
5
|
+
export function packageVersion() {
|
|
6
|
+
return readPackageJson().version;
|
|
7
|
+
}
|
|
8
|
+
export function renderVersion() {
|
|
9
|
+
const packageJson = readPackageJson();
|
|
10
|
+
return `${packageJson.name} ${packageJson.version}`;
|
|
11
|
+
}
|
|
12
|
+
export function packageName() {
|
|
13
|
+
return readPackageJson().name;
|
|
14
|
+
}
|
|
15
|
+
export function renderVersionTag(latestVersion) {
|
|
16
|
+
const current = packageVersion();
|
|
17
|
+
const updateSuffix = latestVersion ? ` -> v.${latestVersion} available` : '';
|
|
18
|
+
return `\u001B[33mv.${current}${updateSuffix}\u001B[0m`;
|
|
19
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wpmoo/odoo",
|
|
3
|
+
"version": "0.8.30",
|
|
4
|
+
"description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/wpmoo-org/wpmoo-odoo.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/wpmoo-org/wpmoo-odoo",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/wpmoo-org/wpmoo-odoo/issues"
|
|
13
|
+
},
|
|
14
|
+
"readmeFilename": "README.md",
|
|
15
|
+
"bin": {
|
|
16
|
+
"wpmoo": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"docs/assets"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^0.11.0",
|
|
33
|
+
"execa": "^9.6.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.0.0",
|
|
37
|
+
"typescript": "^5.8.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|