@wpmoo/toolkit 0.9.12 → 0.9.13
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/dist/scaffold.js +2 -1
- package/dist/templates.js +358 -5
- package/package.json +1 -1
package/dist/scaffold.js
CHANGED
|
@@ -4,7 +4,7 @@ import { applyExternalAsset, renderExternalAssetCommand, writeTextFile } from '.
|
|
|
4
4
|
import { markerPath, renderEnvironmentMetadata } from './environment.js';
|
|
5
5
|
import { plannedExternalAssetOptions, renderComposeEnvExample } from './external-templates.js';
|
|
6
6
|
import { cloneRepository, ensureSubmodule, ensureRemoteHasBranch, realGit, stageAll, syncSubmodules, } from './git.js';
|
|
7
|
-
import { renderAgents, renderAppstoreRelease, renderGitignore, renderMooDelegationScript, renderPlaceholder, renderReadme, } from './templates.js';
|
|
7
|
+
import { renderAgents, renderAppstoreRelease, renderGitignore, renderMooDelegationScript, renderPlaceholder, renderReadme, renderStatusScript, } from './templates.js';
|
|
8
8
|
import { validateAddonName, validateRepoPath } from './path-validation.js';
|
|
9
9
|
import { renderSourceManifest, sourceManifestEntriesFromMetadata } from './source-manifest.js';
|
|
10
10
|
function validateSourceRepo(repo) {
|
|
@@ -57,6 +57,7 @@ export function generatedFiles(options) {
|
|
|
57
57
|
const files = [
|
|
58
58
|
{ path: markerPath, content: renderEnvironmentMetadata(safeOptions) },
|
|
59
59
|
{ path: 'moo', content: renderMooDelegationScript(), mode: 0o755 },
|
|
60
|
+
{ path: 'scripts/status.sh', content: renderStatusScript(), mode: 0o755 },
|
|
60
61
|
{ path: '.gitignore', content: renderGitignore() },
|
|
61
62
|
{ path: 'README.md', content: renderReadme(safeOptions) },
|
|
62
63
|
{ path: 'AGENTS.md', content: renderAgents(safeOptions) },
|
package/dist/templates.js
CHANGED
|
@@ -225,8 +225,9 @@ exposes them through \`/mnt/wpmoo-addons\`.
|
|
|
225
225
|
|
|
226
226
|
\`./moo\` routes day-to-day service and module workflows to local scripts in
|
|
227
227
|
\`./scripts/\` (for example \`start\`, \`logs\`, \`update\`, \`test\`, \`snapshot\`).
|
|
228
|
-
\`./moo status\`
|
|
229
|
-
\`
|
|
228
|
+
\`./moo status\` runs local offline metadata checks without needing network access.
|
|
229
|
+
\`./moo doctor\` remains the package fallback command and runs via
|
|
230
|
+
\`npx --yes ${fallbackPackageSpec()} doctor\`.
|
|
230
231
|
|
|
231
232
|
### Start And Inspect Services
|
|
232
233
|
|
|
@@ -472,7 +473,10 @@ Daily commands:
|
|
|
472
473
|
install, update, test, resetdb, snapshot, restore-snapshot, lint, pot
|
|
473
474
|
|
|
474
475
|
Management commands:
|
|
475
|
-
|
|
476
|
+
source, add-repo, remove-repo, add-module, remove-module, reset, doctor
|
|
477
|
+
|
|
478
|
+
Local diagnostics:
|
|
479
|
+
status [--json]
|
|
476
480
|
|
|
477
481
|
Run ./moo <command> with invalid arguments to see command-specific usage.
|
|
478
482
|
HELP
|
|
@@ -629,7 +633,14 @@ case "$command" in
|
|
|
629
633
|
"--version"|"-v"|"version")
|
|
630
634
|
run_package_command "$@"
|
|
631
635
|
;;
|
|
632
|
-
"
|
|
636
|
+
"status")
|
|
637
|
+
shift
|
|
638
|
+
if [[ -x ./scripts/status.sh ]]; then
|
|
639
|
+
run_script ./scripts/status.sh "$@"
|
|
640
|
+
fi
|
|
641
|
+
run_package_command "$command" "$@"
|
|
642
|
+
;;
|
|
643
|
+
"create"|"add-repo"|"remove-repo"|"add-module"|"remove-module"|"source"|"reset"|"doctor")
|
|
633
644
|
run_package_command "$@"
|
|
634
645
|
;;
|
|
635
646
|
"start")
|
|
@@ -723,6 +734,347 @@ case "$command" in
|
|
|
723
734
|
esac
|
|
724
735
|
`;
|
|
725
736
|
}
|
|
737
|
+
export function renderStatusScript() {
|
|
738
|
+
return `#!/usr/bin/env bash
|
|
739
|
+
set -euo pipefail
|
|
740
|
+
|
|
741
|
+
script_dir="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")" && pwd)"
|
|
742
|
+
root_dir="$(cd -- "$script_dir/.." && pwd)"
|
|
743
|
+
cd "$root_dir"
|
|
744
|
+
|
|
745
|
+
node --input-type=module - "$@" <<'NODE'
|
|
746
|
+
import { access, readdir, readFile, stat } from 'node:fs/promises';
|
|
747
|
+
import { isAbsolute, join } from 'node:path';
|
|
748
|
+
|
|
749
|
+
const args = process.argv.slice(2);
|
|
750
|
+
if (!args.every((arg) => arg === '--json')) {
|
|
751
|
+
console.error('Usage: ./moo status [--json]');
|
|
752
|
+
process.exit(2);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const json = args.includes('--json');
|
|
756
|
+
const target = process.cwd();
|
|
757
|
+
const metadataPath = '.wpmoo/odoo.json';
|
|
758
|
+
const validSourceTypes = new Set(['private', 'oca', 'external']);
|
|
759
|
+
|
|
760
|
+
async function exists(path) {
|
|
761
|
+
try {
|
|
762
|
+
await access(path);
|
|
763
|
+
return true;
|
|
764
|
+
} catch {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function isDirectory(path) {
|
|
770
|
+
try {
|
|
771
|
+
return (await stat(path)).isDirectory();
|
|
772
|
+
} catch {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function isRecord(value) {
|
|
778
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function isValidPathSegment(value) {
|
|
782
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
783
|
+
return Boolean(
|
|
784
|
+
normalized &&
|
|
785
|
+
normalized !== '.' &&
|
|
786
|
+
normalized !== '..' &&
|
|
787
|
+
!normalized.includes('/') &&
|
|
788
|
+
!normalized.includes('\\\\') &&
|
|
789
|
+
!normalized.includes('\\0') &&
|
|
790
|
+
!normalized.includes(':') &&
|
|
791
|
+
!isAbsolute(normalized) &&
|
|
792
|
+
!/^[a-zA-Z]:/.test(normalized),
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function normalizeSourceType(sourceType) {
|
|
797
|
+
return typeof sourceType === 'string' && validSourceTypes.has(sourceType) ? sourceType : 'private';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function countModuleCandidates(root) {
|
|
801
|
+
if (!(await isDirectory(root))) return 0;
|
|
802
|
+
const stack = [root];
|
|
803
|
+
let count = 0;
|
|
804
|
+
|
|
805
|
+
while (stack.length > 0) {
|
|
806
|
+
const current = stack.pop();
|
|
807
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
808
|
+
let hasManifest = false;
|
|
809
|
+
|
|
810
|
+
for (const entry of entries) {
|
|
811
|
+
if (entry.isFile() && entry.name === '__manifest__.py') {
|
|
812
|
+
hasManifest = true;
|
|
813
|
+
} else if (entry.isDirectory()) {
|
|
814
|
+
stack.push(join(current, entry.name));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (hasManifest) count += 1;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return count;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function parseEnvContent(content) {
|
|
825
|
+
const values = new Map();
|
|
826
|
+
for (const rawLine of content.split(/\\r?\\n/)) {
|
|
827
|
+
const line = rawLine.trim();
|
|
828
|
+
if (!line || line.startsWith('#')) continue;
|
|
829
|
+
const separator = line.indexOf('=');
|
|
830
|
+
if (separator === -1) continue;
|
|
831
|
+
const key = line.slice(0, separator).trim();
|
|
832
|
+
let value = line.slice(separator + 1).trim();
|
|
833
|
+
if (
|
|
834
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
835
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
836
|
+
) {
|
|
837
|
+
value = value.slice(1, -1);
|
|
838
|
+
}
|
|
839
|
+
values.set(key, value);
|
|
840
|
+
}
|
|
841
|
+
return values;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function readEnvFile() {
|
|
845
|
+
if (!(await exists(join(target, '.env')))) return undefined;
|
|
846
|
+
return parseEnvContent(await readFile(join(target, '.env'), 'utf8'));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function selectedComposeEnvironment(env) {
|
|
850
|
+
const envName = env?.get('WPMOO_ENV')?.trim();
|
|
851
|
+
return envName || 'dev';
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function isValidComposeEnvironmentName(value) {
|
|
855
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function isValidOdooVersion(value) {
|
|
859
|
+
return /^\\d+\\.\\d+$/.test(value);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function compactOverlayError(envName, overlayFile) {
|
|
863
|
+
if (envName === 'dev') return 'Missing compact compose overlay: ' + overlayFile;
|
|
864
|
+
return 'Missing compact compose overlay for WPMOO_ENV=' + envName + ': ' + overlayFile;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function detectComposeLayout(odooVersion) {
|
|
868
|
+
const envName = selectedComposeEnvironment(await readEnvFile());
|
|
869
|
+
if (!isValidComposeEnvironmentName(envName)) {
|
|
870
|
+
return {
|
|
871
|
+
files: [],
|
|
872
|
+
missingFiles: [],
|
|
873
|
+
errors: ['Invalid WPMOO_ENV in .env: expected a simple compose overlay name, got ' + envName],
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const compactBase = 'compose.yaml';
|
|
878
|
+
const compactOverlay = 'compose/' + envName + '.yaml';
|
|
879
|
+
const hasCompactBase = await exists(join(target, compactBase));
|
|
880
|
+
const hasCompactOverlay = await exists(join(target, compactOverlay));
|
|
881
|
+
|
|
882
|
+
if (hasCompactBase && hasCompactOverlay) {
|
|
883
|
+
return { files: [compactBase, compactOverlay], missingFiles: [], errors: [] };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (hasCompactBase || hasCompactOverlay) {
|
|
887
|
+
const errors = [];
|
|
888
|
+
const missingFiles = [];
|
|
889
|
+
if (!hasCompactBase) {
|
|
890
|
+
missingFiles.push(compactBase);
|
|
891
|
+
errors.push('Missing compact compose base: ' + compactBase);
|
|
892
|
+
}
|
|
893
|
+
if (!hasCompactOverlay) {
|
|
894
|
+
missingFiles.push(compactOverlay);
|
|
895
|
+
errors.push(compactOverlayError(envName, compactOverlay));
|
|
896
|
+
}
|
|
897
|
+
return { files: [], missingFiles, errors };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!isValidOdooVersion(odooVersion)) {
|
|
901
|
+
return {
|
|
902
|
+
files: [],
|
|
903
|
+
missingFiles: [],
|
|
904
|
+
errors: ['Invalid Odoo version for compose file: ' + odooVersion],
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const legacyFile = 'docker-compose_' + odooVersion + '.yml';
|
|
909
|
+
if (await exists(join(target, legacyFile))) {
|
|
910
|
+
return { files: [legacyFile], missingFiles: [], errors: [] };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
files: [],
|
|
915
|
+
missingFiles: [legacyFile],
|
|
916
|
+
errors: ['Missing compose file: ' + legacyFile],
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function coreFileIssues(odooVersion) {
|
|
921
|
+
const missing = [];
|
|
922
|
+
for (const check of [
|
|
923
|
+
{ label: 'moo', path: 'moo' },
|
|
924
|
+
{ label: 'README.md', path: 'README.md' },
|
|
925
|
+
{ label: 'AGENTS.md', path: 'AGENTS.md' },
|
|
926
|
+
]) {
|
|
927
|
+
if (!(await exists(join(target, check.path)))) missing.push(check.label);
|
|
928
|
+
}
|
|
929
|
+
if (!(await isDirectory(join(target, 'scripts')))) missing.push('scripts/');
|
|
930
|
+
|
|
931
|
+
const composeLayout = await detectComposeLayout(odooVersion);
|
|
932
|
+
missing.push(...composeLayout.missingFiles);
|
|
933
|
+
return { missing, composeFiles: composeLayout.files, composeErrors: composeLayout.errors };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function summaryText(status) {
|
|
937
|
+
if (status.kind === 'no_environment') return 'No WPMoo environment detected.';
|
|
938
|
+
if (status.kind === 'invalid_metadata') return 'Environment metadata is invalid.';
|
|
939
|
+
const needsAttention =
|
|
940
|
+
status.missingCoreFiles.length > 0 ||
|
|
941
|
+
status.invalidSourceRepoPaths.length > 0 ||
|
|
942
|
+
status.composeErrors.length > 0;
|
|
943
|
+
const prefix = needsAttention ? 'Environment needs attention' : 'Environment ready';
|
|
944
|
+
return (
|
|
945
|
+
prefix +
|
|
946
|
+
': Odoo ' +
|
|
947
|
+
status.odooVersion +
|
|
948
|
+
', source repos ' +
|
|
949
|
+
status.sourceRepoCount +
|
|
950
|
+
', module candidates ' +
|
|
951
|
+
status.moduleCandidateCount +
|
|
952
|
+
'.'
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function isHealthy(status) {
|
|
957
|
+
return (
|
|
958
|
+
status.kind === 'environment' &&
|
|
959
|
+
status.missingCoreFiles.length === 0 &&
|
|
960
|
+
status.invalidSourceRepoPaths.length === 0 &&
|
|
961
|
+
status.composeErrors.length === 0
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function renderStatus(status) {
|
|
966
|
+
const lines = ['Status: ' + summaryText(status)];
|
|
967
|
+
if (status.kind === 'no_environment') {
|
|
968
|
+
lines.push('Metadata: missing ' + status.metadataPath);
|
|
969
|
+
lines.push('Next: ' + status.recommendedNextAction);
|
|
970
|
+
return lines.join('\\n');
|
|
971
|
+
}
|
|
972
|
+
if (status.kind === 'invalid_metadata') {
|
|
973
|
+
lines.push('Metadata: invalid ' + status.metadataPath);
|
|
974
|
+
lines.push('Error: ' + status.metadataError);
|
|
975
|
+
lines.push('Next: ' + status.recommendedNextAction);
|
|
976
|
+
return lines.join('\\n');
|
|
977
|
+
}
|
|
978
|
+
lines.push('Metadata: ' + status.metadataPath);
|
|
979
|
+
lines.push('Odoo: ' + status.odooVersion);
|
|
980
|
+
lines.push('Compose files: ' + (status.composeFiles.length > 0 ? status.composeFiles.join(', ') : '(missing)'));
|
|
981
|
+
if (status.composeErrors.length > 0) lines.push('Compose errors: ' + status.composeErrors.join(', '));
|
|
982
|
+
lines.push('Source repos: ' + status.sourceRepoCount);
|
|
983
|
+
lines.push('Source repo paths: ' + (status.sourceRepoPaths.length > 0 ? status.sourceRepoPaths.join(', ') : '(none configured)'));
|
|
984
|
+
if (status.invalidSourceRepoPaths.length > 0) {
|
|
985
|
+
lines.push('Invalid source repo paths: ' + status.invalidSourceRepoPaths.join(', '));
|
|
986
|
+
}
|
|
987
|
+
lines.push('Module candidates: ' + status.moduleCandidateCount);
|
|
988
|
+
lines.push('Missing core files: ' + (status.missingCoreFiles.length > 0 ? status.missingCoreFiles.join(', ') : '(none)'));
|
|
989
|
+
lines.push('Next: ' + status.recommendedNextAction);
|
|
990
|
+
return lines.join('\\n');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function getStatus() {
|
|
994
|
+
if (!(await exists(join(target, metadataPath)))) {
|
|
995
|
+
return {
|
|
996
|
+
kind: 'no_environment',
|
|
997
|
+
target,
|
|
998
|
+
metadataPath,
|
|
999
|
+
recommendedNextAction: 'Run npx @wpmoo/toolkit create ...',
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let metadata;
|
|
1004
|
+
try {
|
|
1005
|
+
const parsed = JSON.parse(await readFile(join(target, metadataPath), 'utf8'));
|
|
1006
|
+
if (!isRecord(parsed)) throw new Error('metadata is not an object');
|
|
1007
|
+
metadata = parsed;
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
return {
|
|
1010
|
+
kind: 'invalid_metadata',
|
|
1011
|
+
target,
|
|
1012
|
+
metadataPath,
|
|
1013
|
+
metadataError: error instanceof Error ? error.message : String(error),
|
|
1014
|
+
recommendedNextAction: 'Fix .wpmoo/odoo.json or run ./moo reset from a valid environment.',
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const odooVersion =
|
|
1019
|
+
typeof metadata.odooVersion === 'string' && metadata.odooVersion.trim() ? metadata.odooVersion.trim() : '19.0';
|
|
1020
|
+
const sourceRepoPaths = [];
|
|
1021
|
+
const sourceRepoLocations = [];
|
|
1022
|
+
const invalidSourceRepoPaths = [];
|
|
1023
|
+
|
|
1024
|
+
for (const repo of Array.isArray(metadata.sourceRepos) ? metadata.sourceRepos : []) {
|
|
1025
|
+
const path = isRecord(repo) && typeof repo.path === 'string' ? repo.path.trim() : '';
|
|
1026
|
+
if (!path) continue;
|
|
1027
|
+
if (!isValidPathSegment(path)) {
|
|
1028
|
+
invalidSourceRepoPaths.push(path);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const sourceType = normalizeSourceType(isRecord(repo) ? repo.sourceType : undefined);
|
|
1032
|
+
sourceRepoPaths.push(path);
|
|
1033
|
+
sourceRepoLocations.push({ sourceType, path });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
let moduleCandidateCount = 0;
|
|
1037
|
+
for (const repo of sourceRepoLocations) {
|
|
1038
|
+
moduleCandidateCount += await countModuleCandidates(join(target, 'odoo/custom/src', repo.sourceType, repo.path));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const { missing, composeFiles, composeErrors } = await coreFileIssues(odooVersion);
|
|
1042
|
+
let recommendedNextAction = 'Run ./moo doctor for deep checks or ./moo start.';
|
|
1043
|
+
if (invalidSourceRepoPaths.length > 0) {
|
|
1044
|
+
recommendedNextAction = 'Fix invalid source repo paths in .wpmoo/odoo.json, then run ./moo doctor.';
|
|
1045
|
+
} else if (missing.length > 0) {
|
|
1046
|
+
recommendedNextAction = 'Run ./moo reset, then ./moo doctor.';
|
|
1047
|
+
} else if (composeErrors.length > 0) {
|
|
1048
|
+
recommendedNextAction = 'Fix compose layout errors, then run ./moo doctor.';
|
|
1049
|
+
} else if (sourceRepoPaths.length === 0) {
|
|
1050
|
+
recommendedNextAction = 'Run ./moo add-repo ...';
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return {
|
|
1054
|
+
kind: 'environment',
|
|
1055
|
+
target,
|
|
1056
|
+
metadataPath,
|
|
1057
|
+
odooVersion,
|
|
1058
|
+
sourceRepoCount: sourceRepoPaths.length,
|
|
1059
|
+
sourceRepoPaths,
|
|
1060
|
+
invalidSourceRepoPaths,
|
|
1061
|
+
moduleCandidateCount,
|
|
1062
|
+
composeFiles,
|
|
1063
|
+
composeErrors,
|
|
1064
|
+
missingCoreFiles: missing,
|
|
1065
|
+
recommendedNextAction,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const status = await getStatus();
|
|
1070
|
+
if (json) {
|
|
1071
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: 'status', ok: isHealthy(status), status }, null, 2));
|
|
1072
|
+
} else {
|
|
1073
|
+
console.log(renderStatus(status));
|
|
1074
|
+
}
|
|
1075
|
+
NODE
|
|
1076
|
+
`;
|
|
1077
|
+
}
|
|
726
1078
|
export function renderAddonsYaml(options) {
|
|
727
1079
|
return `# Addons activated from source submodules.
|
|
728
1080
|
#
|
|
@@ -876,7 +1228,8 @@ Useful maintenance commands:
|
|
|
876
1228
|
|
|
877
1229
|
Daily script delegation vs package fallback:
|
|
878
1230
|
- \`./moo start\`, \`logs\`, \`install\`, \`update\`, \`test\`, \`snapshot\`, and related runtime tasks delegate to local \`./scripts/*.sh\`.
|
|
879
|
-
- \`./moo status\`
|
|
1231
|
+
- \`./moo status\` runs local offline metadata checks through \`./scripts/status.sh\`.
|
|
1232
|
+
- \`./moo doctor\` remains a package fallback command routed to \`npx --yes ${fallbackPackageSpec()} doctor\`.
|
|
880
1233
|
|
|
881
1234
|
Only report completion after the relevant update/test/lint command exits cleanly.
|
|
882
1235
|
`;
|