qa360 2.1.5 ā 2.1.6
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/init.js +19 -3
- package/dist/core/ai/anthropic-provider.js +50 -38
- package/dist/core/ai/deepseek-provider.js +46 -34
- package/dist/core/ai/ollama-provider.js +41 -29
- package/dist/core/ai/openai-provider.js +46 -34
- package/dist/core/pack-v2/loader.d.ts +1 -0
- package/dist/core/pack-v2/loader.js +46 -13
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -530,9 +530,25 @@ export async function initCommand(options = {}) {
|
|
|
530
530
|
try {
|
|
531
531
|
console.log(chalk.bold.cyan('\nš QA360 Pack Generator (v2)\n'));
|
|
532
532
|
// Determine output file
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
533
|
+
let outputFile;
|
|
534
|
+
if (options.output) {
|
|
535
|
+
const resolved = resolve(options.output);
|
|
536
|
+
// Check if it's a directory or if it doesn't have an extension
|
|
537
|
+
if (options.output.endsWith('/') || !options.output.includes('.')) {
|
|
538
|
+
// It's a directory path, create the file inside it
|
|
539
|
+
mkdirSync(resolved, { recursive: true });
|
|
540
|
+
outputFile = join(resolved, 'qa360.yml');
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// It's a file path, ensure parent directory exists
|
|
544
|
+
const dir = resolve(resolved, '..');
|
|
545
|
+
mkdirSync(dir, { recursive: true });
|
|
546
|
+
outputFile = resolved;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
outputFile = join(process.cwd(), 'qa360.yml');
|
|
551
|
+
}
|
|
536
552
|
// Check if file exists
|
|
537
553
|
if (existsSync(outputFile) && !options.force) {
|
|
538
554
|
const overwrite = await inquirer.prompt([
|
|
@@ -36,6 +36,9 @@ export class AnthropicProvider {
|
|
|
36
36
|
}
|
|
37
37
|
// Try a minimal API call
|
|
38
38
|
try {
|
|
39
|
+
// Use timeout with AbortController for compatibility
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
39
42
|
const response = await fetch(`${this.baseURL}/v1/messages`, {
|
|
40
43
|
method: 'POST',
|
|
41
44
|
headers: {
|
|
@@ -43,13 +46,14 @@ export class AnthropicProvider {
|
|
|
43
46
|
'anthropic-version': this.version,
|
|
44
47
|
'content-type': 'application/json',
|
|
45
48
|
},
|
|
46
|
-
signal:
|
|
49
|
+
signal: controller.signal,
|
|
47
50
|
body: JSON.stringify({
|
|
48
51
|
model: this.defaultModel,
|
|
49
52
|
max_tokens: 1,
|
|
50
53
|
messages: [{ role: 'user', content: 'Hi' }]
|
|
51
54
|
})
|
|
52
55
|
});
|
|
56
|
+
clearTimeout(timeoutId);
|
|
53
57
|
// Anthropic may return 400 for malformed request but still be available
|
|
54
58
|
// We consider it available if we get any response (not a network error)
|
|
55
59
|
return response.status !== 401 && response.status !== 403;
|
|
@@ -65,46 +69,54 @@ export class AnthropicProvider {
|
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
71
|
const messages = this.buildMessages(request);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
status: response.status,
|
|
88
|
-
response: error
|
|
72
|
+
// Use timeout with AbortController for compatibility
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${this.baseURL}/v1/messages`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'x-api-key': this.apiKey,
|
|
80
|
+
'anthropic-version': this.version,
|
|
81
|
+
'content-type': 'application/json',
|
|
82
|
+
},
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
model: this.defaultModel,
|
|
86
|
+
messages,
|
|
87
|
+
system: request.systemPrompt,
|
|
88
|
+
temperature: request.temperature ?? 0.7,
|
|
89
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
90
|
+
})
|
|
89
91
|
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const error = await response.text().catch(() => 'Unknown error');
|
|
94
|
+
throw new AnthropicError(`Anthropic request failed: ${response.status} ${error}`, {
|
|
95
|
+
status: response.status,
|
|
96
|
+
response: error
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
if (data.error) {
|
|
101
|
+
throw new AnthropicError(`Anthropic API error: ${data.error.message}`, {
|
|
102
|
+
type: data.error.type
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const content = this.extractContent(data.content);
|
|
106
|
+
return {
|
|
107
|
+
content,
|
|
108
|
+
usage: {
|
|
109
|
+
promptTokens: data.usage?.input_tokens || 0,
|
|
110
|
+
completionTokens: data.usage?.output_tokens || 0,
|
|
111
|
+
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
|
|
112
|
+
},
|
|
113
|
+
model: data.model,
|
|
114
|
+
finishReason: data.stop_reason === 'end_turn' ? 'stop' : 'length'
|
|
115
|
+
};
|
|
90
116
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
throw new AnthropicError(`Anthropic API error: ${data.error.message}`, {
|
|
94
|
-
type: data.error.type
|
|
95
|
-
});
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeoutId);
|
|
96
119
|
}
|
|
97
|
-
const content = this.extractContent(data.content);
|
|
98
|
-
return {
|
|
99
|
-
content,
|
|
100
|
-
usage: {
|
|
101
|
-
promptTokens: data.usage?.input_tokens || 0,
|
|
102
|
-
completionTokens: data.usage?.output_tokens || 0,
|
|
103
|
-
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
|
|
104
|
-
},
|
|
105
|
-
model: data.model,
|
|
106
|
-
finishReason: data.stop_reason === 'end_turn' ? 'stop' : 'length'
|
|
107
|
-
};
|
|
108
120
|
}
|
|
109
121
|
async *stream(request) {
|
|
110
122
|
if (!this.apiKey) {
|
|
@@ -48,13 +48,17 @@ export class DeepSeekProvider {
|
|
|
48
48
|
}
|
|
49
49
|
// Try a minimal API call
|
|
50
50
|
try {
|
|
51
|
+
// Use timeout with AbortController for compatibility
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
51
54
|
const response = await fetch(`${this.baseURL}/v1/models`, {
|
|
52
55
|
method: 'GET',
|
|
53
56
|
headers: {
|
|
54
57
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
55
58
|
},
|
|
56
|
-
signal:
|
|
59
|
+
signal: controller.signal,
|
|
57
60
|
});
|
|
61
|
+
clearTimeout(timeoutId);
|
|
58
62
|
return response.ok;
|
|
59
63
|
}
|
|
60
64
|
catch {
|
|
@@ -69,42 +73,50 @@ export class DeepSeekProvider {
|
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
const messages = this.buildMessages(request);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
status: response.status,
|
|
90
|
-
response: errorText
|
|
76
|
+
// Use timeout with AbortController for compatibility
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(`${this.baseURL}/v1/chat/completions`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
model: this.defaultModel,
|
|
89
|
+
messages,
|
|
90
|
+
temperature: request.temperature ?? 0.7,
|
|
91
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
92
|
+
})
|
|
91
93
|
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
96
|
+
throw new DeepSeekError(`DeepSeek request failed: ${response.status} ${errorText}`, {
|
|
97
|
+
status: response.status,
|
|
98
|
+
response: errorText
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
const choice = data.choices?.[0];
|
|
103
|
+
if (!choice) {
|
|
104
|
+
throw new DeepSeekError('No choices returned from DeepSeek');
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
content: choice.message.content,
|
|
108
|
+
usage: {
|
|
109
|
+
promptTokens: data.usage?.prompt_tokens || 0,
|
|
110
|
+
completionTokens: data.usage?.completion_tokens || 0,
|
|
111
|
+
totalTokens: data.usage?.total_tokens || 0,
|
|
112
|
+
},
|
|
113
|
+
model: data.model,
|
|
114
|
+
finishReason: choice.finish_reason === 'stop' ? 'stop' : 'length'
|
|
115
|
+
};
|
|
92
116
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!choice) {
|
|
96
|
-
throw new DeepSeekError('No choices returned from DeepSeek');
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeoutId);
|
|
97
119
|
}
|
|
98
|
-
return {
|
|
99
|
-
content: choice.message.content,
|
|
100
|
-
usage: {
|
|
101
|
-
promptTokens: data.usage?.prompt_tokens || 0,
|
|
102
|
-
completionTokens: data.usage?.completion_tokens || 0,
|
|
103
|
-
totalTokens: data.usage?.total_tokens || 0,
|
|
104
|
-
},
|
|
105
|
-
model: data.model,
|
|
106
|
-
finishReason: choice.finish_reason === 'stop' ? 'stop' : 'length'
|
|
107
|
-
};
|
|
108
120
|
}
|
|
109
121
|
async *stream(request) {
|
|
110
122
|
if (!this.apiKey) {
|
|
@@ -32,9 +32,13 @@ export class OllamaProvider {
|
|
|
32
32
|
}
|
|
33
33
|
async isAvailable() {
|
|
34
34
|
try {
|
|
35
|
+
// Use timeout with AbortSignal - wrap for compatibility
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
35
38
|
const response = await fetch(`${this.baseUrl}/api/tags`, {
|
|
36
|
-
signal:
|
|
39
|
+
signal: controller.signal,
|
|
37
40
|
});
|
|
41
|
+
clearTimeout(timeoutId);
|
|
38
42
|
return response.ok;
|
|
39
43
|
}
|
|
40
44
|
catch {
|
|
@@ -43,37 +47,45 @@ export class OllamaProvider {
|
|
|
43
47
|
}
|
|
44
48
|
async generate(request) {
|
|
45
49
|
const fullPrompt = this.buildFullPrompt(request);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Use timeout with AbortController for compatibility
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
model: this.defaultModel,
|
|
60
|
+
prompt: fullPrompt,
|
|
61
|
+
stream: false,
|
|
62
|
+
options: {
|
|
63
|
+
temperature: request.temperature ?? 0.7,
|
|
64
|
+
num_predict: request.maxTokens ?? 4096,
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new OllamaError(`Ollama request failed: ${response.status} ${response.statusText}`, { response: await response.text().catch(() => 'Unknown error') });
|
|
70
|
+
}
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
if (data.error) {
|
|
73
|
+
throw new OllamaError(data.error);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
content: data.response,
|
|
77
|
+
usage: {
|
|
78
|
+
promptTokens: data.prompt_eval_count || 0,
|
|
79
|
+
completionTokens: data.eval_count || 0,
|
|
80
|
+
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0),
|
|
81
|
+
},
|
|
51
82
|
model: this.defaultModel,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
options: {
|
|
55
|
-
temperature: request.temperature ?? 0.7,
|
|
56
|
-
num_predict: request.maxTokens ?? 4096,
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
});
|
|
60
|
-
if (!response.ok) {
|
|
61
|
-
throw new OllamaError(`Ollama request failed: ${response.status} ${response.statusText}`, { response: await response.text().catch(() => 'Unknown error') });
|
|
83
|
+
finishReason: data.done_reason || 'stop',
|
|
84
|
+
};
|
|
62
85
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw new OllamaError(data.error);
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
66
88
|
}
|
|
67
|
-
return {
|
|
68
|
-
content: data.response,
|
|
69
|
-
usage: {
|
|
70
|
-
promptTokens: data.prompt_eval_count || 0,
|
|
71
|
-
completionTokens: data.eval_count || 0,
|
|
72
|
-
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0),
|
|
73
|
-
},
|
|
74
|
-
model: this.defaultModel,
|
|
75
|
-
finishReason: data.done_reason || 'stop',
|
|
76
|
-
};
|
|
77
89
|
}
|
|
78
90
|
async *stream(request) {
|
|
79
91
|
const fullPrompt = this.buildFullPrompt(request);
|
|
@@ -36,13 +36,17 @@ export class OpenAIProvider {
|
|
|
36
36
|
}
|
|
37
37
|
// Try a minimal API call
|
|
38
38
|
try {
|
|
39
|
+
// Use timeout with AbortController for compatibility
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
39
42
|
const response = await fetch(`${this.baseURL}/models`, {
|
|
40
43
|
method: 'GET',
|
|
41
44
|
headers: {
|
|
42
45
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
43
46
|
},
|
|
44
|
-
signal:
|
|
47
|
+
signal: controller.signal,
|
|
45
48
|
});
|
|
49
|
+
clearTimeout(timeoutId);
|
|
46
50
|
return response.ok;
|
|
47
51
|
}
|
|
48
52
|
catch {
|
|
@@ -56,42 +60,50 @@ export class OpenAIProvider {
|
|
|
56
60
|
});
|
|
57
61
|
}
|
|
58
62
|
const messages = this.buildMessages(request);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
status: response.status,
|
|
77
|
-
response: error
|
|
63
|
+
// Use timeout with AbortController for compatibility
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
model: this.defaultModel,
|
|
76
|
+
messages,
|
|
77
|
+
temperature: request.temperature ?? 0.7,
|
|
78
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
79
|
+
})
|
|
78
80
|
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = await response.text().catch(() => 'Unknown error');
|
|
83
|
+
throw new OpenAIError(`OpenAI request failed: ${response.status} ${error}`, {
|
|
84
|
+
status: response.status,
|
|
85
|
+
response: error
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
const choice = data.choices?.[0];
|
|
90
|
+
if (!choice) {
|
|
91
|
+
throw new OpenAIError('No choices returned from OpenAI');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
content: choice.message.content,
|
|
95
|
+
usage: {
|
|
96
|
+
promptTokens: data.usage?.prompt_tokens || 0,
|
|
97
|
+
completionTokens: data.usage?.completion_tokens || 0,
|
|
98
|
+
totalTokens: data.usage?.total_tokens || 0,
|
|
99
|
+
},
|
|
100
|
+
model: data.model,
|
|
101
|
+
finishReason: choice.finish_reason === 'stop' ? 'stop' : 'length'
|
|
102
|
+
};
|
|
79
103
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!choice) {
|
|
83
|
-
throw new OpenAIError('No choices returned from OpenAI');
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
84
106
|
}
|
|
85
|
-
return {
|
|
86
|
-
content: choice.message.content,
|
|
87
|
-
usage: {
|
|
88
|
-
promptTokens: data.usage?.prompt_tokens || 0,
|
|
89
|
-
completionTokens: data.usage?.completion_tokens || 0,
|
|
90
|
-
totalTokens: data.usage?.total_tokens || 0,
|
|
91
|
-
},
|
|
92
|
-
model: data.model,
|
|
93
|
-
finishReason: choice.finish_reason === 'stop' ? 'stop' : 'length'
|
|
94
|
-
};
|
|
95
107
|
}
|
|
96
108
|
async *stream(request) {
|
|
97
109
|
if (!this.apiKey) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* QA360 Pack v2 Loader
|
|
3
3
|
*
|
|
4
4
|
* Loads and parses pack.yml v2 files from the filesystem.
|
|
5
|
+
* Supports environment variable substitution with ${VAR} and ${VAR:-default} syntax.
|
|
5
6
|
*/
|
|
6
7
|
import { PackConfigV2, PackValidationResultV2, ValidationError } from '../types/pack-v2.js';
|
|
7
8
|
export interface PackLoadResult {
|
|
@@ -2,12 +2,41 @@
|
|
|
2
2
|
* QA360 Pack v2 Loader
|
|
3
3
|
*
|
|
4
4
|
* Loads and parses pack.yml v2 files from the filesystem.
|
|
5
|
+
* Supports environment variable substitution with ${VAR} and ${VAR:-default} syntax.
|
|
5
6
|
*/
|
|
6
7
|
import { readFileSync, existsSync } from 'fs';
|
|
7
8
|
import { resolve } from 'path';
|
|
8
9
|
import * as yaml from 'js-yaml';
|
|
9
10
|
import { PackValidatorV2 } from './validator.js';
|
|
10
11
|
import { PackMigrator } from './migrator.js';
|
|
12
|
+
/**
|
|
13
|
+
* Substitute environment variables in a string
|
|
14
|
+
* Supports ${VAR} and ${VAR:-default} syntax
|
|
15
|
+
*/
|
|
16
|
+
function substituteEnvVars(str) {
|
|
17
|
+
return str.replace(/\$\{([^}:]+)(:-([^}]*))?\}/g, (match, varName, _, defaultValue) => {
|
|
18
|
+
return process.env[varName] ?? defaultValue ?? match;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Recursively substitute environment variables in an object
|
|
23
|
+
*/
|
|
24
|
+
function substituteEnvVarsRecursive(obj) {
|
|
25
|
+
if (typeof obj === 'string') {
|
|
26
|
+
return substituteEnvVars(obj);
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(obj)) {
|
|
29
|
+
return obj.map(substituteEnvVarsRecursive);
|
|
30
|
+
}
|
|
31
|
+
if (obj !== null && typeof obj === 'object') {
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
34
|
+
result[key] = substituteEnvVarsRecursive(value);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
return obj;
|
|
39
|
+
}
|
|
11
40
|
export class PackLoaderV2 {
|
|
12
41
|
cache = new Map();
|
|
13
42
|
/**
|
|
@@ -30,16 +59,18 @@ export class PackLoaderV2 {
|
|
|
30
59
|
const content = readFileSync(resolvedPath, 'utf-8');
|
|
31
60
|
// Parse YAML
|
|
32
61
|
const parsed = yaml.load(content);
|
|
62
|
+
// Substitute environment variables
|
|
63
|
+
const substituted = substituteEnvVarsRecursive(parsed);
|
|
33
64
|
// Detect version
|
|
34
65
|
const migrator = new PackMigrator();
|
|
35
|
-
const detectedVersion = migrator.detectVersion(
|
|
66
|
+
const detectedVersion = migrator.detectVersion(substituted);
|
|
36
67
|
// Set format
|
|
37
68
|
const format = detectedVersion === 1 ? 'v1' : detectedVersion === 2 ? 'v2' : 'unknown';
|
|
38
69
|
// For v1 without migration
|
|
39
70
|
if (detectedVersion === 1 && options?.migrate === false) {
|
|
40
71
|
return {
|
|
41
72
|
success: true,
|
|
42
|
-
pack:
|
|
73
|
+
pack: substituted,
|
|
43
74
|
format: 'v1',
|
|
44
75
|
migrated: false,
|
|
45
76
|
sourcePath: resolvedPath
|
|
@@ -51,10 +82,10 @@ export class PackLoaderV2 {
|
|
|
51
82
|
let migrationWarnings = [];
|
|
52
83
|
if (detectedVersion === 1) {
|
|
53
84
|
// Check for legacy format before migrating
|
|
54
|
-
if (migrator.isLegacyFormat(
|
|
85
|
+
if (migrator.isLegacyFormat(substituted)) {
|
|
55
86
|
return {
|
|
56
87
|
success: false,
|
|
57
|
-
error: new Error(`Legacy pack format detected (version ${
|
|
88
|
+
error: new Error(`Legacy pack format detected (version ${substituted.version || '0.9.x'}). ` +
|
|
58
89
|
`This format is no longer supported.\n\n` +
|
|
59
90
|
`To fix this:\n` +
|
|
60
91
|
` 1. Delete the old pack file: ${resolvedPath}\n` +
|
|
@@ -65,7 +96,7 @@ export class PackLoaderV2 {
|
|
|
65
96
|
};
|
|
66
97
|
}
|
|
67
98
|
// Migrate v1 to v2
|
|
68
|
-
const migrationResult = migrator.migrate(
|
|
99
|
+
const migrationResult = migrator.migrate(substituted);
|
|
69
100
|
if (!migrationResult.success) {
|
|
70
101
|
return {
|
|
71
102
|
success: false,
|
|
@@ -80,14 +111,14 @@ export class PackLoaderV2 {
|
|
|
80
111
|
migrationWarnings = migrationResult.warnings;
|
|
81
112
|
}
|
|
82
113
|
else if (detectedVersion === 2) {
|
|
83
|
-
pack =
|
|
114
|
+
pack = substituted;
|
|
84
115
|
}
|
|
85
116
|
else {
|
|
86
117
|
// Check for legacy format and provide helpful error
|
|
87
|
-
if (migrator.isLegacyFormat(
|
|
118
|
+
if (migrator.isLegacyFormat(substituted)) {
|
|
88
119
|
return {
|
|
89
120
|
success: false,
|
|
90
|
-
error: new Error(`Legacy pack format detected (version ${
|
|
121
|
+
error: new Error(`Legacy pack format detected (version ${substituted.version || '0.9.x'}). ` +
|
|
91
122
|
`This format is no longer supported.\n\n` +
|
|
92
123
|
`To fix this:\n` +
|
|
93
124
|
` 1. Delete the old pack file: ${resolvedPath}\n` +
|
|
@@ -99,7 +130,7 @@ export class PackLoaderV2 {
|
|
|
99
130
|
}
|
|
100
131
|
return {
|
|
101
132
|
success: false,
|
|
102
|
-
error: new Error(`Unknown pack format. Version: ${
|
|
133
|
+
error: new Error(`Unknown pack format. Version: ${substituted.version}`),
|
|
103
134
|
sourcePath: resolvedPath,
|
|
104
135
|
format: 'unknown'
|
|
105
136
|
};
|
|
@@ -189,13 +220,15 @@ export class PackLoaderV2 {
|
|
|
189
220
|
async loadFromString(yamlContent, options) {
|
|
190
221
|
try {
|
|
191
222
|
const parsed = yaml.load(yamlContent);
|
|
223
|
+
// Substitute environment variables
|
|
224
|
+
const substituted = substituteEnvVarsRecursive(parsed);
|
|
192
225
|
// Detect version and migrate if needed
|
|
193
226
|
const migrator = new PackMigrator();
|
|
194
|
-
const detectedVersion = migrator.detectVersion(
|
|
227
|
+
const detectedVersion = migrator.detectVersion(substituted);
|
|
195
228
|
let pack;
|
|
196
229
|
let migrated = false;
|
|
197
230
|
if (detectedVersion === 1) {
|
|
198
|
-
const migrationResult = migrator.migrate(
|
|
231
|
+
const migrationResult = migrator.migrate(substituted);
|
|
199
232
|
if (!migrationResult.success) {
|
|
200
233
|
return {
|
|
201
234
|
success: false,
|
|
@@ -206,12 +239,12 @@ export class PackLoaderV2 {
|
|
|
206
239
|
migrated = true;
|
|
207
240
|
}
|
|
208
241
|
else if (detectedVersion === 2) {
|
|
209
|
-
pack =
|
|
242
|
+
pack = substituted;
|
|
210
243
|
}
|
|
211
244
|
else {
|
|
212
245
|
return {
|
|
213
246
|
success: false,
|
|
214
|
-
error: new Error(`Unknown pack format. Version: ${
|
|
247
|
+
error: new Error(`Unknown pack format. Version: ${substituted.version}`)
|
|
215
248
|
};
|
|
216
249
|
}
|
|
217
250
|
// Validate if requested
|