multimodel-dev-os 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.ai/adapters/custom-adapter.example.yaml +9 -0
  2. package/.ai/adapters/registry.yaml +56 -0
  3. package/.ai/models/README.md +14 -0
  4. package/.ai/models/local-models.yaml +20 -0
  5. package/.ai/models/providers.yaml +29 -0
  6. package/.ai/models/registry.yaml +73 -0
  7. package/.ai/models/routing-presets.yaml +23 -0
  8. package/.ai/skills/custom-skill.example.md +15 -0
  9. package/.ai/templates/custom-template.example.yaml +19 -0
  10. package/.ai/templates/registry.yaml +522 -0
  11. package/README.md +30 -18
  12. package/bin/multimodel-dev-os.js +810 -91
  13. package/docs/.vitepress/config.js +36 -1
  14. package/docs/adapter-authoring.md +46 -0
  15. package/docs/agent-compatibility.md +51 -0
  16. package/docs/cli-roadmap.md +15 -18
  17. package/docs/final-launch.md +5 -4
  18. package/docs/local-models.md +48 -0
  19. package/docs/mobile-android.md +75 -0
  20. package/docs/model-compatibility.md +65 -0
  21. package/docs/model-routing.md +45 -0
  22. package/docs/npm-publishing.md +27 -0
  23. package/docs/package-safety.md +29 -0
  24. package/docs/provider-strategy.md +44 -0
  25. package/docs/public/llms-full.txt +82 -73
  26. package/docs/public/llms.txt +36 -34
  27. package/docs/quickstart.md +7 -6
  28. package/docs/registry-contribution.md +20 -0
  29. package/docs/release-policy.md +26 -0
  30. package/docs/skill-authoring.md +56 -0
  31. package/docs/template-authoring.md +65 -0
  32. package/docs/token-optimization.md +27 -0
  33. package/docs/v2-migration.md +31 -0
  34. package/docs/v2-release-checklist.md +30 -0
  35. package/docs/v2-roadmap.md +95 -0
  36. package/examples/expo-react-native-android/.ai/config.yaml +22 -0
  37. package/examples/expo-react-native-android/.ai/context/architecture.md +18 -0
  38. package/examples/expo-react-native-android/.ai/context/context-budget.md +4 -0
  39. package/examples/expo-react-native-android/.ai/context/model-map.md +6 -0
  40. package/examples/expo-react-native-android/.ai/context/project-brief.md +9 -0
  41. package/examples/expo-react-native-android/.ai/session-logs/.gitkeep +1 -0
  42. package/examples/expo-react-native-android/.ai/skills/expo-android-build.md +11 -0
  43. package/examples/expo-react-native-android/AGENTS.md +20 -0
  44. package/examples/expo-react-native-android/MEMORY.md +13 -0
  45. package/examples/expo-react-native-android/README.md +101 -0
  46. package/examples/expo-react-native-android/RUNBOOK.md +36 -0
  47. package/examples/expo-react-native-android/TASKS.md +14 -0
  48. package/examples/expo-react-native-android/app.config.ts +40 -0
  49. package/examples/expo-react-native-android/app.json +34 -0
  50. package/examples/expo-react-native-android/eas.json +26 -0
  51. package/examples/expo-react-native-android/jest.config.js +11 -0
  52. package/examples/expo-react-native-android/src/app/_layout.tsx +89 -0
  53. package/examples/expo-react-native-android/src/lib/secure-storage.ts +63 -0
  54. package/examples/expo-react-native-android/src/services/api-client.ts +106 -0
  55. package/package.json +3 -2
  56. package/scripts/install.ps1 +230 -230
  57. package/scripts/install.sh +1 -1
  58. package/scripts/prepublish-guard.js +43 -0
  59. package/scripts/verify.js +178 -1
@@ -13,7 +13,7 @@ const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
14
14
  const sourceRoot = resolve(__dirname, '..');
15
15
 
16
- let version = '0.5.1';
16
+ let version = '2.0.0';
17
17
  try {
18
18
  const pkgData = JSON.parse(readFileSync(resolve(sourceRoot, 'package.json'), 'utf8'));
19
19
  version = pkgData.version;
@@ -31,7 +31,18 @@ function parseArgs(args) {
31
31
  caveman: false,
32
32
  dryRun: false,
33
33
  force: false,
34
- help: false
34
+ help: false,
35
+ tokens: false,
36
+ modelPreset: null,
37
+ agent: null,
38
+ stack: null,
39
+ mobile: null,
40
+ aiApp: null,
41
+ json: false,
42
+ threshold: null,
43
+ registry: null,
44
+ allRegistries: false,
45
+ release: false
35
46
  };
36
47
 
37
48
  for (let i = 0; i < args.length; i++) {
@@ -50,6 +61,28 @@ function parseArgs(args) {
50
61
  params.force = true;
51
62
  } else if (arg === '--help' || arg === '-h') {
52
63
  params.help = true;
64
+ } else if (arg === '--tokens') {
65
+ params.tokens = true;
66
+ } else if (arg === '--all-registries') {
67
+ params.allRegistries = true;
68
+ } else if (arg === '--release') {
69
+ params.release = true;
70
+ } else if (arg === '--json') {
71
+ params.json = true;
72
+ } else if (arg === '--threshold') {
73
+ params.threshold = args[++i];
74
+ } else if (arg === '--registry') {
75
+ params.registry = args[++i];
76
+ } else if (arg === '--model-preset') {
77
+ params.modelPreset = args[++i];
78
+ } else if (arg === '--agent') {
79
+ params.agent = args[++i];
80
+ } else if (arg === '--stack') {
81
+ params.stack = args[++i];
82
+ } else if (arg === '--mobile') {
83
+ params.mobile = args[++i];
84
+ } else if (arg === '--ai-app') {
85
+ params.aiApp = args[++i];
53
86
  } else if (!params.command && !arg.startsWith('-')) {
54
87
  params.command = arg;
55
88
  }
@@ -60,43 +93,41 @@ function parseArgs(args) {
60
93
  const params = parseArgs(ARGS);
61
94
  const COMMAND = params.command;
62
95
 
63
- const TEMPLATES = {
64
- 'nextjs-saas': {
65
- name: 'nextjs-saas',
66
- description: 'Next.js App Router starter with TypeScript, Prisma database, Tailwind CSS, and Stripe subscription setup.',
67
- stack: 'Next.js 14, React 18, TypeScript, Tailwind CSS, Prisma ORM, Stripe payments',
68
- skill: 'nextjs-action-build.md',
69
- skillDesc: 'React Server Actions secure implementation conventions.'
70
- },
71
- 'wordpress-site': {
72
- name: 'wordpress-site',
73
- description: 'WordPress custom block theme and plugin development profile with secure PHP database query rules.',
74
- stack: 'WordPress Core, PHP, Gutenberg Block APIs, theme customization hooks',
75
- skill: 'plugin-boilerplate.md',
76
- skillDesc: 'PHP hook registrations and sanitization gates boilerplate.'
77
- },
78
- 'ecommerce-store': {
79
- name: 'ecommerce-store',
80
- description: 'PCI-compliant headless e-commerce store with secure checkout loops, card state validations, and Stripe webhooks.',
81
- stack: 'Headless Store API, cart states, secure payment webhooks, order database triggers',
82
- skill: 'webhook-handler.md',
83
- skillDesc: 'Stripe order checkout webhook secure listener verification rules.'
84
- },
85
- 'seo-landing-page': {
86
- name: 'seo-landing-page',
87
- description: 'Ultra-fast static landing page layout optimized for Astro, high Core Web Vitals scores, and JSON-LD schema markup.',
88
- stack: 'Astro, HTML5, structured JSON-LD SEO markup, asset minification frameworks',
89
- skill: 'seo-audit.md',
90
- skillDesc: 'Lighthouse audits optimization guidelines and Core Web Vitals targets.'
91
- },
92
- 'general-app': {
93
- name: 'general-app',
94
- description: 'Baseline generic fallback profile for standard backend systems (Python, Go, Node, Rust) and universal git workflows.',
95
- stack: 'Universal backends baseline structure, default git flow parameters',
96
- skill: 'example-skill.md',
97
- skillDesc: 'Generic baseline instructions and coding standards.'
98
- }
99
- };
96
+ function loadTemplates(customPath) {
97
+ let path = customPath || join(sourceRoot, '.ai', 'templates', 'registry.yaml');
98
+ try {
99
+ if (existsSync(path)) {
100
+ const templatesRegistry = parseYaml(readFileSync(path, 'utf8'));
101
+ return templatesRegistry.templates || {};
102
+ }
103
+ } catch (e) {}
104
+ return {
105
+ 'general-app': {
106
+ name: 'general-app',
107
+ description: 'Baseline generic fallback profile for standard backend systems.',
108
+ stack: 'Universal backends baseline structure',
109
+ skill: 'example-skill.md',
110
+ skillDesc: 'Generic baseline instructions and coding standards.',
111
+ status: 'stable',
112
+ maturity: 'production-ready',
113
+ required_files: ['AGENTS.md', 'MEMORY.md', 'TASKS.md', 'RUNBOOK.md', '.ai/config.yaml']
114
+ }
115
+ };
116
+ }
117
+
118
+ function loadAdapters(customPath) {
119
+ let path = customPath || join(sourceRoot, '.ai', 'adapters', 'registry.yaml');
120
+ try {
121
+ if (existsSync(path)) {
122
+ const adaptersRegistry = parseYaml(readFileSync(path, 'utf8'));
123
+ return adaptersRegistry.adapters || {};
124
+ }
125
+ } catch (e) {}
126
+ return {};
127
+ }
128
+
129
+ const TEMPLATES = loadTemplates(params.registry);
130
+ const ADAPTERS = loadAdapters(params.registry);
100
131
 
101
132
  if (params.help || !COMMAND) {
102
133
  showHelp();
@@ -104,11 +135,16 @@ if (params.help || !COMMAND) {
104
135
  }
105
136
 
106
137
  if (COMMAND === 'init') {
138
+ if (params.mobile === 'android') {
139
+ params.template = 'expo-react-native-android';
140
+ } else if (params.aiApp === 'rag') {
141
+ params.template = 'rag-knowledge-base';
142
+ }
107
143
  handleInit(params);
108
144
  } else if (COMMAND === 'verify') {
109
145
  handleVerify(params);
110
146
  } else if (COMMAND === 'templates' || COMMAND === 'list-templates') {
111
- handleListTemplates();
147
+ handleListTemplates(params);
112
148
  } else if (COMMAND === 'show-template') {
113
149
  const tName = ARGS[1];
114
150
  if (!tName || tName.startsWith('-')) {
@@ -120,6 +156,63 @@ if (COMMAND === 'init') {
120
156
  handleDoctor(params);
121
157
  } else if (COMMAND === 'validate') {
122
158
  handleValidate(params);
159
+ } else if (COMMAND === 'validate-template') {
160
+ const tName = ARGS[1];
161
+ if (!tName || tName.startsWith('-')) {
162
+ console.error('\x1b[31mError: Please specify a template name. Example: node bin/multimodel-dev-os.js validate-template nextjs-saas\x1b[0m');
163
+ process.exit(1);
164
+ }
165
+ handleValidateTemplate(tName);
166
+ } else if (COMMAND === 'validate-adapter') {
167
+ const aName = ARGS[1];
168
+ if (!aName || aName.startsWith('-')) {
169
+ console.error('\x1b[31mError: Please specify an adapter name. Example: node bin/multimodel-dev-os.js validate-adapter cursor\x1b[0m');
170
+ process.exit(1);
171
+ }
172
+ handleValidateAdapter(aName);
173
+ } else if (COMMAND === 'validate-skill') {
174
+ const sName = ARGS[1];
175
+ if (!sName || sName.startsWith('-')) {
176
+ console.error('\x1b[31mError: Please specify a skill name. Example: node bin/multimodel-dev-os.js validate-skill custom-skill.example\x1b[0m');
177
+ process.exit(1);
178
+ }
179
+ handleValidateSkill(sName, params);
180
+ } else if (COMMAND === 'models') {
181
+ handleListModels(params);
182
+ } else if (COMMAND === 'show-model') {
183
+ const mName = ARGS[1];
184
+ if (!mName || mName.startsWith('-')) {
185
+ console.error('\x1b[31mError: Please specify a model name. Example: node bin/multimodel-dev-os.js show-model claude-sonnet-latest\x1b[0m');
186
+ process.exit(1);
187
+ }
188
+ handleShowModel(mName);
189
+ } else if (COMMAND === 'providers') {
190
+ handleListProviders();
191
+ } else if (COMMAND === 'route-model') {
192
+ const taskName = ARGS[1];
193
+ if (!taskName || taskName.startsWith('-')) {
194
+ console.error('\x1b[31mError: Please specify a task. Example: node bin/multimodel-dev-os.js route-model planning\x1b[0m');
195
+ process.exit(1);
196
+ }
197
+ handleRouteModel(taskName);
198
+ } else if (COMMAND === 'adapters') {
199
+ handleListAdapters(params);
200
+ } else if (COMMAND === 'show-adapter') {
201
+ const aName = ARGS[1];
202
+ if (!aName || aName.startsWith('-')) {
203
+ console.error('\x1b[31mError: Please specify an adapter name. Example: node bin/multimodel-dev-os.js show-adapter cursor\x1b[0m');
204
+ process.exit(1);
205
+ }
206
+ handleShowAdapter(aName);
207
+ } else if (COMMAND === 'skills') {
208
+ handleListSkills(params);
209
+ } else if (COMMAND === 'show-skill') {
210
+ const sName = ARGS[1];
211
+ if (!sName || sName.startsWith('-')) {
212
+ console.error('\x1b[31mError: Please specify a skill name. Example: node bin/multimodel-dev-os.js show-skill bug-fix\x1b[0m');
213
+ process.exit(1);
214
+ }
215
+ handleShowSkill(sName, params);
123
216
  } else {
124
217
  console.error(`\x1b[31mUnknown command: ${COMMAND}\x1b[0m`);
125
218
  showHelp();
@@ -137,23 +230,42 @@ function showHelp() {
137
230
  console.log(' list-templates Alias for templates command');
138
231
  console.log(' show-template <t> Inspect detailed stack specifications of template <t>');
139
232
  console.log(' doctor Advisory checkup of project compatibility loops and ignored folders');
140
- console.log(' validate Strict validation checks to verify directory schema compliance\n');
233
+ console.log(' validate Strict validation checks to verify directory schema compliance');
234
+ console.log(' validate-template Validate registry keys and source folder files for template');
235
+ console.log(' validate-adapter Validate registry keys and source assets for IDE adapter');
236
+ console.log(' validate-skill Verify custom skill conforms to core prompt structure');
237
+ console.log(' models List registered model aliases in the capabilities registry');
238
+ console.log(' show-model <m> View specifications of model <m> in registry');
239
+ console.log(' providers List configured AI provider API endpoints');
240
+ console.log(' route-model <tsk> Suggest optimal model mapping for task <tsk>');
241
+ console.log(' adapters List IDE and terminal tool adapters');
242
+ console.log(' show-adapter <a> Inspect config specifications of adapter <a>');
243
+ console.log(' skills List active skills custom prompts in target workspace');
244
+ console.log(' show-skill <s> View prompt contents of target workspace skill <s>\n');
141
245
  console.log('Options:');
142
246
  console.log(' -t, --target <path> Target folder destination (default: current working directory)');
143
- console.log(' --template <name> Template profile: nextjs-saas, wordpress-site, ecommerce-store,');
144
- console.log(' seo-landing-page, general-app (default: general-app)');
145
- console.log(' -a, --adapter <name> Inject specific adapter: codex, antigravity, cursor, claude, gemini, vscode');
247
+ console.log(' --template <name> Template profile: nextjs-saas, expo-react-native-android, etc.');
248
+ console.log(' -a, --adapter <name> Inject specific adapter: cursor, claude, vscode, gemini, etc.');
146
249
  console.log(' --caveman Use minimal-token templates (~79% fewer tokens)');
250
+ console.log(' --tokens Run a deeper token-sink size analysis during doctor checkup');
251
+ console.log(' --json Output raw JSON data for listing commands (models, adapters, templates)');
252
+ console.log(' --threshold <val> Set custom size threshold for doctor tokens checks (e.g. 50KB)');
253
+ console.log(' --registry <path> Override default registry (for templates/adapters list or check)');
147
254
  console.log(' -d, --dry-run Preview planned file actions without modifying the filesystem');
148
255
  console.log(' -f, --force Overwrite existing files without prompting\n');
149
256
  }
150
257
 
151
- function handleListTemplates() {
258
+ function handleListTemplates(options) {
259
+ if (options && options.json) {
260
+ console.log(JSON.stringify(TEMPLATES, null, 2));
261
+ return;
262
+ }
152
263
  console.log(`\n🧠 \x1b[36mBuilt-in Template Profiles [v${version}]\x1b[0m`);
153
264
  console.log('==================================================');
154
265
  Object.keys(TEMPLATES).forEach(key => {
155
266
  const t = TEMPLATES[key];
156
- console.log(`\n\x1b[32m* ${t.name}\x1b[0m`);
267
+ const statusStr = t.status === 'planned' ? ' (Planned)' : t.status === 'experimental' ? ' (Experimental)' : '';
268
+ console.log(`\n\x1b[32m* ${t.name}${statusStr}\x1b[0m`);
157
269
  console.log(` \x1b[33mStack:\x1b[0m ${t.stack}`);
158
270
  console.log(` \x1b[37mDescription:\x1b[0m ${t.description}`);
159
271
  });
@@ -163,16 +275,20 @@ function handleListTemplates() {
163
275
  function handleShowTemplate(name) {
164
276
  const t = TEMPLATES[name];
165
277
  if (!t) {
166
- console.error(`\n\x1b[31mError: Template '${name}' does not exist. Available: nextjs-saas, wordpress-site, ecommerce-store, seo-landing-page, general-app\x1b[0m\n`);
278
+ const available = Object.keys(TEMPLATES).join(', ');
279
+ console.error(`\n\x1b[31mError: Template '${name}' does not exist. Available: ${available}\x1b[0m\n`);
167
280
  process.exit(1);
168
281
  }
169
282
 
170
- console.log(`\n🔍 \x1b[36mTemplate Profile: ${t.name}\x1b[0m`);
283
+ const statusStr = t.status === 'planned' ? ' (Planned)' : t.status === 'experimental' ? ' (Experimental)' : ' (Stable)';
284
+ console.log(`\n🔍 \x1b[36mTemplate Profile: ${t.name}${statusStr}\x1b[0m`);
171
285
  console.log('==================================================');
172
286
  console.log(`\x1b[33mStack Blueprint:\x1b[0m ${t.stack}`);
173
287
  console.log(`\x1b[33mOverview:\x1b[0m ${t.description}`);
174
- console.log(`\x1b[33mHighlighted Skill:\x1b[0m .ai/skills/${t.skill}`);
175
- console.log(` └─> ${t.skillDesc}`);
288
+ if (t.skill) {
289
+ console.log(`\x1b[33mHighlighted Skill:\x1b[0m .ai/skills/${t.skill}`);
290
+ console.log(` └─> ${t.skillDesc}`);
291
+ }
176
292
  console.log('\n\x1b[33mScaffolding Directory Layout:\x1b[0m');
177
293
  console.log(' ├── AGENTS.md (Stack building conventions)');
178
294
  console.log(' ├── MEMORY.md (Architectural constraints record)');
@@ -186,12 +302,25 @@ function handleShowTemplate(name) {
186
302
  console.log(' │ ├── model-map.md (AI routing specifications)');
187
303
  console.log(' │ └── context-budget.md (Token allocation guidelines)');
188
304
  console.log(` └── skills/`);
189
- console.log(` └── ${t.skill} (Custom template skills code boiler)`);
305
+ if (t.skill) {
306
+ console.log(` └── ${t.skill} (Custom template skills code boiler)`);
307
+ } else {
308
+ console.log(` └── [custom-skill].md (Custom template skills code boiler)`);
309
+ }
190
310
  console.log('\nUse \x1b[32minit --template ' + t.name + '\x1b[0m to bootstrap this profile.\n');
191
311
  }
192
312
 
193
313
  function handleInit(options) {
194
314
  console.log(`\n\x1b[34mInitializing multimodel-dev-os in: ${options.target}\x1b[0m`);
315
+
316
+ // Check if requested template is planned
317
+ const tInfo = TEMPLATES[options.template];
318
+ if (tInfo && tInfo.status === 'planned') {
319
+ console.warn(` \x1b[33m[WARNING] Template '${options.template}' is a PLANNED template in the roadmap.\x1b[0m`);
320
+ console.warn(` It is not fully scaffolded yet and will fall back to bootstrapping the 'general-app' profile.\n`);
321
+ options.template = 'general-app';
322
+ }
323
+
195
324
  console.log(`Template profile: \x1b[32m${options.template}\x1b[0m`);
196
325
  if (options.caveman) console.log('Bone variant: \x1b[33mCaveman Mode Active\x1b[0m');
197
326
  if (options.dryRun) console.log('\x1b[36mDry Run active - no actual modifications will occur\x1b[0m');
@@ -337,44 +466,15 @@ function handleInit(options) {
337
466
  // Copy root-level adapter rule files if selected
338
467
  if (!options.dryRun) {
339
468
  options.adapters.forEach(adapter => {
340
- if (adapter === 'cursor') {
341
- const srcFile = join(sourceRoot, 'adapters/cursor/.cursorrules');
342
- const destFile = join(options.target, '.cursorrules');
343
- if (existsSync(srcFile)) {
344
- writeFileSync(destFile, readFileSync(srcFile));
345
- console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m .cursorrules`);
346
- }
347
- } else if (adapter === 'claude') {
348
- const srcFile = join(sourceRoot, 'adapters/claude/CLAUDE.md');
349
- const destFile = join(options.target, 'CLAUDE.md');
350
- if (existsSync(srcFile)) {
351
- writeFileSync(destFile, readFileSync(srcFile));
352
- console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m CLAUDE.md`);
353
- }
354
- } else if (adapter === 'vscode') {
355
- const srcFile = join(sourceRoot, 'adapters/vscode/.vscode/settings.json');
356
- const destDir = join(options.target, '.vscode');
357
- const destFile = join(destDir, 'settings.json');
469
+ const a = ADAPTERS[adapter];
470
+ if (a && a.rules_file) {
471
+ const srcFile = join(sourceRoot, 'adapters', adapter, a.rules_file);
472
+ const destFile = join(options.target, a.rules_file);
473
+ const destDir = dirname(destFile);
358
474
  if (existsSync(srcFile)) {
359
475
  if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
360
476
  writeFileSync(destFile, readFileSync(srcFile));
361
- console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m .vscode/settings.json`);
362
- }
363
- } else if (adapter === 'gemini') {
364
- const srcFile = join(sourceRoot, 'adapters/gemini/GEMINI.md');
365
- const destFile = join(options.target, 'GEMINI.md');
366
- if (existsSync(srcFile)) {
367
- writeFileSync(destFile, readFileSync(srcFile));
368
- console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m GEMINI.md`);
369
- }
370
- } else if (adapter === 'antigravity') {
371
- const srcFile = join(sourceRoot, 'adapters/antigravity/.gemini/settings.json');
372
- const destDir = join(options.target, '.gemini');
373
- const destFile = join(destDir, 'settings.json');
374
- if (existsSync(srcFile)) {
375
- if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
376
- writeFileSync(destFile, readFileSync(srcFile));
377
- console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m .gemini/settings.json`);
477
+ console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m ${a.rules_file}`);
378
478
  }
379
479
  }
380
480
  });
@@ -393,11 +493,10 @@ function handleInit(options) {
393
493
  } else {
394
494
  // Dry run notes
395
495
  options.adapters.forEach(adapter => {
396
- if (adapter === 'cursor') console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m .cursorrules`);
397
- else if (adapter === 'claude') console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m CLAUDE.md`);
398
- else if (adapter === 'vscode') console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m .vscode/settings.json`);
399
- else if (adapter === 'gemini') console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m GEMINI.md`);
400
- else if (adapter === 'antigravity') console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m .gemini/settings.json`);
496
+ const a = ADAPTERS[adapter];
497
+ if (a && a.rules_file) {
498
+ console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m ${a.rules_file}`);
499
+ }
401
500
  });
402
501
  }
403
502
 
@@ -458,6 +557,14 @@ function handleVerify(options) {
458
557
  }
459
558
 
460
559
  function handleDoctor(options) {
560
+ if (options.tokens) {
561
+ handleDoctorTokens(options);
562
+ return;
563
+ }
564
+ if (options.release) {
565
+ handleDoctorRelease(options);
566
+ return;
567
+ }
461
568
  console.log(`\n🩺 \x1b[36mRunning advisory doctor checkup in: ${options.target}\x1b[0m\n`);
462
569
 
463
570
  let warnings = 0;
@@ -562,6 +669,10 @@ function handleDoctor(options) {
562
669
  }
563
670
 
564
671
  function handleValidate(options) {
672
+ if (options && options.allRegistries) {
673
+ handleValidateAllRegistries();
674
+ return;
675
+ }
565
676
  console.log(`\n🛡 \x1b[34mRunning strict schema validation in: ${options.target}\x1b[0m\n`);
566
677
 
567
678
  let errors = 0;
@@ -642,6 +753,26 @@ function handleValidate(options) {
642
753
  assertAdapter('antigravity', '.gemini/settings.json');
643
754
  }
644
755
 
756
+ // Template-specific validation
757
+ if (options.template) {
758
+ const tInfo = TEMPLATES[options.template];
759
+ if (tInfo && Array.isArray(tInfo.required_files)) {
760
+ console.log(`\n📋 Validating required files for template '${options.template}':`);
761
+ tInfo.required_files.forEach(f => assertPath(f, 'file'));
762
+ } else if (options.template === 'expo-react-native-android') {
763
+ const mobileFiles = [
764
+ 'app.json',
765
+ 'eas.json',
766
+ 'app.config.ts',
767
+ 'jest.config.js',
768
+ 'src/app/_layout.tsx',
769
+ 'src/lib/secure-storage.ts',
770
+ 'src/services/api-client.ts'
771
+ ];
772
+ mobileFiles.forEach(f => assertPath(f, 'file'));
773
+ }
774
+ }
775
+
645
776
  console.log('\n==================================================');
646
777
  if (errors > 0) {
647
778
  console.error(` \x1b[31mValidation FAILED. Found ${errors} strict structural compliance errors.\x1b[0m\n`);
@@ -651,3 +782,591 @@ function handleValidate(options) {
651
782
  process.exit(0);
652
783
  }
653
784
  }
785
+
786
+ // --- YAML Parser Helper ---
787
+ function parseYaml(content) {
788
+ try {
789
+ const root = {};
790
+ const stack = [{ obj: root, indent: -1, key: null, isArray: false }];
791
+
792
+ const lines = content.split(/\r?\n/);
793
+ for (let line of lines) {
794
+ const commentIdx = line.indexOf('#');
795
+ if (commentIdx !== -1) {
796
+ line = line.substring(0, commentIdx);
797
+ }
798
+ line = line.trimEnd();
799
+ if (!line.trim()) continue;
800
+
801
+ const indent = line.match(/^ */)[0].length;
802
+ let trimmed = line.trim();
803
+
804
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
805
+ stack.pop();
806
+ }
807
+
808
+ const parent = stack[stack.length - 1];
809
+
810
+ if (trimmed.startsWith('-')) {
811
+ trimmed = trimmed.substring(1).trim();
812
+ if (!Array.isArray(parent.obj)) {
813
+ const grandparent = stack[stack.length - 2];
814
+ if (grandparent) {
815
+ grandparent.obj[parent.key] = [];
816
+ parent.obj = grandparent.obj[parent.key];
817
+ }
818
+ }
819
+
820
+ const colonIdx = trimmed.indexOf(':');
821
+ if (colonIdx === -1) {
822
+ parent.obj.push(trimmed);
823
+ } else {
824
+ const key = trimmed.substring(0, colonIdx).trim();
825
+ let val = trimmed.substring(colonIdx + 1).trim();
826
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
827
+ val = val.substring(1, val.length - 1);
828
+ }
829
+ if (val === 'true') val = true;
830
+ else if (val === 'false') val = false;
831
+ else if (val === 'null') val = null;
832
+ else if (/^\d+$/.test(val)) val = parseInt(val, 10);
833
+
834
+ const newObj = { [key]: val };
835
+ parent.obj.push(newObj);
836
+ stack.push({ obj: newObj, indent: indent, key: key, isArray: false });
837
+ }
838
+ } else {
839
+ const colonIdx = trimmed.indexOf(':');
840
+ if (colonIdx === -1) continue;
841
+
842
+ const key = trimmed.substring(0, colonIdx).trim();
843
+ let val = trimmed.substring(colonIdx + 1).trim();
844
+
845
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
846
+ val = val.substring(1, val.length - 1);
847
+ }
848
+ if (val === 'true') val = true;
849
+ else if (val === 'false') val = false;
850
+ else if (val === 'null') val = null;
851
+ else if (/^\d+$/.test(val)) val = parseInt(val, 10);
852
+
853
+ if (val === '') {
854
+ parent.obj[key] = {};
855
+ stack.push({ obj: parent.obj[key], indent: indent, key: key, isArray: false });
856
+ } else {
857
+ parent.obj[key] = val;
858
+ }
859
+ }
860
+ }
861
+ return root;
862
+ } catch (e) {
863
+ console.warn(`\x1b[33m[WARNING] Failed to parse YAML: ${e.message}\x1b[0m`);
864
+ return {};
865
+ }
866
+ }
867
+
868
+ // --- Command Handler Functions ---
869
+ function handleListModels(options) {
870
+ const registryPath = join(sourceRoot, '.ai', 'models', 'registry.yaml');
871
+ if (!existsSync(registryPath)) {
872
+ console.error('Error: Model registry not found.');
873
+ process.exit(1);
874
+ }
875
+ const registry = parseYaml(readFileSync(registryPath, 'utf8'));
876
+ const models = registry.models || {};
877
+ if (options && options.json) {
878
+ console.log(JSON.stringify(models, null, 2));
879
+ return;
880
+ }
881
+ console.log(`\n🤖 \x1b[36mModel Registry [v${version}]\x1b[0m`);
882
+ console.log('==================================================');
883
+ Object.keys(models).forEach(name => {
884
+ const m = models[name];
885
+ console.log(`\n\x1b[32m* ${name}\x1b[0m (${m.alias || ''})`);
886
+ console.log(` \x1b[33mProvider:\x1b[0m ${m.provider}`);
887
+ console.log(` \x1b[33mOfficial ID:\x1b[0m ${m.official_id}`);
888
+ console.log(` \x1b[33mContext Window:\x1b[0m ${m.context_window} tokens`);
889
+ console.log(` \x1b[33mTiers:\x1b[0m Cost: ${m.tiers?.cost}, Reasoning: ${m.tiers?.reasoning}, Coding: ${m.tiers?.coding}`);
890
+ });
891
+ console.log('\nUse \x1b[36mshow-model <model-alias>\x1b[0m to view detailed model capabilities.\n');
892
+ }
893
+
894
+ function handleShowModel(name) {
895
+ const registryPath = join(sourceRoot, '.ai', 'models', 'registry.yaml');
896
+ if (!existsSync(registryPath)) {
897
+ console.error('Error: Model registry not found.');
898
+ process.exit(1);
899
+ }
900
+ const registry = parseYaml(readFileSync(registryPath, 'utf8'));
901
+ const models = registry.models || {};
902
+ const m = models[name];
903
+ if (!m) {
904
+ console.error(`\x1b[31mError: Model alias '${name}' not found in registry.\x1b[0m`);
905
+ process.exit(1);
906
+ }
907
+ console.log(`\n🔍 \x1b[36mModel: ${name}\x1b[0m`);
908
+ console.log('==================================================');
909
+ console.log(`\x1b[33mProvider:\x1b[0m ${m.provider}`);
910
+ console.log(`\x1b[33mAlias:\x1b[0m ${m.alias}`);
911
+ console.log(`\x1b[33mOfficial ID:\x1b[0m ${m.official_id}`);
912
+ console.log(`\x1b[33mContext Window:\x1b[0m ${m.context_window} tokens`);
913
+ console.log(`\x1b[33mCapabilities:\x1b[0m`);
914
+ console.log(` ├─ Vision: ${m.capabilities?.vision ? 'Yes' : 'No'}`);
915
+ console.log(` └─ Tool Use: ${m.capabilities?.tool_use ? 'Yes' : 'No'}`);
916
+ console.log(`\x1b[33mTiers:\x1b[0m`);
917
+ console.log(` ├─ Cost: ${m.tiers?.cost}`);
918
+ console.log(` ├─ Speed: ${m.tiers?.speed}`);
919
+ console.log(` ├─ Reasoning: ${m.tiers?.reasoning}`);
920
+ console.log(` └─ Coding: ${m.tiers?.coding}`);
921
+ console.log();
922
+ }
923
+
924
+ function handleListProviders() {
925
+ const providersPath = join(sourceRoot, '.ai', 'models', 'providers.yaml');
926
+ if (!existsSync(providersPath)) {
927
+ console.error('Error: Providers registry not found.');
928
+ process.exit(1);
929
+ }
930
+ const reg = parseYaml(readFileSync(providersPath, 'utf8'));
931
+ const providers = reg.providers || {};
932
+ console.log(`\n🔌 \x1b[36mAI Providers [v${version}]\x1b[0m`);
933
+ console.log('==================================================');
934
+ Object.keys(providers).forEach(name => {
935
+ const p = providers[name];
936
+ console.log(`\n\x1b[32m* ${p.name || name}\x1b[0m (${name})`);
937
+ console.log(` \x1b[33mEndpoint:\x1b[0m ${p.api_endpoint || 'Local'}`);
938
+ console.log(` \x1b[33mEnv Key:\x1b[0m ${p.env_key || 'None'}`);
939
+ });
940
+ console.log();
941
+ }
942
+
943
+ function handleRouteModel(task) {
944
+ const presetsPath = join(sourceRoot, '.ai', 'models', 'routing-presets.yaml');
945
+ if (!existsSync(presetsPath)) {
946
+ console.error('Error: Routing presets not found.');
947
+ process.exit(1);
948
+ }
949
+ const reg = parseYaml(readFileSync(presetsPath, 'utf8'));
950
+ const presets = reg.presets || {};
951
+ const preset = presets[task];
952
+ if (!preset) {
953
+ console.error(`\x1b[31mError: Routing preset for task '${task}' not found. Available: ${Object.keys(presets).join(', ')}\x1b[0m`);
954
+ process.exit(1);
955
+ }
956
+ console.log(`\n🎯 \x1b[36mRouting Suggestion for: ${task}\x1b[0m`);
957
+ console.log('==================================================');
958
+ console.log(`\x1b[33mPrimary Model:\x1b[0m \x1b[32m${preset.primary}\x1b[0m`);
959
+ console.log(`\x1b[33mFallback Model:\x1b[0m \x1b[33m${preset.fallback}\x1b[0m`);
960
+ console.log();
961
+ }
962
+
963
+ function handleListAdapters(options) {
964
+ const adaptersPath = join(sourceRoot, '.ai', 'adapters', 'registry.yaml');
965
+ if (!existsSync(adaptersPath)) {
966
+ console.error('Error: Adapters registry not found.');
967
+ process.exit(1);
968
+ }
969
+ const reg = parseYaml(readFileSync(adaptersPath, 'utf8'));
970
+ const adapters = reg.adapters || {};
971
+ if (options && options.json) {
972
+ console.log(JSON.stringify(adapters, null, 2));
973
+ return;
974
+ }
975
+ console.log(`\n🔌 \x1b[36mIDE & Agent Adapters [v${version}]\x1b[0m`);
976
+ console.log('==================================================');
977
+ Object.keys(adapters).forEach(name => {
978
+ const a = adapters[name];
979
+ console.log(`\n\x1b[32m* ${a.name || name}\x1b[0m (${name})`);
980
+ console.log(` \x1b[33mRules File:\x1b[0m ${a.rules_file}`);
981
+ console.log(` \x1b[33mAdapter Type:\x1b[0m ${a.type}`);
982
+ console.log(` \x1b[33mRule Format:\x1b[0m ${a.format}`);
983
+ });
984
+ console.log('\nUse \x1b[36mshow-adapter <adapter-name>\x1b[0m to view detailed adapter metadata.\n');
985
+ }
986
+
987
+ function handleShowAdapter(name) {
988
+ const adaptersPath = join(sourceRoot, '.ai', 'adapters', 'registry.yaml');
989
+ if (!existsSync(adaptersPath)) {
990
+ console.error('Error: Adapters registry not found.');
991
+ process.exit(1);
992
+ }
993
+ const reg = parseYaml(readFileSync(adaptersPath, 'utf8'));
994
+ const adapters = reg.adapters || {};
995
+ const a = adapters[name];
996
+ if (!a) {
997
+ console.error(`\x1b[31mError: Adapter '${name}' not found in registry.\x1b[0m`);
998
+ process.exit(1);
999
+ }
1000
+ console.log(`\n🔍 \x1b[36mAdapter: ${a.name || name}\x1b[0m`);
1001
+ console.log('==================================================');
1002
+ console.log(`\x1b[33mRules File:\x1b[0m ${a.rules_file}`);
1003
+ console.log(`\x1b[33mType:\x1b[0m ${a.type}`);
1004
+ console.log(`\x1b[33mFormat:\x1b[0m ${a.format}`);
1005
+ console.log();
1006
+ }
1007
+
1008
+ function handleListSkills(options) {
1009
+ const skillsDir = join(options.target, '.ai', 'skills');
1010
+ if (!existsSync(skillsDir)) {
1011
+ console.log('\n\x1b[33m[Notice] .ai/skills directory is not initialized in the target workspace.\x1b[0m\n');
1012
+ return;
1013
+ }
1014
+ const files = readdirSync(skillsDir).filter(f => f.endsWith('.md'));
1015
+ console.log(`\n🧠 \x1b[36mAvailable Skills in Target [v${version}]\x1b[0m`);
1016
+ console.log('==================================================');
1017
+ files.forEach(f => {
1018
+ console.log(` \x1b[32m- ${f.replace('.md', '')}\x1b[0m (file: .ai/skills/${f})`);
1019
+ });
1020
+ console.log('\nUse \x1b[36mshow-skill <skill-name>\x1b[0m to read a skill\'s prompt text.\n');
1021
+ }
1022
+
1023
+ function handleShowSkill(name, options) {
1024
+ const skillsDir = join(options.target, '.ai', 'skills');
1025
+ const skillFile = join(skillsDir, name.endsWith('.md') ? name : `${name}.md`);
1026
+ if (!existsSync(skillFile)) {
1027
+ console.error(`\x1b[31mError: Skill '${name}' not found in target .ai/skills/.\x1b[0m`);
1028
+ process.exit(1);
1029
+ }
1030
+ console.log(`\n📖 \x1b[36mSkill Prompt: ${name}\x1b[0m`);
1031
+ console.log('==================================================');
1032
+ console.log(readFileSync(skillFile, 'utf8'));
1033
+ console.log();
1034
+ }
1035
+
1036
+ function parseThresholdToBytes(val) {
1037
+ if (!val) return 100 * 1024; // Default 100KB
1038
+ const matches = val.match(/^(\d+)(KB|MB|B)?$/i);
1039
+ if (!matches) return 100 * 1024;
1040
+ const num = parseInt(matches[1], 10);
1041
+ const unit = (matches[2] || '').toUpperCase();
1042
+ if (unit === 'MB') return num * 1024 * 1024;
1043
+ if (unit === 'KB') return num * 1024;
1044
+ return num;
1045
+ }
1046
+
1047
+ function handleDoctorTokens(options) {
1048
+ console.log(`\n🪙 \x1b[36mRunning Token Budget & Sink Audit in: ${options.target}\x1b[0m\n`);
1049
+
1050
+ const filesFound = [];
1051
+ const ignoredDirs = ['.git', 'node_modules', 'dist', 'build', '.next', '.expo', 'bin', 'assets', 'docs', 'web-build', 'out', 'coverage', '.nuxt', '.svelte-kit', 'bower_components', 'vendor'];
1052
+
1053
+ function scan(dir) {
1054
+ if (!existsSync(dir)) return;
1055
+ const items = readdirSync(dir);
1056
+ for (const item of items) {
1057
+ if (ignoredDirs.includes(item)) continue;
1058
+ const fullPath = join(dir, item);
1059
+ try {
1060
+ const stat = statSync(fullPath);
1061
+ if (stat.isDirectory()) {
1062
+ scan(fullPath);
1063
+ } else if (stat.isFile()) {
1064
+ filesFound.push({
1065
+ relPath: replaceBackslashes(fullPath.replace(options.target, '')),
1066
+ size: stat.size
1067
+ });
1068
+ }
1069
+ } catch (e) {}
1070
+ }
1071
+ }
1072
+
1073
+ function replaceBackslashes(p) {
1074
+ let clean = p.replace(/\\/g, '/');
1075
+ if (clean.startsWith('/')) clean = clean.substring(1);
1076
+ return clean;
1077
+ }
1078
+
1079
+ scan(options.target);
1080
+
1081
+ filesFound.sort((a, b) => b.size - a.size);
1082
+
1083
+ const thresholdBytes = parseThresholdToBytes(options.threshold);
1084
+ const thresholdStr = options.threshold || '100KB';
1085
+
1086
+ console.log('Top 10 Largest Files in Scanned Workspace:');
1087
+ filesFound.slice(0, 10).forEach(f => {
1088
+ let sizeDesc = `${f.size} bytes`;
1089
+ if (f.size > 1024 * 1024) sizeDesc = `${(f.size / (1024 * 1024)).toFixed(2)} MB`;
1090
+ else if (f.size > 1024) sizeDesc = `${(f.size / 1024).toFixed(2)} KB`;
1091
+
1092
+ let color = '\x1b[32m';
1093
+ if (f.size > thresholdBytes) color = '\x1b[31m';
1094
+ else if (f.size > thresholdBytes * 0.3) color = '\x1b[33m';
1095
+
1096
+ console.log(` ${color}* ${f.relPath}\x1b[0m (${sizeDesc})`);
1097
+ });
1098
+
1099
+ console.log('\n==================================================');
1100
+ console.log(`Total Scanned Files: ${filesFound.length}`);
1101
+ console.log(`Recommendation: Exclude files in red (>${thresholdStr}) from active coding prompts or add them to your adapter ignore rules.`);
1102
+ console.log();
1103
+ }
1104
+
1105
+ function handleValidateTemplate(name) {
1106
+ const t = TEMPLATES[name];
1107
+ if (!t) {
1108
+ console.error(`\x1b[31mError: Template '${name}' not found in registry.\x1b[0m`);
1109
+ process.exit(1);
1110
+ }
1111
+ console.log(`\n📋 \x1b[34mValidating Template: ${name}\x1b[0m`);
1112
+
1113
+ let errors = 0;
1114
+ const reqKeys = ['name', 'description', 'stack', 'category', 'status', 'maturity', 'required_files'];
1115
+ reqKeys.forEach(k => {
1116
+ if (t[k] === undefined || t[k] === null) {
1117
+ console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
1118
+ errors++;
1119
+ } else {
1120
+ console.log(` \x1b[32m✓\x1b[0m Registry key: ${k}`);
1121
+ }
1122
+ });
1123
+
1124
+ const templateDir = join(sourceRoot, 'examples', name);
1125
+ if (!existsSync(templateDir)) {
1126
+ console.error(` \x1b[31m✗ Source folder missing: examples/${name}\x1b[0m`);
1127
+ errors++;
1128
+ } else {
1129
+ console.log(` \x1b[32m✓\x1b[0m Source folder: examples/${name}`);
1130
+ if (Array.isArray(t.required_files)) {
1131
+ t.required_files.forEach(f => {
1132
+ const filePath = join(templateDir, f);
1133
+ const globalPath = join(sourceRoot, f);
1134
+ if (existsSync(filePath)) {
1135
+ console.log(` \x1b[32m✓\x1b[0m Required file (template override): ${f}`);
1136
+ } else if (existsSync(globalPath)) {
1137
+ console.log(` \x1b[32m✓\x1b[0m Required file (global fallback): ${f}`);
1138
+ } else {
1139
+ console.error(` \x1b[31m✗ Required file missing: ${f}\x1b[0m`);
1140
+ errors++;
1141
+ }
1142
+ });
1143
+ }
1144
+ }
1145
+
1146
+ if (errors > 0) {
1147
+ console.error(`\n\x1b[31mValidation FAILED with ${errors} errors.\x1b[0m\n`);
1148
+ process.exit(1);
1149
+ } else {
1150
+ console.log(`\n\x1b[32m✔ Template '${name}' is fully valid and compliant!\x1b[0m\n`);
1151
+ process.exit(0);
1152
+ }
1153
+ }
1154
+
1155
+ function handleValidateAdapter(name) {
1156
+ const a = ADAPTERS[name];
1157
+ if (!a) {
1158
+ console.error(`\x1b[31mError: Adapter '${name}' not found in registry.\x1b[0m`);
1159
+ process.exit(1);
1160
+ }
1161
+ console.log(`\n📋 \x1b[34mValidating Adapter: ${name}\x1b[0m`);
1162
+
1163
+ let errors = 0;
1164
+ const reqKeys = ['name', 'rules_file', 'format', 'type'];
1165
+ reqKeys.forEach(k => {
1166
+ if (!a[k]) {
1167
+ console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
1168
+ errors++;
1169
+ } else {
1170
+ console.log(` \x1b[32m✓\x1b[0m Registry key: ${k}`);
1171
+ }
1172
+ });
1173
+
1174
+ const adapterDir = join(sourceRoot, 'adapters', name);
1175
+ if (!existsSync(adapterDir)) {
1176
+ console.error(` \x1b[31m✗ Source folder missing: adapters/${name}\x1b[0m`);
1177
+ errors++;
1178
+ } else {
1179
+ console.log(` \x1b[32m✓\x1b[0m Source folder: adapters/${name}`);
1180
+ const setupFile = join(adapterDir, 'setup.md');
1181
+ if (existsSync(setupFile)) {
1182
+ console.log(` \x1b[32m✓\x1b[0m Required file: setup.md`);
1183
+ } else {
1184
+ console.error(` \x1b[31m✗ Required file missing: adapters/${name}/setup.md\x1b[0m`);
1185
+ errors++;
1186
+ }
1187
+
1188
+ if (a.rules_file) {
1189
+ const rulesFile = join(adapterDir, a.rules_file);
1190
+ if (existsSync(rulesFile)) {
1191
+ console.log(` \x1b[32m✓\x1b[0m Rules file: ${a.rules_file}`);
1192
+ } else {
1193
+ console.error(` \x1b[31m✗ Rules file missing: adapters/${name}/${a.rules_file}\x1b[0m`);
1194
+ errors++;
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ if (errors > 0) {
1200
+ console.error(`\n\x1b[31mValidation FAILED with ${errors} errors.\x1b[0m\n`);
1201
+ process.exit(1);
1202
+ } else {
1203
+ console.log(`\n\x1b[32m✔ Adapter '${name}' is fully valid and compliant!\x1b[0m\n`);
1204
+ process.exit(0);
1205
+ }
1206
+ }
1207
+
1208
+ function handleValidateSkill(name, options) {
1209
+ const skillsDir = join(options.target, '.ai', 'skills');
1210
+ let skillFile = join(skillsDir, name.endsWith('.md') ? name : `${name}.md`);
1211
+ if (!existsSync(skillFile)) {
1212
+ skillFile = join(sourceRoot, '.ai', 'skills', name.endsWith('.md') ? name : `${name}.md`);
1213
+ }
1214
+
1215
+ if (!existsSync(skillFile)) {
1216
+ console.error(`\x1b[31mError: Skill '${name}' not found.\x1b[0m`);
1217
+ process.exit(1);
1218
+ }
1219
+
1220
+ console.log(`\n📋 \x1b[34mValidating Skill: ${name}\x1b[0m`);
1221
+ const content = readFileSync(skillFile, 'utf8');
1222
+ let errors = 0;
1223
+
1224
+ const reqHeaders = [
1225
+ { header: '# Purpose', regex: /^#\s+Purpose/mi },
1226
+ { header: '# Activation Trigger', regex: /^#\s+Activation\s+Trigger/mi },
1227
+ { header: '# Input Context', regex: /^#\s+Input\s+Context/mi },
1228
+ { header: '# Output Contract', regex: /^#\s+Output\s+Contract/mi },
1229
+ { header: '# Token Budget', regex: /^#\s+Token\s+Budget/mi }
1230
+ ];
1231
+
1232
+ reqHeaders.forEach(req => {
1233
+ if (req.regex.test(content)) {
1234
+ console.log(` \x1b[32m✓\x1b[0m Found required header: ${req.header}`);
1235
+ } else {
1236
+ console.error(` \x1b[31m✗ Missing required header: ${req.header}\x1b[0m`);
1237
+ errors++;
1238
+ }
1239
+ });
1240
+
1241
+ if (errors > 0) {
1242
+ console.error(`\n\x1b[31mValidation FAILED with ${errors} errors.\x1b[0m\n`);
1243
+ process.exit(1);
1244
+ } else {
1245
+ console.log(`\n\x1b[32m✔ Skill '${name}' is fully valid and compliant!\x1b[0m\n`);
1246
+ process.exit(0);
1247
+ }
1248
+ }
1249
+
1250
+ function handleValidateAllRegistries() {
1251
+ console.log(`\n🛡 \x1b[34mValidating All Registry Entries\x1b[0m\n`);
1252
+ let errors = 0;
1253
+
1254
+ // Validate all templates
1255
+ console.log('--- Templates Registry Validation ---');
1256
+ Object.keys(TEMPLATES).forEach(name => {
1257
+ const t = TEMPLATES[name];
1258
+ console.log(`\nValidating Template: ${name}`);
1259
+ const reqKeys = ['name', 'description', 'stack', 'category', 'status', 'maturity'];
1260
+ if (t.status !== 'planned') {
1261
+ reqKeys.push('required_files');
1262
+ }
1263
+ reqKeys.forEach(k => {
1264
+ if (t[k] === undefined || t[k] === null) {
1265
+ console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
1266
+ errors++;
1267
+ }
1268
+ });
1269
+
1270
+ const templateDir = join(sourceRoot, 'examples', name);
1271
+ if (t.status === 'stable' && !existsSync(templateDir)) {
1272
+ console.error(` \x1b[31m✗ Stable template source folder missing: examples/${name}\x1b[0m`);
1273
+ errors++;
1274
+ }
1275
+ });
1276
+
1277
+ // Validate all adapters
1278
+ console.log('\n--- Adapters Registry Validation ---');
1279
+ Object.keys(ADAPTERS).forEach(name => {
1280
+ const a = ADAPTERS[name];
1281
+ console.log(`Validating Adapter: ${name}`);
1282
+ const reqKeys = ['name', 'rules_file', 'format', 'type'];
1283
+ reqKeys.forEach(k => {
1284
+ if (!a[k]) {
1285
+ console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
1286
+ errors++;
1287
+ }
1288
+ });
1289
+ });
1290
+
1291
+ console.log('\n==================================================');
1292
+ if (errors > 0) {
1293
+ console.error(` \x1b[31mAll Registries validation FAILED. Found ${errors} schema errors.\x1b[0m\n`);
1294
+ process.exit(1);
1295
+ } else {
1296
+ console.log(' \x1b[32m✔ All Registries validation PASSED. All templates and adapters are valid.\x1b[0m\n');
1297
+ process.exit(0);
1298
+ }
1299
+ }
1300
+
1301
+ function handleDoctorRelease(options) {
1302
+ console.log(`\n🩺 \x1b[36mRunning release audit doctor in: ${sourceRoot}\x1b[0m\n`);
1303
+ let warnings = 0;
1304
+
1305
+ // 1. Version checks
1306
+ let packageVersion = 'unknown';
1307
+ try {
1308
+ const pkg = JSON.parse(readFileSync(join(sourceRoot, 'package.json'), 'utf8'));
1309
+ packageVersion = pkg.version;
1310
+ console.log(` \x1b[32m✓\x1b[0m package.json version: ${packageVersion}`);
1311
+ } catch (e) {
1312
+ console.warn(' \x1b[31m✗\x1b[0m Failed to parse package.json');
1313
+ warnings++;
1314
+ }
1315
+
1316
+ const checkInstallScript = (filename, regex) => {
1317
+ const filePath = join(sourceRoot, filename);
1318
+ if (existsSync(filePath)) {
1319
+ const content = readFileSync(filePath, 'utf8');
1320
+ const match = content.match(regex);
1321
+ if (match && match[1] === packageVersion) {
1322
+ console.log(` \x1b[32m✓\x1b[0m ${filename} version aligns: ${match[1]}`);
1323
+ } else {
1324
+ console.warn(` \x1b[33m[WARNING]\x1b[0m ${filename} version mismatch (found ${match ? match[1] : 'none'}, expected ${packageVersion})`);
1325
+ warnings++;
1326
+ }
1327
+ }
1328
+ };
1329
+
1330
+ checkInstallScript('scripts/install.sh', /VERSION="([^"]+)"/i);
1331
+ checkInstallScript('scripts/install.ps1', /\$VERSION\s*=\s*"([^"]+)"/i);
1332
+
1333
+ // 2. Blacklisted files audit
1334
+ const blacklist = ['.npmrc'];
1335
+ blacklist.forEach(file => {
1336
+ const fullPath = join(sourceRoot, file);
1337
+ if (existsSync(fullPath)) {
1338
+ console.warn(` \x1b[33m[WARNING]\x1b[0m Blacklisted file found in release root: ${file}`);
1339
+ warnings++;
1340
+ } else {
1341
+ console.log(` \x1b[32m✓\x1b[0m No root blacklisted file: ${file}`);
1342
+ }
1343
+ });
1344
+
1345
+ // Recursively scan examples/ for .env and keystores
1346
+ const scanSafety = (dir) => {
1347
+ if (!existsSync(dir)) return;
1348
+ const items = readdirSync(dir);
1349
+ for (const item of items) {
1350
+ const fullPath = join(dir, item);
1351
+ try {
1352
+ const stat = statSync(fullPath);
1353
+ if (stat.isDirectory()) {
1354
+ scanSafety(fullPath);
1355
+ } else if (stat.isFile()) {
1356
+ if (item === '.env' || item.endsWith('.keystore') || item.endsWith('.jks')) {
1357
+ console.warn(` \x1b[33m[WARNING]\x1b[0m Unsafe file inside templates/examples: ${fullPath.replace(sourceRoot, '')}`);
1358
+ warnings++;
1359
+ }
1360
+ }
1361
+ } catch (e) {}
1362
+ }
1363
+ };
1364
+ scanSafety(join(sourceRoot, 'examples'));
1365
+
1366
+ console.log('\n==================================================');
1367
+ if (warnings > 0) {
1368
+ console.warn(` \x1b[33mRelease doctor complete with ${warnings} warnings.\x1b[0m\n`);
1369
+ } else {
1370
+ console.log(' \x1b[32m✔ Release hygiene checks PASSED successfully!\x1b[0m\n');
1371
+ }
1372
+ }