multimodel-dev-os 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.ai/plugins/README.md +30 -0
  2. package/.ai/plugins/plugin.example.yaml +32 -0
  3. package/.ai/schema/plugin.schema.json +56 -0
  4. package/README.md +76 -219
  5. package/assets/adapter-sync-flow.svg +84 -0
  6. package/assets/architecture-preview.svg +46 -31
  7. package/assets/onboarding-flow.svg +79 -0
  8. package/assets/social-preview.svg +1 -1
  9. package/assets/terminal-demo.svg +22 -23
  10. package/bin/multimodel-dev-os.js +601 -1
  11. package/docs/.vitepress/config.js +25 -8
  12. package/docs/5-day-roadmap.md +9 -9
  13. package/docs/CLI.md +250 -111
  14. package/docs/architecture.md +31 -7
  15. package/docs/comparison.md +72 -25
  16. package/docs/compatibility.md +2 -2
  17. package/docs/dashboard.md +105 -0
  18. package/docs/demo.md +23 -60
  19. package/docs/demos/adapter-sync.md +103 -0
  20. package/docs/demos/existing-repo-onboarding.md +125 -0
  21. package/docs/demos/index.md +91 -0
  22. package/docs/demos/multi-agent-handoff.md +88 -0
  23. package/docs/demos/release-check.md +109 -0
  24. package/docs/demos/safe-improvement-loop.md +119 -0
  25. package/docs/distribution.md +195 -0
  26. package/docs/faq.md +91 -24
  27. package/docs/index.md +192 -81
  28. package/docs/installers.md +18 -4
  29. package/docs/launch-kit.md +97 -49
  30. package/docs/npm-publishing.md +6 -6
  31. package/docs/plugin-authoring.md +99 -0
  32. package/docs/plugin-hooks.md +80 -0
  33. package/docs/public/assets/adapter-sync-flow.svg +84 -0
  34. package/docs/public/assets/onboarding-flow.svg +79 -0
  35. package/docs/public/llms-full.txt +16 -3
  36. package/docs/public/llms.txt +13 -1
  37. package/docs/public/sitemap.xml +55 -0
  38. package/docs/quickstart.md +80 -26
  39. package/docs/repository-command-center.md +16 -0
  40. package/docs/tui-safety.md +59 -0
  41. package/docs/use-cases.md +21 -0
  42. package/docs/v2-roadmap.md +75 -88
  43. package/docs/workflow-orchestration.md +3 -0
  44. package/examples/adapter-sync/README.md +45 -0
  45. package/examples/command-center/README.md +59 -0
  46. package/examples/real-repo-onboarding/README.md +53 -0
  47. package/examples/safe-improvement-loop/README.md +48 -0
  48. package/package.json +1 -1
  49. package/scripts/install.ps1 +1 -1
  50. package/scripts/install.sh +1 -1
@@ -9,6 +9,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSy
9
9
  import { join, dirname, resolve, relative, isAbsolute, basename } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { createHash } from 'crypto';
12
+ import readline from 'readline';
13
+ import { execSync } from 'child_process';
12
14
 
13
15
  const __filename = fileURLToPath(import.meta.url);
14
16
  const __dirname = dirname(__filename);
@@ -409,6 +411,41 @@ if (COMMAND === 'init') {
409
411
  console.log('Example: node bin/multimodel-dev-os.js adapter status');
410
412
  process.exit(1);
411
413
  }
414
+ } else if (COMMAND === 'dashboard' || COMMAND === 'ui') {
415
+ handleDashboard(params);
416
+ } else if (COMMAND === 'plugin') {
417
+ const positional = getPositionalArgs(ARGS);
418
+ const sub = positional[1];
419
+ if (sub === 'list') {
420
+ handlePluginList(params);
421
+ } else if (sub === 'show') {
422
+ const pSlug = positional[2];
423
+ if (!pSlug) {
424
+ console.error('\x1b[31mError: Please specify a plugin name/slug.\x1b[0m');
425
+ process.exit(1);
426
+ }
427
+ handlePluginShow(pSlug, params);
428
+ } else if (sub === 'validate') {
429
+ const pPath = positional[2];
430
+ if (!pPath) {
431
+ console.error('\x1b[31mError: Please specify a plugin configuration file path.\x1b[0m');
432
+ process.exit(1);
433
+ }
434
+ handlePluginValidate(pPath, params);
435
+ } else if (sub === 'install') {
436
+ const pPath = positional[2];
437
+ if (!pPath) {
438
+ console.error('\x1b[31mError: Please specify a plugin configuration file path to install.\x1b[0m');
439
+ process.exit(1);
440
+ }
441
+ handlePluginInstall(pPath, params);
442
+ } else if (sub === 'status') {
443
+ handlePluginStatus(params);
444
+ } else {
445
+ console.error('\x1b[31mError: Please specify a plugin subcommand: list, show, validate, install, or status.\x1b[0m');
446
+ console.log('Example: node bin/multimodel-dev-os.js plugin list');
447
+ process.exit(1);
448
+ }
412
449
  } else {
413
450
  console.error(`\x1b[31mUnknown command: ${COMMAND}\x1b[0m`);
414
451
  showHelp();
@@ -423,6 +460,7 @@ function showHelp() {
423
460
  console.log(' init Initialize a project with configs and adapters');
424
461
  console.log(' scan Scan project structure and framework signals');
425
462
  console.log(' status Show compact dashboard summarizing repository intelligence state');
463
+ console.log(' dashboard Launch the interactive terminal command center (alias: ui)');
426
464
  console.log(' memory <subcmd> Manage hash-compressed codebase memory (subcmd: build, refresh, diff)');
427
465
  console.log(' feedback <subcmd> Manage developer feedback loops (subcmd: add, list, summarize)');
428
466
  console.log(' improve <subcmd> Manage codebase self-improvement proposals (subcmd: propose, review, status, validate, diff, apply, log)');
@@ -430,6 +468,7 @@ function showHelp() {
430
468
  console.log(' handoff <subcmd> Compile or print token-compressed agent session summaries (subcmd: build, show)');
431
469
  console.log(' onboard <subcmd> Safely integrate MultiModel Dev OS into existing repo (subcmd: analyze, recommend, plan, apply, status)');
432
470
  console.log(' adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)');
471
+ console.log(' plugin <subcmd> Manage declarative plugins (subcmd: list, show, validate, install, status)');
433
472
  console.log(' verify Validate structural integrity of an existing project');
434
473
  console.log(' templates List all built-in template profiles with details');
435
474
  console.log(' list-templates Alias for templates command');
@@ -1065,7 +1104,11 @@ function parseYaml(content) {
1065
1104
 
1066
1105
  const colonIdx = trimmed.indexOf(':');
1067
1106
  if (colonIdx === -1) {
1068
- parent.obj.push(trimmed);
1107
+ let val = trimmed;
1108
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
1109
+ val = val.substring(1, val.length - 1);
1110
+ }
1111
+ parent.obj.push(val);
1069
1112
  } else {
1070
1113
  const key = trimmed.substring(0, colonIdx).trim();
1071
1114
  let val = trimmed.substring(colonIdx + 1).trim();
@@ -4258,4 +4301,561 @@ function handleDoctorOnboarding(options) {
4258
4301
  }
4259
4302
  }
4260
4303
 
4304
+ // --- Phase 3 & 4 & 5 & 6: TUI Dashboard & Plugin Hooks System ---
4305
+
4306
+ function selectMenu(title, items, callback) {
4307
+ let cursor = 0;
4308
+
4309
+ const draw = () => {
4310
+ console.clear();
4311
+ console.log(`\n🧠 \x1b[36m${title}\x1b[0m`);
4312
+ console.log('==================================================');
4313
+ items.forEach((item, index) => {
4314
+ if (index === cursor) {
4315
+ console.log(` \x1b[32m❯ ${item.name}\x1b[0m`);
4316
+ } else {
4317
+ console.log(` ${item.name}`);
4318
+ }
4319
+ });
4320
+ console.log('\n\x1b[90m(Use Arrow keys to navigate, Enter to select, Esc/Ctrl+C to exit)\x1b[0m\n');
4321
+ };
4322
+
4323
+ readline.emitKeypressEvents(process.stdin);
4324
+ if (process.stdin.isTTY) {
4325
+ process.stdin.setRawMode(true);
4326
+ }
4327
+ process.stdin.resume();
4328
+
4329
+ const onKeypress = (str, key) => {
4330
+ if (!key) return;
4331
+ if (key.name === 'up') {
4332
+ cursor = (cursor - 1 + items.length) % items.length;
4333
+ draw();
4334
+ } else if (key.name === 'down') {
4335
+ cursor = (cursor + 1) % items.length;
4336
+ draw();
4337
+ } else if (key.name === 'return') {
4338
+ cleanup();
4339
+ callback(items[cursor]);
4340
+ } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
4341
+ cleanup();
4342
+ process.exit(0);
4343
+ }
4344
+ };
4345
+
4346
+ const cleanup = () => {
4347
+ process.stdin.removeListener('keypress', onKeypress);
4348
+ if (process.stdin.isTTY) {
4349
+ process.stdin.setRawMode(false);
4350
+ }
4351
+ process.stdin.pause();
4352
+ };
4353
+
4354
+ process.stdin.on('keypress', onKeypress);
4355
+ draw();
4356
+ }
4357
+
4358
+ function handleDashboard(options) {
4359
+ const mainMenu = [
4360
+ { name: 'Active Workspace Status', action: 'command', command: 'status' },
4361
+ { name: 'Codebase Scan Analysis', action: 'command', command: 'scan' },
4362
+ { name: 'Onboarding Operations...', action: 'submenu', menu: 'onboard' },
4363
+ { name: 'Adapter Synchronization...', action: 'submenu', menu: 'adapter' },
4364
+ { name: 'Memory & Intelligence...', action: 'submenu', menu: 'memory' },
4365
+ { name: 'Developer Feedback Loops...', action: 'submenu', menu: 'feedback' },
4366
+ { name: 'Quality Gates & Diagnostics...', action: 'submenu', menu: 'quality' },
4367
+ { name: 'Plugins Status Overview', action: 'command', command: 'plugin status' },
4368
+ { name: 'Exit Command Center', action: 'exit' }
4369
+ ];
4370
+
4371
+ const submenus = {
4372
+ onboard: [
4373
+ { name: '← Back to Main Menu', action: 'back' },
4374
+ { name: 'Onboard: Analyze Repository', action: 'command', command: 'onboard analyze' },
4375
+ { name: 'Onboard: Recommendation Summary', action: 'command', command: 'onboard recommend' },
4376
+ { name: 'Onboard: Generate Integration Plan', action: 'command', command: 'onboard plan' },
4377
+ { name: 'Onboard: Apply Configs (Dry Run)', action: 'command', command: 'onboard apply --dry-run' },
4378
+ { name: 'Onboard: View Status Heuristics', action: 'command', command: 'onboard status' }
4379
+ ],
4380
+ adapter: [
4381
+ { name: '← Back to Main Menu', action: 'back' },
4382
+ { name: 'Adapters: Check Sync Status', action: 'command', command: 'adapter status' },
4383
+ { name: 'Adapters: Sync All rule files (Dry Run)', action: 'command', command: 'adapter sync all --dry-run' },
4384
+ { name: 'Adapters: Diff Cursor rules', action: 'command', command: 'adapter diff cursor' },
4385
+ { name: 'Adapters: Diff Claude rules', action: 'command', command: 'adapter diff claude' }
4386
+ ],
4387
+ memory: [
4388
+ { name: '← Back to Main Menu', action: 'back' },
4389
+ { name: 'Memory: Build index', action: 'command', command: 'memory build' },
4390
+ { name: 'Memory: Refresh changes', action: 'command', command: 'memory refresh' },
4391
+ { name: 'Memory: Diff index status', action: 'command', command: 'memory diff' },
4392
+ { name: 'Handoff: Build session summary', action: 'command', command: 'handoff build' },
4393
+ { name: 'Handoff: Print summary to terminal', action: 'command', command: 'handoff show' }
4394
+ ],
4395
+ feedback: [
4396
+ { name: '← Back to Main Menu', action: 'back' },
4397
+ { name: 'Feedback: List developer corrections', action: 'command', command: 'feedback list' },
4398
+ { name: 'Feedback: Summarize to learning rules', action: 'command', command: 'feedback summarize' },
4399
+ { name: 'Proposals: Propose improvement proposal', action: 'command', command: 'improve propose' },
4400
+ { name: 'Proposals: Review active proposals list', action: 'command', command: 'improve review' }
4401
+ ],
4402
+ quality: [
4403
+ { name: '← Back to Main Menu', action: 'back' },
4404
+ { name: 'Doctor: Run Advisory Diagnostics', action: 'command', command: 'doctor' },
4405
+ { name: 'Validate: Strict Schema Compliance', action: 'command', command: 'validate' },
4406
+ { name: 'Verify: Run Release verification tests', action: 'command', command: 'verify' }
4407
+ ]
4408
+ };
4409
+
4410
+ if (!process.stdout.isTTY || !process.stdin.isTTY || options.dryRun) {
4411
+ console.log(`\n🧠 \x1b[36mMultiModel Dev OS Command Center (Headless mode)\x1b[0m`);
4412
+ console.log('==================================================');
4413
+ mainMenu.forEach(item => {
4414
+ if (item.action === 'command') {
4415
+ console.log(` - ${item.name}: equivalent command: "npx multimodel-dev-os ${item.command}"`);
4416
+ } else if (item.action === 'submenu') {
4417
+ console.log(` - ${item.name}`);
4418
+ submenus[item.menu].forEach(sub => {
4419
+ if (sub.action === 'command') {
4420
+ console.log(` └─ ${sub.name}: equivalent command: "npx multimodel-dev-os ${sub.command}"`);
4421
+ }
4422
+ });
4423
+ }
4424
+ });
4425
+ console.log('');
4426
+ return;
4427
+ }
4428
+
4429
+ const runCommandWrapper = (cmdStr) => {
4430
+ console.clear();
4431
+ console.log(`\n\x1b[36mRunning Command:\x1b[0m npx multimodel-dev-os ${cmdStr}`);
4432
+ console.log('--------------------------------------------------\n');
4433
+ try {
4434
+ const cliPath = join(sourceRoot, 'bin', 'multimodel-dev-os.js');
4435
+ execSync(`node "${cliPath}" ${cmdStr} --target "${options.target}"`, { stdio: 'inherit' });
4436
+ } catch (e) {
4437
+ console.error(`\n\x1b[31mCommand failed with error: ${e.message}\x1b[0m`);
4438
+ }
4439
+ console.log('\n--------------------------------------------------');
4440
+ console.log('Press any key to return to menu...');
4441
+ if (process.stdin.isTTY) {
4442
+ process.stdin.setRawMode(true);
4443
+ }
4444
+ process.stdin.resume();
4445
+ return new Promise(resolve => {
4446
+ process.stdin.once('keypress', () => {
4447
+ resolve();
4448
+ });
4449
+ });
4450
+ };
4451
+
4452
+ const showMenu = (menuItems, title) => {
4453
+ selectMenu(title, menuItems, async (selected) => {
4454
+ if (selected.action === 'exit') {
4455
+ process.exit(0);
4456
+ } else if (selected.action === 'back') {
4457
+ showMenu(mainMenu, 'MultiModel Dev OS Command Center');
4458
+ } else if (selected.action === 'submenu') {
4459
+ showMenu(submenus[selected.menu], selected.name);
4460
+ } else if (selected.action === 'command') {
4461
+ await runCommandWrapper(selected.command);
4462
+ showMenu(menuItems, title);
4463
+ }
4464
+ });
4465
+ };
4466
+
4467
+ showMenu(mainMenu, 'MultiModel Dev OS Command Center');
4468
+ }
4469
+
4470
+ function getPluginsDir(targetDir) {
4471
+ return join(targetDir, '.ai', 'plugins');
4472
+ }
4473
+
4474
+ function handlePluginList(options) {
4475
+ const pluginsDir = getPluginsDir(options.target);
4476
+ if (!existsSync(pluginsDir)) {
4477
+ if (options.json) {
4478
+ console.log('[]');
4479
+ return;
4480
+ }
4481
+ console.log(`\n🔌 \x1b[36mInstalled Plugins in: ${options.target}\x1b[0m`);
4482
+ console.log('==================================================');
4483
+ console.log(' No plugins installed. Try:');
4484
+ console.log(' npx multimodel-dev-os plugin install .ai/plugins/plugin.example.yaml --approved');
4485
+ console.log('');
4486
+ return;
4487
+ }
4488
+
4489
+ let files = [];
4490
+ try {
4491
+ files = readdirSync(pluginsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
4492
+ } catch (e) {}
4493
+
4494
+ const plugins = [];
4495
+ files.forEach(f => {
4496
+ try {
4497
+ const p = parseYaml(readFileSync(join(pluginsDir, f), 'utf8'));
4498
+ if (p && p.name && p.slug) {
4499
+ plugins.push(p);
4500
+ }
4501
+ } catch (e) {}
4502
+ });
4503
+
4504
+ if (options.json) {
4505
+ console.log(JSON.stringify(plugins, null, 2));
4506
+ return;
4507
+ }
4508
+
4509
+ console.log(`\n🔌 \x1b[36mInstalled Plugins in: ${options.target} (${plugins.length})\x1b[0m`);
4510
+ console.log('==================================================');
4511
+ if (plugins.length === 0) {
4512
+ console.log(' No plugins installed.');
4513
+ } else {
4514
+ plugins.forEach(p => {
4515
+ console.log(`\n\x1b[32m* ${p.name} (v${p.version || '1.0.0'})\x1b[0m [slug: \x1b[33m${p.slug}\x1b[0m]`);
4516
+ console.log(` Description: ${p.description || 'No description'}`);
4517
+ console.log(` Author: ${p.author || 'Unknown'}`);
4518
+ });
4519
+ }
4520
+ console.log('\nUse \x1b[36mplugin show <slug>\x1b[0m to view detailed plugin capabilities.\n');
4521
+ }
4522
+
4523
+ function handlePluginShow(slug, options) {
4524
+ const pluginsDir = getPluginsDir(options.target);
4525
+ let p = null;
4526
+ if (existsSync(pluginsDir)) {
4527
+ const files = readdirSync(pluginsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
4528
+ for (const f of files) {
4529
+ try {
4530
+ const parsed = parseYaml(readFileSync(join(pluginsDir, f), 'utf8'));
4531
+ if (parsed && parsed.slug === slug) {
4532
+ p = parsed;
4533
+ break;
4534
+ }
4535
+ } catch (e) {}
4536
+ }
4537
+ }
4538
+
4539
+ if (!p) {
4540
+ console.error(`\x1b[31mError: Plugin with slug '${slug}' is not installed.\x1b[0m`);
4541
+ process.exit(1);
4542
+ }
4543
+
4544
+ console.log(`\n🔍 \x1b[36mPlugin Specifications: ${p.name} (v${p.version})\x1b[0m`);
4545
+ console.log('==================================================');
4546
+ console.log(`\x1b[33mSlug:\x1b[0m ${p.slug}`);
4547
+ console.log(`\x1b[33mAuthor:\x1b[0m ${p.author}`);
4548
+ console.log(`\x1b[33mDescription:\x1b[0m ${p.description}`);
4549
+ if (p.safety_notes) {
4550
+ console.log(`\x1b[33mSafety Notes:\x1b[0m ${p.safety_notes}`);
4551
+ }
4552
+
4553
+ if (p.allowed_file_patterns) {
4554
+ console.log('\n\x1b[33mAllowed Write Subdirectories:\x1b[0m');
4555
+ p.allowed_file_patterns.forEach(pat => console.log(` - ${pat}`));
4556
+ }
4557
+
4558
+ if (p.templates) {
4559
+ console.log('\n\x1b[33mCustom Templates:\x1b[0m');
4560
+ Object.keys(p.templates).forEach(k => {
4561
+ console.log(` - \x1b[32m${k}\x1b[0m: ${p.templates[k].description || p.templates[k].name}`);
4562
+ });
4563
+ }
4564
+
4565
+ if (p.workflows) {
4566
+ console.log('\n\x1b[33mCustom Workflows:\x1b[0m');
4567
+ Object.keys(p.workflows).forEach(k => {
4568
+ console.log(` - \x1b[32m${k}\x1b[0m: ${p.workflows[k].description || p.workflows[k].name}`);
4569
+ });
4570
+ }
4571
+
4572
+ if (p.adapters) {
4573
+ console.log('\n\x1b[33mCustom Adapters:\x1b[0m');
4574
+ Object.keys(p.adapters).forEach(k => {
4575
+ console.log(` - \x1b[32m${k}\x1b[0m: ${p.adapters[k].targetFile}`);
4576
+ });
4577
+ }
4578
+ console.log('');
4579
+ }
4580
+
4581
+ function handlePluginValidate(pluginPath, options) {
4582
+ const fullPath = resolve(process.cwd(), pluginPath);
4583
+ if (!existsSync(fullPath)) {
4584
+ console.error(`\x1b[31mError: Plugin file not found at: ${pluginPath}\x1b[0m`);
4585
+ process.exit(1);
4586
+ }
4587
+
4588
+ console.log(`\n📋 \x1b[34mValidating Plugin: ${pluginPath}\x1b[0m`);
4589
+
4590
+ let errors = 0;
4591
+ let plugin = null;
4592
+ try {
4593
+ plugin = parseYaml(readFileSync(fullPath, 'utf8'));
4594
+ } catch (e) {
4595
+ console.error(` \x1b[31m✗ Failed to parse YAML: ${e.message}\x1b[0m`);
4596
+ errors++;
4597
+ }
4598
+
4599
+ if (plugin) {
4600
+ const reqKeys = ['name', 'slug', 'version', 'description', 'author'];
4601
+ reqKeys.forEach(k => {
4602
+ if (plugin[k] === undefined || plugin[k] === null) {
4603
+ console.error(` \x1b[31m✗ Missing required key: ${k}\x1b[0m`);
4604
+ errors++;
4605
+ } else if (typeof plugin[k] !== 'string') {
4606
+ console.error(` \x1b[31m✗ Key '${k}' must be a string (found: ${typeof plugin[k]})\x1b[0m`);
4607
+ errors++;
4608
+ } else {
4609
+ console.log(` \x1b[32m✓\x1b[0m Key: ${k} ("${plugin[k]}")`);
4610
+ }
4611
+ });
4612
+
4613
+ if (plugin.allowed_file_patterns !== undefined) {
4614
+ if (!Array.isArray(plugin.allowed_file_patterns)) {
4615
+ console.error(` \x1b[31m✗ allowed_file_patterns must be an array\x1b[0m`);
4616
+ errors++;
4617
+ } else {
4618
+ plugin.allowed_file_patterns.forEach(pat => {
4619
+ if (typeof pat !== 'string') {
4620
+ console.error(` \x1b[31m✗ allowed_file_patterns item must be a string: ${pat}\x1b[0m`);
4621
+ errors++;
4622
+ }
4623
+ });
4624
+ console.log(` \x1b[32m✓\x1b[0m allowed_file_patterns checked: ${plugin.allowed_file_patterns.length} items`);
4625
+ }
4626
+ }
4627
+
4628
+ if (plugin.denied_file_patterns !== undefined) {
4629
+ if (!Array.isArray(plugin.denied_file_patterns)) {
4630
+ console.error(` \x1b[31m✗ denied_file_patterns must be an array\x1b[0m`);
4631
+ errors++;
4632
+ } else {
4633
+ plugin.denied_file_patterns.forEach(pat => {
4634
+ if (typeof pat !== 'string') {
4635
+ console.error(` \x1b[31m✗ denied_file_patterns item must be a string: ${pat}\x1b[0m`);
4636
+ errors++;
4637
+ }
4638
+ });
4639
+ console.log(` \x1b[32m✓\x1b[0m denied_file_patterns checked: ${plugin.denied_file_patterns.length} items`);
4640
+ }
4641
+ }
4642
+
4643
+ if (plugin.workflows !== undefined) {
4644
+ if (typeof plugin.workflows !== 'object' || Array.isArray(plugin.workflows)) {
4645
+ console.error(` \x1b[31m✗ workflows must be an object\x1b[0m`);
4646
+ errors++;
4647
+ } else {
4648
+ console.log(` \x1b[32m✓\x1b[0m workflows object checked`);
4649
+ }
4650
+ }
4651
+
4652
+ if (plugin.templates !== undefined) {
4653
+ if (typeof plugin.templates !== 'object' || Array.isArray(plugin.templates)) {
4654
+ console.error(` \x1b[31m✗ templates must be an object\x1b[0m`);
4655
+ errors++;
4656
+ } else {
4657
+ console.log(` \x1b[32m✓\x1b[0m templates object checked`);
4658
+ }
4659
+ }
4660
+
4661
+ if (plugin.adapters !== undefined) {
4662
+ if (typeof plugin.adapters !== 'object' || Array.isArray(plugin.adapters)) {
4663
+ console.error(` \x1b[31m✗ adapters must be an object\x1b[0m`);
4664
+ errors++;
4665
+ } else {
4666
+ console.log(` \x1b[32m✓\x1b[0m adapters object checked`);
4667
+ }
4668
+ }
4669
+
4670
+ if (plugin.safety_notes !== undefined) {
4671
+ if (typeof plugin.safety_notes !== 'string') {
4672
+ console.error(` \x1b[31m✗ safety_notes must be a string\x1b[0m`);
4673
+ errors++;
4674
+ } else {
4675
+ console.log(` \x1b[32m✓\x1b[0m safety_notes checked`);
4676
+ }
4677
+ }
4678
+ }
4679
+
4680
+ if (errors > 0) {
4681
+ console.error(`\n\x1b[31mPlugin validation FAILED with ${errors} errors.\x1b[0m\n`);
4682
+ if (options && options.noExit) return false;
4683
+ process.exit(1);
4684
+ } else {
4685
+ console.log(`\n\x1b[32m✔ Plugin '${plugin.slug || plugin.name}' is fully valid and compliant!\x1b[0m\n`);
4686
+ if (options && options.noExit) return true;
4687
+ return true;
4688
+ }
4689
+ }
4690
+
4691
+ function handlePluginInstall(pluginPath, options) {
4692
+ const fullPath = resolve(process.cwd(), pluginPath);
4693
+ if (!existsSync(fullPath)) {
4694
+ console.error(`\x1b[31mError: Plugin file not found at: ${pluginPath}\x1b[0m`);
4695
+ process.exit(1);
4696
+ }
4697
+
4698
+ const isValid = handlePluginValidate(pluginPath, { noExit: true });
4699
+ if (!isValid) {
4700
+ console.error(`\x1b[31mError: Plugin validation failed. Installation aborted.\x1b[0m`);
4701
+ process.exit(1);
4702
+ }
4703
+
4704
+ const pluginContent = readFileSync(fullPath, 'utf8');
4705
+ const plugin = parseYaml(pluginContent);
4706
+ const slug = plugin.slug;
4707
+ const sourceDir = dirname(fullPath);
4708
+
4709
+ console.log(`\n📥 \x1b[34mInstalling Plugin: ${plugin.name} [slug: ${slug}]\x1b[0m`);
4710
+
4711
+ const filesToCopy = [];
4712
+ filesToCopy.push({
4713
+ src: fullPath,
4714
+ dest: join('.ai', 'plugins', `${slug}.yaml`),
4715
+ description: 'Plugin Manifest'
4716
+ });
4717
+
4718
+ if (Array.isArray(plugin.allowed_file_patterns)) {
4719
+ plugin.allowed_file_patterns.forEach(pattern => {
4720
+ const normPattern = pattern.replace(/\\/g, '/').trim();
4721
+
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
+
4732
+ 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));
4741
+
4742
+ if (!isSafeSubdir || hasTraversal || isBlacklisted) {
4743
+ console.error(`\x1b[31mError: Path pattern '${pattern}' violates safety boundaries. Installation aborted.\x1b[0m`);
4744
+ process.exit(1);
4745
+ }
4746
+
4747
+ const srcFile = join(sourceDir, normPattern);
4748
+ if (existsSync(srcFile) && statSync(srcFile).isFile()) {
4749
+ filesToCopy.push({
4750
+ src: srcFile,
4751
+ dest: normPattern,
4752
+ description: `Plugin asset: ${normPattern}`
4753
+ });
4754
+ }
4755
+ });
4756
+ }
4757
+
4758
+ let conflicts = false;
4759
+ filesToCopy.forEach(item => {
4760
+ const destPath = join(options.target, item.dest);
4761
+ if (existsSync(destPath)) {
4762
+ if (!options.force) {
4763
+ console.error(` \x1b[31mConflict:\x1b[0m File already exists at destination: ${item.dest}`);
4764
+ conflicts = true;
4765
+ }
4766
+ }
4767
+ });
4768
+
4769
+ if (conflicts) {
4770
+ console.error(`\n\x1b[31mInstallation aborted due to overwrite conflicts. Run with --force to overwrite (creates .bak backups).\x1b[0m\n`);
4771
+ process.exit(1);
4772
+ }
4773
+
4774
+ if (!options.approved) {
4775
+ console.log(`\n\x1b[33mPlanned Installation Actions:\x1b[0m`);
4776
+ filesToCopy.forEach(item => {
4777
+ const exists = existsSync(join(options.target, item.dest));
4778
+ const suffix = exists ? ' \x1b[33m(will overwrite)\x1b[0m' : '';
4779
+ console.log(` - \x1b[36m[WOULD COPY]\x1b[0m ${item.src} -> ${item.dest}${suffix}`);
4780
+ });
4781
+ console.log(`\nRun with \x1b[32m--approved\x1b[0m to apply these changes.\n`);
4782
+ return;
4783
+ }
4784
+
4785
+ filesToCopy.forEach(item => {
4786
+ const destPath = join(options.target, item.dest);
4787
+ const destDir = dirname(destPath);
4788
+ if (!existsSync(destDir)) {
4789
+ mkdirSync(destDir, { recursive: true });
4790
+ }
4791
+
4792
+ if (existsSync(destPath)) {
4793
+ const bakPath = `${destPath}.bak`;
4794
+ writeFileSync(bakPath, readFileSync(destPath));
4795
+ console.log(` \x1b[33mBACKUP:\x1b[0m Created backup: ${item.dest}.bak`);
4796
+ }
4797
+
4798
+ writeFileSync(destPath, readFileSync(item.src));
4799
+ console.log(` \x1b[32mCOPY:\x1b[0m ${item.dest}`);
4800
+ });
4801
+
4802
+ console.log(`\n\x1b[32m✔ Plugin '${plugin.name}' installed successfully!\x1b[0m\n`);
4803
+ }
4804
+
4805
+ function handlePluginStatus(options) {
4806
+ const pluginsDir = getPluginsDir(options.target);
4807
+ console.log(`\n🔌 \x1b[36mAuditing Plugins Status in: ${options.target}\x1b[0m`);
4808
+ console.log('==================================================');
4809
+
4810
+ if (!existsSync(pluginsDir)) {
4811
+ console.log(' No plugins directory found. 0 plugins installed.\n');
4812
+ return;
4813
+ }
4814
+
4815
+ let files = [];
4816
+ try {
4817
+ files = readdirSync(pluginsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
4818
+ } catch (e) {}
4819
+
4820
+ if (files.length === 0) {
4821
+ console.log(' No plugins installed.\n');
4822
+ return;
4823
+ }
4824
+
4825
+ files.forEach(f => {
4826
+ try {
4827
+ const pPath = join(pluginsDir, f);
4828
+ const p = parseYaml(readFileSync(pPath, 'utf8'));
4829
+ if (p && p.name) {
4830
+ console.log(`\n* \x1b[32m${p.name}\x1b[0m (v${p.version || '1.0.0'})`);
4831
+ let missingCount = 0;
4832
+ let presentCount = 0;
4833
+
4834
+ if (Array.isArray(p.allowed_file_patterns)) {
4835
+ p.allowed_file_patterns.forEach(pat => {
4836
+ const destPath = join(options.target, pat);
4837
+ if (existsSync(destPath) && statSync(destPath).isFile()) {
4838
+ presentCount++;
4839
+ } else {
4840
+ missingCount++;
4841
+ }
4842
+ });
4843
+ }
4844
+
4845
+ const total = presentCount + missingCount;
4846
+ if (total === 0) {
4847
+ console.log(` Status: \x1b[32mHealthy\x1b[0m (Declarative only)`);
4848
+ } else if (missingCount === 0) {
4849
+ console.log(` Status: \x1b[32mHealthy\x1b[0m (All ${presentCount}/${total} assets present)`);
4850
+ } else {
4851
+ console.log(` Status: \x1b[33mIncomplete\x1b[0m (${presentCount}/${total} assets present, ${missingCount} missing)`);
4852
+ }
4853
+ }
4854
+ } catch (e) {
4855
+ console.log(` - \x1b[31mError reading: ${f}\x1b[0m (${e.message})`);
4856
+ }
4857
+ });
4858
+ console.log('');
4859
+ }
4860
+
4261
4861