From ce9a9ee42aae7467c38c7235f4a0cd9990583250 Mon Sep 17 00:00:00 2001 From: Kinin-Code-Offical <125186556+Kinin-Code-Offical@users.noreply.github.com> Date: Mon, 22 Dec 2025 03:06:50 +0300 Subject: [PATCH 1/2] feat(p1): add support bundle command --- src/cli.ts | 2 + src/commands/support.ts | 158 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 src/commands/support.ts diff --git a/src/cli.ts b/src/cli.ts index 263ffc3..0afb05a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,6 +21,7 @@ import { authCommand } from './commands/auth.js'; import { setupCommand } from './commands/setup.js'; import { pathsCommand } from './commands/paths.js'; import { upgradeCommand } from './commands/upgrade.js'; +import { supportCommand } from './commands/support.js'; import { logger } from './core/logger.js'; const program = new Command(); @@ -51,6 +52,7 @@ program.addCommand(authCommand); program.addCommand(setupCommand); program.addCommand(pathsCommand); program.addCommand(upgradeCommand); +program.addCommand(supportCommand); program.parseAsync(process.argv).catch(err => { logger.error('Unhandled error', err); diff --git a/src/commands/support.ts b/src/commands/support.ts new file mode 100644 index 0000000..68d2e69 --- /dev/null +++ b/src/commands/support.ts @@ -0,0 +1,158 @@ +import { Command } from 'commander'; +import path from 'path'; +import fs from 'fs-extra'; +import axios from 'axios'; +import { logger } from '../core/logger.js'; +import { readConfig } from '../core/config.js'; +import { isRunning } from '../core/proxy.js'; +import { checkGcloudInstalled, getActiveAccount, checkAdc, listInstances } from '../core/gcloud.js'; +import { checkEnvironmentDetailed } from '../system/env.js'; +import { isServiceInstalled, isServiceRunning } from '../system/service.js'; +import { getEnvVar, runPs } from '../system/powershell.js'; +import { PATHS, PATHS_REASON, PATHS_SOURCE, ENV_VARS } from '../system/paths.js'; + +function formatTimestamp(): string { + return new Date().toISOString().replace(/[:.]/g, '-'); +} + +function buildPathsReport(): string { + const lines = [ + 'CloudSQLCTL Paths', + `Home: ${PATHS.HOME}`, + `Bin: ${PATHS.BIN}`, + `Logs: ${PATHS.LOGS}`, + `Config: ${PATHS.CONFIG_FILE}`, + `Proxy: ${PATHS.PROXY_EXE}`, + `Secrets: ${PATHS.SECRETS}`, + '', + `Resolution Source: ${PATHS_SOURCE}`, + `Reason: ${PATHS_REASON}`, + ]; + return `${lines.join('\n')}\n`; +} + +async function buildStatusReport(): Promise { + const processRunning = await isRunning(); + const serviceInstalled = await isServiceInstalled(); + const serviceRunning = serviceInstalled ? await isServiceRunning() : false; + const config = await readConfig(); + + const lines = [ + 'CloudSQLCTL Status', + `Service: ${serviceInstalled ? (serviceRunning ? 'RUNNING' : 'STOPPED') : 'NOT INSTALLED'}`, + `Process: ${processRunning ? 'RUNNING' : 'STOPPED'}`, + `Instance: ${config.selectedInstance || 'Unknown'}`, + `Port: ${config.proxyPort || 5432}`, + ]; + + return `${lines.join('\n')}\n`; +} + +async function buildDoctorReport(): Promise { + const lines: string[] = []; + lines.push('CloudSQLCTL Diagnostics'); + + const gcloudInstalled = await checkGcloudInstalled(); + lines.push(`gcloud: ${gcloudInstalled ? 'OK' : 'FAIL'}`); + + const account = await getActiveAccount(); + lines.push(`gcloud account: ${account || 'none'}`); + + const adc = await checkAdc(); + lines.push(`ADC: ${adc ? 'OK' : 'WARN'}`); + + try { + await listInstances(); + lines.push('list instances: OK'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + lines.push(`list instances: FAIL (${message})`); + } + + const machineEnv = await checkEnvironmentDetailed('Machine'); + if (machineEnv.ok) { + lines.push('env (machine): OK'); + } else { + lines.push('env (machine): WARN'); + machineEnv.problems.forEach(p => lines.push(` - ${p}`)); + } + + const userEnv = await checkEnvironmentDetailed('User'); + if (userEnv.ok) { + lines.push('env (user): OK'); + } else { + lines.push('env (user): WARN'); + userEnv.problems.forEach(p => lines.push(` - ${p}`)); + } + + const proxyExists = await fs.pathExists(PATHS.PROXY_EXE); + lines.push(`proxy binary: ${proxyExists ? 'OK' : 'FAIL'}`); + + const serviceInstalled = await isServiceInstalled(); + lines.push(`service installed: ${serviceInstalled ? 'yes' : 'no'}`); + + if (serviceInstalled) { + const serviceCreds = await getEnvVar(ENV_VARS.GOOGLE_CREDS, 'Machine'); + lines.push(`service creds: ${serviceCreds ? 'set' : 'not set'}`); + } + + try { + await axios.get('https://api.github.com', { timeout: 5000 }); + lines.push('github api: OK'); + } catch { + lines.push('github api: FAIL'); + } + + return `${lines.join('\n')}\n`; +} + +export const supportCommand = new Command('support') + .description('Support utilities'); + +supportCommand + .command('bundle') + .description('Create a support bundle zip with logs, config, doctor, paths, and status') + .option('--output ', 'Output zip path') + .option('--keep', 'Keep staging directory after bundling') + .action(async (options) => { + try { + const timestamp = formatTimestamp(); + const stagingDir = path.join(PATHS.TEMP, `support-bundle-${timestamp}`); + const outputPath = options.output + ? path.resolve(options.output) + : path.join(PATHS.TEMP, `cloudsqlctl-support-${timestamp}.zip`); + + await fs.ensureDir(stagingDir); + await fs.ensureDir(path.dirname(outputPath)); + + await fs.writeFile(path.join(stagingDir, 'paths.txt'), buildPathsReport()); + await fs.writeFile(path.join(stagingDir, 'status.txt'), await buildStatusReport()); + await fs.writeFile(path.join(stagingDir, 'doctor.txt'), await buildDoctorReport()); + + if (await fs.pathExists(PATHS.CONFIG_FILE)) { + await fs.copy(PATHS.CONFIG_FILE, path.join(stagingDir, 'config.json')); + } else { + await fs.writeFile(path.join(stagingDir, 'config-missing.txt'), 'config.json not found'); + } + + if (await fs.pathExists(PATHS.LOGS)) { + await fs.copy(PATHS.LOGS, path.join(stagingDir, 'logs')); + } else { + await fs.writeFile(path.join(stagingDir, 'logs-missing.txt'), 'logs directory not found'); + } + + await runPs('& { Compress-Archive -Path $args[0] -DestinationPath $args[1] -Force }', [ + path.join(stagingDir, '*'), + outputPath + ]); + + if (!options.keep) { + await fs.remove(stagingDir); + } + + logger.info(`Support bundle created: ${outputPath}`); + } catch (error) { + logger.error('Failed to create support bundle', error); + process.exit(1); + } + }); From 0dfe61a6f822ed169cc8285e186d506429c94928 Mon Sep 17 00:00:00 2001 From: Kinin-Code-Offical <125186556+Kinin-Code-Offical@users.noreply.github.com> Date: Mon, 22 Dec 2025 03:11:20 +0300 Subject: [PATCH 2/2] chore(p1): update docs for support command --- docs/commands.md | 21 +++++++++++++++++++-- tools/generate-docs.mjs | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 6a65167..a8b1234 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,7 +1,7 @@ # Cloud SQL Proxy CLI Reference -**Version:** 0.4.13 -**Generated:** 2025-12-21 +**Version:** 0.4.14 +**Generated:** 2025-12-22 ## Overview @@ -40,6 +40,7 @@ Commands: paths Show resolved system paths and configuration locations upgrade [options] Upgrade cloudsqlctl to the latest version + support Support utilities help [command] display help for command ``` @@ -328,3 +329,19 @@ Options: --json Output status in JSON format -h, --help display help for command ``` + +### support + +```text +Usage: cloudsqlctl support [options] [command] + +Support utilities + +Options: + -h, --help display help for command + +Commands: + bundle [options] Create a support bundle zip with logs, config, doctor, + paths, and status + help [command] display help for command +``` diff --git a/tools/generate-docs.mjs b/tools/generate-docs.mjs index 959e9bf..a574e35 100644 --- a/tools/generate-docs.mjs +++ b/tools/generate-docs.mjs @@ -68,6 +68,7 @@ async function generateDocs() { 'setup', 'paths', 'upgrade', + 'support', ]; content += `## Commands\n\n`;