@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/scaffold.js
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { chmod, mkdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { applyExternalAsset, renderExternalAssetCommand, writeTextFile } from './external-assets.js';
|
|
4
|
-
import { markerPath, renderEnvironmentMetadata } from './environment.js';
|
|
5
|
-
import { plannedExternalAssetOptions, renderComposeEnvExample } from './external-templates.js';
|
|
6
|
-
import { cloneRepository, ensureSubmodule, ensureRemoteHasBranch, realGit, stageAll, syncSubmodules, } from './git.js';
|
|
7
|
-
import { renderAgents, renderAppstoreRelease, renderGitignore, renderMooDelegationScript, renderPlaceholder, renderReadme, } from './templates.js';
|
|
8
|
-
import { validateAddonName, validateRepoPath } from './path-validation.js';
|
|
9
|
-
import { renderSourceManifest, sourceManifestEntriesFromMetadata } from './source-manifest.js';
|
|
10
|
-
function validateSourceRepo(repo) {
|
|
11
|
-
const path = validateRepoPath(repo.path);
|
|
12
|
-
return {
|
|
13
|
-
...repo,
|
|
14
|
-
path,
|
|
15
|
-
sourceType: repo.sourceType ?? 'private',
|
|
16
|
-
addons: repo.addons.map(validateAddonName),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function sourceRepoSubmodulePath(repo) {
|
|
20
|
-
return `odoo/custom/src/${repo.sourceType ?? 'private'}/${repo.path}`;
|
|
21
|
-
}
|
|
22
|
-
function validateScaffoldOptions(options) {
|
|
23
|
-
return {
|
|
24
|
-
...options,
|
|
25
|
-
sourceRepos: options.sourceRepos.map(validateSourceRepo),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export function generatedFiles(options) {
|
|
29
|
-
const safeOptions = validateScaffoldOptions(options);
|
|
30
|
-
const sourceDirReadmes = [
|
|
31
|
-
{
|
|
32
|
-
path: 'odoo/custom/src/private/README.md',
|
|
33
|
-
title: 'private',
|
|
34
|
-
body: 'Project-owned/private addon repositories go here.',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
path: 'odoo/custom/src/oca/README.md',
|
|
38
|
-
title: 'oca',
|
|
39
|
-
body: 'OCA repositories go here, for example server-tools, web, queue.',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
path: 'odoo/custom/src/external/README.md',
|
|
43
|
-
title: 'external',
|
|
44
|
-
body: 'Non-OCA third-party, vendor, and community addon repositories go here.',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
path: 'odoo/custom/patches/README.md',
|
|
48
|
-
title: 'patches',
|
|
49
|
-
body: 'Local patches for upstream/vendor/OCA repositories go here.',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
path: 'odoo/custom/manifests/README.md',
|
|
53
|
-
title: 'manifests',
|
|
54
|
-
body: 'Manifest/lock/list files for external sources and pinned revisions go here.',
|
|
55
|
-
},
|
|
56
|
-
];
|
|
57
|
-
const files = [
|
|
58
|
-
{ path: markerPath, content: renderEnvironmentMetadata(safeOptions) },
|
|
59
|
-
{ path: 'moo', content: renderMooDelegationScript(), mode: 0o755 },
|
|
60
|
-
{ path: '.gitignore', content: renderGitignore() },
|
|
61
|
-
{ path: 'README.md', content: renderReadme(safeOptions) },
|
|
62
|
-
{ path: 'AGENTS.md', content: renderAgents(safeOptions) },
|
|
63
|
-
{ path: 'docs/appstore-release.md', content: renderAppstoreRelease(safeOptions) },
|
|
64
|
-
{
|
|
65
|
-
path: 'odoo/custom/manifests/sources.yaml',
|
|
66
|
-
content: renderSourceManifest(sourceManifestEntriesFromMetadata(safeOptions.sourceRepos, safeOptions.odooVersion)),
|
|
67
|
-
},
|
|
68
|
-
];
|
|
69
|
-
return [
|
|
70
|
-
...files,
|
|
71
|
-
...sourceDirReadmes.map((readme) => ({
|
|
72
|
-
path: readme.path,
|
|
73
|
-
content: renderPlaceholder(readme.title, readme.body),
|
|
74
|
-
})),
|
|
75
|
-
];
|
|
76
|
-
}
|
|
77
|
-
async function writeGeneratedFiles(target, files) {
|
|
78
|
-
for (const file of files) {
|
|
79
|
-
const destination = join(target, file.path);
|
|
80
|
-
await mkdir(join(destination, '..'), { recursive: true });
|
|
81
|
-
await writeFile(destination, file.content, 'utf8');
|
|
82
|
-
if (file.mode !== undefined) {
|
|
83
|
-
await chmod(destination, file.mode);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function pathExists(path) {
|
|
88
|
-
try {
|
|
89
|
-
await stat(path);
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async function isGitRepository(git, target) {
|
|
97
|
-
if (!(await pathExists(target))) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
const result = await git.run(target, ['rev-parse', '--is-inside-work-tree']);
|
|
102
|
-
return result.stdout.trim() === 'true';
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async function prepareTargetRepository(options, git) {
|
|
109
|
-
if (await isGitRepository(git, options.target)) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (await pathExists(options.target)) {
|
|
113
|
-
throw new Error(`Target exists but is not a Git repository: ${options.target}\n` +
|
|
114
|
-
'Clone the dev environment repository first, or remove the directory and run the CLI again.');
|
|
115
|
-
}
|
|
116
|
-
await mkdir(dirname(options.target), { recursive: true });
|
|
117
|
-
await cloneRepository(git, dirname(options.target), options.devRepoUrl, options.target);
|
|
118
|
-
}
|
|
119
|
-
export async function scaffold(options, git = realGit) {
|
|
120
|
-
const safeOptions = validateScaffoldOptions(options);
|
|
121
|
-
const files = generatedFiles(safeOptions);
|
|
122
|
-
const externalAssets = plannedExternalAssetOptions(safeOptions);
|
|
123
|
-
const plannedCommands = [
|
|
124
|
-
...externalAssets.map((assetOptions) => renderExternalAssetCommand(assetOptions)),
|
|
125
|
-
...safeOptions.sourceRepos.map((repo) => `git submodule add -b ${safeOptions.odooVersion} ${repo.url} ${sourceRepoSubmodulePath(repo)}`),
|
|
126
|
-
];
|
|
127
|
-
if (safeOptions.stage) {
|
|
128
|
-
plannedCommands.push('git add .');
|
|
129
|
-
}
|
|
130
|
-
if (safeOptions.dryRun) {
|
|
131
|
-
return {
|
|
132
|
-
plannedFiles: files.map((file) => file.path),
|
|
133
|
-
plannedCommands,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if (!safeOptions.skipSubmodules || safeOptions.stage) {
|
|
137
|
-
await prepareTargetRepository(safeOptions, git);
|
|
138
|
-
}
|
|
139
|
-
await writeGeneratedFiles(safeOptions.target, files);
|
|
140
|
-
for (const assetOptions of externalAssets) {
|
|
141
|
-
await applyExternalAsset(assetOptions, git);
|
|
142
|
-
}
|
|
143
|
-
await writeTextFile(join(safeOptions.target, '.env.example'), renderComposeEnvExample(safeOptions));
|
|
144
|
-
if (!safeOptions.skipSubmodules) {
|
|
145
|
-
for (const repo of safeOptions.sourceRepos) {
|
|
146
|
-
await ensureRemoteHasBranch(git, safeOptions.target, repo.url, safeOptions.odooVersion, safeOptions.initEmptyRepos);
|
|
147
|
-
}
|
|
148
|
-
for (const repo of safeOptions.sourceRepos) {
|
|
149
|
-
await mkdir(join(safeOptions.target, 'odoo/custom/src', repo.sourceType ?? 'private'), { recursive: true });
|
|
150
|
-
await ensureSubmodule(git, safeOptions.target, repo.url, safeOptions.odooVersion, sourceRepoSubmodulePath(repo));
|
|
151
|
-
}
|
|
152
|
-
await syncSubmodules(git, safeOptions.target);
|
|
153
|
-
}
|
|
154
|
-
if (safeOptions.stage) {
|
|
155
|
-
await stageAll(git, safeOptions.target);
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
plannedFiles: files.map((file) => file.path),
|
|
159
|
-
plannedCommands,
|
|
160
|
-
};
|
|
161
|
-
}
|
package/dist/source-actions.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { defaultOdooVersion, readEnvironmentMetadata, replaceSourceRepos } from './environment.js';
|
|
2
|
-
import { realGit, stageAll } from './git.js';
|
|
3
|
-
import { listGitmoduleSources, readSourceManifest, sourceManifestEntriesFromMetadata, sourceReposFromManifest, syncManifestFromMetadataAndGitmodules, writeSourceManifest, } from './source-manifest.js';
|
|
4
|
-
function cloneSourceEntries(entries) {
|
|
5
|
-
return entries.map((entry) => ({
|
|
6
|
-
...entry,
|
|
7
|
-
addons: [...entry.addons],
|
|
8
|
-
}));
|
|
9
|
-
}
|
|
10
|
-
export async function listSources(target) {
|
|
11
|
-
const metadata = await readEnvironmentMetadata(target);
|
|
12
|
-
const manifest = await readSourceManifest(target);
|
|
13
|
-
if (manifest.sources.length > 0) {
|
|
14
|
-
return manifest.sources;
|
|
15
|
-
}
|
|
16
|
-
if (metadata?.sourceRepos.length) {
|
|
17
|
-
return sourceManifestEntriesFromMetadata(metadata.sourceRepos, metadata.odooVersion);
|
|
18
|
-
}
|
|
19
|
-
return syncManifestFromMetadataAndGitmodules([], metadata?.odooVersion ?? defaultOdooVersion, await listGitmoduleSources(target));
|
|
20
|
-
}
|
|
21
|
-
export function renderSourceList(entries) {
|
|
22
|
-
if (entries.length === 0) {
|
|
23
|
-
return 'No source repositories configured.';
|
|
24
|
-
}
|
|
25
|
-
return entries
|
|
26
|
-
.map((entry) => {
|
|
27
|
-
const branch = entry.branch ? ` @ ${entry.branch}` : '';
|
|
28
|
-
const addons = entry.addons.length ? ` addons: ${entry.addons.join(', ')}` : '';
|
|
29
|
-
return `${entry.type}/${entry.path}${branch} -> ${entry.url}${addons}`;
|
|
30
|
-
})
|
|
31
|
-
.join('\n');
|
|
32
|
-
}
|
|
33
|
-
export function sourceListJson(entries) {
|
|
34
|
-
return {
|
|
35
|
-
schemaVersion: 1,
|
|
36
|
-
command: 'source list',
|
|
37
|
-
ok: true,
|
|
38
|
-
sources: cloneSourceEntries(entries),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export function sourceSyncJson(entries, target) {
|
|
42
|
-
return {
|
|
43
|
-
schemaVersion: 1,
|
|
44
|
-
command: 'source sync',
|
|
45
|
-
ok: true,
|
|
46
|
-
target,
|
|
47
|
-
sources: cloneSourceEntries(entries),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
export async function syncSources(options, git = realGit) {
|
|
51
|
-
const metadata = await readEnvironmentMetadata(options.target);
|
|
52
|
-
const manifest = await readSourceManifest(options.target);
|
|
53
|
-
const gitmodules = await listGitmoduleSources(options.target);
|
|
54
|
-
const fallbackBranch = metadata?.odooVersion ?? defaultOdooVersion;
|
|
55
|
-
const baseRepos = metadata?.sourceRepos.length ? metadata.sourceRepos : sourceReposFromManifest(manifest.sources);
|
|
56
|
-
const entries = syncManifestFromMetadataAndGitmodules(baseRepos, fallbackBranch, gitmodules);
|
|
57
|
-
await writeSourceManifest(options.target, entries);
|
|
58
|
-
if (metadata) {
|
|
59
|
-
await replaceSourceRepos(options.target, sourceReposFromManifest(entries));
|
|
60
|
-
}
|
|
61
|
-
if (options.stage) {
|
|
62
|
-
await stageAll(git, options.target);
|
|
63
|
-
}
|
|
64
|
-
return entries;
|
|
65
|
-
}
|
package/dist/source-manifest.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { isValidPathSegment, validateRepoPath } from './path-validation.js';
|
|
4
|
-
const validSourceTypes = ['private', 'oca', 'external'];
|
|
5
|
-
export const sourceManifestPath = 'odoo/custom/manifests/sources.yaml';
|
|
6
|
-
function fail(message) {
|
|
7
|
-
throw new Error(`Invalid source manifest ${sourceManifestPath}: ${message}`);
|
|
8
|
-
}
|
|
9
|
-
export function normalizeSourceType(value) {
|
|
10
|
-
return validSourceTypes.includes(value) ? value : 'private';
|
|
11
|
-
}
|
|
12
|
-
function dedupeAndSort(entries) {
|
|
13
|
-
const uniqueByTypePath = new Map();
|
|
14
|
-
for (const entry of entries) {
|
|
15
|
-
uniqueByTypePath.set(`${entry.type}:${entry.path}`, entry);
|
|
16
|
-
}
|
|
17
|
-
return [...uniqueByTypePath.values()].sort((left, right) => {
|
|
18
|
-
const typeOrder = left.type.localeCompare(right.type);
|
|
19
|
-
if (typeOrder !== 0)
|
|
20
|
-
return typeOrder;
|
|
21
|
-
return left.path.localeCompare(right.path);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
function stripInlineComment(raw) {
|
|
25
|
-
let inSingle = false;
|
|
26
|
-
let inDouble = false;
|
|
27
|
-
let escaped = false;
|
|
28
|
-
for (let index = 0; index < raw.length; index += 1) {
|
|
29
|
-
const char = raw[index];
|
|
30
|
-
if (escaped) {
|
|
31
|
-
escaped = false;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (char === '\\') {
|
|
35
|
-
escaped = true;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (char === "'" && !inDouble) {
|
|
39
|
-
inSingle = !inSingle;
|
|
40
|
-
}
|
|
41
|
-
else if (char === '"' && !inSingle) {
|
|
42
|
-
inDouble = !inDouble;
|
|
43
|
-
}
|
|
44
|
-
else if (char === '#' && !inSingle && !inDouble) {
|
|
45
|
-
return raw.slice(0, index).trimEnd();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return raw;
|
|
49
|
-
}
|
|
50
|
-
function parseScalar(raw) {
|
|
51
|
-
const trimmed = raw.trim();
|
|
52
|
-
if (!trimmed)
|
|
53
|
-
return '';
|
|
54
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
55
|
-
return JSON.parse(trimmed);
|
|
56
|
-
}
|
|
57
|
-
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
58
|
-
return trimmed.slice(1, -1).replace(/\\'/g, "'");
|
|
59
|
-
}
|
|
60
|
-
return trimmed;
|
|
61
|
-
}
|
|
62
|
-
function leadingSpaces(line) {
|
|
63
|
-
return line.length - line.trimStart().length;
|
|
64
|
-
}
|
|
65
|
-
function parseSourcesBlock(content) {
|
|
66
|
-
const lines = content.split(/\r?\n/).map((line, index) => ({
|
|
67
|
-
lineNumber: index + 1,
|
|
68
|
-
line: line.replace(/\t/g, ' '),
|
|
69
|
-
trimmedLine: line.replace(/\t/g, ' ').trim(),
|
|
70
|
-
}));
|
|
71
|
-
const sourcesKeywordLine = lines.find((line) => /^\s*sources\s*:\s*(?:\[[^\]]*\])?\s*$/.test(stripInlineComment(line.line)));
|
|
72
|
-
if (!sourcesKeywordLine) {
|
|
73
|
-
fail('Missing top-level sources entry.');
|
|
74
|
-
}
|
|
75
|
-
const rawSourcesValue = stripInlineComment(sourcesKeywordLine.line).replace(/^\s*sources\s*:\s*/, '');
|
|
76
|
-
if (rawSourcesValue === '[]') {
|
|
77
|
-
return { sources: [] };
|
|
78
|
-
}
|
|
79
|
-
if (rawSourcesValue && rawSourcesValue !== '') {
|
|
80
|
-
fail(`Unexpected non-list value on line ${sourcesKeywordLine.lineNumber}: sources`);
|
|
81
|
-
}
|
|
82
|
-
const sourceLines = lines.slice(sourcesKeywordLine.lineNumber);
|
|
83
|
-
const parsed = [];
|
|
84
|
-
let index = 0;
|
|
85
|
-
while (index < sourceLines.length) {
|
|
86
|
-
const headerLine = sourceLines[index];
|
|
87
|
-
const noCommentHeader = stripInlineComment(headerLine.line);
|
|
88
|
-
if (!noCommentHeader.trim()) {
|
|
89
|
-
index += 1;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const itemMatch = /^\s*-\s*type:\s*(.+)\s*$/.exec(noCommentHeader);
|
|
93
|
-
if (!itemMatch) {
|
|
94
|
-
index += 1;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const item = {
|
|
98
|
-
type: normalizeSourceType(parseScalar(itemMatch[1])),
|
|
99
|
-
path: '',
|
|
100
|
-
url: '',
|
|
101
|
-
addons: [],
|
|
102
|
-
};
|
|
103
|
-
index += 1;
|
|
104
|
-
while (index < sourceLines.length) {
|
|
105
|
-
const rawLine = sourceLines[index];
|
|
106
|
-
const noComment = stripInlineComment(rawLine.line);
|
|
107
|
-
const trimmed = noComment.trim();
|
|
108
|
-
if (!trimmed) {
|
|
109
|
-
index += 1;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (/^\s*-\s*type:\s*/.test(noComment)) {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
const pathMatch = /^\s*path:\s*(.+)\s*$/.exec(noComment);
|
|
116
|
-
if (pathMatch) {
|
|
117
|
-
item.path = validateRepoPath(parseScalar(pathMatch[1]));
|
|
118
|
-
index += 1;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const urlMatch = /^\s*url:\s*(.+)\s*$/.exec(noComment);
|
|
122
|
-
if (urlMatch) {
|
|
123
|
-
item.url = parseScalar(urlMatch[1]);
|
|
124
|
-
index += 1;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const branchMatch = /^\s*branch:\s*(.+)\s*$/.exec(noComment);
|
|
128
|
-
if (branchMatch) {
|
|
129
|
-
item.branch = parseScalar(branchMatch[1]);
|
|
130
|
-
index += 1;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const addonsLine = /^\s*addons:\s*$/.exec(noComment);
|
|
134
|
-
if (addonsLine) {
|
|
135
|
-
const baseIndent = leadingSpaces(rawLine.line) + 2;
|
|
136
|
-
index += 1;
|
|
137
|
-
while (index < sourceLines.length) {
|
|
138
|
-
const addonRaw = stripInlineComment(sourceLines[index].line);
|
|
139
|
-
const addonTrimmed = addonRaw.trim();
|
|
140
|
-
if (!addonTrimmed) {
|
|
141
|
-
index += 1;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const addonMatch = /^\s*-\s*(.+)\s*$/.exec(addonRaw);
|
|
145
|
-
if (!addonMatch) {
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
if (leadingSpaces(addonRaw) < baseIndent) {
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
const addon = parseScalar(addonMatch[1]);
|
|
152
|
-
if (addon) {
|
|
153
|
-
item.addons.push(addon);
|
|
154
|
-
}
|
|
155
|
-
index += 1;
|
|
156
|
-
}
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
fail(`Unexpected source entry field on line ${rawLine.lineNumber}: ${trimmed}`);
|
|
160
|
-
}
|
|
161
|
-
if (!item.path) {
|
|
162
|
-
fail(`Manifest entry missing path at line ${headerLine.lineNumber}`);
|
|
163
|
-
}
|
|
164
|
-
if (!item.url) {
|
|
165
|
-
fail(`Manifest entry missing url for ${item.type}:${item.path} at line ${headerLine.lineNumber}`);
|
|
166
|
-
}
|
|
167
|
-
if (!isValidPathSegment(item.path)) {
|
|
168
|
-
fail(`Invalid manifest path at line ${headerLine.lineNumber}: ${item.path}`);
|
|
169
|
-
}
|
|
170
|
-
if (item.addons.length === 0) {
|
|
171
|
-
item.addons.push(item.path);
|
|
172
|
-
}
|
|
173
|
-
item.addons = [...new Set(item.addons.map((addon) => validateRepoPath(addon)))].sort();
|
|
174
|
-
parsed.push(item);
|
|
175
|
-
}
|
|
176
|
-
return { sources: dedupeAndSort(parsed.filter((entry) => isValidPathSegment(entry.path))) };
|
|
177
|
-
}
|
|
178
|
-
export async function readSourceManifest(target) {
|
|
179
|
-
try {
|
|
180
|
-
const content = await readFile(join(target, sourceManifestPath), 'utf8');
|
|
181
|
-
return parseSourcesBlock(content);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
if (error.code === 'ENOENT') {
|
|
185
|
-
return { sources: [] };
|
|
186
|
-
}
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function renderQuoted(value) {
|
|
191
|
-
return JSON.stringify(value);
|
|
192
|
-
}
|
|
193
|
-
export function renderSourceManifest(entries) {
|
|
194
|
-
const normalized = dedupeAndSort(entries).map((entry) => {
|
|
195
|
-
const addons = [...new Set(entry.addons.map((addon) => validateRepoPath(addon)))].sort();
|
|
196
|
-
return {
|
|
197
|
-
type: entry.type,
|
|
198
|
-
path: validateRepoPath(entry.path),
|
|
199
|
-
url: entry.url.trim(),
|
|
200
|
-
branch: entry.branch?.trim(),
|
|
201
|
-
addons: addons.length ? addons : [validateRepoPath(entry.path)],
|
|
202
|
-
};
|
|
203
|
-
});
|
|
204
|
-
if (normalized.length === 0) {
|
|
205
|
-
return 'sources: []\n';
|
|
206
|
-
}
|
|
207
|
-
const body = normalized
|
|
208
|
-
.map((entry) => {
|
|
209
|
-
const lines = [
|
|
210
|
-
` - type: ${renderQuoted(entry.type)}`,
|
|
211
|
-
` path: ${renderQuoted(entry.path)}`,
|
|
212
|
-
` url: ${renderQuoted(entry.url)}`,
|
|
213
|
-
];
|
|
214
|
-
lines.push(` branch: ${renderQuoted(entry.branch ?? '')}`);
|
|
215
|
-
lines.push(' addons:');
|
|
216
|
-
for (const addon of entry.addons) {
|
|
217
|
-
lines.push(` - ${renderQuoted(addon)}`);
|
|
218
|
-
}
|
|
219
|
-
return lines.join('\n');
|
|
220
|
-
})
|
|
221
|
-
.join('\n');
|
|
222
|
-
return `sources:\n${body}\n`;
|
|
223
|
-
}
|
|
224
|
-
export async function writeSourceManifest(target, entries) {
|
|
225
|
-
const content = renderSourceManifest(entries);
|
|
226
|
-
const path = join(target, sourceManifestPath);
|
|
227
|
-
await mkdir(join(path, '..'), { recursive: true });
|
|
228
|
-
await writeFile(path, content, 'utf8');
|
|
229
|
-
}
|
|
230
|
-
function entryKey(type, path) {
|
|
231
|
-
return `${type}:${path}`;
|
|
232
|
-
}
|
|
233
|
-
export async function upsertSourceManifestEntry(target, entry) {
|
|
234
|
-
const manifest = await readSourceManifest(target);
|
|
235
|
-
const normalized = {
|
|
236
|
-
...entry,
|
|
237
|
-
type: normalizeSourceType(entry.type),
|
|
238
|
-
path: validateRepoPath(entry.path),
|
|
239
|
-
};
|
|
240
|
-
const next = dedupeAndSort(manifest.sources.filter((current) => entryKey(current.type, current.path) !== entryKey(normalized.type, normalized.path)));
|
|
241
|
-
next.push(normalized);
|
|
242
|
-
await writeSourceManifest(target, next);
|
|
243
|
-
}
|
|
244
|
-
export async function removeSourceManifestEntry(target, type, path) {
|
|
245
|
-
const manifest = await readSourceManifest(target);
|
|
246
|
-
const key = entryKey(normalizeSourceType(type), validateRepoPath(path));
|
|
247
|
-
const next = manifest.sources.filter((entry) => entryKey(entry.type, entry.path) !== key);
|
|
248
|
-
await writeSourceManifest(target, next);
|
|
249
|
-
}
|
|
250
|
-
export function sourceManifestEntriesFromMetadata(sourceRepos, fallbackBranch) {
|
|
251
|
-
return sourceRepos.map((repo) => ({
|
|
252
|
-
type: normalizeSourceType(repo.sourceType),
|
|
253
|
-
path: validateRepoPath(repo.path),
|
|
254
|
-
url: repo.url.trim(),
|
|
255
|
-
branch: fallbackBranch,
|
|
256
|
-
addons: repo.addons.length ? [...new Set(repo.addons.map((addon) => validateRepoPath(addon)))] : [validateRepoPath(repo.path)],
|
|
257
|
-
}));
|
|
258
|
-
}
|
|
259
|
-
export async function listGitmoduleSources(target) {
|
|
260
|
-
try {
|
|
261
|
-
const gitmodules = await readFile(join(target, '.gitmodules'), 'utf8');
|
|
262
|
-
const lines = gitmodules.split(/\r?\n/);
|
|
263
|
-
const locations = [];
|
|
264
|
-
const pathRegex = /^\s*path\s*=\s*odoo\/custom\/src\/(private|oca|external)\/(.+)\s*$/;
|
|
265
|
-
const urlRegex = /^\s*url\s*=\s*(.+)\s*$/;
|
|
266
|
-
let pending;
|
|
267
|
-
for (const line of lines) {
|
|
268
|
-
const parsedPath = line.match(pathRegex);
|
|
269
|
-
if (parsedPath) {
|
|
270
|
-
const sourceType = parsedPath[1];
|
|
271
|
-
const repoPath = parsedPath[2]?.trim() ?? '';
|
|
272
|
-
if (!repoPath || !isValidPathSegment(repoPath)) {
|
|
273
|
-
pending = undefined;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
pending = {
|
|
277
|
-
type: sourceType,
|
|
278
|
-
path: validateRepoPath(repoPath),
|
|
279
|
-
url: '',
|
|
280
|
-
};
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
const parsedUrl = line.match(urlRegex);
|
|
284
|
-
if (!parsedUrl || !pending) {
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const url = parseScalar(parsedUrl[1]);
|
|
288
|
-
if (url) {
|
|
289
|
-
locations.push({ ...pending, url });
|
|
290
|
-
}
|
|
291
|
-
pending = undefined;
|
|
292
|
-
}
|
|
293
|
-
return locations;
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
return [];
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
export function syncManifestFromMetadataAndGitmodules(sourceRepos, fallbackBranch, gitmodules = []) {
|
|
300
|
-
const byGitmodule = new Map();
|
|
301
|
-
for (const location of gitmodules) {
|
|
302
|
-
byGitmodule.set(`${normalizeSourceType(location.type)}:${location.path}`, location);
|
|
303
|
-
}
|
|
304
|
-
const entries = [];
|
|
305
|
-
for (const repo of sourceRepos) {
|
|
306
|
-
const normalized = {
|
|
307
|
-
type: normalizeSourceType(repo.sourceType),
|
|
308
|
-
path: validateRepoPath(repo.path),
|
|
309
|
-
url: repo.url.trim() || byGitmodule.get(`${normalizeSourceType(repo.sourceType)}:${repo.path}`)?.url || '',
|
|
310
|
-
branch: fallbackBranch,
|
|
311
|
-
addons: repo.addons.map(validateRepoPath),
|
|
312
|
-
};
|
|
313
|
-
entries.push(normalized);
|
|
314
|
-
}
|
|
315
|
-
for (const location of gitmodules) {
|
|
316
|
-
const key = `${location.type}:${location.path}`;
|
|
317
|
-
if (entries.some((entry) => `${entry.type}:${entry.path}` === key)) {
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
entries.push({
|
|
321
|
-
type: location.type,
|
|
322
|
-
path: location.path,
|
|
323
|
-
url: location.url,
|
|
324
|
-
branch: fallbackBranch,
|
|
325
|
-
addons: [location.path],
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
return dedupeAndSort(entries);
|
|
329
|
-
}
|
|
330
|
-
export function sourceReposFromManifest(entries) {
|
|
331
|
-
const normalized = dedupeAndSort(entries);
|
|
332
|
-
return normalized.map((entry) => ({
|
|
333
|
-
sourceType: entry.type,
|
|
334
|
-
path: validateRepoPath(entry.path),
|
|
335
|
-
url: entry.url,
|
|
336
|
-
addons: entry.addons.length ? [...new Set(entry.addons.map((addon) => validateRepoPath(addon)))] : [validateRepoPath(entry.path)],
|
|
337
|
-
}));
|
|
338
|
-
}
|