postgresai 0.14.0-dev.7 → 0.14.0-dev.71

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.
Files changed (83) hide show
  1. package/README.md +161 -61
  2. package/bin/postgres-ai.ts +1982 -404
  3. package/bun.lock +258 -0
  4. package/bunfig.toml +20 -0
  5. package/dist/bin/postgres-ai.js +29395 -1576
  6. package/dist/sql/01.role.sql +16 -0
  7. package/dist/sql/02.permissions.sql +37 -0
  8. package/dist/sql/03.optional_rds.sql +6 -0
  9. package/dist/sql/04.optional_self_managed.sql +8 -0
  10. package/dist/sql/05.helpers.sql +439 -0
  11. package/dist/sql/sql/01.role.sql +16 -0
  12. package/dist/sql/sql/02.permissions.sql +37 -0
  13. package/dist/sql/sql/03.optional_rds.sql +6 -0
  14. package/dist/sql/sql/04.optional_self_managed.sql +8 -0
  15. package/dist/sql/sql/05.helpers.sql +439 -0
  16. package/lib/auth-server.ts +124 -106
  17. package/lib/checkup-api.ts +386 -0
  18. package/lib/checkup.ts +1396 -0
  19. package/lib/config.ts +6 -3
  20. package/lib/init.ts +568 -155
  21. package/lib/issues.ts +400 -191
  22. package/lib/mcp-server.ts +213 -90
  23. package/lib/metrics-embedded.ts +79 -0
  24. package/lib/metrics-loader.ts +127 -0
  25. package/lib/supabase.ts +769 -0
  26. package/lib/util.ts +61 -0
  27. package/package.json +20 -10
  28. package/packages/postgres-ai/README.md +26 -0
  29. package/packages/postgres-ai/bin/postgres-ai.js +27 -0
  30. package/packages/postgres-ai/package.json +27 -0
  31. package/scripts/embed-metrics.ts +154 -0
  32. package/sql/01.role.sql +16 -0
  33. package/sql/02.permissions.sql +37 -0
  34. package/sql/03.optional_rds.sql +6 -0
  35. package/sql/04.optional_self_managed.sql +8 -0
  36. package/sql/05.helpers.sql +439 -0
  37. package/test/auth.test.ts +258 -0
  38. package/test/checkup.integration.test.ts +321 -0
  39. package/test/checkup.test.ts +1117 -0
  40. package/test/config-consistency.test.ts +36 -0
  41. package/test/init.integration.test.ts +500 -0
  42. package/test/init.test.ts +682 -0
  43. package/test/issues.cli.test.ts +314 -0
  44. package/test/issues.test.ts +456 -0
  45. package/test/mcp-server.test.ts +988 -0
  46. package/test/schema-validation.test.ts +81 -0
  47. package/test/supabase.test.ts +568 -0
  48. package/test/test-utils.ts +128 -0
  49. package/tsconfig.json +12 -20
  50. package/dist/bin/postgres-ai.d.ts +0 -3
  51. package/dist/bin/postgres-ai.d.ts.map +0 -1
  52. package/dist/bin/postgres-ai.js.map +0 -1
  53. package/dist/lib/auth-server.d.ts +0 -31
  54. package/dist/lib/auth-server.d.ts.map +0 -1
  55. package/dist/lib/auth-server.js +0 -263
  56. package/dist/lib/auth-server.js.map +0 -1
  57. package/dist/lib/config.d.ts +0 -45
  58. package/dist/lib/config.d.ts.map +0 -1
  59. package/dist/lib/config.js +0 -181
  60. package/dist/lib/config.js.map +0 -1
  61. package/dist/lib/init.d.ts +0 -61
  62. package/dist/lib/init.d.ts.map +0 -1
  63. package/dist/lib/init.js +0 -359
  64. package/dist/lib/init.js.map +0 -1
  65. package/dist/lib/issues.d.ts +0 -75
  66. package/dist/lib/issues.d.ts.map +0 -1
  67. package/dist/lib/issues.js +0 -336
  68. package/dist/lib/issues.js.map +0 -1
  69. package/dist/lib/mcp-server.d.ts +0 -9
  70. package/dist/lib/mcp-server.d.ts.map +0 -1
  71. package/dist/lib/mcp-server.js +0 -168
  72. package/dist/lib/mcp-server.js.map +0 -1
  73. package/dist/lib/pkce.d.ts +0 -32
  74. package/dist/lib/pkce.d.ts.map +0 -1
  75. package/dist/lib/pkce.js +0 -101
  76. package/dist/lib/pkce.js.map +0 -1
  77. package/dist/lib/util.d.ts +0 -27
  78. package/dist/lib/util.d.ts.map +0 -1
  79. package/dist/lib/util.js +0 -46
  80. package/dist/lib/util.js.map +0 -1
  81. package/dist/package.json +0 -46
  82. package/test/init.integration.test.cjs +0 -269
  83. package/test/init.test.cjs +0 -69
@@ -1,32 +0,0 @@
1
- /**
2
- * PKCE parameters for OAuth 2.0 Authorization Code Flow with PKCE
3
- */
4
- export interface PKCEParams {
5
- codeVerifier: string;
6
- codeChallenge: string;
7
- codeChallengeMethod: "S256";
8
- state: string;
9
- }
10
- /**
11
- * Generate PKCE code verifier
12
- * @returns Random code verifier (43-128 characters)
13
- */
14
- export declare function generateCodeVerifier(): string;
15
- /**
16
- * Generate PKCE code challenge from verifier
17
- * Uses S256 method (SHA256)
18
- * @param verifier - Code verifier string
19
- * @returns Base64URL-encoded SHA256 hash of verifier
20
- */
21
- export declare function generateCodeChallenge(verifier: string): string;
22
- /**
23
- * Generate random state for CSRF protection
24
- * @returns Random state string
25
- */
26
- export declare function generateState(): string;
27
- /**
28
- * Generate complete PKCE parameters
29
- * @returns Object with verifier, challenge, challengeMethod, and state
30
- */
31
- export declare function generatePKCEParams(): PKCEParams;
32
- //# sourceMappingURL=pkce.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../lib/pkce.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAyBD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,CAW/C"}
package/dist/lib/pkce.js DELETED
@@ -1,101 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.generateCodeVerifier = generateCodeVerifier;
37
- exports.generateCodeChallenge = generateCodeChallenge;
38
- exports.generateState = generateState;
39
- exports.generatePKCEParams = generatePKCEParams;
40
- const crypto = __importStar(require("crypto"));
41
- /**
42
- * Generate a cryptographically random string for PKCE
43
- * @param length - Length of the string (43-128 characters per RFC 7636)
44
- * @returns Base64URL-encoded random string
45
- */
46
- function generateRandomString(length = 64) {
47
- const bytes = crypto.randomBytes(length);
48
- return base64URLEncode(bytes);
49
- }
50
- /**
51
- * Base64URL encode (without padding)
52
- * @param buffer - Buffer to encode
53
- * @returns Base64URL-encoded string
54
- */
55
- function base64URLEncode(buffer) {
56
- return buffer
57
- .toString("base64")
58
- .replace(/\+/g, "-")
59
- .replace(/\//g, "_")
60
- .replace(/=/g, "");
61
- }
62
- /**
63
- * Generate PKCE code verifier
64
- * @returns Random code verifier (43-128 characters)
65
- */
66
- function generateCodeVerifier() {
67
- return generateRandomString(32); // 32 bytes = 43 chars after base64url encoding
68
- }
69
- /**
70
- * Generate PKCE code challenge from verifier
71
- * Uses S256 method (SHA256)
72
- * @param verifier - Code verifier string
73
- * @returns Base64URL-encoded SHA256 hash of verifier
74
- */
75
- function generateCodeChallenge(verifier) {
76
- const hash = crypto.createHash("sha256").update(verifier).digest();
77
- return base64URLEncode(hash);
78
- }
79
- /**
80
- * Generate random state for CSRF protection
81
- * @returns Random state string
82
- */
83
- function generateState() {
84
- return generateRandomString(16); // 16 bytes = 22 chars
85
- }
86
- /**
87
- * Generate complete PKCE parameters
88
- * @returns Object with verifier, challenge, challengeMethod, and state
89
- */
90
- function generatePKCEParams() {
91
- const verifier = generateCodeVerifier();
92
- const challenge = generateCodeChallenge(verifier);
93
- const state = generateState();
94
- return {
95
- codeVerifier: verifier,
96
- codeChallenge: challenge,
97
- codeChallengeMethod: "S256",
98
- state: state,
99
- };
100
- }
101
- //# sourceMappingURL=pkce.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../lib/pkce.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,oDAEC;AAQD,sDAGC;AAMD,sCAEC;AAMD,gDAWC;AA7ED,+CAAiC;AAYjC;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,SAAiB,EAAE;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM;SACV,QAAQ,CAAC,QAAQ,CAAC;SAClB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,SAAgB,oBAAoB;IAClC,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,+CAA+C;AAClF,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,QAAgB;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;IACnE,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa;IAC3B,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,sBAAsB;AACzD,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB;IAChC,MAAM,QAAQ,GAAG,oBAAoB,EAAE,CAAC;IACxC,MAAM,SAAS,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;IAE9B,OAAO;QACL,YAAY,EAAE,QAAQ;QACtB,aAAa,EAAE,SAAS;QACxB,mBAAmB,EAAE,MAAM;QAC3B,KAAK,EAAE,KAAK;KACb,CAAC;AACJ,CAAC"}
@@ -1,27 +0,0 @@
1
- export declare function maskSecret(secret: string): string;
2
- export interface RootOptsLike {
3
- apiBaseUrl?: string;
4
- uiBaseUrl?: string;
5
- }
6
- export interface ConfigLike {
7
- baseUrl?: string | null;
8
- }
9
- export interface ResolvedBaseUrls {
10
- apiBaseUrl: string;
11
- uiBaseUrl: string;
12
- }
13
- /**
14
- * Normalize a base URL by trimming a single trailing slash and validating.
15
- * @throws Error if the URL is invalid
16
- */
17
- export declare function normalizeBaseUrl(value: string): string;
18
- /**
19
- * Resolve API and UI base URLs using precedence and normalize them.
20
- * Precedence (API): opts.apiBaseUrl → env.PGAI_API_BASE_URL → cfg.baseUrl → default
21
- * Precedence (UI): opts.uiBaseUrl → env.PGAI_UI_BASE_URL → default
22
- */
23
- export declare function resolveBaseUrls(opts?: RootOptsLike, cfg?: ConfigLike, defaults?: {
24
- apiBaseUrl?: string;
25
- uiBaseUrl?: string;
26
- }): ResolvedBaseUrls;
27
- //# sourceMappingURL=util.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../lib/util.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKjD;AAGD,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,CAAC,EAAE,YAAY,EACnB,GAAG,CAAC,EAAE,UAAU,EAChB,QAAQ,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACzD,gBAAgB,CAWlB"}
package/dist/lib/util.js DELETED
@@ -1,46 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.maskSecret = maskSecret;
4
- exports.normalizeBaseUrl = normalizeBaseUrl;
5
- exports.resolveBaseUrls = resolveBaseUrls;
6
- function maskSecret(secret) {
7
- if (!secret)
8
- return "";
9
- if (secret.length <= 8)
10
- return "****";
11
- if (secret.length <= 16)
12
- return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
13
- return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
14
- }
15
- /**
16
- * Normalize a base URL by trimming a single trailing slash and validating.
17
- * @throws Error if the URL is invalid
18
- */
19
- function normalizeBaseUrl(value) {
20
- const trimmed = (value || "").replace(/\/$/, "");
21
- try {
22
- // Validate
23
- // eslint-disable-next-line no-new
24
- new URL(trimmed);
25
- }
26
- catch {
27
- throw new Error(`Invalid base URL: ${value}`);
28
- }
29
- return trimmed;
30
- }
31
- /**
32
- * Resolve API and UI base URLs using precedence and normalize them.
33
- * Precedence (API): opts.apiBaseUrl → env.PGAI_API_BASE_URL → cfg.baseUrl → default
34
- * Precedence (UI): opts.uiBaseUrl → env.PGAI_UI_BASE_URL → default
35
- */
36
- function resolveBaseUrls(opts, cfg, defaults = {}) {
37
- const defApi = defaults.apiBaseUrl || "https://postgres.ai/api/general/";
38
- const defUi = defaults.uiBaseUrl || "https://console.postgres.ai";
39
- const apiCandidate = (opts?.apiBaseUrl || process.env.PGAI_API_BASE_URL || cfg?.baseUrl || defApi);
40
- const uiCandidate = (opts?.uiBaseUrl || process.env.PGAI_UI_BASE_URL || defUi);
41
- return {
42
- apiBaseUrl: normalizeBaseUrl(apiCandidate),
43
- uiBaseUrl: normalizeBaseUrl(uiCandidate),
44
- };
45
- }
46
- //# sourceMappingURL=util.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../../lib/util.ts"],"names":[],"mappings":";;AAAA,gCAKC;AAqBD,4CAUC;AAOD,0CAeC;AA1DD,SAAgB,UAAU,CAAC,MAAc;IACvC,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IACtC,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE;QAAE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3G,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAChI,CAAC;AAiBD;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,KAAa;IAC5C,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,WAAW;QACX,kCAAkC;QAClC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAC7B,IAAmB,EACnB,GAAgB,EAChB,WAAwD,EAAE;IAE1D,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,IAAI,kCAAkC,CAAC;IACzE,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,IAAI,6BAA6B,CAAC;IAElE,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,GAAG,EAAE,OAAO,IAAI,MAAM,CAAW,CAAC;IAC7G,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,KAAK,CAAW,CAAC;IAEzF,OAAO;QACL,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC;QAC1C,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC;KACzC,CAAC;AACJ,CAAC"}
package/dist/package.json DELETED
@@ -1,46 +0,0 @@
1
- {
2
- "name": "postgresai",
3
- "version": "0.14.0-dev.7",
4
- "description": "postgres_ai CLI (Node.js)",
5
- "license": "Apache-2.0",
6
- "private": false,
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://gitlab.com/postgres-ai/postgres_ai.git"
10
- },
11
- "homepage": "https://gitlab.com/postgres-ai/postgres_ai",
12
- "bugs": {
13
- "url": "https://gitlab.com/postgres-ai/postgres_ai/-/issues"
14
- },
15
- "bin": {
16
- "postgres-ai": "./dist/bin/postgres-ai.js",
17
- "postgresai": "./dist/bin/postgres-ai.js",
18
- "pgai": "./dist/bin/postgres-ai.js"
19
- },
20
- "type": "commonjs",
21
- "engines": {
22
- "node": ">=18"
23
- },
24
- "scripts": {
25
- "build": "tsc",
26
- "prepare": "npm run build",
27
- "start": "node ./dist/bin/postgres-ai.js --help",
28
- "dev": "tsc --watch",
29
- "test": "npm run build && node --test test/*.test.cjs"
30
- },
31
- "dependencies": {
32
- "@modelcontextprotocol/sdk": "^1.20.2",
33
- "commander": "^12.1.0",
34
- "js-yaml": "^4.1.0",
35
- "pg": "^8.16.3"
36
- },
37
- "devDependencies": {
38
- "@types/js-yaml": "^4.0.9",
39
- "@types/node": "^18.19.0",
40
- "@types/pg": "^8.15.6",
41
- "typescript": "^5.3.3"
42
- },
43
- "publishConfig": {
44
- "access": "public"
45
- }
46
- }
@@ -1,269 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const fs = require("node:fs");
4
- const os = require("node:os");
5
- const path = require("node:path");
6
- const net = require("node:net");
7
- const { spawn, spawnSync } = require("node:child_process");
8
-
9
- function findOnPath(cmd) {
10
- const which = spawnSync("sh", ["-lc", `command -v ${cmd}`], { encoding: "utf8" });
11
- if (which.status === 0) return String(which.stdout || "").trim();
12
- return null;
13
- }
14
-
15
- function findPgBin(cmd) {
16
- const p = findOnPath(cmd);
17
- if (p) return p;
18
-
19
- // Debian/Ubuntu (GitLab CI node:*-bullseye images): binaries usually live here.
20
- // We avoid filesystem globbing in JS and just ask the shell.
21
- const probe = spawnSync(
22
- "sh",
23
- [
24
- "-lc",
25
- `ls -1 /usr/lib/postgresql/*/bin/${cmd} 2>/dev/null | head -n 1 || true`,
26
- ],
27
- { encoding: "utf8" }
28
- );
29
- const out = String(probe.stdout || "").trim();
30
- if (out) return out;
31
-
32
- return null;
33
- }
34
-
35
- function havePostgresBinaries() {
36
- return !!(findPgBin("initdb") && findPgBin("postgres"));
37
- }
38
-
39
- async function getFreePort() {
40
- return await new Promise((resolve, reject) => {
41
- const srv = net.createServer();
42
- srv.listen(0, "127.0.0.1", () => {
43
- const addr = srv.address();
44
- srv.close((err) => {
45
- if (err) return reject(err);
46
- resolve(addr.port);
47
- });
48
- });
49
- srv.on("error", reject);
50
- });
51
- }
52
-
53
- async function waitFor(fn, { timeoutMs = 10000, intervalMs = 100 } = {}) {
54
- const start = Date.now();
55
- // eslint-disable-next-line no-constant-condition
56
- while (true) {
57
- try {
58
- return await fn();
59
- } catch (e) {
60
- if (Date.now() - start > timeoutMs) throw e;
61
- await new Promise((r) => setTimeout(r, intervalMs));
62
- }
63
- }
64
- }
65
-
66
- async function withTempPostgres(t) {
67
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "postgresai-init-"));
68
- const dataDir = path.join(tmpRoot, "data");
69
- const socketDir = path.join(tmpRoot, "sock");
70
- fs.mkdirSync(socketDir, { recursive: true });
71
-
72
- const initdb = findPgBin("initdb");
73
- const postgresBin = findPgBin("postgres");
74
- assert.ok(initdb && postgresBin, "PostgreSQL binaries not found (need initdb and postgres)");
75
-
76
- const init = spawnSync(initdb, ["-D", dataDir, "-U", "postgres", "-A", "trust"], {
77
- encoding: "utf8",
78
- });
79
- assert.equal(init.status, 0, init.stderr || init.stdout);
80
-
81
- // Configure: local socket trust, TCP scram.
82
- const hbaPath = path.join(dataDir, "pg_hba.conf");
83
- fs.appendFileSync(
84
- hbaPath,
85
- "\n# Added by postgresai init integration tests\nlocal all all trust\nhost all all 127.0.0.1/32 scram-sha-256\nhost all all ::1/128 scram-sha-256\n",
86
- "utf8"
87
- );
88
-
89
- const port = await getFreePort();
90
-
91
- const postgresProc = spawn(postgresBin, ["-D", dataDir, "-k", socketDir, "-h", "127.0.0.1", "-p", String(port)], {
92
- stdio: ["ignore", "pipe", "pipe"],
93
- });
94
-
95
- const { Client } = require("pg");
96
-
97
- const connectLocal = async (database = "postgres") => {
98
- // IMPORTANT: must match the port Postgres is started with; otherwise pg defaults to 5432 and the socket path won't exist.
99
- const c = new Client({ host: socketDir, port, user: "postgres", database });
100
- await c.connect();
101
- return c;
102
- };
103
-
104
- await waitFor(async () => {
105
- const c = await connectLocal();
106
- await c.end();
107
- });
108
-
109
- const postgresPassword = "postgrespw";
110
- {
111
- const c = await connectLocal();
112
- await c.query("alter user postgres password $1", [postgresPassword]);
113
- await c.query("create database testdb");
114
- await c.end();
115
- }
116
-
117
- t.after(async () => {
118
- postgresProc.kill("SIGTERM");
119
- try {
120
- await waitFor(
121
- async () => {
122
- if (postgresProc.exitCode === null) throw new Error("still running");
123
- },
124
- { timeoutMs: 5000, intervalMs: 100 }
125
- );
126
- } catch {
127
- postgresProc.kill("SIGKILL");
128
- }
129
- fs.rmSync(tmpRoot, { recursive: true, force: true });
130
- });
131
-
132
- const adminUri = `postgresql://postgres:${postgresPassword}@127.0.0.1:${port}/testdb`;
133
- return { port, socketDir, adminUri, postgresPassword };
134
- }
135
-
136
- async function runCliInit(args, env = {}) {
137
- const node = process.execPath;
138
- const cliPath = path.resolve(__dirname, "..", "dist", "bin", "postgres-ai.js");
139
- const res = spawnSync(node, [cliPath, "init", ...args], {
140
- encoding: "utf8",
141
- env: { ...process.env, ...env },
142
- });
143
- return res;
144
- }
145
-
146
- test(
147
- "integration: init supports URI / conninfo / psql-like connection styles",
148
- { skip: !havePostgresBinaries() },
149
- async (t) => {
150
- const pg = await withTempPostgres(t);
151
-
152
- // 1) positional URI
153
- {
154
- const r = await runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
155
- assert.equal(r.status, 0, r.stderr || r.stdout);
156
- }
157
-
158
- // 2) conninfo
159
- {
160
- const conninfo = `dbname=testdb host=127.0.0.1 port=${pg.port} user=postgres password=${pg.postgresPassword}`;
161
- const r = await runCliInit([conninfo, "--password", "monpw2", "--skip-optional-permissions"]);
162
- assert.equal(r.status, 0, r.stderr || r.stdout);
163
- }
164
-
165
- // 3) psql-like options (+ PGPASSWORD)
166
- {
167
- const r = await runCliInit(
168
- [
169
- "-h",
170
- "127.0.0.1",
171
- "-p",
172
- String(pg.port),
173
- "-U",
174
- "postgres",
175
- "-d",
176
- "testdb",
177
- "--password",
178
- "monpw3",
179
- "--skip-optional-permissions",
180
- ],
181
- { PGPASSWORD: pg.postgresPassword }
182
- );
183
- assert.equal(r.status, 0, r.stderr || r.stdout);
184
- }
185
- }
186
- );
187
-
188
- test(
189
- "integration: init fixes slightly-off permissions idempotently",
190
- { skip: !havePostgresBinaries() },
191
- async (t) => {
192
- const pg = await withTempPostgres(t);
193
- const { Client } = require("pg");
194
-
195
- // Create monitoring role with wrong password, no grants.
196
- {
197
- const c = new Client({ connectionString: pg.adminUri });
198
- await c.connect();
199
- await c.query(
200
- "do $$ begin if not exists (select 1 from pg_roles where rolname='postgres_ai_mon') then create role postgres_ai_mon login password 'wrong'; end if; end $$;"
201
- );
202
- await c.end();
203
- }
204
-
205
- // Run init (should grant everything).
206
- {
207
- const r = await runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
208
- assert.equal(r.status, 0, r.stderr || r.stdout);
209
- }
210
-
211
- // Verify privileges.
212
- {
213
- const c = new Client({ connectionString: pg.adminUri });
214
- await c.connect();
215
- const dbOk = await c.query(
216
- "select has_database_privilege('postgres_ai_mon', current_database(), 'CONNECT') as ok"
217
- );
218
- assert.equal(dbOk.rows[0].ok, true);
219
- const roleOk = await c.query("select pg_has_role('postgres_ai_mon', 'pg_monitor', 'member') as ok");
220
- assert.equal(roleOk.rows[0].ok, true);
221
- const idxOk = await c.query(
222
- "select has_table_privilege('postgres_ai_mon', 'pg_catalog.pg_index', 'SELECT') as ok"
223
- );
224
- assert.equal(idxOk.rows[0].ok, true);
225
- const viewOk = await c.query(
226
- "select has_table_privilege('postgres_ai_mon', 'public.pg_statistic', 'SELECT') as ok"
227
- );
228
- assert.equal(viewOk.rows[0].ok, true);
229
- const sp = await c.query("select rolconfig from pg_roles where rolname='postgres_ai_mon'");
230
- assert.ok(Array.isArray(sp.rows[0].rolconfig));
231
- assert.ok(sp.rows[0].rolconfig.some((v) => String(v).includes("search_path=")));
232
- await c.end();
233
- }
234
-
235
- // Run init again (idempotent).
236
- {
237
- const r = await runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
238
- assert.equal(r.status, 0, r.stderr || r.stdout);
239
- }
240
- }
241
- );
242
-
243
- test("integration: init reports nicely when lacking permissions", { skip: !havePostgresBinaries() }, async (t) => {
244
- const pg = await withTempPostgres(t);
245
- const { Client } = require("pg");
246
-
247
- // Create limited user that can connect but cannot create roles / grant.
248
- const limitedPw = "limitedpw";
249
- {
250
- const c = new Client({ connectionString: pg.adminUri });
251
- await c.connect();
252
- await c.query(
253
- "do $$ begin if not exists (select 1 from pg_roles where rolname='limited') then create role limited login password $1; end if; end $$;",
254
- [limitedPw]
255
- );
256
- await c.query("grant connect on database testdb to limited");
257
- await c.end();
258
- }
259
-
260
- const limitedUri = `postgresql://limited:${limitedPw}@127.0.0.1:${pg.port}/testdb`;
261
- const r = await runCliInit([limitedUri, "--password", "monpw", "--skip-optional-permissions"]);
262
- assert.notEqual(r.status, 0);
263
- assert.match(r.stderr, /init failed:/);
264
- // Should include step context and hint.
265
- assert.match(r.stderr, /Failed at step "/);
266
- assert.match(r.stderr, /Hint: connect as a superuser/i);
267
- });
268
-
269
-
@@ -1,69 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert/strict");
3
-
4
- // These tests intentionally import the compiled JS output.
5
- // Run via: npm --prefix cli test
6
- const init = require("../dist/lib/init.js");
7
-
8
- test("maskConnectionString hides password when present", () => {
9
- const masked = init.maskConnectionString("postgresql://user:secret@localhost:5432/mydb");
10
- assert.match(masked, /postgresql:\/\/user:\*{5}@localhost:5432\/mydb/);
11
- assert.doesNotMatch(masked, /secret/);
12
- });
13
-
14
- test("parseLibpqConninfo parses basic host/dbname/user/port/password", () => {
15
- const cfg = init.parseLibpqConninfo("dbname=mydb host=localhost user=alice port=5432 password=secret");
16
- assert.equal(cfg.database, "mydb");
17
- assert.equal(cfg.host, "localhost");
18
- assert.equal(cfg.user, "alice");
19
- assert.equal(cfg.port, 5432);
20
- assert.equal(cfg.password, "secret");
21
- });
22
-
23
- test("parseLibpqConninfo supports quoted values", () => {
24
- const cfg = init.parseLibpqConninfo("dbname='my db' host='local host'");
25
- assert.equal(cfg.database, "my db");
26
- assert.equal(cfg.host, "local host");
27
- });
28
-
29
- test("buildInitPlan includes create user when role does not exist", async () => {
30
- const plan = await init.buildInitPlan({
31
- database: "mydb",
32
- monitoringUser: "postgres_ai_mon",
33
- monitoringPassword: "pw",
34
- includeOptionalPermissions: false,
35
- roleExists: false,
36
- });
37
-
38
- assert.equal(plan.database, "mydb");
39
- assert.ok(plan.steps.some((s) => s.name === "create monitoring user"));
40
- assert.ok(!plan.steps.some((s) => s.optional));
41
- });
42
-
43
- test("buildInitPlan includes alter user when role exists", async () => {
44
- const plan = await init.buildInitPlan({
45
- database: "mydb",
46
- monitoringUser: "postgres_ai_mon",
47
- monitoringPassword: "pw",
48
- includeOptionalPermissions: true,
49
- roleExists: true,
50
- });
51
-
52
- assert.ok(plan.steps.some((s) => s.name === "update monitoring user password"));
53
- assert.ok(plan.steps.some((s) => s.optional));
54
- });
55
-
56
- test("resolveAdminConnection accepts positional URI", () => {
57
- const r = init.resolveAdminConnection({ conn: "postgresql://u:p@h:5432/d" });
58
- assert.ok(r.clientConfig.connectionString);
59
- assert.doesNotMatch(r.display, /:p@/);
60
- });
61
-
62
- test("resolveAdminConnection accepts positional conninfo", () => {
63
- const r = init.resolveAdminConnection({ conn: "dbname=mydb host=localhost user=alice" });
64
- assert.equal(r.clientConfig.database, "mydb");
65
- assert.equal(r.clientConfig.host, "localhost");
66
- assert.equal(r.clientConfig.user, "alice");
67
- });
68
-
69
-