@pmate/cli 0.31.0 → 0.33.0

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
@@ -234,19 +234,19 @@ Notes:
234
234
  - `pmate vercel` is a thin wrapper around the installed `vercel` CLI.
235
235
  - v1 only supports `vercel.scope` in PMate config.
236
236
  - `pmate vercel deploys` requires `--project <name>`.
237
- # App-scoped access logs
237
+ # Service logs
238
238
 
239
- Developers access logs use the same authenticated API as `dev.pmate.chat`, so CLI users cannot bypass App-Service associations or `pmate:dev` authorization by reading the Indexer directly.
239
+ Service logs use the same authenticated API as `dev.pmate.chat`. Each registered Service has `app` (application/deployment) and `runtime` (PM2/Nginx) Table topics on the dedicated `log` chain.
240
240
 
241
241
  ```sh
242
- pmate service log pmate:developers-api --app pmate:developers --page 0 --json
243
- pmate service log pmate:developers-api --app pmate:developers --search http.access --since-ms 1790000000000 --json
242
+ pmate service log pmate:issues-api --topic application --page 0 --json
243
+ pmate service log pmate:issues-api --topic nginx --field status=500 --env prod
244
+ pmate service log pmate:issues-api --field requestId=<id> --follow
244
245
  ```
245
246
 
246
- Pages are zero-based. `--search` searches the last 24 hours; the ordinary list follows the Indexer's retention window (currently seven days). `--since-ms` is an epoch-millisecond lower bound. This mode does not accept `--env`, `--follow`, `--history`, or `--base-url`: environment filtering and follow are not implemented by the shared API and are never silently ignored. Without `--app`, the existing operational service-log command is unchanged.
247
-
248
- A current CLI session is exchanged in memory for `pmate:developers`; the active App and session file are not changed. If login is required, run `pmate login --app pmate:developers`. Access denial never falls back to anonymous log reads. Neither source nor exchanged tokens are printed.
247
+ Pages are zero-based. Lists and `--search` cover the chain's configured retention window (30 days in production). `--since-ms` adds an epoch-millisecond lower bound. `--field name=value` (repeatable, scalar JSON values) and `--env` filter on the server. Topics are independent sources such as `application`, `nginx`, `pm2`, `deployment`, `pv`, and `vitals`; custom fields live in `data`. Old `--type`, `--level`, `--source`, and `--request-id` flags are removed. The legacy `--app` argument is optional and does not select a different source.
249
248
 
249
+ A current CLI session is exchanged in memory for `pmate:developers`; the active App and session file are unchanged. If needed, run `pmate login --app pmate:developers`. Permission denial never falls back to anonymous Indexer reads. Credentials are never printed.
250
250
 
251
251
  ### Package a desktop app
252
252
 
package/dist/index.d.ts CHANGED
@@ -970,6 +970,8 @@ type ServiceCommandArgs = {
970
970
  } | {
971
971
  type: Command.Service;
972
972
  action: "log";
973
+ topic?: string;
974
+ fields?: Record<string, string | number | boolean>;
973
975
  app?: string;
974
976
  sinceMs?: number;
975
977
  namespace: string;
package/dist/index.js CHANGED
@@ -18740,6 +18740,51 @@ function capitalize(value) {
18740
18740
  return value.slice(0, 1).toUpperCase() + value.slice(1);
18741
18741
  }
18742
18742
 
18743
+ // src/services/DeveloperLogsClient.ts
18744
+ init_sessionStore();
18745
+ init_namespacedRef();
18746
+ var appRef = "pmate:developers";
18747
+ async function queryDeveloperLogs(input, fetcher = fetch) {
18748
+ if (input.app) parseNamespacedRef(input.app, { entityLabel: "app ref" });
18749
+ parseNamespacedRef(input.service, { entityLabel: "service ref" });
18750
+ if (!Number.isSafeInteger(input.page) || input.page < 0) throw new Error("Invalid log page");
18751
+ if (input.sinceMs !== void 0 && (!Number.isSafeInteger(input.sinceMs) || input.sinceMs < 0)) throw new Error("Invalid log time");
18752
+ if (input.keyword && input.keyword.length > 256) throw new Error("Log search must be at most 256 characters");
18753
+ const session = readRequiredSession({ preferredApp: appRef, fallbackToActiveApp: true });
18754
+ if (Date.parse(session.expiresAt) <= Date.now() || !Number.isFinite(Date.parse(session.expiresAt)))
18755
+ throw new Error("Session expired. Run pmate login --app pmate:developers");
18756
+ async function request2(url, init) {
18757
+ let response;
18758
+ try {
18759
+ response = await fetcher(url, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4) });
18760
+ } catch {
18761
+ throw new Error("Developers log request failed; no anonymous fallback was attempted");
18762
+ }
18763
+ if (!response.ok) throw new Error(`Developers log request failed (HTTP ${response.status}); check login, pmate:dev role`);
18764
+ let body;
18765
+ try {
18766
+ body = await response.json();
18767
+ } catch {
18768
+ throw new Error("Invalid Developers API response");
18769
+ }
18770
+ if (!body?.success) throw new Error("Developers API rejected the request");
18771
+ return body.data;
18772
+ }
18773
+ const exchanged = await request2("https://auth-api-v2.pmate.chat/session/exchange", {
18774
+ method: "POST",
18775
+ headers: { Authorization: `Bearer ${session.token}`, "Content-Type": "application/json" },
18776
+ body: JSON.stringify({ app: appRef })
18777
+ });
18778
+ if (exchanged?.session?.identity?.app !== appRef || exchanged.session.identity.accountId !== session.accountId || typeof exchanged.token !== "string" || !exchanged.token)
18779
+ throw new Error("Developers session identity mismatch");
18780
+ const params = new URLSearchParams({ service: input.service, page: String(input.page), topic: input.topic ?? "application" });
18781
+ if (input.environment) params.set("environment", input.environment);
18782
+ if (input.fields) params.set("fields", JSON.stringify(input.fields));
18783
+ if (input.keyword) params.set("keyword", input.keyword);
18784
+ if (input.sinceMs !== void 0) params.set("sinceMs", String(input.sinceMs));
18785
+ return request2(`https://dev-api.pmate.chat/api/logs?${params}`, { headers: { Authorization: `Bearer ${exchanged.token}` } });
18786
+ }
18787
+
18743
18788
  // src/services/ServiceRuntimeRunner.ts
18744
18789
  var import_node_fs22 = require("fs");
18745
18790
  var import_node_os12 = __toESM(require("os"));
@@ -19111,15 +19156,21 @@ var ServiceRuntimeRunner = class {
19111
19156
  const namespace = service.namespace;
19112
19157
  const appName = service.name;
19113
19158
  const page = await this.fetchLogPage(namespace, appName, {
19159
+ ...options,
19114
19160
  page: options.page ?? 0,
19115
19161
  history: options.history ?? false,
19116
19162
  search: options.search
19117
19163
  });
19118
- ui.title(`Logs for ${namespace}/${appName}`);
19164
+ if (options.json) {
19165
+ console.log(JSON.stringify(page));
19166
+ return;
19167
+ }
19168
+ ui.title(`Logs for ${namespace}/${appName} \xB7 ${options.topic ?? "app"}`);
19119
19169
  ui.info("Source", { mode: options.history ? "history" : "online" });
19120
19170
  this.renderPage(page);
19121
19171
  if (options.follow) {
19122
19172
  await this.followLogs(namespace, appName, {
19173
+ ...options,
19123
19174
  page: options.page ?? 0,
19124
19175
  history: options.history ?? false,
19125
19176
  search: options.search,
@@ -19136,7 +19187,7 @@ var ServiceRuntimeRunner = class {
19136
19187
  interactive?.rl.on("close", () => {
19137
19188
  running = false;
19138
19189
  });
19139
- const seen = new Set(options.seedItems.map((item) => item.hash));
19190
+ const seen = new Set(options.seedItems.map((item) => item.id ?? item.hash));
19140
19191
  ui.info(
19141
19192
  "Follow",
19142
19193
  interactive ? "enabled (poll every 2s, Ctrl+C to stop, /help for filters)" : "enabled (poll every 2s, Ctrl+C to stop)"
@@ -19147,6 +19198,7 @@ var ServiceRuntimeRunner = class {
19147
19198
  let page;
19148
19199
  try {
19149
19200
  page = await this.fetchLogPage(namespace, appName, {
19201
+ ...options,
19150
19202
  page: options.page,
19151
19203
  history: options.history,
19152
19204
  search: options.search
@@ -19156,12 +19208,12 @@ var ServiceRuntimeRunner = class {
19156
19208
  ui.warn("Follow fetch failed", { message });
19157
19209
  continue;
19158
19210
  }
19159
- const newItems = page.data.filter((item) => !seen.has(item.hash));
19211
+ const newItems = page.data.filter((item) => !seen.has(item.id ?? item.hash));
19160
19212
  if (newItems.length === 0) {
19161
19213
  continue;
19162
19214
  }
19163
19215
  for (const item of newItems) {
19164
- seen.add(item.hash);
19216
+ seen.add(item.id ?? item.hash);
19165
19217
  recent.unshift(item);
19166
19218
  if (recent.length > 200) {
19167
19219
  recent.pop();
@@ -19747,35 +19799,16 @@ var ServiceRuntimeRunner = class {
19747
19799
  await this.runRemoteCommand(server, command);
19748
19800
  }
19749
19801
  async fetchLogPage(namespace, appName, options) {
19750
- const baseUrl = (process.env.INDEXER_BASE_URL || "https://indexer.pmate.chat").replace(/\/$/, "");
19751
- const chainId = resolveLogChainId();
19752
- const hasSearch = Boolean(options.search?.trim());
19753
- const action = options.history || !hasSearch ? "list" : "search";
19754
- const url = new URL(
19755
- `/chains/${encodeURIComponent(chainId)}/namespaces/${encodeURIComponent(namespace)}/log/${encodeURIComponent(appName)}/${action}`,
19756
- `${baseUrl}/`
19757
- );
19758
- url.searchParams.set("page_id", String(options.page));
19759
- if (hasSearch && !options.history) {
19760
- url.searchParams.set("keyword", options.search.trim());
19761
- }
19762
- const response = await fetch(url.toString(), {
19763
- method: "GET",
19764
- headers: { Accept: "application/json" }
19802
+ resolveLogChainId();
19803
+ return await queryDeveloperLogs({
19804
+ service: `${namespace}:${appName}`,
19805
+ page: options.page,
19806
+ keyword: options.search,
19807
+ topic: options.topic,
19808
+ fields: options.fields,
19809
+ environment: options.envName,
19810
+ sinceMs: options.sinceMs
19765
19811
  });
19766
- const payload = await response.json();
19767
- if (response.status === 404 && /resource not found/i.test(payload.message || "")) {
19768
- return {
19769
- page: options.page,
19770
- totalPage: 0,
19771
- pageSize: 100,
19772
- data: []
19773
- };
19774
- }
19775
- if (!response.ok || !payload.success || !payload.data) {
19776
- throw new Error(payload.message || `Log request failed: ${response.status}`);
19777
- }
19778
- return payload.data;
19779
19812
  }
19780
19813
  envNameToStage(envName) {
19781
19814
  if (envName === "dev" || envName === "preview" || envName === "prod") {
@@ -19801,8 +19834,8 @@ var ServiceRuntimeRunner = class {
19801
19834
  const type = isErrorLike ? import_chalk3.default.bold.red(`[${item.type}]`) : import_chalk3.default.bold.blue(`[${item.type}]`);
19802
19835
  const level = resolvedLevel ? `${isErrorLike ? import_chalk3.default.bold.red(resolvedLevel.toUpperCase()) : import_chalk3.default.bold.yellow(resolvedLevel.toUpperCase())} ` : "";
19803
19836
  const data = this.formatLogData(item.data);
19804
- const hash = import_chalk3.default.gray(`#${item.hash}`);
19805
- return `${ts} ${level}${type} ${data} ${hash}`;
19837
+ const hash = import_chalk3.default.gray(`#${item.id ?? item.hash}`);
19838
+ return `${ts} ${level}${type} ${item.source ?? ""} ${item.message ?? ""} ${data} ${hash}`;
19806
19839
  }
19807
19840
  formatLogData(value) {
19808
19841
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -20459,7 +20492,8 @@ var ServiceRuntimeRunner = class {
20459
20492
  }
20460
20493
  };
20461
20494
  function resolveLogChainId(env = process.env) {
20462
- return env.BLOCKCHAIN_CHAIN_ID?.trim() || "app-log";
20495
+ if (env.APP_LOG_CHAIN_ID && env.APP_LOG_CHAIN_ID !== "log") throw new Error("Logs require log chain");
20496
+ return "log";
20463
20497
  }
20464
20498
  function shellQuote2(value) {
20465
20499
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -20474,51 +20508,6 @@ function parseServiceRef(serviceId) {
20474
20508
 
20475
20509
  // src/commands/ServiceCommandHandler.ts
20476
20510
  init_namespacedRef();
20477
-
20478
- // src/services/DeveloperLogsClient.ts
20479
- init_sessionStore();
20480
- init_namespacedRef();
20481
- var appRef = "pmate:developers";
20482
- async function queryDeveloperLogs(input, fetcher = fetch) {
20483
- parseNamespacedRef(input.app, { entityLabel: "app ref" });
20484
- parseNamespacedRef(input.service, { entityLabel: "service ref" });
20485
- if (!Number.isSafeInteger(input.page) || input.page < 0) throw new Error("Invalid log page");
20486
- if (input.sinceMs !== void 0 && (!Number.isSafeInteger(input.sinceMs) || input.sinceMs < 0)) throw new Error("Invalid log time");
20487
- if (input.keyword && input.keyword.length > 256) throw new Error("Log search must be at most 256 characters");
20488
- const session = readRequiredSession({ preferredApp: appRef, fallbackToActiveApp: true });
20489
- if (Date.parse(session.expiresAt) <= Date.now() || !Number.isFinite(Date.parse(session.expiresAt)))
20490
- throw new Error("Session expired. Run pmate login --app pmate:developers");
20491
- async function request2(url, init) {
20492
- let response;
20493
- try {
20494
- response = await fetcher(url, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4) });
20495
- } catch {
20496
- throw new Error("Developers log request failed; no anonymous fallback was attempted");
20497
- }
20498
- if (!response.ok) throw new Error(`Developers log request failed (HTTP ${response.status}); check login, pmate:dev role and App-Service access`);
20499
- let body;
20500
- try {
20501
- body = await response.json();
20502
- } catch {
20503
- throw new Error("Invalid Developers API response");
20504
- }
20505
- if (!body?.success) throw new Error("Developers API rejected the request");
20506
- return body.data;
20507
- }
20508
- const exchanged = await request2("https://auth-api-v2.pmate.chat/session/exchange", {
20509
- method: "POST",
20510
- headers: { Authorization: `Bearer ${session.token}`, "Content-Type": "application/json" },
20511
- body: JSON.stringify({ app: appRef })
20512
- });
20513
- if (exchanged?.session?.identity?.app !== appRef || exchanged.session.identity.accountId !== session.accountId || typeof exchanged.token !== "string" || !exchanged.token)
20514
- throw new Error("Developers session identity mismatch");
20515
- const params = new URLSearchParams({ app: input.app, service: input.service, page: String(input.page) });
20516
- if (input.keyword) params.set("keyword", input.keyword);
20517
- if (input.sinceMs !== void 0) params.set("sinceMs", String(input.sinceMs));
20518
- return request2(`https://dev-api.pmate.chat/api/logs?${params}`, { headers: { Authorization: `Bearer ${exchanged.token}` } });
20519
- }
20520
-
20521
- // src/commands/ServiceCommandHandler.ts
20522
20511
  var ServiceCommandHandler = class {
20523
20512
  async handle(args) {
20524
20513
  const baseUrl = this.resolveBaseUrl(args.baseUrl);
@@ -20716,7 +20705,7 @@ var ServiceCommandHandler = class {
20716
20705
  }
20717
20706
  case "log": {
20718
20707
  if (args.app) {
20719
- const result = await queryDeveloperLogs({ app: args.app, service: `${args.namespace}:${args.name}`, page: args.page, keyword: args.search, sinceMs: args.sinceMs });
20708
+ const result = await queryDeveloperLogs({ app: args.app, service: `${args.namespace}:${args.name}`, page: args.page, keyword: args.search, sinceMs: args.sinceMs, topic: args.topic, fields: args.fields, environment: args.env });
20720
20709
  console.log(JSON.stringify(result, null, args.json ? void 0 : 2));
20721
20710
  return;
20722
20711
  }
@@ -20727,7 +20716,11 @@ var ServiceCommandHandler = class {
20727
20716
  history: args.history,
20728
20717
  search: args.search,
20729
20718
  follow: args.follow,
20730
- envName: args.env
20719
+ envName: args.env,
20720
+ topic: args.topic,
20721
+ fields: args.fields,
20722
+ sinceMs: args.sinceMs,
20723
+ json: args.json
20731
20724
  });
20732
20725
  return;
20733
20726
  }
@@ -23998,6 +23991,7 @@ function parseServiceArgs(tokens) {
23998
23991
  let page = 0;
23999
23992
  let app;
24000
23993
  let sinceMs;
23994
+ const logFilters = {};
24001
23995
  let history = false;
24002
23996
  let search;
24003
23997
  let follow = false;
@@ -24005,6 +23999,28 @@ function parseServiceArgs(tokens) {
24005
23999
  while (rest.length > 0) {
24006
24000
  const token = rest.shift();
24007
24001
  switch (token) {
24002
+ case "--topic": {
24003
+ if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
24004
+ const value = requireValue4(rest, token);
24005
+ if (!/^[a-z][a-z0-9-]{0,31}$/.test(value)) throw new Error("Invalid --topic");
24006
+ logFilters.topic = value;
24007
+ break;
24008
+ }
24009
+ case "--field": {
24010
+ if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
24011
+ const value = requireValue4(rest, token);
24012
+ const match = /^([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)=(.*)$/.exec(value);
24013
+ if (!match || match[1].length > 128 || match[2].length > 256) throw new Error("Use --field name=value");
24014
+ let parsed = match[2];
24015
+ try {
24016
+ parsed = JSON.parse(match[2]);
24017
+ } catch {
24018
+ }
24019
+ if (!["string", "number", "boolean"].includes(typeof parsed)) throw new Error("Field value must be text, number or boolean");
24020
+ logFilters.fields = { ...logFilters.fields, [match[1]]: parsed };
24021
+ if (Object.keys(logFilters.fields).length > 20) throw new Error("At most 20 field filters");
24022
+ break;
24023
+ }
24008
24024
  case "--app":
24009
24025
  if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
24010
24026
  app = parseNamespacedRef(requireValue4(rest, token), { entityLabel: "app ref" }).fullId;
@@ -24055,8 +24071,7 @@ function parseServiceArgs(tokens) {
24055
24071
  const name = parsedRef.objectId;
24056
24072
  if (action === "log") {
24057
24073
  if (app && (history || follow || env || baseUrl)) throw new Error("App logs do not support --history, --follow, --env or --base-url; use --page, --search and --since-ms");
24058
- if (sinceMs !== void 0 && !app) throw new Error("--since-ms requires --app");
24059
- return { type: "service" /* Service */, action, namespace, name, baseUrl, json, page, history, search, follow, env, ...app ? { app } : {}, ...sinceMs !== void 0 ? { sinceMs } : {} };
24074
+ return { type: "service" /* Service */, action, namespace, name, baseUrl, json, page, history, search, follow, env, ...logFilters, ...app ? { app } : {}, ...sinceMs !== void 0 ? { sinceMs } : {} };
24060
24075
  }
24061
24076
  if (action === "deploy-logs" || action === "deploy-history") {
24062
24077
  return { type: "service" /* Service */, action, namespace, name, env, page, baseUrl, json };
@@ -26303,7 +26318,7 @@ var HELP_TEXT = [
26303
26318
  ` ${import_chalk4.default.gray(" init <ns:objectId> [--cwd <dir>] [--base-url <url>]")}`,
26304
26319
  ` ${import_chalk4.default.gray(" ssl set <ns:objectId> --cert <key> [--base-url <url>] [--json]")}`,
26305
26320
  ` ${import_chalk4.default.gray(" ssl refresh <ns:objectId> [--cert <key>] [--dry-run] [--base-url <url>] [--json]")}`,
26306
- ` ${import_chalk4.default.gray(" log <ns:objectId> [--page <n>] [--history] [--search <keyword>] [--follow|-f] [--base-url <url>]")}`,
26321
+ ` ${import_chalk4.default.gray(" log <ns:objectId> [--topic <topic>] [--env <env>] [--field <name=value>] [--search <keyword>] [--since-ms <ms>] [--page <n>] [--follow|-f] [--json]")}`,
26307
26322
  ` ${import_chalk4.default.gray(" default base URL: https://apps-api.pmate.chat")}`,
26308
26323
  ` ${import_chalk4.default.cyan("host")} Manage host-level environment variables`,
26309
26324
  ` ${import_chalk4.default.gray(" env list [--host <id>] [--json]")}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {