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