@wpmoo/odoo 0.8.69 → 0.9.1

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 (47) hide show
  1. package/bin/wpmoo.js +10 -0
  2. package/package.json +11 -45
  3. package/LICENSE +0 -22
  4. package/README.md +0 -510
  5. package/dist/addons-yaml.js +0 -59
  6. package/dist/args.js +0 -259
  7. package/dist/cli.js +0 -996
  8. package/dist/cockpit/command-palette.js +0 -23
  9. package/dist/cockpit/command-registry.js +0 -91
  10. package/dist/cockpit/daily-prompts.js +0 -177
  11. package/dist/cockpit/menu.js +0 -96
  12. package/dist/cockpit/safety.js +0 -22
  13. package/dist/compose-layout.js +0 -118
  14. package/dist/daily-actions.js +0 -190
  15. package/dist/doctor.js +0 -519
  16. package/dist/environment-context.js +0 -10
  17. package/dist/environment-version.js +0 -5
  18. package/dist/environment.js +0 -136
  19. package/dist/external-assets.js +0 -153
  20. package/dist/external-templates.js +0 -86
  21. package/dist/git.js +0 -98
  22. package/dist/github.js +0 -87
  23. package/dist/help.js +0 -151
  24. package/dist/menu-navigation.js +0 -67
  25. package/dist/module-actions.js +0 -114
  26. package/dist/odoo-versions.js +0 -1
  27. package/dist/path-validation.js +0 -50
  28. package/dist/prompt-copy.js +0 -8
  29. package/dist/prompt-repositories.js +0 -34
  30. package/dist/prompts/index.js +0 -149
  31. package/dist/repo-actions.js +0 -158
  32. package/dist/repo-url.js +0 -27
  33. package/dist/repository-preflight.js +0 -46
  34. package/dist/safe-reset.js +0 -217
  35. package/dist/scaffold.js +0 -161
  36. package/dist/source-actions.js +0 -65
  37. package/dist/source-manifest.js +0 -338
  38. package/dist/status.js +0 -239
  39. package/dist/templates.js +0 -749
  40. package/dist/types.js +0 -1
  41. package/dist/update-check.js +0 -106
  42. package/dist/version.js +0 -19
  43. package/docs/assets/patreon-donate.png +0 -0
  44. package/docs/assets/wpmoo-banner.png +0 -0
  45. package/docs/external-resources.md +0 -136
  46. package/docs/generated-environment-verification.md +0 -140
  47. package/docs/handoff.md +0 -29
@@ -1,153 +0,0 @@
1
- import { cp, mkdir, mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { dirname, join, relative, resolve } from 'node:path';
4
- import { realGit } from './git.js';
5
- const defaultExcludes = ['.git', 'node_modules', '.DS_Store'];
6
- async function pathExists(path) {
7
- try {
8
- await stat(path);
9
- return true;
10
- }
11
- catch {
12
- return false;
13
- }
14
- }
15
- async function statIfExists(path) {
16
- try {
17
- return await stat(path);
18
- }
19
- catch {
20
- return undefined;
21
- }
22
- }
23
- function expandHome(path) {
24
- if (path === '~')
25
- return process.env.HOME ?? path;
26
- if (path.startsWith('~/'))
27
- return join(process.env.HOME ?? '~', path.slice(2));
28
- return path;
29
- }
30
- export function gitUrlFromSource(source) {
31
- if (source.startsWith('gh:')) {
32
- return `https://github.com/${source.slice(3).replace(/\.git$/, '')}.git`;
33
- }
34
- if (source.startsWith('git:github.com/')) {
35
- return `https://github.com/${source.slice('git:github.com/'.length).replace(/\.git$/, '')}.git`;
36
- }
37
- if (/^(https?:|ssh:|git:)/.test(source) || /^[^\s@]+@[^\s:]+:.+/.test(source)) {
38
- return source;
39
- }
40
- return undefined;
41
- }
42
- async function checkoutSource(git, source, ref) {
43
- const gitUrl = gitUrlFromSource(source);
44
- if (!gitUrl) {
45
- return { root: resolve(expandHome(source)) };
46
- }
47
- const tempRoot = await mkdtemp(join(tmpdir(), 'wpmoo-external-'));
48
- const cloneDir = join(tempRoot, 'source');
49
- const cloneArgs = ['clone', '--depth', '1'];
50
- if (ref) {
51
- cloneArgs.push('--branch', ref);
52
- }
53
- cloneArgs.push(gitUrl, cloneDir);
54
- try {
55
- await git.run(tempRoot, cloneArgs);
56
- }
57
- catch (error) {
58
- if (!ref) {
59
- await rm(tempRoot, { recursive: true, force: true });
60
- throw error;
61
- }
62
- await rm(cloneDir, { recursive: true, force: true });
63
- await git.run(tempRoot, ['clone', gitUrl, cloneDir]);
64
- await git.run(cloneDir, ['checkout', ref]);
65
- }
66
- return { root: cloneDir, cleanup: tempRoot };
67
- }
68
- function isExcluded(relativePath, excludes) {
69
- const normalized = relativePath.split('\\').join('/');
70
- return excludes.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
71
- }
72
- async function removeDestinationTypeConflicts(sourcePath, destinationPath, excludes) {
73
- async function visit(source) {
74
- const rel = relative(sourcePath, source);
75
- if (rel && isExcluded(rel, excludes))
76
- return;
77
- const sourceStat = await stat(source);
78
- const destination = rel ? join(destinationPath, rel) : destinationPath;
79
- const destinationStat = await statIfExists(destination);
80
- if (destinationStat && sourceStat.isDirectory() !== destinationStat.isDirectory()) {
81
- await rm(destination, { recursive: true, force: true });
82
- }
83
- if (!sourceStat.isDirectory()) {
84
- return;
85
- }
86
- const entries = await readdir(source);
87
- await Promise.all(entries.map((entry) => visit(join(source, entry))));
88
- }
89
- await visit(sourcePath);
90
- }
91
- async function copyDirectory(options, checkedOut) {
92
- const selectedSourceSubdir = await selectSourceSubdir(options, checkedOut.root);
93
- const sourcePath = selectedSourceSubdir ? join(checkedOut.root, selectedSourceSubdir) : checkedOut.root;
94
- const destinationPath = options.destinationSubdir
95
- ? join(options.destination, options.destinationSubdir)
96
- : options.destination;
97
- if (!(await pathExists(sourcePath))) {
98
- throw new Error(`External asset source path does not exist: ${sourcePath}`);
99
- }
100
- const excludes = [...defaultExcludes, ...(options.exclude ?? [])];
101
- await mkdir(destinationPath, { recursive: true });
102
- await removeDestinationTypeConflicts(sourcePath, destinationPath, excludes);
103
- await cp(sourcePath, destinationPath, {
104
- recursive: true,
105
- force: true,
106
- filter: (source) => {
107
- const rel = relative(sourcePath, source);
108
- return !rel || !isExcluded(rel, excludes);
109
- },
110
- });
111
- if (options.readmeDestination) {
112
- const selectedReadmePath = selectedSourceSubdir ? join(checkedOut.root, selectedSourceSubdir, 'README.md') : undefined;
113
- const readmePath = selectedReadmePath && (await pathExists(selectedReadmePath))
114
- ? selectedReadmePath
115
- : join(checkedOut.root, 'README.md');
116
- if (await pathExists(readmePath)) {
117
- const destination = join(options.destination, options.readmeDestination);
118
- await mkdir(dirname(destination), { recursive: true });
119
- await cp(readmePath, destination, { force: true });
120
- }
121
- }
122
- }
123
- async function selectSourceSubdir(options, root) {
124
- for (const candidate of options.sourceSubdirCandidates ?? []) {
125
- if (await pathExists(join(root, candidate))) {
126
- return candidate;
127
- }
128
- }
129
- return options.sourceSubdir;
130
- }
131
- export function renderExternalAssetCommand(options) {
132
- const sourcePath = options.sourceSubdir ? `${options.source}/${options.sourceSubdir}` : options.source;
133
- const destinationPath = options.destinationSubdir
134
- ? `${options.destination}/${options.destinationSubdir}`
135
- : options.destination;
136
- const ref = options.ref ? `#${options.ref}` : '';
137
- return `copy external ${options.label}: ${sourcePath}${ref} -> ${destinationPath}`;
138
- }
139
- export async function applyExternalAsset(options, git = realGit) {
140
- const checkedOut = await checkoutSource(git, options.source, options.ref);
141
- try {
142
- await copyDirectory(options, checkedOut);
143
- }
144
- finally {
145
- if (checkedOut.cleanup) {
146
- await rm(checkedOut.cleanup, { recursive: true, force: true });
147
- }
148
- }
149
- }
150
- export async function writeTextFile(path, content) {
151
- await mkdir(dirname(path), { recursive: true });
152
- await writeFile(path, content, 'utf8');
153
- }
@@ -1,86 +0,0 @@
1
- export const defaultComposeTemplateUrl = 'gh:wpmoo-org/odoo-docker-compose';
2
- export const defaultAgentSkillsTemplateUrl = 'gh:wpmoo-org/odoo-skills';
3
- function odooMajorVersion(odooVersion) {
4
- return odooVersion.split('.', 1)[0] || odooVersion;
5
- }
6
- export function defaultPostgresVersion(odooVersion) {
7
- const major = odooMajorVersion(odooVersion);
8
- if (major === '19')
9
- return '18';
10
- if (major === '18')
11
- return '17';
12
- if (major === '17')
13
- return '15';
14
- if (major === '16')
15
- return '14';
16
- return '17';
17
- }
18
- export function defaultHttpPort(odooVersion) {
19
- const major = odooMajorVersion(odooVersion).padStart(2, '0');
20
- return `100${major}`;
21
- }
22
- export function defaultGeventPort(odooVersion) {
23
- const major = odooMajorVersion(odooVersion).padStart(2, '0');
24
- return `200${major}`;
25
- }
26
- export function defaultTestModule(options) {
27
- return options.sourceRepos.flatMap((repo) => repo.addons)[0] ?? options.product;
28
- }
29
- export function renderComposeEnvExample(options) {
30
- return [
31
- '# Copy to .env and edit for local development.',
32
- `ODOO_VERSION=${options.odooVersion}`,
33
- `ODOO_IMAGE=odoo:${odooMajorVersion(options.odooVersion)}`,
34
- `POSTGRES_IMAGE=postgres:${options.postgresVersion ?? defaultPostgresVersion(options.odooVersion)}`,
35
- `HTTP_PORT=${options.httpPort ?? defaultHttpPort(options.odooVersion)}`,
36
- `GEVENT_PORT=${options.geventPort ?? defaultGeventPort(options.odooVersion)}`,
37
- 'POSTGRES_PASSWORD=odoo',
38
- 'ODOO_MASTER_PASSWORD=admin',
39
- `ODOO_TEST_MODULE=${defaultTestModule(options)}`,
40
- 'WPMOO_ENV=dev',
41
- 'WPMOO_SNAPSHOT_RETENTION_COUNT=0',
42
- '',
43
- '# Required only when intentionally running destructive database actions',
44
- '# such as resetdb or restore-snapshot with WPMOO_ENV=stage or WPMOO_ENV=prod.',
45
- '# WPMOO_ALLOW_DESTRUCTIVE=1',
46
- '',
47
- ].join('\n');
48
- }
49
- export function composeTemplateOptions(options) {
50
- return {
51
- label: 'compose',
52
- source: options.composeTemplateUrl ?? defaultComposeTemplateUrl,
53
- destination: options.target,
54
- ref: options.composeTemplateRef,
55
- sourceSubdirCandidates: ['resources/generated-env'],
56
- exclude: [
57
- '.github',
58
- 'docs/assets',
59
- 'test',
60
- 'README.md',
61
- 'README-template.md',
62
- '.gitignore',
63
- 'LICENSE',
64
- 'package.json',
65
- 'package-lock.json',
66
- ],
67
- readmeDestination: 'docs/compose.md',
68
- };
69
- }
70
- export function agentSkillsTemplateOptions(options) {
71
- if (!options.agentSkillsTemplateUrl) {
72
- return undefined;
73
- }
74
- return {
75
- label: 'agent-skills',
76
- source: options.agentSkillsTemplateUrl,
77
- destination: options.target,
78
- ref: options.agentSkillsTemplateRef,
79
- sourceSubdir: 'skills',
80
- destinationSubdir: '.agents/skills',
81
- exclude: ['package.json', 'package-lock.json'],
82
- };
83
- }
84
- export function plannedExternalAssetOptions(options) {
85
- return [composeTemplateOptions(options), agentSkillsTemplateOptions(options)].filter((assetOptions) => Boolean(assetOptions));
86
- }
package/dist/git.js DELETED
@@ -1,98 +0,0 @@
1
- import { mkdtemp, rm } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { basename, join } from 'node:path';
4
- import { execa } from 'execa';
5
- export const realGit = {
6
- async run(cwd, args) {
7
- const result = await execa('git', args, {
8
- cwd,
9
- env: {
10
- ...process.env,
11
- GIT_ALLOW_PROTOCOL: 'file:https:ssh:git',
12
- },
13
- });
14
- return { stdout: result.stdout, stderr: result.stderr };
15
- },
16
- };
17
- export async function hasRemoteHeads(git, cwd, repoUrl) {
18
- const result = await git.run(cwd, ['ls-remote', '--heads', repoUrl]);
19
- return result.stdout.trim().length > 0;
20
- }
21
- export async function initializeEmptyRemote(git, cwd, repoUrl, branch) {
22
- const tempRoot = await mkdtemp(join(tmpdir(), 'wpmoo-init-'));
23
- const cloneDir = join(tempRoot, basename(repoUrl).replace(/\.git$/, '') || 'repo');
24
- try {
25
- await git.run(tempRoot, ['clone', repoUrl, cloneDir]);
26
- await git.run(cloneDir, ['config', 'user.name', 'Create Odoo Dev Bot']);
27
- await git.run(cloneDir, ['config', 'user.email', 'dev@example.com']);
28
- await git.run(cloneDir, ['commit', '--allow-empty', '-m', 'Initial commit']);
29
- await git.run(cloneDir, ['push', 'origin', `HEAD:${branch}`]);
30
- }
31
- finally {
32
- await rm(tempRoot, { recursive: true, force: true });
33
- }
34
- }
35
- export async function ensureRemoteHasBranch(git, cwd, repoUrl, branch, initEmptyRepos) {
36
- const hasHeads = await hasRemoteHeads(git, cwd, repoUrl);
37
- if (!hasHeads) {
38
- if (!initEmptyRepos) {
39
- throw new Error(`Repository has no commits: ${repoUrl}`);
40
- }
41
- await initializeEmptyRemote(git, cwd, repoUrl, branch);
42
- return;
43
- }
44
- const branchResult = await git.run(cwd, ['ls-remote', '--heads', repoUrl, branch]);
45
- if (!branchResult.stdout.trim()) {
46
- throw new Error(`Repository ${repoUrl} does not have branch ${branch}`);
47
- }
48
- }
49
- export async function addSubmodule(git, target, repoUrl, branch, path) {
50
- await git.run(target, ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-b', branch, repoUrl, path]);
51
- }
52
- export async function isTrackedPath(git, target, path) {
53
- try {
54
- await git.run(target, ['ls-files', '--error-unmatch', path]);
55
- return true;
56
- }
57
- catch {
58
- return false;
59
- }
60
- }
61
- export async function ensureSubmodule(git, target, repoUrl, branch, path) {
62
- if (await isTrackedPath(git, target, path)) {
63
- await git.run(target, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init', '--recursive', path]);
64
- return;
65
- }
66
- await addSubmodule(git, target, repoUrl, branch, path);
67
- }
68
- export async function hasUncommittedChanges(git, cwd) {
69
- const result = await git.run(cwd, ['status', '--porcelain']);
70
- return result.stdout.trim().length > 0;
71
- }
72
- export async function removeSubmodule(git, target, path) {
73
- try {
74
- await git.run(target, ['submodule', 'deinit', '-f', path]);
75
- }
76
- catch {
77
- // `git rm` below is the authoritative removal; deinit can fail for partially initialized submodules.
78
- }
79
- await git.run(target, ['rm', '-f', path]);
80
- }
81
- export async function cloneRepository(git, cwd, repoUrl, target) {
82
- await git.run(cwd, ['clone', repoUrl, target]);
83
- }
84
- export async function syncSubmodules(git, target) {
85
- await git.run(target, ['submodule', 'sync', '--recursive']);
86
- }
87
- export async function stageAll(git, target) {
88
- await git.run(target, ['add', '.']);
89
- }
90
- export async function getOriginUrl(git, target) {
91
- try {
92
- const result = await git.run(target, ['remote', 'get-url', 'origin']);
93
- return result.stdout.trim() || undefined;
94
- }
95
- catch {
96
- return undefined;
97
- }
98
- }
package/dist/github.js DELETED
@@ -1,87 +0,0 @@
1
- import { execa } from 'execa';
2
- export const realGitHub = {
3
- async run(args) {
4
- const result = await execa('gh', args);
5
- return { stdout: result.stdout, stderr: result.stderr };
6
- },
7
- };
8
- export function parseGitHubRepoUrl(repoUrl) {
9
- const normalized = repoUrl.trim().replace(/[?#].*$/, '').replace(/\/+$/, '').replace(/\.git$/, '');
10
- const httpsMatch = normalized.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/);
11
- if (httpsMatch) {
12
- return { owner: httpsMatch[1], name: httpsMatch[2] };
13
- }
14
- const sshMatch = normalized.match(/^git@github\.com:([^/]+)\/([^/]+)$/);
15
- if (sshMatch) {
16
- return { owner: sshMatch[1], name: sshMatch[2] };
17
- }
18
- return undefined;
19
- }
20
- export function githubSlug(repoUrl) {
21
- const repo = parseGitHubRepoUrl(repoUrl);
22
- return repo ? `${repo.owner}/${repo.name}` : undefined;
23
- }
24
- export function githubRepositoryUrl(owner, repo) {
25
- return `https://github.com/${owner}/${repo}.git`;
26
- }
27
- export async function isGitHubCliAvailable(runner = realGitHub) {
28
- try {
29
- await runner.run(['--version']);
30
- return true;
31
- }
32
- catch {
33
- return false;
34
- }
35
- }
36
- export async function getAuthenticatedGitHubLogin(runner = realGitHub) {
37
- const result = await runner.run(['api', 'user', '--jq', '.login']);
38
- const login = result.stdout.trim();
39
- if (!login) {
40
- throw new Error('GitHub CLI is not authenticated');
41
- }
42
- return login;
43
- }
44
- export async function isGitHubAuthenticated(runner = realGitHub) {
45
- try {
46
- await getAuthenticatedGitHubLogin(runner);
47
- return true;
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
- export async function listGitHubOrganizations(runner = realGitHub) {
54
- const result = await runner.run(['api', 'user/orgs', '--jq', '.[].login']);
55
- return result.stdout
56
- .split('\n')
57
- .map((line) => line.trim())
58
- .filter(Boolean);
59
- }
60
- export async function getGitHubAccounts(runner = realGitHub) {
61
- const user = await getAuthenticatedGitHubLogin(runner);
62
- const organizations = await listGitHubOrganizations(runner);
63
- return [
64
- { login: user, type: 'user' },
65
- ...organizations.map((login) => ({ login, type: 'organization' })),
66
- ];
67
- }
68
- export async function getGitHubRepositoryStatus(runner, repoUrl) {
69
- const slug = githubSlug(repoUrl);
70
- if (!slug) {
71
- return { status: 'unsupported' };
72
- }
73
- try {
74
- await runner.run(['repo', 'view', slug, '--json', 'name']);
75
- return { status: 'accessible', slug };
76
- }
77
- catch {
78
- return { status: 'inaccessible', slug };
79
- }
80
- }
81
- export async function createGitHubRepository(runner, repoUrl, visibility) {
82
- const slug = githubSlug(repoUrl);
83
- if (!slug) {
84
- throw new Error(`Only GitHub repository URLs can be created automatically: ${repoUrl}`);
85
- }
86
- await runner.run(['repo', 'create', slug, visibility === 'private' ? '--private' : '--public']);
87
- }
package/dist/help.js DELETED
@@ -1,151 +0,0 @@
1
- export function renderHelp() {
2
- return `@wpmoo/odoo
3
-
4
- WPMoo Tool for Odoo lifecycle workflows.
5
-
6
- Usage:
7
- npx @wpmoo/odoo
8
- npx @wpmoo/odoo create --product <slug> [--target <path>] --dev-repo-url <url> --source-repo-url <url>
9
- npx @wpmoo/odoo status
10
- npx @wpmoo/odoo status --json
11
- npx @wpmoo/odoo add-repo --repo-url <url> [--source-type private|oca|external]
12
- npx @wpmoo/odoo remove-repo --repo <name>
13
- npx @wpmoo/odoo source list
14
- npx @wpmoo/odoo source list --json
15
- npx @wpmoo/odoo source sync
16
- npx @wpmoo/odoo source sync --json
17
- npx @wpmoo/odoo source add --repo-url <url> [--source-type private|oca|external]
18
- npx @wpmoo/odoo source remove --repo <name> [--source-type private|oca|external]
19
- npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> [--source-type <category>]
20
- npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name> [--source-type <category>]
21
- npx @wpmoo/odoo reset [--dry-run]
22
- npx @wpmoo/odoo doctor [--fix]
23
- npx @wpmoo/odoo doctor --json
24
- npx @wpmoo/odoo start
25
- npx @wpmoo/odoo stop
26
- npx @wpmoo/odoo logs [service]
27
- npx @wpmoo/odoo restart
28
- npx @wpmoo/odoo shell
29
- npx @wpmoo/odoo psql [db]
30
- npx @wpmoo/odoo install <module[,module]> [db]
31
- npx @wpmoo/odoo update <module[,module]> [db]
32
- npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
33
- npx @wpmoo/odoo resetdb [db] [module[,module]]
34
- npx @wpmoo/odoo snapshot [db] [snapshot-name]
35
- npx @wpmoo/odoo restore-snapshot [--dry-run] <snapshot-name> [db]
36
- npx @wpmoo/odoo lint
37
- npx @wpmoo/odoo pot <module[,module]> [db] [output]
38
-
39
- Options:
40
- --product <slug> Product slug, for example my_odoo_module.
41
- --odoo-version <branch> Odoo branch to pin submodules to. Default: 19.0.
42
- --dev-repo-url <url> Optional development environment repository URL for docs.
43
- --target <path> Target dev repo directory. Default: ./<product>_dev.
44
- --engine <value> Environment engine: compose. Default: compose.
45
- --compose-template-url <url> Standalone compose resource source. Default: gh:wpmoo-org/odoo-docker-compose.
46
- --compose-template-ref <ref> Git ref for the compose resource.
47
- --agent-skills-template Install project Agent Skills from a standalone skills resource.
48
- --agent-skills-template-url <url>
49
- Agent Skills resource source. Default: gh:wpmoo-org/odoo-skills.
50
- --agent-skills-template-ref <ref>
51
- Git ref for the Agent Skills resource.
52
- --postgres-version <value> PostgreSQL image version written to compose .env.example.
53
- --http-port <port> Host HTTP port written to .env.example.
54
- --gevent-port <port> Host gevent/live chat port written to .env.example.
55
- --json Emit machine-readable JSON. Human-readable output remains the default.
56
- --repo-url <url> Source repo URL for add-repo.
57
- --source-type <category> Source repo category for add-repo/remove-repo/add-module/remove-module. One of private, oca, external. Default: private.
58
- --repo <name> Source repo folder name for repo/module actions.
59
- --module <name> Odoo module technical name for module actions.
60
- --delete-files Also delete module files in remove-module. Default: false.
61
- --odoo-version <branch> Override the environment Odoo branch for add-repo/add-module.
62
- --source-repo-url <url> Source repo URL. Repeat for multiple repos.
63
- --source-path <path> Advanced: local folder for the preceding source repo.
64
- --source-addons <list> Advanced: comma-separated addons for the preceding source repo.
65
- --create-missing-repos Create inaccessible GitHub repos with gh CLI.
66
- --repo-visibility <value> Visibility for created repos: private or public. Default: private.
67
- --init-empty-repos Initialize empty source repos with the selected branch.
68
- --dry-run Print planned files and commands without writing.
69
- --stage=false Do not run git add .
70
- --no-update-check Skip the startup npm update check.
71
- --version, -v Show the package version.
72
- --help, -h Show this help.
73
-
74
- Daily actions:
75
- Daily actions must be run from a generated environment root containing .wpmoo/odoo.json.
76
- They delegate to the fixed scripts copied from the compose resource under ./scripts.
77
- Generated environments also include ./moo for local compose commands such as ./moo start.
78
- Use ./moo or npx @wpmoo/odoo with the same daily action arguments.
79
-
80
- Cockpit:
81
- Run npx @wpmoo/odoo inside a generated environment to open the cockpit.
82
- Use Command palette / to search slash commands across services, modules, database,
83
- diagnostics, repositories, and maintenance categories.
84
- Direct commands such as npx @wpmoo/odoo status and npx @wpmoo/odoo test remain available.
85
-
86
- Wizard local-only path:
87
- Run npx @wpmoo/odoo from a workspace directory to open the create wizard.
88
- Choose any environment folder; the default is ./<product>_dev.
89
- Skip Git/GitHub connection to create a local-only environment.
90
- Add source repos later from the cockpit or with add-repo.
91
-
92
- Status and doctor:
93
- status: fast and offline. Reads local environment metadata and files only.
94
- doctor: deeper health check. May check Docker CLI access and GitHub workflows.
95
- doctor --fix: applies safe file-level repairs. Runs doctor again after fixes.
96
-
97
- Task recipes:
98
- Create environment:
99
- npx @wpmoo/odoo
100
- npx @wpmoo/odoo create --product <slug> --dev-repo-url <url> --source-repo-url <url>
101
- Create local-only environment:
102
- npx @wpmoo/odoo
103
- Add source repo:
104
- npx @wpmoo/odoo add-repo --repo-url <url> --source-type oca
105
- Inspect and sync source manifest:
106
- npx @wpmoo/odoo source list
107
- npx @wpmoo/odoo source sync
108
- Add module:
109
- npx @wpmoo/odoo add-module --repo <source-repo> --module <module-name> --source-type private|oca|external
110
- Remove module:
111
- npx @wpmoo/odoo remove-module --repo <source-repo> --module <module-name> --source-type private|oca|external
112
- Add OCA module:
113
- npx @wpmoo/odoo add-module --repo sale-workflow --module sale_order_line_no_discount --source-type oca
114
- Run tests:
115
- npx @wpmoo/odoo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]
116
- Safe reset and recover:
117
- npx @wpmoo/odoo snapshot [db] [snapshot-name]
118
- npx @wpmoo/odoo reset --dry-run
119
- npx @wpmoo/odoo reset
120
- npx @wpmoo/odoo restore-snapshot --dry-run <snapshot-name> [db]
121
- npx @wpmoo/odoo restore-snapshot <snapshot-name> [db]
122
- Daily command checks:
123
- npx @wpmoo/odoo status
124
- npx @wpmoo/odoo doctor
125
- npx @wpmoo/odoo doctor --fix
126
- npx @wpmoo/odoo logs [service]
127
- npx @wpmoo/odoo restart
128
-
129
- Machine-readable JSON output:
130
- for automation and VS Code cockpit integration while keeping default human-readable output.
131
- npx @wpmoo/odoo status --json
132
- npx @wpmoo/odoo source list --json
133
- npx @wpmoo/odoo source sync --json
134
- npx @wpmoo/odoo doctor --json
135
-
136
- Example:
137
- npx @wpmoo/odoo create \\
138
- --product odoo_sample_module \\
139
- --odoo-version 19.0 \\
140
- --target ./custom_odoo_dev \\
141
- --dev-repo-url https://github.com/example-org/odoo_sample_module_dev.git \\
142
- --source-repo-url https://github.com/example-org/odoo_sample_module.git
143
-
144
- Compose resource example:
145
- npx @wpmoo/odoo create \\
146
- --engine compose \\
147
- --product odoo_sample_module \\
148
- --source-repo-url https://github.com/example-org/odoo_sample_module.git \\
149
- --agent-skills-template
150
- `;
151
- }
@@ -1,67 +0,0 @@
1
- import { emitKeypressEvents } from 'node:readline';
2
- let lastPromptCancelKey;
3
- export class MenuBackSignal extends Error {
4
- constructor() {
5
- super('Return to previous menu');
6
- this.name = 'MenuBackSignal';
7
- }
8
- }
9
- export function isMenuBackSignal(error) {
10
- return error instanceof MenuBackSignal;
11
- }
12
- export function menuIntroTitle(title, action) {
13
- return action === 'back' ? `${title} · Back (Esc)` : title;
14
- }
15
- export function menuPromptMessage(message, action) {
16
- return action === 'back' ? `${message} · Esc to go back` : message;
17
- }
18
- export function promptCancelOutcome(cancelled, action, key) {
19
- if (!cancelled) {
20
- return 'continue';
21
- }
22
- if (action === 'back' && key !== 'interrupt') {
23
- return 'back';
24
- }
25
- return 'exit';
26
- }
27
- export function recordPromptCancelKey(key) {
28
- if (key.ctrl && key.name === 'c') {
29
- lastPromptCancelKey = 'interrupt';
30
- return;
31
- }
32
- if (key.name === 'escape' || key.sequence === '\u001B') {
33
- lastPromptCancelKey = 'escape';
34
- return;
35
- }
36
- lastPromptCancelKey = 'other';
37
- }
38
- export function consumePromptCancelKey() {
39
- const key = lastPromptCancelKey;
40
- lastPromptCancelKey = undefined;
41
- return key;
42
- }
43
- export function installPromptCancelKeyTracker(input = process.stdin) {
44
- emitKeypressEvents(input);
45
- const listener = (_value, key) => {
46
- if (key.ctrl || key.name === 'escape' || key.sequence === '\u001B') {
47
- recordPromptCancelKey(key);
48
- }
49
- };
50
- input.on('keypress', listener);
51
- return () => input.off('keypress', listener);
52
- }
53
- export function handlePromptCancel(cancelled, action) {
54
- const outcome = promptCancelOutcome(cancelled, action, consumePromptCancelKey());
55
- if (outcome === 'continue') {
56
- return;
57
- }
58
- if (outcome === 'back') {
59
- throw new MenuBackSignal();
60
- }
61
- process.exit(1);
62
- }
63
- export function handleUnavailableMenuChoice(action) {
64
- if (action === 'back') {
65
- throw new MenuBackSignal();
66
- }
67
- }