Skip to content

Commit b5ebf20

Browse files
P1: Support bundle command (zip logs+config+doctor+paths+status) (#35)
* feat(p1): add support bundle command * chore(p1): update docs for support command
1 parent 959d6eb commit b5ebf20

File tree

4 files changed

+180
-2
lines changed

4 files changed

+180
-2
lines changed

docs/commands.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Cloud SQL Proxy CLI Reference
22

3-
**Version:** 0.4.13
4-
**Generated:** 2025-12-21
3+
**Version:** 0.4.14
4+
**Generated:** 2025-12-22
55

66
## Overview
77

@@ -40,6 +40,7 @@ Commands:
4040
paths Show resolved system paths and configuration
4141
locations
4242
upgrade [options] Upgrade cloudsqlctl to the latest version
43+
support Support utilities
4344
help [command] display help for command
4445
```
4546

@@ -328,3 +329,19 @@ Options:
328329
--json Output status in JSON format
329330
-h, --help display help for command
330331
```
332+
333+
### support
334+
335+
```text
336+
Usage: cloudsqlctl support [options] [command]
337+
338+
Support utilities
339+
340+
Options:
341+
-h, --help display help for command
342+
343+
Commands:
344+
bundle [options] Create a support bundle zip with logs, config, doctor,
345+
paths, and status
346+
help [command] display help for command
347+
```

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { authCommand } from './commands/auth.js';
2121
import { setupCommand } from './commands/setup.js';
2222
import { pathsCommand } from './commands/paths.js';
2323
import { upgradeCommand } from './commands/upgrade.js';
24+
import { supportCommand } from './commands/support.js';
2425
import { logger } from './core/logger.js';
2526

2627
const program = new Command();
@@ -51,6 +52,7 @@ program.addCommand(authCommand);
5152
program.addCommand(setupCommand);
5253
program.addCommand(pathsCommand);
5354
program.addCommand(upgradeCommand);
55+
program.addCommand(supportCommand);
5456

5557
program.parseAsync(process.argv).catch(err => {
5658
logger.error('Unhandled error', err);

src/commands/support.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { Command } from 'commander';
2+
import path from 'path';
3+
import fs from 'fs-extra';
4+
import axios from 'axios';
5+
import { logger } from '../core/logger.js';
6+
import { readConfig } from '../core/config.js';
7+
import { isRunning } from '../core/proxy.js';
8+
import { checkGcloudInstalled, getActiveAccount, checkAdc, listInstances } from '../core/gcloud.js';
9+
import { checkEnvironmentDetailed } from '../system/env.js';
10+
import { isServiceInstalled, isServiceRunning } from '../system/service.js';
11+
import { getEnvVar, runPs } from '../system/powershell.js';
12+
import { PATHS, PATHS_REASON, PATHS_SOURCE, ENV_VARS } from '../system/paths.js';
13+
14+
function formatTimestamp(): string {
15+
return new Date().toISOString().replace(/[:.]/g, '-');
16+
}
17+
18+
function buildPathsReport(): string {
19+
const lines = [
20+
'CloudSQLCTL Paths',
21+
`Home: ${PATHS.HOME}`,
22+
`Bin: ${PATHS.BIN}`,
23+
`Logs: ${PATHS.LOGS}`,
24+
`Config: ${PATHS.CONFIG_FILE}`,
25+
`Proxy: ${PATHS.PROXY_EXE}`,
26+
`Secrets: ${PATHS.SECRETS}`,
27+
'',
28+
`Resolution Source: ${PATHS_SOURCE}`,
29+
`Reason: ${PATHS_REASON}`,
30+
];
31+
return `${lines.join('\n')}\n`;
32+
}
33+
34+
async function buildStatusReport(): Promise<string> {
35+
const processRunning = await isRunning();
36+
const serviceInstalled = await isServiceInstalled();
37+
const serviceRunning = serviceInstalled ? await isServiceRunning() : false;
38+
const config = await readConfig();
39+
40+
const lines = [
41+
'CloudSQLCTL Status',
42+
`Service: ${serviceInstalled ? (serviceRunning ? 'RUNNING' : 'STOPPED') : 'NOT INSTALLED'}`,
43+
`Process: ${processRunning ? 'RUNNING' : 'STOPPED'}`,
44+
`Instance: ${config.selectedInstance || 'Unknown'}`,
45+
`Port: ${config.proxyPort || 5432}`,
46+
];
47+
48+
return `${lines.join('\n')}\n`;
49+
}
50+
51+
async function buildDoctorReport(): Promise<string> {
52+
const lines: string[] = [];
53+
lines.push('CloudSQLCTL Diagnostics');
54+
55+
const gcloudInstalled = await checkGcloudInstalled();
56+
lines.push(`gcloud: ${gcloudInstalled ? 'OK' : 'FAIL'}`);
57+
58+
const account = await getActiveAccount();
59+
lines.push(`gcloud account: ${account || 'none'}`);
60+
61+
const adc = await checkAdc();
62+
lines.push(`ADC: ${adc ? 'OK' : 'WARN'}`);
63+
64+
try {
65+
await listInstances();
66+
lines.push('list instances: OK');
67+
} catch (error) {
68+
const message = error instanceof Error ? error.message : String(error);
69+
lines.push(`list instances: FAIL (${message})`);
70+
}
71+
72+
const machineEnv = await checkEnvironmentDetailed('Machine');
73+
if (machineEnv.ok) {
74+
lines.push('env (machine): OK');
75+
} else {
76+
lines.push('env (machine): WARN');
77+
machineEnv.problems.forEach(p => lines.push(` - ${p}`));
78+
}
79+
80+
const userEnv = await checkEnvironmentDetailed('User');
81+
if (userEnv.ok) {
82+
lines.push('env (user): OK');
83+
} else {
84+
lines.push('env (user): WARN');
85+
userEnv.problems.forEach(p => lines.push(` - ${p}`));
86+
}
87+
88+
const proxyExists = await fs.pathExists(PATHS.PROXY_EXE);
89+
lines.push(`proxy binary: ${proxyExists ? 'OK' : 'FAIL'}`);
90+
91+
const serviceInstalled = await isServiceInstalled();
92+
lines.push(`service installed: ${serviceInstalled ? 'yes' : 'no'}`);
93+
94+
if (serviceInstalled) {
95+
const serviceCreds = await getEnvVar(ENV_VARS.GOOGLE_CREDS, 'Machine');
96+
lines.push(`service creds: ${serviceCreds ? 'set' : 'not set'}`);
97+
}
98+
99+
try {
100+
await axios.get('https://api.github.com', { timeout: 5000 });
101+
lines.push('github api: OK');
102+
} catch {
103+
lines.push('github api: FAIL');
104+
}
105+
106+
return `${lines.join('\n')}\n`;
107+
}
108+
109+
export const supportCommand = new Command('support')
110+
.description('Support utilities');
111+
112+
supportCommand
113+
.command('bundle')
114+
.description('Create a support bundle zip with logs, config, doctor, paths, and status')
115+
.option('--output <path>', 'Output zip path')
116+
.option('--keep', 'Keep staging directory after bundling')
117+
.action(async (options) => {
118+
try {
119+
const timestamp = formatTimestamp();
120+
const stagingDir = path.join(PATHS.TEMP, `support-bundle-${timestamp}`);
121+
const outputPath = options.output
122+
? path.resolve(options.output)
123+
: path.join(PATHS.TEMP, `cloudsqlctl-support-${timestamp}.zip`);
124+
125+
await fs.ensureDir(stagingDir);
126+
await fs.ensureDir(path.dirname(outputPath));
127+
128+
await fs.writeFile(path.join(stagingDir, 'paths.txt'), buildPathsReport());
129+
await fs.writeFile(path.join(stagingDir, 'status.txt'), await buildStatusReport());
130+
await fs.writeFile(path.join(stagingDir, 'doctor.txt'), await buildDoctorReport());
131+
132+
if (await fs.pathExists(PATHS.CONFIG_FILE)) {
133+
await fs.copy(PATHS.CONFIG_FILE, path.join(stagingDir, 'config.json'));
134+
} else {
135+
await fs.writeFile(path.join(stagingDir, 'config-missing.txt'), 'config.json not found');
136+
}
137+
138+
if (await fs.pathExists(PATHS.LOGS)) {
139+
await fs.copy(PATHS.LOGS, path.join(stagingDir, 'logs'));
140+
} else {
141+
await fs.writeFile(path.join(stagingDir, 'logs-missing.txt'), 'logs directory not found');
142+
}
143+
144+
await runPs('& { Compress-Archive -Path $args[0] -DestinationPath $args[1] -Force }', [
145+
path.join(stagingDir, '*'),
146+
outputPath
147+
]);
148+
149+
if (!options.keep) {
150+
await fs.remove(stagingDir);
151+
}
152+
153+
logger.info(`Support bundle created: ${outputPath}`);
154+
} catch (error) {
155+
logger.error('Failed to create support bundle', error);
156+
process.exit(1);
157+
}
158+
});

tools/generate-docs.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ async function generateDocs() {
6868
'setup',
6969
'paths',
7070
'upgrade',
71+
'support',
7172
];
7273

7374
content += `## Commands\n\n`;

0 commit comments

Comments
 (0)