multimodel-dev-os 2.8.0 → 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 (51) 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 +7 -6
  22. package/bin/multimodel-dev-os.js +1421 -61
  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 +22 -9
  30. package/docs/faq.md +20 -1
  31. package/docs/plugin-authoring.md +7 -1
  32. package/docs/plugin-catalog.md +35 -0
  33. package/docs/plugin-hooks.md +15 -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/tui-safety.md +1 -1
  45. package/docs/v2-roadmap.md +21 -7
  46. package/docs/workflow-marketplace.md +22 -0
  47. package/docs/workflow-orchestration.md +6 -0
  48. package/package.json +1 -1
  49. package/scripts/install.ps1 +0 -0
  50. package/scripts/install.sh +0 -0
  51. package/scripts/verify.js +546 -10
@@ -52,7 +52,11 @@ function parseArgs(args) {
52
52
  title: null,
53
53
  approved: false,
54
54
  intelligence: false,
55
- onboarding: false
55
+ onboarding: false,
56
+ listActions: false,
57
+ category: null,
58
+ source: null,
59
+ allSources: false
56
60
  };
57
61
 
58
62
  for (let i = 0; i < args.length; i++) {
@@ -67,6 +71,8 @@ function parseArgs(args) {
67
71
  params.caveman = true;
68
72
  } else if (arg === '--dry-run' || arg === '-d') {
69
73
  params.dryRun = true;
74
+ } else if (arg === '--list-actions') {
75
+ params.listActions = true;
70
76
  } else if (arg === '--force' || arg === '-f') {
71
77
  params.force = true;
72
78
  } else if (arg === '--help' || arg === '-h') {
@@ -105,6 +111,12 @@ function parseArgs(args) {
105
111
  params.title = args[++i];
106
112
  } else if (arg === '--approved') {
107
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;
108
120
  } else if (!params.command && !arg.startsWith('-')) {
109
121
  params.command = arg;
110
122
  }
@@ -119,7 +131,7 @@ function getPositionalArgs(args) {
119
131
  if (arg === '--target' || arg === '-t' || arg === '--template' || arg === '--adapter' || arg === '-a' ||
120
132
  arg === '--threshold' || arg === '--registry' || arg === '--model-preset' || arg === '--agent' ||
121
133
  arg === '--stack' || arg === '--mobile' || arg === '--type' || arg === '--tags' || arg === '--files' ||
122
- arg === '--title') {
134
+ arg === '--title' || arg === '--category') {
123
135
  i++; // skip next arg (its value)
124
136
  } else if (arg.startsWith('-')) {
125
137
  // it's a flag, skip
@@ -446,6 +458,96 @@ if (COMMAND === 'init') {
446
458
  console.log('Example: node bin/multimodel-dev-os.js plugin list');
447
459
  process.exit(1);
448
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
+ }
449
551
  } else {
450
552
  console.error(`\x1b[31mUnknown command: ${COMMAND}\x1b[0m`);
451
553
  showHelp();
@@ -469,6 +571,8 @@ function showHelp() {
469
571
  console.log(' onboard <subcmd> Safely integrate MultiModel Dev OS into existing repo (subcmd: analyze, recommend, plan, apply, status)');
470
572
  console.log(' adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)');
471
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)');
472
576
  console.log(' verify Validate structural integrity of an existing project');
473
577
  console.log(' templates List all built-in template profiles with details');
474
578
  console.log(' list-templates Alias for templates command');
@@ -491,6 +595,9 @@ function showHelp() {
491
595
  console.log(' --type <type> Feedback classification (correction, preference, bug, etc.)');
492
596
  console.log(' --tags <list> Comma-separated descriptor tags for feedback');
493
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');
494
601
  console.log(' --title <text> Specifies title for codebase improvement proposal');
495
602
  console.log(' --approved Explicitly approve and execute proposal/onboarding/adapter sync writes');
496
603
  console.log(' --template <name> Template profile: nextjs-saas, expo-react-native-android, etc.');
@@ -1069,6 +1176,30 @@ function handleValidate(options) {
1069
1176
  }
1070
1177
 
1071
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
+
1072
1203
  function parseYaml(content) {
1073
1204
  try {
1074
1205
  const root = {};
@@ -1076,7 +1207,21 @@ function parseYaml(content) {
1076
1207
 
1077
1208
  const lines = content.split(/\r?\n/);
1078
1209
  for (let line of lines) {
1079
- 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
+ }
1080
1225
  if (commentIdx !== -1) {
1081
1226
  line = line.substring(0, commentIdx);
1082
1227
  }
@@ -1108,17 +1253,26 @@ function parseYaml(content) {
1108
1253
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1109
1254
  val = val.substring(1, val.length - 1);
1110
1255
  }
1256
+ if (val.startsWith('[') && val.endsWith(']')) {
1257
+ val = parseFlowArray(val);
1258
+ }
1111
1259
  parent.obj.push(val);
1112
1260
  } else {
1113
1261
  const key = trimmed.substring(0, colonIdx).trim();
1114
1262
  let val = trimmed.substring(colonIdx + 1).trim();
1263
+ let isQuoted = false;
1115
1264
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1116
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);
1117
1275
  }
1118
- if (val === 'true') val = true;
1119
- else if (val === 'false') val = false;
1120
- else if (val === 'null') val = null;
1121
- else if (/^\d+$/.test(val)) val = parseInt(val, 10);
1122
1276
 
1123
1277
  const newObj = { [key]: val };
1124
1278
  parent.obj.push(newObj);
@@ -1130,14 +1284,19 @@ function parseYaml(content) {
1130
1284
 
1131
1285
  const key = trimmed.substring(0, colonIdx).trim();
1132
1286
  let val = trimmed.substring(colonIdx + 1).trim();
1133
-
1287
+ let isQuoted = false;
1134
1288
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1135
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);
1136
1299
  }
1137
- if (val === 'true') val = true;
1138
- else if (val === 'false') val = false;
1139
- else if (val === 'null') val = null;
1140
- else if (/^\d+$/.test(val)) val = parseInt(val, 10);
1141
1300
 
1142
1301
  if (val === '') {
1143
1302
  parent.obj[key] = {};
@@ -4363,6 +4522,8 @@ function handleDashboard(options) {
4363
4522
  { name: 'Adapter Synchronization...', action: 'submenu', menu: 'adapter' },
4364
4523
  { name: 'Memory & Intelligence...', action: 'submenu', menu: 'memory' },
4365
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' },
4366
4527
  { name: 'Quality Gates & Diagnostics...', action: 'submenu', menu: 'quality' },
4367
4528
  { name: 'Plugins Status Overview', action: 'command', command: 'plugin status' },
4368
4529
  { name: 'Exit Command Center', action: 'exit' }
@@ -4399,36 +4560,54 @@ function handleDashboard(options) {
4399
4560
  { name: 'Proposals: Propose improvement proposal', action: 'command', command: 'improve propose' },
4400
4561
  { name: 'Proposals: Review active proposals list', action: 'command', command: 'improve review' }
4401
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
+ ],
4402
4569
  quality: [
4403
4570
  { name: '← Back to Main Menu', action: 'back' },
4404
4571
  { name: 'Doctor: Run Advisory Diagnostics', action: 'command', command: 'doctor' },
4405
4572
  { name: 'Validate: Strict Schema Compliance', action: 'command', command: 'validate' },
4406
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' }
4407
4581
  ]
4408
4582
  };
4409
4583
 
4410
- if (!process.stdout.isTTY || !process.stdin.isTTY || options.dryRun) {
4411
- console.log(`\n🧠 \x1b[36mMultiModel Dev OS Command Center (Headless mode)\x1b[0m`);
4584
+ if (!process.stdout.isTTY || !process.stdin.isTTY || options.dryRun || options.listActions) {
4585
+ console.log(`\n📊 \x1b[36mMultiModel Dev OS Command Center (Headless/CI Preview)\x1b[0m`);
4586
+ console.log(`Target Workspace: \x1b[32m${options.target}\x1b[0m`);
4412
4587
  console.log('==================================================');
4588
+
4589
+ const targetFlag = options.target === process.cwd() ? '' : ` --target "${options.target}"`;
4590
+
4413
4591
  mainMenu.forEach(item => {
4414
4592
  if (item.action === 'command') {
4415
- console.log(` - ${item.name}: equivalent command: "npx multimodel-dev-os ${item.command}"`);
4593
+ console.log(` \x1b[33m•\x1b[0m ${item.name.padEnd(30)} \x1b[36mnpx multimodel-dev-os ${item.command}${targetFlag}\x1b[0m`);
4416
4594
  } else if (item.action === 'submenu') {
4417
- console.log(` - ${item.name}`);
4595
+ console.log(`\n \x1b[35m[${item.name.replace('...', '')}]\x1b[0m`);
4418
4596
  submenus[item.menu].forEach(sub => {
4419
4597
  if (sub.action === 'command') {
4420
- console.log(` └─ ${sub.name}: equivalent command: "npx multimodel-dev-os ${sub.command}"`);
4598
+ console.log(` └─ ${sub.name.padEnd(35)} \x1b[36mnpx multimodel-dev-os ${sub.command}${targetFlag}\x1b[0m`);
4421
4599
  }
4422
4600
  });
4423
4601
  }
4424
4602
  });
4425
- console.log('');
4603
+ console.log('\n\x1b[90m(Run with -t or --target to specify another workspace directory)\x1b[0m\n');
4426
4604
  return;
4427
4605
  }
4428
4606
 
4429
4607
  const runCommandWrapper = (cmdStr) => {
4430
4608
  console.clear();
4431
- console.log(`\n\x1b[36mRunning Command:\x1b[0m npx multimodel-dev-os ${cmdStr}`);
4609
+ const targetFlag = options.target === process.cwd() ? '' : ` --target "${options.target}"`;
4610
+ console.log(`\n\x1b[36mRunning Command:\x1b[0m npx multimodel-dev-os ${cmdStr}${targetFlag}`);
4432
4611
  console.log('--------------------------------------------------\n');
4433
4612
  try {
4434
4613
  const cliPath = join(sourceRoot, 'bin', 'multimodel-dev-os.js');
@@ -4473,6 +4652,9 @@ function getPluginsDir(targetDir) {
4473
4652
 
4474
4653
  function handlePluginList(options) {
4475
4654
  const pluginsDir = getPluginsDir(options.target);
4655
+ const rawRelPath = relative(process.cwd(), join(sourceRoot, '.ai', 'plugins', 'plugin.example.yaml')).replace(/\\/g, '/');
4656
+ const examplePath = rawRelPath.startsWith('.') ? rawRelPath : `./${rawRelPath}`;
4657
+
4476
4658
  if (!existsSync(pluginsDir)) {
4477
4659
  if (options.json) {
4478
4660
  console.log('[]');
@@ -4481,7 +4663,7 @@ function handlePluginList(options) {
4481
4663
  console.log(`\n🔌 \x1b[36mInstalled Plugins in: ${options.target}\x1b[0m`);
4482
4664
  console.log('==================================================');
4483
4665
  console.log(' No plugins installed. Try:');
4484
- console.log(' npx multimodel-dev-os plugin install .ai/plugins/plugin.example.yaml --approved');
4666
+ console.log(` npx multimodel-dev-os plugin install ${examplePath} --approved`);
4485
4667
  console.log('');
4486
4668
  return;
4487
4669
  }
@@ -4509,7 +4691,8 @@ function handlePluginList(options) {
4509
4691
  console.log(`\n🔌 \x1b[36mInstalled Plugins in: ${options.target} (${plugins.length})\x1b[0m`);
4510
4692
  console.log('==================================================');
4511
4693
  if (plugins.length === 0) {
4512
- console.log(' No plugins installed.');
4694
+ console.log(' No plugins installed. Try:');
4695
+ console.log(` npx multimodel-dev-os plugin install ${examplePath} --approved`);
4513
4696
  } else {
4514
4697
  plugins.forEach(p => {
4515
4698
  console.log(`\n\x1b[32m* ${p.name} (v${p.version || '1.0.0'})\x1b[0m [slug: \x1b[33m${p.slug}\x1b[0m]`);
@@ -4521,6 +4704,11 @@ function handlePluginList(options) {
4521
4704
  }
4522
4705
 
4523
4706
  function handlePluginShow(slug, options) {
4707
+ if (!/^[a-z0-9-_]+$/i.test(slug)) {
4708
+ console.error(`\x1b[31mError: Invalid plugin slug '${slug}'. Slugs must be alphanumeric with dashes or underscores only.\x1b[0m`);
4709
+ process.exit(1);
4710
+ }
4711
+
4524
4712
  const pluginsDir = getPluginsDir(options.target);
4525
4713
  let p = null;
4526
4714
  if (existsSync(pluginsDir)) {
@@ -4538,6 +4726,7 @@ function handlePluginShow(slug, options) {
4538
4726
 
4539
4727
  if (!p) {
4540
4728
  console.error(`\x1b[31mError: Plugin with slug '${slug}' is not installed.\x1b[0m`);
4729
+ console.error(` Run \x1b[36mplugin list\x1b[0m to see installed plugins, or validate a new plugin config using \x1b[36mplugin validate <path>\x1b[0m.`);
4541
4730
  process.exit(1);
4542
4731
  }
4543
4732
 
@@ -4586,13 +4775,14 @@ function handlePluginValidate(pluginPath, options) {
4586
4775
  }
4587
4776
 
4588
4777
  console.log(`\n📋 \x1b[34mValidating Plugin: ${pluginPath}\x1b[0m`);
4778
+ console.log('==================================================');
4589
4779
 
4590
4780
  let errors = 0;
4591
4781
  let plugin = null;
4592
4782
  try {
4593
4783
  plugin = parseYaml(readFileSync(fullPath, 'utf8'));
4594
4784
  } catch (e) {
4595
- console.error(` \x1b[31m✗ Failed to parse YAML: ${e.message}\x1b[0m`);
4785
+ console.error(` \x1b[31m✗ [SYNTAX] Failed to parse YAML: ${e.message}\x1b[0m`);
4596
4786
  errors++;
4597
4787
  }
4598
4788
 
@@ -4600,79 +4790,114 @@ function handlePluginValidate(pluginPath, options) {
4600
4790
  const reqKeys = ['name', 'slug', 'version', 'description', 'author'];
4601
4791
  reqKeys.forEach(k => {
4602
4792
  if (plugin[k] === undefined || plugin[k] === null) {
4603
- console.error(` \x1b[31m✗ Missing required key: ${k}\x1b[0m`);
4793
+ console.error(` \x1b[31m✗ [METADATA] Missing required key: ${k}\x1b[0m`);
4604
4794
  errors++;
4605
4795
  } else if (typeof plugin[k] !== 'string') {
4606
- console.error(` \x1b[31m✗ Key '${k}' must be a string (found: ${typeof plugin[k]})\x1b[0m`);
4796
+ console.error(` \x1b[31m✗ [METADATA] Key '${k}' must be a string (found: ${typeof plugin[k]})\x1b[0m`);
4607
4797
  errors++;
4798
+ } else if (k === 'slug') {
4799
+ if (!/^[a-z0-9-_]+$/i.test(plugin[k])) {
4800
+ console.error(` \x1b[31m✗ [METADATA] Key 'slug' must be alphanumeric with dashes or underscores only (found: "${plugin[k]}")\x1b[0m`);
4801
+ errors++;
4802
+ } else {
4803
+ console.log(` \x1b[32m✓ [METADATA] Key: slug ("${plugin[k]}")`);
4804
+ }
4608
4805
  } else {
4609
- console.log(` \x1b[32m✓\x1b[0m Key: ${k} ("${plugin[k]}")`);
4806
+ console.log(` \x1b[32m[METADATA] Key: ${k} ("${plugin[k]}")`);
4610
4807
  }
4611
4808
  });
4612
4809
 
4613
4810
  if (plugin.allowed_file_patterns !== undefined) {
4614
4811
  if (!Array.isArray(plugin.allowed_file_patterns)) {
4615
- console.error(` \x1b[31m✗ allowed_file_patterns must be an array\x1b[0m`);
4812
+ console.error(` \x1b[31m✗ [SAFETY] allowed_file_patterns must be an array\x1b[0m`);
4616
4813
  errors++;
4617
4814
  } else {
4618
4815
  plugin.allowed_file_patterns.forEach(pat => {
4619
4816
  if (typeof pat !== 'string') {
4620
- console.error(` \x1b[31m✗ allowed_file_patterns item must be a string: ${pat}\x1b[0m`);
4817
+ console.error(` \x1b[31m✗ [SAFETY] allowed_file_patterns item must be a string: ${pat}\x1b[0m`);
4818
+ errors++;
4819
+ return;
4820
+ }
4821
+ const normPattern = pat.replace(/\\/g, '/').trim();
4822
+ const isSafeSubdir = [
4823
+ '.ai/plugins/',
4824
+ '.ai/registries/',
4825
+ '.ai/templates/',
4826
+ '.ai/skills/',
4827
+ '.ai/checks/',
4828
+ '.ai/prompts/',
4829
+ '.ai/adapters/'
4830
+ ].some(prefix => normPattern.startsWith(prefix));
4831
+
4832
+ const hasTraversal = normPattern.includes('..') || normPattern.startsWith('/');
4833
+ const isBlacklisted = [
4834
+ '.env',
4835
+ '.npmrc',
4836
+ '.git/',
4837
+ 'node_modules/',
4838
+ 'package.json',
4839
+ 'package-lock.json'
4840
+ ].some(black => normPattern.includes(black));
4841
+
4842
+ if (!isSafeSubdir || hasTraversal || isBlacklisted) {
4843
+ console.error(` \x1b[31m✗ [SAFETY] File pattern '${pat}' violates safety boundaries (must reside under .ai/ or adapters/, contain no '..', and exclude blacklisted files)\x1b[0m`);
4621
4844
  errors++;
4622
4845
  }
4623
4846
  });
4624
- console.log(` \x1b[32m✓\x1b[0m allowed_file_patterns checked: ${plugin.allowed_file_patterns.length} items`);
4847
+ if (errors === 0) {
4848
+ console.log(` \x1b[32m✓ [SAFETY] allowed_file_patterns verified: ${plugin.allowed_file_patterns.length} items`);
4849
+ }
4625
4850
  }
4626
4851
  }
4627
4852
 
4628
4853
  if (plugin.denied_file_patterns !== undefined) {
4629
4854
  if (!Array.isArray(plugin.denied_file_patterns)) {
4630
- console.error(` \x1b[31m✗ denied_file_patterns must be an array\x1b[0m`);
4855
+ console.error(` \x1b[31m✗ [SAFETY] denied_file_patterns must be an array\x1b[0m`);
4631
4856
  errors++;
4632
4857
  } else {
4633
4858
  plugin.denied_file_patterns.forEach(pat => {
4634
4859
  if (typeof pat !== 'string') {
4635
- console.error(` \x1b[31m✗ denied_file_patterns item must be a string: ${pat}\x1b[0m`);
4860
+ console.error(` \x1b[31m✗ [SAFETY] denied_file_patterns item must be a string: ${pat}\x1b[0m`);
4636
4861
  errors++;
4637
4862
  }
4638
4863
  });
4639
- console.log(` \x1b[32m✓\x1b[0m denied_file_patterns checked: ${plugin.denied_file_patterns.length} items`);
4864
+ console.log(` \x1b[32m[SAFETY] denied_file_patterns verified: ${plugin.denied_file_patterns.length} items`);
4640
4865
  }
4641
4866
  }
4642
4867
 
4643
4868
  if (plugin.workflows !== undefined) {
4644
4869
  if (typeof plugin.workflows !== 'object' || Array.isArray(plugin.workflows)) {
4645
- console.error(` \x1b[31m✗ workflows must be an object\x1b[0m`);
4870
+ console.error(` \x1b[31m✗ [CAPABILITIES] workflows must be an object\x1b[0m`);
4646
4871
  errors++;
4647
4872
  } else {
4648
- console.log(` \x1b[32m✓\x1b[0m workflows object checked`);
4873
+ console.log(` \x1b[32m[CAPABILITIES] workflows verified`);
4649
4874
  }
4650
4875
  }
4651
4876
 
4652
4877
  if (plugin.templates !== undefined) {
4653
4878
  if (typeof plugin.templates !== 'object' || Array.isArray(plugin.templates)) {
4654
- console.error(` \x1b[31m✗ templates must be an object\x1b[0m`);
4879
+ console.error(` \x1b[31m✗ [CAPABILITIES] templates must be an object\x1b[0m`);
4655
4880
  errors++;
4656
4881
  } else {
4657
- console.log(` \x1b[32m✓\x1b[0m templates object checked`);
4882
+ console.log(` \x1b[32m[CAPABILITIES] templates verified`);
4658
4883
  }
4659
4884
  }
4660
4885
 
4661
4886
  if (plugin.adapters !== undefined) {
4662
4887
  if (typeof plugin.adapters !== 'object' || Array.isArray(plugin.adapters)) {
4663
- console.error(` \x1b[31m✗ adapters must be an object\x1b[0m`);
4888
+ console.error(` \x1b[31m✗ [CAPABILITIES] adapters must be an object\x1b[0m`);
4664
4889
  errors++;
4665
4890
  } else {
4666
- console.log(` \x1b[32m✓\x1b[0m adapters object checked`);
4891
+ console.log(` \x1b[32m[CAPABILITIES] adapters verified`);
4667
4892
  }
4668
4893
  }
4669
4894
 
4670
4895
  if (plugin.safety_notes !== undefined) {
4671
4896
  if (typeof plugin.safety_notes !== 'string') {
4672
- console.error(` \x1b[31m✗ safety_notes must be a string\x1b[0m`);
4897
+ console.error(` \x1b[31m✗ [SAFETY] safety_notes must be a string\x1b[0m`);
4673
4898
  errors++;
4674
4899
  } else {
4675
- console.log(` \x1b[32m✓\x1b[0m safety_notes checked`);
4900
+ console.log(` \x1b[32m[SAFETY] safety_notes verified`);
4676
4901
  }
4677
4902
  }
4678
4903
  }
@@ -4682,7 +4907,9 @@ function handlePluginValidate(pluginPath, options) {
4682
4907
  if (options && options.noExit) return false;
4683
4908
  process.exit(1);
4684
4909
  } else {
4685
- console.log(`\n\x1b[32m✔ Plugin '${plugin.slug || plugin.name}' is fully valid and compliant!\x1b[0m\n`);
4910
+ console.log(`\n\x1b[32m✔ Plugin '${plugin.slug || plugin.name}' is fully valid and compliant!\x1b[0m`);
4911
+ console.log(`\n\x1b[35mRecommended Next Command:\x1b[0m`);
4912
+ console.log(` npx multimodel-dev-os plugin install ${pluginPath} --approved\n`);
4686
4913
  if (options && options.noExit) return true;
4687
4914
  return true;
4688
4915
  }
@@ -4701,6 +4928,7 @@ function handlePluginInstall(pluginPath, options) {
4701
4928
  process.exit(1);
4702
4929
  }
4703
4930
 
4931
+ const policy = loadRegistryPolicy(options.target || process.cwd());
4704
4932
  const pluginContent = readFileSync(fullPath, 'utf8');
4705
4933
  const plugin = parseYaml(pluginContent);
4706
4934
  const slug = plugin.slug;
@@ -4719,31 +4947,21 @@ function handlePluginInstall(pluginPath, options) {
4719
4947
  plugin.allowed_file_patterns.forEach(pattern => {
4720
4948
  const normPattern = pattern.replace(/\\/g, '/').trim();
4721
4949
 
4722
- const isSafeSubdir = [
4723
- '.ai/plugins/',
4724
- '.ai/registries/',
4725
- '.ai/templates/',
4726
- '.ai/skills/',
4727
- '.ai/checks/',
4728
- '.ai/prompts/',
4729
- '.ai/adapters/'
4730
- ].some(prefix => normPattern.startsWith(prefix));
4731
-
4950
+ const isSafeSubdir = policy.allowed_write_roots.some(prefix => normPattern.startsWith(prefix));
4732
4951
  const hasTraversal = normPattern.includes('..') || normPattern.startsWith('/');
4733
- const isBlacklisted = [
4734
- '.env',
4735
- '.npmrc',
4736
- '.git/',
4737
- 'node_modules/',
4738
- 'package.json',
4739
- 'package-lock.json'
4740
- ].some(black => normPattern.includes(black));
4952
+ const isBlacklisted = policy.blocked_paths.some(black => normPattern.includes(black));
4741
4953
 
4742
4954
  if (!isSafeSubdir || hasTraversal || isBlacklisted) {
4743
4955
  console.error(`\x1b[31mError: Path pattern '${pattern}' violates safety boundaries. Installation aborted.\x1b[0m`);
4744
4956
  process.exit(1);
4745
4957
  }
4746
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
+
4747
4965
  const srcFile = join(sourceDir, normPattern);
4748
4966
  if (existsSync(srcFile) && statSync(srcFile).isFile()) {
4749
4967
  filesToCopy.push({
@@ -4755,6 +4973,22 @@ function handlePluginInstall(pluginPath, options) {
4755
4973
  });
4756
4974
  }
4757
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
+
4758
4992
  let conflicts = false;
4759
4993
  filesToCopy.forEach(item => {
4760
4994
  const destPath = join(options.target, item.dest);
@@ -4772,14 +5006,16 @@ function handlePluginInstall(pluginPath, options) {
4772
5006
  }
4773
5007
 
4774
5008
  if (!options.approved) {
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)`);
4775
5011
  console.log(`\n\x1b[33mPlanned Installation Actions:\x1b[0m`);
4776
5012
  filesToCopy.forEach(item => {
4777
5013
  const exists = existsSync(join(options.target, item.dest));
4778
5014
  const suffix = exists ? ' \x1b[33m(will overwrite)\x1b[0m' : '';
4779
5015
  console.log(` - \x1b[36m[WOULD COPY]\x1b[0m ${item.src} -> ${item.dest}${suffix}`);
4780
5016
  });
4781
- console.log(`\nRun with \x1b[32m--approved\x1b[0m to apply these changes.\n`);
4782
- return;
5017
+ console.error(`\n\x1b[31mError: Installation refused. Run with --approved to apply these changes.\x1b[0m\n`);
5018
+ process.exit(1);
4783
5019
  }
4784
5020
 
4785
5021
  filesToCopy.forEach(item => {
@@ -4799,7 +5035,23 @@ function handlePluginInstall(pluginPath, options) {
4799
5035
  console.log(` \x1b[32mCOPY:\x1b[0m ${item.dest}`);
4800
5036
  });
4801
5037
 
4802
- console.log(`\n\x1b[32m✔ Plugin '${plugin.name}' installed successfully!\x1b[0m\n`);
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)`);
5040
+ console.log(`\nSummary of actions:`);
5041
+ console.log(` - Manifest registered: .ai/plugins/${slug}.yaml`);
5042
+ const assetCount = filesToCopy.length - 1;
5043
+ console.log(` - Synced assets: ${assetCount} file(s)`);
5044
+
5045
+ console.log(`\n\x1b[35mRecommended Next Commands:\x1b[0m`);
5046
+ console.log(` • View plugin details: npx multimodel-dev-os plugin show ${slug}`);
5047
+ console.log(` • Audit plugin health: npx multimodel-dev-os plugin status --target .`);
5048
+ if (plugin.workflows) {
5049
+ const wfKeys = Object.keys(plugin.workflows);
5050
+ if (wfKeys.length > 0) {
5051
+ console.log(` • Run custom workflow: npx multimodel-dev-os workflow run ${wfKeys[0]}`);
5052
+ }
5053
+ }
5054
+ console.log('');
4803
5055
  }
4804
5056
 
4805
5057
  function handlePluginStatus(options) {
@@ -4849,6 +5101,15 @@ function handlePluginStatus(options) {
4849
5101
  console.log(` Status: \x1b[32mHealthy\x1b[0m (All ${presentCount}/${total} assets present)`);
4850
5102
  } else {
4851
5103
  console.log(` Status: \x1b[33mIncomplete\x1b[0m (${presentCount}/${total} assets present, ${missingCount} missing)`);
5104
+ console.log(` Missing Assets:`);
5105
+ p.allowed_file_patterns.forEach(pat => {
5106
+ const destPath = join(options.target, pat);
5107
+ if (!existsSync(destPath) || !statSync(destPath).isFile()) {
5108
+ console.log(` \x1b[31m✗\x1b[0m ${pat}`);
5109
+ }
5110
+ });
5111
+ console.log(` To fix: Reinstall the plugin or validate the configuration:`);
5112
+ console.log(` npx multimodel-dev-os plugin validate <path-to-plugin-source.yaml>`);
4852
5113
  }
4853
5114
  }
4854
5115
  } catch (e) {
@@ -4858,4 +5119,1103 @@ function handlePluginStatus(options) {
4858
5119
  console.log('');
4859
5120
  }
4860
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
+ }
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
+ }
4861
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
+ }