qa360 2.3.1 → 2.3.2
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/dist/commands/ask.js +49 -22
- package/dist/commands/coverage.js +16 -3
- package/dist/commands/generate.js +11 -4
- package/dist/commands/ollama.js +13 -5
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +86 -0
- package/dist/core/ai/ollama-provider.d.ts +4 -0
- package/dist/core/ai/ollama-provider.js +27 -6
- package/dist/index.js +2 -3
- package/dist/utils/config.js +36 -3
- package/package.json +1 -1
package/dist/commands/ask.js
CHANGED
|
@@ -184,33 +184,42 @@ export class QA360Ask {
|
|
|
184
184
|
console.log(chalk.gray(`Analyse: "${query}"`));
|
|
185
185
|
// Detection de patterns simples (Phase 1 - deterministe)
|
|
186
186
|
const patterns = {
|
|
187
|
-
|
|
188
|
-
web: /\b(web|ui|interface|page|site|browser)\b/i,
|
|
189
|
-
performance: /\b(performance|perf|load|stress|speed|latency)\b/i,
|
|
187
|
+
complete: /\b(complete|full|comprehensive|end-to-end|e2e|suite|tout)\b/i,
|
|
190
188
|
security: /\b(security|sécurité|secu|dast|vuln|scan|pentest|zap)\b/i,
|
|
189
|
+
performance: /\b(performance|perf|load|stress|speed|latency|charge|charge)\b/i,
|
|
191
190
|
accessibility: /\b(accessibility|a11y|accessible|wcag)\b/i,
|
|
192
|
-
|
|
191
|
+
web: /\b(web|ui|interface|page|site|browser|login|auth|signin|signup|registration|connexion|inscription)\b/i,
|
|
192
|
+
api: /\b(api|rest|graphql|endpoint|swagger|openapi)\b/i
|
|
193
193
|
};
|
|
194
194
|
// URL extraction
|
|
195
195
|
const urlMatch = query.match(/https?:\/\/[^\s]+/);
|
|
196
196
|
const target = urlMatch ? urlMatch[0] : 'https://httpbin.org';
|
|
197
|
-
// Type detection with priority
|
|
198
|
-
let type = '
|
|
199
|
-
// Check
|
|
197
|
+
// Type detection with priority order
|
|
198
|
+
let type = 'web'; // default to web (UI) for generic queries like "login tests"
|
|
199
|
+
// Check patterns in priority order
|
|
200
200
|
if (patterns.complete.test(query)) {
|
|
201
201
|
type = 'complete';
|
|
202
202
|
}
|
|
203
203
|
else if (patterns.security.test(query)) {
|
|
204
204
|
type = 'security';
|
|
205
205
|
}
|
|
206
|
-
else {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
else if (patterns.performance.test(query)) {
|
|
207
|
+
type = 'performance';
|
|
208
|
+
}
|
|
209
|
+
else if (patterns.accessibility.test(query)) {
|
|
210
|
+
type = 'accessibility';
|
|
211
|
+
}
|
|
212
|
+
else if (patterns.web.test(query)) {
|
|
213
|
+
type = 'web';
|
|
214
|
+
}
|
|
215
|
+
else if (patterns.api.test(query)) {
|
|
216
|
+
type = 'api';
|
|
217
|
+
}
|
|
218
|
+
// If no pattern matched, default to 'web' for login-like scenarios, otherwise 'api'
|
|
219
|
+
// Special handling for login/auth scenarios - default to web (UI) tests
|
|
220
|
+
const loginKeywords = /\b(login|auth|authentication|signin|signup|register|connexion|inscription|form)\b/i;
|
|
221
|
+
if (loginKeywords.test(query) && type === 'api') {
|
|
222
|
+
type = 'web';
|
|
214
223
|
}
|
|
215
224
|
// Pack name from query
|
|
216
225
|
const name = query.toLowerCase()
|
|
@@ -316,6 +325,29 @@ export class QA360Ask {
|
|
|
316
325
|
}
|
|
317
326
|
generateWebPack(name, description, baseUrl, authConfig) {
|
|
318
327
|
const auth = buildAuthConfig(authConfig);
|
|
328
|
+
// Check if this is a login/auth scenario based on name or description
|
|
329
|
+
const isLoginScenario = /\b(login|auth|signin|signup|connexion|inscription)\b/i.test(name + ' ' + description);
|
|
330
|
+
// Build pages based on scenario type
|
|
331
|
+
const pages = isLoginScenario ? [
|
|
332
|
+
{
|
|
333
|
+
url: '/login',
|
|
334
|
+
expectedElements: ['input[type="email"]', 'input[type="password"]', 'button[type="submit"]'],
|
|
335
|
+
actions: [
|
|
336
|
+
{ type: 'fill', selector: 'input[type="email"]', value: '${TEST_EMAIL}' },
|
|
337
|
+
{ type: 'fill', selector: 'input[type="password"]', value: '${TEST_PASSWORD}' },
|
|
338
|
+
{ type: 'click', selector: 'button[type="submit"]' }
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
url: '/',
|
|
343
|
+
expectedElements: ['header', 'nav']
|
|
344
|
+
}
|
|
345
|
+
] : [
|
|
346
|
+
{
|
|
347
|
+
url: '/',
|
|
348
|
+
expectedElements: ['body', 'main']
|
|
349
|
+
}
|
|
350
|
+
];
|
|
319
351
|
return {
|
|
320
352
|
version: 2,
|
|
321
353
|
name,
|
|
@@ -328,12 +360,7 @@ export class QA360Ask {
|
|
|
328
360
|
auth: auth.profileName,
|
|
329
361
|
config: {
|
|
330
362
|
baseUrl,
|
|
331
|
-
pages
|
|
332
|
-
{
|
|
333
|
-
url: '/',
|
|
334
|
-
expectedElements: ['body', 'main']
|
|
335
|
-
}
|
|
336
|
-
]
|
|
363
|
+
pages
|
|
337
364
|
}
|
|
338
365
|
},
|
|
339
366
|
'a11y': {
|
|
@@ -343,7 +370,7 @@ export class QA360Ask {
|
|
|
343
370
|
baseUrl,
|
|
344
371
|
pages: [
|
|
345
372
|
{
|
|
346
|
-
url: '/',
|
|
373
|
+
url: isLoginScenario ? '/login' : '/',
|
|
347
374
|
a11yRules: ['wcag2a', 'wcag2aa']
|
|
348
375
|
}
|
|
349
376
|
]
|
|
@@ -18,7 +18,14 @@ coverageCommand
|
|
|
18
18
|
.option('--verbose', 'Show detailed file-by-file coverage')
|
|
19
19
|
.action(async (options) => {
|
|
20
20
|
if (!options.file && !options.run) {
|
|
21
|
-
console.error('Error: Please specify --file or --run');
|
|
21
|
+
console.error('Error: Please specify --file or --run to show coverage\n');
|
|
22
|
+
console.error('Usage: qa360 coverage show --file <path>');
|
|
23
|
+
console.error(' or: qa360 coverage show --run <id>\n');
|
|
24
|
+
console.error('Examples:');
|
|
25
|
+
console.error(' qa360 coverage show --file coverage/coverage-final.json');
|
|
26
|
+
console.error(' qa360 coverage show --file coverage/lcov.info');
|
|
27
|
+
console.error(' qa360 coverage show --run abc123\n');
|
|
28
|
+
console.error('Tip: Use "qa360 coverage list" to find available coverage files in your project.');
|
|
22
29
|
process.exit(1);
|
|
23
30
|
}
|
|
24
31
|
const collector = new CoverageCollector();
|
|
@@ -82,7 +89,10 @@ coverageCommand
|
|
|
82
89
|
.option('--json', 'Output as JSON')
|
|
83
90
|
.action(async (options) => {
|
|
84
91
|
if (!options.file) {
|
|
85
|
-
console.error('Error: Please specify --file');
|
|
92
|
+
console.error('Error: Please specify --file to analyze coverage\n');
|
|
93
|
+
console.error('Usage: qa360 coverage analyze --file <path>\n');
|
|
94
|
+
console.error('Example: qa360 coverage analyze --file coverage/coverage-final.json\n');
|
|
95
|
+
console.error('Tip: Use "qa360 coverage list" to find available coverage files in your project.');
|
|
86
96
|
process.exit(1);
|
|
87
97
|
}
|
|
88
98
|
if (!existsSync(options.file)) {
|
|
@@ -218,7 +228,10 @@ coverageCommand
|
|
|
218
228
|
.option('--fail', 'Exit with error code if thresholds not met')
|
|
219
229
|
.action(async (options) => {
|
|
220
230
|
if (!options.file) {
|
|
221
|
-
console.error('Error: Please specify --file');
|
|
231
|
+
console.error('Error: Please specify --file to check coverage\n');
|
|
232
|
+
console.error('Usage: qa360 coverage check --file <path>\n');
|
|
233
|
+
console.error('Example: qa360 coverage check --file coverage/coverage-final.json\n');
|
|
234
|
+
console.error('Tip: Use "qa360 coverage list" to find available coverage files in your project.');
|
|
222
235
|
process.exit(1);
|
|
223
236
|
}
|
|
224
237
|
if (!existsSync(options.file)) {
|
|
@@ -91,10 +91,17 @@ export async function generateCommand(source, options = {}) {
|
|
|
91
91
|
const { available, models, recommended } = await checkGenerationAvailability();
|
|
92
92
|
if (!available) {
|
|
93
93
|
spinner.fail('Ollama not available');
|
|
94
|
-
console.log(chalk.red('\
|
|
95
|
-
console.log(chalk.yellow('\
|
|
96
|
-
console.log(chalk.gray('
|
|
97
|
-
console.log(chalk.
|
|
94
|
+
console.log(chalk.red('\n❌ Ollama is not running or not reachable'));
|
|
95
|
+
console.log(chalk.yellow('\n🔧 Troubleshooting:'));
|
|
96
|
+
console.log(chalk.gray(' 1. Check if Ollama is running:'));
|
|
97
|
+
console.log(chalk.cyan(' ollama list'));
|
|
98
|
+
console.log(chalk.gray(' 2. Start Ollama if needed:'));
|
|
99
|
+
console.log(chalk.cyan(' ollama serve'));
|
|
100
|
+
console.log(chalk.gray(' 3. Test QA360 connection to Ollama:'));
|
|
101
|
+
console.log(chalk.cyan(' qa360 ollama test'));
|
|
102
|
+
console.log(chalk.gray(' 4. Set custom Ollama URL if needed:'));
|
|
103
|
+
console.log(chalk.cyan(' qa360 ollama config --set-url http://localhost:11434'));
|
|
104
|
+
console.log(chalk.yellow('\n📦 Install Ollama:'));
|
|
98
105
|
console.log(chalk.gray(' brew install ollama # macOS'));
|
|
99
106
|
console.log(chalk.gray(' # Or visit: https://ollama.com\n'));
|
|
100
107
|
return;
|
package/dist/commands/ollama.js
CHANGED
|
@@ -19,12 +19,20 @@ export async function ollamaTestCommand(options = {}) {
|
|
|
19
19
|
const check = await checkOllamaSetup(model);
|
|
20
20
|
if (!check.available) {
|
|
21
21
|
spinner.fail('Ollama is not available');
|
|
22
|
-
console.log(chalk.red('\n❌ Ollama is not running or not
|
|
22
|
+
console.log(chalk.red('\n❌ Ollama is not running or not reachable'));
|
|
23
|
+
console.log(chalk.yellow('\n🔧 Troubleshooting steps:'));
|
|
24
|
+
console.log(chalk.gray('\n 1. Verify Ollama is running:'));
|
|
25
|
+
console.log(chalk.cyan(' ollama list'));
|
|
26
|
+
console.log(chalk.gray('\n 2. Start Ollama if not running:'));
|
|
27
|
+
console.log(chalk.cyan(' ollama serve'));
|
|
28
|
+
console.log(chalk.gray('\n 3. Check Ollama is listening on default port:'));
|
|
29
|
+
console.log(chalk.cyan(' curl http://localhost:11434/api/tags'));
|
|
30
|
+
console.log(chalk.gray('\n 4. Set custom URL if Ollama is on different port/host:'));
|
|
31
|
+
console.log(chalk.cyan(' qa360 ollama config --set-url http://localhost:11434'));
|
|
32
|
+
console.log(chalk.gray('\n 5. Check for firewall/proxy issues blocking localhost:11434'));
|
|
23
33
|
console.log(chalk.yellow('\n📦 To install Ollama:'));
|
|
24
34
|
console.log(chalk.gray(' brew install ollama # macOS'));
|
|
25
|
-
console.log(chalk.gray(' # Or visit: https://ollama.com'));
|
|
26
|
-
console.log(chalk.yellow('\n🚀 To start Ollama:'));
|
|
27
|
-
console.log(chalk.gray(' ollama serve'));
|
|
35
|
+
console.log(chalk.gray(' # Or visit: https://ollama.com\n'));
|
|
28
36
|
return;
|
|
29
37
|
}
|
|
30
38
|
spinner.succeed('Ollama is running!');
|
|
@@ -175,7 +183,7 @@ export async function ollamaGenerateCommand(prompt, options = {}) {
|
|
|
175
183
|
export async function ollamaConfigCommand(options = {}) {
|
|
176
184
|
const config = loadQA360Config();
|
|
177
185
|
const ollamaConfig = config.ollama || {};
|
|
178
|
-
const defaultBaseUrl = 'http://
|
|
186
|
+
const defaultBaseUrl = 'http://127.0.0.1:11434';
|
|
179
187
|
const defaultModel = 'deepseek-coder';
|
|
180
188
|
// Update config if options provided
|
|
181
189
|
if (options.setUrl || options.setModel) {
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -39,3 +39,7 @@ export declare function displayResults(result: Phase3RunResult): void;
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function runCommand(packArg?: string, options?: RunOptions): Promise<void>;
|
|
41
41
|
export default runCommand;
|
|
42
|
+
/**
|
|
43
|
+
* Preflight command - Validate pack without executing tests
|
|
44
|
+
*/
|
|
45
|
+
export declare function preflightCommand(packArg?: string): Promise<void>;
|
package/dist/commands/run.js
CHANGED
|
@@ -170,3 +170,89 @@ export async function runCommand(packArg, options = {}) {
|
|
|
170
170
|
}
|
|
171
171
|
// Export for CLI integration
|
|
172
172
|
export default runCommand;
|
|
173
|
+
/**
|
|
174
|
+
* Preflight command - Validate pack without executing tests
|
|
175
|
+
*/
|
|
176
|
+
export async function preflightCommand(packArg) {
|
|
177
|
+
try {
|
|
178
|
+
console.log(chalk.blue('\n🔍 QA360 Preflight - Dry-run validation\n'));
|
|
179
|
+
// Step 1: Find pack file
|
|
180
|
+
const packPath = findPackFile(packArg);
|
|
181
|
+
console.log(chalk.gray(`Pack file: ${packPath}`));
|
|
182
|
+
// Step 2: Load and validate pack
|
|
183
|
+
const pack = await loadPack(packPath);
|
|
184
|
+
console.log(chalk.green(`✅ Pack: ${pack.name} v${pack.version}`));
|
|
185
|
+
// Step 3: Validate structure
|
|
186
|
+
const issues = [];
|
|
187
|
+
// Check required fields
|
|
188
|
+
if (!pack.name) {
|
|
189
|
+
issues.push('Missing required field: name');
|
|
190
|
+
}
|
|
191
|
+
if (!pack.version) {
|
|
192
|
+
issues.push('Missing required field: version');
|
|
193
|
+
}
|
|
194
|
+
if (!pack.gates || Object.keys(pack.gates).length === 0) {
|
|
195
|
+
issues.push('No gates defined - nothing to test');
|
|
196
|
+
}
|
|
197
|
+
// Validate gates
|
|
198
|
+
const gateNames = Object.keys(pack.gates || {});
|
|
199
|
+
if (gateNames.length > 0) {
|
|
200
|
+
console.log(chalk.gray(`\nGates (${gateNames.length}):`));
|
|
201
|
+
for (const [gateName, gateConfig] of Object.entries(pack.gates || {})) {
|
|
202
|
+
const gate = gateConfig;
|
|
203
|
+
const status = gate.enabled === false ? chalk.yellow('(disabled)') : chalk.green('(enabled)');
|
|
204
|
+
console.log(` ${status} ${chalk.cyan(gateName)}`);
|
|
205
|
+
if (!gate.adapter) {
|
|
206
|
+
issues.push(` ⚠️ Gate "${gateName}" missing adapter`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log(` Adapter: ${gate.adapter}`);
|
|
210
|
+
}
|
|
211
|
+
// Show pages/config summary
|
|
212
|
+
if (gate.config?.pages) {
|
|
213
|
+
const pages = gate.config.pages;
|
|
214
|
+
const pageCount = Array.isArray(pages) ? pages.length : Object.keys(pages).length;
|
|
215
|
+
console.log(` Pages: ${pageCount}`);
|
|
216
|
+
}
|
|
217
|
+
if (gate.config?.smoke) {
|
|
218
|
+
const smoke = gate.config.smoke;
|
|
219
|
+
const testCount = Array.isArray(smoke) ? smoke.length : 1;
|
|
220
|
+
console.log(` Smoke tests: ${testCount}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Validate auth config (only for v2 packs)
|
|
225
|
+
if (pack.version === 2 && pack.auth) {
|
|
226
|
+
const auth = pack.auth;
|
|
227
|
+
console.log(chalk.gray('\nAuthentication:'));
|
|
228
|
+
if (auth.api) {
|
|
229
|
+
console.log(` API: ${auth.api}`);
|
|
230
|
+
}
|
|
231
|
+
if (auth.ui) {
|
|
232
|
+
console.log(` UI: ${auth.ui}`);
|
|
233
|
+
}
|
|
234
|
+
if (auth.profiles) {
|
|
235
|
+
const profileNames = Object.keys(auth.profiles);
|
|
236
|
+
console.log(` Profiles: ${profileNames.join(', ') || 'none'}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Summary
|
|
240
|
+
console.log(chalk.bold('\n' + '─'.repeat(50)));
|
|
241
|
+
if (issues.length === 0) {
|
|
242
|
+
console.log(chalk.green('✅ Preflight check passed'));
|
|
243
|
+
console.log(chalk.gray(' Run "qa360 run" to execute tests'));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
console.log(chalk.yellow('⚠️ Issues found:'));
|
|
247
|
+
for (const issue of issues) {
|
|
248
|
+
console.log(chalk.yellow(` ${issue}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
console.log('');
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error(chalk.red('\n❌ Preflight validation failed:'));
|
|
255
|
+
console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -26,6 +26,10 @@ export declare class OllamaProvider implements LLMProvider {
|
|
|
26
26
|
private readonly timeout;
|
|
27
27
|
constructor(config?: OllamaConfig);
|
|
28
28
|
isAvailable(): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Ensure URL has valid format (http:// prefix)
|
|
31
|
+
*/
|
|
32
|
+
private ensureValidUrl;
|
|
29
33
|
generate(request: GenerationRequest): Promise<GenerationResponse>;
|
|
30
34
|
stream(request: GenerationRequest): AsyncIterable<string>;
|
|
31
35
|
countTokens(text: string): number;
|
|
@@ -26,17 +26,20 @@ export class OllamaProvider {
|
|
|
26
26
|
defaultModel;
|
|
27
27
|
timeout;
|
|
28
28
|
constructor(config = {}) {
|
|
29
|
-
|
|
29
|
+
// Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues
|
|
30
|
+
this.baseUrl = config.baseUrl || process.env.OLLAMA_BASE_URL || 'http://127.0.0.1:11434';
|
|
30
31
|
this.defaultModel = config.model || process.env.OLLAMA_MODEL || 'deepseek-coder';
|
|
31
32
|
this.timeout = config.timeout || 120000; // 2 minutes default
|
|
32
33
|
}
|
|
33
34
|
async isAvailable() {
|
|
34
35
|
try {
|
|
36
|
+
// Ensure URL has proper format
|
|
37
|
+
const url = this.ensureValidUrl(this.baseUrl);
|
|
35
38
|
// Use a longer timeout (15 seconds) to accommodate slower systems
|
|
36
39
|
// Ollama can take time to respond, especially on first run or with many models
|
|
37
40
|
const controller = new AbortController();
|
|
38
41
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
39
|
-
const response = await fetch(`${
|
|
42
|
+
const response = await fetch(`${url}/api/tags`, {
|
|
40
43
|
signal: controller.signal,
|
|
41
44
|
});
|
|
42
45
|
clearTimeout(timeoutId);
|
|
@@ -49,21 +52,37 @@ export class OllamaProvider {
|
|
|
49
52
|
// Timeout - Ollama is slow to respond
|
|
50
53
|
console.debug(`[Ollama] Connection to ${this.baseUrl} timed out after 15s`);
|
|
51
54
|
}
|
|
55
|
+
else if (error.message.includes('ECONNREFUSED')) {
|
|
56
|
+
console.debug(`[Ollama] Connection refused - Ollama may not be running at ${this.baseUrl}`);
|
|
57
|
+
}
|
|
58
|
+
else if (error.message.includes('ENOTFOUND')) {
|
|
59
|
+
console.debug(`[Ollama] Host not found - check Ollama URL: ${this.baseUrl}`);
|
|
60
|
+
}
|
|
52
61
|
else {
|
|
53
|
-
// Other error
|
|
62
|
+
// Other error
|
|
54
63
|
console.debug(`[Ollama] Connection error: ${error.message}`);
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
return false;
|
|
58
67
|
}
|
|
59
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Ensure URL has valid format (http:// prefix)
|
|
71
|
+
*/
|
|
72
|
+
ensureValidUrl(url) {
|
|
73
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
74
|
+
return `http://${url}`;
|
|
75
|
+
}
|
|
76
|
+
return url;
|
|
77
|
+
}
|
|
60
78
|
async generate(request) {
|
|
61
79
|
const fullPrompt = this.buildFullPrompt(request);
|
|
80
|
+
const url = this.ensureValidUrl(this.baseUrl);
|
|
62
81
|
// Use timeout with AbortController for compatibility
|
|
63
82
|
const controller = new AbortController();
|
|
64
83
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
65
84
|
try {
|
|
66
|
-
const response = await fetch(`${
|
|
85
|
+
const response = await fetch(`${url}/api/generate`, {
|
|
67
86
|
method: 'POST',
|
|
68
87
|
headers: { 'Content-Type': 'application/json' },
|
|
69
88
|
signal: controller.signal,
|
|
@@ -101,7 +120,8 @@ export class OllamaProvider {
|
|
|
101
120
|
}
|
|
102
121
|
async *stream(request) {
|
|
103
122
|
const fullPrompt = this.buildFullPrompt(request);
|
|
104
|
-
const
|
|
123
|
+
const url = this.ensureValidUrl(this.baseUrl);
|
|
124
|
+
const response = await fetch(`${url}/api/generate`, {
|
|
105
125
|
method: 'POST',
|
|
106
126
|
headers: { 'Content-Type': 'application/json' },
|
|
107
127
|
body: JSON.stringify({
|
|
@@ -153,7 +173,8 @@ export class OllamaProvider {
|
|
|
153
173
|
* List available models from Ollama
|
|
154
174
|
*/
|
|
155
175
|
async listModels() {
|
|
156
|
-
const
|
|
176
|
+
const url = this.ensureValidUrl(this.baseUrl);
|
|
177
|
+
const response = await fetch(`${url}/api/tags`);
|
|
157
178
|
if (!response.ok) {
|
|
158
179
|
throw new OllamaError('Failed to list models');
|
|
159
180
|
}
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ import { doctorCommand } from './commands/doctor.js';
|
|
|
35
35
|
import { askCommand } from './commands/ask.js';
|
|
36
36
|
import { initCommand } from './commands/init.js';
|
|
37
37
|
import { examplesListCommand, examplesCopyCommand, examplesShowCommand } from './commands/examples.js';
|
|
38
|
-
import { runCommand } from './commands/run.js';
|
|
38
|
+
import { runCommand, preflightCommand } from './commands/run.js';
|
|
39
39
|
import { reportCommand } from './commands/report.js';
|
|
40
40
|
import { verifyCommand } from './commands/verify.js';
|
|
41
41
|
import { explainCommand } from './commands/explain.js';
|
|
@@ -125,8 +125,7 @@ program
|
|
|
125
125
|
.command('preflight [pack]')
|
|
126
126
|
.description('Dry-run validation without execution')
|
|
127
127
|
.action(async (pack) => {
|
|
128
|
-
|
|
129
|
-
console.log(chalk.yellow('⚠️ Phase 2 implementation coming soon...'));
|
|
128
|
+
await preflightCommand(pack);
|
|
130
129
|
});
|
|
131
130
|
program
|
|
132
131
|
.command('report')
|
package/dist/utils/config.js
CHANGED
|
@@ -109,7 +109,31 @@ function parseYaml(content) {
|
|
|
109
109
|
if (listMatch) {
|
|
110
110
|
const value = listMatch[1].trim();
|
|
111
111
|
if (Array.isArray(parent)) {
|
|
112
|
-
|
|
112
|
+
// Check if this is an object item (key: value) or a simple value
|
|
113
|
+
const colonIdx = value.indexOf(':');
|
|
114
|
+
if (colonIdx > 0 && !value.startsWith("'") && !value.startsWith('"')) {
|
|
115
|
+
// Object item like "- url: /" or "- key: value"
|
|
116
|
+
const objKey = value.slice(0, colonIdx).trim();
|
|
117
|
+
let objValue = value.slice(colonIdx + 1).trim();
|
|
118
|
+
// Check if next line is more indented (nested object properties)
|
|
119
|
+
if (i < lines.length - 1) {
|
|
120
|
+
const nextIndent = lines[i + 1].search(/\S|$/);
|
|
121
|
+
if (nextIndent > indent) {
|
|
122
|
+
// This is the start of a nested object
|
|
123
|
+
const obj = {};
|
|
124
|
+
obj[objKey] = parseValue(objValue);
|
|
125
|
+
parent.push(obj);
|
|
126
|
+
stack.push({ obj, indent });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Single-line object item
|
|
131
|
+
parent.push({ [objKey]: parseValue(objValue) });
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Simple value item
|
|
135
|
+
parent.push(parseValue(value));
|
|
136
|
+
}
|
|
113
137
|
}
|
|
114
138
|
continue;
|
|
115
139
|
}
|
|
@@ -144,6 +168,9 @@ function parseYaml(content) {
|
|
|
144
168
|
if (i < lines.length - 1) {
|
|
145
169
|
const nextIndent = lines[i + 1].search(/\S|$/);
|
|
146
170
|
if (nextIndent > indent) {
|
|
171
|
+
const nextLine = lines[i + 1].trim();
|
|
172
|
+
// Check if next line starts a list (array) or a key-value (object)
|
|
173
|
+
const isList = nextLine.startsWith('-');
|
|
147
174
|
if (Array.isArray(parent[key])) {
|
|
148
175
|
// Already an array
|
|
149
176
|
}
|
|
@@ -151,12 +178,18 @@ function parseYaml(content) {
|
|
|
151
178
|
// Nested object
|
|
152
179
|
stack.push({ obj: parent[key], indent });
|
|
153
180
|
}
|
|
154
|
-
else {
|
|
155
|
-
// Start of array
|
|
181
|
+
else if (isList) {
|
|
182
|
+
// Start of array (next line is a list item)
|
|
156
183
|
const arr = [];
|
|
157
184
|
parent[key] = arr;
|
|
158
185
|
stack.push({ obj: arr, indent });
|
|
159
186
|
}
|
|
187
|
+
else {
|
|
188
|
+
// Start of object (next line is a key-value pair)
|
|
189
|
+
const obj = {};
|
|
190
|
+
parent[key] = obj;
|
|
191
|
+
stack.push({ obj, indent });
|
|
192
|
+
}
|
|
160
193
|
}
|
|
161
194
|
}
|
|
162
195
|
}
|