@wpmoo/odoo 0.8.68 → 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.
- package/bin/wpmoo.js +10 -0
- package/package.json +11 -44
- package/LICENSE +0 -22
- package/README.md +0 -510
- package/dist/addons-yaml.js +0 -59
- package/dist/args.js +0 -259
- package/dist/cli.js +0 -1002
- package/dist/cockpit/command-palette.js +0 -19
- package/dist/cockpit/command-registry.js +0 -91
- package/dist/cockpit/daily-prompts.js +0 -177
- package/dist/cockpit/menu.js +0 -88
- package/dist/cockpit/safety.js +0 -22
- package/dist/compose-layout.js +0 -118
- package/dist/daily-actions.js +0 -190
- package/dist/doctor.js +0 -519
- package/dist/environment-context.js +0 -10
- package/dist/environment-version.js +0 -5
- package/dist/environment.js +0 -136
- package/dist/external-assets.js +0 -153
- package/dist/external-templates.js +0 -86
- package/dist/git.js +0 -98
- package/dist/github.js +0 -87
- package/dist/help.js +0 -151
- package/dist/menu-navigation.js +0 -67
- package/dist/module-actions.js +0 -114
- package/dist/odoo-versions.js +0 -1
- package/dist/path-validation.js +0 -50
- package/dist/prompt-copy.js +0 -8
- package/dist/prompt-repositories.js +0 -34
- package/dist/repo-actions.js +0 -158
- package/dist/repo-url.js +0 -27
- package/dist/repository-preflight.js +0 -46
- package/dist/safe-reset.js +0 -217
- package/dist/scaffold.js +0 -161
- package/dist/source-actions.js +0 -65
- package/dist/source-manifest.js +0 -338
- package/dist/status.js +0 -239
- package/dist/templates.js +0 -754
- package/dist/types.js +0 -1
- package/dist/update-check.js +0 -106
- package/dist/version.js +0 -19
- package/docs/assets/patreon-donate.png +0 -0
- package/docs/assets/wpmoo-banner.png +0 -0
- package/docs/external-resources.md +0 -136
- package/docs/generated-environment-verification.md +0 -140
- package/docs/handoff.md +0 -29
package/dist/external-assets.js
DELETED
|
@@ -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
|
-
}
|
package/dist/menu-navigation.js
DELETED
|
@@ -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
|
-
}
|