postgresai 0.17.0-rc.1 → 0.17.0-rc.2

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.
package/README.md CHANGED
@@ -250,7 +250,7 @@ postgresai mon health [--wait <sec>] # Check monitoring services health
250
250
 
251
251
  When `--instance-id <uuid>` (or `PGAI_INSTANCE_ID`) is set, `local-install` forwards the id to the platform, which **adopts** the already-provisioned monitoring instance instead of self-registering a duplicate under an auto-created `postgres-ai-monitoring` project. The CLI then persists the adopted instance's real project to `.pgwatch-config`, so checkup reports upload alongside the rest of that instance's health data. Adoption is awaited (with one automatic retry); if it fails, the CLI warns and reports fall back to the default project until you re-run `local-install`. Without the flag, the legacy self-registration path is byte-for-byte unchanged.
252
252
 
253
- `local-install` writes `.env` in the monitoring directory. It preserves existing `REPLICATOR_PASSWORD` and `VM_AUTH_*` values or generates new random ones when missing; `VM_AUTH_USERNAME` defaults to `vmauth` when absent. The replication password is used by the demo PostgreSQL standby replication user, and the VM auth credentials are required before Docker Compose can provision Grafana datasources. If you run `docker compose` directly or maintain `.env` yourself, set both VM auth values before upgrading. For rotation, run `VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh` from the monitoring directory so `.env`, `sink-prometheus`, and `grafana` update together.
253
+ `local-install` writes `.env` in the monitoring directory. It preserves existing `REPLICATOR_PASSWORD`, `VM_AUTH_*` and the VictoriaMetrics admin-endpoint keys (`VM_DELETE_AUTH_KEY`, `VM_SNAPSHOT_AUTH_KEY`, `VM_FORCE_MERGE_AUTH_KEY`, `VM_PPROF_AUTH_KEY`, added in 0.17) or generates new random ones when missing. The admin keys live on the `sink-prometheus` command line, so `mon update` and `mon update-config` also recreate that container to apply them; `mon restart` (`docker compose restart`) does not; `VM_AUTH_USERNAME` defaults to `vmauth` when absent. The replication password is used by the demo PostgreSQL standby replication user, and the VM auth credentials are required before Docker Compose can provision Grafana datasources. If you run `docker compose` directly or maintain `.env` yourself, set both VM auth values before upgrading. For rotation, run `VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh` from the monitoring directory so `.env`, `sink-prometheus`, and `grafana` update together.
254
254
 
255
255
  #### Monitoring target databases (`mon targets` subgroup)
256
256
  ```bash
@@ -301,8 +301,8 @@ run `pgai orgs`) to discover the available ids.
301
301
  Tools exposed:
302
302
  - `list_issues`: returns the same JSON as `postgresai issues list` (args: `{ org_id, status?, hidden_only?, limit?, offset?, debug? }`).
303
303
  - `view_issue`: view a single issue with its comments (args: `{ issue_id, org_id, debug? }`).
304
- - `create_issue`: create a new issue (args: `{ title, description?, org_id, attachments?, debug? }`).
305
- - `update_issue`: update title/description/status/labels (args: `{ issue_id, org_id, title?, description?, status?, labels?, attachments?, debug? }`).
304
+ - `create_issue`: create a new issue (args: `{ title, description?, org_id, project_id?, labels?, attachments?, is_hidden?, debug? }`).
305
+ - `update_issue`: update title/description/status/labels/is_hidden (args: `{ issue_id, org_id, title?, description?, status?, labels?, attachments?, is_hidden?, debug? }`).
306
306
  - `post_issue_comment`: post a comment (args: `{ issue_id, org_id, content?, parent_comment_id?, attachments?, debug? }`).
307
307
  - `update_issue_comment`: update an existing comment (args: `{ comment_id, org_id, content?, attachments?, debug? }`).
308
308
  - `upload_file`: upload a local file and return the storage URL plus a ready-to-paste markdown link (args: `{ path, org_id, debug? }`).
@@ -353,8 +353,10 @@ sensitive.
353
353
  postgresai issues list # List issues (shows: id, title, status, created_at; is_hidden only when set)
354
354
  postgresai issues list --hidden-only # Only hidden issues (PostgresAI staff)
355
355
  postgresai issues view <issueId> # View issue details and comments
356
- postgresai issues create --org-id <id> --title <t> # Create a new issue
356
+ postgresai issues create --org-id <id> <title> # Create a new issue
357
+ postgresai issues create --org-id <id> <title> --hidden # Create a hidden issue (PostgresAI staff)
357
358
  postgresai issues update <issueId> [--title ... --status ...]# Update an existing issue
359
+ postgresai issues update <issueId> --hidden|--no-hidden # Hide / unhide an issue (PostgresAI staff)
358
360
  postgresai issues post-comment <issueId> <content> # Post a comment to an issue
359
361
  postgresai issues update-comment <commentId> <content> # Update an existing comment
360
362
  postgresai issues files upload <path> # Upload a file, print URL + markdown
@@ -370,7 +372,11 @@ postgresai issues files download <url> [-o <path>] # Download a file
370
372
  Hidden issues are staff-internal. `issues list` and `issues view` mark them
371
373
  with `is_hidden: true`; the key is omitted entirely otherwise, so ordinary
372
374
  issues look exactly as they always have. `--hidden-only` lists just the hidden
373
- ones, filtered server-side.
375
+ ones, filtered server-side. `issues create --hidden` creates one, and
376
+ `issues update --hidden` / `--no-hidden` hides or unhides an existing issue
377
+ (MCP: `is_hidden` on `create_issue` / `update_issue`). The CLI does no staff
378
+ check of its own: the platform refuses a visibility change for a non-staff
379
+ credential with a plain error, and the CLI prints it and exits non-zero.
374
380
 
375
381
  Staff access is granted per credential, not per person, and a credential that
376
382
  does not qualify simply sees nothing — `--hidden-only` returns an empty list
@@ -13524,7 +13524,7 @@ var {
13524
13524
  // package.json
13525
13525
  var package_default = {
13526
13526
  name: "postgresai",
13527
- version: "0.17.0-rc.1",
13527
+ version: "0.17.0-rc.2",
13528
13528
  description: "postgres_ai CLI",
13529
13529
  license: "Apache-2.0",
13530
13530
  private: false,
@@ -16884,7 +16884,7 @@ var Result = import_lib.default.Result;
16884
16884
  var TypeOverrides = import_lib.default.TypeOverrides;
16885
16885
  var defaults = import_lib.default.defaults;
16886
16886
  // package.json
16887
- var version = "0.17.0-rc.1";
16887
+ var version = "0.17.0-rc.2";
16888
16888
  var package_default2 = {
16889
16889
  name: "postgresai",
16890
16890
  version,
@@ -17473,7 +17473,7 @@ async function fetchIssue(params) {
17473
17473
  }
17474
17474
  }
17475
17475
  async function createIssue(params) {
17476
- const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, debug } = params;
17476
+ const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, hidden, debug } = params;
17477
17477
  if (!apiKey) {
17478
17478
  throw new Error("API key is required");
17479
17479
  }
@@ -17498,6 +17498,9 @@ async function createIssue(params) {
17498
17498
  if (labels && labels.length > 0) {
17499
17499
  bodyObj.labels = labels;
17500
17500
  }
17501
+ if (hidden === true) {
17502
+ bodyObj.is_hidden = true;
17503
+ }
17501
17504
  const body = JSON.stringify(bodyObj);
17502
17505
  const headers = issueRequestHeaders(apiKey, orgScope);
17503
17506
  if (debug) {
@@ -17579,15 +17582,15 @@ async function createIssueComment(params) {
17579
17582
  }
17580
17583
  }
17581
17584
  async function updateIssue(params) {
17582
- const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, debug } = params;
17585
+ const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, hidden, debug } = params;
17583
17586
  if (!apiKey) {
17584
17587
  throw new Error("API key is required");
17585
17588
  }
17586
17589
  if (!issueId) {
17587
17590
  throw new Error("issueId is required");
17588
17591
  }
17589
- if (title === undefined && description === undefined && status === undefined && labels === undefined) {
17590
- throw new Error("At least one field to update is required (title, description, status, or labels)");
17592
+ if (title === undefined && description === undefined && status === undefined && labels === undefined && hidden === undefined) {
17593
+ throw new Error("At least one field to update is required (title, description, status, labels, or hidden)");
17591
17594
  }
17592
17595
  const base = normalizeBaseUrl(apiBaseUrl);
17593
17596
  const url = new URL(`${base}/rpc/issue_update`);
@@ -17606,6 +17609,9 @@ async function updateIssue(params) {
17606
17609
  if (labels !== undefined) {
17607
17610
  bodyObj.p_labels = labels;
17608
17611
  }
17612
+ if (hidden !== undefined) {
17613
+ bodyObj.p_is_hidden = hidden;
17614
+ }
17609
17615
  const body = JSON.stringify(bodyObj);
17610
17616
  const headers = issueRequestHeaders(apiKey, orgScope);
17611
17617
  if (debug) {
@@ -24947,6 +24953,13 @@ var ORG_ID_TOOL_PROPERTY = {
24947
24953
  description: "Organization ID. Optional with a per-organization token (uses the stored org); REQUIRED with a global token, which can reach every organization the user belongs to and will not assume one. Use orgs_list / `pgai orgs` to discover valid values."
24948
24954
  }
24949
24955
  };
24956
+ function parseHiddenArg(raw) {
24957
+ if (raw === undefined || raw === null)
24958
+ return {};
24959
+ if (typeof raw === "boolean")
24960
+ return { value: raw };
24961
+ return { error: { content: [{ type: "text", text: "is_hidden must be a boolean" }], isError: true } };
24962
+ }
24950
24963
  async function handleToolCall(req, rootOpts, extra) {
24951
24964
  const toolName = req.params.name;
24952
24965
  const args = req.params.arguments || {};
@@ -25033,6 +25046,10 @@ async function handleToolCall(req, rootOpts, extra) {
25033
25046
  let description = rawDescription ? interpretEscapes(rawDescription) : undefined;
25034
25047
  const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
25035
25048
  const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
25049
+ const hiddenArg = parseHiddenArg(args.is_hidden);
25050
+ if (hiddenArg.error)
25051
+ return hiddenArg.error;
25052
+ const hidden = hiddenArg.value === true;
25036
25053
  const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
25037
25054
  const scope = resolveMcpOrgScope(args, apiKey, cfg);
25038
25055
  if (scope.error)
@@ -25046,7 +25063,7 @@ async function handleToolCall(req, rootOpts, extra) {
25046
25063
  const uploaded = await uploadAttachments({ apiKey, orgScope: scope.orgScope, storageBaseUrl, attachmentPaths: attachments, debug });
25047
25064
  description = appendAttachmentsToContent(description ?? "", uploaded);
25048
25065
  }
25049
- const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, orgScope: scope.orgScope, description, projectId, labels, debug });
25066
+ const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, orgScope: scope.orgScope, description, projectId, labels, hidden, debug });
25050
25067
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25051
25068
  }
25052
25069
  if (toolName === "update_issue") {
@@ -25060,9 +25077,13 @@ async function handleToolCall(req, rootOpts, extra) {
25060
25077
  let description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
25061
25078
  const status = args.status !== undefined ? Number(args.status) : undefined;
25062
25079
  const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
25080
+ const hiddenArg = parseHiddenArg(args.is_hidden);
25081
+ if (hiddenArg.error)
25082
+ return hiddenArg.error;
25083
+ const hidden = hiddenArg.value;
25063
25084
  const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
25064
- if (title === undefined && description === undefined && status === undefined && labels === undefined && attachments.length === 0) {
25065
- return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, labels, or attachments)" }], isError: true };
25085
+ if (title === undefined && description === undefined && status === undefined && labels === undefined && hidden === undefined && attachments.length === 0) {
25086
+ return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, labels, is_hidden, or attachments)" }], isError: true };
25066
25087
  }
25067
25088
  if (status !== undefined && (Number.isNaN(status) || status !== 0 && status !== 1)) {
25068
25089
  return { content: [{ type: "text", text: "status must be 0 (open) or 1 (closed)" }], isError: true };
@@ -25082,7 +25103,7 @@ async function handleToolCall(req, rootOpts, extra) {
25082
25103
  const uploaded = await uploadAttachments({ apiKey, orgScope: scope.orgScope, storageBaseUrl, attachmentPaths: attachments, debug });
25083
25104
  description = appendAttachmentsToContent(description, uploaded);
25084
25105
  }
25085
- const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, orgScope: scope.orgScope, debug });
25106
+ const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, hidden, orgScope: scope.orgScope, debug });
25086
25107
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25087
25108
  }
25088
25109
  if (toolName === "update_issue_comment") {
@@ -25357,6 +25378,7 @@ async function startMcpServer(rootOpts, extra) {
25357
25378
  items: { type: "string" },
25358
25379
  description: "Local file paths to upload and append as markdown links to the description (images render inline)"
25359
25380
  },
25381
+ is_hidden: { type: "boolean", description: "Create as a hidden, staff-internal issue (PostgresAI staff only; same as CLI --hidden)" },
25360
25382
  debug: { type: "boolean", description: "Enable verbose debug logs" }
25361
25383
  },
25362
25384
  required: ["title"],
@@ -25365,7 +25387,7 @@ async function startMcpServer(rootOpts, extra) {
25365
25387
  },
25366
25388
  {
25367
25389
  name: "update_issue",
25368
- description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen. Local files passed via 'attachments' are uploaded and appended to 'description'; if 'description' is omitted, the existing description is fetched first and appended to.",
25390
+ description: "Update an existing issue (title, description, status, labels, is_hidden). Use status=1 to close, status=0 to reopen. Local files passed via 'attachments' are uploaded and appended to 'description'; if 'description' is omitted, the existing description is fetched first and appended to.",
25369
25391
  inputSchema: {
25370
25392
  type: "object",
25371
25393
  properties: {
@@ -25384,6 +25406,7 @@ async function startMcpServer(rootOpts, extra) {
25384
25406
  items: { type: "string" },
25385
25407
  description: "Local file paths to upload and append as markdown links (images render inline). When provided without 'description', the existing description is fetched and appended to."
25386
25408
  },
25409
+ is_hidden: { type: "boolean", description: "true hides the issue, false unhides it (PostgresAI staff only; same as CLI --hidden / --no-hidden). Omit (or pass null) to leave unchanged." },
25387
25410
  debug: { type: "boolean", description: "Enable verbose debug logs" }
25388
25411
  },
25389
25412
  required: ["issue_id"],
@@ -25731,7 +25754,7 @@ async function fetchIssue2(params) {
25731
25754
  }
25732
25755
  }
25733
25756
  async function createIssue2(params) {
25734
- const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, debug } = params;
25757
+ const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, hidden, debug } = params;
25735
25758
  if (!apiKey) {
25736
25759
  throw new Error("API key is required");
25737
25760
  }
@@ -25756,6 +25779,9 @@ async function createIssue2(params) {
25756
25779
  if (labels && labels.length > 0) {
25757
25780
  bodyObj.labels = labels;
25758
25781
  }
25782
+ if (hidden === true) {
25783
+ bodyObj.is_hidden = true;
25784
+ }
25759
25785
  const body = JSON.stringify(bodyObj);
25760
25786
  const headers = issueRequestHeaders2(apiKey, orgScope);
25761
25787
  if (debug) {
@@ -25837,15 +25863,15 @@ async function createIssueComment2(params) {
25837
25863
  }
25838
25864
  }
25839
25865
  async function updateIssue2(params) {
25840
- const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, debug } = params;
25866
+ const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, hidden, debug } = params;
25841
25867
  if (!apiKey) {
25842
25868
  throw new Error("API key is required");
25843
25869
  }
25844
25870
  if (!issueId) {
25845
25871
  throw new Error("issueId is required");
25846
25872
  }
25847
- if (title === undefined && description === undefined && status === undefined && labels === undefined) {
25848
- throw new Error("At least one field to update is required (title, description, status, or labels)");
25873
+ if (title === undefined && description === undefined && status === undefined && labels === undefined && hidden === undefined) {
25874
+ throw new Error("At least one field to update is required (title, description, status, labels, or hidden)");
25849
25875
  }
25850
25876
  const base = normalizeBaseUrl(apiBaseUrl);
25851
25877
  const url = new URL(`${base}/rpc/issue_update`);
@@ -25864,6 +25890,9 @@ async function updateIssue2(params) {
25864
25890
  if (labels !== undefined) {
25865
25891
  bodyObj.p_labels = labels;
25866
25892
  }
25893
+ if (hidden !== undefined) {
25894
+ bodyObj.p_is_hidden = hidden;
25895
+ }
25867
25896
  const body = JSON.stringify(bodyObj);
25868
25897
  const headers = issueRequestHeaders2(apiKey, orgScope);
25869
25898
  if (debug) {
@@ -38672,47 +38701,101 @@ function stripMatchingQuotes(value) {
38672
38701
  }
38673
38702
  return trimmed;
38674
38703
  }
38704
+ var VM_ADMIN_AUTH_KEYS = [
38705
+ "VM_DELETE_AUTH_KEY",
38706
+ "VM_SNAPSHOT_AUTH_KEY",
38707
+ "VM_FORCE_MERGE_AUTH_KEY",
38708
+ "VM_PPROF_AUTH_KEY"
38709
+ ];
38675
38710
  var REQUIRED_ENV_KEYS = [
38676
38711
  { key: "REPLICATOR_PASSWORD", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.13" },
38677
38712
  { key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
38678
- { key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" }
38713
+ { key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" },
38714
+ { key: "VM_DELETE_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" },
38715
+ { key: "VM_SNAPSHOT_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" },
38716
+ { key: "VM_FORCE_MERGE_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" },
38717
+ { key: "VM_PPROF_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" }
38679
38718
  ];
38680
38719
  function ensureRequiredEnvVars(projectDir) {
38681
38720
  const envFile = path7.resolve(projectDir, ".env");
38682
38721
  const existing = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
38683
38722
  const added = [];
38684
38723
  const appendLines = [];
38724
+ let content = existing;
38685
38725
  for (const spec of REQUIRED_ENV_KEYS) {
38686
- const re = new RegExp(`^${spec.key}=`, "m");
38687
- if (!re.test(existing)) {
38726
+ const present = new RegExp(`^[ ]*(?:export[ ]+)?${spec.key}=`, "m");
38727
+ if (!present.test(content)) {
38688
38728
  appendLines.push(`${spec.key}=${spec.defaultValue()}`);
38689
38729
  added.push(spec.key);
38730
+ continue;
38731
+ }
38732
+ if (VM_ADMIN_AUTH_KEYS.includes(spec.key)) {
38733
+ const blank = new RegExp(`^([ ]*(?:export[ ]+)?${spec.key}=)[ ]*(?:""|'')?[ ]*\r?$`, "gm");
38734
+ if (blank.test(content)) {
38735
+ const value = spec.defaultValue();
38736
+ content = content.replace(blank, (_match, prefix) => `${prefix}${value}`);
38737
+ added.push(spec.key);
38738
+ }
38690
38739
  }
38691
38740
  }
38692
- if (appendLines.length === 0) {
38741
+ if (appendLines.length === 0 && content === existing) {
38693
38742
  return added;
38694
38743
  }
38695
- const needsTrailingNewline = existing.length > 0 && !existing.endsWith(`
38744
+ const needsTrailingNewline = content.length > 0 && !content.endsWith(`
38696
38745
  `);
38697
- const newContent = existing + (needsTrailingNewline ? `
38698
- ` : "") + appendLines.join(`
38746
+ const newContent = content + (needsTrailingNewline ? `
38747
+ ` : "") + (appendLines.length > 0 ? appendLines.join(`
38699
38748
  `) + `
38700
- `;
38701
- fs9.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
38749
+ ` : "");
38750
+ writeEnvFile(envFile, newContent);
38702
38751
  return added;
38703
38752
  }
38753
+ function writeEnvFile(envFile, content) {
38754
+ let targetFile = envFile;
38755
+ try {
38756
+ if (fs9.existsSync(envFile))
38757
+ targetFile = fs9.realpathSync(envFile);
38758
+ } catch {}
38759
+ const tmpFile = `${targetFile}.tmp`;
38760
+ fs9.writeFileSync(tmpFile, content, { encoding: "utf8", mode: 384 });
38761
+ try {
38762
+ fs9.chmodSync(tmpFile, 384);
38763
+ if (fs9.existsSync(targetFile)) {
38764
+ const st = fs9.statSync(targetFile);
38765
+ fs9.chownSync(tmpFile, st.uid, st.gid);
38766
+ }
38767
+ } catch {}
38768
+ try {
38769
+ fs9.renameSync(tmpFile, targetFile);
38770
+ } catch (error) {
38771
+ try {
38772
+ fs9.unlinkSync(tmpFile);
38773
+ } catch {}
38774
+ throw error;
38775
+ }
38776
+ }
38704
38777
  var LOCAL_INSTALL_MANAGED_ENV_KEYS = [
38705
38778
  "PGAI_TAG",
38706
38779
  "PGAI_REGISTRY",
38707
38780
  "GF_SECURITY_ADMIN_PASSWORD",
38708
38781
  "REPLICATOR_PASSWORD",
38709
38782
  "VM_AUTH_USERNAME",
38710
- "VM_AUTH_PASSWORD"
38783
+ "VM_AUTH_PASSWORD",
38784
+ "VM_DELETE_AUTH_KEY",
38785
+ "VM_SNAPSHOT_AUTH_KEY",
38786
+ "VM_FORCE_MERGE_AUTH_KEY",
38787
+ "VM_PPROF_AUTH_KEY"
38711
38788
  ];
38712
38789
  function buildLocalInstallEnv(existingEnv, imageTag) {
38713
38790
  const readRaw = (key) => {
38714
- const m = existingEnv.match(new RegExp(`^${key}=(.+)$`, "m"));
38715
- return m ? m[1].trim() : null;
38791
+ const re = new RegExp(`^[ ]*(?:export[ ]+)?${key}=(.*)$`, "gm");
38792
+ let value = null;
38793
+ for (const m of existingEnv.matchAll(re)) {
38794
+ value = m[1].trim();
38795
+ }
38796
+ if (value === null)
38797
+ return null;
38798
+ return stripMatchingQuotes(value) === "" ? null : value;
38716
38799
  };
38717
38800
  const envLines = [`PGAI_TAG=${imageTag}`];
38718
38801
  const registry = readRaw("PGAI_REGISTRY");
@@ -38727,11 +38810,15 @@ function buildLocalInstallEnv(existingEnv, imageTag) {
38727
38810
  envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername ? stripMatchingQuotes(vmAuthUsername) : "vmauth"}`);
38728
38811
  const vmAuthPassword = readRaw("VM_AUTH_PASSWORD");
38729
38812
  envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword ? stripMatchingQuotes(vmAuthPassword) : crypto2.randomBytes(18).toString("base64")}`);
38813
+ for (const key of VM_ADMIN_AUTH_KEYS) {
38814
+ const existing = readRaw(key);
38815
+ envLines.push(`${key}=${existing ? stripMatchingQuotes(existing) : crypto2.randomBytes(32).toString("hex")}`);
38816
+ }
38730
38817
  const preservedKeys = [];
38731
38818
  for (const line of existingEnv.split(/\r?\n/)) {
38732
38819
  if (!line.trim())
38733
38820
  continue;
38734
- const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
38821
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/);
38735
38822
  if (m) {
38736
38823
  if (LOCAL_INSTALL_MANAGED_ENV_KEYS.includes(m[1]))
38737
38824
  continue;
@@ -38743,9 +38830,9 @@ function buildLocalInstallEnv(existingEnv, imageTag) {
38743
38830
  `) + `
38744
38831
  `, preservedKeys };
38745
38832
  }
38746
- async function execFilePromise(file, args) {
38833
+ async function execFilePromise(file, args, options = {}) {
38747
38834
  return new Promise((resolve, reject) => {
38748
- childProcess.execFile(file, args, (error, stdout, stderr) => {
38835
+ childProcess.execFile(file, args, options, (error, stdout, stderr) => {
38749
38836
  if (error) {
38750
38837
  const err = error;
38751
38838
  err.code = typeof error.code === "number" ? error.code : 1;
@@ -40484,6 +40571,12 @@ function isDockerRunning() {
40484
40571
  return false;
40485
40572
  }
40486
40573
  }
40574
+ function shouldScaleOutNodeExporter(args) {
40575
+ const upIndex = args.indexOf("up");
40576
+ if (upIndex === -1)
40577
+ return false;
40578
+ return !args.slice(upIndex + 1).some((a) => !a.startsWith("-"));
40579
+ }
40487
40580
  function getComposeCmd() {
40488
40581
  const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
40489
40582
  if (tryCmd("docker", ["compose", "version"]))
@@ -40696,7 +40789,7 @@ async function runCompose(args, grafanaPassword) {
40696
40789
  }
40697
40790
  }
40698
40791
  const finalArgs = [...args];
40699
- if (process.platform === "darwin" && args.includes("up")) {
40792
+ if (process.platform === "darwin" && shouldScaleOutNodeExporter(args)) {
40700
40793
  finalArgs.push("--scale", "self-node-exporter=0");
40701
40794
  }
40702
40795
  return new Promise((resolve) => {
@@ -40708,6 +40801,51 @@ async function runCompose(args, grafanaPassword) {
40708
40801
  child.on("close", (code) => resolve(code || 0));
40709
40802
  });
40710
40803
  }
40804
+ async function applySinkPrometheusConfig(projectDir) {
40805
+ const composeCmd = getComposeCmd();
40806
+ if (!composeCmd) {
40807
+ console.error(`
40808
+ \u2717 docker compose not found, so the sink-prometheus config was not applied.`);
40809
+ return false;
40810
+ }
40811
+ let running;
40812
+ try {
40813
+ const { stdout } = await execFilePromise(composeCmd[0], [
40814
+ ...composeCmd.slice(1),
40815
+ "-f",
40816
+ path7.resolve(projectDir, "docker-compose.yml"),
40817
+ "ps",
40818
+ "-q",
40819
+ "sink-prometheus"
40820
+ ], { cwd: projectDir });
40821
+ running = stdout.trim().length > 0;
40822
+ } catch (error) {
40823
+ console.error(`
40824
+ \u2717 Could not determine whether sink-prometheus is running.`);
40825
+ console.error(" The admin-endpoint keys may NOT be in effect. To apply them:");
40826
+ console.error(" docker compose up -d --no-deps sink-prometheus");
40827
+ if (process.env.DEBUG) {
40828
+ console.error(` ${error instanceof Error ? error.message : String(error)}`);
40829
+ }
40830
+ return false;
40831
+ }
40832
+ if (!running) {
40833
+ console.log(`
40834
+ (sink-prometheus is not running - the new config applies when you start the stack)`);
40835
+ return true;
40836
+ }
40837
+ console.log(`
40838
+ Applying sink-prometheus config (VictoriaMetrics admin keys, #359)...`);
40839
+ const code = await runCompose(["up", "-d", "--no-deps", "sink-prometheus"]);
40840
+ if (code !== 0) {
40841
+ console.error("\u2717 Could not apply the sink-prometheus config.");
40842
+ console.error(" The admin-endpoint keys are NOT in effect until you run:");
40843
+ console.error(" docker compose up -d --no-deps sink-prometheus");
40844
+ return false;
40845
+ }
40846
+ console.log("\u2713 sink-prometheus is running the current config");
40847
+ return true;
40848
+ }
40711
40849
  program2.command("help").description("show help").action(() => {
40712
40850
  program2.outputHelp();
40713
40851
  });
@@ -40746,7 +40884,7 @@ mon.command("local-install").description("install local monitoring stack (genera
40746
40884
  const previousTag = previousTagMatch ? previousTagMatch[1].trim() : null;
40747
40885
  const imageTag = opts.tag || package_default.version;
40748
40886
  const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag);
40749
- fs9.writeFileSync(envFile, envContent, { encoding: "utf8", mode: 384 });
40887
+ writeEnvFile(envFile, envContent);
40750
40888
  if (preservedKeys.length > 0) {
40751
40889
  console.log(`Preserved existing .env settings: ${preservedKeys.join(", ")}
40752
40890
  `);
@@ -41042,9 +41180,9 @@ Searched: ${demoCandidates.join(", ")}
41042
41180
  const envLines = envContent.split(/\r?\n/).filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l)).filter((l, i, arr) => !(i === arr.length - 1 && l === ""));
41043
41181
  envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
41044
41182
  envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
41045
- fs9.writeFileSync(envFile, envLines.join(`
41183
+ writeEnvFile(envFile, envLines.join(`
41046
41184
  `) + `
41047
- `, { encoding: "utf8", mode: 384 });
41185
+ `);
41048
41186
  }
41049
41187
  console.log(`\u2713 VictoriaMetrics auth configured
41050
41188
  `);
@@ -41345,8 +41483,13 @@ mon.command("update-config").description("apply monitoring services configuratio
41345
41483
  }
41346
41484
  await refreshBundledComposeIfStale(projectDir);
41347
41485
  const code = await runCompose(["run", "--rm", "sources-generator"]);
41348
- if (code !== 0)
41486
+ if (code !== 0) {
41349
41487
  process.exitCode = code;
41488
+ return;
41489
+ }
41490
+ if (!await applySinkPrometheusConfig(projectDir)) {
41491
+ process.exitCode = 1;
41492
+ }
41350
41493
  });
41351
41494
  mon.command("update").description("update monitoring stack (migrate .env, pull images)").action(async () => {
41352
41495
  console.log(`Updating PostgresAI monitoring stack...
@@ -41388,6 +41531,10 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
41388
41531
  Updating Docker images...`);
41389
41532
  const code = await runCompose(["pull"]);
41390
41533
  if (code === 0) {
41534
+ if (!await applySinkPrometheusConfig(projectDir)) {
41535
+ process.exitCode = 1;
41536
+ return;
41537
+ }
41391
41538
  console.log(`
41392
41539
  \u2713 Update completed successfully`);
41393
41540
  console.log(`
@@ -42228,7 +42375,7 @@ withOrgOptions(issues.command("create <title>")).description("create a new issue
42228
42375
  }, []).option("--attach <path>", "attach a file (uploads to storage and appends a markdown link to the description; repeatable)", (value, previous) => {
42229
42376
  previous.push(value);
42230
42377
  return previous;
42231
- }, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
42378
+ }, []).option("--hidden", "create as a hidden issue (PostgresAI staff)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
42232
42379
  const rootOpts = program2.opts();
42233
42380
  const cfg = readConfig();
42234
42381
  const { apiKey } = getConfig(rootOpts);
@@ -42289,6 +42436,7 @@ withOrgOptions(issues.command("create <title>")).description("create a new issue
42289
42436
  description: augmentedDescription,
42290
42437
  projectId,
42291
42438
  labels,
42439
+ hidden: opts.hidden === true,
42292
42440
  debug: !!opts.debug
42293
42441
  });
42294
42442
  spinner.stop();
@@ -42306,7 +42454,7 @@ withOrgOptions(issues.command("update <issueId>")).description("update an existi
42306
42454
  }, []).option("--clear-labels", "set labels to an empty list").option("--attach <path>", "attach a file (uploads and appends a markdown link to --description; if --description is omitted the existing description is fetched and appended to; repeatable)", (value, previous) => {
42307
42455
  previous.push(value);
42308
42456
  return previous;
42309
- }, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
42457
+ }, []).option("--hidden", "hide the issue (PostgresAI staff)").option("--no-hidden", "unhide the issue (PostgresAI staff)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
42310
42458
  const rootOpts = program2.opts();
42311
42459
  const cfg = readConfig();
42312
42460
  const { apiKey } = getConfig(rootOpts);
@@ -42377,6 +42525,7 @@ withOrgOptions(issues.command("update <issueId>")).description("update an existi
42377
42525
  description,
42378
42526
  status,
42379
42527
  labels,
42528
+ hidden: typeof opts.hidden === "boolean" ? opts.hidden : undefined,
42380
42529
  debug: !!opts.debug
42381
42530
  });
42382
42531
  spinner.stop();
@@ -43371,5 +43520,6 @@ export {
43371
43520
  refreshBundledComposeIfStale,
43372
43521
  registerMonitoringInstance,
43373
43522
  resolveAdoptedProject,
43523
+ shouldScaleOutNodeExporter,
43374
43524
  updatePgwatchConfig
43375
43525
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.17.0-rc.1",
3
+ "version": "0.17.0-rc.2",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,