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.
Files changed (59) hide show
  1. package/README.md +4 -0
  2. package/bin/multimodel-dev-os.js +3419 -3573
  3. package/docs/.vitepress/config.js +2 -2
  4. package/docs/index.md +5 -5
  5. package/docs/npm-publishing.md +5 -5
  6. package/docs/package-safety.md +24 -0
  7. package/docs/public/llms-full.txt +1 -1
  8. package/docs/public/llms.txt +1 -1
  9. package/docs/public/sitemap.xml +10 -0
  10. package/docs/registry-policy.md +4 -0
  11. package/docs/registry-security.md +7 -0
  12. package/docs/registry-sync.md +6 -0
  13. package/docs/release-policy.md +6 -5
  14. package/docs/testing.md +133 -0
  15. package/docs/trusted-registries.md +4 -0
  16. package/docs/v3-roadmap.md +20 -2
  17. package/package.json +10 -3
  18. package/scripts/build-cli.js +59 -0
  19. package/scripts/check-build-fresh.js +52 -0
  20. package/scripts/install.ps1 +1 -1
  21. package/scripts/install.sh +1 -1
  22. package/scripts/verify.js +221 -14
  23. package/scripts/verify.sh +11 -1
  24. package/src/catalog/loader.js +117 -0
  25. package/src/cli/args.js +118 -0
  26. package/src/cli/help.js +60 -0
  27. package/src/cli/main.js +5718 -0
  28. package/src/core/globals.js +52 -0
  29. package/src/core/hashes.js +15 -0
  30. package/src/core/policy.js +36 -0
  31. package/src/core/security.js +61 -0
  32. package/src/core/yaml.js +136 -0
  33. package/src/plugin/manifest.js +95 -0
  34. package/src/registry/sources.js +40 -0
  35. package/src/registry/validation.js +45 -0
  36. package/tests/README.md +37 -0
  37. package/tests/fixtures/README.md +22 -0
  38. package/tests/fixtures/custom-template-example/README.md +10 -0
  39. package/tests/fixtures/proposals/approved-append-line.md +28 -0
  40. package/tests/fixtures/proposals/approved-create-file.md +29 -0
  41. package/tests/fixtures/proposals/approved-replace-text.md +30 -0
  42. package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
  43. package/tests/fixtures/proposals/no-operations.md +18 -0
  44. package/tests/fixtures/proposals/path-traversal.md +29 -0
  45. package/tests/fixtures/proposals/pending-proposal.md +29 -0
  46. package/tests/fixtures/proposals/protected-path.md +29 -0
  47. package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
  48. package/tests/fixtures/registry-overrides/README.md +20 -0
  49. package/tests/smoke/README.md +37 -0
  50. package/tests/smoke/cli-smoke.md +49 -0
  51. package/tests/unit/build-output.test.js +40 -0
  52. package/tests/unit/catalog-loader.test.js +44 -0
  53. package/tests/unit/path-safety.test.js +62 -0
  54. package/tests/unit/plugin-manifest.test.js +94 -0
  55. package/tests/unit/prepublish-guard.test.js +35 -0
  56. package/tests/unit/registry-policy.test.js +46 -0
  57. package/tests/unit/registry-url-validation.test.js +64 -0
  58. package/tests/unit/yaml.test.js +92 -0
  59. 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-v0.2.md');
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.1 with MMDO_ALLOW_PUBLISH=true
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.1 when MMDO_ALLOW_PUBLISH=true`);
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.1: ${errText || err.message}`);
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.1
583
- if (expectedVersion === '3.0.1') {
584
- console.log(` ${GREEN}āœ“${NC} package.json version is exactly 3.0.1`);
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.1 (found ${expectedVersion})`);
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', stdio: ['ignore', 'pipe', 'pipe'] });
927
- if (packOutput.includes(`multimodel-dev-os@${expectedVersion}`) || packOutput.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || packOutput.includes(`version: ${expectedVersion}`)) {
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 stdout`);
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
- if (stdErrOut.includes(`multimodel-dev-os@${expectedVersion}`) || stdErrOut.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || stdOutOut.includes(`multimodel-dev-os-${expectedVersion}.tgz`)) {
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-v0.2.md"
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
+ }
@@ -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
+ }
@@ -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
+ }