multimodel-dev-os 2.8.1 → 2.9.1

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 (50) hide show
  1. package/.ai/plugins/catalog/.ai/checks/pre-commit-gate.md +14 -0
  2. package/.ai/plugins/catalog/.ai/skills/checkout-ops.md +12 -0
  3. package/.ai/plugins/catalog/.ai/skills/git-operations.md +21 -0
  4. package/.ai/plugins/catalog/.ai/skills/nextjs-builder.md +12 -0
  5. package/.ai/plugins/catalog/.ai/skills/release-ops.md +12 -0
  6. package/.ai/plugins/catalog/.ai/skills/seo-audit-ops.md +14 -0
  7. package/.ai/plugins/catalog/.ai/skills/wp-helper.md +13 -0
  8. package/.ai/plugins/catalog/README.md +34 -0
  9. package/.ai/plugins/catalog/ecommerce-workflows.yaml +14 -0
  10. package/.ai/plugins/catalog/git-workflows.yaml +22 -0
  11. package/.ai/plugins/catalog/nextjs-workflows.yaml +14 -0
  12. package/.ai/plugins/catalog/release-workflows.yaml +14 -0
  13. package/.ai/plugins/catalog/seo-workflows.yaml +19 -0
  14. package/.ai/plugins/catalog/wordpress-workflows.yaml +14 -0
  15. package/.ai/plugins/catalog.yaml +161 -0
  16. package/.ai/policies/registry-policy.yaml +51 -0
  17. package/.ai/registries/sources.yaml +15 -0
  18. package/.ai/registry-cache/README.md +35 -0
  19. package/.ai/schema/registry-manifest.schema.json +57 -0
  20. package/.ai/schema/registry-policy.schema.json +66 -0
  21. package/README.md +6 -5
  22. package/bin/multimodel-dev-os.js +1309 -30
  23. package/docs/.vitepress/config.js +16 -2
  24. package/docs/CLI.md +54 -1
  25. package/docs/architecture.md +9 -3
  26. package/docs/catalog-authoring.md +63 -0
  27. package/docs/catalog.md +72 -0
  28. package/docs/comparison.md +1 -0
  29. package/docs/dashboard.md +13 -2
  30. package/docs/faq.md +19 -0
  31. package/docs/plugin-authoring.md +6 -0
  32. package/docs/plugin-catalog.md +35 -0
  33. package/docs/plugin-hooks.md +6 -0
  34. package/docs/public/llms-full.txt +18 -1
  35. package/docs/public/llms.txt +17 -1
  36. package/docs/public/sitemap.xml +45 -0
  37. package/docs/quickstart.md +17 -0
  38. package/docs/registry-policy.md +93 -0
  39. package/docs/registry-security.md +67 -0
  40. package/docs/registry-sync.md +106 -0
  41. package/docs/remote-catalog-authoring.md +139 -0
  42. package/docs/repository-command-center.md +2 -0
  43. package/docs/trusted-registries.md +77 -0
  44. package/docs/v2-roadmap.md +13 -4
  45. package/docs/workflow-marketplace.md +22 -0
  46. package/docs/workflow-orchestration.md +6 -0
  47. package/package.json +1 -1
  48. package/scripts/install.ps1 +0 -0
  49. package/scripts/install.sh +0 -0
  50. package/scripts/verify.js +458 -10
@@ -53,7 +53,10 @@ function parseArgs(args) {
53
53
  approved: false,
54
54
  intelligence: false,
55
55
  onboarding: false,
56
- listActions: false
56
+ listActions: false,
57
+ category: null,
58
+ source: null,
59
+ allSources: false
57
60
  };
58
61
 
59
62
  for (let i = 0; i < args.length; i++) {
@@ -108,6 +111,12 @@ function parseArgs(args) {
108
111
  params.title = args[++i];
109
112
  } else if (arg === '--approved') {
110
113
  params.approved = true;
114
+ } else if (arg === '--category') {
115
+ params.category = args[++i];
116
+ } else if (arg === '--source') {
117
+ params.source = args[++i];
118
+ } else if (arg === '--all-sources') {
119
+ params.allSources = true;
111
120
  } else if (!params.command && !arg.startsWith('-')) {
112
121
  params.command = arg;
113
122
  }
@@ -122,7 +131,7 @@ function getPositionalArgs(args) {
122
131
  if (arg === '--target' || arg === '-t' || arg === '--template' || arg === '--adapter' || arg === '-a' ||
123
132
  arg === '--threshold' || arg === '--registry' || arg === '--model-preset' || arg === '--agent' ||
124
133
  arg === '--stack' || arg === '--mobile' || arg === '--type' || arg === '--tags' || arg === '--files' ||
125
- arg === '--title') {
134
+ arg === '--title' || arg === '--category') {
126
135
  i++; // skip next arg (its value)
127
136
  } else if (arg.startsWith('-')) {
128
137
  // it's a flag, skip
@@ -449,6 +458,96 @@ if (COMMAND === 'init') {
449
458
  console.log('Example: node bin/multimodel-dev-os.js plugin list');
450
459
  process.exit(1);
451
460
  }
461
+ } else if (COMMAND === 'catalog') {
462
+ const positional = getPositionalArgs(ARGS);
463
+ const sub = positional[1];
464
+ if (sub === 'list') {
465
+ handleCatalogList(params);
466
+ } else if (sub === 'search') {
467
+ const query = positional[2];
468
+ if (!query) {
469
+ console.error('\x1b[31mError: Please specify a search query.\x1b[0m');
470
+ process.exit(1);
471
+ }
472
+ handleCatalogSearch(query, params);
473
+ } else if (sub === 'show') {
474
+ const slug = positional[2];
475
+ if (!slug) {
476
+ console.error('\x1b[31mError: Please specify a catalog plugin slug.\x1b[0m');
477
+ process.exit(1);
478
+ }
479
+ handleCatalogShow(slug, params);
480
+ } else if (sub === 'categories') {
481
+ handleCatalogCategories(params);
482
+ } else if (sub === 'recommend') {
483
+ handleCatalogRecommend(params);
484
+ } else if (sub === 'install') {
485
+ const slug = positional[2];
486
+ if (!slug) {
487
+ console.error('\x1b[31mError: Please specify a catalog plugin slug to install.\x1b[0m');
488
+ process.exit(1);
489
+ }
490
+ handleCatalogInstall(slug, params);
491
+ } else if (sub === 'status') {
492
+ handleCatalogStatus(params);
493
+ } else {
494
+ console.error('\x1b[31mError: Please specify a catalog subcommand: list, search, show, categories, recommend, install, or status.\x1b[0m');
495
+ console.log('Example: node bin/multimodel-dev-os.js catalog list');
496
+ process.exit(1);
497
+ }
498
+ } else if (COMMAND === 'registry') {
499
+ const positional = getPositionalArgs(ARGS);
500
+ const sub = positional[1];
501
+ if (sub === 'list') {
502
+ handleRegistryList(params);
503
+ } else if (sub === 'add') {
504
+ const rName = positional[2];
505
+ const rUrl = positional[3];
506
+ if (!rName || !rUrl) {
507
+ console.error('\x1b[31mError: Please specify a registry name and URL.\x1b[0m');
508
+ console.log('Example: node bin/multimodel-dev-os.js registry add official https://example.com/catalog.yaml --approved');
509
+ process.exit(1);
510
+ }
511
+ handleRegistryAdd(rName, rUrl, params);
512
+ } else if (sub === 'remove') {
513
+ const rName = positional[2];
514
+ if (!rName) {
515
+ console.error('\x1b[31mError: Please specify a registry name to remove.\x1b[0m');
516
+ process.exit(1);
517
+ }
518
+ handleRegistryRemove(rName, params);
519
+ } else if (sub === 'sync') {
520
+ const rName = positional[2];
521
+ if (!rName) {
522
+ console.error('\x1b[31mError: Please specify a registry name to sync.\x1b[0m');
523
+ process.exit(1);
524
+ }
525
+ handleRegistrySync(rName, params);
526
+ } else if (sub === 'status') {
527
+ handleRegistryStatus(params);
528
+ } else if (sub === 'verify') {
529
+ const rName = positional[2] || 'bundled';
530
+ handleRegistryVerify(rName, params);
531
+ } else if (sub === 'show') {
532
+ const rName = positional[2];
533
+ if (!rName) {
534
+ console.error('\x1b[31mError: Please specify a registry name to show.\x1b[0m');
535
+ process.exit(1);
536
+ }
537
+ handleRegistryShow(rName, params);
538
+ } else if (sub === 'cache') {
539
+ const cacheSub = positional[2];
540
+ if (cacheSub === 'clear') {
541
+ handleRegistryCacheClear(params);
542
+ } else {
543
+ console.error('\x1b[31mError: Please specify a cache subcommand: clear.\x1b[0m');
544
+ process.exit(1);
545
+ }
546
+ } else {
547
+ console.error('\x1b[31mError: Please specify a registry subcommand: list, add, remove, sync, status, verify, show, or cache.\x1b[0m');
548
+ console.log('Example: node bin/multimodel-dev-os.js registry list');
549
+ process.exit(1);
550
+ }
452
551
  } else {
453
552
  console.error(`\x1b[31mUnknown command: ${COMMAND}\x1b[0m`);
454
553
  showHelp();
@@ -472,6 +571,8 @@ function showHelp() {
472
571
  console.log(' onboard <subcmd> Safely integrate MultiModel Dev OS into existing repo (subcmd: analyze, recommend, plan, apply, status)');
473
572
  console.log(' adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)');
474
573
  console.log(' plugin <subcmd> Manage declarative plugins (subcmd: list, show, validate, install, status)');
574
+ console.log(' catalog <subcmd> Manage Workflow Marketplace & Plugin Catalog (subcmd: list, search, show, categories, recommend, install, status)');
575
+ console.log(' registry <subcmd> Manage trusted remote catalog registries (subcmd: list, add, remove, sync, status, verify, show, cache)');
475
576
  console.log(' verify Validate structural integrity of an existing project');
476
577
  console.log(' templates List all built-in template profiles with details');
477
578
  console.log(' list-templates Alias for templates command');
@@ -494,6 +595,9 @@ function showHelp() {
494
595
  console.log(' --type <type> Feedback classification (correction, preference, bug, etc.)');
495
596
  console.log(' --tags <list> Comma-separated descriptor tags for feedback');
496
597
  console.log(' --files <list> Comma-separated target files for feedback');
598
+ console.log(' --category <name> Filter catalog plugins list by category');
599
+ console.log(' --source <src> Catalog source filter: bundled, local, or remote:<name>');
600
+ console.log(' --all-sources Include all enabled catalog sources in listings');
497
601
  console.log(' --title <text> Specifies title for codebase improvement proposal');
498
602
  console.log(' --approved Explicitly approve and execute proposal/onboarding/adapter sync writes');
499
603
  console.log(' --template <name> Template profile: nextjs-saas, expo-react-native-android, etc.');
@@ -1072,6 +1176,30 @@ function handleValidate(options) {
1072
1176
  }
1073
1177
 
1074
1178
  // --- YAML Parser Helper ---
1179
+ function parseFlowArray(str) {
1180
+ const contents = str.slice(1, -1).trim();
1181
+ if (!contents) return [];
1182
+
1183
+ const result = [];
1184
+ const regex = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([^,\s][^,]*[^,\s]|[^,\s])/g;
1185
+ let match;
1186
+ while ((match = regex.exec(contents)) !== null) {
1187
+ if (match[1] !== undefined) {
1188
+ result.push(match[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\'));
1189
+ } else if (match[2] !== undefined) {
1190
+ result.push(match[2].replace(/\\'/g, "'").replace(/\\\\/g, '\\'));
1191
+ } else if (match[3] !== undefined) {
1192
+ let val = match[3].trim();
1193
+ if (val === 'true') val = true;
1194
+ else if (val === 'false') val = false;
1195
+ else if (val === 'null') val = null;
1196
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
1197
+ result.push(val);
1198
+ }
1199
+ }
1200
+ return result;
1201
+ }
1202
+
1075
1203
  function parseYaml(content) {
1076
1204
  try {
1077
1205
  const root = {};
@@ -1079,7 +1207,21 @@ function parseYaml(content) {
1079
1207
 
1080
1208
  const lines = content.split(/\r?\n/);
1081
1209
  for (let line of lines) {
1082
- const commentIdx = line.indexOf('#');
1210
+ // Find comment index outside quotes
1211
+ let commentIdx = -1;
1212
+ let insideDouble = false;
1213
+ let insideSingle = false;
1214
+ for (let i = 0; i < line.length; i++) {
1215
+ const char = line[i];
1216
+ if (char === '"' && (i === 0 || line[i-1] !== '\\')) {
1217
+ insideDouble = !insideDouble;
1218
+ } else if (char === "'" && (i === 0 || line[i-1] !== '\\')) {
1219
+ insideSingle = !insideSingle;
1220
+ } else if (char === '#' && !insideDouble && !insideSingle) {
1221
+ commentIdx = i;
1222
+ break;
1223
+ }
1224
+ }
1083
1225
  if (commentIdx !== -1) {
1084
1226
  line = line.substring(0, commentIdx);
1085
1227
  }
@@ -1111,17 +1253,26 @@ function parseYaml(content) {
1111
1253
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1112
1254
  val = val.substring(1, val.length - 1);
1113
1255
  }
1256
+ if (val.startsWith('[') && val.endsWith(']')) {
1257
+ val = parseFlowArray(val);
1258
+ }
1114
1259
  parent.obj.push(val);
1115
1260
  } else {
1116
1261
  const key = trimmed.substring(0, colonIdx).trim();
1117
1262
  let val = trimmed.substring(colonIdx + 1).trim();
1263
+ let isQuoted = false;
1118
1264
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1119
1265
  val = val.substring(1, val.length - 1);
1266
+ isQuoted = true;
1267
+ }
1268
+ if (val.startsWith('[') && val.endsWith(']')) {
1269
+ val = parseFlowArray(val);
1270
+ } else if (!isQuoted) {
1271
+ if (val === 'true') val = true;
1272
+ else if (val === 'false') val = false;
1273
+ else if (val === 'null') val = null;
1274
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
1120
1275
  }
1121
- if (val === 'true') val = true;
1122
- else if (val === 'false') val = false;
1123
- else if (val === 'null') val = null;
1124
- else if (/^\d+$/.test(val)) val = parseInt(val, 10);
1125
1276
 
1126
1277
  const newObj = { [key]: val };
1127
1278
  parent.obj.push(newObj);
@@ -1133,14 +1284,19 @@ function parseYaml(content) {
1133
1284
 
1134
1285
  const key = trimmed.substring(0, colonIdx).trim();
1135
1286
  let val = trimmed.substring(colonIdx + 1).trim();
1136
-
1287
+ let isQuoted = false;
1137
1288
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1138
1289
  val = val.substring(1, val.length - 1);
1290
+ isQuoted = true;
1291
+ }
1292
+ if (val.startsWith('[') && val.endsWith(']')) {
1293
+ val = parseFlowArray(val);
1294
+ } else if (!isQuoted) {
1295
+ if (val === 'true') val = true;
1296
+ else if (val === 'false') val = false;
1297
+ else if (val === 'null') val = null;
1298
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
1139
1299
  }
1140
- if (val === 'true') val = true;
1141
- else if (val === 'false') val = false;
1142
- else if (val === 'null') val = null;
1143
- else if (/^\d+$/.test(val)) val = parseInt(val, 10);
1144
1300
 
1145
1301
  if (val === '') {
1146
1302
  parent.obj[key] = {};
@@ -4366,6 +4522,8 @@ function handleDashboard(options) {
4366
4522
  { name: 'Adapter Synchronization...', action: 'submenu', menu: 'adapter' },
4367
4523
  { name: 'Memory & Intelligence...', action: 'submenu', menu: 'memory' },
4368
4524
  { name: 'Developer Feedback Loops...', action: 'submenu', menu: 'feedback' },
4525
+ { name: 'Workflow Marketplace Catalog...', action: 'submenu', menu: 'catalog' },
4526
+ { name: 'Registry Sources & Cache...', action: 'submenu', menu: 'registry' },
4369
4527
  { name: 'Quality Gates & Diagnostics...', action: 'submenu', menu: 'quality' },
4370
4528
  { name: 'Plugins Status Overview', action: 'command', command: 'plugin status' },
4371
4529
  { name: 'Exit Command Center', action: 'exit' }
@@ -4402,11 +4560,24 @@ function handleDashboard(options) {
4402
4560
  { name: 'Proposals: Propose improvement proposal', action: 'command', command: 'improve propose' },
4403
4561
  { name: 'Proposals: Review active proposals list', action: 'command', command: 'improve review' }
4404
4562
  ],
4563
+ catalog: [
4564
+ { name: '← Back to Main Menu', action: 'back' },
4565
+ { name: 'Catalog: List bundled plugins', action: 'command', command: 'catalog list' },
4566
+ { name: 'Catalog: Recommend for current repo', action: 'command', command: 'catalog recommend' },
4567
+ { name: 'Catalog: Show installed catalog status', action: 'command', command: 'catalog status' }
4568
+ ],
4405
4569
  quality: [
4406
4570
  { name: '← Back to Main Menu', action: 'back' },
4407
4571
  { name: 'Doctor: Run Advisory Diagnostics', action: 'command', command: 'doctor' },
4408
4572
  { name: 'Validate: Strict Schema Compliance', action: 'command', command: 'validate' },
4409
4573
  { name: 'Verify: Run Release verification tests', action: 'command', command: 'verify' }
4574
+ ],
4575
+ registry: [
4576
+ { name: '← Back to Main Menu', action: 'back' },
4577
+ { name: 'Registry: List configured sources', action: 'command', command: 'registry list' },
4578
+ { name: 'Registry: Show sync status', action: 'command', command: 'registry status' },
4579
+ { name: 'Registry: Verify cache integrity', action: 'command', command: 'registry verify bundled' },
4580
+ { name: 'Registry: Show policy status', action: 'command', command: 'registry status' }
4410
4581
  ]
4411
4582
  };
4412
4583
 
@@ -4757,6 +4928,7 @@ function handlePluginInstall(pluginPath, options) {
4757
4928
  process.exit(1);
4758
4929
  }
4759
4930
 
4931
+ const policy = loadRegistryPolicy(options.target || process.cwd());
4760
4932
  const pluginContent = readFileSync(fullPath, 'utf8');
4761
4933
  const plugin = parseYaml(pluginContent);
4762
4934
  const slug = plugin.slug;
@@ -4775,31 +4947,21 @@ function handlePluginInstall(pluginPath, options) {
4775
4947
  plugin.allowed_file_patterns.forEach(pattern => {
4776
4948
  const normPattern = pattern.replace(/\\/g, '/').trim();
4777
4949
 
4778
- const isSafeSubdir = [
4779
- '.ai/plugins/',
4780
- '.ai/registries/',
4781
- '.ai/templates/',
4782
- '.ai/skills/',
4783
- '.ai/checks/',
4784
- '.ai/prompts/',
4785
- '.ai/adapters/'
4786
- ].some(prefix => normPattern.startsWith(prefix));
4787
-
4950
+ const isSafeSubdir = policy.allowed_write_roots.some(prefix => normPattern.startsWith(prefix));
4788
4951
  const hasTraversal = normPattern.includes('..') || normPattern.startsWith('/');
4789
- const isBlacklisted = [
4790
- '.env',
4791
- '.npmrc',
4792
- '.git/',
4793
- 'node_modules/',
4794
- 'package.json',
4795
- 'package-lock.json'
4796
- ].some(black => normPattern.includes(black));
4952
+ const isBlacklisted = policy.blocked_paths.some(black => normPattern.includes(black));
4797
4953
 
4798
4954
  if (!isSafeSubdir || hasTraversal || isBlacklisted) {
4799
4955
  console.error(`\x1b[31mError: Path pattern '${pattern}' violates safety boundaries. Installation aborted.\x1b[0m`);
4800
4956
  process.exit(1);
4801
4957
  }
4802
4958
 
4959
+ const ext = '.' + normPattern.split('.').pop();
4960
+ if (!policy.allowed_file_extensions.includes(ext)) {
4961
+ console.error(`\x1b[31mError: File extension '${ext}' for asset '${pattern}' is not allowed by policy. Installation aborted.\x1b[0m`);
4962
+ process.exit(1);
4963
+ }
4964
+
4803
4965
  const srcFile = join(sourceDir, normPattern);
4804
4966
  if (existsSync(srcFile) && statSync(srcFile).isFile()) {
4805
4967
  filesToCopy.push({
@@ -4811,6 +4973,22 @@ function handlePluginInstall(pluginPath, options) {
4811
4973
  });
4812
4974
  }
4813
4975
 
4976
+ if (filesToCopy.length > policy.max_plugin_files) {
4977
+ console.error(`\x1b[31mError: Plugin file count (${filesToCopy.length}) exceeds policy limit (${policy.max_plugin_files}). Installation aborted.\x1b[0m`);
4978
+ process.exit(1);
4979
+ }
4980
+
4981
+ let totalSize = 0;
4982
+ filesToCopy.forEach(item => {
4983
+ if (existsSync(item.src)) {
4984
+ totalSize += statSync(item.src).size;
4985
+ }
4986
+ });
4987
+ if (totalSize > policy.max_plugin_size_kb * 1024) {
4988
+ console.error(`\x1b[31mError: Plugin total size (${(totalSize / 1024).toFixed(1)}KB) exceeds policy limit (${policy.max_plugin_size_kb}KB). Installation aborted.\x1b[0m`);
4989
+ process.exit(1);
4990
+ }
4991
+
4814
4992
  let conflicts = false;
4815
4993
  filesToCopy.forEach(item => {
4816
4994
  const destPath = join(options.target, item.dest);
@@ -4829,6 +5007,7 @@ function handlePluginInstall(pluginPath, options) {
4829
5007
 
4830
5008
  if (!options.approved) {
4831
5009
  console.error(`\x1b[31mError: Plugin cannot be installed without explicit user approval. Pass the --approved flag.\x1b[0m`);
5010
+ console.log(`\n\x1b[33mSafety Status:\x1b[0m Sandbox checks: PASSED (Declarative only, offline, zero-dependency)`);
4832
5011
  console.log(`\n\x1b[33mPlanned Installation Actions:\x1b[0m`);
4833
5012
  filesToCopy.forEach(item => {
4834
5013
  const exists = existsSync(join(options.target, item.dest));
@@ -4857,6 +5036,7 @@ function handlePluginInstall(pluginPath, options) {
4857
5036
  });
4858
5037
 
4859
5038
  console.log(`\n\x1b[32m✔ Plugin '${plugin.name}' installed successfully!\x1b[0m`);
5039
+ console.log(`\n\x1b[32mSafety Status:\x1b[0m Sandboxed isolation: VERIFIED (All files written inside whitelisted .ai/ & adapters/ folders)`);
4860
5040
  console.log(`\nSummary of actions:`);
4861
5041
  console.log(` - Manifest registered: .ai/plugins/${slug}.yaml`);
4862
5042
  const assetCount = filesToCopy.length - 1;
@@ -4939,4 +5119,1103 @@ function handlePluginStatus(options) {
4939
5119
  console.log('');
4940
5120
  }
4941
5121
 
5122
+ // --- Phase 8: Workflow Marketplace / Plugin Catalog ---
5123
+
5124
+ function loadCatalog(options = {}) {
5125
+ let catalog;
5126
+ if (options.allSources) {
5127
+ catalog = loadAllCatalogs(options);
5128
+ } else if (options.source) {
5129
+ catalog = loadCatalogFromSource(options.source, options);
5130
+ } else {
5131
+ const path = join(sourceRoot, '.ai', 'plugins', 'catalog.yaml');
5132
+ try {
5133
+ if (existsSync(path)) {
5134
+ const reg = parseYaml(readFileSync(path, 'utf8'));
5135
+ catalog = reg.catalog || { plugins: [] };
5136
+ } else {
5137
+ catalog = { plugins: [] };
5138
+ }
5139
+ } catch (e) {
5140
+ catalog = { plugins: [] };
5141
+ }
5142
+ (catalog.plugins || []).forEach(p => { p._source = 'bundled'; });
5143
+ }
5144
+ return catalog;
5145
+ }
5146
+
5147
+ function handleCatalogList(options) {
5148
+ const catalog = loadCatalog(options);
5149
+ const plugins = catalog.plugins || [];
5150
+
5151
+ const filtered = options.category
5152
+ ? plugins.filter(p => p.category.toLowerCase() === options.category.toLowerCase())
5153
+ : plugins;
5154
+
5155
+ if (options.json) {
5156
+ console.log(JSON.stringify(filtered, null, 2));
5157
+ return;
5158
+ }
5159
+
5160
+ console.log(`\n📚 \x1b[36mWorkflow Marketplace & Plugin Catalog [v${version}]\x1b[0m`);
5161
+ console.log('==================================================');
5162
+ if (options.category) {
5163
+ console.log(`Filtering by category: \x1b[33m${options.category}\x1b[0m`);
5164
+ }
5165
+
5166
+ const installedSlugs = new Set();
5167
+ const pluginsDir = getPluginsDir(options.target);
5168
+ if (existsSync(pluginsDir)) {
5169
+ try {
5170
+ const files = readdirSync(pluginsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
5171
+ files.forEach(f => {
5172
+ try {
5173
+ const parsed = parseYaml(readFileSync(join(pluginsDir, f), 'utf8'));
5174
+ if (parsed && parsed.slug) {
5175
+ installedSlugs.add(parsed.slug);
5176
+ }
5177
+ } catch (e) {}
5178
+ });
5179
+ } catch (e) {}
5180
+ }
5181
+
5182
+ filtered.forEach(p => {
5183
+ const isInst = installedSlugs.has(p.slug) ? ' \x1b[90m(✓ Installed)\x1b[0m' : '';
5184
+ console.log(`\n\x1b[32m* ${p.name}\x1b[0m (v${p.version})${isInst}`);
5185
+ console.log(` slug: \x1b[33m${p.slug}\x1b[0m`);
5186
+ console.log(` source: ${p._source || 'bundled'}`);
5187
+ console.log(` category: ${p.category}`);
5188
+ console.log(` description: ${p.description}`);
5189
+ console.log(` safety: ${p.safety_level || 'sandboxed'} (${p.install_scope || 'declarative'})`);
5190
+ });
5191
+
5192
+ console.log('\nUse \x1b[36mcatalog list --category <category>\x1b[0m to filter listings by category.');
5193
+ console.log('Use \x1b[36mcatalog show <slug>\x1b[0m to inspect capabilities and installation manifest preview.');
5194
+ console.log('Use \x1b[36mcatalog install <slug> --approved\x1b[0m to install a plugin.\n');
5195
+ }
5196
+
5197
+ function handleCatalogSearch(query, options) {
5198
+ const catalog = loadCatalog(options);
5199
+ const plugins = catalog.plugins || [];
5200
+ const lcQuery = query.toLowerCase();
5201
+
5202
+ const matches = plugins.filter(p => {
5203
+ return p.slug.toLowerCase().includes(lcQuery) ||
5204
+ p.name.toLowerCase().includes(lcQuery) ||
5205
+ p.description.toLowerCase().includes(lcQuery) ||
5206
+ p.category.toLowerCase().includes(lcQuery) ||
5207
+ (Array.isArray(p.tags) && p.tags.some(t => t.toLowerCase().includes(lcQuery)));
5208
+ });
5209
+
5210
+ if (options.json) {
5211
+ console.log(JSON.stringify(matches, null, 2));
5212
+ return;
5213
+ }
5214
+
5215
+ console.log(`\n🔍 \x1b[36mSearch Catalog Results for query: "${query}" (${matches.length} matches)\x1b[0m`);
5216
+ console.log('==================================================');
5217
+
5218
+ if (matches.length === 0) {
5219
+ console.log(` \x1b[33mWarning: No plugins found matching '${query}'. Try running 'catalog list' to view all entries.\x1b[0m`);
5220
+ } else {
5221
+ matches.forEach(p => {
5222
+ console.log(`\n\x1b[32m* ${p.name}\x1b[0m (v${p.version}) [slug: \x1b[33m${p.slug}\x1b[0m]`);
5223
+ console.log(` category: ${p.category}`);
5224
+ console.log(` description: ${p.description}`);
5225
+ });
5226
+ }
5227
+ console.log('');
5228
+ }
5229
+
5230
+ function handleCatalogShow(slug, options) {
5231
+ const catalog = loadCatalog(options);
5232
+ const plugins = catalog.plugins || [];
5233
+ const p = plugins.find(item => item.slug === slug);
5234
+
5235
+ if (!p) {
5236
+ console.error(`\x1b[31mError: Plugin with slug '${slug}' not found in catalog.\x1b[0m`);
5237
+ process.exit(1);
5238
+ }
5239
+
5240
+ if (options.json) {
5241
+ console.log(JSON.stringify(p, null, 2));
5242
+ return;
5243
+ }
5244
+
5245
+ console.log(`\n🔍 \x1b[36mCatalog Plugin: ${p.name} (v${p.version})\x1b[0m`);
5246
+ console.log('==================================================');
5247
+ console.log(`\x1b[33mSlug:\x1b[0m ${p.slug}`);
5248
+ console.log(`\x1b[33mSource:\x1b[0m ${p._source || 'bundled'}`);
5249
+ console.log(`\x1b[33mCategory:\x1b[0m ${p.category}`);
5250
+ console.log(`\x1b[33mDescription:\x1b[0m ${p.description}`);
5251
+ console.log(`\x1b[33mRecommended:\x1b[0m ${p.recommended_for}`);
5252
+ console.log(`\x1b[33mSafety Level:\x1b[0m ${p.safety_level} (declarative-only, sandboxed)`);
5253
+ console.log(`\x1b[33mScope:\x1b[0m ${p.install_scope}`);
5254
+
5255
+ if (p.use_cases) {
5256
+ console.log('\n\x1b[33mUse Cases:\x1b[0m');
5257
+ p.use_cases.forEach(uc => console.log(` - ${uc}`));
5258
+ }
5259
+
5260
+ if (p.provided_workflows) {
5261
+ console.log('\n\x1b[33mProvided Workflows:\x1b[0m');
5262
+ p.provided_workflows.forEach(w => console.log(` - \x1b[32m${w}\x1b[0m`));
5263
+ }
5264
+
5265
+ if (p.files_preview) {
5266
+ console.log('\n\x1b[33mPlanned Write Files:\x1b[0m');
5267
+ p.files_preview.forEach(f => console.log(` - \x1b[36m${f.dest}\x1b[0m`));
5268
+ }
5269
+
5270
+ console.log(`\nTo install this plugin, run:`);
5271
+ console.log(` \x1b[36mnpx multimodel-dev-os catalog install ${p.slug} --approved\x1b[0m\n`);
5272
+ }
5273
+
5274
+ function handleCatalogCategories(options) {
5275
+ const catalog = loadCatalog(options);
5276
+ const plugins = catalog.plugins || [];
5277
+ const categories = Array.from(new Set(plugins.map(p => p.category))).sort();
5278
+
5279
+ if (options.json) {
5280
+ console.log(JSON.stringify(categories, null, 2));
5281
+ return;
5282
+ }
5283
+
5284
+ console.log(`\n📚 \x1b[36mMarketplace Categories (${categories.length})\x1b[0m`);
5285
+ console.log('==================================================');
5286
+ categories.forEach(c => console.log(` - ${c}`));
5287
+ console.log('\nUse \x1b[36mcatalog list --category <category>\x1b[0m to list plugins in a category.\n');
5288
+ }
5289
+
5290
+ function handleCatalogInstall(slug, options) {
5291
+ const catalog = loadCatalog(options);
5292
+ const plugins = catalog.plugins || [];
5293
+ const p = plugins.find(item => item.slug === slug);
5294
+
5295
+ if (!p) {
5296
+ console.error(`\x1b[31mError: Plugin with slug '${slug}' not found in catalog.\x1b[0m`);
5297
+ process.exit(1);
5298
+ }
5299
+
5300
+ const policy = loadRegistryPolicy(options.target || process.cwd());
5301
+
5302
+ let srcPath;
5303
+ if (p._source === 'bundled') {
5304
+ srcPath = join(sourceRoot, '.ai', 'plugins', 'catalog', `${slug}.yaml`);
5305
+ } else if (p._source === 'local') {
5306
+ srcPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog', `${slug}.yaml`);
5307
+ } else if (p._source && p._source.startsWith('remote:')) {
5308
+ const regName = p._source.substring(7);
5309
+ const sources = loadRegistrySources();
5310
+ const src = sources.find(s => s.name === regName);
5311
+ if (src) {
5312
+ if (!policy.allow_untrusted_install && (src.trust_level === 'untrusted' || src.trust_level === 'community')) {
5313
+ console.error(`\x1b[31mError: Installation from untrusted or community registry '${regName}' is blocked by policy.\x1b[0m`);
5314
+ console.error(` Registry trust level: ${src.trust_level}`);
5315
+ console.error(` Policy allow_untrusted_install: ${policy.allow_untrusted_install}`);
5316
+ process.exit(1);
5317
+ }
5318
+ }
5319
+ srcPath = join(sourceRoot, '.ai', 'registry-cache', regName, 'catalog', `${slug}.yaml`);
5320
+ } else {
5321
+ srcPath = join(sourceRoot, '.ai', 'plugins', 'catalog', `${slug}.yaml`);
5322
+ }
5323
+
5324
+ if (!existsSync(srcPath)) {
5325
+ console.error(`\x1b[31mError: Packed plugin manifest not found at: ${srcPath}\x1b[0m`);
5326
+ process.exit(1);
5327
+ }
5328
+
5329
+ handlePluginInstall(srcPath, options);
5330
+ }
5331
+
5332
+ function handleCatalogStatus(options) {
5333
+ const catalog = loadCatalog(options);
5334
+ const plugins = catalog.plugins || [];
5335
+ const pluginsDir = getPluginsDir(options.target);
5336
+
5337
+ console.log(`\n📊 \x1b[36mAuditing Catalog Plugins in: ${options.target}\x1b[0m`);
5338
+ console.log('==================================================');
5339
+
5340
+ if (plugins.length === 0) {
5341
+ console.log(' No catalog entries found.');
5342
+ return;
5343
+ }
5344
+
5345
+ plugins.forEach(p => {
5346
+ const slug = p.slug;
5347
+ const destManifest = join(pluginsDir, `${slug}.yaml`);
5348
+ if (!existsSync(destManifest)) {
5349
+ console.log(` - \x1b[33m${p.name}\x1b[0m (v${p.version}): \x1b[90mNot installed\x1b[0m`);
5350
+ console.log(` Install via: \x1b[36mnpx multimodel-dev-os catalog install ${slug} --approved\x1b[0m`);
5351
+ } else {
5352
+ let missingCount = 0;
5353
+ let presentCount = 0;
5354
+
5355
+ try {
5356
+ const targetP = parseYaml(readFileSync(destManifest, 'utf8'));
5357
+ if (Array.isArray(targetP.allowed_file_patterns)) {
5358
+ targetP.allowed_file_patterns.forEach(pat => {
5359
+ const destPath = join(options.target, pat);
5360
+ if (existsSync(destPath) && statSync(destPath).isFile()) {
5361
+ presentCount++;
5362
+ } else {
5363
+ missingCount++;
5364
+ }
5365
+ });
5366
+ }
5367
+
5368
+ const total = presentCount + missingCount;
5369
+ if (total === 0 || missingCount === 0) {
5370
+ console.log(` - \x1b[32m${p.name}\x1b[0m (v${p.version}): \x1b[32m✓ Installed (Up-to-date)\x1b[0m`);
5371
+ } else {
5372
+ console.log(` - \x1b[33m${p.name}\x1b[0m (v${p.version}): \x1b[33m! Incomplete (Missing assets)\x1b[0m (${presentCount}/${total} files present)`);
5373
+ }
5374
+ } catch (e) {
5375
+ console.log(` - \x1b[31m${p.name}\x1b[0m (v${p.version}): \x1b[31mInstalled (Read error: ${e.message})\x1b[0m`);
5376
+ }
5377
+ }
5378
+ });
5379
+
5380
+ console.log('\nUse \x1b[36mcatalog show <slug>\x1b[0m to view detailed plugin metadata.');
5381
+ console.log('Use \x1b[36mcatalog install <slug> --approved\x1b[0m to install or update a plugin.\n');
5382
+ }
5383
+
5384
+ function handleCatalogRecommend(options) {
5385
+ const analysis = getAnalysis(options.target);
5386
+ const catalog = loadCatalog(options);
5387
+ const plugins = catalog.plugins || [];
5388
+
5389
+ const recs = [];
5390
+
5391
+ plugins.forEach(p => {
5392
+ let conf = 0.5;
5393
+ let reason = 'General codebase utility';
5394
+ const signals = [];
5395
+
5396
+ if (p.slug === 'git-workflows') {
5397
+ conf = 0.8;
5398
+ signals.push('Generic repository template matched');
5399
+ if (analysis.githubWorkflows && analysis.githubWorkflows.length > 0) {
5400
+ conf = 0.95;
5401
+ signals.push('Existing GitHub Actions workflows detected');
5402
+ reason = 'Enforces git pre-push and pre-commit checks locally before executing remote pipeline checks.';
5403
+ } else {
5404
+ reason = 'Standard git repository quality and branch cleanliness checks.';
5405
+ }
5406
+ } else if (p.slug === 'nextjs-workflows') {
5407
+ if (analysis.frameworks && analysis.frameworks.some(f => f.toLowerCase().includes('next'))) {
5408
+ conf = 0.95;
5409
+ signals.push('Next.js framework framework signals detected');
5410
+ reason = 'Integrates routing checking and server actions verification rules for App Router.';
5411
+ } else if (analysis.packageScripts && analysis.packageScripts.some(s => s.includes('next'))) {
5412
+ conf = 0.9;
5413
+ signals.push('Next package scripts detected in package.json');
5414
+ reason = 'Configures Next.js specific builder guidelines.';
5415
+ } else {
5416
+ conf = 0.1;
5417
+ }
5418
+ } else if (p.slug === 'wordpress-workflows') {
5419
+ if (analysis.repoType === 'WordPress theme/plugin') {
5420
+ conf = 0.95;
5421
+ signals.push('WordPress folder layout and php structures identified');
5422
+ reason = 'Ensures WordPress coding standards and security hooks validations are applied.';
5423
+ } else if (analysis.language === 'PHP') {
5424
+ conf = 0.6;
5425
+ signals.push('PHP dominant language detected');
5426
+ reason = 'Provides standard boilerplate checkups for PHP sites.';
5427
+ } else {
5428
+ conf = 0.1;
5429
+ }
5430
+ } else if (p.slug === 'ecommerce-workflows') {
5431
+ const isShop = analysis.frameworks && analysis.frameworks.some(f => f.toLowerCase().includes('shopify'));
5432
+ const isShopScript = analysis.packageScripts && analysis.packageScripts.some(s => s.includes('stripe') || s.includes('shop'));
5433
+ if (isShop || isShopScript) {
5434
+ conf = 0.9;
5435
+ signals.push('E-commerce keywords or framework scripts detected');
5436
+ reason = 'Validates payment gateway routes and Stripe webhook security signatures.';
5437
+ } else {
5438
+ let hasKeywords = false;
5439
+ try {
5440
+ const files = readdirSync(options.target);
5441
+ hasKeywords = files.some(f => f.includes('stripe') || f.includes('checkout') || f.includes('payment') || f.includes('cart'));
5442
+ } catch (e) {}
5443
+ if (hasKeywords) {
5444
+ conf = 0.85;
5445
+ signals.push('E-commerce transaction filenames detected');
5446
+ reason = 'Secures checkout endpoints and verifies webhook signature validations.';
5447
+ } else {
5448
+ conf = 0.4;
5449
+ }
5450
+ }
5451
+ } else if (p.slug === 'seo-workflows') {
5452
+ if (analysis.repoType === 'docs') {
5453
+ conf = 0.8;
5454
+ signals.push('Documentation heavy layout detected');
5455
+ reason = 'Audits sitemaps and page heading hierarchies for documentation search optimization.';
5456
+ } else if (analysis.language === 'Markdown-heavy') {
5457
+ conf = 0.75;
5458
+ signals.push('Markdown-heavy content layout detected');
5459
+ reason = 'Enforces metadata validations.';
5460
+ } else {
5461
+ conf = 0.6;
5462
+ signals.push('Frontend presentation site signals detected');
5463
+ reason = 'Validates HTML page hierarchy and meta tag checklist rules.';
5464
+ }
5465
+ } else if (p.slug === 'release-workflows') {
5466
+ if (analysis.repoType === 'library') {
5467
+ conf = 0.9;
5468
+ signals.push('Library/Module repository distribution pattern detected');
5469
+ reason = 'Verifies package hygiene, versions alignment, and npm pre-flight checks.';
5470
+ } else if (analysis.packageScripts && analysis.packageScripts.some(s => s.includes('release') || s.includes('publish') || s.includes('build'))) {
5471
+ conf = 0.8;
5472
+ signals.push('Release/Build commands registered in package.json');
5473
+ reason = 'Maintains release prep checklists and doctor verifications.';
5474
+ } else {
5475
+ conf = 0.5;
5476
+ }
5477
+ }
5478
+
5479
+ if (conf >= 0.5) {
5480
+ recs.push({
5481
+ plugin: p,
5482
+ confidence: conf,
5483
+ signals,
5484
+ reason
5485
+ });
5486
+ }
5487
+ });
5488
+
5489
+ recs.sort((a, b) => b.confidence - a.confidence);
5490
+
5491
+ if (options.json) {
5492
+ console.log(JSON.stringify(recs, null, 2));
5493
+ return;
5494
+ }
5495
+
5496
+ console.log(`\n💡 \x1b[36mMarketplace Recommendations for: ${options.target}\x1b[0m`);
5497
+ console.log('==================================================');
5498
+ if (recs.length === 0) {
5499
+ console.log(' No matching recommendations found.');
5500
+ } else {
5501
+ recs.forEach(r => {
5502
+ console.log(`\n* \x1b[32m${r.plugin.name}\x1b[0m`);
5503
+ console.log(` Detected Signals: \x1b[33m${r.signals.join(', ')}\x1b[0m`);
5504
+ console.log(` Confidence Level: \x1b[35m${(r.confidence * 100).toFixed(0)}%\x1b[0m`);
5505
+ console.log(` Why Recommended: ${r.reason}`);
5506
+ console.log(` Install Command: \x1b[36mnpx multimodel-dev-os catalog install ${r.plugin.slug} --approved\x1b[0m`);
5507
+ console.log(` Safety Notes: Declarative sandbox only (offline, writes to .ai/ & adapters/ only, no scripts)`);
5508
+ });
5509
+ }
5510
+ console.log('');
5511
+ }
5512
+
5513
+ // --- SHA256 Checksum Helper ---
5514
+ function computeSHA256(content) {
5515
+ return createHash('sha256').update(content, 'utf8').digest('hex');
5516
+ }
5517
+
5518
+ // --- Registry Policy Loader ---
5519
+ function loadRegistryPolicy(targetDir) {
5520
+ const defaults = {
5521
+ allow_remote_registries: false,
5522
+ require_approval_for_remote_sync: true,
5523
+ require_checksum: true,
5524
+ require_signature: false,
5525
+ allow_untrusted_install: false,
5526
+ allowed_write_roots: ['.ai/', 'adapters/'],
5527
+ blocked_paths: ['.env', '.npmrc', '.git/', 'node_modules/', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'],
5528
+ max_plugin_files: 20,
5529
+ max_plugin_size_kb: 100,
5530
+ max_registry_cache_size_kb: 512,
5531
+ allowed_file_extensions: ['.md', '.yaml', '.yml', '.json']
5532
+ };
5533
+ const paths = [
5534
+ join(targetDir, '.ai', 'policies', 'registry-policy.yaml'),
5535
+ join(sourceRoot, '.ai', 'policies', 'registry-policy.yaml')
5536
+ ];
5537
+ for (const p of paths) {
5538
+ if (existsSync(p)) {
5539
+ try {
5540
+ const parsed = parseYaml(readFileSync(p, 'utf8'));
5541
+ return { ...defaults, ...parsed };
5542
+ } catch (e) {}
5543
+ }
5544
+ }
5545
+ return defaults;
5546
+ }
5547
+
5548
+ // --- Registry Sources Loader ---
5549
+ function loadRegistrySources() {
5550
+ const paths = [
5551
+ join(sourceRoot, '.ai', 'registries', 'sources.yaml')
5552
+ ];
5553
+ for (const p of paths) {
5554
+ if (existsSync(p)) {
5555
+ try {
5556
+ const parsed = parseYaml(readFileSync(p, 'utf8'));
5557
+ return parsed.sources || [];
5558
+ } catch (e) {}
5559
+ }
5560
+ }
5561
+ return [{ name: 'bundled', type: 'local', url: '.ai/plugins/catalog.yaml', enabled: true, trust_level: 'trusted', safety_policy: 'sandboxed', signature_required: false, checksum_required: false }];
5562
+ }
5563
+
5564
+ function saveRegistrySources(sources) {
5565
+ const path = join(sourceRoot, '.ai', 'registries', 'sources.yaml');
5566
+ let yaml = '# Registry Sources Configuration\n';
5567
+ yaml += '# Remote registries are DISABLED by default.\n';
5568
+ yaml += '# Enable via .ai/policies/registry-policy.yaml (set allow_remote_registries: true)\n\n';
5569
+ yaml += 'sources:\n';
5570
+ sources.forEach(s => {
5571
+ yaml += ` - name: "${s.name}"\n`;
5572
+ yaml += ` type: "${s.type}"\n`;
5573
+ yaml += ` url: "${s.url}"\n`;
5574
+ yaml += ` enabled: ${s.enabled}\n`;
5575
+ yaml += ` trust_level: "${s.trust_level}"\n`;
5576
+ yaml += ` safety_policy: "${s.safety_policy}"\n`;
5577
+ yaml += ` signature_required: ${s.signature_required}\n`;
5578
+ yaml += ` checksum_required: ${s.checksum_required}\n`;
5579
+ if (s.last_synced_at) yaml += ` last_synced_at: "${s.last_synced_at}"\n`;
5580
+ if (s.pinned_commit_or_hash) yaml += ` pinned_commit_or_hash: "${s.pinned_commit_or_hash}"\n`;
5581
+ });
5582
+ writeFileSync(path, yaml, 'utf8');
5583
+ }
5584
+
5585
+ // --- Extended loadCatalog with source parameter ---
5586
+ function loadCatalogFromSource(source, options = {}) {
5587
+ if (!source || source === 'bundled') {
5588
+ return loadCatalog();
5589
+ } else if (source === 'local') {
5590
+ const localPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog.yaml');
5591
+ try {
5592
+ if (existsSync(localPath)) {
5593
+ const reg = parseYaml(readFileSync(localPath, 'utf8'));
5594
+ const catalog = reg.catalog || { plugins: [] };
5595
+ (catalog.plugins || []).forEach(p => { p._source = 'local'; });
5596
+ return catalog;
5597
+ }
5598
+ } catch (e) {}
5599
+ return { plugins: [] };
5600
+ } else if (source.startsWith('remote:')) {
5601
+ const regName = source.substring(7);
5602
+ const cachePath = join(sourceRoot, '.ai', 'registry-cache', regName, 'catalog.yaml');
5603
+ try {
5604
+ if (existsSync(cachePath)) {
5605
+ const reg = parseYaml(readFileSync(cachePath, 'utf8'));
5606
+ const catalog = reg.catalog || { plugins: [] };
5607
+ (catalog.plugins || []).forEach(p => { p._source = `remote:${regName}`; });
5608
+ return catalog;
5609
+ }
5610
+ } catch (e) {}
5611
+ return { plugins: [] };
5612
+ }
5613
+ return { plugins: [] };
5614
+ }
5615
+
5616
+ function loadAllCatalogs(options = {}) {
5617
+ const sources = loadRegistrySources();
5618
+ const policy = loadRegistryPolicy(options.target || process.cwd());
5619
+ const allPlugins = [];
5620
+
5621
+ // Always include bundled
5622
+ const bundled = loadCatalog();
5623
+ (bundled.plugins || []).forEach(p => { p._source = 'bundled'; allPlugins.push(p); });
5624
+
5625
+ // Include local workspace catalog if different from bundled
5626
+ const localPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog.yaml');
5627
+ if (existsSync(localPath)) {
5628
+ try {
5629
+ const localCat = parseYaml(readFileSync(localPath, 'utf8'));
5630
+ const localPlugins = (localCat.catalog || {}).plugins || [];
5631
+ localPlugins.forEach(p => {
5632
+ if (!allPlugins.some(bp => bp.slug === p.slug)) {
5633
+ p._source = 'local';
5634
+ allPlugins.push(p);
5635
+ }
5636
+ });
5637
+ } catch (e) {}
5638
+ }
5639
+
5640
+ // Include remote caches if policy allows
5641
+ if (policy.allow_remote_registries) {
5642
+ sources.filter(s => s.type !== 'local' && s.enabled).forEach(s => {
5643
+ const cachePath = join(sourceRoot, '.ai', 'registry-cache', s.name, 'catalog.yaml');
5644
+ if (existsSync(cachePath)) {
5645
+ try {
5646
+ const remoteCat = parseYaml(readFileSync(cachePath, 'utf8'));
5647
+ const remotePlugins = (remoteCat.catalog || {}).plugins || [];
5648
+ remotePlugins.forEach(p => {
5649
+ if (!allPlugins.some(bp => bp.slug === p.slug)) {
5650
+ p._source = `remote:${s.name}`;
5651
+ allPlugins.push(p);
5652
+ }
5653
+ });
5654
+ } catch (e) {}
5655
+ }
5656
+ });
5657
+ }
5658
+
5659
+ return { plugins: allPlugins };
5660
+ }
5661
+
5662
+ // --- Registry Handlers ---
5663
+
5664
+ function handleRegistryList(options) {
5665
+ const sources = loadRegistrySources();
5666
+ const policy = loadRegistryPolicy(options.target);
5667
+
5668
+ if (options.json) {
5669
+ console.log(JSON.stringify(sources, null, 2));
5670
+ return;
5671
+ }
5672
+
5673
+ console.log(`\n🗂️ \x1b[36mRegistry Sources [v${version}]\x1b[0m`);
5674
+ console.log('==================================================');
5675
+ console.log(`Policy: allow_remote_registries = \x1b[${policy.allow_remote_registries ? '32mtrue' : '33mfalse'}\x1b[0m\n`);
5676
+
5677
+ sources.forEach(s => {
5678
+ const status = s.enabled ? '\x1b[32m● enabled\x1b[0m' : '\x1b[90m○ disabled\x1b[0m';
5679
+ console.log(` \x1b[32m${s.name}\x1b[0m ${status}`);
5680
+ console.log(` type: ${s.type}`);
5681
+ console.log(` url: ${s.url}`);
5682
+ console.log(` trust_level: ${s.trust_level}`);
5683
+ console.log(` safety_policy: ${s.safety_policy}`);
5684
+ console.log(` checksum: ${s.checksum_required ? 'required' : 'not required'}`);
5685
+ console.log(` signature: ${s.signature_required ? 'required' : 'not required'}`);
5686
+ if (s.last_synced_at) console.log(` last_synced: ${s.last_synced_at}`);
5687
+ });
5688
+
5689
+ console.log('\nUse \x1b[36mregistry show <name>\x1b[0m to view detailed source metadata.');
5690
+ console.log('Use \x1b[36mregistry status\x1b[0m to see cache health and sync timestamps.\n');
5691
+ }
5692
+
5693
+ function handleRegistryAdd(name, url, options) {
5694
+ const policy = loadRegistryPolicy(options.target);
5695
+
5696
+ if (!policy.allow_remote_registries) {
5697
+ console.error('\x1b[31mError: Remote registries are disabled by policy.\x1b[0m');
5698
+ console.log('\nTo enable, set \x1b[33mallow_remote_registries: true\x1b[0m in:');
5699
+ console.log(' .ai/policies/registry-policy.yaml\n');
5700
+ process.exit(1);
5701
+ }
5702
+
5703
+ if (!options.approved) {
5704
+ console.error('\x1b[31mError: Registry cannot be added without explicit approval. Pass the --approved flag.\x1b[0m');
5705
+ console.log(`\n\x1b[33mPlanned Action:\x1b[0m Add registry source '${name}' pointing to:`);
5706
+ console.log(` URL: ${url}`);
5707
+ console.log(` Type: https`);
5708
+ console.log(` Trust Level: community`);
5709
+ console.log(` Checksum: required`);
5710
+ console.log(`\nRun with --approved to apply:\n npx multimodel-dev-os registry add ${name} ${url} --approved\n`);
5711
+ process.exit(1);
5712
+ }
5713
+
5714
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
5715
+ console.error(`\x1b[31mError: Registry name '${name}' contains invalid characters. Use only alphanumeric, dash, or underscore.\x1b[0m`);
5716
+ process.exit(1);
5717
+ }
5718
+
5719
+ const sources = loadRegistrySources();
5720
+ if (sources.some(s => s.name === name)) {
5721
+ console.error(`\x1b[31mError: Registry '${name}' already exists. Remove it first with: registry remove ${name} --approved\x1b[0m`);
5722
+ process.exit(1);
5723
+ }
5724
+
5725
+ const type = url.endsWith('.git') ? 'git' : 'https';
5726
+
5727
+ sources.push({
5728
+ name,
5729
+ type,
5730
+ url,
5731
+ enabled: true,
5732
+ trust_level: 'community',
5733
+ safety_policy: 'sandboxed',
5734
+ signature_required: false,
5735
+ checksum_required: true
5736
+ });
5737
+
5738
+ saveRegistrySources(sources);
5739
+ console.log(`\n\x1b[32m✔ Registry '${name}' added successfully!\x1b[0m`);
5740
+ console.log(` Type: ${type}`);
5741
+ console.log(` URL: ${url}`);
5742
+ console.log(` Trust Level: community`);
5743
+ console.log(`\nNext steps:`);
5744
+ console.log(` Sync: npx multimodel-dev-os registry sync ${name} --approved`);
5745
+ console.log(` Browse: npx multimodel-dev-os catalog list --source remote:${name}\n`);
5746
+ }
5747
+
5748
+ function handleRegistryRemove(name, options) {
5749
+ if (name === 'bundled') {
5750
+ console.error('\x1b[31mError: The bundled registry cannot be removed.\x1b[0m');
5751
+ process.exit(1);
5752
+ }
5753
+
5754
+ if (!options.approved) {
5755
+ console.error(`\x1b[31mError: Registry cannot be removed without explicit approval. Pass the --approved flag.\x1b[0m`);
5756
+ console.log(`\n\x1b[33mPlanned Action:\x1b[0m Remove registry source '${name}' and delete cached files.`);
5757
+ console.log(`\nRun with --approved to apply:\n npx multimodel-dev-os registry remove ${name} --approved\n`);
5758
+ process.exit(1);
5759
+ }
5760
+
5761
+ const sources = loadRegistrySources();
5762
+ const idx = sources.findIndex(s => s.name === name);
5763
+ if (idx === -1) {
5764
+ console.error(`\x1b[31mError: Registry '${name}' not found.\x1b[0m`);
5765
+ process.exit(1);
5766
+ }
5767
+
5768
+ sources.splice(idx, 1);
5769
+ saveRegistrySources(sources);
5770
+
5771
+ // Remove cache directory
5772
+ const cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
5773
+ if (existsSync(cacheDir)) {
5774
+ try {
5775
+ const files = readdirSync(cacheDir);
5776
+ files.forEach(f => {
5777
+ const fp = join(cacheDir, f);
5778
+ if (statSync(fp).isFile()) {
5779
+ writeFileSync(fp, ''); // Clear content before unlink
5780
+ }
5781
+ });
5782
+ } catch (e) {}
5783
+ }
5784
+
5785
+ console.log(`\n\x1b[32m✔ Registry '${name}' removed successfully.\x1b[0m`);
5786
+ console.log(` Source entry removed from .ai/registries/sources.yaml`);
5787
+ if (existsSync(cacheDir)) {
5788
+ console.log(` Cache directory cleared: .ai/registry-cache/${name}/`);
5789
+ }
5790
+ console.log('');
5791
+ }
5792
+
5793
+ function handleRegistrySync(name, options) {
5794
+ const policy = loadRegistryPolicy(options.target);
5795
+ const sources = loadRegistrySources();
5796
+ const source = sources.find(s => s.name === name);
5797
+
5798
+ if (!source) {
5799
+ console.error(`\x1b[31mError: Registry '${name}' not found in sources.\x1b[0m`);
5800
+ console.log('Use \x1b[36mregistry list\x1b[0m to view configured sources.');
5801
+ process.exit(1);
5802
+ }
5803
+
5804
+ if (source.type === 'local') {
5805
+ console.log(`\n\x1b[33mNote: Registry '${name}' is a local source and does not require syncing.\x1b[0m\n`);
5806
+ return;
5807
+ }
5808
+
5809
+ if (!policy.allow_remote_registries) {
5810
+ console.error('\x1b[31mError: Remote registries are disabled by policy.\x1b[0m');
5811
+ console.log('\nTo enable, set \x1b[33mallow_remote_registries: true\x1b[0m in:');
5812
+ console.log(' .ai/policies/registry-policy.yaml\n');
5813
+ process.exit(1);
5814
+ }
5815
+
5816
+ if (!options.approved) {
5817
+ console.log(`\n⚠️ \x1b[33mRegistry Sync Refused — Approval Required\x1b[0m`);
5818
+ console.log('==================================================');
5819
+ console.log(`Registry: \x1b[32m${name}\x1b[0m`);
5820
+ console.log(`URL: ${source.url}`);
5821
+ console.log(`Trust Level: ${source.trust_level}`);
5822
+ console.log(`Checksum: ${source.checksum_required ? 'Required (SHA256)' : 'Not required'}`);
5823
+ console.log(`Signature: ${source.signature_required ? 'Required' : 'Not required (v3.0.0)'}`);
5824
+ console.log(`\n\x1b[33mPlanned Actions:\x1b[0m`);
5825
+ console.log(` [DOWNLOAD] catalog.yaml → .ai/registry-cache/${name}/catalog.yaml`);
5826
+ console.log(` [DOWNLOAD] manifest.json → .ai/registry-cache/${name}/manifest.json`);
5827
+ console.log(` [COMPUTE] checksums.json → .ai/registry-cache/${name}/checksums.json`);
5828
+ console.log(`\n\x1b[33mPost-Sync:\x1b[0m`);
5829
+ console.log(` • No files are installed automatically.`);
5830
+ console.log(` • No plugins are activated automatically.`);
5831
+ console.log(` • Use 'catalog list --source remote:${name}' to browse cached entries.`);
5832
+ console.log(` • Use 'catalog install <slug> --approved' to install individual plugins.`);
5833
+ console.log(`\nPolicy Status: allow_remote_registries=${policy.allow_remote_registries}, require_checksum=${policy.require_checksum}`);
5834
+ console.log(`\nRun with --approved to proceed:`);
5835
+ console.log(` npx multimodel-dev-os registry sync ${name} --approved\n`);
5836
+ process.exit(1);
5837
+ }
5838
+
5839
+ // Sync requires network - output sync initiation message
5840
+ const cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
5841
+ if (!existsSync(cacheDir)) {
5842
+ mkdirSync(cacheDir, { recursive: true });
5843
+ }
5844
+
5845
+ console.log(`\n🔄 \x1b[36mSyncing Registry: ${name}\x1b[0m`);
5846
+ console.log('==================================================');
5847
+
5848
+ // Attempt HTTPS download using Node built-in
5849
+ const url = source.url;
5850
+ const catalogUrl = url.endsWith('/') ? `${url}catalog.yaml` : url;
5851
+ const manifestUrl = catalogUrl.replace(/catalog\.yaml$/, 'manifest.json');
5852
+
5853
+ try {
5854
+ const catalogDest = join(cacheDir, 'catalog.yaml');
5855
+ const manifestDest = join(cacheDir, 'manifest.json');
5856
+
5857
+ // Synchronous HTTP download using inline Node script via execSync
5858
+ const fetchUrlSync = (targetUrl) => {
5859
+ const script = `
5860
+ const mod = require('${targetUrl.startsWith('https') ? 'https' : 'http'}');
5861
+ mod.get('${targetUrl}', (res) => {
5862
+ if (res.statusCode !== 200) { process.stderr.write('HTTP_ERROR:' + res.statusCode); process.exit(1); }
5863
+ let d = '';
5864
+ res.on('data', c => d += c);
5865
+ res.on('end', () => process.stdout.write(d));
5866
+ }).on('error', e => { process.stderr.write('NET_ERROR:' + e.message); process.exit(1); });
5867
+ `;
5868
+ return execSync(`node -e "${script.replace(/\n/g, ' ').replace(/"/g, '\\"')}"`, { encoding: 'utf8', timeout: 30000 });
5869
+ };
5870
+
5871
+ console.log(`Downloading: ${catalogUrl}`);
5872
+ console.log(` → .ai/registry-cache/${name}/catalog.yaml ...`);
5873
+
5874
+ const catalogData = fetchUrlSync(catalogUrl);
5875
+ writeFileSync(catalogDest, catalogData, 'utf8');
5876
+ const catalogSize = (Buffer.byteLength(catalogData) / 1024).toFixed(1);
5877
+ console.log(` → OK (${catalogSize}KB)`);
5878
+
5879
+ let manifestData = null;
5880
+ try {
5881
+ console.log(`Downloading: ${manifestUrl}`);
5882
+ console.log(` → .ai/registry-cache/${name}/manifest.json ...`);
5883
+ manifestData = fetchUrlSync(manifestUrl);
5884
+ writeFileSync(manifestDest, manifestData, 'utf8');
5885
+ const manifestSize = (Buffer.byteLength(manifestData) / 1024).toFixed(1);
5886
+ console.log(` → OK (${manifestSize}KB)`);
5887
+ } catch (e) {
5888
+ console.log(` → \x1b[33mNot found (optional)\x1b[0m`);
5889
+ }
4942
5890
 
5891
+ // Compute checksums
5892
+ console.log('Computing checksums...');
5893
+ const checksums = {
5894
+ 'catalog.yaml': `sha256:${computeSHA256(catalogData)}`
5895
+ };
5896
+ if (manifestData) {
5897
+ checksums['manifest.json'] = `sha256:${computeSHA256(manifestData)}`;
5898
+ }
5899
+
5900
+ const baseUrl = catalogUrl.substring(0, catalogUrl.lastIndexOf('/') + 1);
5901
+ let totalSize = Buffer.byteLength(catalogData) + (manifestData ? Buffer.byteLength(manifestData) : 0);
5902
+
5903
+ if (manifestData) {
5904
+ try {
5905
+ const manifestObj = JSON.parse(manifestData);
5906
+ if (manifestObj.files_hashes) {
5907
+ for (const [file, hash] of Object.entries(manifestObj.files_hashes)) {
5908
+ if (file === 'catalog.yaml' || file === 'manifest.json') continue;
5909
+
5910
+ // Check path safety inside registry cache
5911
+ const fileDest = join(cacheDir, file);
5912
+ const relativeToCache = relative(cacheDir, fileDest);
5913
+ if (relativeToCache.includes('..') || isAbsolute(relativeToCache)) {
5914
+ console.error(`\x1b[31mError: Safe path violation in manifest files list: ${file}\x1b[0m`);
5915
+ process.exit(1);
5916
+ }
5917
+
5918
+ console.log(`Downloading: ${baseUrl}${file}`);
5919
+ console.log(` → .ai/registry-cache/${name}/${file} ...`);
5920
+ const fileData = fetchUrlSync(`${baseUrl}${file}`);
5921
+
5922
+ totalSize += Buffer.byteLength(fileData);
5923
+ if (totalSize > policy.max_registry_cache_size_kb * 1024) {
5924
+ console.error(`\x1b[31mError: Registry cache size limit exceeded (max: ${policy.max_registry_cache_size_kb}KB).\x1b[0m`);
5925
+ process.exit(1);
5926
+ }
5927
+
5928
+ const fileDir = dirname(fileDest);
5929
+ if (!existsSync(fileDir)) {
5930
+ mkdirSync(fileDir, { recursive: true });
5931
+ }
5932
+ writeFileSync(fileDest, fileData, 'utf8');
5933
+ const fileSize = (Buffer.byteLength(fileData) / 1024).toFixed(1);
5934
+ console.log(` → OK (${fileSize}KB)`);
5935
+
5936
+ const actualHash = computeSHA256(fileData);
5937
+ const expectedHash = hash.replace('sha256:', '');
5938
+ if (policy.require_checksum && actualHash !== expectedHash) {
5939
+ console.error(`\x1b[31mError: Checksum verification failed for synced file: ${file}\x1b[0m`);
5940
+ console.error(` Expected: ${expectedHash}`);
5941
+ console.error(` Actual: ${actualHash}`);
5942
+ process.exit(1);
5943
+ }
5944
+ checksums[file] = `sha256:${actualHash}`;
5945
+ }
5946
+ }
5947
+ } catch (err) {
5948
+ console.error(`\x1b[31mError: Failed to process registry manifest files: ${err.message}\x1b[0m`);
5949
+ process.exit(1);
5950
+ }
5951
+ }
5952
+
5953
+ const checksumsJson = JSON.stringify(checksums, null, 2);
5954
+ writeFileSync(join(cacheDir, 'checksums.json'), checksumsJson, 'utf8');
5955
+ console.log(` → .ai/registry-cache/${name}/checksums.json ... OK`);
5956
+
5957
+ // Verify checksum against manifest if available
5958
+ if (policy.require_checksum && manifestData) {
5959
+ try {
5960
+ const manifest = JSON.parse(manifestData);
5961
+ if (manifest.catalog_hash) {
5962
+ const expectedHash = manifest.catalog_hash.replace('sha256:', '');
5963
+ const actualHash = computeSHA256(catalogData);
5964
+ if (expectedHash === actualHash) {
5965
+ console.log(`\n\x1b[32mChecksum verification: PASSED\x1b[0m`);
5966
+ } else {
5967
+ console.error(`\n\x1b[31mChecksum verification: FAILED\x1b[0m`);
5968
+ console.error(` Expected: ${expectedHash}`);
5969
+ console.error(` Actual: ${actualHash}`);
5970
+ process.exit(1);
5971
+ }
5972
+ }
5973
+ } catch (e) {}
5974
+ }
5975
+
5976
+ // Update last_synced_at
5977
+ source.last_synced_at = new Date().toISOString();
5978
+ source.pinned_commit_or_hash = computeSHA256(catalogData);
5979
+ saveRegistrySources(sources);
5980
+
5981
+ // Count plugins
5982
+ let pluginCount = 0;
5983
+ try {
5984
+ const catParsed = parseYaml(catalogData);
5985
+ pluginCount = ((catParsed.catalog || {}).plugins || []).length;
5986
+ } catch (e) {}
5987
+
5988
+ console.log(`\n\x1b[32m✔ Registry '${name}' synced successfully!\x1b[0m`);
5989
+ console.log(` Cache location: .ai/registry-cache/${name}/`);
5990
+ console.log(` Plugins cached: ${pluginCount} entries`);
5991
+ console.log(` Checksum status: VERIFIED (SHA256)`);
5992
+ console.log(` Last synced: ${source.last_synced_at}`);
5993
+ console.log(`\nNext steps:`);
5994
+ console.log(` • Browse: npx multimodel-dev-os catalog list --source remote:${name}`);
5995
+ console.log(` • Verify: npx multimodel-dev-os registry verify ${name}`);
5996
+ console.log(` • Install: npx multimodel-dev-os catalog install <slug> --approved\n`);
5997
+ } catch (e) {
5998
+ console.error(`\n\x1b[31mSync failed: ${e.message}\x1b[0m`);
5999
+ console.log('\nPossible causes:');
6000
+ console.log(' • Network unreachable or URL invalid');
6001
+ console.log(' • Remote server returned an error');
6002
+ console.log(` • Check URL: ${catalogUrl}\n`);
6003
+ process.exit(1);
6004
+ }
6005
+ }
6006
+
6007
+ function handleRegistryStatus(options) {
6008
+ const sources = loadRegistrySources();
6009
+ const policy = loadRegistryPolicy(options.target);
6010
+
6011
+ if (options.json) {
6012
+ console.log(JSON.stringify({ sources, policy: { allow_remote_registries: policy.allow_remote_registries, require_checksum: policy.require_checksum } }, null, 2));
6013
+ return;
6014
+ }
6015
+
6016
+ console.log(`\n📊 \x1b[36mRegistry Status [v${version}]\x1b[0m`);
6017
+ console.log('==================================================');
6018
+ console.log(`\x1b[33mPolicy:\x1b[0m`);
6019
+ console.log(` allow_remote_registries: \x1b[${policy.allow_remote_registries ? '32mtrue' : '33mfalse'}\x1b[0m`);
6020
+ console.log(` require_checksum: ${policy.require_checksum}`);
6021
+ console.log(` require_signature: ${policy.require_signature}`);
6022
+ console.log(` allow_untrusted_install: ${policy.allow_untrusted_install}`);
6023
+ console.log(` max_plugin_files: ${policy.max_plugin_files}`);
6024
+ console.log(` max_plugin_size_kb: ${policy.max_plugin_size_kb}`);
6025
+ console.log(` max_registry_cache_size: ${policy.max_registry_cache_size_kb}KB`);
6026
+
6027
+ console.log(`\n\x1b[33mSources:\x1b[0m`);
6028
+ sources.forEach(s => {
6029
+ const status = s.enabled ? '\x1b[32m● enabled\x1b[0m' : '\x1b[90m○ disabled\x1b[0m';
6030
+ const synced = s.last_synced_at ? `synced: ${s.last_synced_at}` : 'never synced';
6031
+ const cacheDir = join(sourceRoot, '.ai', 'registry-cache', s.name);
6032
+ const hasCache = s.type !== 'local' && existsSync(cacheDir);
6033
+
6034
+ console.log(` ${s.name} ${status} (${s.type}, ${s.trust_level})`);
6035
+ if (s.type !== 'local') {
6036
+ console.log(` URL: ${s.url}`);
6037
+ console.log(` Cache: ${hasCache ? '\x1b[32mcached\x1b[0m' : '\x1b[90mnot cached\x1b[0m'}`);
6038
+ console.log(` Sync: ${synced}`);
6039
+ }
6040
+ });
6041
+
6042
+ console.log('\nUse \x1b[36mregistry verify <name>\x1b[0m to check cache integrity.');
6043
+ console.log('Use \x1b[36mregistry sync <name> --approved\x1b[0m to refresh a remote cache.\n');
6044
+ }
6045
+
6046
+ function handleRegistryVerify(name, options) {
6047
+ console.log(`\n🔍 \x1b[36mVerifying Registry: ${name}\x1b[0m`);
6048
+ console.log('==================================================');
6049
+
6050
+ if (name === 'bundled') {
6051
+ const catalogPath = join(sourceRoot, '.ai', 'plugins', 'catalog.yaml');
6052
+ if (!existsSync(catalogPath)) {
6053
+ console.error('\x1b[31mError: Bundled catalog.yaml not found.\x1b[0m');
6054
+ process.exit(1);
6055
+ }
6056
+ const content = readFileSync(catalogPath, 'utf8');
6057
+ const hash = computeSHA256(content);
6058
+ console.log(` File: .ai/plugins/catalog.yaml`);
6059
+ console.log(` SHA256: ${hash}`);
6060
+ console.log(` Status: \x1b[32m✓ Present and readable\x1b[0m`);
6061
+
6062
+ // Verify it parses
6063
+ try {
6064
+ const parsed = parseYaml(content);
6065
+ const pluginCount = ((parsed.catalog || {}).plugins || []).length;
6066
+ console.log(` Plugins: ${pluginCount} entries parsed successfully`);
6067
+ console.log(`\n\x1b[32m✔ Bundled registry verification passed.\x1b[0m\n`);
6068
+ } catch (e) {
6069
+ console.error(`\n\x1b[31m✗ Bundled registry verification failed: ${e.message}\x1b[0m\n`);
6070
+ process.exit(1);
6071
+ }
6072
+ return;
6073
+ }
6074
+
6075
+ // Verify remote cache
6076
+ const cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
6077
+ if (!existsSync(cacheDir)) {
6078
+ console.error(`\x1b[31mError: No cache found for registry '${name}'. Run registry sync first.\x1b[0m`);
6079
+ process.exit(1);
6080
+ }
6081
+
6082
+ const checksumPath = join(cacheDir, 'checksums.json');
6083
+ if (!existsSync(checksumPath)) {
6084
+ console.error(`\x1b[31mError: No checksums.json found in cache for '${name}'.\x1b[0m`);
6085
+ process.exit(1);
6086
+ }
6087
+
6088
+ try {
6089
+ const checksums = JSON.parse(readFileSync(checksumPath, 'utf8'));
6090
+ let allPassed = true;
6091
+
6092
+ Object.entries(checksums).forEach(([file, expectedHash]) => {
6093
+ const filePath = join(cacheDir, file);
6094
+ if (!existsSync(filePath)) {
6095
+ console.log(` \x1b[31m✗ ${file}: MISSING\x1b[0m`);
6096
+ allPassed = false;
6097
+ return;
6098
+ }
6099
+ const content = readFileSync(filePath, 'utf8');
6100
+ const actualHash = `sha256:${computeSHA256(content)}`;
6101
+ if (actualHash === expectedHash) {
6102
+ console.log(` \x1b[32m✓ ${file}: VERIFIED\x1b[0m`);
6103
+ } else {
6104
+ console.log(` \x1b[31m✗ ${file}: MISMATCH\x1b[0m`);
6105
+ console.log(` Expected: ${expectedHash}`);
6106
+ console.log(` Actual: ${actualHash}`);
6107
+ allPassed = false;
6108
+ }
6109
+ });
6110
+
6111
+ if (allPassed) {
6112
+ console.log(`\n\x1b[32m✔ Registry '${name}' verification passed.\x1b[0m\n`);
6113
+ } else {
6114
+ console.error(`\n\x1b[31m✗ Registry '${name}' verification failed. Re-sync recommended.\x1b[0m\n`);
6115
+ process.exit(1);
6116
+ }
6117
+ } catch (e) {
6118
+ console.error(`\x1b[31mError: Failed to read checksums: ${e.message}\x1b[0m`);
6119
+ process.exit(1);
6120
+ }
6121
+ }
6122
+
6123
+ function handleRegistryShow(name, options) {
6124
+ const sources = loadRegistrySources();
6125
+ const source = sources.find(s => s.name === name);
6126
+
6127
+ if (!source) {
6128
+ console.error(`\x1b[31mError: Registry '${name}' not found.\x1b[0m`);
6129
+ process.exit(1);
6130
+ }
6131
+
6132
+ if (options.json) {
6133
+ console.log(JSON.stringify(source, null, 2));
6134
+ return;
6135
+ }
6136
+
6137
+ console.log(`\n🔍 \x1b[36mRegistry Source: ${name}\x1b[0m`);
6138
+ console.log('==================================================');
6139
+ console.log(`\x1b[33mName:\x1b[0m ${source.name}`);
6140
+ console.log(`\x1b[33mType:\x1b[0m ${source.type}`);
6141
+ console.log(`\x1b[33mURL:\x1b[0m ${source.url}`);
6142
+ console.log(`\x1b[33mEnabled:\x1b[0m ${source.enabled}`);
6143
+ console.log(`\x1b[33mTrust Level:\x1b[0m ${source.trust_level}`);
6144
+ console.log(`\x1b[33mSafety Policy:\x1b[0m ${source.safety_policy}`);
6145
+ console.log(`\x1b[33mChecksum:\x1b[0m ${source.checksum_required ? 'Required' : 'Not required'}`);
6146
+ console.log(`\x1b[33mSignature:\x1b[0m ${source.signature_required ? 'Required' : 'Not required'}`);
6147
+
6148
+ if (source.last_synced_at) {
6149
+ console.log(`\x1b[33mLast Synced:\x1b[0m ${source.last_synced_at}`);
6150
+ }
6151
+ if (source.pinned_commit_or_hash) {
6152
+ console.log(`\x1b[33mPinned Hash:\x1b[0m ${source.pinned_commit_or_hash}`);
6153
+ }
6154
+
6155
+ // Show cache status for remote registries
6156
+ if (source.type !== 'local') {
6157
+ const cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
6158
+ if (existsSync(cacheDir)) {
6159
+ const catalogPath = join(cacheDir, 'catalog.yaml');
6160
+ if (existsSync(catalogPath)) {
6161
+ try {
6162
+ const parsed = parseYaml(readFileSync(catalogPath, 'utf8'));
6163
+ const count = ((parsed.catalog || {}).plugins || []).length;
6164
+ console.log(`\x1b[33mCached Plugins:\x1b[0m ${count} entries`);
6165
+ } catch (e) {
6166
+ console.log(`\x1b[33mCached Plugins:\x1b[0m \x1b[31m(parse error)\x1b[0m`);
6167
+ }
6168
+ } else {
6169
+ console.log(`\x1b[33mCache Status:\x1b[0m \x1b[90mEmpty\x1b[0m`);
6170
+ }
6171
+ } else {
6172
+ console.log(`\x1b[33mCache Status:\x1b[0m \x1b[90mNot synced\x1b[0m`);
6173
+ }
6174
+ }
6175
+
6176
+ console.log('');
6177
+ }
6178
+
6179
+ function handleRegistryCacheClear(options) {
6180
+ if (!options.approved) {
6181
+ console.error('\x1b[31mError: Cache cannot be cleared without explicit approval. Pass the --approved flag.\x1b[0m');
6182
+ const cacheRoot = join(sourceRoot, '.ai', 'registry-cache');
6183
+ if (existsSync(cacheRoot)) {
6184
+ const dirs = readdirSync(cacheRoot).filter(d => d !== 'README.md');
6185
+ console.log(`\n\x1b[33mPlanned Action:\x1b[0m Clear ${dirs.length} cached registry directories:`);
6186
+ dirs.forEach(d => console.log(` - .ai/registry-cache/${d}/`));
6187
+ } else {
6188
+ console.log('\n\x1b[33mNo cache directories found.\x1b[0m');
6189
+ }
6190
+ console.log(`\nRun with --approved to apply:\n npx multimodel-dev-os registry cache clear --approved\n`);
6191
+ process.exit(1);
6192
+ }
6193
+
6194
+ const cacheRoot = join(sourceRoot, '.ai', 'registry-cache');
6195
+ if (!existsSync(cacheRoot)) {
6196
+ console.log('\n\x1b[33mNo registry cache directory found. Nothing to clear.\x1b[0m\n');
6197
+ return;
6198
+ }
6199
+
6200
+ const entries = readdirSync(cacheRoot).filter(d => d !== 'README.md');
6201
+ let cleared = 0;
6202
+ entries.forEach(d => {
6203
+ const dirPath = join(cacheRoot, d);
6204
+ try {
6205
+ if (statSync(dirPath).isDirectory()) {
6206
+ const files = readdirSync(dirPath);
6207
+ files.forEach(f => {
6208
+ const fp = join(dirPath, f);
6209
+ if (statSync(fp).isFile()) {
6210
+ writeFileSync(fp, '');
6211
+ }
6212
+ });
6213
+ cleared++;
6214
+ }
6215
+ } catch (e) {}
6216
+ });
6217
+
6218
+ console.log(`\n\x1b[32m✔ Registry cache cleared.\x1b[0m`);
6219
+ console.log(` Directories processed: ${cleared}`);
6220
+ console.log(` Cache root: .ai/registry-cache/\n`);
6221
+ }