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.
- 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 +810 -91
- 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 +15 -18
- package/docs/final-launch.md +5 -4
- 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 +27 -0
- 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/quickstart.md +7 -6
- package/docs/registry-contribution.md +20 -0
- package/docs/release-policy.md +26 -0
- package/docs/skill-authoring.md +56 -0
- package/docs/template-authoring.md +65 -0
- 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.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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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');
|
|
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 .
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|