postgresai 0.14.0-dev.87 → 0.14.0-dev.89

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.
@@ -2115,10 +2115,10 @@ async function runCompose(args: string[], grafanaPassword?: string): Promise<num
2115
2115
  }
2116
2116
  }
2117
2117
 
2118
- // On macOS, node-exporter can't mount host root filesystem - skip it
2118
+ // On macOS, self-node-exporter can't mount host root filesystem - skip it
2119
2119
  const finalArgs = [...args];
2120
2120
  if (process.platform === "darwin" && args.includes("up")) {
2121
- finalArgs.push("--scale", "node-exporter=0");
2121
+ finalArgs.push("--scale", "self-node-exporter=0");
2122
2122
  }
2123
2123
 
2124
2124
  return new Promise<number>((resolve) => {
@@ -2496,15 +2496,15 @@ mon
2496
2496
  // Known container names for cleanup
2497
2497
  const MONITORING_CONTAINERS = [
2498
2498
  "postgres-ai-config-init",
2499
- "node-exporter",
2500
- "cadvisor",
2499
+ "self-node-exporter",
2500
+ "self-cadvisor",
2501
2501
  "grafana-with-datasources",
2502
2502
  "sink-postgres",
2503
2503
  "sink-prometheus",
2504
2504
  "target-db",
2505
2505
  "pgwatch-postgres",
2506
2506
  "pgwatch-prometheus",
2507
- "postgres-exporter-sink",
2507
+ "self-postgres-exporter",
2508
2508
  "flask-pgss-api",
2509
2509
  "sources-generator",
2510
2510
  "postgres-reports",
@@ -13064,7 +13064,7 @@ var {
13064
13064
  // package.json
13065
13065
  var package_default = {
13066
13066
  name: "postgresai",
13067
- version: "0.14.0-dev.87",
13067
+ version: "0.14.0-dev.89",
13068
13068
  description: "postgres_ai CLI",
13069
13069
  license: "Apache-2.0",
13070
13070
  private: false,
@@ -13100,7 +13100,8 @@ var package_default = {
13100
13100
  test: "bun run embed-all && bun test",
13101
13101
  "test:fast": "bun run embed-all && bun test --coverage=false",
13102
13102
  "test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
13103
- typecheck: "bun run embed-all && bunx tsc --noEmit"
13103
+ typecheck: "bun run embed-all && bunx tsc --noEmit",
13104
+ "release-notes": "bun run scripts/generate-release-notes.ts"
13104
13105
  },
13105
13106
  dependencies: {
13106
13107
  "@modelcontextprotocol/sdk": "^1.20.2",
@@ -15889,7 +15890,7 @@ var Result = import_lib.default.Result;
15889
15890
  var TypeOverrides = import_lib.default.TypeOverrides;
15890
15891
  var defaults = import_lib.default.defaults;
15891
15892
  // package.json
15892
- var version = "0.14.0-dev.87";
15893
+ var version = "0.14.0-dev.89";
15893
15894
  var package_default2 = {
15894
15895
  name: "postgresai",
15895
15896
  version,
@@ -15928,7 +15929,8 @@ var package_default2 = {
15928
15929
  test: "bun run embed-all && bun test",
15929
15930
  "test:fast": "bun run embed-all && bun test --coverage=false",
15930
15931
  "test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
15931
- typecheck: "bun run embed-all && bunx tsc --noEmit"
15932
+ typecheck: "bun run embed-all && bunx tsc --noEmit",
15933
+ "release-notes": "bun run scripts/generate-release-notes.ts"
15932
15934
  },
15933
15935
  dependencies: {
15934
15936
  "@modelcontextprotocol/sdk": "^1.20.2",
@@ -30075,7 +30077,7 @@ async function runCompose(args, grafanaPassword) {
30075
30077
  }
30076
30078
  const finalArgs = [...args];
30077
30079
  if (process.platform === "darwin" && args.includes("up")) {
30078
- finalArgs.push("--scale", "node-exporter=0");
30080
+ finalArgs.push("--scale", "self-node-exporter=0");
30079
30081
  }
30080
30082
  return new Promise((resolve6) => {
30081
30083
  const child = spawn2(cmd[0], [...cmd.slice(1), "-f", composeFile, ...finalArgs], {
@@ -30439,15 +30441,15 @@ mon.command("start").description("start monitoring services").action(async () =>
30439
30441
  });
30440
30442
  var MONITORING_CONTAINERS = [
30441
30443
  "postgres-ai-config-init",
30442
- "node-exporter",
30443
- "cadvisor",
30444
+ "self-node-exporter",
30445
+ "self-cadvisor",
30444
30446
  "grafana-with-datasources",
30445
30447
  "sink-postgres",
30446
30448
  "sink-prometheus",
30447
30449
  "target-db",
30448
30450
  "pgwatch-postgres",
30449
30451
  "pgwatch-prometheus",
30450
- "postgres-exporter-sink",
30452
+ "self-postgres-exporter",
30451
30453
  "flask-pgss-api",
30452
30454
  "sources-generator",
30453
30455
  "postgres-reports"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.14.0-dev.87",
3
+ "version": "0.14.0-dev.89",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -36,7 +36,8 @@
36
36
  "test": "bun run embed-all && bun test",
37
37
  "test:fast": "bun run embed-all && bun test --coverage=false",
38
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"
39
+ "typecheck": "bun run embed-all && bunx tsc --noEmit",
40
+ "release-notes": "bun run scripts/generate-release-notes.ts"
40
41
  },
41
42
  "dependencies": {
42
43
  "@modelcontextprotocol/sdk": "^1.20.2",
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Release Notes Generator
4
+ *
5
+ * Generates release notes from git commit history using conventional commits.
6
+ * Analyzes commits between two references (tags, commits, or branches).
7
+ *
8
+ * Usage:
9
+ * bun run scripts/generate-release-notes.ts [options]
10
+ *
11
+ * Options:
12
+ * --since <ref> Start reference (tag, commit, branch). Default: auto-detect last tag
13
+ * --until <ref> End reference. Default: HEAD
14
+ * --version <ver> Version string for the release (e.g., "0.14.0")
15
+ * --format <fmt> Output format: markdown (default), json
16
+ * --output <file> Write to file instead of stdout
17
+ */
18
+
19
+ import { execFileSync } from "child_process";
20
+ import * as fs from "fs";
21
+
22
+ // Valid git ref pattern: alphanumeric, dots, hyphens, underscores, slashes, tildes, carets
23
+ const GIT_REF_PATTERN = /^[a-zA-Z0-9._~^/+-]+$/;
24
+ // Valid git SHA pattern: 7-40 hex characters
25
+ const GIT_SHA_PATTERN = /^[a-f0-9]{7,40}$/i;
26
+
27
+ function isValidGitRef(ref: string): boolean {
28
+ return GIT_REF_PATTERN.test(ref) && !ref.includes("..");
29
+ }
30
+
31
+ function isValidGitSha(sha: string): boolean {
32
+ return GIT_SHA_PATTERN.test(sha);
33
+ }
34
+
35
+ // Conventional commit types and their display names
36
+ const COMMIT_TYPES: Record<string, { title: string; emoji: string; priority: number }> = {
37
+ feat: { title: "New Features", emoji: "🚀", priority: 1 },
38
+ fix: { title: "Bug Fixes", emoji: "🐛", priority: 2 },
39
+ perf: { title: "Performance Improvements", emoji: "⚡", priority: 3 },
40
+ refactor: { title: "Refactoring", emoji: "♻️", priority: 4 },
41
+ docs: { title: "Documentation", emoji: "📚", priority: 5 },
42
+ chore: { title: "Maintenance", emoji: "🔧", priority: 6 },
43
+ test: { title: "Testing", emoji: "🧪", priority: 7 },
44
+ ci: { title: "CI/CD", emoji: "🔄", priority: 8 },
45
+ build: { title: "Build System", emoji: "📦", priority: 9 },
46
+ style: { title: "Code Style", emoji: "💅", priority: 10 },
47
+ };
48
+
49
+ // Scopes to highlight (CLI, monitoring, etc.)
50
+ const KNOWN_SCOPES = ["cli", "monitoring", "reporter", "grafana", "mcp", "prepare-db", "checkup", "deps", "ci", "formula", "pgai", "dashboards"];
51
+
52
+ // Author name mappings for deduplication
53
+ const AUTHOR_ALIASES: Record<string, string> = {
54
+ "Nik Samokhvalov": "Nikolay Samokhvalov",
55
+ };
56
+
57
+ // Authors to exclude from contributors list (bots, AI assistants)
58
+ const EXCLUDED_AUTHORS = ["Claude", "dependabot[bot]", "github-actions[bot]"];
59
+
60
+ function normalizeAuthor(author: string): string {
61
+ return AUTHOR_ALIASES[author] || author;
62
+ }
63
+
64
+ function isExcludedAuthor(author: string): boolean {
65
+ return EXCLUDED_AUTHORS.some((excluded) => author.toLowerCase().includes(excluded.toLowerCase()));
66
+ }
67
+
68
+ interface ParsedCommit {
69
+ hash: string;
70
+ shortHash: string;
71
+ type: string;
72
+ scope: string | null;
73
+ subject: string;
74
+ body: string;
75
+ breaking: boolean;
76
+ date: string;
77
+ author: string;
78
+ }
79
+
80
+ interface ReleaseNotes {
81
+ version: string;
82
+ date: string;
83
+ sinceRef: string;
84
+ untilRef: string;
85
+ commits: ParsedCommit[];
86
+ categories: Record<string, ParsedCommit[]>;
87
+ breaking: ParsedCommit[];
88
+ stats: {
89
+ total: number;
90
+ features: number;
91
+ fixes: number;
92
+ contributors: string[];
93
+ };
94
+ }
95
+
96
+ function gitExec(args: string[]): string {
97
+ try {
98
+ const result = execFileSync("git", args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
99
+ return result.trim();
100
+ } catch (err) {
101
+ return "";
102
+ }
103
+ }
104
+
105
+ function parseArgs(): { since: string; until: string; version: string; format: string; output: string | null } {
106
+ const args = process.argv.slice(2);
107
+ const result = { since: "", until: "HEAD", version: "", format: "markdown", output: null as string | null };
108
+
109
+ for (let i = 0; i < args.length; i++) {
110
+ const arg = args[i];
111
+ const next = args[i + 1];
112
+ switch (arg) {
113
+ case "--since":
114
+ result.since = next || "";
115
+ i++;
116
+ break;
117
+ case "--until":
118
+ result.until = next || "HEAD";
119
+ i++;
120
+ break;
121
+ case "--version":
122
+ result.version = next || "";
123
+ i++;
124
+ break;
125
+ case "--format":
126
+ result.format = next || "markdown";
127
+ i++;
128
+ break;
129
+ case "--output":
130
+ result.output = next || null;
131
+ i++;
132
+ break;
133
+ case "--help":
134
+ console.log(`
135
+ Release Notes Generator
136
+
137
+ Usage: bun run scripts/generate-release-notes.ts [options]
138
+
139
+ Options:
140
+ --since <ref> Start reference (tag, commit, or branch)
141
+ Default: auto-detect last release tag
142
+ --until <ref> End reference (tag, commit, or branch)
143
+ Default: HEAD
144
+ --version <ver> Version string for the release header
145
+ Default: derived from --until or current date
146
+ --format <fmt> Output format: markdown (default) or json
147
+ --output <file> Write to file instead of stdout
148
+
149
+ Examples:
150
+ # Generate notes for upcoming 0.14.0 release
151
+ bun run scripts/generate-release-notes.ts --version 0.14.0
152
+
153
+ # Generate notes between two commits
154
+ bun run scripts/generate-release-notes.ts --since abc123 --until def456
155
+
156
+ # Output as JSON
157
+ bun run scripts/generate-release-notes.ts --format json
158
+ `);
159
+ process.exit(0);
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+
165
+ function detectLastTag(): string {
166
+ // Try to find the last version tag
167
+ const tags = gitExec(["tag", "--sort=-version:refname"]).split("\n").filter(Boolean);
168
+
169
+ // Look for semantic version tags
170
+ for (const tag of tags) {
171
+ if (/^v?\d+\.\d+/.test(tag)) {
172
+ return tag;
173
+ }
174
+ }
175
+
176
+ // Fallback: find a meaningful starting point from commit messages
177
+ const versionCommits = gitExec(["log", "--grep=prepare-for-0.14\\|0.13\\|release", "--format=%H"]).split("\n").filter(Boolean);
178
+ if (versionCommits.length > 0 && versionCommits[0]) {
179
+ return versionCommits[0];
180
+ }
181
+
182
+ // Last resort: 100 commits back
183
+ return "HEAD~100";
184
+ }
185
+
186
+ function getCommitsBetween(since: string, until: string): string[] {
187
+ // Validate refs to prevent command injection
188
+ if (since && !isValidGitRef(since)) {
189
+ throw new Error(`Invalid git ref: ${since}`);
190
+ }
191
+ if (!isValidGitRef(until)) {
192
+ throw new Error(`Invalid git ref: ${until}`);
193
+ }
194
+
195
+ // Get commit hashes between the two refs
196
+ const range = since ? `${since}..${until}` : until;
197
+ const output = gitExec(["log", range, "--format=%H", "--no-merges"]);
198
+ return output.split("\n").filter(Boolean);
199
+ }
200
+
201
+ function parseCommit(hash: string): ParsedCommit | null {
202
+ // Validate hash to prevent command injection
203
+ if (!isValidGitSha(hash)) {
204
+ return null;
205
+ }
206
+
207
+ // Use null byte as delimiter (unlikely to appear in commit messages)
208
+ const format = "%H%x00%h%x00%s%x00%b%x00%ad%x00%an";
209
+ const output = gitExec(["log", "-1", `--format=${format}`, "--date=short", hash]);
210
+
211
+ if (!output) return null;
212
+
213
+ const parts = output.split("\x00");
214
+ const [fullHash, shortHash, subject, body, date, author] = parts;
215
+
216
+ if (!subject) return null;
217
+
218
+ const trimmedBody = (body || "").trim();
219
+
220
+ // Parse conventional commit format: type(scope): subject
221
+ // Also handle: type: subject, type!: subject (breaking)
222
+ const match = subject.match(/^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
223
+
224
+ let type = "other";
225
+ let scope: string | null = null;
226
+ let breaking = false;
227
+ let cleanSubject = subject;
228
+
229
+ if (match) {
230
+ type = match[1]?.toLowerCase() || "other";
231
+ scope = match[2] || null;
232
+ breaking = !!match[3] || trimmedBody.includes("BREAKING CHANGE");
233
+ cleanSubject = match[4] || subject;
234
+ }
235
+
236
+ // Normalize type aliases
237
+ if (type === "feature") type = "feat";
238
+ if (type === "bugfix") type = "fix";
239
+
240
+ return {
241
+ hash: fullHash || hash,
242
+ shortHash: shortHash || hash.slice(0, 7),
243
+ type,
244
+ scope,
245
+ subject: cleanSubject,
246
+ body: trimmedBody,
247
+ breaking,
248
+ date: date || new Date().toISOString().split("T")[0] || "",
249
+ author: (author || "").trim(),
250
+ };
251
+ }
252
+
253
+ function categorizeCommits(commits: ParsedCommit[]): Record<string, ParsedCommit[]> {
254
+ const categories: Record<string, ParsedCommit[]> = {};
255
+
256
+ for (const commit of commits) {
257
+ const type = COMMIT_TYPES[commit.type] ? commit.type : "other";
258
+ if (!categories[type]) {
259
+ categories[type] = [];
260
+ }
261
+ categories[type].push(commit);
262
+ }
263
+
264
+ return categories;
265
+ }
266
+
267
+ function generateMarkdown(notes: ReleaseNotes): string {
268
+ const lines: string[] = [];
269
+
270
+ // Header
271
+ const dateStr = new Date().toISOString().split("T")[0];
272
+ lines.push(`# Release ${notes.version || "Notes"}`);
273
+ lines.push("");
274
+ lines.push(`**Release Date:** ${dateStr}`);
275
+ lines.push("");
276
+
277
+ // Stats summary
278
+ lines.push("## Summary");
279
+ lines.push("");
280
+ lines.push(`This release includes **${notes.stats.total}** changes:`);
281
+ lines.push(`- ${notes.stats.features} new features`);
282
+ lines.push(`- ${notes.stats.fixes} bug fixes`);
283
+ lines.push("");
284
+
285
+ // Breaking changes (if any)
286
+ if (notes.breaking.length > 0) {
287
+ lines.push("## Breaking Changes");
288
+ lines.push("");
289
+ for (const commit of notes.breaking) {
290
+ const scopeStr = commit.scope ? `**${commit.scope}:** ` : "";
291
+ lines.push(`- ${scopeStr}${commit.subject}`);
292
+ }
293
+ lines.push("");
294
+ }
295
+
296
+ // Categories sorted by priority
297
+ const sortedTypes = Object.keys(notes.categories).sort((a, b) => {
298
+ const pa = COMMIT_TYPES[a]?.priority ?? 99;
299
+ const pb = COMMIT_TYPES[b]?.priority ?? 99;
300
+ return pa - pb;
301
+ });
302
+
303
+ for (const type of sortedTypes) {
304
+ const commits = notes.categories[type];
305
+ if (!commits || commits.length === 0) continue;
306
+
307
+ const typeInfo = COMMIT_TYPES[type] || { title: "Other Changes", emoji: "📝", priority: 99 };
308
+ lines.push(`## ${typeInfo.emoji} ${typeInfo.title}`);
309
+ lines.push("");
310
+
311
+ // Group by scope within each type
312
+ const byScope: Record<string, ParsedCommit[]> = {};
313
+ for (const commit of commits) {
314
+ const scope = commit.scope || "_general";
315
+ if (!byScope[scope]) byScope[scope] = [];
316
+ byScope[scope].push(commit);
317
+ }
318
+
319
+ // Sort scopes: known scopes first, then alphabetically
320
+ const scopes = Object.keys(byScope).sort((a, b) => {
321
+ if (a === "_general") return 1;
322
+ if (b === "_general") return -1;
323
+ const aKnown = KNOWN_SCOPES.includes(a);
324
+ const bKnown = KNOWN_SCOPES.includes(b);
325
+ if (aKnown && !bKnown) return -1;
326
+ if (!aKnown && bKnown) return 1;
327
+ return a.localeCompare(b);
328
+ });
329
+
330
+ for (const scope of scopes) {
331
+ const scopeCommits = byScope[scope] || [];
332
+ if (scope !== "_general" && scopeCommits.length > 0) {
333
+ lines.push(`### ${scope}`);
334
+ lines.push("");
335
+ }
336
+ for (const commit of scopeCommits) {
337
+ lines.push(`- ${commit.subject} (\`${commit.shortHash}\`)`);
338
+ }
339
+ lines.push("");
340
+ }
341
+ }
342
+
343
+ // Contributors
344
+ if (notes.stats.contributors.length > 0) {
345
+ lines.push("## Contributors");
346
+ lines.push("");
347
+ lines.push("Thank you to all contributors:");
348
+ lines.push("");
349
+ for (const contributor of notes.stats.contributors.sort()) {
350
+ lines.push(`- ${contributor}`);
351
+ }
352
+ lines.push("");
353
+ }
354
+
355
+ return lines.join("\n");
356
+ }
357
+
358
+ function generateJson(notes: ReleaseNotes): string {
359
+ return JSON.stringify(notes, null, 2);
360
+ }
361
+
362
+ async function main() {
363
+ const args = parseArgs();
364
+
365
+ // Determine the range
366
+ const since = args.since || detectLastTag();
367
+ const until = args.until;
368
+
369
+ const log = (msg: string) => process.stderr.write(msg + "\n");
370
+ log(`Analyzing commits from ${since} to ${until}...`);
371
+
372
+ // Get and parse commits
373
+ const hashes = getCommitsBetween(since, until);
374
+ log(`Found ${hashes.length} commits to analyze`);
375
+
376
+ const commits: ParsedCommit[] = [];
377
+ for (const hash of hashes) {
378
+ const parsed = parseCommit(hash);
379
+ if (parsed) {
380
+ commits.push(parsed);
381
+ }
382
+ }
383
+
384
+ // Build release notes structure
385
+ const categories = categorizeCommits(commits);
386
+ const breaking = commits.filter((c) => c.breaking);
387
+
388
+ // Normalize author names and exclude bots/AI
389
+ const contributors = [
390
+ ...new Set(
391
+ commits
392
+ .map((c) => normalizeAuthor(c.author))
393
+ .filter((author) => author && !isExcludedAuthor(author))
394
+ ),
395
+ ];
396
+
397
+ const notes: ReleaseNotes = {
398
+ version: args.version || "",
399
+ date: new Date().toISOString().split("T")[0] || "",
400
+ sinceRef: since,
401
+ untilRef: until,
402
+ commits,
403
+ categories,
404
+ breaking,
405
+ stats: {
406
+ total: commits.length,
407
+ features: categories["feat"]?.length || 0,
408
+ fixes: categories["fix"]?.length || 0,
409
+ contributors,
410
+ },
411
+ };
412
+
413
+ // Generate output
414
+ let output: string;
415
+ if (args.format === "json") {
416
+ output = generateJson(notes);
417
+ } else {
418
+ output = generateMarkdown(notes);
419
+ }
420
+
421
+ // Write output
422
+ if (args.output) {
423
+ fs.writeFileSync(args.output, output, "utf8");
424
+ log(`Release notes written to: ${args.output}`);
425
+ } else {
426
+ console.log(output);
427
+ }
428
+ }
429
+
430
+ main().catch((err) => {
431
+ console.error("Error generating release notes:", err);
432
+ process.exit(1);
433
+ });
@@ -215,7 +215,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
215
215
  } finally {
216
216
  await pg.cleanup();
217
217
  }
218
- });
218
+ }, { timeout: 15000 });
219
219
 
220
220
  test("requires explicit monitoring password in non-interactive mode", async () => {
221
221
  pg = await createTempPostgres();
@@ -239,7 +239,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
239
239
  } finally {
240
240
  await pg.cleanup();
241
241
  }
242
- });
242
+ }, { timeout: 15000 });
243
243
 
244
244
  test(
245
245
  "fixes slightly-off permissions idempotently",
@@ -375,6 +375,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
375
375
  }
376
376
  );
377
377
 
378
+ // 15s timeout for PostgreSQL startup + two CLI init commands in slow CI
378
379
  test("--reset-password updates the monitoring role login password", async () => {
379
380
  pg = await createTempPostgres();
380
381
 
@@ -405,7 +406,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
405
406
  } finally {
406
407
  await pg.cleanup();
407
408
  }
408
- });
409
+ }, { timeout: 15000 });
409
410
 
410
411
  // 60s timeout for PostgreSQL startup + multiple SQL queries in slow CI
411
412
  test("explain_generic validates input and prevents SQL injection", async () => {