@pmate/cli 0.31.0 → 0.32.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 app --page 0 --json
243
+ pmate service log pmate:issues-api --topic runtime --source nginx --level error --env prod
244
+ pmate service log pmate:issues-api --request-id <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. `--type`, `--level`, `--source`, `--request-id` and `--env` filter on the server. 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,11 @@ type ServiceCommandArgs = {
970
970
  } | {
971
971
  type: Command.Service;
972
972
  action: "log";
973
+ topic?: "app" | "runtime";
974
+ level?: string;
975
+ source?: string;
976
+ requestId?: string;
977
+ logType?: string;
973
978
  app?: string;
974
979
  sinceMs?: number;
975
980
  namespace: string;
package/dist/index.js CHANGED
@@ -18740,6 +18740,50 @@ 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 ?? "app" });
18781
+ for (const key of ["type", "level", "source", "requestId", "environment"]) if (input[key]) params.set(key, input[key]);
18782
+ if (input.keyword) params.set("keyword", input.keyword);
18783
+ if (input.sinceMs !== void 0) params.set("sinceMs", String(input.sinceMs));
18784
+ return request2(`https://dev-api.pmate.chat/api/logs?${params}`, { headers: { Authorization: `Bearer ${exchanged.token}` } });
18785
+ }
18786
+
18743
18787
  // src/services/ServiceRuntimeRunner.ts
18744
18788
  var import_node_fs22 = require("fs");
18745
18789
  var import_node_os12 = __toESM(require("os"));
@@ -19111,15 +19155,21 @@ var ServiceRuntimeRunner = class {
19111
19155
  const namespace = service.namespace;
19112
19156
  const appName = service.name;
19113
19157
  const page = await this.fetchLogPage(namespace, appName, {
19158
+ ...options,
19114
19159
  page: options.page ?? 0,
19115
19160
  history: options.history ?? false,
19116
19161
  search: options.search
19117
19162
  });
19118
- ui.title(`Logs for ${namespace}/${appName}`);
19163
+ if (options.json) {
19164
+ console.log(JSON.stringify(page));
19165
+ return;
19166
+ }
19167
+ ui.title(`Logs for ${namespace}/${appName} \xB7 ${options.topic ?? "app"}`);
19119
19168
  ui.info("Source", { mode: options.history ? "history" : "online" });
19120
19169
  this.renderPage(page);
19121
19170
  if (options.follow) {
19122
19171
  await this.followLogs(namespace, appName, {
19172
+ ...options,
19123
19173
  page: options.page ?? 0,
19124
19174
  history: options.history ?? false,
19125
19175
  search: options.search,
@@ -19136,7 +19186,7 @@ var ServiceRuntimeRunner = class {
19136
19186
  interactive?.rl.on("close", () => {
19137
19187
  running = false;
19138
19188
  });
19139
- const seen = new Set(options.seedItems.map((item) => item.hash));
19189
+ const seen = new Set(options.seedItems.map((item) => item.id ?? item.hash));
19140
19190
  ui.info(
19141
19191
  "Follow",
19142
19192
  interactive ? "enabled (poll every 2s, Ctrl+C to stop, /help for filters)" : "enabled (poll every 2s, Ctrl+C to stop)"
@@ -19147,6 +19197,7 @@ var ServiceRuntimeRunner = class {
19147
19197
  let page;
19148
19198
  try {
19149
19199
  page = await this.fetchLogPage(namespace, appName, {
19200
+ ...options,
19150
19201
  page: options.page,
19151
19202
  history: options.history,
19152
19203
  search: options.search
@@ -19156,12 +19207,12 @@ var ServiceRuntimeRunner = class {
19156
19207
  ui.warn("Follow fetch failed", { message });
19157
19208
  continue;
19158
19209
  }
19159
- const newItems = page.data.filter((item) => !seen.has(item.hash));
19210
+ const newItems = page.data.filter((item) => !seen.has(item.id ?? item.hash));
19160
19211
  if (newItems.length === 0) {
19161
19212
  continue;
19162
19213
  }
19163
19214
  for (const item of newItems) {
19164
- seen.add(item.hash);
19215
+ seen.add(item.id ?? item.hash);
19165
19216
  recent.unshift(item);
19166
19217
  if (recent.length > 200) {
19167
19218
  recent.pop();
@@ -19747,35 +19798,19 @@ var ServiceRuntimeRunner = class {
19747
19798
  await this.runRemoteCommand(server, command);
19748
19799
  }
19749
19800
  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" }
19801
+ resolveLogChainId();
19802
+ return await queryDeveloperLogs({
19803
+ service: `${namespace}:${appName}`,
19804
+ page: options.page,
19805
+ keyword: options.search,
19806
+ topic: options.topic,
19807
+ type: options.logType,
19808
+ level: options.level,
19809
+ source: options.source,
19810
+ requestId: options.requestId,
19811
+ environment: options.envName,
19812
+ sinceMs: options.sinceMs
19765
19813
  });
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
19814
  }
19780
19815
  envNameToStage(envName) {
19781
19816
  if (envName === "dev" || envName === "preview" || envName === "prod") {
@@ -19801,8 +19836,8 @@ var ServiceRuntimeRunner = class {
19801
19836
  const type = isErrorLike ? import_chalk3.default.bold.red(`[${item.type}]`) : import_chalk3.default.bold.blue(`[${item.type}]`);
19802
19837
  const level = resolvedLevel ? `${isErrorLike ? import_chalk3.default.bold.red(resolvedLevel.toUpperCase()) : import_chalk3.default.bold.yellow(resolvedLevel.toUpperCase())} ` : "";
19803
19838
  const data = this.formatLogData(item.data);
19804
- const hash = import_chalk3.default.gray(`#${item.hash}`);
19805
- return `${ts} ${level}${type} ${data} ${hash}`;
19839
+ const hash = import_chalk3.default.gray(`#${item.id ?? item.hash}`);
19840
+ return `${ts} ${level}${type} ${item.source ?? ""} ${item.message ?? ""} ${data} ${hash}`;
19806
19841
  }
19807
19842
  formatLogData(value) {
19808
19843
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -20459,7 +20494,8 @@ var ServiceRuntimeRunner = class {
20459
20494
  }
20460
20495
  };
20461
20496
  function resolveLogChainId(env = process.env) {
20462
- return env.BLOCKCHAIN_CHAIN_ID?.trim() || "app-log";
20497
+ if (env.APP_LOG_CHAIN_ID && env.APP_LOG_CHAIN_ID !== "log") throw new Error("Logs require log chain");
20498
+ return "log";
20463
20499
  }
20464
20500
  function shellQuote2(value) {
20465
20501
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -20474,51 +20510,6 @@ function parseServiceRef(serviceId) {
20474
20510
 
20475
20511
  // src/commands/ServiceCommandHandler.ts
20476
20512
  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
20513
  var ServiceCommandHandler = class {
20523
20514
  async handle(args) {
20524
20515
  const baseUrl = this.resolveBaseUrl(args.baseUrl);
@@ -20716,7 +20707,7 @@ var ServiceCommandHandler = class {
20716
20707
  }
20717
20708
  case "log": {
20718
20709
  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 });
20710
+ const result = await queryDeveloperLogs({ app: args.app, service: `${args.namespace}:${args.name}`, page: args.page, keyword: args.search, sinceMs: args.sinceMs, topic: args.topic, type: args.logType, level: args.level, source: args.source, requestId: args.requestId });
20720
20711
  console.log(JSON.stringify(result, null, args.json ? void 0 : 2));
20721
20712
  return;
20722
20713
  }
@@ -20727,7 +20718,14 @@ var ServiceCommandHandler = class {
20727
20718
  history: args.history,
20728
20719
  search: args.search,
20729
20720
  follow: args.follow,
20730
- envName: args.env
20721
+ envName: args.env,
20722
+ topic: args.topic,
20723
+ level: args.level,
20724
+ source: args.source,
20725
+ requestId: args.requestId,
20726
+ logType: args.logType,
20727
+ sinceMs: args.sinceMs,
20728
+ json: args.json
20731
20729
  });
20732
20730
  return;
20733
20731
  }
@@ -23998,6 +23996,7 @@ function parseServiceArgs(tokens) {
23998
23996
  let page = 0;
23999
23997
  let app;
24000
23998
  let sinceMs;
23999
+ const logFilters = {};
24001
24000
  let history = false;
24002
24001
  let search;
24003
24002
  let follow = false;
@@ -24005,6 +24004,26 @@ function parseServiceArgs(tokens) {
24005
24004
  while (rest.length > 0) {
24006
24005
  const token = rest.shift();
24007
24006
  switch (token) {
24007
+ case "--topic":
24008
+ case "--level":
24009
+ case "--source":
24010
+ case "--request-id":
24011
+ case "--type": {
24012
+ if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
24013
+ const value = requireValue4(rest, token);
24014
+ if (token === "--topic") {
24015
+ if (value !== "app" && value !== "runtime") throw new Error("Invalid --topic");
24016
+ logFilters.topic = value;
24017
+ } else if (token === "--level") {
24018
+ if (!["debug", "info", "warn", "error"].includes(value)) throw new Error("Invalid --level");
24019
+ logFilters.level = value;
24020
+ } else if (token === "--source") {
24021
+ if (!["pm2", "nginx", "application", "deployment"].includes(value)) throw new Error("Invalid --source");
24022
+ logFilters.source = value;
24023
+ } else if (token === "--request-id") logFilters.requestId = value;
24024
+ else logFilters.logType = value;
24025
+ break;
24026
+ }
24008
24027
  case "--app":
24009
24028
  if (action !== "log") throw new Error(`Unknown service ${action} option: ${token}`);
24010
24029
  app = parseNamespacedRef(requireValue4(rest, token), { entityLabel: "app ref" }).fullId;
@@ -24055,8 +24074,7 @@ function parseServiceArgs(tokens) {
24055
24074
  const name = parsedRef.objectId;
24056
24075
  if (action === "log") {
24057
24076
  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 } : {} };
24077
+ return { type: "service" /* Service */, action, namespace, name, baseUrl, json, page, history, search, follow, env, ...logFilters, ...app ? { app } : {}, ...sinceMs !== void 0 ? { sinceMs } : {} };
24060
24078
  }
24061
24079
  if (action === "deploy-logs" || action === "deploy-history") {
24062
24080
  return { type: "service" /* Service */, action, namespace, name, env, page, baseUrl, json };
@@ -26303,7 +26321,7 @@ var HELP_TEXT = [
26303
26321
  ` ${import_chalk4.default.gray(" init <ns:objectId> [--cwd <dir>] [--base-url <url>]")}`,
26304
26322
  ` ${import_chalk4.default.gray(" ssl set <ns:objectId> --cert <key> [--base-url <url>] [--json]")}`,
26305
26323
  ` ${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>]")}`,
26324
+ ` ${import_chalk4.default.gray(" log <ns:objectId> [--topic app|runtime] [--env <env>] [--level <level>] [--source <source>] [--type <type>] [--request-id <id>] [--search <keyword>] [--since-ms <ms>] [--page <n>] [--follow|-f] [--json]")}`,
26307
26325
  ` ${import_chalk4.default.gray(" default base URL: https://apps-api.pmate.chat")}`,
26308
26326
  ` ${import_chalk4.default.cyan("host")} Manage host-level environment variables`,
26309
26327
  ` ${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.32.0",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {