@k2works/claude-code-booster 1.11.0 → 1.13.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.
@@ -0,0 +1,726 @@
1
+ 'use strict';
2
+
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import readline from 'readline';
6
+ import { execSync } from 'child_process';
7
+ import { cleanDockerEnv } from './shared.js';
8
+
9
+ // ============================================
10
+ // 設定
11
+ // ============================================
12
+
13
+ /**
14
+ * プロジェクト定義を取得
15
+ * sonarqube.config.json から読み込み、なければデフォルトを返す
16
+ *
17
+ * sonarqube.config.json のフォーマット:
18
+ * {
19
+ * "projects": [
20
+ * { "name": "backend", "label": "Backend", "projectKey": "my-backend", "scanType": "sonar-scanner", "srcDir": "apps/backend" },
21
+ * { "name": "frontend", "label": "Frontend", "projectKey": "my-frontend", "scanType": "sonar-scanner", "srcDir": "apps/frontend" }
22
+ * ]
23
+ * }
24
+ *
25
+ * scanType: "sonar-scanner" (npx sonarqube-scanner) | "sbt" (sbt sonarScan) | "maven" (mvn sonar:sonar) | "gradle" (gradle sonar)
26
+ */
27
+ function loadProjects() {
28
+ const configPath = path.join(process.cwd(), 'sonarqube.config.json');
29
+ if (fs.existsSync(configPath)) {
30
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
31
+ return config.projects || [];
32
+ }
33
+ // デフォルト: 単一プロジェクト(sonar-scanner)
34
+ return [
35
+ {
36
+ name: 'app',
37
+ label: 'Application',
38
+ projectKey: process.env.SONAR_PROJECT_KEY || 'my-project',
39
+ scanType: 'sonar-scanner',
40
+ srcDir: '.',
41
+ },
42
+ ];
43
+ }
44
+
45
+ /** SonarQube ポート */
46
+ function sonarPort() {
47
+ return process.env.LOCAL_SONAR_PORT || '9000';
48
+ }
49
+
50
+ /** DB パスワード */
51
+ function sonarDbPassword() {
52
+ return process.env.LOCAL_SONAR_DB_PASSWORD || 'sonarqube_password';
53
+ }
54
+
55
+ /** SonarQube ホスト URL */
56
+ function sonarHostUrl() {
57
+ return process.env.SONAR_HOST_URL || `http://localhost:${sonarPort()}`;
58
+ }
59
+
60
+ /** docker-compose.yml のディレクトリ */
61
+ function composeDir() {
62
+ return path.join(process.cwd(), 'ops', 'docker', 'sonarqube-local');
63
+ }
64
+
65
+ // ============================================
66
+ // ヘルパー関数
67
+ // ============================================
68
+
69
+ /**
70
+ * ローカルコマンドを実行
71
+ * @param {string} command - 実行するコマンド
72
+ * @param {Object} [opts] - オプション
73
+ * @param {boolean} [opts.ignoreError] - エラーを無視するか
74
+ * @param {string} [opts.cwd] - 作業ディレクトリ
75
+ */
76
+ function localExec(command, opts = {}) {
77
+ try {
78
+ execSync(command, {
79
+ stdio: 'inherit',
80
+ shell: true,
81
+ env: cleanDockerEnv(),
82
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
83
+ });
84
+ } catch (error) {
85
+ if (!opts.ignoreError) {
86
+ throw error;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * docker compose コマンドを構築・実行
93
+ * @param {string} subcommand - docker compose サブコマンド
94
+ * @param {Object} [opts] - オプション
95
+ */
96
+ function dockerCompose(subcommand, opts = {}) {
97
+ const dir = composeDir();
98
+ localExec(`docker compose -f "${dir}/docker-compose.yml" ${subcommand}`, opts);
99
+ }
100
+
101
+ /**
102
+ * 対話的な確認プロンプト
103
+ * @param {string} question - 質問文
104
+ * @returns {Promise<boolean>}
105
+ */
106
+ function confirm(question) {
107
+ return new Promise((resolve) => {
108
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
109
+ rl.question(question, (answer) => {
110
+ rl.close();
111
+ resolve(answer.trim().toLowerCase() === 'y');
112
+ });
113
+ });
114
+ }
115
+
116
+ /**
117
+ * docker-compose.yml の内容を生成
118
+ * @returns {string} YAML 文字列
119
+ */
120
+ function generateSonarComposeContent() {
121
+ const dbPassword = sonarDbPassword();
122
+ const port = sonarPort();
123
+
124
+ return `services:
125
+ sonarqube:
126
+ image: sonarqube:community
127
+ container_name: sonarqube
128
+ restart: unless-stopped
129
+ depends_on:
130
+ sonarqube-db:
131
+ condition: service_healthy
132
+ ports:
133
+ - "${port}:9000"
134
+ environment:
135
+ SONAR_JDBC_URL: jdbc:postgresql://sonarqube-db:5432/sonarqube
136
+ SONAR_JDBC_USERNAME: sonarqube
137
+ SONAR_JDBC_PASSWORD: ${dbPassword}
138
+ SONAR_CE_JAVAOPTS: "-Xmx512m -Xms512m"
139
+ SONAR_WEB_JAVAOPTS: "-Xmx256m -Xms256m"
140
+ SONAR_SEARCH_JAVAOPTS: "-Xmx512m -Xms512m"
141
+ volumes:
142
+ - sonarqube_data:/opt/sonarqube/data
143
+ - sonarqube_logs:/opt/sonarqube/logs
144
+ - sonarqube_extensions:/opt/sonarqube/extensions
145
+ networks:
146
+ - sonarqube-net
147
+ healthcheck:
148
+ test: ["CMD-SHELL", "curl -f http://localhost:9000/api/system/status || exit 1"]
149
+ interval: 30s
150
+ timeout: 10s
151
+ retries: 5
152
+ start_period: 300s
153
+
154
+ sonarqube-db:
155
+ image: postgres:16-alpine
156
+ container_name: sonarqube-db
157
+ restart: unless-stopped
158
+ environment:
159
+ POSTGRES_DB: sonarqube
160
+ POSTGRES_USER: sonarqube
161
+ POSTGRES_PASSWORD: ${dbPassword}
162
+ volumes:
163
+ - sonarqube_postgresql:/var/lib/postgresql/data
164
+ networks:
165
+ - sonarqube-net
166
+ healthcheck:
167
+ test: ["CMD-SHELL", "pg_isready -U sonarqube"]
168
+ interval: 10s
169
+ timeout: 5s
170
+ retries: 5
171
+
172
+ networks:
173
+ sonarqube-net:
174
+ driver: bridge
175
+
176
+ volumes:
177
+ sonarqube_data:
178
+ sonarqube_logs:
179
+ sonarqube_extensions:
180
+ sonarqube_postgresql:
181
+ `;
182
+ }
183
+
184
+ /**
185
+ * 指定ミリ秒だけ同期的にスリープ
186
+ * @param {number} ms - 待機ミリ秒
187
+ */
188
+ function sleepSync(ms) {
189
+ execSync(`node -e "setTimeout(()=>{},${ms})"`, { stdio: 'ignore' });
190
+ }
191
+
192
+ /**
193
+ * ヘルスチェック待機(SonarQube コンテナ)
194
+ */
195
+ function waitForSonarHealthy() {
196
+ console.log('ヘルスチェック待機中(最大 5 分)...');
197
+ const maxRetries = 30;
198
+ for (let i = 1; i <= maxRetries; i++) {
199
+ let status = 'not found';
200
+ try {
201
+ status = execSync(
202
+ 'docker inspect --format="{{.State.Health.Status}}" sonarqube',
203
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
204
+ ).trim().replace(/"/g, '');
205
+ } catch {
206
+ // コンテナが見つからない場合
207
+ }
208
+ if (status === 'healthy') {
209
+ console.log(' sonarqube: healthy');
210
+ return;
211
+ }
212
+ if (i === maxRetries) {
213
+ console.log(` sonarqube: timeout (status=${status})`);
214
+ return;
215
+ }
216
+ sleepSync(10000);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * SonarQube プロジェクトキーを取得
222
+ * 環境変数 SONAR_PROJECT_KEY で指定可能
223
+ * @returns {string} プロジェクトキー
224
+ */
225
+ function sonarProjectKey() {
226
+ const projects = loadProjects();
227
+ return process.env.SONAR_PROJECT_KEY || (projects.length > 0 ? projects[0].projectKey : 'my-project');
228
+ }
229
+
230
+ /**
231
+ * SonarQube API を呼び出す
232
+ * @param {string} apiPath - API パス(例: /api/qualitygates/project_status)
233
+ * @param {Object} [params] - クエリパラメータ
234
+ * @returns {Object} レスポンス JSON
235
+ */
236
+ function sonarApi(apiPath, params = {}) {
237
+ const token = requireSonarToken();
238
+ const hostUrl = sonarHostUrl();
239
+ const qs = new URLSearchParams(params).toString();
240
+ const url = `${hostUrl}${apiPath}${qs ? '?' + qs : ''}`;
241
+ const auth = Buffer.from(`${token}:`).toString('base64');
242
+ const result = execSync(
243
+ `curl -sf -H "Authorization: Basic ${auth}" "${url}"`,
244
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], shell: true, env: cleanDockerEnv() },
245
+ );
246
+ return JSON.parse(result);
247
+ }
248
+
249
+ /**
250
+ * SonarQube トークンの検証
251
+ * @returns {string} トークン
252
+ */
253
+ function requireSonarToken() {
254
+ const token = process.env.SONAR_TOKEN;
255
+ if (!token) {
256
+ throw new Error('SONAR_TOKEN を .env に設定してください');
257
+ }
258
+ return token;
259
+ }
260
+
261
+ /**
262
+ * スキャンタイプに応じたスキャンコマンドを実行
263
+ * @param {Object} project - プロジェクト定義
264
+ * @param {string} token - SonarQube トークン
265
+ * @param {string} hostUrl - SonarQube ホスト URL
266
+ */
267
+ function runScan(project, token, hostUrl) {
268
+ const cwd = path.join(process.cwd(), project.srcDir);
269
+
270
+ switch (project.scanType) {
271
+ case 'sbt':
272
+ execSync(
273
+ `sbt -Dsonar.host.url=${hostUrl} -Dsonar.token=${token} sonarScan`,
274
+ { stdio: 'inherit', cwd, shell: true, env: cleanDockerEnv() },
275
+ );
276
+ break;
277
+
278
+ case 'maven':
279
+ execSync(
280
+ `mvn sonar:sonar ` +
281
+ `-Dsonar.projectKey=${project.projectKey} ` +
282
+ `-Dsonar.projectName="${project.label}" ` +
283
+ `-Dsonar.host.url=${hostUrl} ` +
284
+ `-Dsonar.token=${token}`,
285
+ { stdio: 'inherit', cwd, shell: true, env: cleanDockerEnv() },
286
+ );
287
+ break;
288
+
289
+ case 'gradle':
290
+ execSync(
291
+ `gradle sonar ` +
292
+ `-Dsonar.projectKey=${project.projectKey} ` +
293
+ `-Dsonar.projectName="${project.label}" ` +
294
+ `-Dsonar.host.url=${hostUrl} ` +
295
+ `-Dsonar.token=${token}`,
296
+ { stdio: 'inherit', cwd, shell: true, env: cleanDockerEnv() },
297
+ );
298
+ break;
299
+
300
+ case 'sonar-scanner':
301
+ default:
302
+ execSync(
303
+ `npx sonarqube-scanner ` +
304
+ `-Dsonar.projectKey=${project.projectKey} ` +
305
+ `-Dsonar.projectName="${project.label}" ` +
306
+ `-Dsonar.sources=src ` +
307
+ `-Dsonar.tests=src ` +
308
+ `-Dsonar.test.inclusions="**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx" ` +
309
+ `-Dsonar.host.url=${hostUrl} ` +
310
+ `-Dsonar.token=${token}`,
311
+ { stdio: 'inherit', cwd, env: cleanDockerEnv() },
312
+ );
313
+ break;
314
+ }
315
+ }
316
+
317
+ // ============================================
318
+ // Gulp タスク登録
319
+ // ============================================
320
+
321
+ /**
322
+ * SonarQube ローカルタスクを gulp に登録する
323
+ * @param {import('gulp').Gulp} gulp - Gulp インスタンス
324
+ */
325
+ export default function (gulp) {
326
+ // ──────────────────────────────────────────────
327
+ // 初回セットアップ
328
+ // ──────────────────────────────────────────────
329
+
330
+ gulp.task('sonar-local:setup', (done) => {
331
+ try {
332
+ const dir = composeDir();
333
+
334
+ console.log('=== SonarQube ローカル セットアップ開始 ===');
335
+
336
+ // 1. Docker 動作確認
337
+ console.log('[1/4] Docker の動作を確認...');
338
+ const devNull = process.platform === 'win32' ? 'NUL' : '/dev/null';
339
+ localExec(`docker info > ${devNull} 2>&1`);
340
+ console.log(' Docker: OK');
341
+
342
+ // 2. ディレクトリ作成
343
+ console.log('[2/4] ディレクトリを作成...');
344
+ fs.mkdirSync(dir, { recursive: true });
345
+
346
+ // 3. docker-compose.yml 配置
347
+ console.log('[3/4] docker-compose.yml を配置...');
348
+ const composePath = path.join(dir, 'docker-compose.yml');
349
+ fs.writeFileSync(composePath, generateSonarComposeContent(), 'utf8');
350
+
351
+ // 4. コンテナ起動 & ヘルスチェック待機
352
+ console.log('[4/4] コンテナを起動...');
353
+ dockerCompose('up -d');
354
+ waitForSonarHealthy();
355
+
356
+ // 完了メッセージ
357
+ const hostUrl = sonarHostUrl();
358
+ console.log('');
359
+ console.log('=== SonarQube ローカル セットアップ完了 ===');
360
+ console.log(` URL: ${hostUrl}`);
361
+ console.log(' 初期ログイン: admin / admin');
362
+ console.log('');
363
+ console.log('次のステップ:');
364
+ console.log(' 1. ブラウザで上記 URL にアクセス');
365
+ console.log(' 2. admin パスワードを変更');
366
+ console.log(' 3. 分析トークンを生成');
367
+ console.log(' 4. .env に SONAR_TOKEN=<トークン> を追加');
368
+ console.log(' 5. .env に SONAR_HOST_URL=http://localhost:9000 を追加');
369
+ done();
370
+ } catch (error) {
371
+ done(error);
372
+ }
373
+ });
374
+
375
+ // ──────────────────────────────────────────────
376
+ // コンテナ操作
377
+ // ──────────────────────────────────────────────
378
+
379
+ gulp.task('sonar-local:open', (done) => {
380
+ try {
381
+ const hostUrl = sonarHostUrl();
382
+ let command;
383
+ if (process.platform === 'win32') {
384
+ command = `start "" "${hostUrl}"`;
385
+ } else if (process.platform === 'linux') {
386
+ command = `xdg-open "${hostUrl}"`;
387
+ } else {
388
+ command = `open "${hostUrl}"`;
389
+ }
390
+ console.log(`=== SonarQube ダッシュボードを開く: ${hostUrl} ===`);
391
+ localExec(command);
392
+ done();
393
+ } catch (error) {
394
+ done(error);
395
+ }
396
+ });
397
+
398
+ gulp.task('sonar-local:start', (done) => {
399
+ try {
400
+ console.log('=== SonarQube ローカル 起動 ===');
401
+ dockerCompose('up -d');
402
+ console.log('=== SonarQube ローカル 起動完了 ===');
403
+ done();
404
+ } catch (error) {
405
+ done(error);
406
+ }
407
+ });
408
+
409
+ gulp.task('sonar-local:stop', (done) => {
410
+ try {
411
+ console.log('=== SonarQube ローカル 停止 ===');
412
+ dockerCompose('down');
413
+ console.log('=== SonarQube ローカル 停止完了 ===');
414
+ done();
415
+ } catch (error) {
416
+ done(error);
417
+ }
418
+ });
419
+
420
+ gulp.task('sonar-local:restart', (done) => {
421
+ try {
422
+ console.log('=== SonarQube ローカル 再起動 ===');
423
+ dockerCompose('restart');
424
+ console.log('=== SonarQube ローカル 再起動完了 ===');
425
+ done();
426
+ } catch (error) {
427
+ done(error);
428
+ }
429
+ });
430
+
431
+ gulp.task('sonar-local:status', (done) => {
432
+ try {
433
+ console.log('=== SonarQube ローカル コンテナ状態 ===');
434
+ dockerCompose('ps');
435
+ done();
436
+ } catch (error) {
437
+ done(error);
438
+ }
439
+ });
440
+
441
+ gulp.task('sonar-local:logs', (done) => {
442
+ try {
443
+ dockerCompose('logs --tail=50');
444
+ done();
445
+ } catch (error) {
446
+ done(error);
447
+ }
448
+ });
449
+
450
+ // ──────────────────────────────────────────────
451
+ // 環境完全削除
452
+ // ──────────────────────────────────────────────
453
+
454
+ gulp.task('sonar-local:clean', async (done) => {
455
+ try {
456
+ console.log('');
457
+ console.log('SonarQube ローカル環境を完全削除します。');
458
+ console.log(' Docker ボリューム(データ)も削除されます。');
459
+ console.log('');
460
+ const ok = await confirm('実行しますか? (y/N): ');
461
+ if (!ok) {
462
+ console.log('キャンセルしました。');
463
+ done();
464
+ return;
465
+ }
466
+
467
+ console.log('=== SonarQube ローカル クリーン開始 ===');
468
+
469
+ console.log('[1/1] Docker コンテナ・ボリュームを停止・削除...');
470
+ dockerCompose('down --rmi all -v', { ignoreError: true });
471
+
472
+ console.log('');
473
+ console.log('=== SonarQube ローカル クリーン完了 ===');
474
+ done();
475
+ } catch (error) {
476
+ done(error);
477
+ }
478
+ });
479
+
480
+ // ──────────────────────────────────────────────
481
+ // スキャン(個別プロジェクト)
482
+ // ──────────────────────────────────────────────
483
+
484
+ gulp.task('sonar-local:scan', (done) => {
485
+ try {
486
+ const token = requireSonarToken();
487
+ const hostUrl = sonarHostUrl();
488
+ const projects = loadProjects();
489
+
490
+ if (projects.length === 0) {
491
+ console.log('スキャン対象のプロジェクトが定義されていません。');
492
+ console.log('sonarqube.config.json を作成するか、環境変数 SONAR_PROJECT_KEY を設定してください。');
493
+ done();
494
+ return;
495
+ }
496
+
497
+ for (const project of projects) {
498
+ console.log(`=== ${project.label} SonarQube スキャン(ローカル) ===`);
499
+ runScan(project, token, hostUrl);
500
+ console.log(`=== ${project.label} スキャン完了 ===`);
501
+ console.log(` 結果: ${hostUrl}/dashboard?id=${project.projectKey}`);
502
+ }
503
+
504
+ done();
505
+ } catch (error) {
506
+ done(error);
507
+ }
508
+ });
509
+
510
+ // ──────────────────────────────────────────────
511
+ // Quality Gate / イシュー確認
512
+ // ──────────────────────────────────────────────
513
+
514
+ gulp.task('sonar-local:gate', (done) => {
515
+ try {
516
+ const projectKey = sonarProjectKey();
517
+ console.log('=== Quality Gate ステータス確認 ===');
518
+
519
+ const data = sonarApi('/api/qualitygates/project_status', { projectKey });
520
+ const status = data.projectStatus?.status || 'UNKNOWN';
521
+ const conditions = data.projectStatus?.conditions || [];
522
+
523
+ const icon = status === 'OK' ? 'PASS' : 'FAIL';
524
+ console.log(` Quality Gate: ${icon} (${status})`);
525
+
526
+ if (conditions.length > 0) {
527
+ console.log('');
528
+ console.log(' 条件:');
529
+ for (const c of conditions) {
530
+ const condIcon = c.status === 'OK' ? 'o' : 'x';
531
+ const actual = c.actualValue ?? '-';
532
+ const threshold = c.errorThreshold ?? '-';
533
+ console.log(` [${condIcon}] ${c.metricKey}: ${actual} (閾値: ${threshold})`);
534
+ }
535
+ }
536
+
537
+ console.log('');
538
+ if (status !== 'OK') {
539
+ console.log(' Quality Gate を通過していません。sonar-local:issues で詳細を確認してください。');
540
+ }
541
+ done();
542
+ } catch (error) {
543
+ done(error);
544
+ }
545
+ });
546
+
547
+ gulp.task('sonar-local:issues', (done) => {
548
+ try {
549
+ const projectKey = sonarProjectKey();
550
+ const hostUrl = sonarHostUrl();
551
+ console.log('=== SonarQube プロジェクトサマリー ===');
552
+
553
+ // メトリクスを取得
554
+ const metricsData = sonarApi('/api/measures/component', {
555
+ component: projectKey,
556
+ metricKeys: [
557
+ 'bugs', 'vulnerabilities', 'code_smells', 'security_hotspots',
558
+ 'coverage', 'duplicated_lines_density', 'duplicated_blocks',
559
+ 'ncloc', 'reliability_issues', 'maintainability_issues', 'security_issues',
560
+ ].join(','),
561
+ });
562
+
563
+ const measures = metricsData.component?.measures || [];
564
+ const metric = (key) => {
565
+ const m = measures.find((x) => x.metric === key);
566
+ return m?.value ?? '-';
567
+ };
568
+
569
+ console.log('');
570
+ console.log('── メトリクス ──');
571
+ console.log(` コード行数 : ${metric('ncloc')}`);
572
+ console.log(` カバレッジ : ${metric('coverage')}%`);
573
+ console.log(` 重複率 : ${metric('duplicated_lines_density')}%`);
574
+ console.log(` 重複ブロック : ${metric('duplicated_blocks')}`);
575
+ console.log(` Bug : ${metric('bugs')}`);
576
+ console.log(` Vulnerability : ${metric('vulnerabilities')}`);
577
+ console.log(` Code Smell : ${metric('code_smells')}`);
578
+ console.log(` Security Hotspot : ${metric('security_hotspots')}`);
579
+ console.log('');
580
+
581
+ // 重複コードの詳細を取得
582
+ const dupDensity = parseFloat(metric('duplicated_lines_density'));
583
+ if (dupDensity > 0) {
584
+ console.log('── 重複コード ──');
585
+ try {
586
+ const dupData = sonarApi('/api/duplications/show', {
587
+ key: projectKey,
588
+ });
589
+ const duplications = dupData.duplications || [];
590
+ const files = dupData.files || {};
591
+ if (duplications.length > 0) {
592
+ for (const dup of duplications) {
593
+ const blocks = dup.blocks || [];
594
+ const blockDescs = blocks.map((b) => {
595
+ const file = files[b._ref]
596
+ ? files[b._ref].name
597
+ : b._ref;
598
+ return ` ${file}:${b.from}-${b.from + b.size - 1} (${b.size} 行)`;
599
+ });
600
+ console.log(' 重複:');
601
+ for (const desc of blockDescs) {
602
+ console.log(` ${desc}`);
603
+ }
604
+ }
605
+ } else {
606
+ console.log(` 重複率 ${dupDensity}% — 詳細はダッシュボードを参照`);
607
+ }
608
+ } catch {
609
+ console.log(` 重複率 ${dupDensity}% — 詳細はダッシュボードを参照`);
610
+ }
611
+ console.log('');
612
+ }
613
+
614
+ // 未解決のイシューを取得(最大 500 件)
615
+ const data = sonarApi('/api/issues/search', {
616
+ componentKeys: projectKey,
617
+ resolved: 'false',
618
+ ps: '500',
619
+ });
620
+
621
+ const total = data.total || 0;
622
+ const issues = data.issues || [];
623
+
624
+ if (total === 0) {
625
+ console.log('── イシュー ──');
626
+ console.log(' 未解決のイシューはありません。');
627
+ } else {
628
+ console.log(`── イシュー (${total} 件) ──`);
629
+
630
+ // 種別ごとに集計
631
+ const byType = {};
632
+ for (const issue of issues) {
633
+ const type = issue.type || 'UNKNOWN';
634
+ if (!byType[type]) byType[type] = [];
635
+ byType[type].push(issue);
636
+ }
637
+
638
+ const typeLabels = {
639
+ BUG: 'Bug',
640
+ VULNERABILITY: 'Vulnerability',
641
+ CODE_SMELL: 'Code Smell',
642
+ SECURITY_HOTSPOT: 'Security Hotspot',
643
+ };
644
+
645
+ for (const [type, items] of Object.entries(byType)) {
646
+ const label = typeLabels[type] || type;
647
+ console.log(` ${label} (${items.length}):`);
648
+ for (const issue of items) {
649
+ const component = (issue.component || '').replace(`${projectKey}:`, '');
650
+ const line = issue.line ? `:${issue.line}` : '';
651
+ const severity = issue.severity || '';
652
+ console.log(` [${severity}] ${component}${line}`);
653
+ console.log(` ${issue.message}`);
654
+ }
655
+ }
656
+ }
657
+
658
+ console.log('');
659
+ console.log(`詳細: ${hostUrl}/dashboard?id=${projectKey}`);
660
+ done();
661
+ } catch (error) {
662
+ done(error);
663
+ }
664
+ });
665
+
666
+ // スキャン → Quality Gate 確認の一連フロー
667
+ gulp.task('sonar-local:check', gulp.series('sonar-local:scan', 'sonar-local:gate'));
668
+
669
+ // ──────────────────────────────────────────────
670
+ // ヘルプ
671
+ // ──────────────────────────────────────────────
672
+
673
+ gulp.task('sonar-local:help', (done) => {
674
+ const projects = loadProjects();
675
+ const projectList = projects.map((p) => ` - ${p.name}: ${p.label} (${p.scanType})`).join('\n');
676
+
677
+ console.log(`
678
+ SonarQube ローカル タスク一覧
679
+ ================================
680
+
681
+ セットアップ:
682
+ sonar-local:setup 初回セットアップ(ローカルに SonarQube を構築)
683
+
684
+ コンテナ操作:
685
+ sonar-local:open ダッシュボードをブラウザで開く
686
+ sonar-local:start コンテナ起動
687
+ sonar-local:stop コンテナ停止
688
+ sonar-local:restart コンテナ再起動
689
+ sonar-local:status コンテナ状態確認
690
+ sonar-local:logs ログ表示(直近 50 行)
691
+
692
+ スキャン:
693
+ sonar-local:scan 全プロジェクトのスキャン実行
694
+
695
+ 分析:
696
+ sonar-local:gate Quality Gate ステータス確認
697
+ sonar-local:issues メトリクス・イシュー・重複コード詳細表示
698
+ sonar-local:check スキャン → Quality Gate 確認の一連フロー
699
+
700
+ 管理:
701
+ sonar-local:clean 環境完全削除(確認プロンプト付き)
702
+ sonar-local:help このヘルプを表示
703
+
704
+ 登録プロジェクト:
705
+ ${projectList}
706
+
707
+ 設定ファイル:
708
+ sonarqube.config.json プロジェクト定義(プロジェクトルートに配置)
709
+
710
+ 環境変数(.env に設定):
711
+ LOCAL_SONAR_PORT SonarQube ポート(デフォルト: 9000)
712
+ LOCAL_SONAR_DB_PASSWORD DB パスワード(デフォルト: sonarqube_password)
713
+ SONAR_HOST_URL SonarQube URL(デフォルト: http://localhost:9000)
714
+ SONAR_TOKEN 分析トークン(スキャン時に必須)
715
+ SONAR_PROJECT_KEY Quality Gate / Issues 確認対象のプロジェクトキー
716
+
717
+ 典型的なフロー:
718
+ 1. sonarqube.config.json を作成(任意)
719
+ 2. npx gulp sonar-local:setup # ローカルに SonarQube を構築
720
+ 3. ブラウザで初期設定・トークン生成
721
+ 4. .env に SONAR_TOKEN を追加
722
+ 5. npx gulp sonar-local:scan # 全プロジェクトをスキャン
723
+ `);
724
+ done();
725
+ });
726
+ }