multimodel-dev-os 3.0.1 ā 3.2.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/README.md +4 -0
- package/bin/multimodel-dev-os.js +3419 -3573
- package/docs/.vitepress/config.js +2 -2
- package/docs/index.md +5 -5
- package/docs/npm-publishing.md +5 -5
- package/docs/package-safety.md +24 -0
- package/docs/public/llms-full.txt +1 -1
- package/docs/public/llms.txt +1 -1
- package/docs/public/sitemap.xml +10 -0
- package/docs/registry-policy.md +4 -0
- package/docs/registry-security.md +7 -0
- package/docs/registry-sync.md +6 -0
- package/docs/release-policy.md +6 -5
- package/docs/testing.md +133 -0
- package/docs/trusted-registries.md +4 -0
- package/docs/v3-roadmap.md +20 -2
- package/package.json +10 -3
- package/scripts/build-cli.js +59 -0
- package/scripts/check-build-fresh.js +52 -0
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/verify.js +221 -14
- package/scripts/verify.sh +11 -1
- package/src/catalog/loader.js +117 -0
- package/src/cli/args.js +118 -0
- package/src/cli/help.js +60 -0
- package/src/cli/main.js +5718 -0
- package/src/core/globals.js +52 -0
- package/src/core/hashes.js +15 -0
- package/src/core/policy.js +36 -0
- package/src/core/security.js +61 -0
- package/src/core/yaml.js +136 -0
- package/src/plugin/manifest.js +95 -0
- package/src/registry/sources.js +40 -0
- package/src/registry/validation.js +45 -0
- package/tests/README.md +37 -0
- package/tests/fixtures/README.md +22 -0
- package/tests/fixtures/custom-template-example/README.md +10 -0
- package/tests/fixtures/proposals/approved-append-line.md +28 -0
- package/tests/fixtures/proposals/approved-create-file.md +29 -0
- package/tests/fixtures/proposals/approved-replace-text.md +30 -0
- package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
- package/tests/fixtures/proposals/no-operations.md +18 -0
- package/tests/fixtures/proposals/path-traversal.md +29 -0
- package/tests/fixtures/proposals/pending-proposal.md +29 -0
- package/tests/fixtures/proposals/protected-path.md +29 -0
- package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
- package/tests/fixtures/registry-overrides/README.md +20 -0
- package/tests/smoke/README.md +37 -0
- package/tests/smoke/cli-smoke.md +49 -0
- package/tests/unit/build-output.test.js +40 -0
- package/tests/unit/catalog-loader.test.js +44 -0
- package/tests/unit/path-safety.test.js +62 -0
- package/tests/unit/plugin-manifest.test.js +94 -0
- package/tests/unit/prepublish-guard.test.js +35 -0
- package/tests/unit/registry-policy.test.js +46 -0
- package/tests/unit/registry-url-validation.test.js +64 -0
- package/tests/unit/yaml.test.js +92 -0
- package/docs/testing-v0.2.md +0 -73
package/scripts/verify.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Runs on Windows, macOS, and Linux with zero external dependencies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, statSync, readdirSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
10
10
|
import { join, resolve, dirname } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { execSync } from 'child_process';
|
|
@@ -189,6 +189,21 @@ checkFile('scripts/pack-template.sh');
|
|
|
189
189
|
checkFile('scripts/prepublish-guard.js');
|
|
190
190
|
checkFile('bin/multimodel-dev-os.js');
|
|
191
191
|
|
|
192
|
+
// --- Modular Source Files ---
|
|
193
|
+
console.log('\nModular Source Files:');
|
|
194
|
+
checkFile('src/cli/main.js');
|
|
195
|
+
checkFile('src/cli/args.js');
|
|
196
|
+
checkFile('src/cli/help.js');
|
|
197
|
+
checkFile('src/core/yaml.js');
|
|
198
|
+
checkFile('src/core/hashes.js');
|
|
199
|
+
checkFile('src/core/policy.js');
|
|
200
|
+
checkFile('src/core/security.js');
|
|
201
|
+
checkFile('src/core/globals.js');
|
|
202
|
+
checkFile('src/registry/validation.js');
|
|
203
|
+
checkFile('src/registry/sources.js');
|
|
204
|
+
checkFile('src/catalog/loader.js');
|
|
205
|
+
checkFile('src/plugin/manifest.js');
|
|
206
|
+
|
|
192
207
|
// --- GitHub Integration ---
|
|
193
208
|
console.log('\nGitHub Workflows:');
|
|
194
209
|
checkFile('.github/workflows/verify.yml');
|
|
@@ -203,7 +218,7 @@ checkFile('docs/adapters.md');
|
|
|
203
218
|
checkFile('docs/installers.md');
|
|
204
219
|
checkFile('docs/cli-roadmap.md');
|
|
205
220
|
checkFile('docs/faq.md');
|
|
206
|
-
checkFile('docs/testing
|
|
221
|
+
checkFile('docs/testing.md');
|
|
207
222
|
checkFile('docs/npm-publishing.md');
|
|
208
223
|
checkFile('docs/templates-guide.md');
|
|
209
224
|
checkFile('docs/protocol.md');
|
|
@@ -549,7 +564,7 @@ try {
|
|
|
549
564
|
}
|
|
550
565
|
}
|
|
551
566
|
|
|
552
|
-
// Test 2: Allows version 3.0
|
|
567
|
+
// Test 2: Allows version 3.2.0 with MMDO_ALLOW_PUBLISH=true
|
|
553
568
|
try {
|
|
554
569
|
const output = execSync('node scripts/prepublish-guard.js', {
|
|
555
570
|
cwd: projectRoot,
|
|
@@ -557,7 +572,7 @@ try {
|
|
|
557
572
|
encoding: 'utf8'
|
|
558
573
|
});
|
|
559
574
|
if (output.includes('Prepublish guard passed')) {
|
|
560
|
-
console.log(` ${GREEN}ā${NC} prepublish guard allows version 3.0
|
|
575
|
+
console.log(` ${GREEN}ā${NC} prepublish guard allows version 3.2.0 when MMDO_ALLOW_PUBLISH=true`);
|
|
561
576
|
pass++;
|
|
562
577
|
} else {
|
|
563
578
|
console.error(` ${RED}ā${NC} prepublish guard passed but stdout missing success indicator`);
|
|
@@ -565,7 +580,7 @@ try {
|
|
|
565
580
|
}
|
|
566
581
|
} catch (err) {
|
|
567
582
|
const errText = err.stderr ? err.stderr.toString() : '';
|
|
568
|
-
console.error(` ${RED}ā${NC} prepublish guard blocked version 3.0
|
|
583
|
+
console.error(` ${RED}ā${NC} prepublish guard blocked version 3.2.0: ${errText || err.message}`);
|
|
569
584
|
fail++;
|
|
570
585
|
}
|
|
571
586
|
|
|
@@ -579,12 +594,12 @@ try {
|
|
|
579
594
|
pass++;
|
|
580
595
|
}
|
|
581
596
|
|
|
582
|
-
// Test 4: Package.json version is exactly 3.0
|
|
583
|
-
if (expectedVersion === '3.0
|
|
584
|
-
console.log(` ${GREEN}ā${NC} package.json version is exactly 3.0
|
|
597
|
+
// Test 4: Package.json version is exactly 3.2.0
|
|
598
|
+
if (expectedVersion === '3.2.0') {
|
|
599
|
+
console.log(` ${GREEN}ā${NC} package.json version is exactly 3.2.0`);
|
|
585
600
|
pass++;
|
|
586
601
|
} else {
|
|
587
|
-
console.error(` ${RED}ā${NC} package.json version is not 3.0
|
|
602
|
+
console.error(` ${RED}ā${NC} package.json version is not 3.2.0 (found ${expectedVersion})`);
|
|
588
603
|
fail++;
|
|
589
604
|
}
|
|
590
605
|
} catch (e) {
|
|
@@ -592,6 +607,52 @@ try {
|
|
|
592
607
|
fail++;
|
|
593
608
|
}
|
|
594
609
|
|
|
610
|
+
// --- Post-build Generated CLI Checks ---
|
|
611
|
+
console.log('\nPost-build Generated CLI Checks:');
|
|
612
|
+
try {
|
|
613
|
+
// 0. Check build freshness
|
|
614
|
+
try {
|
|
615
|
+
execSync('node scripts/check-build-fresh.js', { cwd: projectRoot, stdio: 'ignore' });
|
|
616
|
+
console.log(` ${GREEN}ā${NC} generated bin matches current source layout`);
|
|
617
|
+
pass++;
|
|
618
|
+
} catch (err) {
|
|
619
|
+
console.error(` ${RED}ā${NC} generated bin is stale! Run 'npm run build' and commit bin/multimodel-dev-os.js`);
|
|
620
|
+
fail++;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const buildPath = join(projectRoot, 'bin', 'multimodel-dev-os.js');
|
|
624
|
+
const binContent = readFileSync(buildPath, 'utf8');
|
|
625
|
+
|
|
626
|
+
const totalShebangs = (binContent.match(/#!/g) || []).length;
|
|
627
|
+
if (binContent.startsWith('#!/usr/bin/env node') && totalShebangs === 1) {
|
|
628
|
+
console.log(` ${GREEN}ā${NC} generated bin has exactly one shebang at the top`);
|
|
629
|
+
pass++;
|
|
630
|
+
} else {
|
|
631
|
+
console.error(` ${RED}ā${NC} generated bin has invalid shebang layout (count: ${totalShebangs})`);
|
|
632
|
+
fail++;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (binContent.includes('// Generated from src/. Do not edit directly.')) {
|
|
636
|
+
console.log(` ${GREEN}ā${NC} generated bin has warning header`);
|
|
637
|
+
pass++;
|
|
638
|
+
} else {
|
|
639
|
+
console.error(` ${RED}ā${NC} generated bin is missing the warning header`);
|
|
640
|
+
fail++;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const hasUnsafeSync = binContent.includes("mod.get('${targetUrl}'") || (binContent.includes('execSync(`node -e "') && binContent.includes('${targetUrl}'));
|
|
644
|
+
if (!hasUnsafeSync && binContent.includes('execFileSync(process.execPath')) {
|
|
645
|
+
console.log(` ${GREEN}ā${NC} generated bin is free of unsafe URL interpolation and uses execFileSync`);
|
|
646
|
+
pass++;
|
|
647
|
+
} else {
|
|
648
|
+
console.error(` ${RED}ā${NC} generated bin fails safety scan (unsafe interpolation found)`);
|
|
649
|
+
fail++;
|
|
650
|
+
}
|
|
651
|
+
} catch (e) {
|
|
652
|
+
console.error(` ${RED}ā${NC} post-build generated CLI checks failed: ${e.message}`);
|
|
653
|
+
fail++;
|
|
654
|
+
}
|
|
655
|
+
|
|
595
656
|
// --- v2.8.0 / v2.8.1 Dashboard & Plugin Tests ---
|
|
596
657
|
console.log('\nRunning TUI Dashboard & Plugin Pre-Flight Tests...');
|
|
597
658
|
|
|
@@ -921,22 +982,77 @@ try {
|
|
|
921
982
|
fail++;
|
|
922
983
|
}
|
|
923
984
|
|
|
924
|
-
// Verify npm pack dry-run shows current version dynamically
|
|
985
|
+
// Verify npm pack dry-run shows current version dynamically and has clean hygiene
|
|
925
986
|
try {
|
|
926
|
-
const packOutput = execSync('npm pack --dry-run', { cwd: projectRoot, encoding: 'utf8'
|
|
927
|
-
|
|
987
|
+
const packOutput = execSync('npm pack --dry-run 2>&1', { cwd: projectRoot, encoding: 'utf8' });
|
|
988
|
+
const combinedOutput = packOutput;
|
|
989
|
+
|
|
990
|
+
const hasVersion = combinedOutput.includes(`multimodel-dev-os@${expectedVersion}`) || combinedOutput.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || combinedOutput.includes(`version: ${expectedVersion}`);
|
|
991
|
+
if (hasVersion) {
|
|
928
992
|
console.log(` ${GREEN}ā${NC} npm pack --dry-run reports version ${expectedVersion}`);
|
|
929
993
|
pass++;
|
|
930
994
|
} else {
|
|
931
|
-
console.error(` ${RED}ā${NC} npm pack --dry-run did not report ${expectedVersion} in
|
|
995
|
+
console.error(` ${RED}ā${NC} npm pack --dry-run did not report ${expectedVersion} in output`);
|
|
996
|
+
fail++;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Hygiene checks
|
|
1000
|
+
const lines = combinedOutput.split('\n');
|
|
1001
|
+
const files = lines
|
|
1002
|
+
.filter(l => l.includes('npm notice') && !l.includes('Tarball Details') && !l.includes('Tarball Filename') && !l.includes('package size:') && !l.includes('unpacked size:') && !l.includes('shasum:') && !l.includes('integrity:') && !l.includes('total files:'))
|
|
1003
|
+
.map(l => {
|
|
1004
|
+
const match = l.match(/npm notice\s+\d+(\.\d+)?[a-zA-Z]+\s+(.+)$/);
|
|
1005
|
+
return match ? match[2].trim() : '';
|
|
1006
|
+
})
|
|
1007
|
+
.filter(f => f !== '');
|
|
1008
|
+
|
|
1009
|
+
const hasSrc = files.some(f => f.startsWith('src/'));
|
|
1010
|
+
const hasTests = files.some(f => f.startsWith('tests/'));
|
|
1011
|
+
|
|
1012
|
+
if (hasSrc && hasTests) {
|
|
1013
|
+
console.log(` ${GREEN}ā${NC} npm pack includes 'src/' and 'tests/' directories`);
|
|
1014
|
+
pass++;
|
|
1015
|
+
} else {
|
|
1016
|
+
console.error(` ${RED}ā${NC} npm pack is missing 'src/' or 'tests/' directory`);
|
|
1017
|
+
fail++;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const hasBlacklisted = files.some(f => f.includes('.npmrc') || f.includes('.env') || f.includes('node_modules') || f.endsWith('.tgz') || f.includes('coverage/'));
|
|
1021
|
+
if (!hasBlacklisted) {
|
|
1022
|
+
console.log(` ${GREEN}ā${NC} npm pack excludes sensitive and temporary files (.npmrc, .env, node_modules, .tgz, coverage)`);
|
|
1023
|
+
pass++;
|
|
1024
|
+
} else {
|
|
1025
|
+
console.error(` ${RED}ā${NC} npm pack contains blacklisted files!`);
|
|
932
1026
|
fail++;
|
|
933
1027
|
}
|
|
934
1028
|
} catch (e) {
|
|
935
1029
|
const stdErrOut = e.stderr ? e.stderr.toString() : '';
|
|
936
1030
|
const stdOutOut = e.stdout ? e.stdout.toString() : '';
|
|
937
|
-
|
|
1031
|
+
const combined = stdErrOut + '\n' + stdOutOut;
|
|
1032
|
+
|
|
1033
|
+
const hasVersion = combined.includes(`multimodel-dev-os@${expectedVersion}`) || combined.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || combined.includes(`version: ${expectedVersion}`);
|
|
1034
|
+
if (hasVersion) {
|
|
938
1035
|
console.log(` ${GREEN}ā${NC} npm pack --dry-run reports version ${expectedVersion}`);
|
|
939
1036
|
pass++;
|
|
1037
|
+
|
|
1038
|
+
const hasSrc = combined.includes('src/') || combined.includes('src\\');
|
|
1039
|
+
const hasTests = combined.includes('tests/') || combined.includes('tests\\');
|
|
1040
|
+
if (hasSrc && hasTests) {
|
|
1041
|
+
console.log(` ${GREEN}ā${NC} npm pack includes 'src/' and 'tests/' directories`);
|
|
1042
|
+
pass++;
|
|
1043
|
+
} else {
|
|
1044
|
+
console.error(` ${RED}ā${NC} npm pack is missing 'src/' or 'tests/' directory`);
|
|
1045
|
+
fail++;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const hasBlacklisted = combined.includes('.npmrc') || combined.includes('.env') || combined.includes('node_modules') || combined.includes('.tgz') || combined.includes('coverage/');
|
|
1049
|
+
if (!hasBlacklisted) {
|
|
1050
|
+
console.log(` ${GREEN}ā${NC} npm pack excludes sensitive and temporary files`);
|
|
1051
|
+
pass++;
|
|
1052
|
+
} else {
|
|
1053
|
+
console.error(` ${RED}ā${NC} npm pack contains blacklisted files!`);
|
|
1054
|
+
fail++;
|
|
1055
|
+
}
|
|
940
1056
|
} else {
|
|
941
1057
|
console.error(` ${RED}ā${NC} npm pack --dry-run failed or did not report ${expectedVersion}: ${e.message}`);
|
|
942
1058
|
fail++;
|
|
@@ -1127,6 +1243,97 @@ try {
|
|
|
1127
1243
|
fail++;
|
|
1128
1244
|
}
|
|
1129
1245
|
|
|
1246
|
+
// Security Hotfix v3.0.2 Regression checks
|
|
1247
|
+
console.log('\nSecurity Hotfix v3.0.2 Regression checks:');
|
|
1248
|
+
|
|
1249
|
+
const tempPolicyDir = join(projectRoot, 'temp-verify-policy');
|
|
1250
|
+
const tempPolicySubdir = join(tempPolicyDir, '.ai', 'policies');
|
|
1251
|
+
const tempPolicyFile = join(tempPolicySubdir, 'registry-policy.yaml');
|
|
1252
|
+
|
|
1253
|
+
try {
|
|
1254
|
+
// Create temporary policy directory and file
|
|
1255
|
+
mkdirSync(tempPolicySubdir, { recursive: true });
|
|
1256
|
+
writeFileSync(tempPolicyFile, 'allow_remote_registries: true\n', 'utf8');
|
|
1257
|
+
|
|
1258
|
+
// 1. registry add rejects malformed URL
|
|
1259
|
+
try {
|
|
1260
|
+
execSync(`node bin/multimodel-dev-os.js registry add testmalformed not-a-url --approved --target "${tempPolicyDir}"`, { cwd: projectRoot, stdio: 'pipe' });
|
|
1261
|
+
console.error(` ${RED}ā${NC} registry add should have rejected malformed URL`);
|
|
1262
|
+
fail++;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
const errText = err.stderr ? err.stderr.toString() : '';
|
|
1265
|
+
if (errText.includes('invalid') || errText.includes('malformed')) {
|
|
1266
|
+
console.log(` ${GREEN}ā${NC} registry add rejects malformed URL`);
|
|
1267
|
+
pass++;
|
|
1268
|
+
} else {
|
|
1269
|
+
console.error(` ${RED}ā${NC} registry add malformed URL failed with unexpected error: ${errText}`);
|
|
1270
|
+
fail++;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// 2. registry add rejects URL containing quote/shell-injection characters
|
|
1275
|
+
try {
|
|
1276
|
+
execSync(`node bin/multimodel-dev-os.js registry add testinjection "https://example.com'console.log(1)" --approved --target "${tempPolicyDir}"`, { cwd: projectRoot, stdio: 'pipe' });
|
|
1277
|
+
console.error(` ${RED}ā${NC} registry add should have rejected URL containing single quote`);
|
|
1278
|
+
fail++;
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
const errText = err.stderr ? err.stderr.toString() : '';
|
|
1281
|
+
if (errText.includes('quote') || errText.includes('invalid') || errText.includes('metacharacter')) {
|
|
1282
|
+
console.log(` ${GREEN}ā${NC} registry add rejects URL containing quote/shell-injection characters`);
|
|
1283
|
+
pass++;
|
|
1284
|
+
} else {
|
|
1285
|
+
console.error(` ${RED}ā${NC} registry add URL with quotes failed with unexpected error: ${errText}`);
|
|
1286
|
+
fail++;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// 3. registry add rejects non-HTTPS remote URL
|
|
1291
|
+
try {
|
|
1292
|
+
execSync(`node bin/multimodel-dev-os.js registry add testnonhttps http://example.com/catalog.yaml --approved --target "${tempPolicyDir}"`, { cwd: projectRoot, stdio: 'pipe' });
|
|
1293
|
+
console.error(` ${RED}ā${NC} registry add should have rejected non-HTTPS URL`);
|
|
1294
|
+
fail++;
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
const errText = err.stderr ? err.stderr.toString() : '';
|
|
1297
|
+
if (errText.includes('Only HTTPS is permitted') || errText.includes('protocol') || errText.includes('invalid')) {
|
|
1298
|
+
console.log(` ${GREEN}ā${NC} registry add rejects non-HTTPS remote URL`);
|
|
1299
|
+
pass++;
|
|
1300
|
+
} else {
|
|
1301
|
+
console.error(` ${RED}ā${NC} registry add non-HTTPS URL failed with unexpected error: ${errText}`);
|
|
1302
|
+
fail++;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
} catch (tempErr) {
|
|
1306
|
+
console.error(` ${RED}ā${NC} Setting up temporary policy folder failed: ${tempErr.message}`);
|
|
1307
|
+
fail++;
|
|
1308
|
+
} finally {
|
|
1309
|
+
// Clean up temporary policy directory
|
|
1310
|
+
try {
|
|
1311
|
+
if (existsSync(tempPolicyDir)) {
|
|
1312
|
+
rmSync(tempPolicyDir, { recursive: true, force: true });
|
|
1313
|
+
}
|
|
1314
|
+
} catch (e) {}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// 4. Codebase structural checks for shell-based fetch URL interpolation
|
|
1318
|
+
try {
|
|
1319
|
+
const cliCode = readFileSync(join(projectRoot, 'bin', 'multimodel-dev-os.js'), 'utf8');
|
|
1320
|
+
|
|
1321
|
+
// Check for mod.get('${targetUrl}') or similar interpolation in node -e
|
|
1322
|
+
const hasUnsafeSync = cliCode.includes("mod.get('${targetUrl}'") || (cliCode.includes('execSync(`node -e "') && cliCode.includes('${targetUrl}'));
|
|
1323
|
+
const usesExecFileSync = cliCode.includes('execFileSync(process.execPath');
|
|
1324
|
+
|
|
1325
|
+
if (!hasUnsafeSync && usesExecFileSync) {
|
|
1326
|
+
console.log(` ${GREEN}ā${NC} fetch helper uses execFileSync and does not use shell-based URL interpolation`);
|
|
1327
|
+
pass++;
|
|
1328
|
+
} else {
|
|
1329
|
+
console.error(` ${RED}ā${NC} codebase security check failed. Unsafe shell execution or URL interpolation detected.`);
|
|
1330
|
+
fail++;
|
|
1331
|
+
}
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
console.error(` ${RED}ā${NC} codebase structural check failed: ${e.message}`);
|
|
1334
|
+
fail++;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1130
1337
|
// Backward compatibility catalog checks
|
|
1131
1338
|
try {
|
|
1132
1339
|
const catList = execSync('node bin/multimodel-dev-os.js catalog list', { cwd: projectRoot, encoding: 'utf8' });
|
package/scripts/verify.sh
CHANGED
|
@@ -190,8 +190,10 @@ check_file "docs/adapters.md"
|
|
|
190
190
|
check_file "docs/installers.md"
|
|
191
191
|
check_file "docs/cli-roadmap.md"
|
|
192
192
|
check_file "docs/faq.md"
|
|
193
|
-
check_file "docs/testing
|
|
193
|
+
check_file "docs/testing.md"
|
|
194
194
|
check_file "docs/npm-publishing.md"
|
|
195
|
+
check_file "docs/release-policy.md"
|
|
196
|
+
check_file "docs/package-safety.md"
|
|
195
197
|
|
|
196
198
|
# --- CLI & Packaging Pre-Flight Tests ---
|
|
197
199
|
echo ""
|
|
@@ -235,6 +237,14 @@ else
|
|
|
235
237
|
PASS=$((PASS + 1))
|
|
236
238
|
fi
|
|
237
239
|
|
|
240
|
+
if ! npm run check:build >/dev/null; then
|
|
241
|
+
echo -e " ${RED}ā${NC} Generated CLI is stale. Run npm run build."
|
|
242
|
+
FAIL=$((FAIL + 1))
|
|
243
|
+
else
|
|
244
|
+
echo -e " ${GREEN}ā${NC} Generated CLI is fresh"
|
|
245
|
+
PASS=$((PASS + 1))
|
|
246
|
+
fi
|
|
247
|
+
|
|
238
248
|
if ! node bin/multimodel-dev-os.js init --dry-run --force >/dev/null; then
|
|
239
249
|
echo -e " ${RED}ā${NC} node bin/multimodel-dev-os.js init --dry-run failed"
|
|
240
250
|
FAIL=$((FAIL + 1))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { sourceRoot } from '../core/globals.js';
|
|
4
|
+
import { parseYaml } from '../core/yaml.js';
|
|
5
|
+
import { loadRegistrySources } from '../registry/sources.js';
|
|
6
|
+
import { loadRegistryPolicy } from '../core/policy.js';
|
|
7
|
+
import { validateRegistryUrl } from '../registry/validation.js';
|
|
8
|
+
|
|
9
|
+
export function loadCatalog(options = {}) {
|
|
10
|
+
let catalog;
|
|
11
|
+
if (options.allSources) {
|
|
12
|
+
catalog = loadAllCatalogs(options);
|
|
13
|
+
} else if (options.source) {
|
|
14
|
+
catalog = loadCatalogFromSource(options.source, options);
|
|
15
|
+
} else {
|
|
16
|
+
const path = join(sourceRoot, '.ai', 'plugins', 'catalog.yaml');
|
|
17
|
+
try {
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
const reg = parseYaml(readFileSync(path, 'utf8'));
|
|
20
|
+
catalog = reg.catalog || { plugins: [] };
|
|
21
|
+
} else {
|
|
22
|
+
catalog = { plugins: [] };
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
catalog = { plugins: [] };
|
|
26
|
+
}
|
|
27
|
+
(catalog.plugins || []).forEach(p => { p._source = 'bundled'; });
|
|
28
|
+
}
|
|
29
|
+
return catalog;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadCatalogFromSource(source, options = {}) {
|
|
33
|
+
if (!source || source === 'bundled') {
|
|
34
|
+
return loadCatalog();
|
|
35
|
+
} else if (source === 'local') {
|
|
36
|
+
const localPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog.yaml');
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(localPath)) {
|
|
39
|
+
const reg = parseYaml(readFileSync(localPath, 'utf8'));
|
|
40
|
+
const catalog = reg.catalog || { plugins: [] };
|
|
41
|
+
(catalog.plugins || []).forEach(p => { p._source = 'local'; });
|
|
42
|
+
return catalog;
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {}
|
|
45
|
+
return { plugins: [] };
|
|
46
|
+
} else if (source.startsWith('remote:')) {
|
|
47
|
+
const regName = source.substring(7);
|
|
48
|
+
const sources = loadRegistrySources();
|
|
49
|
+
const src = sources.find(s => s.name === regName);
|
|
50
|
+
if (src && src.type !== 'local') {
|
|
51
|
+
const policy = loadRegistryPolicy(options.target || process.cwd());
|
|
52
|
+
try {
|
|
53
|
+
validateRegistryUrl(src.url, policy);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`\x1b[31mError: Registry '${regName}' has an invalid URL: ${err.message}\x1b[0m`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const cachePath = join(sourceRoot, '.ai', 'registry-cache', regName, 'catalog.yaml');
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(cachePath)) {
|
|
62
|
+
const reg = parseYaml(readFileSync(cachePath, 'utf8'));
|
|
63
|
+
const catalog = reg.catalog || { plugins: [] };
|
|
64
|
+
(catalog.plugins || []).forEach(p => { p._source = `remote:${regName}`; });
|
|
65
|
+
return catalog;
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
return { plugins: [] };
|
|
69
|
+
}
|
|
70
|
+
return { plugins: [] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function loadAllCatalogs(options = {}) {
|
|
74
|
+
const sources = loadRegistrySources();
|
|
75
|
+
const policy = loadRegistryPolicy(options.target || process.cwd());
|
|
76
|
+
const allPlugins = [];
|
|
77
|
+
|
|
78
|
+
// Always include bundled
|
|
79
|
+
const bundled = loadCatalog();
|
|
80
|
+
(bundled.plugins || []).forEach(p => { p._source = 'bundled'; allPlugins.push(p); });
|
|
81
|
+
|
|
82
|
+
// Include local workspace catalog if different from bundled
|
|
83
|
+
const localPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog.yaml');
|
|
84
|
+
if (existsSync(localPath)) {
|
|
85
|
+
try {
|
|
86
|
+
const localCat = parseYaml(readFileSync(localPath, 'utf8'));
|
|
87
|
+
const localPlugins = (localCat.catalog || {}).plugins || [];
|
|
88
|
+
localPlugins.forEach(p => {
|
|
89
|
+
if (!allPlugins.some(bp => bp.slug === p.slug)) {
|
|
90
|
+
p._source = 'local';
|
|
91
|
+
allPlugins.push(p);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Include remote caches if policy allows
|
|
98
|
+
if (policy.allow_remote_registries) {
|
|
99
|
+
sources.filter(s => s.type !== 'local' && s.enabled).forEach(s => {
|
|
100
|
+
const cachePath = join(sourceRoot, '.ai', 'registry-cache', s.name, 'catalog.yaml');
|
|
101
|
+
if (existsSync(cachePath)) {
|
|
102
|
+
try {
|
|
103
|
+
const remoteCat = parseYaml(readFileSync(cachePath, 'utf8'));
|
|
104
|
+
const remotePlugins = (remoteCat.catalog || {}).plugins || [];
|
|
105
|
+
remotePlugins.forEach(p => {
|
|
106
|
+
if (!allPlugins.some(bp => bp.slug === p.slug)) {
|
|
107
|
+
p._source = `remote:${s.name}`;
|
|
108
|
+
allPlugins.push(p);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} catch (e) {}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { plugins: allPlugins };
|
|
117
|
+
}
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
|
|
3
|
+
export function parseArgs(args) {
|
|
4
|
+
const params = {
|
|
5
|
+
command: null,
|
|
6
|
+
target: process.cwd(),
|
|
7
|
+
template: 'general-app',
|
|
8
|
+
adapters: [],
|
|
9
|
+
caveman: false,
|
|
10
|
+
dryRun: false,
|
|
11
|
+
force: false,
|
|
12
|
+
help: false,
|
|
13
|
+
tokens: false,
|
|
14
|
+
modelPreset: null,
|
|
15
|
+
agent: null,
|
|
16
|
+
stack: null,
|
|
17
|
+
mobile: null,
|
|
18
|
+
aiApp: null,
|
|
19
|
+
json: false,
|
|
20
|
+
threshold: null,
|
|
21
|
+
registry: null,
|
|
22
|
+
allRegistries: false,
|
|
23
|
+
release: false,
|
|
24
|
+
type: 'unknown',
|
|
25
|
+
tags: '',
|
|
26
|
+
files: '',
|
|
27
|
+
title: null,
|
|
28
|
+
approved: false,
|
|
29
|
+
intelligence: false,
|
|
30
|
+
onboarding: false,
|
|
31
|
+
listActions: false,
|
|
32
|
+
category: null,
|
|
33
|
+
source: null,
|
|
34
|
+
allSources: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
if (arg === '--target' || arg === '-t') {
|
|
40
|
+
params.target = resolve(args[++i]);
|
|
41
|
+
} else if (arg === '--template') {
|
|
42
|
+
params.template = args[++i];
|
|
43
|
+
} else if (arg === '--adapter' || arg === '-a') {
|
|
44
|
+
params.adapters.push(args[++i]);
|
|
45
|
+
} else if (arg === '--caveman') {
|
|
46
|
+
params.caveman = true;
|
|
47
|
+
} else if (arg === '--dry-run' || arg === '-d') {
|
|
48
|
+
params.dryRun = true;
|
|
49
|
+
} else if (arg === '--list-actions') {
|
|
50
|
+
params.listActions = true;
|
|
51
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
52
|
+
params.force = true;
|
|
53
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
54
|
+
params.help = true;
|
|
55
|
+
} else if (arg === '--tokens') {
|
|
56
|
+
params.tokens = true;
|
|
57
|
+
} else if (arg === '--all-registries') {
|
|
58
|
+
params.allRegistries = true;
|
|
59
|
+
} else if (arg === '--release') {
|
|
60
|
+
params.release = true;
|
|
61
|
+
} else if (arg === '--intelligence') {
|
|
62
|
+
params.intelligence = true;
|
|
63
|
+
} else if (arg === '--onboarding') {
|
|
64
|
+
params.onboarding = true;
|
|
65
|
+
} else if (arg === '--json') {
|
|
66
|
+
params.json = true;
|
|
67
|
+
} else if (arg === '--threshold') {
|
|
68
|
+
params.threshold = args[++i];
|
|
69
|
+
} else if (arg === '--registry') {
|
|
70
|
+
params.registry = args[++i];
|
|
71
|
+
} else if (arg === '--model-preset') {
|
|
72
|
+
params.modelPreset = args[++i];
|
|
73
|
+
} else if (arg === '--agent') {
|
|
74
|
+
params.agent = args[++i];
|
|
75
|
+
} else if (arg === '--stack') {
|
|
76
|
+
params.stack = args[++i];
|
|
77
|
+
} else if (arg === '--mobile') {
|
|
78
|
+
params.mobile = args[++i];
|
|
79
|
+
} else if (arg === '--type') {
|
|
80
|
+
params.type = args[++i];
|
|
81
|
+
} else if (arg === '--tags') {
|
|
82
|
+
params.tags = args[++i];
|
|
83
|
+
} else if (arg === '--files') {
|
|
84
|
+
params.files = args[++i];
|
|
85
|
+
} else if (arg === '--title') {
|
|
86
|
+
params.title = args[++i];
|
|
87
|
+
} else if (arg === '--approved') {
|
|
88
|
+
params.approved = true;
|
|
89
|
+
} else if (arg === '--category') {
|
|
90
|
+
params.category = args[++i];
|
|
91
|
+
} else if (arg === '--source') {
|
|
92
|
+
params.source = args[++i];
|
|
93
|
+
} else if (arg === '--all-sources') {
|
|
94
|
+
params.allSources = true;
|
|
95
|
+
} else if (!params.command && !arg.startsWith('-')) {
|
|
96
|
+
params.command = arg;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return params;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getPositionalArgs(args) {
|
|
103
|
+
const positionalArgs = [];
|
|
104
|
+
for (let i = 0; i < args.length; i++) {
|
|
105
|
+
const arg = args[i];
|
|
106
|
+
if (arg === '--target' || arg === '-t' || arg === '--template' || arg === '--adapter' || arg === '-a' ||
|
|
107
|
+
arg === '--threshold' || arg === '--registry' || arg === '--model-preset' || arg === '--agent' ||
|
|
108
|
+
arg === '--stack' || arg === '--mobile' || arg === '--type' || arg === '--tags' || arg === '--files' ||
|
|
109
|
+
arg === '--title' || arg === '--category') {
|
|
110
|
+
i++; // skip next arg (its value)
|
|
111
|
+
} else if (arg.startsWith('-')) {
|
|
112
|
+
// it's a flag, skip
|
|
113
|
+
} else {
|
|
114
|
+
positionalArgs.push(arg);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return positionalArgs;
|
|
118
|
+
}
|
package/src/cli/help.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { version } from '../core/globals.js';
|
|
2
|
+
|
|
3
|
+
export function showHelp() {
|
|
4
|
+
console.log(`\nš§ \x1b[36mmultimodel-dev-os CLI v${version}\x1b[0m`);
|
|
5
|
+
console.log('====================================');
|
|
6
|
+
console.log('Usage: node bin/multimodel-dev-os.js <command> [options]\n');
|
|
7
|
+
console.log('Commands:');
|
|
8
|
+
console.log(' init Initialize a project with configs and adapters');
|
|
9
|
+
console.log(' scan Scan project structure and framework signals');
|
|
10
|
+
console.log(' status Show compact dashboard summarizing repository intelligence state');
|
|
11
|
+
console.log(' dashboard Launch the interactive terminal command center (alias: ui)');
|
|
12
|
+
console.log(' memory <subcmd> Manage hash-compressed codebase memory (subcmd: build, refresh, diff)');
|
|
13
|
+
console.log(' feedback <subcmd> Manage developer feedback loops (subcmd: add, list, summarize)');
|
|
14
|
+
console.log(' improve <subcmd> Manage codebase self-improvement proposals (subcmd: propose, review, status, validate, diff, apply, log)');
|
|
15
|
+
console.log(' workflow <subcmd> Orchestrate read-only development workflow pipelines (subcmd: list, show, plan, run)');
|
|
16
|
+
console.log(' handoff <subcmd> Compile or print token-compressed agent session summaries (subcmd: build, show)');
|
|
17
|
+
console.log(' onboard <subcmd> Safely integrate MultiModel Dev OS into existing repo (subcmd: analyze, recommend, plan, apply, status)');
|
|
18
|
+
console.log(' adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)');
|
|
19
|
+
console.log(' plugin <subcmd> Manage declarative plugins (subcmd: list, show, validate, install, status)');
|
|
20
|
+
console.log(' catalog <subcmd> Manage Workflow Marketplace & Plugin Catalog (subcmd: list, search, show, categories, recommend, install, status)');
|
|
21
|
+
console.log(' registry <subcmd> Manage trusted remote catalog registries (subcmd: list, add, remove, sync, status, verify, show, cache)');
|
|
22
|
+
console.log(' verify Validate structural integrity of an existing project');
|
|
23
|
+
console.log(' templates List all built-in template profiles with details');
|
|
24
|
+
console.log(' list-templates Alias for templates command');
|
|
25
|
+
console.log(' show-template <t> Inspect detailed stack specifications of template <t>');
|
|
26
|
+
console.log(' doctor Advisory checkup of project compatibility loops and ignored folders');
|
|
27
|
+
console.log(' validate Strict validation checks to verify directory schema compliance');
|
|
28
|
+
console.log(' validate-template Validate registry keys and source folder files for template');
|
|
29
|
+
console.log(' validate-adapter Validate registry keys and source assets for IDE adapter');
|
|
30
|
+
console.log(' validate-skill Verify custom skill conforms to core prompt structure');
|
|
31
|
+
console.log(' models List registered model aliases in the capabilities registry');
|
|
32
|
+
console.log(' show-model <m> View specifications of model <m> in registry');
|
|
33
|
+
console.log(' providers List configured AI provider API endpoints');
|
|
34
|
+
console.log(' route-model <tsk> Suggest optimal model mapping for task <tsk>');
|
|
35
|
+
console.log(' adapters List IDE and terminal tool adapters');
|
|
36
|
+
console.log(' show-adapter <a> Inspect config specifications of adapter <a>');
|
|
37
|
+
console.log(' skills List active skills custom prompts in target workspace');
|
|
38
|
+
console.log(' show-skill <s> View prompt contents of target workspace skill <s>\n');
|
|
39
|
+
console.log('Options:');
|
|
40
|
+
console.log(' -t, --target <path> Target folder destination (default: current working directory)');
|
|
41
|
+
console.log(' --type <type> Feedback classification (correction, preference, bug, etc.)');
|
|
42
|
+
console.log(' --tags <list> Comma-separated descriptor tags for feedback');
|
|
43
|
+
console.log(' --files <list> Comma-separated target files for feedback');
|
|
44
|
+
console.log(' --category <name> Filter catalog plugins list by category');
|
|
45
|
+
console.log(' --source <src> Catalog source filter: bundled, local, or remote:<name>');
|
|
46
|
+
console.log(' --all-sources Include all enabled catalog sources in listings');
|
|
47
|
+
console.log(' --title <text> Specifies title for codebase improvement proposal');
|
|
48
|
+
console.log(' --approved Explicitly approve and execute proposal/onboarding/adapter sync writes');
|
|
49
|
+
console.log(' --template <name> Template profile: nextjs-saas, expo-react-native-android, etc.');
|
|
50
|
+
console.log(' -a, --adapter <name> Inject specific adapter: cursor, claude, vscode, gemini, etc.');
|
|
51
|
+
console.log(' --caveman Use minimal-token templates (~79% fewer tokens)');
|
|
52
|
+
console.log(' --tokens Run a deeper token-sink size analysis during doctor checkup');
|
|
53
|
+
console.log(' --intelligence Run diagnostic checkup of repository intelligence config');
|
|
54
|
+
console.log(' --onboarding Run diagnostic checkup of repository onboarding setup');
|
|
55
|
+
console.log(' --json Output raw JSON data for listing commands (models, adapters, templates)');
|
|
56
|
+
console.log(' --threshold <val> Set custom size threshold for doctor tokens checks (e.g. 50KB)');
|
|
57
|
+
console.log(' --registry <path> Override default registry (for templates/adapters list or check)');
|
|
58
|
+
console.log(' -d, --dry-run Preview planned file actions without modifying the filesystem');
|
|
59
|
+
console.log(' -f, --force Overwrite existing files without prompting\n');
|
|
60
|
+
}
|