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