claude-git-hooks 2.35.3 → 2.44.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.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Features:
6
6
  * - Dynamic help from command registry (no args, --help, -h)
7
- * - AI help: routes questions to Claude with CLAUDE.md as knowledge base
7
+ * - AI help: two-pass librarian navigating .library/ for answers
8
8
  * - Report issue: interactive issue creation guided by Claude
9
9
  */
10
10
 
@@ -160,59 +160,130 @@ function printAiResponse(response, source) {
160
160
  console.log('');
161
161
  }
162
162
 
163
+ /** Package root directory — resolved once from __dirname */
164
+ const _packageRoot = path.join(__dirname, '..', '..');
165
+
163
166
  /**
164
- * Read CLAUDE.md from package root
165
- * Why: CLAUDE.md is the primary knowledge source shipped with the npm package
167
+ * Catalog directories within .library/ to auto-discover
168
+ * Why: These contain index and shelf files that form the navigational catalog.
169
+ * books/ is excluded — those are fetched on-demand in Pass 2.
170
+ */
171
+ const _CATALOG_DIRS = ['by-code', 'by-domain', 'by-task-type'];
172
+
173
+ /**
174
+ * Read a single file from package root, returning null on failure
166
175
  *
167
- * @returns {Promise<string|null>} CLAUDE.md content or null
176
+ * @param {string} relativePath - Path relative to package root
177
+ * @returns {Promise<string|null>} File content or null
168
178
  */
169
- const readClaudeMd = async () => {
170
- const claudeMdPath = path.join(__dirname, '..', '..', 'CLAUDE.md');
179
+ const _readPackageFile = async (relativePath) => {
171
180
  try {
172
- const content = await fs.readFile(claudeMdPath, 'utf8');
173
- logger.debug('help - readClaudeMd', 'CLAUDE.md loaded', { length: content.length });
174
- return content;
175
- } catch (error) {
176
- logger.debug('help - readClaudeMd', 'CLAUDE.md not found', {
177
- path: claudeMdPath,
178
- error: error.message
179
- });
181
+ return await fs.readFile(path.join(_packageRoot, relativePath), 'utf8');
182
+ } catch {
183
+ return null;
184
+ }
185
+ };
186
+
187
+ /**
188
+ * Read the project catalog from .library/ and CLAUDE.md
189
+ * Why: The catalog provides navigational context for the AI librarian (Pass 1).
190
+ * Auto-discovers index/shelf files so the catalog stays current as .library/ grows.
191
+ *
192
+ * Reads:
193
+ * - CLAUDE.md (global rules)
194
+ * - .library/index.md (repo identity, capabilities)
195
+ * - .library/conventions.md (coding standards)
196
+ * - .library/by-code/*.md (source-path shelves)
197
+ * - .library/by-domain/*.md (workflow reading lists)
198
+ * - .library/by-task-type/*.md (task guidance)
199
+ *
200
+ * @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
201
+ */
202
+ const readLibraryCatalog = async () => {
203
+ const sections = [];
204
+
205
+ // Fixed files: CLAUDE.md + core library files
206
+ const fixedFiles = [
207
+ { label: 'CLAUDE.md', path: 'CLAUDE.md' },
208
+ { label: '.library/index.md', path: '.library/index.md' },
209
+ { label: '.library/conventions.md', path: '.library/conventions.md' }
210
+ ];
211
+
212
+ for (const file of fixedFiles) {
213
+ const content = await _readPackageFile(file.path);
214
+ if (content) {
215
+ sections.push(`--- ${file.label} ---\n${content}`);
216
+ }
217
+ }
218
+
219
+ // Auto-discover .md files in each catalog directory
220
+ for (const dir of _CATALOG_DIRS) {
221
+ const dirPath = path.join(_packageRoot, '.library', dir);
222
+ let entries;
223
+ try {
224
+ entries = await fs.readdir(dirPath);
225
+ } catch {
226
+ logger.debug('help - readLibraryCatalog', `Could not read .library/${dir}/`);
227
+ continue;
228
+ }
229
+
230
+ const mdFiles = entries.filter((f) => f.endsWith('.md')).sort();
231
+ for (const file of mdFiles) {
232
+ const relativePath = `.library/${dir}/${file}`;
233
+ const content = await _readPackageFile(relativePath);
234
+ if (content) {
235
+ sections.push(`--- ${relativePath} ---\n${content}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ if (sections.length === 0) {
241
+ logger.debug('help - readLibraryCatalog', 'No catalog files found');
180
242
  return null;
181
243
  }
244
+
245
+ const catalog = sections.join('\n\n');
246
+ logger.debug('help - readLibraryCatalog', 'Catalog loaded', {
247
+ sections: sections.length,
248
+ length: catalog.length
249
+ });
250
+ return catalog;
182
251
  };
183
252
 
184
253
  /**
185
- * AI-powered help: routes question to Claude with CLAUDE.md context
186
- * Why: Provides intelligent answers about claude-hooks based on documentation
254
+ * AI-powered help: two-pass librarian navigating .library/
255
+ * Why: Provides intelligent answers by reading the project catalog, then
256
+ * fetching specific books from local disk only when deeper detail is needed.
187
257
  *
188
258
  * Flow:
189
- * 1. Read CLAUDE.md from package root
190
- * 2. Load HELP_QUERY.md template
191
- * 3. Call Claude with 30s timeout
192
- * 4. If NEED_MORE_CONTEXT: fetch referenced files from GitHub, re-query
193
- * 5. Display response
194
- * 6. On any error: fall back to static help
259
+ * 1. Read catalog from .library/ (auto-discovered indexes + shelves)
260
+ * 2. Pass 1: Send catalog to Claude via HELP_NAVIGATE.md
261
+ * 3. If ANSWER: display directly (1 Claude call)
262
+ * 4. If BOOKS: read requested books from local disk, send to HELP_QUERY.md (2 Claude calls)
263
+ * 5. On any error: fall back to static help
195
264
  *
196
265
  * @param {string} question - User's natural language question
197
266
  */
198
267
  async function runAiHelp(question) {
199
268
  try {
200
- const claudeMdContent = await readClaudeMd();
201
- if (!claudeMdContent) {
269
+ const catalog = await readLibraryCatalog();
270
+ if (!catalog) {
202
271
  logger.debug(
203
272
  'help - runAiHelp',
204
- 'CLAUDE.md not available, falling back to static help'
273
+ 'Catalog not available, falling back to static help'
205
274
  );
206
275
  showStaticHelp();
207
276
  return;
208
277
  }
209
278
 
210
- const prompt = await loadPrompt('HELP_QUERY.md', {
211
- DOCUMENTATION: claudeMdContent,
279
+ const prompt = await loadPrompt('HELP_NAVIGATE.md', {
280
+ CATALOG: catalog,
212
281
  QUESTION: question
213
282
  });
214
283
 
215
- logger.debug('help - runAiHelp', 'Querying Claude', { questionLength: question.length });
284
+ logger.debug('help - runAiHelp', 'Querying Claude (Pass 1)', {
285
+ questionLength: question.length
286
+ });
216
287
  console.log('\nSearching documentation...\n');
217
288
 
218
289
  const pkg = getPackageJson();
@@ -221,95 +292,98 @@ async function runAiHelp(question) {
221
292
  const response = await executeClaudeWithRetry(prompt, { timeout: 60000 });
222
293
  const trimmedResponse = response.trim();
223
294
 
224
- // Check for NEED_MORE_CONTEXT second pass
225
- const needMoreLine = trimmedResponse
226
- .split('\n')
227
- .find((l) => l.includes('NEED_MORE_CONTEXT'));
228
- if (needMoreLine) {
229
- const enrichedResponse = await handleNeedMoreContext(
230
- needMoreLine,
295
+ // Check for BOOKS: second pass
296
+ const booksLine = trimmedResponse.split('\n').find((l) => l.startsWith('BOOKS:'));
297
+ if (booksLine) {
298
+ const enrichedAnswer = await readAndAnswerWithBooks(
299
+ booksLine,
231
300
  question,
232
- claudeMdContent
301
+ catalog
233
302
  );
234
- if (enrichedResponse) {
235
- printAiResponse(enrichedResponse, `${localVersion} + source`);
236
- return;
237
- }
238
- // Enrichment failed: show first-pass answer with marker line stripped
239
- const cleanResponse = trimmedResponse
240
- .split('\n')
241
- .filter((l) => !l.includes('NEED_MORE_CONTEXT'))
242
- .join('\n')
243
- .trim();
244
- if (cleanResponse) {
245
- printAiResponse(cleanResponse, localVersion);
303
+ if (enrichedAnswer) {
304
+ printAiResponse(enrichedAnswer, `${localVersion} + source`);
246
305
  return;
247
306
  }
307
+ // Books not readable — avoid surfacing raw BOOKS: line to the user
308
+ showStaticHelp();
309
+ return;
248
310
  }
249
311
 
250
- printAiResponse(trimmedResponse, localVersion);
251
- } catch (error) {
312
+ // ANSWER: response or fallback — strip the "ANSWER:" prefix if present
313
+ const answerLine = trimmedResponse.indexOf('ANSWER:');
314
+ const answer =
315
+ answerLine !== -1
316
+ ? trimmedResponse.substring(answerLine + 'ANSWER:'.length).trim()
317
+ : trimmedResponse;
318
+
319
+ printAiResponse(answer, localVersion);
320
+ } catch (err) {
252
321
  logger.debug('help - runAiHelp', 'AI help failed, falling back to static help', {
253
- error: error.message
322
+ error: err.message
254
323
  });
255
324
  showStaticHelp();
256
325
  }
257
326
  }
258
327
 
259
328
  /**
260
- * Handle NEED_MORE_CONTEXT response by fetching referenced files from GitHub
329
+ * Pass 2: Read requested books from local disk and answer with enriched context
261
330
  *
262
- * @param {string} needMoreLine - The line containing NEED_MORE_CONTEXT marker
331
+ * @param {string} booksLine - The line starting with "BOOKS:" containing comma-separated paths
263
332
  * @param {string} question - Original user question
264
- * @param {string} claudeMdContent - Original CLAUDE.md content
265
- * @returns {Promise<string|null>} Enriched response or null on failure
333
+ * @param {string} catalog - The catalog content from Pass 1
334
+ * @returns {Promise<string|null>} Enriched answer or null on failure
266
335
  */
267
- async function handleNeedMoreContext(needMoreLine, question, claudeMdContent) {
336
+ async function readAndAnswerWithBooks(booksLine, question, catalog) {
268
337
  try {
269
- // Parse file paths from line: "NEED_MORE_CONTEXT: file1.js, file2.js"
270
- const pathsPart = needMoreLine.replace('NEED_MORE_CONTEXT', '').replace(':', '').trim();
271
- const filePaths = pathsPart
338
+ const pathsPart = booksLine.replace('BOOKS:', '').trim();
339
+ const bookPaths = pathsPart
272
340
  .split(',')
273
341
  .map((p) => p.trim())
274
342
  .filter(Boolean);
275
343
 
276
- if (filePaths.length === 0) {
277
- logger.debug('help - handleNeedMoreContext', 'No file paths in NEED_MORE_CONTEXT');
344
+ if (bookPaths.length === 0) {
345
+ logger.debug('help - readAndAnswerWithBooks', 'No book paths in BOOKS line');
278
346
  return null;
279
347
  }
280
348
 
281
- logger.debug('help - handleNeedMoreContext', 'Fetching additional files', { filePaths });
282
-
283
- const { owner, repo } = getSourceRepo();
284
-
285
- // Fetch files in parallel (no ref = default branch, avoids 404 on unreleased tags)
286
- const fetchResults = await Promise.all(
287
- filePaths.map(async (filePath) => {
288
- const content = await fetchFileContent(owner, repo, filePath);
289
- return { filePath, content };
349
+ logger.debug('help - readAndAnswerWithBooks', 'Reading books from disk', { bookPaths });
350
+
351
+ // Read books from local filesystem in parallel
352
+ // Contain LLM-sourced paths to the package root to prevent traversal
353
+ const readResults = await Promise.all(
354
+ bookPaths.map(async (bookPath) => {
355
+ const resolvedPath = path.resolve(_packageRoot, bookPath);
356
+ if (!resolvedPath.startsWith(_packageRoot + path.sep)) {
357
+ logger.debug('help - readAndAnswerWithBooks', 'Path traversal blocked', { bookPath });
358
+ return { bookPath, content: null };
359
+ }
360
+ const content = await _readPackageFile(bookPath);
361
+ return { bookPath, content };
290
362
  })
291
363
  );
292
364
 
293
- // Build enriched documentation
294
- const additionalContent = fetchResults
365
+ const bookContent = readResults
295
366
  .filter((r) => r.content !== null)
296
- .map((r) => `\n--- Source: ${r.filePath} ---\n${r.content}`)
297
- .join('\n');
367
+ .map((r) => `--- ${r.bookPath} ---\n${r.content}`)
368
+ .join('\n\n');
298
369
 
299
- if (!additionalContent) {
300
- logger.debug('help - handleNeedMoreContext', 'No additional content fetched');
370
+ if (!bookContent) {
371
+ logger.debug('help - readAndAnswerWithBooks', 'No book content read from disk');
301
372
  return null;
302
373
  }
303
374
 
304
375
  const enrichedPrompt = await loadPrompt('HELP_QUERY.md', {
305
- DOCUMENTATION: `${claudeMdContent}\n\n=== ADDITIONAL SOURCE FILES ===\n${additionalContent}`,
376
+ CATALOG: catalog,
377
+ BOOKS: bookContent,
306
378
  QUESTION: question
307
379
  });
308
380
 
309
381
  const enrichedResponse = await executeClaudeWithRetry(enrichedPrompt, { timeout: 60000 });
310
382
  return enrichedResponse.trim();
311
- } catch (error) {
312
- logger.debug('help - handleNeedMoreContext', 'Enrichment failed', { error: error.message });
383
+ } catch (err) {
384
+ logger.debug('help - readAndAnswerWithBooks', 'Book enrichment failed', {
385
+ error: err.message
386
+ });
313
387
  return null;
314
388
  }
315
389
  }
@@ -371,8 +445,8 @@ async function runReportIssue() {
371
445
  return;
372
446
  }
373
447
 
374
- // Step 5: Read CLAUDE.md for project context
375
- const claudeMdContent = await readClaudeMd();
448
+ // Step 5: Read CLAUDE.md for project context (report-issue only needs basic context)
449
+ const claudeMdContent = await _readPackageFile('CLAUDE.md');
376
450
 
377
451
  // Step 6: Generate questions from template using Claude
378
452
  const questionsPrompt = await loadPrompt('HELP_REPORT_ISSUE.md', {
@@ -19,6 +19,7 @@ import https from 'https';
19
19
  import { fileURLToPath } from 'url';
20
20
  import { dirname } from 'path';
21
21
  import { getRunningWSLDistros } from '../utils/claude-client.js';
22
+ import logger from '../utils/logger.js';
22
23
 
23
24
  // Why: ES6 modules don't have __dirname, need to recreate it
24
25
  const __filename = fileURLToPath(import.meta.url);
@@ -38,20 +39,24 @@ export function log(message, color = 'reset') {
38
39
  }
39
40
 
40
41
  export function error(message) {
41
- console.error(`${colors.red}❌ ${message}${colors.reset}`);
42
+ logger.error('helpers', message);
43
+ }
44
+
45
+ export function fatal(message) {
46
+ logger.error('helpers', message);
42
47
  process.exit(1);
43
48
  }
44
49
 
45
50
  export function success(message) {
46
- log(`✅ ${message}`, 'green');
51
+ logger.success(message);
47
52
  }
48
53
 
49
54
  export function info(message) {
50
- log(`ℹ️ ${message}`, 'blue');
55
+ logger.info(message);
51
56
  }
52
57
 
53
58
  export function warning(message) {
54
- log(`⚠️ ${message}`, 'yellow');
59
+ logger.warning(message);
55
60
  }
56
61
 
57
62
  /**
@@ -5,7 +5,7 @@
5
5
 
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import { error, success, info, warning, checkGitRepo, getGitHooksPath } from './helpers.js';
8
+ import { error, fatal, success, info, warning, checkGitRepo, getGitHooksPath } from './helpers.js';
9
9
  import { removeCompletions } from './install.js';
10
10
 
11
11
  /**
@@ -14,7 +14,7 @@ import { removeCompletions } from './install.js';
14
14
  */
15
15
  export function runEnable(hookName) {
16
16
  if (!checkGitRepo()) {
17
- error('You are not in a Git repository.');
17
+ fatal('You are not in a Git repository.');
18
18
  }
19
19
 
20
20
  const hooksDir = getGitHooksPath();
@@ -41,7 +41,7 @@ export function runEnable(hookName) {
41
41
  */
42
42
  export function runDisable(hookName) {
43
43
  if (!checkGitRepo()) {
44
- error('You are not in a Git repository.');
44
+ fatal('You are not in a Git repository.');
45
45
  }
46
46
 
47
47
  const hooksDir = getGitHooksPath();
@@ -67,7 +67,7 @@ export function runDisable(hookName) {
67
67
  */
68
68
  export function runStatus() {
69
69
  if (!checkGitRepo()) {
70
- error('You are not in a Git repository.');
70
+ fatal('You are not in a Git repository.');
71
71
  }
72
72
 
73
73
  info('Claude Git Hooks status:\n');
@@ -128,7 +128,7 @@ export function runStatus() {
128
128
  */
129
129
  export function runUninstall() {
130
130
  if (!checkGitRepo()) {
131
- error('You are not in a Git repository.');
131
+ fatal('You are not in a Git repository.');
132
132
  }
133
133
 
134
134
  info('Uninstalling Claude Git Hooks...');
@@ -18,6 +18,7 @@ import os from 'os';
18
18
  import readline from 'readline';
19
19
  import {
20
20
  error,
21
+ fatal,
21
22
  success,
22
23
  info,
23
24
  warning,
@@ -176,7 +177,7 @@ async function checkAndInstallDependencies(skipAuth = false) {
176
177
  const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
177
178
  success(`Node.js ${nodeVersion}`);
178
179
  } catch (e) {
179
- error('Node.js is not installed. Install Node.js and try again.');
180
+ fatal('Node.js is not installed. Install Node.js and try again.');
180
181
  }
181
182
 
182
183
  // Check npm
@@ -184,7 +185,7 @@ async function checkAndInstallDependencies(skipAuth = false) {
184
185
  const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
185
186
  success(`npm ${npmVersion}`);
186
187
  } catch (e) {
187
- error('npm is not installed.');
188
+ fatal('npm is not installed.');
188
189
  }
189
190
 
190
191
  // Check Maven (optional — needed for backend/fullstack presets with Spotless)
@@ -205,7 +206,7 @@ async function checkAndInstallDependencies(skipAuth = false) {
205
206
  const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
206
207
  success(`${gitVersion}`);
207
208
  } catch (e) {
208
- error('Git is not installed. Install Git and try again.');
209
+ fatal('Git is not installed. Install Git and try again.');
209
210
  }
210
211
 
211
212
  // v2.0.0+: Unix tools (sed, awk, grep, etc.) no longer needed (pure Node.js implementation)
@@ -387,20 +388,30 @@ async function autoMigrateConfig(newConfigPath, backupConfigPath) {
387
388
  */
388
389
  export async function runInstall(args) {
389
390
  if (!checkGitRepo()) {
390
- error(
391
+ fatal(
391
392
  'You are not in a Git repository. Please run this command from the root of a repository.'
392
393
  );
393
394
  }
394
395
 
395
396
  const isForce = args.includes('--force');
396
397
  const skipAuth = args.includes('--skip-auth');
398
+ const isHeadless = args.includes('--headless');
399
+ const verifySdk = args.includes('--verify-sdk');
397
400
 
398
- // Check for updates (unless --skip-auth flag)
399
- if (!skipAuth && !isForce) {
401
+ if (skipAuth) {
402
+ warning('--skip-auth is deprecated. Use --headless for full CI/container mode.');
403
+ }
404
+
405
+ const skipVersionCheck = skipAuth || isHeadless;
406
+
407
+ // Check for updates (unless --skip-auth, --headless, or --force flag)
408
+ if (!skipVersionCheck && !isForce) {
400
409
  await checkVersionAndPromptUpdate();
401
410
  }
402
411
 
403
- if (isForce) {
412
+ if (isHeadless) {
413
+ info('Installing Claude Git Hooks (headless mode)...');
414
+ } else if (isForce) {
404
415
  info('Installing Claude Git Hooks (force mode)...');
405
416
  } else {
406
417
  info('Installing Claude Git Hooks...');
@@ -408,7 +419,12 @@ export async function runInstall(args) {
408
419
 
409
420
  // v2.0.0+: No sudo needed (pure Node.js, no system packages required)
410
421
  // Check dependencies
411
- await checkAndInstallDependencies(skipAuth);
422
+ if (!isHeadless) {
423
+ await checkAndInstallDependencies(skipAuth);
424
+ } else {
425
+ // Headless: skip Claude CLI/auth checks and Maven — CI manages its own deps
426
+ info('Skipping dependency checks (headless mode)');
427
+ }
412
428
 
413
429
  const templatesPath = getTemplatesPath();
414
430
  const hooksPath = getGitHooksPath();
@@ -658,33 +674,66 @@ export async function runInstall(args) {
658
674
  info('settings.local.json created (add your GitHub token here)');
659
675
  }
660
676
 
661
- // Configure Git
662
- configureGit();
677
+ // Configure Git — skipped in headless to avoid mutating CI git config
678
+ if (!isHeadless) {
679
+ configureGit();
680
+ }
663
681
 
664
- // Update .gitignore
665
- updateGitignore();
682
+ // Update .gitignore — skipped in headless: CI repos manage .gitignore explicitly
683
+ if (!isHeadless) {
684
+ updateGitignore();
685
+ }
666
686
 
667
- // Install shell completions
668
- installCompletions();
687
+ // Install shell completions — skipped in headless: shell completions are useless in CI
688
+ if (!isHeadless) {
689
+ installCompletions();
690
+ }
669
691
 
670
- // Check linter toolchain availability for the selected preset
671
- try {
672
- const config = await getConfig();
673
- const presetName = config.preset || 'default';
674
- if (config.linting?.enabled !== false) {
675
- const { checkLinterAvailability } = await import('../utils/linter-runner.js');
676
- await checkLinterAvailability(presetName);
692
+ // Check linter toolchain availability skipped in headless: linter availability is a CI concern
693
+ if (!isHeadless) {
694
+ try {
695
+ const config = await getConfig();
696
+ const presetName = config.preset || 'default';
697
+ if (config.linting?.enabled !== false) {
698
+ const { checkLinterAvailability } = await import('../utils/linter-runner.js');
699
+ await checkLinterAvailability(presetName);
700
+ }
701
+ } catch {
702
+ // Non-fatal — linter check failure should not block installation
677
703
  }
678
- } catch {
679
- // Non-fatal — linter check failure should not block installation
680
704
  }
681
705
 
682
- success('Claude Git Hooks installed successfully! 🎉');
683
- console.log('\nRun claude-hooks --help to see all available commands.');
706
+ if (isHeadless) {
707
+ success('claude-hooks installed (headless mode).');
708
+ } else {
709
+ success('Claude Git Hooks installed successfully! 🎉');
710
+ console.log('\nRun claude-hooks --help to see all available commands.');
711
+
712
+ // Run GitHub token setup
713
+ console.log('');
714
+ await runSetupGitHub();
715
+ }
684
716
 
685
- // Run GitHub token setup
686
- console.log('');
687
- await runSetupGitHub();
717
+ // Optional SDK verification — opt-in, never automatic
718
+ if (verifySdk) {
719
+ info('Verifying SDK connectivity (1-token ping)...');
720
+ try {
721
+ const { verifySDKConnection } = await import('../utils/claude-client.js');
722
+ const result = await verifySDKConnection();
723
+ if (result.ok) {
724
+ success(`SDK verified: ${result.usage.input_tokens} in / ${result.usage.output_tokens} out tokens.`);
725
+ } else {
726
+ throw new Error(result.error);
727
+ }
728
+ } catch (sdkErr) {
729
+ if (isHeadless) {
730
+ error(`SDK verification failed: ${sdkErr.message}`);
731
+ process.exit(1);
732
+ } else {
733
+ warning(`SDK verification failed: ${sdkErr.message}. Install completed but SDK is not reachable.`);
734
+ }
735
+ }
736
+ }
688
737
  }
689
738
 
690
739
  // ── Shell completion generation ──────────────────────────────────────────────