multimodel-dev-os 1.1.0 → 2.0.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/adapters/custom-adapter.example.yaml +9 -0
- package/.ai/adapters/registry.yaml +56 -0
- package/.ai/models/README.md +14 -0
- package/.ai/models/local-models.yaml +20 -0
- package/.ai/models/providers.yaml +29 -0
- package/.ai/models/registry.yaml +73 -0
- package/.ai/models/routing-presets.yaml +23 -0
- package/.ai/skills/custom-skill.example.md +15 -0
- package/.ai/templates/custom-template.example.yaml +19 -0
- package/.ai/templates/registry.yaml +522 -0
- package/README.md +30 -18
- package/bin/multimodel-dev-os.js +835 -92
- package/docs/.vitepress/config.js +36 -1
- package/docs/adapter-authoring.md +46 -0
- package/docs/agent-compatibility.md +51 -0
- package/docs/cli-roadmap.md +12 -23
- package/docs/faq.md +1 -1
- package/docs/final-launch.md +5 -4
- package/docs/index.md +5 -5
- package/docs/local-models.md +48 -0
- package/docs/mobile-android.md +75 -0
- package/docs/model-compatibility.md +65 -0
- package/docs/model-routing.md +45 -0
- package/docs/npm-publishing.md +38 -11
- package/docs/package-safety.md +29 -0
- package/docs/provider-strategy.md +44 -0
- package/docs/public/llms-full.txt +82 -73
- package/docs/public/llms.txt +36 -34
- package/docs/public/sitemap.xml +51 -1
- package/docs/quickstart.md +11 -18
- package/docs/registry-contribution.md +20 -0
- package/docs/release-policy.md +9 -0
- package/docs/skill-authoring.md +56 -0
- package/docs/template-authoring.md +65 -0
- package/docs/templates-guide.md +3 -3
- package/docs/token-optimization.md +27 -0
- package/docs/v2-migration.md +31 -0
- package/docs/v2-release-checklist.md +30 -0
- package/docs/v2-roadmap.md +95 -0
- package/examples/expo-react-native-android/.ai/config.yaml +22 -0
- package/examples/expo-react-native-android/.ai/context/architecture.md +18 -0
- package/examples/expo-react-native-android/.ai/context/context-budget.md +4 -0
- package/examples/expo-react-native-android/.ai/context/model-map.md +6 -0
- package/examples/expo-react-native-android/.ai/context/project-brief.md +9 -0
- package/examples/expo-react-native-android/.ai/session-logs/.gitkeep +1 -0
- package/examples/expo-react-native-android/.ai/skills/expo-android-build.md +11 -0
- package/examples/expo-react-native-android/AGENTS.md +20 -0
- package/examples/expo-react-native-android/MEMORY.md +13 -0
- package/examples/expo-react-native-android/README.md +101 -0
- package/examples/expo-react-native-android/RUNBOOK.md +36 -0
- package/examples/expo-react-native-android/TASKS.md +14 -0
- package/examples/expo-react-native-android/app.config.ts +40 -0
- package/examples/expo-react-native-android/app.json +34 -0
- package/examples/expo-react-native-android/eas.json +26 -0
- package/examples/expo-react-native-android/jest.config.js +11 -0
- package/examples/expo-react-native-android/src/app/_layout.tsx +89 -0
- package/examples/expo-react-native-android/src/lib/secure-storage.ts +63 -0
- package/examples/expo-react-native-android/src/services/api-client.ts +106 -0
- package/package.json +3 -2
- package/scripts/install.ps1 +230 -230
- package/scripts/install.sh +1 -1
- package/scripts/prepublish-guard.js +43 -0
- package/scripts/verify.js +178 -1
package/bin/multimodel-dev-os.js
CHANGED
|
@@ -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.
|
|
16
|
+
let version = '2.0.1';
|
|
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
|
-
|
|
64
|
-
'
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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,
|
|
144
|
-
console.log('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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,26 @@ 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
|
-
|
|
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 planned for a future release and is not yet available.\x1b[0m`);
|
|
320
|
+
console.warn(` To view available templates, run: \x1b[36mnpx multimodel-dev-os templates\x1b[0m`);
|
|
321
|
+
console.warn(` Falling back to the stable \x1b[32m'general-app'\x1b[0m profile...\n`);
|
|
322
|
+
options.template = 'general-app';
|
|
323
|
+
}
|
|
324
|
+
|
|
195
325
|
console.log(`Template profile: \x1b[32m${options.template}\x1b[0m`);
|
|
196
326
|
if (options.caveman) console.log('Bone variant: \x1b[33mCaveman Mode Active\x1b[0m');
|
|
197
327
|
if (options.dryRun) console.log('\x1b[36mDry Run active - no actual modifications will occur\x1b[0m');
|
|
@@ -202,7 +332,9 @@ function handleInit(options) {
|
|
|
202
332
|
// Source path mapping for core files
|
|
203
333
|
let templateDir = join(sourceRoot, 'examples', options.template);
|
|
204
334
|
if (!existsSync(templateDir)) {
|
|
205
|
-
console.warn(` \x1b[33m[WARNING] Template '${options.template}'
|
|
335
|
+
console.warn(` \x1b[33m[WARNING] Template '${options.template}' source files could not be found.\x1b[0m`);
|
|
336
|
+
console.warn(` To view available templates, run: \x1b[36mnpx multimodel-dev-os templates\x1b[0m`);
|
|
337
|
+
console.warn(` Falling back to the stable \x1b[32m'general-app'\x1b[0m profile...\n`);
|
|
206
338
|
templateDir = join(sourceRoot, 'examples', 'general-app');
|
|
207
339
|
}
|
|
208
340
|
|
|
@@ -337,44 +469,15 @@ function handleInit(options) {
|
|
|
337
469
|
// Copy root-level adapter rule files if selected
|
|
338
470
|
if (!options.dryRun) {
|
|
339
471
|
options.adapters.forEach(adapter => {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
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');
|
|
472
|
+
const a = ADAPTERS[adapter];
|
|
473
|
+
if (a && a.rules_file) {
|
|
474
|
+
const srcFile = join(sourceRoot, 'adapters', adapter, a.rules_file);
|
|
475
|
+
const destFile = join(options.target, a.rules_file);
|
|
476
|
+
const destDir = dirname(destFile);
|
|
358
477
|
if (existsSync(srcFile)) {
|
|
359
478
|
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
360
479
|
writeFileSync(destFile, readFileSync(srcFile));
|
|
361
|
-
console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m .
|
|
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`);
|
|
480
|
+
console.log(` \x1b[32mCREATE ROOT ADAPTER FILE:\x1b[0m ${a.rules_file}`);
|
|
378
481
|
}
|
|
379
482
|
}
|
|
380
483
|
});
|
|
@@ -393,15 +496,35 @@ function handleInit(options) {
|
|
|
393
496
|
} else {
|
|
394
497
|
// Dry run notes
|
|
395
498
|
options.adapters.forEach(adapter => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
else if (adapter === 'antigravity') console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m .gemini/settings.json`);
|
|
499
|
+
const a = ADAPTERS[adapter];
|
|
500
|
+
if (a && a.rules_file) {
|
|
501
|
+
console.log(` \x1b[36m[DRY-RUN] WOULD CREATE ROOT ADAPTER FILE:\x1b[0m ${a.rules_file}`);
|
|
502
|
+
}
|
|
401
503
|
});
|
|
402
504
|
}
|
|
403
505
|
|
|
404
506
|
console.log(`\n\x1b[32m✔ Project initialized successfully! [Total Operations: ${operations.length}]\x1b[0m\n`);
|
|
507
|
+
console.log(`\x1b[36mNext Steps to Complete Integration:\x1b[0m`);
|
|
508
|
+
console.log(` 1. \x1b[1mEdit AGENTS.md\x1b[0m in your project root to document your stack context.`);
|
|
509
|
+
console.log(` 2. \x1b[1mEdit .ai/config.yaml\x1b[0m to configure active model routing presets.`);
|
|
510
|
+
if (options.adapters.length > 0) {
|
|
511
|
+
console.log(` 3. \x1b[1mActivate IDE / Agent Rules:\x1b[0m`);
|
|
512
|
+
console.log(` Ensure adapter configuration files are copied or linked to the root of your workspace:`);
|
|
513
|
+
options.adapters.forEach(adapter => {
|
|
514
|
+
const a = ADAPTERS[adapter];
|
|
515
|
+
if (a && a.rules_file) {
|
|
516
|
+
console.log(` - For \x1b[32m${a.name || adapter}\x1b[0m: Check the root-level \x1b[33m${a.rules_file}\x1b[0m file`);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
} else {
|
|
520
|
+
console.log(` 3. \x1b[1mSelect IDE / Tool Adapters:\x1b[0m`);
|
|
521
|
+
console.log(` To generate rules for Cursor, Claude Code, etc., run:`);
|
|
522
|
+
console.log(` \x1b[36mnpx multimodel-dev-os init --adapter cursor --adapter claude\x1b[0m`);
|
|
523
|
+
}
|
|
524
|
+
console.log(` 4. \x1b[1mRun Diagnostics:\x1b[0m`);
|
|
525
|
+
console.log(` Verify your workspace structural compliance:`);
|
|
526
|
+
console.log(` \x1b[36mnpx multimodel-dev-os validate\x1b[0m`);
|
|
527
|
+
console.log(` \x1b[36mnpx multimodel-dev-os doctor\x1b[0m\n`);
|
|
405
528
|
}
|
|
406
529
|
|
|
407
530
|
function handleVerify(options) {
|
|
@@ -458,6 +581,14 @@ function handleVerify(options) {
|
|
|
458
581
|
}
|
|
459
582
|
|
|
460
583
|
function handleDoctor(options) {
|
|
584
|
+
if (options.tokens) {
|
|
585
|
+
handleDoctorTokens(options);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (options.release) {
|
|
589
|
+
handleDoctorRelease(options);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
461
592
|
console.log(`\n🩺 \x1b[36mRunning advisory doctor checkup in: ${options.target}\x1b[0m\n`);
|
|
462
593
|
|
|
463
594
|
let warnings = 0;
|
|
@@ -562,6 +693,10 @@ function handleDoctor(options) {
|
|
|
562
693
|
}
|
|
563
694
|
|
|
564
695
|
function handleValidate(options) {
|
|
696
|
+
if (options && options.allRegistries) {
|
|
697
|
+
handleValidateAllRegistries();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
565
700
|
console.log(`\n🛡 \x1b[34mRunning strict schema validation in: ${options.target}\x1b[0m\n`);
|
|
566
701
|
|
|
567
702
|
let errors = 0;
|
|
@@ -642,6 +777,26 @@ function handleValidate(options) {
|
|
|
642
777
|
assertAdapter('antigravity', '.gemini/settings.json');
|
|
643
778
|
}
|
|
644
779
|
|
|
780
|
+
// Template-specific validation
|
|
781
|
+
if (options.template) {
|
|
782
|
+
const tInfo = TEMPLATES[options.template];
|
|
783
|
+
if (tInfo && Array.isArray(tInfo.required_files)) {
|
|
784
|
+
console.log(`\n📋 Validating required files for template '${options.template}':`);
|
|
785
|
+
tInfo.required_files.forEach(f => assertPath(f, 'file'));
|
|
786
|
+
} else if (options.template === 'expo-react-native-android') {
|
|
787
|
+
const mobileFiles = [
|
|
788
|
+
'app.json',
|
|
789
|
+
'eas.json',
|
|
790
|
+
'app.config.ts',
|
|
791
|
+
'jest.config.js',
|
|
792
|
+
'src/app/_layout.tsx',
|
|
793
|
+
'src/lib/secure-storage.ts',
|
|
794
|
+
'src/services/api-client.ts'
|
|
795
|
+
];
|
|
796
|
+
mobileFiles.forEach(f => assertPath(f, 'file'));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
645
800
|
console.log('\n==================================================');
|
|
646
801
|
if (errors > 0) {
|
|
647
802
|
console.error(` \x1b[31mValidation FAILED. Found ${errors} strict structural compliance errors.\x1b[0m\n`);
|
|
@@ -651,3 +806,591 @@ function handleValidate(options) {
|
|
|
651
806
|
process.exit(0);
|
|
652
807
|
}
|
|
653
808
|
}
|
|
809
|
+
|
|
810
|
+
// --- YAML Parser Helper ---
|
|
811
|
+
function parseYaml(content) {
|
|
812
|
+
try {
|
|
813
|
+
const root = {};
|
|
814
|
+
const stack = [{ obj: root, indent: -1, key: null, isArray: false }];
|
|
815
|
+
|
|
816
|
+
const lines = content.split(/\r?\n/);
|
|
817
|
+
for (let line of lines) {
|
|
818
|
+
const commentIdx = line.indexOf('#');
|
|
819
|
+
if (commentIdx !== -1) {
|
|
820
|
+
line = line.substring(0, commentIdx);
|
|
821
|
+
}
|
|
822
|
+
line = line.trimEnd();
|
|
823
|
+
if (!line.trim()) continue;
|
|
824
|
+
|
|
825
|
+
const indent = line.match(/^ */)[0].length;
|
|
826
|
+
let trimmed = line.trim();
|
|
827
|
+
|
|
828
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
829
|
+
stack.pop();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const parent = stack[stack.length - 1];
|
|
833
|
+
|
|
834
|
+
if (trimmed.startsWith('-')) {
|
|
835
|
+
trimmed = trimmed.substring(1).trim();
|
|
836
|
+
if (!Array.isArray(parent.obj)) {
|
|
837
|
+
const grandparent = stack[stack.length - 2];
|
|
838
|
+
if (grandparent) {
|
|
839
|
+
grandparent.obj[parent.key] = [];
|
|
840
|
+
parent.obj = grandparent.obj[parent.key];
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const colonIdx = trimmed.indexOf(':');
|
|
845
|
+
if (colonIdx === -1) {
|
|
846
|
+
parent.obj.push(trimmed);
|
|
847
|
+
} else {
|
|
848
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
849
|
+
let val = trimmed.substring(colonIdx + 1).trim();
|
|
850
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
851
|
+
val = val.substring(1, val.length - 1);
|
|
852
|
+
}
|
|
853
|
+
if (val === 'true') val = true;
|
|
854
|
+
else if (val === 'false') val = false;
|
|
855
|
+
else if (val === 'null') val = null;
|
|
856
|
+
else if (/^\d+$/.test(val)) val = parseInt(val, 10);
|
|
857
|
+
|
|
858
|
+
const newObj = { [key]: val };
|
|
859
|
+
parent.obj.push(newObj);
|
|
860
|
+
stack.push({ obj: newObj, indent: indent, key: key, isArray: false });
|
|
861
|
+
}
|
|
862
|
+
} else {
|
|
863
|
+
const colonIdx = trimmed.indexOf(':');
|
|
864
|
+
if (colonIdx === -1) continue;
|
|
865
|
+
|
|
866
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
867
|
+
let val = trimmed.substring(colonIdx + 1).trim();
|
|
868
|
+
|
|
869
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
870
|
+
val = val.substring(1, val.length - 1);
|
|
871
|
+
}
|
|
872
|
+
if (val === 'true') val = true;
|
|
873
|
+
else if (val === 'false') val = false;
|
|
874
|
+
else if (val === 'null') val = null;
|
|
875
|
+
else if (/^\d+$/.test(val)) val = parseInt(val, 10);
|
|
876
|
+
|
|
877
|
+
if (val === '') {
|
|
878
|
+
parent.obj[key] = {};
|
|
879
|
+
stack.push({ obj: parent.obj[key], indent: indent, key: key, isArray: false });
|
|
880
|
+
} else {
|
|
881
|
+
parent.obj[key] = val;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return root;
|
|
886
|
+
} catch (e) {
|
|
887
|
+
console.warn(`\x1b[33m[WARNING] Failed to parse YAML: ${e.message}\x1b[0m`);
|
|
888
|
+
return {};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// --- Command Handler Functions ---
|
|
893
|
+
function handleListModels(options) {
|
|
894
|
+
const registryPath = join(sourceRoot, '.ai', 'models', 'registry.yaml');
|
|
895
|
+
if (!existsSync(registryPath)) {
|
|
896
|
+
console.error('Error: Model registry not found.');
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
const registry = parseYaml(readFileSync(registryPath, 'utf8'));
|
|
900
|
+
const models = registry.models || {};
|
|
901
|
+
if (options && options.json) {
|
|
902
|
+
console.log(JSON.stringify(models, null, 2));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
console.log(`\n🤖 \x1b[36mModel Registry [v${version}]\x1b[0m`);
|
|
906
|
+
console.log('==================================================');
|
|
907
|
+
Object.keys(models).forEach(name => {
|
|
908
|
+
const m = models[name];
|
|
909
|
+
console.log(`\n\x1b[32m* ${name}\x1b[0m (${m.alias || ''})`);
|
|
910
|
+
console.log(` \x1b[33mProvider:\x1b[0m ${m.provider}`);
|
|
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[33mTiers:\x1b[0m Cost: ${m.tiers?.cost}, Reasoning: ${m.tiers?.reasoning}, Coding: ${m.tiers?.coding}`);
|
|
914
|
+
});
|
|
915
|
+
console.log('\nUse \x1b[36mshow-model <model-alias>\x1b[0m to view detailed model capabilities.\n');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function handleShowModel(name) {
|
|
919
|
+
const registryPath = join(sourceRoot, '.ai', 'models', 'registry.yaml');
|
|
920
|
+
if (!existsSync(registryPath)) {
|
|
921
|
+
console.error('Error: Model registry not found.');
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
const registry = parseYaml(readFileSync(registryPath, 'utf8'));
|
|
925
|
+
const models = registry.models || {};
|
|
926
|
+
const m = models[name];
|
|
927
|
+
if (!m) {
|
|
928
|
+
console.error(`\x1b[31mError: Model alias '${name}' not found in registry.\x1b[0m`);
|
|
929
|
+
process.exit(1);
|
|
930
|
+
}
|
|
931
|
+
console.log(`\n🔍 \x1b[36mModel: ${name}\x1b[0m`);
|
|
932
|
+
console.log('==================================================');
|
|
933
|
+
console.log(`\x1b[33mProvider:\x1b[0m ${m.provider}`);
|
|
934
|
+
console.log(`\x1b[33mAlias:\x1b[0m ${m.alias}`);
|
|
935
|
+
console.log(`\x1b[33mOfficial ID:\x1b[0m ${m.official_id}`);
|
|
936
|
+
console.log(`\x1b[33mContext Window:\x1b[0m ${m.context_window} tokens`);
|
|
937
|
+
console.log(`\x1b[33mCapabilities:\x1b[0m`);
|
|
938
|
+
console.log(` ├─ Vision: ${m.capabilities?.vision ? 'Yes' : 'No'}`);
|
|
939
|
+
console.log(` └─ Tool Use: ${m.capabilities?.tool_use ? 'Yes' : 'No'}`);
|
|
940
|
+
console.log(`\x1b[33mTiers:\x1b[0m`);
|
|
941
|
+
console.log(` ├─ Cost: ${m.tiers?.cost}`);
|
|
942
|
+
console.log(` ├─ Speed: ${m.tiers?.speed}`);
|
|
943
|
+
console.log(` ├─ Reasoning: ${m.tiers?.reasoning}`);
|
|
944
|
+
console.log(` └─ Coding: ${m.tiers?.coding}`);
|
|
945
|
+
console.log();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function handleListProviders() {
|
|
949
|
+
const providersPath = join(sourceRoot, '.ai', 'models', 'providers.yaml');
|
|
950
|
+
if (!existsSync(providersPath)) {
|
|
951
|
+
console.error('Error: Providers registry not found.');
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
const reg = parseYaml(readFileSync(providersPath, 'utf8'));
|
|
955
|
+
const providers = reg.providers || {};
|
|
956
|
+
console.log(`\n🔌 \x1b[36mAI Providers [v${version}]\x1b[0m`);
|
|
957
|
+
console.log('==================================================');
|
|
958
|
+
Object.keys(providers).forEach(name => {
|
|
959
|
+
const p = providers[name];
|
|
960
|
+
console.log(`\n\x1b[32m* ${p.name || name}\x1b[0m (${name})`);
|
|
961
|
+
console.log(` \x1b[33mEndpoint:\x1b[0m ${p.api_endpoint || 'Local'}`);
|
|
962
|
+
console.log(` \x1b[33mEnv Key:\x1b[0m ${p.env_key || 'None'}`);
|
|
963
|
+
});
|
|
964
|
+
console.log();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function handleRouteModel(task) {
|
|
968
|
+
const presetsPath = join(sourceRoot, '.ai', 'models', 'routing-presets.yaml');
|
|
969
|
+
if (!existsSync(presetsPath)) {
|
|
970
|
+
console.error('Error: Routing presets not found.');
|
|
971
|
+
process.exit(1);
|
|
972
|
+
}
|
|
973
|
+
const reg = parseYaml(readFileSync(presetsPath, 'utf8'));
|
|
974
|
+
const presets = reg.presets || {};
|
|
975
|
+
const preset = presets[task];
|
|
976
|
+
if (!preset) {
|
|
977
|
+
console.error(`\x1b[31mError: Routing preset for task '${task}' not found. Available: ${Object.keys(presets).join(', ')}\x1b[0m`);
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
console.log(`\n🎯 \x1b[36mRouting Suggestion for: ${task}\x1b[0m`);
|
|
981
|
+
console.log('==================================================');
|
|
982
|
+
console.log(`\x1b[33mPrimary Model:\x1b[0m \x1b[32m${preset.primary}\x1b[0m`);
|
|
983
|
+
console.log(`\x1b[33mFallback Model:\x1b[0m \x1b[33m${preset.fallback}\x1b[0m`);
|
|
984
|
+
console.log();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function handleListAdapters(options) {
|
|
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
|
+
if (options && options.json) {
|
|
996
|
+
console.log(JSON.stringify(adapters, null, 2));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
console.log(`\n🔌 \x1b[36mIDE & Agent Adapters [v${version}]\x1b[0m`);
|
|
1000
|
+
console.log('==================================================');
|
|
1001
|
+
Object.keys(adapters).forEach(name => {
|
|
1002
|
+
const a = adapters[name];
|
|
1003
|
+
console.log(`\n\x1b[32m* ${a.name || name}\x1b[0m (${name})`);
|
|
1004
|
+
console.log(` \x1b[33mRules File:\x1b[0m ${a.rules_file}`);
|
|
1005
|
+
console.log(` \x1b[33mAdapter Type:\x1b[0m ${a.type}`);
|
|
1006
|
+
console.log(` \x1b[33mRule Format:\x1b[0m ${a.format}`);
|
|
1007
|
+
});
|
|
1008
|
+
console.log('\nUse \x1b[36mshow-adapter <adapter-name>\x1b[0m to view detailed adapter metadata.\n');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function handleShowAdapter(name) {
|
|
1012
|
+
const adaptersPath = join(sourceRoot, '.ai', 'adapters', 'registry.yaml');
|
|
1013
|
+
if (!existsSync(adaptersPath)) {
|
|
1014
|
+
console.error('Error: Adapters registry not found.');
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
const reg = parseYaml(readFileSync(adaptersPath, 'utf8'));
|
|
1018
|
+
const adapters = reg.adapters || {};
|
|
1019
|
+
const a = adapters[name];
|
|
1020
|
+
if (!a) {
|
|
1021
|
+
console.error(`\x1b[31mError: Adapter '${name}' not found in registry.\x1b[0m`);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
console.log(`\n🔍 \x1b[36mAdapter: ${a.name || name}\x1b[0m`);
|
|
1025
|
+
console.log('==================================================');
|
|
1026
|
+
console.log(`\x1b[33mRules File:\x1b[0m ${a.rules_file}`);
|
|
1027
|
+
console.log(`\x1b[33mType:\x1b[0m ${a.type}`);
|
|
1028
|
+
console.log(`\x1b[33mFormat:\x1b[0m ${a.format}`);
|
|
1029
|
+
console.log();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function handleListSkills(options) {
|
|
1033
|
+
const skillsDir = join(options.target, '.ai', 'skills');
|
|
1034
|
+
if (!existsSync(skillsDir)) {
|
|
1035
|
+
console.log('\n\x1b[33m[Notice] .ai/skills directory is not initialized in the target workspace.\x1b[0m\n');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const files = readdirSync(skillsDir).filter(f => f.endsWith('.md'));
|
|
1039
|
+
console.log(`\n🧠 \x1b[36mAvailable Skills in Target [v${version}]\x1b[0m`);
|
|
1040
|
+
console.log('==================================================');
|
|
1041
|
+
files.forEach(f => {
|
|
1042
|
+
console.log(` \x1b[32m- ${f.replace('.md', '')}\x1b[0m (file: .ai/skills/${f})`);
|
|
1043
|
+
});
|
|
1044
|
+
console.log('\nUse \x1b[36mshow-skill <skill-name>\x1b[0m to read a skill\'s prompt text.\n');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function handleShowSkill(name, options) {
|
|
1048
|
+
const skillsDir = join(options.target, '.ai', 'skills');
|
|
1049
|
+
const skillFile = join(skillsDir, name.endsWith('.md') ? name : `${name}.md`);
|
|
1050
|
+
if (!existsSync(skillFile)) {
|
|
1051
|
+
console.error(`\x1b[31mError: Skill '${name}' not found in target .ai/skills/.\x1b[0m`);
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
console.log(`\n📖 \x1b[36mSkill Prompt: ${name}\x1b[0m`);
|
|
1055
|
+
console.log('==================================================');
|
|
1056
|
+
console.log(readFileSync(skillFile, 'utf8'));
|
|
1057
|
+
console.log();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function parseThresholdToBytes(val) {
|
|
1061
|
+
if (!val) return 100 * 1024; // Default 100KB
|
|
1062
|
+
const matches = val.match(/^(\d+)(KB|MB|B)?$/i);
|
|
1063
|
+
if (!matches) return 100 * 1024;
|
|
1064
|
+
const num = parseInt(matches[1], 10);
|
|
1065
|
+
const unit = (matches[2] || '').toUpperCase();
|
|
1066
|
+
if (unit === 'MB') return num * 1024 * 1024;
|
|
1067
|
+
if (unit === 'KB') return num * 1024;
|
|
1068
|
+
return num;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function handleDoctorTokens(options) {
|
|
1072
|
+
console.log(`\n🪙 \x1b[36mRunning Token Budget & Sink Audit in: ${options.target}\x1b[0m\n`);
|
|
1073
|
+
|
|
1074
|
+
const filesFound = [];
|
|
1075
|
+
const ignoredDirs = ['.git', 'node_modules', 'dist', 'build', '.next', '.expo', 'bin', 'assets', 'docs', 'web-build', 'out', 'coverage', '.nuxt', '.svelte-kit', 'bower_components', 'vendor'];
|
|
1076
|
+
|
|
1077
|
+
function scan(dir) {
|
|
1078
|
+
if (!existsSync(dir)) return;
|
|
1079
|
+
const items = readdirSync(dir);
|
|
1080
|
+
for (const item of items) {
|
|
1081
|
+
if (ignoredDirs.includes(item)) continue;
|
|
1082
|
+
const fullPath = join(dir, item);
|
|
1083
|
+
try {
|
|
1084
|
+
const stat = statSync(fullPath);
|
|
1085
|
+
if (stat.isDirectory()) {
|
|
1086
|
+
scan(fullPath);
|
|
1087
|
+
} else if (stat.isFile()) {
|
|
1088
|
+
filesFound.push({
|
|
1089
|
+
relPath: replaceBackslashes(fullPath.replace(options.target, '')),
|
|
1090
|
+
size: stat.size
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
} catch (e) {}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function replaceBackslashes(p) {
|
|
1098
|
+
let clean = p.replace(/\\/g, '/');
|
|
1099
|
+
if (clean.startsWith('/')) clean = clean.substring(1);
|
|
1100
|
+
return clean;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
scan(options.target);
|
|
1104
|
+
|
|
1105
|
+
filesFound.sort((a, b) => b.size - a.size);
|
|
1106
|
+
|
|
1107
|
+
const thresholdBytes = parseThresholdToBytes(options.threshold);
|
|
1108
|
+
const thresholdStr = options.threshold || '100KB';
|
|
1109
|
+
|
|
1110
|
+
console.log('Top 10 Largest Files in Scanned Workspace:');
|
|
1111
|
+
filesFound.slice(0, 10).forEach(f => {
|
|
1112
|
+
let sizeDesc = `${f.size} bytes`;
|
|
1113
|
+
if (f.size > 1024 * 1024) sizeDesc = `${(f.size / (1024 * 1024)).toFixed(2)} MB`;
|
|
1114
|
+
else if (f.size > 1024) sizeDesc = `${(f.size / 1024).toFixed(2)} KB`;
|
|
1115
|
+
|
|
1116
|
+
let color = '\x1b[32m';
|
|
1117
|
+
if (f.size > thresholdBytes) color = '\x1b[31m';
|
|
1118
|
+
else if (f.size > thresholdBytes * 0.3) color = '\x1b[33m';
|
|
1119
|
+
|
|
1120
|
+
console.log(` ${color}* ${f.relPath}\x1b[0m (${sizeDesc})`);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
console.log('\n==================================================');
|
|
1124
|
+
console.log(`Total Scanned Files: ${filesFound.length}`);
|
|
1125
|
+
console.log(`Recommendation: Exclude files in red (>${thresholdStr}) from active coding prompts or add them to your adapter ignore rules.`);
|
|
1126
|
+
console.log();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function handleValidateTemplate(name) {
|
|
1130
|
+
const t = TEMPLATES[name];
|
|
1131
|
+
if (!t) {
|
|
1132
|
+
console.error(`\x1b[31mError: Template '${name}' not found in registry.\x1b[0m`);
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
console.log(`\n📋 \x1b[34mValidating Template: ${name}\x1b[0m`);
|
|
1136
|
+
|
|
1137
|
+
let errors = 0;
|
|
1138
|
+
const reqKeys = ['name', 'description', 'stack', 'category', 'status', 'maturity', 'required_files'];
|
|
1139
|
+
reqKeys.forEach(k => {
|
|
1140
|
+
if (t[k] === undefined || t[k] === null) {
|
|
1141
|
+
console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
|
|
1142
|
+
errors++;
|
|
1143
|
+
} else {
|
|
1144
|
+
console.log(` \x1b[32m✓\x1b[0m Registry key: ${k}`);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
const templateDir = join(sourceRoot, 'examples', name);
|
|
1149
|
+
if (!existsSync(templateDir)) {
|
|
1150
|
+
console.error(` \x1b[31m✗ Source folder missing: examples/${name}\x1b[0m`);
|
|
1151
|
+
errors++;
|
|
1152
|
+
} else {
|
|
1153
|
+
console.log(` \x1b[32m✓\x1b[0m Source folder: examples/${name}`);
|
|
1154
|
+
if (Array.isArray(t.required_files)) {
|
|
1155
|
+
t.required_files.forEach(f => {
|
|
1156
|
+
const filePath = join(templateDir, f);
|
|
1157
|
+
const globalPath = join(sourceRoot, f);
|
|
1158
|
+
if (existsSync(filePath)) {
|
|
1159
|
+
console.log(` \x1b[32m✓\x1b[0m Required file (template override): ${f}`);
|
|
1160
|
+
} else if (existsSync(globalPath)) {
|
|
1161
|
+
console.log(` \x1b[32m✓\x1b[0m Required file (global fallback): ${f}`);
|
|
1162
|
+
} else {
|
|
1163
|
+
console.error(` \x1b[31m✗ Required file missing: ${f}\x1b[0m`);
|
|
1164
|
+
errors++;
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (errors > 0) {
|
|
1171
|
+
console.error(`\n\x1b[31mValidation FAILED with ${errors} errors.\x1b[0m\n`);
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
} else {
|
|
1174
|
+
console.log(`\n\x1b[32m✔ Template '${name}' is fully valid and compliant!\x1b[0m\n`);
|
|
1175
|
+
process.exit(0);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function handleValidateAdapter(name) {
|
|
1180
|
+
const a = ADAPTERS[name];
|
|
1181
|
+
if (!a) {
|
|
1182
|
+
console.error(`\x1b[31mError: Adapter '${name}' not found in registry.\x1b[0m`);
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1185
|
+
console.log(`\n📋 \x1b[34mValidating Adapter: ${name}\x1b[0m`);
|
|
1186
|
+
|
|
1187
|
+
let errors = 0;
|
|
1188
|
+
const reqKeys = ['name', 'rules_file', 'format', 'type'];
|
|
1189
|
+
reqKeys.forEach(k => {
|
|
1190
|
+
if (!a[k]) {
|
|
1191
|
+
console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
|
|
1192
|
+
errors++;
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log(` \x1b[32m✓\x1b[0m Registry key: ${k}`);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const adapterDir = join(sourceRoot, 'adapters', name);
|
|
1199
|
+
if (!existsSync(adapterDir)) {
|
|
1200
|
+
console.error(` \x1b[31m✗ Source folder missing: adapters/${name}\x1b[0m`);
|
|
1201
|
+
errors++;
|
|
1202
|
+
} else {
|
|
1203
|
+
console.log(` \x1b[32m✓\x1b[0m Source folder: adapters/${name}`);
|
|
1204
|
+
const setupFile = join(adapterDir, 'setup.md');
|
|
1205
|
+
if (existsSync(setupFile)) {
|
|
1206
|
+
console.log(` \x1b[32m✓\x1b[0m Required file: setup.md`);
|
|
1207
|
+
} else {
|
|
1208
|
+
console.error(` \x1b[31m✗ Required file missing: adapters/${name}/setup.md\x1b[0m`);
|
|
1209
|
+
errors++;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (a.rules_file) {
|
|
1213
|
+
const rulesFile = join(adapterDir, a.rules_file);
|
|
1214
|
+
if (existsSync(rulesFile)) {
|
|
1215
|
+
console.log(` \x1b[32m✓\x1b[0m Rules file: ${a.rules_file}`);
|
|
1216
|
+
} else {
|
|
1217
|
+
console.error(` \x1b[31m✗ Rules file missing: adapters/${name}/${a.rules_file}\x1b[0m`);
|
|
1218
|
+
errors++;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (errors > 0) {
|
|
1224
|
+
console.error(`\n\x1b[31mValidation FAILED with ${errors} errors.\x1b[0m\n`);
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
} else {
|
|
1227
|
+
console.log(`\n\x1b[32m✔ Adapter '${name}' is fully valid and compliant!\x1b[0m\n`);
|
|
1228
|
+
process.exit(0);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function handleValidateSkill(name, options) {
|
|
1233
|
+
const skillsDir = join(options.target, '.ai', 'skills');
|
|
1234
|
+
let skillFile = join(skillsDir, name.endsWith('.md') ? name : `${name}.md`);
|
|
1235
|
+
if (!existsSync(skillFile)) {
|
|
1236
|
+
skillFile = join(sourceRoot, '.ai', 'skills', name.endsWith('.md') ? name : `${name}.md`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (!existsSync(skillFile)) {
|
|
1240
|
+
console.error(`\x1b[31mError: Skill '${name}' not found.\x1b[0m`);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
console.log(`\n📋 \x1b[34mValidating Skill: ${name}\x1b[0m`);
|
|
1245
|
+
const content = readFileSync(skillFile, 'utf8');
|
|
1246
|
+
let errors = 0;
|
|
1247
|
+
|
|
1248
|
+
const reqHeaders = [
|
|
1249
|
+
{ header: '# Purpose', regex: /^#\s+Purpose/mi },
|
|
1250
|
+
{ header: '# Activation Trigger', regex: /^#\s+Activation\s+Trigger/mi },
|
|
1251
|
+
{ header: '# Input Context', regex: /^#\s+Input\s+Context/mi },
|
|
1252
|
+
{ header: '# Output Contract', regex: /^#\s+Output\s+Contract/mi },
|
|
1253
|
+
{ header: '# Token Budget', regex: /^#\s+Token\s+Budget/mi }
|
|
1254
|
+
];
|
|
1255
|
+
|
|
1256
|
+
reqHeaders.forEach(req => {
|
|
1257
|
+
if (req.regex.test(content)) {
|
|
1258
|
+
console.log(` \x1b[32m✓\x1b[0m Found required header: ${req.header}`);
|
|
1259
|
+
} else {
|
|
1260
|
+
console.error(` \x1b[31m✗ Missing required header: ${req.header}\x1b[0m`);
|
|
1261
|
+
errors++;
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
if (errors > 0) {
|
|
1266
|
+
console.error(`\n\x1b[31mValidation FAILED with ${errors} errors.\x1b[0m\n`);
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
} else {
|
|
1269
|
+
console.log(`\n\x1b[32m✔ Skill '${name}' is fully valid and compliant!\x1b[0m\n`);
|
|
1270
|
+
process.exit(0);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function handleValidateAllRegistries() {
|
|
1275
|
+
console.log(`\n🛡 \x1b[34mValidating All Registry Entries\x1b[0m\n`);
|
|
1276
|
+
let errors = 0;
|
|
1277
|
+
|
|
1278
|
+
// Validate all templates
|
|
1279
|
+
console.log('--- Templates Registry Validation ---');
|
|
1280
|
+
Object.keys(TEMPLATES).forEach(name => {
|
|
1281
|
+
const t = TEMPLATES[name];
|
|
1282
|
+
console.log(`\nValidating Template: ${name}`);
|
|
1283
|
+
const reqKeys = ['name', 'description', 'stack', 'category', 'status', 'maturity'];
|
|
1284
|
+
if (t.status !== 'planned') {
|
|
1285
|
+
reqKeys.push('required_files');
|
|
1286
|
+
}
|
|
1287
|
+
reqKeys.forEach(k => {
|
|
1288
|
+
if (t[k] === undefined || t[k] === null) {
|
|
1289
|
+
console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
|
|
1290
|
+
errors++;
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const templateDir = join(sourceRoot, 'examples', name);
|
|
1295
|
+
if (t.status === 'stable' && !existsSync(templateDir)) {
|
|
1296
|
+
console.error(` \x1b[31m✗ Stable template source folder missing: examples/${name}\x1b[0m`);
|
|
1297
|
+
errors++;
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Validate all adapters
|
|
1302
|
+
console.log('\n--- Adapters Registry Validation ---');
|
|
1303
|
+
Object.keys(ADAPTERS).forEach(name => {
|
|
1304
|
+
const a = ADAPTERS[name];
|
|
1305
|
+
console.log(`Validating Adapter: ${name}`);
|
|
1306
|
+
const reqKeys = ['name', 'rules_file', 'format', 'type'];
|
|
1307
|
+
reqKeys.forEach(k => {
|
|
1308
|
+
if (!a[k]) {
|
|
1309
|
+
console.error(` \x1b[31m✗ Missing registry key: ${k}\x1b[0m`);
|
|
1310
|
+
errors++;
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
console.log('\n==================================================');
|
|
1316
|
+
if (errors > 0) {
|
|
1317
|
+
console.error(` \x1b[31mAll Registries validation FAILED. Found ${errors} schema errors.\x1b[0m\n`);
|
|
1318
|
+
process.exit(1);
|
|
1319
|
+
} else {
|
|
1320
|
+
console.log(' \x1b[32m✔ All Registries validation PASSED. All templates and adapters are valid.\x1b[0m\n');
|
|
1321
|
+
process.exit(0);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function handleDoctorRelease(options) {
|
|
1326
|
+
console.log(`\n🩺 \x1b[36mRunning release audit doctor in: ${sourceRoot}\x1b[0m\n`);
|
|
1327
|
+
let warnings = 0;
|
|
1328
|
+
|
|
1329
|
+
// 1. Version checks
|
|
1330
|
+
let packageVersion = 'unknown';
|
|
1331
|
+
try {
|
|
1332
|
+
const pkg = JSON.parse(readFileSync(join(sourceRoot, 'package.json'), 'utf8'));
|
|
1333
|
+
packageVersion = pkg.version;
|
|
1334
|
+
console.log(` \x1b[32m✓\x1b[0m package.json version: ${packageVersion}`);
|
|
1335
|
+
} catch (e) {
|
|
1336
|
+
console.warn(' \x1b[31m✗\x1b[0m Failed to parse package.json');
|
|
1337
|
+
warnings++;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const checkInstallScript = (filename, regex) => {
|
|
1341
|
+
const filePath = join(sourceRoot, filename);
|
|
1342
|
+
if (existsSync(filePath)) {
|
|
1343
|
+
const content = readFileSync(filePath, 'utf8');
|
|
1344
|
+
const match = content.match(regex);
|
|
1345
|
+
if (match && match[1] === packageVersion) {
|
|
1346
|
+
console.log(` \x1b[32m✓\x1b[0m ${filename} version aligns: ${match[1]}`);
|
|
1347
|
+
} else {
|
|
1348
|
+
console.warn(` \x1b[33m[WARNING]\x1b[0m ${filename} version mismatch (found ${match ? match[1] : 'none'}, expected ${packageVersion})`);
|
|
1349
|
+
warnings++;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
checkInstallScript('scripts/install.sh', /VERSION="([^"]+)"/i);
|
|
1355
|
+
checkInstallScript('scripts/install.ps1', /\$VERSION\s*=\s*"([^"]+)"/i);
|
|
1356
|
+
|
|
1357
|
+
// 2. Blacklisted files audit
|
|
1358
|
+
const blacklist = ['.npmrc'];
|
|
1359
|
+
blacklist.forEach(file => {
|
|
1360
|
+
const fullPath = join(sourceRoot, file);
|
|
1361
|
+
if (existsSync(fullPath)) {
|
|
1362
|
+
console.warn(` \x1b[33m[WARNING]\x1b[0m Blacklisted file found in release root: ${file}`);
|
|
1363
|
+
warnings++;
|
|
1364
|
+
} else {
|
|
1365
|
+
console.log(` \x1b[32m✓\x1b[0m No root blacklisted file: ${file}`);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
// Recursively scan examples/ for .env and keystores
|
|
1370
|
+
const scanSafety = (dir) => {
|
|
1371
|
+
if (!existsSync(dir)) return;
|
|
1372
|
+
const items = readdirSync(dir);
|
|
1373
|
+
for (const item of items) {
|
|
1374
|
+
const fullPath = join(dir, item);
|
|
1375
|
+
try {
|
|
1376
|
+
const stat = statSync(fullPath);
|
|
1377
|
+
if (stat.isDirectory()) {
|
|
1378
|
+
scanSafety(fullPath);
|
|
1379
|
+
} else if (stat.isFile()) {
|
|
1380
|
+
if (item === '.env' || item.endsWith('.keystore') || item.endsWith('.jks')) {
|
|
1381
|
+
console.warn(` \x1b[33m[WARNING]\x1b[0m Unsafe file inside templates/examples: ${fullPath.replace(sourceRoot, '')}`);
|
|
1382
|
+
warnings++;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
} catch (e) {}
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
scanSafety(join(sourceRoot, 'examples'));
|
|
1389
|
+
|
|
1390
|
+
console.log('\n==================================================');
|
|
1391
|
+
if (warnings > 0) {
|
|
1392
|
+
console.warn(` \x1b[33mRelease doctor complete with ${warnings} warnings.\x1b[0m\n`);
|
|
1393
|
+
} else {
|
|
1394
|
+
console.log(' \x1b[32m✔ Release hygiene checks PASSED successfully!\x1b[0m\n');
|
|
1395
|
+
}
|
|
1396
|
+
}
|