@wpmoo/toolkit 0.9.0

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.
Files changed (46) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +519 -0
  3. package/dist/addons-yaml.js +59 -0
  4. package/dist/args.js +259 -0
  5. package/dist/cli.js +1039 -0
  6. package/dist/cockpit/command-palette.js +23 -0
  7. package/dist/cockpit/command-registry.js +91 -0
  8. package/dist/cockpit/daily-prompts.js +177 -0
  9. package/dist/cockpit/menu.js +99 -0
  10. package/dist/cockpit/safety.js +22 -0
  11. package/dist/compose-layout.js +118 -0
  12. package/dist/daily-actions.js +190 -0
  13. package/dist/doctor.js +519 -0
  14. package/dist/environment-context.js +10 -0
  15. package/dist/environment-version.js +5 -0
  16. package/dist/environment.js +136 -0
  17. package/dist/external-assets.js +153 -0
  18. package/dist/external-templates.js +86 -0
  19. package/dist/git.js +98 -0
  20. package/dist/github.js +87 -0
  21. package/dist/help.js +157 -0
  22. package/dist/menu-navigation.js +67 -0
  23. package/dist/module-actions.js +114 -0
  24. package/dist/odoo-versions.js +1 -0
  25. package/dist/path-validation.js +50 -0
  26. package/dist/prompt-copy.js +8 -0
  27. package/dist/prompt-repositories.js +34 -0
  28. package/dist/prompts/index.js +174 -0
  29. package/dist/repo-actions.js +158 -0
  30. package/dist/repo-url.js +27 -0
  31. package/dist/repository-preflight.js +46 -0
  32. package/dist/safe-reset.js +217 -0
  33. package/dist/scaffold.js +161 -0
  34. package/dist/source-actions.js +65 -0
  35. package/dist/source-manifest.js +338 -0
  36. package/dist/status.js +239 -0
  37. package/dist/templates.js +758 -0
  38. package/dist/types.js +1 -0
  39. package/dist/update-check.js +106 -0
  40. package/dist/version.js +19 -0
  41. package/docs/assets/patreon-donate.png +0 -0
  42. package/docs/assets/wpmoo-banner.png +0 -0
  43. package/docs/external-resources.md +136 -0
  44. package/docs/generated-environment-verification.md +140 -0
  45. package/docs/handoff.md +29 -0
  46. package/package.json +65 -0
@@ -0,0 +1,758 @@
1
+ import { packageName, packageVersion } from './version.js';
2
+ function fallbackPackageSpec() {
3
+ return `${packageName()}@${packageVersion()}`;
4
+ }
5
+ export function defaultCommunityAddons(product) {
6
+ return [product];
7
+ }
8
+ export function defaultProAddons(product) {
9
+ return [`${product}_pro`];
10
+ }
11
+ function titleizeProduct(product) {
12
+ return product
13
+ .split(/[_-]+/)
14
+ .filter(Boolean)
15
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
16
+ .join(' ');
17
+ }
18
+ function yamlList(items) {
19
+ return items.map((item) => ` - ${item}`).join('\n');
20
+ }
21
+ function allAddons(options) {
22
+ return options.sourceRepos.flatMap((repo) => repo.addons);
23
+ }
24
+ function hasSourceRepos(options) {
25
+ return options.sourceRepos.length > 0;
26
+ }
27
+ function sourceTypeOf(repo) {
28
+ return repo.sourceType ?? 'private';
29
+ }
30
+ function sourceRepoRelativePath(repo) {
31
+ return `odoo/custom/src/${sourceTypeOf(repo)}/${repo.path}`;
32
+ }
33
+ function repositoryLayout(options) {
34
+ const sourceRepoRows = hasSourceRepos(options)
35
+ ? options.sourceRepos
36
+ .map((repo, index) => {
37
+ const connector = index === options.sourceRepos.length - 1 ? '└──' : '├──';
38
+ return `│ │ │ ${connector} ${repo.path}/ # Project-owned addon source repository`;
39
+ })
40
+ .join('\n')
41
+ : '│ │ │ └── (add project-owned repos with ./moo add-repo)';
42
+ return `${options.devRepo}/ # Development environment root
43
+ ├── compose.yaml # Base Docker Compose file
44
+ ├── compose/ # Compose overlays for each workflow
45
+ │ ├── dev.yaml # Local development services
46
+ │ ├── debug.yaml # Debug tooling and debug-friendly settings
47
+ │ ├── test.yaml # Test runner services and test database setup
48
+ │ ├── stage.yaml # Staging-like deployment overlay
49
+ │ ├── prod.yaml # Production deployment overlay
50
+ │ ├── proxy.yaml # Reverse proxy / edge routing overlay
51
+ │ └── tools.yaml # Optional maintenance and helper tools
52
+ ├── config/ # Runtime configuration mounted into containers
53
+ │ ├── odoo/ # Odoo server configuration
54
+ │ │ ├── odoo.conf # Main Odoo configuration file
55
+ │ │ └── requirements.txt # Extra Python dependencies for the Odoo container
56
+ │ └── logrotate/ # Log rotation configuration
57
+ │ └── odoo # Logrotate rules for Odoo logs
58
+ ├── resources/ # Container-side helper resources
59
+ │ └── odoo/ # Resources specific to the Odoo service
60
+ │ └── entrypoint.sh # Container startup script that discovers addons
61
+ ├── moo # Local command hub shortcut
62
+ ├── scripts/ # Shell scripts used by the local command hub
63
+ ├── odoo/ # Odoo workspace data and custom source tree
64
+ │ └── custom/ # Custom addon layer for this environment
65
+ │ ├── src/ # Source repository checkout root
66
+ │ │ ├── private/ # Project-owned/private addon repositories
67
+ ${sourceRepoRows}
68
+ │ │ ├── oca/ # OCA addon repositories
69
+ │ │ └── external/ # Non-OCA third-party addon repositories
70
+ │ ├── patches/ # Local patches for upstream repositories
71
+ │ └── manifests/ # Source manifests, locks, and pinned revisions
72
+ ├── docs/ # Project-specific documentation
73
+ │ ├── appstore-release.md # Odoo App Store release checklist and notes
74
+ │ └── compose.md # Compose layout and operations reference
75
+ ├── .env.example # Template for local environment variables
76
+ ├── README.md # This environment overview
77
+ └── AGENTS.md # Agent instructions for this environment`;
78
+ }
79
+ function sourceRepoDocs(options) {
80
+ if (!hasSourceRepos(options)) {
81
+ return `This environment was scaffolded without source repository submodules.
82
+ Add source repositories later from the cockpit or with \`npx @wpmoo/toolkit add-repo\`.
83
+ They can be organized under:
84
+
85
+ \`odoo/custom/src/private\` for project-owned/private addon repositories,
86
+ \`odoo/custom/src/oca\` for OCA repositories, and
87
+ \`odoo/custom/src/external\` for non-OCA third-party repositories.
88
+
89
+ Pinned external manifests and local patches should live under
90
+ \`odoo/custom/manifests\` and \`odoo/custom/patches\` respectively.`;
91
+ }
92
+ return options.sourceRepos
93
+ .map((repo) => `### ${repo.path}
94
+
95
+ URL:
96
+
97
+ \`\`\`text
98
+ ${repo.url}
99
+ \`\`\`
100
+
101
+ Submodule path:
102
+
103
+ \`\`\`text
104
+ ${sourceRepoRelativePath(repo)}
105
+ \`\`\`
106
+
107
+ Source manifest entry:
108
+
109
+ \`\`\`text
110
+ odoo/custom/manifests/sources.yaml
111
+ \`\`\`
112
+
113
+ Expected addon layout:
114
+
115
+ \`\`\`text
116
+ ${repo.path}/
117
+ ${repo.addons.map((addon) => `├── ${addon}/`).join('\n')}
118
+ \`\`\``)
119
+ .join('\n\n');
120
+ }
121
+ function cloneDocs(options) {
122
+ if (!hasSourceRepos(options)) {
123
+ return `## Local Folder
124
+
125
+ This environment is ready in this folder:
126
+
127
+ \`\`\`bash
128
+ cd ${options.devRepo}
129
+ \`\`\`
130
+
131
+ If you later connect it to Git, commit the generated files after reviewing them.
132
+ `;
133
+ }
134
+ return `## Clone
135
+
136
+ Clone with submodules:
137
+
138
+ \`\`\`bash
139
+ git clone --recurse-submodules ${options.devRepoUrl}
140
+ cd ${options.devRepo}
141
+ \`\`\`
142
+
143
+ If already cloned:
144
+
145
+ \`\`\`bash
146
+ git submodule update --init --recursive
147
+ \`\`\`
148
+ `;
149
+ }
150
+ function optionalAgentSkillsReadme(options) {
151
+ if (!options.agentSkillsTemplateUrl)
152
+ return '';
153
+ return `
154
+ ## Agent Skills
155
+
156
+ This environment is configured to install project-local Agent Skills from:
157
+
158
+ \`\`\`text
159
+ ${options.agentSkillsTemplateUrl}${options.agentSkillsTemplateRef ? `#${options.agentSkillsTemplateRef}` : ''}
160
+ \`\`\`
161
+
162
+ After external resource installation, skills normally live under:
163
+
164
+ \`\`\`text
165
+ .agents/skills/
166
+ \`\`\`
167
+
168
+ Agents that support the Agent Skills standard can load them on demand.
169
+ `;
170
+ }
171
+ function optionalAgentSkillsAgentsSection(options) {
172
+ if (!options.agentSkillsTemplateUrl)
173
+ return '';
174
+ return `
175
+ ## Active Agent Skills
176
+
177
+ When using an agent that supports Agent Skills, prefer the project-local skills
178
+ installed under \`.agents/skills/\`. They are sourced from:
179
+
180
+ \`\`\`text
181
+ ${options.agentSkillsTemplateUrl}${options.agentSkillsTemplateRef ? `#${options.agentSkillsTemplateRef}` : ''}
182
+ \`\`\`
183
+ `;
184
+ }
185
+ function environmentKind() {
186
+ return 'Docker Compose';
187
+ }
188
+ function repoDuplicationNote() {
189
+ return 'Keep source repositories under the relevant source directory (`private`, `oca`, or `external`); the Compose entrypoint exposes discovered addons through `/mnt/wpmoo-addons`.';
190
+ }
191
+ function verificationCommand(options) {
192
+ const firstAddon = allAddons(options)[0] ?? options.product;
193
+ return `./moo test ${firstAddon}`;
194
+ }
195
+ function environmentUsageDocs(options) {
196
+ return `## Docker Compose Notes
197
+
198
+ This environment uses the compact WPMoo Compose layout:
199
+
200
+ \`\`\`text
201
+ compose.yaml
202
+ compose/dev.yaml
203
+ compose/stage.yaml
204
+ compose/prod.yaml
205
+ config/odoo/odoo.conf
206
+ resources/odoo/entrypoint.sh
207
+ \`\`\`
208
+
209
+ Development uses compose.yaml plus compose/dev.yaml by default.
210
+ Set WPMOO_ENV=stage or WPMOO_ENV=prod only after providing production-grade secrets and volumes.
211
+
212
+ If copied from the standalone resource, additional compose notes are in
213
+ \`docs/compose.md\`.
214
+
215
+ Source repositories stay under \`odoo/custom/src/{private,oca,external}\` when
216
+ configured. At
217
+ container startup, \`entrypoint.sh\` scans those repositories for addons and
218
+ exposes them through \`/mnt/wpmoo-addons\`.
219
+
220
+ ## Daily Command Hub (\`./moo\`)
221
+
222
+ \`./moo\` routes day-to-day service and module workflows to local scripts in
223
+ \`./scripts/\` (for example \`start\`, \`logs\`, \`update\`, \`test\`, \`snapshot\`).
224
+ \`./moo status\` and \`./moo doctor\` are package fallback commands that run via
225
+ \`npx --yes ${fallbackPackageSpec()} ...\`.
226
+
227
+ ### Start And Inspect Services
228
+
229
+ \`\`\`bash
230
+ cp .env.example .env
231
+ ./moo start
232
+ ./moo logs odoo
233
+ ./moo shell
234
+ ./moo psql postgres
235
+ ./moo stop
236
+ \`\`\`
237
+
238
+ ### Run, Update, And Test Modules
239
+
240
+ \`\`\`bash
241
+ ./moo install ${allAddons(options)[0] ?? options.product}
242
+ ./moo update ${allAddons(options)[0] ?? options.product}
243
+ ./moo test ${allAddons(options)[0] ?? options.product}
244
+ \`\`\`
245
+
246
+ ### Snapshot And Restore
247
+
248
+ \`\`\`bash
249
+ ./moo snapshot devel before-update
250
+ ./moo restore-snapshot --dry-run before-update devel
251
+ ./moo restore-snapshot before-update devel
252
+ \`\`\`
253
+
254
+ ### Lint
255
+
256
+ \`\`\`bash
257
+ ./moo lint
258
+ \`\`\`
259
+
260
+ ### Export Translations
261
+
262
+ \`\`\`bash
263
+ ./moo pot ${allAddons(options)[0] ?? options.product} devel i18n/${allAddons(options)[0] ?? options.product}.pot
264
+ \`\`\`
265
+
266
+ ### Recover / Reset
267
+
268
+ \`\`\`bash
269
+ ./moo doctor
270
+ ./moo status
271
+ ./moo resetdb devel ${allAddons(options)[0] ?? options.product}
272
+ \`\`\`
273
+ `;
274
+ }
275
+ const BANNER_GRADIENT_START = [31, 151, 231];
276
+ const BANNER_GRADIENT_END = [209, 95, 127];
277
+ const ANSI_BOLD = '\u001B[1m';
278
+ const ANSI_DIM = '\u001B[2m';
279
+ const ANSI_INFO = '\u001B[38;2;139;166;190m';
280
+ const ANSI_RESET = '\u001B[0m';
281
+ const BANNER_DIVIDER_WIDTH = 40;
282
+ function gradientColor(column, width) {
283
+ const ratio = width <= 1 ? 0 : column / (width - 1);
284
+ const [startR, startG, startB] = BANNER_GRADIENT_START;
285
+ const [endR, endG, endB] = BANNER_GRADIENT_END;
286
+ const r = Math.round(startR + (endR - startR) * ratio);
287
+ const g = Math.round(startG + (endG - startG) * ratio);
288
+ const b = Math.round(startB + (endB - startB) * ratio);
289
+ return `\u001B[38;2;${r};${g};${b}m`;
290
+ }
291
+ function applyBannerGradient(banner) {
292
+ const lines = banner.split('\n');
293
+ return lines
294
+ .map((line) => {
295
+ const width = line.length;
296
+ return Array.from(line)
297
+ .map((character, column) => `${gradientColor(column, width)}${character}`)
298
+ .join('');
299
+ })
300
+ .join('\n');
301
+ }
302
+ function renderDimInfo(value) {
303
+ return `${ANSI_DIM}${ANSI_INFO}${value}${ANSI_RESET}`;
304
+ }
305
+ export function renderBanner(details = [], options = {}) {
306
+ const title = `${applyBannerGradient('WPMoo Toolkit')}${options.version ? ` ${renderDimInfo(options.version)}` : ''}`;
307
+ const header = [
308
+ title,
309
+ applyBannerGradient('Workflow Platform · Micro Object Oriented'),
310
+ renderDimInfo('Development, staging, and production workflows for Odoo projects.'),
311
+ applyBannerGradient('─'.repeat(BANNER_DIVIDER_WIDTH)),
312
+ ].join('\n');
313
+ const detailsBlock = details.length > 0 ? `\n${details.map((line) => renderDimInfo(line)).join('\n')}` : '';
314
+ return `\n${ANSI_BOLD}${header}${ANSI_RESET}${detailsBlock}`;
315
+ }
316
+ export function renderGitignore() {
317
+ return `# macOS/editor noise
318
+ .DS_Store
319
+ .idea/
320
+ .vscode/*.log
321
+
322
+ # Node/local package files
323
+ node_modules/
324
+ dist/
325
+ coverage/
326
+ *.log
327
+
328
+ # Python/cache files
329
+ __pycache__/
330
+ *.py[cod]
331
+ .pytest_cache/
332
+ .mypy_cache/
333
+ .ruff_cache/
334
+
335
+ # Local environment files
336
+ .env
337
+ .env.*
338
+ !.env.example
339
+ *.local
340
+
341
+ # Local generated files
342
+ *.code-workspace
343
+ addons/
344
+ auto/
345
+ backups/
346
+ data/
347
+ filestore/
348
+ postgresql/
349
+ sessions/
350
+ odoo/custom/auto/
351
+ odoo/custom/src/*/.git-aggregate-cache/
352
+
353
+ # Backups and archives
354
+ *.bak
355
+ *.backup
356
+ *.dump
357
+ *.sql
358
+ *.zip
359
+ *.tar
360
+ *.tar.gz
361
+ `;
362
+ }
363
+ export function renderMooDelegationScript() {
364
+ return `#!/usr/bin/env bash
365
+ set -euo pipefail
366
+
367
+ script_dir="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")" && pwd)"
368
+ cd "$script_dir"
369
+
370
+ usage() {
371
+ case "$1" in
372
+ "start") echo "Usage: ./moo start" ;;
373
+ "stop") echo "Usage: ./moo stop" ;;
374
+ "logs") echo "Usage: ./moo logs [service]" ;;
375
+ "restart") echo "Usage: ./moo restart" ;;
376
+ "shell") echo "Usage: ./moo shell" ;;
377
+ "psql") echo "Usage: ./moo psql [db]" ;;
378
+ "install") echo "Usage: ./moo install <module[,module]> [db]" ;;
379
+ "update") echo "Usage: ./moo update <module[,module]> [db]" ;;
380
+ "test") echo "Usage: ./moo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]" ;;
381
+ "resetdb") echo "Usage: ./moo resetdb [db] [module[,module]]" ;;
382
+ "snapshot") echo "Usage: ./moo snapshot [db] [snapshot-name]" ;;
383
+ "restore-snapshot") echo "Usage: ./moo restore-snapshot [--dry-run] <snapshot-name> [db]" ;;
384
+ "lint") echo "Usage: ./moo lint" ;;
385
+ "pot") echo "Usage: ./moo pot <module[,module]> [db] [output]" ;;
386
+ esac
387
+ }
388
+
389
+ fail_usage() {
390
+ usage "$1" >&2
391
+ exit 2
392
+ }
393
+
394
+ require_no_args() {
395
+ local command="$1"
396
+ shift
397
+ if [[ "$#" -ne 0 ]]; then
398
+ fail_usage "$command"
399
+ fi
400
+ }
401
+
402
+ optional_single_arg() {
403
+ local command="$1"
404
+ local fallback="$2"
405
+ shift 2
406
+ if [[ "$#" -gt 1 ]]; then
407
+ fail_usage "$command"
408
+ fi
409
+ printf '%s\\n' "\${1:-$fallback}"
410
+ }
411
+
412
+ require_module_args() {
413
+ local command="$1"
414
+ shift
415
+ if [[ "$#" -lt 1 || "\${1:-}" == -* || "$#" -gt 2 ]]; then
416
+ fail_usage "$command"
417
+ fi
418
+ }
419
+
420
+ positional_args() {
421
+ local command="$1"
422
+ local min="$2"
423
+ local max="$3"
424
+ shift 3
425
+ if [[ "$#" -lt "$min" || "$#" -gt "$max" ]]; then
426
+ fail_usage "$command"
427
+ fi
428
+ for arg in "$@"; do
429
+ if [[ "$arg" == -* ]]; then
430
+ fail_usage "$command"
431
+ fi
432
+ done
433
+ }
434
+
435
+ validate_test_args() {
436
+ if [[ "$#" -lt 1 || "\${1:-}" == -* ]]; then
437
+ fail_usage "test"
438
+ fi
439
+
440
+ shift
441
+ while [[ "$#" -gt 0 ]]; do
442
+ case "$1" in
443
+ "--db"|"--tags")
444
+ if [[ "$#" -lt 2 || "\${2:-}" == --* ]]; then
445
+ echo "Missing value for $1" >&2
446
+ exit 2
447
+ fi
448
+ shift 2
449
+ ;;
450
+ "--mode")
451
+ if [[ "$#" -lt 2 || "\${2:-}" == --* ]]; then
452
+ echo "Missing value for --mode" >&2
453
+ exit 2
454
+ fi
455
+ if [[ "$2" != "init" && "$2" != "update" ]]; then
456
+ echo "Invalid value for --mode: expected init or update" >&2
457
+ exit 2
458
+ fi
459
+ shift 2
460
+ ;;
461
+ *)
462
+ echo "Unknown option for ./moo test: $1" >&2
463
+ exit 2
464
+ ;;
465
+ esac
466
+ done
467
+ }
468
+
469
+ run_script() {
470
+ local script="$1"
471
+ shift
472
+ if [[ ! -x "$script" ]]; then
473
+ echo "Missing daily action script: \${script#./}" >&2
474
+ exit 1
475
+ fi
476
+ exec "$script" "$@"
477
+ }
478
+
479
+ command="\${1:-}"
480
+ case "$command" in
481
+ "start")
482
+ shift
483
+ require_no_args "$command" "$@"
484
+ run_script ./scripts/up.sh
485
+ ;;
486
+ "stop")
487
+ shift
488
+ require_no_args "$command" "$@"
489
+ run_script ./scripts/down.sh
490
+ ;;
491
+ "logs")
492
+ shift
493
+ service="$(optional_single_arg "$command" "odoo" "$@")"
494
+ run_script ./scripts/logs.sh "$service"
495
+ ;;
496
+ "restart")
497
+ shift
498
+ require_no_args "$command" "$@"
499
+ run_script ./scripts/restart.sh
500
+ ;;
501
+ "shell")
502
+ shift
503
+ require_no_args "$command" "$@"
504
+ run_script ./scripts/shell.sh
505
+ ;;
506
+ "psql")
507
+ shift
508
+ db="$(optional_single_arg "$command" "postgres" "$@")"
509
+ run_script ./scripts/psql.sh "$db"
510
+ ;;
511
+ "install")
512
+ shift
513
+ require_module_args "$command" "$@"
514
+ run_script ./scripts/install.sh "$@"
515
+ ;;
516
+ "update")
517
+ shift
518
+ require_module_args "$command" "$@"
519
+ run_script ./scripts/update.sh "$@"
520
+ ;;
521
+ "test")
522
+ shift
523
+ validate_test_args "$@"
524
+ run_script ./scripts/test.sh "$@"
525
+ ;;
526
+ "resetdb")
527
+ shift
528
+ positional_args "$command" 0 2 "$@"
529
+ run_script ./scripts/resetdb.sh "$@"
530
+ ;;
531
+ "snapshot")
532
+ shift
533
+ positional_args "$command" 0 2 "$@"
534
+ run_script ./scripts/snapshot.sh "$@"
535
+ ;;
536
+ "restore-snapshot")
537
+ shift
538
+ restore_args=()
539
+ if [[ "\${1:-}" == "--dry-run" ]]; then
540
+ restore_args+=("--dry-run")
541
+ shift
542
+ fi
543
+ positional_args "$command" 1 2 "$@"
544
+ restore_args+=("$@")
545
+ run_script ./scripts/restore-snapshot.sh "\${restore_args[@]}"
546
+ ;;
547
+ "lint")
548
+ shift
549
+ require_no_args "$command" "$@"
550
+ run_script ./scripts/lint.sh
551
+ ;;
552
+ "pot")
553
+ shift
554
+ positional_args "$command" 1 3 "$@"
555
+ run_script ./scripts/pot.sh "$@"
556
+ ;;
557
+ *)
558
+ exec npx --yes ${fallbackPackageSpec()} "$@"
559
+ ;;
560
+ esac
561
+ `;
562
+ }
563
+ export function renderAddonsYaml(options) {
564
+ return `# Addons activated from source submodules.
565
+ #
566
+ # Source repos are managed as Git submodules under odoo/custom/src/private.
567
+ # Do not duplicate these same repos in repos.yaml.
568
+
569
+ ${options.sourceRepos.map((repo) => `${sourceTypeOf(repo)}/${repo.path}:\n${yamlList(repo.addons)}`).join('\n\n')}
570
+ `;
571
+ }
572
+ export function renderReposYaml(options) {
573
+ return `# git-aggregator repositories.
574
+ #
575
+ # Project source repositories are intentionally not listed here because
576
+ # they are pinned as Git submodules:
577
+ #
578
+ ${options.sourceRepos.map((repo) => `# - ${sourceTypeOf(repo)}/${repo.path}`).join('\n')}
579
+ #
580
+ # Keep this file for upstream/OCA repositories that should be aggregated.
581
+
582
+ odoo:
583
+ defaults:
584
+ depth: $DEPTH_MERGE
585
+ remotes:
586
+ origin: https://github.com/OCA/OCB.git
587
+ odoo: https://github.com/odoo/odoo.git
588
+ target: origin $ODOO_VERSION
589
+ merges:
590
+ - origin $ODOO_VERSION
591
+ `;
592
+ }
593
+ export function renderReadme(options) {
594
+ const title = titleizeProduct(options.product);
595
+ return `# ${title} Development Environment
596
+
597
+ Private ${environmentKind()} development environment for the ${title} product.
598
+
599
+ This folder owns the development environment only. Product source code lives
600
+ in source repository submodules under \`odoo/custom/src/private\`,
601
+ \`odoo/custom/src/oca\`, or \`odoo/custom/src/external\` when source
602
+ repositories are connected.
603
+
604
+ ## Repository Layout
605
+
606
+ \`\`\`text
607
+ ${repositoryLayout(options)}
608
+ \`\`\`
609
+
610
+ ${cloneDocs(options)}
611
+
612
+ ## WPMoo CLI Shortcut
613
+
614
+ This environment includes a local \`moo\` shortcut script. From the repository
615
+ root:
616
+
617
+ \`\`\`bash
618
+ ./moo
619
+ ./moo start
620
+ ./moo stop
621
+ ./moo restart
622
+ ./moo doctor
623
+ ./moo add-module
624
+ \`\`\`
625
+
626
+ Optionally, if this repository root is on your \`PATH\`, you can run \`moo ...\`
627
+ from anywhere and the script will return to this environment root first.
628
+ ${optionalAgentSkillsReadme(options)}
629
+ ## Source Repositories
630
+
631
+ ${sourceRepoDocs(options)}
632
+
633
+ ${environmentUsageDocs(options)}
634
+ ## Branching
635
+
636
+ Use Odoo major-version branches in source repositories when you add them:
637
+
638
+ \`\`\`text
639
+ ${options.odooVersion}
640
+ \`\`\`
641
+
642
+ If this environment is connected to Git, the dev repository can stay on \`main\`
643
+ and pin exact source commits through submodule references.
644
+ `;
645
+ }
646
+ export function renderAgents(options) {
647
+ const repoList = hasSourceRepos(options)
648
+ ? options.sourceRepos.map((repo) => `- \`${repo.path}\`: \`${repo.url}\``).join('\n')
649
+ : '- No source repositories are configured yet.';
650
+ const sourceLayout = hasSourceRepos(options)
651
+ ? `Product repositories are Git submodules. They are listed under the private
652
+ source directory below for this environment:
653
+
654
+ \`\`\`text
655
+ ${options.sourceRepos.map(sourceRepoRelativePath).join('\n')}
656
+ \`\`\`
657
+
658
+ ${repoDuplicationNote()}`
659
+ : 'No source repositories are configured yet. Use `./moo add-repo` or the cockpit Repositories menu before module-specific work.';
660
+ const addonList = hasSourceRepos(options)
661
+ ? options.sourceRepos
662
+ .map((repo) => `\`${repo.path}\` addons:\n${repo.addons.map((addon) => `- \`${addon}\``).join('\n')}`)
663
+ .join('\n\n')
664
+ : 'No addon boundaries are known yet. Add source repositories before module-specific implementation.';
665
+ return `# AGENTS.md
666
+
667
+ ## Project
668
+
669
+ Private ${environmentKind()} development environment for the ${titleizeProduct(options.product)} product.
670
+
671
+ ## Repository Roles
672
+
673
+ - \`${options.devRepo}\`: environment/config only, private.
674
+ ${repoList}
675
+
676
+ ## Source Layout
677
+
678
+ ${sourceLayout}
679
+ ${optionalAgentSkillsAgentsSection(options)}
680
+ ## Addon Boundaries
681
+
682
+ ${addonList}
683
+
684
+ Public/community addons must not depend on private paid addons. Private paid
685
+ addons may depend on public/community addons.
686
+
687
+ ## Odoo 19 Rules
688
+
689
+ - Use \`<list>\` instead of \`<tree>\`.
690
+ - Use direct view expressions such as \`invisible="..."\` instead of \`attrs\`.
691
+ - Use \`models.Constraint\` instead of \`_sql_constraints\`.
692
+ - Use \`@api.ondelete(at_uninstall=False)\` for delete validation.
693
+ - Avoid \`default_*\` field names in \`res.config.settings\`.
694
+ - Keep core/community installable without any pro modules.
695
+
696
+ ## Verification
697
+
698
+ Use the environment's addon test/update command:
699
+
700
+ \`\`\`bash
701
+ ${verificationCommand(options)}
702
+ \`\`\`
703
+
704
+ Useful maintenance commands:
705
+
706
+ \`\`\`bash
707
+ ./moo lint
708
+ ./moo resetdb [db] [module[,module]]
709
+ ./moo snapshot [db] [snapshot-name]
710
+ ./moo restore-snapshot [--dry-run] <snapshot-name> [db]
711
+ ./moo pot <module[,module]> [db] [output]
712
+ \`\`\`
713
+
714
+ Daily script delegation vs package fallback:
715
+ - \`./moo start\`, \`logs\`, \`install\`, \`update\`, \`test\`, \`snapshot\`, and related runtime tasks delegate to local \`./scripts/*.sh\`.
716
+ - \`./moo status\` and \`./moo doctor\` are package fallback commands routed to \`npx --yes ${fallbackPackageSpec()} ...\`.
717
+
718
+ Only report completion after the relevant update/test/lint command exits cleanly.
719
+ `;
720
+ }
721
+ export function renderAppstoreRelease(options) {
722
+ return `# Odoo Apps Release Notes
723
+
724
+ Paid addons can live together in a private source repository during development.
725
+ Each paid addon still needs its own App Store metadata.
726
+
727
+ Per addon checklist:
728
+
729
+ - \`__manifest__.py\` has correct \`name\`, \`summary\`, \`version\`, \`depends\`,
730
+ \`license\`, \`price\`, \`currency\`, and \`support\`.
731
+ - \`license\` is appropriate for paid distribution, typically \`OPL-1\`.
732
+ - \`static/description/icon.png\` exists.
733
+ - \`static/description/index.html\` exists.
734
+ - Screenshots are stored under \`static/description/\`.
735
+ - Community dependency versions are compatible with the target Odoo major
736
+ version.
737
+
738
+ Recommended release flow:
739
+
740
+ 1. Develop in the relevant private source repository.
741
+ 2. Update the addon manifest version.
742
+ 3. Run update/test commands in this dev environment.
743
+ 4. Tag the source commit.
744
+ 5. Prepare an App Store publish package or publish mirror per paid addon.
745
+ 6. Trigger Odoo Apps repository scan/update.
746
+
747
+ If App Store scan coupling becomes a problem, create separate private publish
748
+ mirror repositories for each paid addon. Keep development in
749
+ \`odoo/custom/src/private\`; mirrors should be generated artifacts, not
750
+ hand-edited source repositories.
751
+ `;
752
+ }
753
+ export function renderPlaceholder(title, body) {
754
+ return `# ${title}
755
+
756
+ ${body}
757
+ `;
758
+ }