Skip to content

Commit b2591ee

Browse files
committed
feat(cli): embed docker-compose.yml in CLI for offline usage
- Add embed-compose.ts build script to generate compose-embedded.ts at build time - Remove network dependency on GitLab for docker-compose.yml download - CLI now works offline from any directory - Strip dev suffix from version for PGAI_TAG (dev images not on Docker Hub) - Add embed-all npm script to run both embed-metrics and embed-compose
1 parent b9e4aeb commit b2591ee

File tree

4 files changed

+86
-44
lines changed

4 files changed

+86
-44
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ cli/**/*.d.ts.map
6060
!cli/jest.config.js
6161
!cli/packages/postgres-ai/bin/postgres-ai.js
6262

63-
# Generated at build time from metrics.yml
63+
# Generated at build time (not committed, recreated by build scripts)
6464
cli/lib/metrics-embedded.ts
65+
cli/lib/compose-embedded.ts
6566

6667
# Generated config files (these are created by the sources-generator)
6768
config/pgwatch-postgres/sources.yml

cli/bin/postgres-ai.ts

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { createInterface } from "readline";
2020
import * as childProcess from "child_process";
2121
import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
2222
import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
23+
import { DOCKER_COMPOSE_CONTENT } from "../lib/compose-embedded";
2324

2425
// Singleton readline interface for stdin prompts
2526
let rl: ReturnType<typeof createInterface> | null = null;
@@ -408,18 +409,18 @@ function getDefaultMonitoringProjectDir(): string {
408409
return path.join(config.getConfigDir(), "monitoring");
409410
}
410411

411-
async function downloadText(url: string): Promise<string> {
412-
const controller = new AbortController();
413-
const timeout = setTimeout(() => controller.abort(), 15_000);
414-
try {
415-
const response = await fetch(url, { signal: controller.signal });
416-
if (!response.ok) {
417-
throw new Error(`HTTP ${response.status} for ${url}`);
418-
}
419-
return await response.text();
420-
} finally {
421-
clearTimeout(timeout);
412+
/**
413+
* Get the stable version for docker image tags.
414+
* Strips dev suffix (e.g., "0.14.0-dev.33" -> "0.14.0") since dev images
415+
* are only published to GitLab registry, not Docker Hub.
416+
*/
417+
function getStableImageTag(): string {
418+
// Allow explicit override via environment variable
419+
if (process.env.PGAI_TAG) {
420+
return process.env.PGAI_TAG;
422421
}
422+
// Strip -dev.X suffix for stable Docker Hub images
423+
return pkg.version.replace(/-dev\.\d+$/, "");
423424
}
424425

425426
async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
@@ -431,31 +432,8 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
431432
fs.mkdirSync(projectDir, { recursive: true, mode: 0o700 });
432433
}
433434

434-
if (!fs.existsSync(composeFile)) {
435-
const refs = [
436-
process.env.PGAI_PROJECT_REF,
437-
pkg.version,
438-
`v${pkg.version}`,
439-
"main",
440-
].filter((v): v is string => Boolean(v && v.trim()));
441-
442-
let lastErr: unknown;
443-
for (const ref of refs) {
444-
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
445-
try {
446-
const text = await downloadText(url);
447-
fs.writeFileSync(composeFile, text, { encoding: "utf8", mode: 0o600 });
448-
break;
449-
} catch (err) {
450-
lastErr = err;
451-
}
452-
}
453-
454-
if (!fs.existsSync(composeFile)) {
455-
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
456-
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
457-
}
458-
}
435+
// Write embedded docker-compose.yml (always overwrite to stay in sync with CLI version)
436+
fs.writeFileSync(composeFile, DOCKER_COMPOSE_CONTENT, { encoding: "utf8", mode: 0o600 });
459437

460438
// Ensure instances.yml exists as a FILE (avoid Docker creating a directory)
461439
if (!fs.existsSync(instancesFile)) {
@@ -472,9 +450,11 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
472450
}
473451

474452
// Ensure .env exists and has PGAI_TAG (compose requires it)
453+
// Use stable version by default; dev users can override with PGAI_TAG env var
475454
const envFile = path.resolve(projectDir, ".env");
476455
if (!fs.existsSync(envFile)) {
477-
const envText = `PGAI_TAG=${pkg.version}\n# PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai\n`;
456+
const stableTag = getStableImageTag();
457+
const envText = `PGAI_TAG=${stableTag}\n# For dev images, use GitLab registry:\n# PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai\n# PGAI_TAG=${pkg.version}\n`;
478458
fs.writeFileSync(envFile, envText, { encoding: "utf8", mode: 0o600 });
479459
}
480460

cli/package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@
2626
},
2727
"scripts": {
2828
"embed-metrics": "bun run scripts/embed-metrics.ts",
29-
"build": "bun run embed-metrics && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r ./sql ./dist/sql",
29+
"embed-compose": "bun run scripts/embed-compose.ts",
30+
"embed-all": "bun run embed-metrics && bun run embed-compose",
31+
"build": "bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r ./sql ./dist/sql",
3032
"prepublishOnly": "npm run build",
3133
"start": "bun ./bin/postgres-ai.ts --help",
3234
"start:node": "node ./dist/bin/postgres-ai.js --help",
33-
"dev": "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
34-
"test": "bun run embed-metrics && bun test",
35-
"test:fast": "bun run embed-metrics && bun test --coverage=false",
36-
"test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
37-
"typecheck": "bun run embed-metrics && bunx tsc --noEmit"
35+
"dev": "bun run embed-all && bun --watch ./bin/postgres-ai.ts",
36+
"test": "bun run embed-all && bun test",
37+
"test:fast": "bun run embed-all && bun test --coverage=false",
38+
"test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
39+
"typecheck": "bun run embed-all && bunx tsc --noEmit"
3840
},
3941
"dependencies": {
4042
"@modelcontextprotocol/sdk": "^1.20.2",

cli/scripts/embed-compose.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Build script to embed docker-compose.yml into the CLI bundle.
4+
*
5+
* This script reads docker-compose.yml from the repo root and generates
6+
* cli/lib/compose-embedded.ts with the content embedded as a TypeScript string.
7+
*
8+
* The generated file is NOT committed to git - it's regenerated at build time.
9+
*
10+
* Usage: bun run scripts/embed-compose.ts
11+
*/
12+
13+
import * as fs from "fs";
14+
import * as path from "path";
15+
16+
// Resolve paths relative to cli/ directory
17+
const CLI_DIR = path.resolve(__dirname, "..");
18+
const COMPOSE_PATH = path.resolve(CLI_DIR, "../docker-compose.yml");
19+
const OUTPUT_PATH = path.resolve(CLI_DIR, "lib/compose-embedded.ts");
20+
21+
function main() {
22+
console.log(`Reading docker-compose.yml from: ${COMPOSE_PATH}`);
23+
24+
if (!fs.existsSync(COMPOSE_PATH)) {
25+
console.error(`ERROR: docker-compose.yml not found at ${COMPOSE_PATH}`);
26+
process.exit(1);
27+
}
28+
29+
const composeContent = fs.readFileSync(COMPOSE_PATH, "utf8");
30+
31+
// Generate TypeScript code
32+
const tsCode = generateTypeScript(composeContent);
33+
34+
// Write output
35+
fs.writeFileSync(OUTPUT_PATH, tsCode, "utf8");
36+
console.log(`Generated: ${OUTPUT_PATH}`);
37+
console.log(`Embedded ${composeContent.split("\n").length} lines`);
38+
}
39+
40+
function generateTypeScript(content: string): string {
41+
const lines: string[] = [
42+
"// AUTO-GENERATED FILE - DO NOT EDIT",
43+
"// Generated from docker-compose.yml by scripts/embed-compose.ts",
44+
`// Generated at: ${new Date().toISOString()}`,
45+
"",
46+
"/**",
47+
" * Embedded docker-compose.yml content for CLI distribution.",
48+
" * ",
49+
" * This allows the CLI to work offline without downloading from GitLab.",
50+
" * Image tags use ${PGAI_TAG} which defaults to CLI version via .env file.",
51+
" */",
52+
`export const DOCKER_COMPOSE_CONTENT = ${JSON.stringify(content)};`,
53+
"",
54+
];
55+
56+
return lines.join("\n");
57+
}
58+
59+
main();

0 commit comments

Comments
 (0)