@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.
@@ -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
+ }
@@ -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
+ }