@wpmoo/toolkit 0.9.11 → 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 +390 -4
- 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
|
|
|
@@ -463,6 +464,24 @@ usage() {
|
|
|
463
464
|
esac
|
|
464
465
|
}
|
|
465
466
|
|
|
467
|
+
show_help() {
|
|
468
|
+
cat <<'HELP'
|
|
469
|
+
Usage: ./moo <command> [args]
|
|
470
|
+
|
|
471
|
+
Daily commands:
|
|
472
|
+
start, stop, logs, restart, shell, psql
|
|
473
|
+
install, update, test, resetdb, snapshot, restore-snapshot, lint, pot
|
|
474
|
+
|
|
475
|
+
Management commands:
|
|
476
|
+
source, add-repo, remove-repo, add-module, remove-module, reset, doctor
|
|
477
|
+
|
|
478
|
+
Local diagnostics:
|
|
479
|
+
status [--json]
|
|
480
|
+
|
|
481
|
+
Run ./moo <command> with invalid arguments to see command-specific usage.
|
|
482
|
+
HELP
|
|
483
|
+
}
|
|
484
|
+
|
|
466
485
|
fail_usage() {
|
|
467
486
|
usage "$1" >&2
|
|
468
487
|
exit 2
|
|
@@ -599,8 +618,31 @@ run_script() {
|
|
|
599
618
|
exec "$script" "$@"
|
|
600
619
|
}
|
|
601
620
|
|
|
621
|
+
run_package_command() {
|
|
622
|
+
exec npx --yes ${fallbackPackageSpec()} "$@"
|
|
623
|
+
}
|
|
624
|
+
|
|
602
625
|
command="\${1:-}"
|
|
603
626
|
case "$command" in
|
|
627
|
+
"")
|
|
628
|
+
run_package_command "$@"
|
|
629
|
+
;;
|
|
630
|
+
"--help"|"-h"|"help")
|
|
631
|
+
show_help
|
|
632
|
+
;;
|
|
633
|
+
"--version"|"-v"|"version")
|
|
634
|
+
run_package_command "$@"
|
|
635
|
+
;;
|
|
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")
|
|
644
|
+
run_package_command "$@"
|
|
645
|
+
;;
|
|
604
646
|
"start")
|
|
605
647
|
shift
|
|
606
648
|
require_no_args "$command" "$@"
|
|
@@ -685,11 +727,354 @@ case "$command" in
|
|
|
685
727
|
run_script ./scripts/pot.sh "$@"
|
|
686
728
|
;;
|
|
687
729
|
*)
|
|
688
|
-
|
|
730
|
+
echo "Unknown ./moo command: $command" >&2
|
|
731
|
+
echo "Run ./moo --help to see supported commands." >&2
|
|
732
|
+
exit 2
|
|
689
733
|
;;
|
|
690
734
|
esac
|
|
691
735
|
`;
|
|
692
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
|
+
}
|
|
693
1078
|
export function renderAddonsYaml(options) {
|
|
694
1079
|
return `# Addons activated from source submodules.
|
|
695
1080
|
#
|
|
@@ -843,7 +1228,8 @@ Useful maintenance commands:
|
|
|
843
1228
|
|
|
844
1229
|
Daily script delegation vs package fallback:
|
|
845
1230
|
- \`./moo start\`, \`logs\`, \`install\`, \`update\`, \`test\`, \`snapshot\`, and related runtime tasks delegate to local \`./scripts/*.sh\`.
|
|
846
|
-
- \`./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\`.
|
|
847
1233
|
|
|
848
1234
|
Only report completion after the relevant update/test/lint command exits cleanly.
|
|
849
1235
|
`;
|