@wpmoo/odoo 0.8.62 → 0.8.64

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 CHANGED
@@ -142,8 +142,8 @@ npx @wpmoo/odoo doctor
142
142
  npx @wpmoo/odoo doctor --fix
143
143
  npx @wpmoo/odoo add-repo --repo-url https://github.com/example-org/odoo_sample_module_reports.git
144
144
  npx @wpmoo/odoo remove-repo --repo odoo_sample_module_reports
145
- npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base
146
- npx @wpmoo/odoo remove-module --repo odoo_sample_module --module odoo_sample_module_base
145
+ npx @wpmoo/odoo add-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
146
+ npx @wpmoo/odoo remove-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
147
147
  npx @wpmoo/odoo reset --dry-run
148
148
  npx @wpmoo/odoo reset
149
149
 
@@ -275,10 +275,13 @@ GitHub CLI is optional for repository setup. When it is available and authentica
275
275
 
276
276
  Add a minimal Odoo module skeleton to a source repository:
277
277
 
278
+ For module actions, `--source-type` selects the source directory (`private`, `oca`, or `external`). Default is `private`.
279
+
278
280
  ```bash
279
281
  npx @wpmoo/odoo add-module \
280
282
  --repo odoo_sample_module \
281
- --module odoo_sample_module_base
283
+ --module odoo_sample_module_base \
284
+ --source-type oca
282
285
  ```
283
286
 
284
287
  Remove a module registration while keeping files:
@@ -286,7 +289,8 @@ Remove a module registration while keeping files:
286
289
  ```bash
287
290
  npx @wpmoo/odoo remove-module \
288
291
  --repo odoo_sample_module \
289
- --module odoo_sample_module_base
292
+ --module odoo_sample_module_base \
293
+ --source-type oca
290
294
  ```
291
295
 
292
296
  Delete module files as well:
package/dist/cli.js CHANGED
@@ -399,21 +399,39 @@ async function ensureAddRepoGitHubRepository(options, cancelAction = 'exit') {
399
399
  await createGitHubRepository(realGitHub, options.repoUrl, visibility);
400
400
  }
401
401
  async function selectSourceRepo(target, cancelAction = 'exit') {
402
- const repos = await listModuleRepos(target);
403
- if (repos.length === 0) {
402
+ const repos = await listSources(target);
403
+ const repoOptions = repos.length > 0
404
+ ? repos.map((repo) => ({
405
+ value: { repoPath: repo.path, sourceType: repo.type },
406
+ label: `${repo.type}/${repo.path}`,
407
+ }))
408
+ : (await listModuleRepos(target)).map((repoPath) => ({
409
+ value: { repoPath, sourceType: 'private' },
410
+ label: `private/${repoPath}`,
411
+ }));
412
+ if (repoOptions.length === 0) {
404
413
  if (cancelAction === 'back') {
405
- note(`No source repos found under ${target}/odoo/custom/src/private.\nNext: choose "Add source repo" first.`, 'Nothing to select');
414
+ note(`No source repos found under ${target}/odoo/custom/src.\nNext: choose "Add source repo" first.`, 'Nothing to select');
406
415
  handleUnavailableMenuChoice(cancelAction);
407
416
  }
408
- throw new Error(`No source repos found under ${target}/odoo/custom/src/private`);
417
+ throw new Error(`No source repos found under ${target}/odoo/custom/src`);
409
418
  }
410
- const repoPath = await select({
419
+ const selected = await select({
411
420
  message: menuPromptMessage('Source repo', cancelAction),
412
- options: repos.map((repo) => ({ value: repo, label: repo })),
413
- initialValue: repos[0],
421
+ options: repoOptions,
422
+ initialValue: repoOptions[0].value,
414
423
  });
415
- handleCancel(repoPath, cancelAction);
416
- return String(repoPath);
424
+ handleCancel(selected, cancelAction);
425
+ if (typeof selected === 'string') {
426
+ return { repoPath: selected, sourceType: 'private' };
427
+ }
428
+ if (typeof selected === 'object' && selected !== null && 'repoPath' in selected && 'sourceType' in selected) {
429
+ return { repoPath: selected.repoPath, sourceType: selected.sourceType };
430
+ }
431
+ return { repoPath: String(selected), sourceType: 'private' };
432
+ }
433
+ function formatSourceRepoPromptPath(target, selected) {
434
+ return renderedSourceRepoPath(target, selected.sourceType, selected.repoPath);
417
435
  }
418
436
  function suggestedModuleName(repoPath) {
419
437
  return 'odoo_sample_module';
@@ -430,6 +448,7 @@ async function addModuleOptionsFromArgs(argv) {
430
448
  target,
431
449
  repoPath,
432
450
  moduleName,
451
+ sourceType: optionalSourceTypeValue(values),
433
452
  odooVersion: await commandOdooVersion(target, stringOption(values, 'odooVersion')),
434
453
  stage: booleanOption(values, 'stage', true),
435
454
  };
@@ -437,15 +456,16 @@ async function addModuleOptionsFromArgs(argv) {
437
456
  async function addModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
438
457
  showSubmenuIntro('Add module to source repo', showIntro, cancelAction);
439
458
  const target = process.cwd();
440
- const repoPath = await selectSourceRepo(target, cancelAction);
459
+ const sourceRepo = await selectSourceRepo(target, cancelAction);
441
460
  const moduleName = asString(await text({
442
461
  message: menuPromptMessage('Module name', cancelAction),
443
- placeholder: suggestedModuleName(repoPath),
462
+ placeholder: suggestedModuleName(sourceRepo.repoPath),
444
463
  validate: (value) => (value.trim() ? undefined : 'Enter the module technical name.'),
445
- }), suggestedModuleName(repoPath), cancelAction);
464
+ }), suggestedModuleName(sourceRepo.repoPath), cancelAction);
446
465
  return {
447
466
  target,
448
- repoPath,
467
+ repoPath: sourceRepo.repoPath,
468
+ sourceType: sourceRepo.sourceType,
449
469
  moduleName,
450
470
  odooVersion: await commandOdooVersion(target),
451
471
  stage: true,
@@ -587,6 +607,7 @@ function removeModuleOptionsFromArgs(argv) {
587
607
  target: resolve(stringOption(values, 'target') ?? process.cwd()),
588
608
  repoPath,
589
609
  moduleName,
610
+ sourceType: optionalSourceTypeValue(values),
590
611
  deleteFiles: booleanOption(values, 'deleteFiles', false),
591
612
  stage: booleanOption(values, 'stage', true),
592
613
  };
@@ -594,14 +615,14 @@ function removeModuleOptionsFromArgs(argv) {
594
615
  async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = 'exit') {
595
616
  showSubmenuIntro('Remove module from source repo', showIntro, cancelAction);
596
617
  const target = process.cwd();
597
- const repoPath = await selectSourceRepo(target, cancelAction);
598
- const modules = await listModulesInSourceRepo(target, repoPath);
618
+ const sourceRepo = await selectSourceRepo(target, cancelAction);
619
+ const modules = await listModulesInSourceRepo(target, sourceRepo.repoPath, sourceRepo.sourceType);
599
620
  if (modules.length === 0) {
600
621
  if (cancelAction === 'back') {
601
- note(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
622
+ note(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}.\nNext: choose "Add module to source repo" first.`, 'Nothing to remove');
602
623
  handleUnavailableMenuChoice(cancelAction);
603
624
  }
604
- throw new Error(`No Odoo modules found under ${target}/odoo/custom/src/private/${repoPath}`);
625
+ throw new Error(`No Odoo modules found under ${formatSourceRepoPromptPath(target, sourceRepo)}`);
605
626
  }
606
627
  const moduleName = await select({
607
628
  message: menuPromptMessage('Module to remove', cancelAction),
@@ -618,7 +639,8 @@ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = '
618
639
  handleCancel(deleteFiles, cancelAction);
619
640
  return {
620
641
  target,
621
- repoPath,
642
+ repoPath: sourceRepo.repoPath,
643
+ sourceType: sourceRepo.sourceType,
622
644
  moduleName: String(moduleName),
623
645
  deleteFiles: Boolean(deleteFiles),
624
646
  stage: true,
@@ -1,6 +1,7 @@
1
1
  import { isCancel, select, text } from '@clack/prompts';
2
2
  import { listModulesInSourceRepo } from '../module-actions.js';
3
3
  import { listModuleRepos } from '../repo-actions.js';
4
+ import { listSources } from '../source-actions.js';
4
5
  import { handlePromptCancel, menuPromptMessage, } from '../menu-navigation.js';
5
6
  const manualModuleValue = '__wpmoo_manual_module_entry__';
6
7
  function defaultCancelHandler(value, action) {
@@ -27,10 +28,13 @@ function requiredString(value, message, deps) {
27
28
  }
28
29
  async function detectedModules(cwd) {
29
30
  try {
30
- const repos = await listModuleRepos(cwd);
31
+ const sources = await listSources(cwd);
32
+ const repos = sources.length > 0
33
+ ? sources.map((source) => ({ path: source.path, sourceType: source.type }))
34
+ : (await listModuleRepos(cwd)).map((path) => ({ path, sourceType: 'private' }));
31
35
  const modules = await Promise.all(repos.map(async (repo) => {
32
36
  try {
33
- return await listModulesInSourceRepo(cwd, repo);
37
+ return await listModulesInSourceRepo(cwd, repo.path, repo.sourceType);
34
38
  }
35
39
  catch {
36
40
  return [];
package/dist/help.js CHANGED
@@ -13,8 +13,8 @@ Usage:
13
13
  npx @wpmoo/odoo source sync
14
14
  npx @wpmoo/odoo source add --repo-url <url> [--source-type private|oca|external]
15
15
  npx @wpmoo/odoo source remove --repo <name> [--source-type private|oca|external]
16
- npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
17
- npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name>
16
+ npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> [--source-type <category>]
17
+ npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name> [--source-type <category>]
18
18
  npx @wpmoo/odoo reset [--dry-run]
19
19
  npx @wpmoo/odoo doctor [--fix]
20
20
  npx @wpmoo/odoo start
@@ -49,7 +49,7 @@ Options:
49
49
  --http-port <port> Host HTTP port written to .env.example.
50
50
  --gevent-port <port> Host gevent/live chat port written to .env.example.
51
51
  --repo-url <url> Source repo URL for add-repo.
52
- --source-type <category> Source repo category for add-repo/remove-repo. One of private, oca, external. Default: private.
52
+ --source-type <category> Source repo category for add-repo/remove-repo/add-module/remove-module. One of private, oca, external. Default: private.
53
53
  --repo <name> Source repo folder name for repo/module actions.
54
54
  --module <name> Odoo module technical name for module actions.
55
55
  --delete-files Also delete module files in remove-module. Default: false.
@@ -101,7 +101,11 @@ Task recipes:
101
101
  npx @wpmoo/odoo source list
102
102
  npx @wpmoo/odoo source sync
103
103
  Add module:
104
- npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name>
104
+ npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> --source-type private|oca|external
105
+ Remove module:
106
+ npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name> --source-type private|oca|external
107
+ Add OCA module:
108
+ npx @wpmoo/odoo add-module --repo sale-workflow --module sale_order_line_no_discount --source-type oca
105
109
  Run tests:
106
110
  npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
107
111
  Safe reset and recover:
@@ -5,11 +5,15 @@ import { readEnvironmentMetadata } from './environment.js';
5
5
  import { realGit, stageAll } from './git.js';
6
6
  import { pathUnderBase, validateModuleName, validateRepoPath } from './path-validation.js';
7
7
  import { readAddonsYaml, writeAddonsYaml } from './repo-actions.js';
8
- function sourceRepoPath(target, repoPath) {
9
- return pathUnderBase(join(target, 'odoo/custom/src/private'), repoPath, 'repo path');
8
+ const validSourceTypes = ['private', 'oca', 'external'];
9
+ function normalizeSourceType(value) {
10
+ return validSourceTypes.includes(value) ? value : 'private';
10
11
  }
11
- function modulePath(target, repoPath, moduleName) {
12
- return pathUnderBase(sourceRepoPath(target, repoPath), moduleName, 'module name');
12
+ function sourceRepoPath(target, sourceType, repoPath) {
13
+ return pathUnderBase(join(target, `odoo/custom/src/${sourceType}`), repoPath, 'repo path');
14
+ }
15
+ function modulePath(target, sourceType, repoPath, moduleName) {
16
+ return pathUnderBase(sourceRepoPath(target, sourceType, repoPath), moduleName, 'module name');
13
17
  }
14
18
  function titleizeModule(moduleName) {
15
19
  return moduleName
@@ -49,7 +53,8 @@ async function usesAddonsYaml(target) {
49
53
  export async function addModuleToSourceRepo(options, git = realGit) {
50
54
  const repoPath = validateRepoPath(options.repoPath);
51
55
  const moduleName = validateModuleName(options.moduleName);
52
- const destination = modulePath(options.target, repoPath, moduleName);
56
+ const sourceType = normalizeSourceType(options.sourceType);
57
+ const destination = modulePath(options.target, sourceType, repoPath, moduleName);
53
58
  await mkdir(join(destination, 'models'), { recursive: true });
54
59
  await mkdir(join(destination, 'security'), { recursive: true });
55
60
  await mkdir(join(destination, 'views'), { recursive: true });
@@ -58,24 +63,25 @@ export async function addModuleToSourceRepo(options, git = realGit) {
58
63
  await writeIfMissing(join(destination, 'models/__init__.py'), '');
59
64
  await writeIfMissing(join(destination, 'security/ir.model.access.csv'), 'id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\n');
60
65
  await writeIfMissing(join(destination, 'views/.gitkeep'), '');
61
- if (await usesAddonsYaml(options.target)) {
66
+ if (sourceType === 'private' && (await usesAddonsYaml(options.target))) {
62
67
  const addonsYaml = await readAddonsYaml(options.target);
63
68
  await writeAddonsYaml(options.target, addModuleToSourceRepoInAddonsYaml(addonsYaml, repoPath, moduleName));
64
69
  }
65
70
  if (options.stage) {
66
- await stageAll(git, sourceRepoPath(options.target, repoPath));
71
+ await stageAll(git, sourceRepoPath(options.target, sourceType, repoPath));
67
72
  await stageAll(git, options.target);
68
73
  }
69
74
  }
70
- export async function listModulesInSourceRepo(target, repoPath) {
75
+ export async function listModulesInSourceRepo(target, repoPath, sourceType) {
71
76
  const safeRepoPath = validateRepoPath(repoPath);
77
+ const resolvedSourceType = normalizeSourceType(sourceType);
72
78
  try {
73
- const entries = await readdir(sourceRepoPath(target, safeRepoPath), { withFileTypes: true });
79
+ const entries = await readdir(sourceRepoPath(target, resolvedSourceType, safeRepoPath), { withFileTypes: true });
74
80
  const modules = await Promise.all(entries
75
81
  .filter((entry) => entry.isDirectory())
76
82
  .map(async (entry) => {
77
83
  try {
78
- await readFile(join(sourceRepoPath(target, safeRepoPath), entry.name, '__manifest__.py'), 'utf8');
84
+ await readFile(join(sourceRepoPath(target, resolvedSourceType, safeRepoPath), entry.name, '__manifest__.py'), 'utf8');
79
85
  return entry.name;
80
86
  }
81
87
  catch {
@@ -91,16 +97,17 @@ export async function listModulesInSourceRepo(target, repoPath) {
91
97
  export async function removeModuleFromSourceRepo(options, git = realGit) {
92
98
  const repoPath = validateRepoPath(options.repoPath);
93
99
  const moduleName = validateModuleName(options.moduleName);
94
- if (await usesAddonsYaml(options.target)) {
100
+ const sourceType = normalizeSourceType(options.sourceType);
101
+ if (sourceType === 'private' && (await usesAddonsYaml(options.target))) {
95
102
  const addonsYaml = await readAddonsYaml(options.target);
96
103
  await writeAddonsYaml(options.target, removeModuleFromSourceRepoInAddonsYaml(addonsYaml, repoPath, moduleName));
97
104
  }
98
105
  if (options.deleteFiles) {
99
- await rm(modulePath(options.target, repoPath, moduleName), { recursive: true, force: true });
106
+ await rm(modulePath(options.target, sourceType, repoPath, moduleName), { recursive: true, force: true });
100
107
  }
101
108
  if (options.stage) {
102
109
  if (options.deleteFiles) {
103
- await stageAll(git, sourceRepoPath(options.target, repoPath));
110
+ await stageAll(git, sourceRepoPath(options.target, sourceType, repoPath));
104
111
  }
105
112
  await stageAll(git, options.target);
106
113
  }
package/dist/status.js CHANGED
@@ -3,6 +3,16 @@ import { join } from 'node:path';
3
3
  import { detectComposeLayout, readEnvFile, selectedComposeEnvironment } from './compose-layout.js';
4
4
  import { defaultOdooVersion, markerPath } from './environment.js';
5
5
  import { isValidPathSegment, validateRepoPath } from './path-validation.js';
6
+ const validSourceTypes = ['private', 'oca', 'external'];
7
+ function normalizeSourceType(sourceType) {
8
+ if (typeof sourceType === 'string' && validSourceTypes.includes(sourceType)) {
9
+ return sourceType;
10
+ }
11
+ return 'private';
12
+ }
13
+ function sourceRepoPath(target, sourceType, path) {
14
+ return join(target, 'odoo/custom/src', sourceType, path);
15
+ }
6
16
  async function pathExists(path) {
7
17
  try {
8
18
  await access(path);
@@ -27,9 +37,11 @@ function parseMetadata(content) {
27
37
  }
28
38
  function sourceRepoPathsFromMetadata(metadata) {
29
39
  const sourceRepoPaths = [];
40
+ const sourceRepoLocations = [];
30
41
  const invalidSourceRepoPaths = [];
31
- if (!Array.isArray(metadata.sourceRepos))
32
- return { sourceRepoPaths, invalidSourceRepoPaths };
42
+ if (!Array.isArray(metadata.sourceRepos)) {
43
+ return { sourceRepoPaths, sourceRepoLocations, invalidSourceRepoPaths };
44
+ }
33
45
  for (const repo of metadata.sourceRepos) {
34
46
  const path = repo && typeof repo.path === 'string' ? repo.path.trim() : '';
35
47
  if (!path)
@@ -38,9 +50,12 @@ function sourceRepoPathsFromMetadata(metadata) {
38
50
  invalidSourceRepoPaths.push(path);
39
51
  continue;
40
52
  }
41
- sourceRepoPaths.push(validateRepoPath(path));
53
+ const sourceType = normalizeSourceType(typeof repo.sourceType === 'string' ? repo.sourceType : undefined);
54
+ const normalizedPath = validateRepoPath(path);
55
+ sourceRepoPaths.push(normalizedPath);
56
+ sourceRepoLocations.push({ sourceType, path: normalizedPath });
42
57
  }
43
- return { sourceRepoPaths, invalidSourceRepoPaths };
58
+ return { sourceRepoPaths, sourceRepoLocations, invalidSourceRepoPaths };
44
59
  }
45
60
  async function missingCoreFiles(target, odooVersion) {
46
61
  const missing = [];
@@ -135,8 +150,8 @@ export async function getEnvironmentStatus(target) {
135
150
  const odooVersion = typeof metadata.odooVersion === 'string' && metadata.odooVersion.trim()
136
151
  ? metadata.odooVersion.trim()
137
152
  : defaultOdooVersion;
138
- const { sourceRepoPaths, invalidSourceRepoPaths } = sourceRepoPathsFromMetadata(metadata);
139
- const repoRoots = sourceRepoPaths.map((path) => join(target, 'odoo/custom/src/private', path));
153
+ const { sourceRepoPaths, sourceRepoLocations, invalidSourceRepoPaths } = sourceRepoPathsFromMetadata(metadata);
154
+ const repoRoots = sourceRepoLocations.map(({ sourceType, path }) => sourceRepoPath(target, sourceType, path));
140
155
  let moduleCandidateCount = 0;
141
156
  for (const repoRoot of repoRoots) {
142
157
  moduleCandidateCount += await countModuleCandidatesInRepoPath(repoRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/odoo",
3
- "version": "0.8.62",
3
+ "version": "0.8.64",
4
4
  "description": "WPMoo Odoo lifecycle tooling for development, staging, and production workflows.",
5
5
  "type": "module",
6
6
  "repository": {