qa360 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/commands/history.js +1 -1
  2. package/dist/commands/pack.js +1 -1
  3. package/dist/commands/run.d.ts +1 -1
  4. package/dist/commands/run.d.ts.map +1 -1
  5. package/dist/commands/run.js +1 -1
  6. package/dist/commands/secrets.js +1 -1
  7. package/dist/commands/serve.js +1 -1
  8. package/dist/commands/verify.js +1 -1
  9. package/dist/core/adapters/gitleaks-secrets.d.ts +115 -0
  10. package/dist/core/adapters/gitleaks-secrets.d.ts.map +1 -0
  11. package/dist/core/adapters/gitleaks-secrets.js +410 -0
  12. package/dist/core/adapters/k6-perf.d.ts +86 -0
  13. package/dist/core/adapters/k6-perf.d.ts.map +1 -0
  14. package/dist/core/adapters/k6-perf.js +398 -0
  15. package/dist/core/adapters/osv-deps.d.ts +124 -0
  16. package/dist/core/adapters/osv-deps.d.ts.map +1 -0
  17. package/dist/core/adapters/osv-deps.js +372 -0
  18. package/dist/core/adapters/playwright-api.d.ts +82 -0
  19. package/dist/core/adapters/playwright-api.d.ts.map +1 -0
  20. package/dist/core/adapters/playwright-api.js +252 -0
  21. package/dist/core/adapters/playwright-ui.d.ts +115 -0
  22. package/dist/core/adapters/playwright-ui.d.ts.map +1 -0
  23. package/dist/core/adapters/playwright-ui.js +346 -0
  24. package/dist/core/adapters/semgrep-sast.d.ts +100 -0
  25. package/dist/core/adapters/semgrep-sast.d.ts.map +1 -0
  26. package/dist/core/adapters/semgrep-sast.js +322 -0
  27. package/dist/core/adapters/zap-dast.d.ts +134 -0
  28. package/dist/core/adapters/zap-dast.d.ts.map +1 -0
  29. package/dist/core/adapters/zap-dast.js +424 -0
  30. package/dist/core/hooks/compose.d.ts +62 -0
  31. package/dist/core/hooks/compose.d.ts.map +1 -0
  32. package/dist/core/hooks/compose.js +225 -0
  33. package/dist/core/hooks/runner.d.ts +69 -0
  34. package/dist/core/hooks/runner.d.ts.map +1 -0
  35. package/dist/core/hooks/runner.js +303 -0
  36. package/dist/core/index.d.ts +74 -0
  37. package/dist/core/index.d.ts.map +1 -0
  38. package/dist/core/index.js +39 -0
  39. package/dist/core/pack/migrator.d.ts +52 -0
  40. package/dist/core/pack/migrator.d.ts.map +1 -0
  41. package/dist/core/pack/migrator.js +304 -0
  42. package/dist/core/pack/validator.d.ts +43 -0
  43. package/dist/core/pack/validator.d.ts.map +1 -0
  44. package/dist/core/pack/validator.js +292 -0
  45. package/dist/core/proof/bundle.d.ts +138 -0
  46. package/dist/core/proof/bundle.d.ts.map +1 -0
  47. package/dist/core/proof/bundle.js +160 -0
  48. package/dist/core/proof/canonicalize.d.ts +48 -0
  49. package/dist/core/proof/canonicalize.d.ts.map +1 -0
  50. package/dist/core/proof/canonicalize.js +105 -0
  51. package/dist/core/proof/index.d.ts +14 -0
  52. package/dist/core/proof/index.d.ts.map +1 -0
  53. package/dist/core/proof/index.js +18 -0
  54. package/dist/core/proof/schema.d.ts +218 -0
  55. package/dist/core/proof/schema.d.ts.map +1 -0
  56. package/dist/core/proof/schema.js +263 -0
  57. package/dist/core/proof/signer.d.ts +112 -0
  58. package/dist/core/proof/signer.d.ts.map +1 -0
  59. package/dist/core/proof/signer.js +226 -0
  60. package/dist/core/proof/verifier.d.ts +98 -0
  61. package/dist/core/proof/verifier.d.ts.map +1 -0
  62. package/dist/core/proof/verifier.js +302 -0
  63. package/dist/core/runner/phase3-runner.d.ts +102 -0
  64. package/dist/core/runner/phase3-runner.d.ts.map +1 -0
  65. package/dist/core/runner/phase3-runner.js +471 -0
  66. package/dist/core/secrets/crypto.d.ts +76 -0
  67. package/dist/core/secrets/crypto.d.ts.map +1 -0
  68. package/dist/core/secrets/crypto.js +225 -0
  69. package/dist/core/secrets/manager.d.ts +77 -0
  70. package/dist/core/secrets/manager.d.ts.map +1 -0
  71. package/dist/core/secrets/manager.js +219 -0
  72. package/dist/core/security/redaction-patterns-extended.d.ts +28 -0
  73. package/dist/core/security/redaction-patterns-extended.d.ts.map +1 -0
  74. package/dist/core/security/redaction-patterns-extended.js +247 -0
  75. package/dist/core/security/redactor.d.ts +72 -0
  76. package/dist/core/security/redactor.d.ts.map +1 -0
  77. package/dist/core/security/redactor.js +279 -0
  78. package/dist/core/serve/diagnostics-collector.d.ts +33 -0
  79. package/dist/core/serve/diagnostics-collector.d.ts.map +1 -0
  80. package/dist/core/serve/diagnostics-collector.js +149 -0
  81. package/dist/core/serve/health-checker.d.ts +45 -0
  82. package/dist/core/serve/health-checker.d.ts.map +1 -0
  83. package/dist/core/serve/health-checker.js +219 -0
  84. package/dist/core/serve/index.d.ts +9 -0
  85. package/dist/core/serve/index.d.ts.map +1 -0
  86. package/dist/core/serve/index.js +8 -0
  87. package/dist/core/serve/metrics-collector.d.ts +25 -0
  88. package/dist/core/serve/metrics-collector.d.ts.map +1 -0
  89. package/dist/core/serve/metrics-collector.js +322 -0
  90. package/dist/core/serve/process-manager.d.ts +37 -0
  91. package/dist/core/serve/process-manager.d.ts.map +1 -0
  92. package/dist/core/serve/process-manager.js +213 -0
  93. package/dist/core/serve/server.d.ts +37 -0
  94. package/dist/core/serve/server.d.ts.map +1 -0
  95. package/dist/core/serve/server.js +191 -0
  96. package/dist/core/types/pack-v1.d.ts +162 -0
  97. package/dist/core/types/pack-v1.d.ts.map +1 -0
  98. package/dist/core/types/pack-v1.js +5 -0
  99. package/dist/core/types/trust-score.d.ts +70 -0
  100. package/dist/core/types/trust-score.d.ts.map +1 -0
  101. package/dist/core/types/trust-score.js +191 -0
  102. package/dist/core/vault/cas.d.ts +87 -0
  103. package/dist/core/vault/cas.d.ts.map +1 -0
  104. package/dist/core/vault/cas.js +255 -0
  105. package/dist/core/vault/index.d.ts +205 -0
  106. package/dist/core/vault/index.d.ts.map +1 -0
  107. package/dist/core/vault/index.js +631 -0
  108. package/package.json +12 -6
@@ -0,0 +1,322 @@
1
+ /**
2
+ * QA360 Metrics Collector
3
+ * Génère des métriques au format OpenMetrics/Prometheus
4
+ */
5
+ import { existsSync } from 'fs';
6
+ import { join } from 'path';
7
+ export class MetricsCollector {
8
+ startTime;
9
+ constructor() {
10
+ this.startTime = Date.now();
11
+ }
12
+ async collect() {
13
+ const metrics = await this.gatherMetrics();
14
+ return this.formatOpenMetrics(metrics);
15
+ }
16
+ async gatherMetrics() {
17
+ const metrics = [];
18
+ // System metrics
19
+ metrics.push({
20
+ name: 'qa360_uptime_seconds',
21
+ value: Math.floor((Date.now() - this.startTime) / 1000),
22
+ help: 'QA360 server uptime in seconds',
23
+ type: 'counter'
24
+ });
25
+ // Version info
26
+ const version = this.getVersion();
27
+ metrics.push({
28
+ name: 'qa360_info',
29
+ value: 1,
30
+ labels: { version },
31
+ help: 'QA360 version information',
32
+ type: 'gauge'
33
+ });
34
+ // Run metrics from history
35
+ const runMetrics = await this.collectRunMetrics();
36
+ metrics.push(...runMetrics);
37
+ // Gate metrics from last run
38
+ const gateMetrics = await this.collectGateMetrics();
39
+ metrics.push(...gateMetrics);
40
+ // Security metrics from last run
41
+ const securityMetrics = await this.collectSecurityMetrics();
42
+ metrics.push(...securityMetrics);
43
+ // Performance metrics from last run
44
+ const perfMetrics = await this.collectPerformanceMetrics();
45
+ metrics.push(...perfMetrics);
46
+ return metrics;
47
+ }
48
+ async collectRunMetrics() {
49
+ const metrics = [];
50
+ try {
51
+ const runsDir = join(process.cwd(), '.qa360', 'runs');
52
+ if (!existsSync(runsDir)) {
53
+ return [
54
+ { name: 'qa360_runs_total', value: 0, help: 'Total number of QA360 runs', type: 'counter' },
55
+ { name: 'qa360_last_run_trust_score', value: 0, help: 'Trust score of last run', type: 'gauge' }
56
+ ];
57
+ }
58
+ const fs = require('fs');
59
+ const runFiles = fs.readdirSync(runsDir).filter((f) => f.endsWith('.json'));
60
+ metrics.push({
61
+ name: 'qa360_runs_total',
62
+ value: runFiles.length,
63
+ help: 'Total number of QA360 runs',
64
+ type: 'counter'
65
+ });
66
+ // Get last run metrics
67
+ if (runFiles.length > 0) {
68
+ const lastRunFile = runFiles.sort().pop();
69
+ const lastRunPath = join(runsDir, lastRunFile);
70
+ const lastRun = JSON.parse(fs.readFileSync(lastRunPath, 'utf8'));
71
+ metrics.push({
72
+ name: 'qa360_last_run_trust_score',
73
+ value: lastRun.trustScore || lastRun.trust_score || 0,
74
+ help: 'Trust score of last run',
75
+ type: 'gauge'
76
+ });
77
+ metrics.push({
78
+ name: 'qa360_last_run_timestamp',
79
+ value: new Date(lastRun.timestamp || lastRun.ts || 0).getTime() / 1000,
80
+ help: 'Timestamp of last run',
81
+ type: 'gauge'
82
+ });
83
+ }
84
+ else {
85
+ metrics.push({
86
+ name: 'qa360_last_run_trust_score',
87
+ value: 0,
88
+ help: 'Trust score of last run',
89
+ type: 'gauge'
90
+ });
91
+ }
92
+ }
93
+ catch {
94
+ metrics.push({
95
+ name: 'qa360_runs_total',
96
+ value: 0,
97
+ help: 'Total number of QA360 runs',
98
+ type: 'counter'
99
+ });
100
+ }
101
+ return metrics;
102
+ }
103
+ async collectGateMetrics() {
104
+ const metrics = [];
105
+ try {
106
+ const lastRun = await this.getLastRunData();
107
+ if (!lastRun)
108
+ return metrics;
109
+ // Gate duration metrics
110
+ const gates = ['api', 'ui', 'perf', 'a11y', 'sast', 'deps', 'secrets', 'dast'];
111
+ for (const gate of gates) {
112
+ const gateData = lastRun[gate] || lastRun.gates?.[gate];
113
+ if (gateData) {
114
+ const duration = gateData.duration_ms || gateData.duration || 0;
115
+ metrics.push({
116
+ name: 'qa360_gate_duration_ms',
117
+ value: duration,
118
+ labels: { gate },
119
+ help: 'Duration of gate execution in milliseconds',
120
+ type: 'gauge'
121
+ });
122
+ const failures = gateData.failures || (gateData.success === false ? 1 : 0);
123
+ metrics.push({
124
+ name: 'qa360_gate_failures_total',
125
+ value: failures,
126
+ labels: { gate },
127
+ help: 'Number of gate failures',
128
+ type: 'counter'
129
+ });
130
+ }
131
+ }
132
+ }
133
+ catch {
134
+ // Return empty metrics on error
135
+ }
136
+ return metrics;
137
+ }
138
+ async collectSecurityMetrics() {
139
+ const metrics = [];
140
+ try {
141
+ const lastRun = await this.getLastRunData();
142
+ if (!lastRun?.security)
143
+ return metrics;
144
+ // SAST findings
145
+ if (lastRun.security.sast) {
146
+ const sastData = lastRun.security.sast;
147
+ const highFindings = sastData.max_high?.actual || 0;
148
+ const criticalFindings = sastData.max_critical?.actual || 0;
149
+ const mediumFindings = sastData.max_medium?.actual || 0;
150
+ metrics.push({
151
+ name: 'qa360_security_findings',
152
+ value: highFindings,
153
+ labels: { kind: 'sast_high' },
154
+ help: 'Number of security findings by type',
155
+ type: 'gauge'
156
+ });
157
+ metrics.push({
158
+ name: 'qa360_security_findings',
159
+ value: criticalFindings,
160
+ labels: { kind: 'sast_critical' },
161
+ help: 'Number of security findings by type',
162
+ type: 'gauge'
163
+ });
164
+ metrics.push({
165
+ name: 'qa360_security_findings',
166
+ value: mediumFindings,
167
+ labels: { kind: 'sast_medium' },
168
+ help: 'Number of security findings by type',
169
+ type: 'gauge'
170
+ });
171
+ }
172
+ // DAST findings
173
+ if (lastRun.security.dast) {
174
+ const dastData = lastRun.security.dast;
175
+ const dastHigh = dastData.max_high?.actual || 0;
176
+ metrics.push({
177
+ name: 'qa360_security_findings',
178
+ value: dastHigh,
179
+ labels: { kind: 'dast_high' },
180
+ help: 'Number of security findings by type',
181
+ type: 'gauge'
182
+ });
183
+ }
184
+ // Dependencies vulnerabilities
185
+ if (lastRun.security.deps) {
186
+ const depsData = lastRun.security.deps;
187
+ const depsHigh = depsData.max_high?.actual || 0;
188
+ metrics.push({
189
+ name: 'qa360_security_findings',
190
+ value: depsHigh,
191
+ labels: { kind: 'deps_high' },
192
+ help: 'Number of security findings by type',
193
+ type: 'gauge'
194
+ });
195
+ }
196
+ // Secrets findings
197
+ if (lastRun.security.secrets) {
198
+ const secretsData = lastRun.security.secrets;
199
+ const secretsFindings = secretsData.max_findings?.actual || 0;
200
+ metrics.push({
201
+ name: 'qa360_security_findings',
202
+ value: secretsFindings,
203
+ labels: { kind: 'secrets' },
204
+ help: 'Number of security findings by type',
205
+ type: 'gauge'
206
+ });
207
+ }
208
+ }
209
+ catch {
210
+ // Return empty metrics on error
211
+ }
212
+ return metrics;
213
+ }
214
+ async collectPerformanceMetrics() {
215
+ const metrics = [];
216
+ try {
217
+ const lastRun = await this.getLastRunData();
218
+ if (!lastRun?.perf && !lastRun?.performance)
219
+ return metrics;
220
+ const perfData = lastRun.perf || lastRun.performance;
221
+ // Performance percentiles
222
+ const p95 = perfData.p95_ms?.actual || perfData.p95 || 0;
223
+ const p90 = perfData.p90_ms?.actual || perfData.p90 || 0;
224
+ const p50 = perfData.p50_ms?.actual || perfData.p50 || 0;
225
+ metrics.push({
226
+ name: 'qa360_perf_p95_ms',
227
+ value: p95,
228
+ help: 'Performance P95 latency in milliseconds',
229
+ type: 'gauge'
230
+ });
231
+ metrics.push({
232
+ name: 'qa360_perf_p90_ms',
233
+ value: p90,
234
+ help: 'Performance P90 latency in milliseconds',
235
+ type: 'gauge'
236
+ });
237
+ metrics.push({
238
+ name: 'qa360_perf_p50_ms',
239
+ value: p50,
240
+ help: 'Performance P50 latency in milliseconds',
241
+ type: 'gauge'
242
+ });
243
+ // Error rate
244
+ const errorRate = perfData.max_errors_rate?.actual || perfData.error_rate || 0;
245
+ metrics.push({
246
+ name: 'qa360_perf_error_rate',
247
+ value: errorRate,
248
+ help: 'Performance error rate',
249
+ type: 'gauge'
250
+ });
251
+ }
252
+ catch {
253
+ // Return empty metrics on error
254
+ }
255
+ return metrics;
256
+ }
257
+ async getLastRunData() {
258
+ try {
259
+ const runsDir = join(process.cwd(), '.qa360', 'runs');
260
+ if (!existsSync(runsDir))
261
+ return null;
262
+ const fs = require('fs');
263
+ const runFiles = fs.readdirSync(runsDir)
264
+ .filter((f) => f.endsWith('.json'))
265
+ .sort();
266
+ if (runFiles.length === 0)
267
+ return null;
268
+ const lastRunFile = runFiles.pop();
269
+ const lastRunPath = join(runsDir, lastRunFile);
270
+ return JSON.parse(fs.readFileSync(lastRunPath, 'utf8'));
271
+ }
272
+ catch {
273
+ return null;
274
+ }
275
+ }
276
+ formatOpenMetrics(metrics) {
277
+ const lines = [];
278
+ // Group metrics by name for proper formatting
279
+ const metricGroups = new Map();
280
+ for (const metric of metrics) {
281
+ if (!metricGroups.has(metric.name)) {
282
+ metricGroups.set(metric.name, []);
283
+ }
284
+ metricGroups.get(metric.name).push(metric);
285
+ }
286
+ // Format each metric group
287
+ for (const [name, metricList] of metricGroups) {
288
+ const firstMetric = metricList[0];
289
+ // Add help comment
290
+ if (firstMetric.help) {
291
+ lines.push(`# HELP ${name} ${firstMetric.help}`);
292
+ }
293
+ // Add type comment
294
+ if (firstMetric.type) {
295
+ lines.push(`# TYPE ${name} ${firstMetric.type}`);
296
+ }
297
+ // Add metric lines
298
+ for (const metric of metricList) {
299
+ let line = name;
300
+ if (metric.labels && Object.keys(metric.labels).length > 0) {
301
+ const labelPairs = Object.entries(metric.labels)
302
+ .map(([key, value]) => `${key}="${value}"`)
303
+ .join(',');
304
+ line += `{${labelPairs}}`;
305
+ }
306
+ line += ` ${metric.value}`;
307
+ lines.push(line);
308
+ }
309
+ lines.push(''); // Empty line between metric groups
310
+ }
311
+ return lines.join('\n');
312
+ }
313
+ getVersion() {
314
+ try {
315
+ const packageInfo = require('../../package.json');
316
+ return packageInfo.version || '0.9.0-core';
317
+ }
318
+ catch {
319
+ return '0.9.0-core';
320
+ }
321
+ }
322
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * QA360 Process Manager
3
+ * Gestion graceful shutdown et cancel safe des processus
4
+ */
5
+ import { ChildProcess } from 'child_process';
6
+ import { EventEmitter } from 'events';
7
+ export interface RunProcess {
8
+ runId: string;
9
+ pid: number;
10
+ startTime: number;
11
+ command: string;
12
+ status: 'running' | 'cancelled' | 'completed' | 'failed';
13
+ children: ChildProcess[];
14
+ }
15
+ export interface CancelResult {
16
+ cancelled: boolean;
17
+ runId: string;
18
+ reason?: string;
19
+ }
20
+ export declare class ProcessManager extends EventEmitter {
21
+ private activeRuns;
22
+ private gracefulTimeout;
23
+ constructor();
24
+ registerRun(runId: string, command: string, mainProcess?: ChildProcess): void;
25
+ addChildProcess(runId: string, child: ChildProcess): void;
26
+ cancelRun(runId: string): Promise<CancelResult>;
27
+ cancelAllRuns(): Promise<CancelResult[]>;
28
+ getActiveRuns(): RunProcess[];
29
+ getRunStatus(runId: string): RunProcess | null;
30
+ private gracefulShutdown;
31
+ private terminateProcess;
32
+ private cleanupDockerContainers;
33
+ private completeRun;
34
+ private setupSignalHandlers;
35
+ dispose(): void;
36
+ }
37
+ //# sourceMappingURL=process-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../../../src/core/serve/process-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAS,YAAY,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAC;IACzD,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,eAAe,CAAS;;IAWhC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,YAAY,GAAG,IAAI;IAqB7E,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;IAOnD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAsC/C,aAAa,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAW9C,aAAa,IAAI,UAAU,EAAE;IAI7B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;YAIhC,gBAAgB;YAiBhB,gBAAgB;YA2ChB,uBAAuB;IAsCrC,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,mBAAmB;IA+B3B,OAAO;CAMR"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * QA360 Process Manager
3
+ * Gestion graceful shutdown et cancel safe des processus
4
+ */
5
+ import { spawn } from 'child_process';
6
+ import { EventEmitter } from 'events';
7
+ export class ProcessManager extends EventEmitter {
8
+ activeRuns = new Map();
9
+ gracefulTimeout = 10000; // 10 seconds
10
+ constructor() {
11
+ super();
12
+ // Augmenter la limite des listeners pour les tests
13
+ if (process.env.NODE_ENV === 'test') {
14
+ process.setMaxListeners(50);
15
+ }
16
+ this.setupSignalHandlers();
17
+ }
18
+ registerRun(runId, command, mainProcess) {
19
+ const runProcess = {
20
+ runId,
21
+ pid: mainProcess?.pid || process.pid,
22
+ startTime: Date.now(),
23
+ command,
24
+ status: 'running',
25
+ children: mainProcess ? [mainProcess] : []
26
+ };
27
+ this.activeRuns.set(runId, runProcess);
28
+ this.emit('run-registered', runProcess);
29
+ // Auto-cleanup on process exit
30
+ if (mainProcess) {
31
+ mainProcess.on('exit', () => {
32
+ this.completeRun(runId);
33
+ });
34
+ }
35
+ }
36
+ addChildProcess(runId, child) {
37
+ const run = this.activeRuns.get(runId);
38
+ if (run) {
39
+ run.children.push(child);
40
+ }
41
+ }
42
+ async cancelRun(runId) {
43
+ const run = this.activeRuns.get(runId);
44
+ if (!run) {
45
+ return {
46
+ cancelled: false,
47
+ runId,
48
+ reason: 'Run not found'
49
+ };
50
+ }
51
+ if (run.status !== 'running') {
52
+ return {
53
+ cancelled: false,
54
+ runId,
55
+ reason: `Run already ${run.status}`
56
+ };
57
+ }
58
+ try {
59
+ await this.gracefulShutdown(run);
60
+ run.status = 'cancelled';
61
+ this.emit('run-cancelled', run);
62
+ return {
63
+ cancelled: true,
64
+ runId
65
+ };
66
+ }
67
+ catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
69
+ return {
70
+ cancelled: false,
71
+ runId,
72
+ reason: `Failed to cancel: ${errorMessage}`
73
+ };
74
+ }
75
+ }
76
+ async cancelAllRuns() {
77
+ const results = [];
78
+ for (const runId of this.activeRuns.keys()) {
79
+ const result = await this.cancelRun(runId);
80
+ results.push(result);
81
+ }
82
+ return results;
83
+ }
84
+ getActiveRuns() {
85
+ return Array.from(this.activeRuns.values());
86
+ }
87
+ getRunStatus(runId) {
88
+ return this.activeRuns.get(runId) || null;
89
+ }
90
+ async gracefulShutdown(run) {
91
+ const promises = [];
92
+ // Send SIGTERM to all child processes
93
+ for (const child of run.children) {
94
+ if (child.pid && !child.killed) {
95
+ promises.push(this.terminateProcess(child));
96
+ }
97
+ }
98
+ // Wait for all processes to terminate
99
+ await Promise.all(promises);
100
+ // Clean up Docker containers if any
101
+ await this.cleanupDockerContainers(run);
102
+ }
103
+ async terminateProcess(process) {
104
+ return new Promise((resolve) => {
105
+ if (!process.pid || process.killed) {
106
+ resolve();
107
+ return;
108
+ }
109
+ let terminated = false;
110
+ // Set up exit handler
111
+ const onExit = () => {
112
+ if (!terminated) {
113
+ terminated = true;
114
+ resolve();
115
+ }
116
+ };
117
+ process.on('exit', onExit);
118
+ process.on('error', onExit);
119
+ // Send SIGTERM
120
+ try {
121
+ process.kill('SIGTERM');
122
+ }
123
+ catch {
124
+ // Process might already be dead
125
+ onExit();
126
+ return;
127
+ }
128
+ // Force kill after timeout
129
+ setTimeout(() => {
130
+ if (!terminated && process.pid && !process.killed) {
131
+ try {
132
+ process.kill('SIGKILL');
133
+ }
134
+ catch {
135
+ // Process might already be dead
136
+ }
137
+ onExit();
138
+ }
139
+ }, this.gracefulTimeout);
140
+ });
141
+ }
142
+ async cleanupDockerContainers(run) {
143
+ try {
144
+ // Check if docker-compose was used
145
+ if (run.command.includes('docker-compose') || run.command.includes('docker compose')) {
146
+ const composeDown = spawn('docker', ['compose', 'down'], {
147
+ stdio: 'pipe',
148
+ timeout: 30000
149
+ });
150
+ await new Promise((resolve) => {
151
+ composeDown.on('exit', () => resolve());
152
+ composeDown.on('error', () => resolve()); // Continue even if cleanup fails
153
+ setTimeout(() => {
154
+ composeDown.kill();
155
+ resolve();
156
+ }, 30000);
157
+ });
158
+ }
159
+ // Clean up any QA360-related containers
160
+ const cleanup = spawn('docker', [
161
+ 'ps', '-q', '--filter', 'label=qa360.run-id=' + run.runId
162
+ ], { stdio: 'pipe' });
163
+ cleanup.stdout.on('data', (data) => {
164
+ const containerIds = data.toString().trim().split('\n').filter(Boolean);
165
+ for (const containerId of containerIds) {
166
+ spawn('docker', ['stop', containerId], { stdio: 'ignore' });
167
+ }
168
+ });
169
+ }
170
+ catch {
171
+ // Docker cleanup is best effort
172
+ }
173
+ }
174
+ completeRun(runId) {
175
+ const run = this.activeRuns.get(runId);
176
+ if (run && run.status === 'running') {
177
+ run.status = 'completed';
178
+ this.emit('run-completed', run);
179
+ }
180
+ }
181
+ setupSignalHandlers() {
182
+ const handleShutdown = async (signal) => {
183
+ console.log(`\nReceived ${signal}. Gracefully shutting down...`);
184
+ try {
185
+ await this.cancelAllRuns();
186
+ console.log('All runs cancelled successfully');
187
+ }
188
+ catch (error) {
189
+ console.error('Error during shutdown:', error);
190
+ }
191
+ process.exit(0);
192
+ };
193
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
194
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
195
+ // Handle uncaught exceptions
196
+ process.on('uncaughtException', async (error) => {
197
+ console.error('Uncaught exception:', error);
198
+ await this.cancelAllRuns();
199
+ process.exit(1);
200
+ });
201
+ process.on('unhandledRejection', async (reason) => {
202
+ console.error('Unhandled rejection:', reason);
203
+ await this.cancelAllRuns();
204
+ process.exit(1);
205
+ });
206
+ }
207
+ dispose() {
208
+ process.removeAllListeners('SIGINT');
209
+ process.removeAllListeners('SIGTERM');
210
+ process.removeAllListeners('uncaughtException');
211
+ process.removeAllListeners('unhandledRejection');
212
+ }
213
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * QA360 Core Observability Server
3
+ * Endpoints: /health, /diag, /metrics, /cancel
4
+ */
5
+ export interface ServeConfig {
6
+ port: number;
7
+ host?: string;
8
+ metrics?: boolean;
9
+ readinessCheck?: boolean;
10
+ verbose?: boolean;
11
+ }
12
+ export interface ServeResponse {
13
+ status: number;
14
+ headers: Record<string, string>;
15
+ body: string;
16
+ }
17
+ export declare class QA360Server {
18
+ private config;
19
+ private server?;
20
+ private healthChecker;
21
+ private diagnosticsCollector;
22
+ private metricsCollector;
23
+ private processManager;
24
+ private redactor;
25
+ private startTime;
26
+ constructor(config: ServeConfig);
27
+ start(): Promise<void>;
28
+ stop(): Promise<void>;
29
+ private handleRequest;
30
+ private routeRequest;
31
+ private handleHealth;
32
+ private handleDiagnostics;
33
+ private handleMetrics;
34
+ private handleCancel;
35
+ private log;
36
+ }
37
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/core/serve/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,WAAW;IASV,OAAO,CAAC,MAAM;IAR1B,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,SAAS,CAAS;gBAEN,MAAM,EAAE,WAAW;IASjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAab,aAAa;YAmCb,YAAY;YAkCZ,YAAY;YAcZ,iBAAiB;YAUjB,aAAa;YAkBb,YAAY;IAsB1B,OAAO,CAAC,GAAG;CAaZ"}