@pmate/cli 0.21.19 → 0.22.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/dist/index.d.ts CHANGED
@@ -738,6 +738,7 @@ type AppCommandArgs = {
738
738
  type: Command.App;
739
739
  action: "icon";
740
740
  id: string;
741
+ subAction?: "export";
741
742
  preset?: "android" | "ios";
742
743
  width?: number;
743
744
  height?: number;
@@ -746,6 +747,19 @@ type AppCommandArgs = {
746
747
  outDir?: string;
747
748
  baseUrl?: string;
748
749
  json: boolean;
750
+ } | {
751
+ type: Command.App;
752
+ action: "icon-add";
753
+ id: string;
754
+ name: string;
755
+ filePath?: string;
756
+ width?: number;
757
+ height?: number;
758
+ format?: "png" | "webp" | "svg" | "ico";
759
+ output?: string;
760
+ force: boolean;
761
+ baseUrl?: string;
762
+ json: boolean;
749
763
  };
750
764
  type ServiceCommandArgs = {
751
765
  type: Command.Service;
package/dist/index.js CHANGED
@@ -17252,6 +17252,8 @@ var IOS_ICON_SPECS = [
17252
17252
  { width: 1024, height: 1024, name: "app-store" }
17253
17253
  ];
17254
17254
  var pngToIco = require("png-to-ico").default;
17255
+ var APP_ICON_VARIANT_KEY_PATTERN = /^[a-z0-9]+$/;
17256
+ var DATA_URL_PATTERN = /^data:([^;,]+);base64,([a-z0-9+/=]+)$/i;
17255
17257
  function normalizeAppIcon(icon, cwd) {
17256
17258
  if (typeof icon !== "string" || !icon.trim()) {
17257
17259
  throw new Error("App icon must be a non-empty http(s) URL.");
@@ -17276,8 +17278,46 @@ function prepareAppPayload(payload, cwd) {
17276
17278
  if (normalized.icon !== void 0) {
17277
17279
  normalized.icon = normalizeAppIcon(normalized.icon, cwd);
17278
17280
  }
17281
+ if (normalized.icons !== void 0) {
17282
+ normalized.icons = normalizeAppIconVariants(normalized.icons);
17283
+ }
17279
17284
  return normalized;
17280
17285
  }
17286
+ function normalizeAppIconVariants(input) {
17287
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
17288
+ throw new Error("App icons must be an object map of variant names to asset references.");
17289
+ }
17290
+ const entries = Object.entries(input);
17291
+ const normalized = Object.fromEntries(
17292
+ entries.map(([key, value]) => [validateAppIconVariantKey(key), normalizeAppIconVariantValue(value)])
17293
+ );
17294
+ return normalized;
17295
+ }
17296
+ function validateAppIconVariantKey(key) {
17297
+ const trimmed = key.trim();
17298
+ if (!trimmed) {
17299
+ throw new Error("App icon variant name is required.");
17300
+ }
17301
+ if (!APP_ICON_VARIANT_KEY_PATTERN.test(trimmed)) {
17302
+ throw new Error(
17303
+ `Invalid app icon variant name "${key}". Use lowercase letters and digits only, for example webp512x512.`
17304
+ );
17305
+ }
17306
+ return trimmed;
17307
+ }
17308
+ function normalizeAppIconVariantValue(value) {
17309
+ if (typeof value !== "string" || !value.trim()) {
17310
+ throw new Error("App icon variant value must be a non-empty URL or image data URL.");
17311
+ }
17312
+ const trimmed = value.trim();
17313
+ if (/^https?:\/\//i.test(trimmed)) {
17314
+ return trimmed;
17315
+ }
17316
+ if (isSupportedImageDataUrl(trimmed)) {
17317
+ return trimmed;
17318
+ }
17319
+ throw new Error("App icon variant value must be an http(s) URL or a supported image data URL.");
17320
+ }
17281
17321
  function createAppIconExportPlans(input) {
17282
17322
  const { namespace, objectId } = parseNamespacedRef(input.id, {
17283
17323
  entityLabel: "app ref"
@@ -17324,6 +17364,30 @@ async function exportAppIcon(icon, plans) {
17324
17364
  (0, import_node_fs15.writeFileSync)(plan.outputPath, bytes);
17325
17365
  }
17326
17366
  }
17367
+ async function renderAppIconVariant(input) {
17368
+ const svg = await resolveAppIconSvg(input.icon);
17369
+ const bytes = await renderAppIcon(svg, input.width, input.height, input.format);
17370
+ let outputPath;
17371
+ if (input.output) {
17372
+ outputPath = import_node_path20.default.resolve(input.cwd, input.output);
17373
+ (0, import_node_fs15.mkdirSync)(import_node_path20.default.dirname(outputPath), { recursive: true });
17374
+ (0, import_node_fs15.writeFileSync)(outputPath, bytes);
17375
+ }
17376
+ return {
17377
+ value: createImageDataUrl(bytes, input.format),
17378
+ ...outputPath ? { outputPath } : {}
17379
+ };
17380
+ }
17381
+ function loadAppIconVariantFile(filePath, cwd) {
17382
+ const absolutePath = import_node_path20.default.resolve(cwd, filePath);
17383
+ const format = inferVariantFormatFromPath(absolutePath);
17384
+ const bytes = (0, import_node_fs15.readFileSync)(absolutePath);
17385
+ return {
17386
+ value: createImageDataUrl(bytes, format),
17387
+ format,
17388
+ absolutePath
17389
+ };
17390
+ }
17327
17391
  async function resolveAppIconSvg(icon) {
17328
17392
  const trimmed = icon.trim();
17329
17393
  if (looksLikeSvgMarkup(trimmed)) {
@@ -17366,6 +17430,44 @@ async function renderAppIcon(svg, width, height, format) {
17366
17430
  function looksLikeSvgMarkup(value) {
17367
17431
  return /<svg[\s>]/i.test(value);
17368
17432
  }
17433
+ function inferVariantFormatFromPath(filePath) {
17434
+ const ext = import_node_path20.default.extname(filePath).toLowerCase();
17435
+ switch (ext) {
17436
+ case ".png":
17437
+ return "png";
17438
+ case ".webp":
17439
+ return "webp";
17440
+ case ".svg":
17441
+ return "svg";
17442
+ case ".ico":
17443
+ return "ico";
17444
+ default:
17445
+ throw new Error(`Unsupported app icon variant file extension "${ext || "(none)"}". Use .png, .webp, .svg, or .ico.`);
17446
+ }
17447
+ }
17448
+ function createImageDataUrl(bytes, format) {
17449
+ return `data:${mimeTypeForIconFormat(format)};base64,${bytes.toString("base64")}`;
17450
+ }
17451
+ function mimeTypeForIconFormat(format) {
17452
+ switch (format) {
17453
+ case "png":
17454
+ return "image/png";
17455
+ case "webp":
17456
+ return "image/webp";
17457
+ case "svg":
17458
+ return "image/svg+xml";
17459
+ case "ico":
17460
+ return "image/x-icon";
17461
+ }
17462
+ }
17463
+ function isSupportedImageDataUrl(value) {
17464
+ const match = value.match(DATA_URL_PATTERN);
17465
+ if (!match) {
17466
+ return false;
17467
+ }
17468
+ const mimeType = match[1]?.toLowerCase();
17469
+ return mimeType === "image/png" || mimeType === "image/webp" || mimeType === "image/svg+xml" || mimeType === "image/x-icon";
17470
+ }
17369
17471
 
17370
17472
  // src/commands/AppCommandHandler.ts
17371
17473
  init_namespacedRef();
@@ -17399,7 +17501,11 @@ var AppCommandHandler = class {
17399
17501
  "GET",
17400
17502
  pathname
17401
17503
  );
17402
- this.renderJson(record, true);
17504
+ if (args.action === "view") {
17505
+ this.renderView(record);
17506
+ } else {
17507
+ this.renderJson(record, true);
17508
+ }
17403
17509
  return;
17404
17510
  }
17405
17511
  case "create": {
@@ -17463,6 +17569,46 @@ var AppCommandHandler = class {
17463
17569
  );
17464
17570
  return;
17465
17571
  }
17572
+ case "icon-add": {
17573
+ const pathname = this.buildAppPath(args.id);
17574
+ const record = await this.request(
17575
+ baseUrl,
17576
+ token,
17577
+ "GET",
17578
+ pathname
17579
+ );
17580
+ const name = validateAppIconVariantKey(args.name);
17581
+ const existingIcons = record.icons || {};
17582
+ if (existingIcons[name] && !args.force) {
17583
+ throw new Error(`App icon variant "${name}" already exists for ${args.id}. Re-run with --force to overwrite it.`);
17584
+ }
17585
+ const source = args.filePath ? loadAppIconVariantFile(args.filePath, process.cwd()) : await this.renderIconVariantFromApp(record, args);
17586
+ const updatedIcons = {
17587
+ ...existingIcons,
17588
+ [name]: source.value
17589
+ };
17590
+ const updatedRecord = await this.request(
17591
+ baseUrl,
17592
+ token,
17593
+ "PUT",
17594
+ pathname,
17595
+ {
17596
+ icons: updatedIcons
17597
+ }
17598
+ );
17599
+ ui.success(`Updated icon variant ${name} for ${args.id}`);
17600
+ this.renderJson(
17601
+ {
17602
+ id: args.id,
17603
+ name,
17604
+ ..."absolutePath" in source ? { filePath: source.absolutePath } : {},
17605
+ ..."outputPath" in source && source.outputPath ? { outputPath: source.outputPath } : {},
17606
+ icons: updatedRecord.icons || updatedIcons
17607
+ },
17608
+ args.json
17609
+ );
17610
+ return;
17611
+ }
17466
17612
  case "delete": {
17467
17613
  const pathname = this.buildAppPath(args.id);
17468
17614
  const record = await this.request(
@@ -17593,12 +17739,53 @@ var AppCommandHandler = class {
17593
17739
  }
17594
17740
  ui.plain(JSON.stringify(value, null, 2));
17595
17741
  }
17742
+ renderView(record) {
17743
+ const appId = formatNamespacedDisplayId(record.namespace, record.id);
17744
+ ui.plain(`App: ${appId}`);
17745
+ ui.plain(`Name: ${record.name}`);
17746
+ ui.plain(`Status: ${record.status}`);
17747
+ if (record.icon) {
17748
+ ui.plain(`Icon: ${record.icon}`);
17749
+ }
17750
+ if (record.icons && Object.keys(record.icons).length > 0) {
17751
+ ui.plain("Icons:");
17752
+ for (const [name, value] of Object.entries(record.icons).sort(
17753
+ ([left], [right]) => left.localeCompare(right)
17754
+ )) {
17755
+ ui.plain(` ${name}: ${value}`);
17756
+ }
17757
+ }
17758
+ if (record.deploys && Object.keys(record.deploys).length > 0) {
17759
+ ui.plain("Deploys:");
17760
+ for (const [platform, serviceRef] of Object.entries(record.deploys)) {
17761
+ if (serviceRef) {
17762
+ ui.plain(` ${platform}: ${serviceRef}`);
17763
+ }
17764
+ }
17765
+ }
17766
+ }
17596
17767
  buildAppPath(appId) {
17597
17768
  const { namespace, objectId } = parseNamespacedRef(appId, {
17598
17769
  entityLabel: "app ref"
17599
17770
  });
17600
17771
  return `/apps/${encodeURIComponent(namespace)}/${encodeURIComponent(objectId)}`;
17601
17772
  }
17773
+ async renderIconVariantFromApp(record, args) {
17774
+ if (!record.icon) {
17775
+ throw new Error(`App icon is missing for ${args.id}`);
17776
+ }
17777
+ if (!args.width || !args.height) {
17778
+ throw new Error("App icon add export requires an explicit size.");
17779
+ }
17780
+ return renderAppIconVariant({
17781
+ icon: record.icon,
17782
+ width: args.width,
17783
+ height: args.height,
17784
+ format: args.format || "png",
17785
+ output: args.output,
17786
+ cwd: process.cwd()
17787
+ });
17788
+ }
17602
17789
  };
17603
17790
  function readNamespaceFromPayload(payload) {
17604
17791
  if (!payload || typeof payload !== "object") {
@@ -20699,6 +20886,97 @@ function parseAppArgs(tokens) {
20699
20886
  return { type: "app" /* App */, action, id, baseUrl, json };
20700
20887
  }
20701
20888
  if (action === "icon") {
20889
+ const nextToken = rest[0];
20890
+ if (nextToken === "add") {
20891
+ rest.shift();
20892
+ const rawRef2 = rest.shift();
20893
+ const name = rest.shift();
20894
+ if (!rawRef2 || rawRef2.startsWith("-") || !name || name.startsWith("-")) {
20895
+ throw new Error(
20896
+ "Usage: app icon add <ns:objectId> <name> (--file <path> | (--size <w>x<h>|--<w>x<h>) [--format <png|webp|svg|ico>] [--output <path>]) [--force] [--base-url <url>] [--json]"
20897
+ );
20898
+ }
20899
+ const id2 = parseNamespacedRef(rawRef2, {
20900
+ entityLabel: "app ref"
20901
+ }).fullId;
20902
+ let width2;
20903
+ let height2;
20904
+ let format2;
20905
+ let output2;
20906
+ let filePath;
20907
+ let force = false;
20908
+ while (rest.length > 0) {
20909
+ const token = rest.shift();
20910
+ const inlineSize = parseInlineSizeToken(token);
20911
+ if (inlineSize) {
20912
+ if (filePath || width2 || height2) {
20913
+ throw new Error("App icon add source already set. Use either --file or one explicit size.");
20914
+ }
20915
+ width2 = inlineSize.width;
20916
+ height2 = inlineSize.height;
20917
+ continue;
20918
+ }
20919
+ switch (token) {
20920
+ case "--file":
20921
+ if (filePath || width2 || height2) {
20922
+ throw new Error("App icon add source already set. Use either --file or one explicit size.");
20923
+ }
20924
+ filePath = requireValue5(rest, token);
20925
+ break;
20926
+ case "--size": {
20927
+ if (filePath || width2 || height2) {
20928
+ throw new Error("App icon add source already set. Use either --file or one explicit size.");
20929
+ }
20930
+ const size = parseSizeValue(requireValue5(rest, token));
20931
+ width2 = size.width;
20932
+ height2 = size.height;
20933
+ break;
20934
+ }
20935
+ case "--format": {
20936
+ const value = requireValue5(rest, token);
20937
+ if (value !== "png" && value !== "webp" && value !== "svg" && value !== "ico") {
20938
+ throw new Error(`Invalid app icon format "${value}". Use png, webp, svg, or ico.`);
20939
+ }
20940
+ format2 = value;
20941
+ break;
20942
+ }
20943
+ case "--output":
20944
+ output2 = requireValue5(rest, token);
20945
+ break;
20946
+ case "--force":
20947
+ force = true;
20948
+ break;
20949
+ case "--base-url":
20950
+ baseUrl = requireValue5(rest, token);
20951
+ break;
20952
+ case "--json":
20953
+ json = true;
20954
+ break;
20955
+ default:
20956
+ throw new Error(`Unknown app icon add option: ${token}`);
20957
+ }
20958
+ }
20959
+ if (!filePath && (!width2 || !height2)) {
20960
+ throw new Error("Missing app icon add source. Use --file <path>, --size <w>x<h>, or --<w>x<h>.");
20961
+ }
20962
+ if (filePath && format2) {
20963
+ throw new Error("App icon add with --file infers format from the file extension. Remove --format.");
20964
+ }
20965
+ return {
20966
+ type: "app" /* App */,
20967
+ action: "icon-add",
20968
+ id: id2,
20969
+ name,
20970
+ filePath,
20971
+ width: width2,
20972
+ height: height2,
20973
+ format: format2,
20974
+ output: output2,
20975
+ force,
20976
+ baseUrl,
20977
+ json
20978
+ };
20979
+ }
20702
20980
  const rawRef = rest.shift();
20703
20981
  if (!rawRef || rawRef.startsWith("-")) {
20704
20982
  throw new Error(
@@ -20789,6 +21067,7 @@ function parseAppArgs(tokens) {
20789
21067
  return {
20790
21068
  type: "app" /* App */,
20791
21069
  action,
21070
+ subAction: "export",
20792
21071
  id,
20793
21072
  preset,
20794
21073
  width,
@@ -22475,6 +22754,8 @@ var HELP_TEXT = [
22475
22754
  ` ${import_chalk4.default.gray(" icon <ns:objectId> --android --out-dir <path> [--base-url <url>] [--json]")}`,
22476
22755
  ` ${import_chalk4.default.gray(" icon <ns:objectId> --ios --out-dir <path> [--base-url <url>] [--json]")}`,
22477
22756
  ` ${import_chalk4.default.gray(" icon <ns:objectId> (--size <w>x<h>|--<w>x<h>) --output <path> [--format <png|webp|svg|ico>] [--base-url <url>] [--json]")}`,
22757
+ ` ${import_chalk4.default.gray(" icon add <ns:objectId> <name> --file <path> [--force] [--base-url <url>] [--json]")}`,
22758
+ ` ${import_chalk4.default.gray(" icon add <ns:objectId> <name> (--size <w>x<h>|--<w>x<h>) [--format <png|webp|svg|ico>] [--output <path>] [--force] [--base-url <url>] [--json]")}`,
22478
22759
  ` ${import_chalk4.default.gray(" delete <ns:objectId> [--base-url <url>] [--json]")}`,
22479
22760
  ` ${import_chalk4.default.gray(" default base URL: https://apps-api.pmate.chat")}`,
22480
22761
  ` ${import_chalk4.default.cyan("service")} Manage service registry entries via apps-api`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.21.19",
3
+ "version": "0.22.0",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {