@pmate/cli 0.21.7 → 0.21.9

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
@@ -620,6 +620,18 @@ type AppCommandArgs = {
620
620
  toId?: string;
621
621
  baseUrl?: string;
622
622
  json: boolean;
623
+ } | {
624
+ type: Command.App;
625
+ action: "icon";
626
+ id: string;
627
+ preset?: "android" | "ios";
628
+ width?: number;
629
+ height?: number;
630
+ format?: "png" | "webp" | "svg" | "ico";
631
+ output?: string;
632
+ outDir?: string;
633
+ baseUrl?: string;
634
+ json: boolean;
623
635
  };
624
636
  type ServiceCommandArgs = {
625
637
  type: Command.Service;
package/dist/index.js CHANGED
@@ -9435,8 +9435,8 @@ __export(index_exports, {
9435
9435
  parseCommandArgs: () => parseCommandArgs
9436
9436
  });
9437
9437
  module.exports = __toCommonJS(index_exports);
9438
- var import_node_fs19 = require("fs");
9439
- var import_node_path24 = __toESM(require("path"));
9438
+ var import_node_fs20 = require("fs");
9439
+ var import_node_path25 = __toESM(require("path"));
9440
9440
  var import_chalk4 = __toESM(require("chalk"));
9441
9441
 
9442
9442
  // src/cli.ts
@@ -14585,9 +14585,9 @@ ${indent}\`\`\``;
14585
14585
  static richTextToPlain(richText) {
14586
14586
  return richText.map((item) => item.plain_text ?? item.text?.content ?? "").join("").trim();
14587
14587
  }
14588
- static async requestJson(path25, init) {
14588
+ static async requestJson(path26, init) {
14589
14589
  const token = this.getToken();
14590
- const response = await fetch(`${this.baseUrl}${path25}`, {
14590
+ const response = await fetch(`${this.baseUrl}${path26}`, {
14591
14591
  method: init.method,
14592
14592
  headers: {
14593
14593
  Authorization: `Bearer ${token}`,
@@ -16223,8 +16223,8 @@ function descriptorPathSegments(descriptor) {
16223
16223
  return ["chains", normalized.chain, "namespaces", normalized.ns, normalized.kind, normalized.name];
16224
16224
  }
16225
16225
  function buildPathUrl(baseUrl, pathSegments, query) {
16226
- const path25 = `/${pathSegments.map((segment) => encodeURIComponent(segment)).join("/")}`;
16227
- const url = new URL(path25, baseUrl);
16226
+ const path26 = `/${pathSegments.map((segment) => encodeURIComponent(segment)).join("/")}`;
16227
+ const url = new URL(path26, baseUrl);
16228
16228
  if (query) {
16229
16229
  for (const [key, value] of Object.entries(query)) {
16230
16230
  url.searchParams.set(key, String(value));
@@ -17173,9 +17173,155 @@ var StoreCommandHandler = class {
17173
17173
  };
17174
17174
 
17175
17175
  // src/commands/AppCommandHandler.ts
17176
+ var import_node_fs17 = require("fs");
17177
+ var import_node_path22 = __toESM(require("path"));
17178
+ init_sessionStore();
17179
+
17180
+ // src/appIcon.ts
17176
17181
  var import_node_fs16 = require("fs");
17177
17182
  var import_node_path21 = __toESM(require("path"));
17178
- init_sessionStore();
17183
+ var import_sharp = __toESM(require("sharp"));
17184
+ init_namespacedRef();
17185
+ var ANDROID_ICON_SPECS = [
17186
+ { width: 48, height: 48, name: "mdpi" },
17187
+ { width: 72, height: 72, name: "hdpi" },
17188
+ { width: 96, height: 96, name: "xhdpi" },
17189
+ { width: 144, height: 144, name: "xxhdpi" },
17190
+ { width: 192, height: 192, name: "xxxhdpi" },
17191
+ { width: 512, height: 512, name: "play-store" }
17192
+ ];
17193
+ var IOS_ICON_SPECS = [
17194
+ { width: 120, height: 120, name: "iphone-notification-2x" },
17195
+ { width: 152, height: 152, name: "ipad-app-2x" },
17196
+ { width: 167, height: 167, name: "ipad-pro-app-2x" },
17197
+ { width: 180, height: 180, name: "iphone-app-3x" },
17198
+ { width: 1024, height: 1024, name: "app-store" }
17199
+ ];
17200
+ var pngToIco = require("png-to-ico").default;
17201
+ function normalizeAppIcon(icon, cwd) {
17202
+ if (typeof icon !== "string" || !icon.trim()) {
17203
+ throw new Error("App icon must be a non-empty SVG string or .svg file path.");
17204
+ }
17205
+ const trimmed = icon.trim();
17206
+ if (looksLikeSvgMarkup(trimmed)) {
17207
+ return trimmed;
17208
+ }
17209
+ if (trimmed.startsWith("data:")) {
17210
+ return normalizeSvgDataUrl(trimmed);
17211
+ }
17212
+ if (/^https?:\/\//i.test(trimmed)) {
17213
+ throw new Error("App icon must be SVG source, not a remote URL.");
17214
+ }
17215
+ const absolutePath = import_node_path21.default.resolve(cwd, trimmed);
17216
+ if (!(0, import_node_fs16.existsSync)(absolutePath)) {
17217
+ if (trimmed.toLowerCase().endsWith(".svg")) {
17218
+ throw new Error(`App icon SVG file not found: ${absolutePath}`);
17219
+ }
17220
+ throw new Error("App icon must be inline SVG, an SVG data URL, or a .svg file path.");
17221
+ }
17222
+ if (import_node_path21.default.extname(absolutePath).toLowerCase() !== ".svg") {
17223
+ throw new Error(`App icon file must use the .svg extension: ${absolutePath}`);
17224
+ }
17225
+ const svg = (0, import_node_fs16.readFileSync)(absolutePath, "utf8").trim();
17226
+ if (!looksLikeSvgMarkup(svg)) {
17227
+ throw new Error(`App icon file is not valid SVG: ${absolutePath}`);
17228
+ }
17229
+ return svg;
17230
+ }
17231
+ function prepareAppPayload(payload, cwd) {
17232
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
17233
+ throw new Error("App payload must be a JSON object.");
17234
+ }
17235
+ const normalized = { ...payload };
17236
+ if (normalized.icon !== void 0) {
17237
+ normalized.icon = normalizeAppIcon(normalized.icon, cwd);
17238
+ }
17239
+ return normalized;
17240
+ }
17241
+ function createAppIconExportPlans(input) {
17242
+ const { namespace, objectId } = parseNamespacedRef(input.id, {
17243
+ entityLabel: "app ref"
17244
+ });
17245
+ if (input.preset) {
17246
+ const specs = input.preset === "android" ? ANDROID_ICON_SPECS : IOS_ICON_SPECS;
17247
+ if (!input.outDir) {
17248
+ throw new Error("App icon preset export requires --out-dir.");
17249
+ }
17250
+ const outDir = import_node_path21.default.resolve(input.cwd, input.outDir);
17251
+ (0, import_node_fs16.mkdirSync)(outDir, { recursive: true });
17252
+ return specs.map((spec) => ({
17253
+ width: spec.width,
17254
+ height: spec.height,
17255
+ format: "png",
17256
+ outputPath: import_node_path21.default.join(
17257
+ outDir,
17258
+ `${namespace}-${objectId}-${spec.name}-${spec.width}x${spec.height}.png`
17259
+ )
17260
+ }));
17261
+ }
17262
+ if (!input.width || !input.height) {
17263
+ throw new Error("App icon export requires either a preset or an explicit size.");
17264
+ }
17265
+ const format = input.format || "png";
17266
+ if (!input.output) {
17267
+ throw new Error("Single-size app icon export requires --output.");
17268
+ }
17269
+ const resolvedOutput = import_node_path21.default.resolve(input.cwd, input.output);
17270
+ (0, import_node_fs16.mkdirSync)(import_node_path21.default.dirname(resolvedOutput), { recursive: true });
17271
+ return [
17272
+ {
17273
+ width: input.width,
17274
+ height: input.height,
17275
+ format,
17276
+ outputPath: resolvedOutput
17277
+ }
17278
+ ];
17279
+ }
17280
+ async function exportAppIcon(svg, plans) {
17281
+ for (const plan of plans) {
17282
+ const bytes = await renderAppIcon(svg, plan.width, plan.height, plan.format);
17283
+ (0, import_node_fs16.writeFileSync)(plan.outputPath, bytes);
17284
+ }
17285
+ }
17286
+ async function renderAppIcon(svg, width, height, format) {
17287
+ if (format === "svg") {
17288
+ return Buffer.from(svg);
17289
+ }
17290
+ const pipeline = (0, import_sharp.default)(Buffer.from(svg)).resize(width, height, {
17291
+ fit: "contain",
17292
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
17293
+ });
17294
+ if (format === "webp") {
17295
+ return pipeline.webp().toBuffer();
17296
+ }
17297
+ if (format === "ico") {
17298
+ const pngBuffer = await pipeline.png().toBuffer();
17299
+ return pngToIco([pngBuffer]);
17300
+ }
17301
+ return pipeline.png().toBuffer();
17302
+ }
17303
+ function looksLikeSvgMarkup(value) {
17304
+ return /<svg[\s>]/i.test(value);
17305
+ }
17306
+ function normalizeSvgDataUrl(value) {
17307
+ const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?(?:;(base64))?,(.*)$/i.exec(value);
17308
+ if (!match) {
17309
+ throw new Error("Invalid app icon data URL.");
17310
+ }
17311
+ const mimeType = (match[1] || "").toLowerCase();
17312
+ if (mimeType !== "image/svg+xml") {
17313
+ throw new Error(`App icon data URL must be image/svg+xml. Received: ${mimeType || "unknown"}`);
17314
+ }
17315
+ const payload = match[3] || "";
17316
+ const decoded = match[2] ? Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
17317
+ const svg = decoded.trim();
17318
+ if (!looksLikeSvgMarkup(svg)) {
17319
+ throw new Error("App icon data URL does not contain valid SVG markup.");
17320
+ }
17321
+ return svg;
17322
+ }
17323
+
17324
+ // src/commands/AppCommandHandler.ts
17179
17325
  init_namespacedRef();
17180
17326
  var AppCommandHandler = class {
17181
17327
  async handle(args) {
@@ -17206,7 +17352,7 @@ var AppCommandHandler = class {
17206
17352
  return;
17207
17353
  }
17208
17354
  case "create": {
17209
- const payload = this.readJsonFile(args.filePath);
17355
+ const payload = this.preparePayload(args.filePath);
17210
17356
  await this.requireNamespace(payload, baseUrl);
17211
17357
  const record = await this.request(
17212
17358
  baseUrl,
@@ -17220,7 +17366,7 @@ var AppCommandHandler = class {
17220
17366
  return;
17221
17367
  }
17222
17368
  case "update": {
17223
- const payload = this.readJsonFile(args.filePath);
17369
+ const payload = this.preparePayload(args.filePath);
17224
17370
  const pathname = this.buildAppPath(args.id);
17225
17371
  const record = await this.request(
17226
17372
  baseUrl,
@@ -17233,6 +17379,39 @@ var AppCommandHandler = class {
17233
17379
  this.renderJson(record, args.json);
17234
17380
  return;
17235
17381
  }
17382
+ case "icon": {
17383
+ const record = await this.request(
17384
+ baseUrl,
17385
+ token,
17386
+ "GET",
17387
+ this.buildAppPath(args.id)
17388
+ );
17389
+ if (!record.icon) {
17390
+ throw new Error(`App icon is missing for ${args.id}`);
17391
+ }
17392
+ const plans = createAppIconExportPlans({
17393
+ id: args.id,
17394
+ preset: args.preset,
17395
+ width: args.width,
17396
+ height: args.height,
17397
+ format: args.format,
17398
+ output: args.output,
17399
+ outDir: args.outDir,
17400
+ cwd: process.cwd()
17401
+ });
17402
+ await exportAppIcon(record.icon, plans);
17403
+ ui.success(`Exported ${plans.length} icon file${plans.length > 1 ? "s" : ""} for ${args.id}`);
17404
+ this.renderJson(
17405
+ plans.map((plan) => ({
17406
+ width: plan.width,
17407
+ height: plan.height,
17408
+ format: plan.format,
17409
+ outputPath: plan.outputPath
17410
+ })),
17411
+ args.json
17412
+ );
17413
+ return;
17414
+ }
17236
17415
  case "delete": {
17237
17416
  const pathname = this.buildAppPath(args.id);
17238
17417
  const record = await this.request(
@@ -17291,11 +17470,11 @@ var AppCommandHandler = class {
17291
17470
  }).token;
17292
17471
  }
17293
17472
  readJsonFile(filePath) {
17294
- const absolutePath = import_node_path21.default.resolve(process.cwd(), filePath);
17295
- if (!(0, import_node_fs16.existsSync)(absolutePath)) {
17473
+ const absolutePath = this.resolveFilePath(filePath);
17474
+ if (!(0, import_node_fs17.existsSync)(absolutePath)) {
17296
17475
  throw new Error(`JSON file not found: ${absolutePath}`);
17297
17476
  }
17298
- const raw = (0, import_node_fs16.readFileSync)(absolutePath, "utf8");
17477
+ const raw = (0, import_node_fs17.readFileSync)(absolutePath, "utf8");
17299
17478
  try {
17300
17479
  return JSON.parse(raw);
17301
17480
  } catch (error) {
@@ -17303,6 +17482,13 @@ var AppCommandHandler = class {
17303
17482
  throw new Error(`Invalid JSON in ${absolutePath}: ${message}`);
17304
17483
  }
17305
17484
  }
17485
+ preparePayload(filePath) {
17486
+ const absolutePath = this.resolveFilePath(filePath);
17487
+ return prepareAppPayload(this.readJsonFile(filePath), import_node_path22.default.dirname(absolutePath));
17488
+ }
17489
+ resolveFilePath(filePath) {
17490
+ return import_node_path22.default.resolve(process.cwd(), filePath);
17491
+ }
17306
17492
  async request(baseUrl, token, method, pathname, body) {
17307
17493
  const response = await fetch(`${baseUrl}${pathname}`, {
17308
17494
  method,
@@ -17375,15 +17561,15 @@ function readNamespaceFromPayload(payload) {
17375
17561
  }
17376
17562
 
17377
17563
  // src/commands/ServiceCommandHandler.ts
17378
- var import_node_fs18 = require("fs");
17564
+ var import_node_fs19 = require("fs");
17379
17565
  var import_promises5 = require("fs/promises");
17380
- var import_node_path23 = __toESM(require("path"));
17566
+ var import_node_path24 = __toESM(require("path"));
17381
17567
  var import_promises6 = require("readline/promises");
17382
17568
 
17383
17569
  // src/services/ServiceRuntimeRunner.ts
17384
- var import_node_fs17 = require("fs");
17570
+ var import_node_fs18 = require("fs");
17385
17571
  var import_node_os8 = __toESM(require("os"));
17386
- var import_node_path22 = __toESM(require("path"));
17572
+ var import_node_path23 = __toESM(require("path"));
17387
17573
  var import_node_child_process10 = require("child_process");
17388
17574
  var import_node_readline = require("readline");
17389
17575
  var import_chalk3 = __toESM(require("chalk"));
@@ -17496,14 +17682,14 @@ var ServiceRuntimeRunner = class {
17496
17682
  }
17497
17683
  resolveRepoRoot(service, cwd) {
17498
17684
  if (cwd) {
17499
- return import_node_path22.default.resolve(process.cwd(), cwd);
17685
+ return import_node_path23.default.resolve(process.cwd(), cwd);
17500
17686
  }
17501
17687
  const repository = this.getDeploy(service).repository?.trim();
17502
17688
  if (repository) {
17503
17689
  const repoName = repository.replace(/\/+$/, "").split("/").pop()?.replace(/\.git$/i, "");
17504
17690
  if (repoName) {
17505
- const candidate = import_node_path22.default.join(import_node_os8.default.homedir(), "dev", "pmate", repoName);
17506
- if ((0, import_node_fs17.existsSync)(candidate)) {
17691
+ const candidate = import_node_path23.default.join(import_node_os8.default.homedir(), "dev", "pmate", repoName);
17692
+ if ((0, import_node_fs18.existsSync)(candidate)) {
17507
17693
  return candidate;
17508
17694
  }
17509
17695
  }
@@ -17516,12 +17702,12 @@ var ServiceRuntimeRunner = class {
17516
17702
  if (!dir) {
17517
17703
  throw new Error(`Missing deploy.dir for ${service.id}`);
17518
17704
  }
17519
- return import_node_path22.default.resolve(repoRoot, dir);
17705
+ return import_node_path23.default.resolve(repoRoot, dir);
17520
17706
  }
17521
17707
  async deployVite(service, options) {
17522
17708
  const appDir = this.resolveLocalDir(service, options.cwd);
17523
- const packageJsonPath = import_node_path22.default.join(appDir, "package.json");
17524
- if (!(0, import_node_fs17.existsSync)(packageJsonPath)) {
17709
+ const packageJsonPath = import_node_path23.default.join(appDir, "package.json");
17710
+ if (!(0, import_node_fs18.existsSync)(packageJsonPath)) {
17525
17711
  throw new Error(`No package.json found at ${packageJsonPath}`);
17526
17712
  }
17527
17713
  const pkg2 = this.loadPackageJson(packageJsonPath);
@@ -17530,8 +17716,8 @@ var ServiceRuntimeRunner = class {
17530
17716
  throw new Error(`Missing package name in ${packageJsonPath}`);
17531
17717
  }
17532
17718
  await this.runCommand("pnpm", ["--filter", pkgName, "build"], this.resolveRepoRoot(service, options.cwd));
17533
- const distDir = import_node_path22.default.join(appDir, "dist");
17534
- if (!(0, import_node_fs17.existsSync)(distDir)) {
17719
+ const distDir = import_node_path23.default.join(appDir, "dist");
17720
+ if (!(0, import_node_fs18.existsSync)(distDir)) {
17535
17721
  throw new Error(`No dist directory found at ${distDir}`);
17536
17722
  }
17537
17723
  const bucketName = this.getDeploy(service).bucket?.trim();
@@ -17542,8 +17728,8 @@ var ServiceRuntimeRunner = class {
17542
17728
  }
17543
17729
  async deployStatic(service, options) {
17544
17730
  const appDir = this.resolveLocalDir(service, options.cwd);
17545
- const srcDir = import_node_path22.default.join(appDir, "src");
17546
- if (!(0, import_node_fs17.existsSync)(srcDir)) {
17731
+ const srcDir = import_node_path23.default.join(appDir, "src");
17732
+ if (!(0, import_node_fs18.existsSync)(srcDir)) {
17547
17733
  throw new Error(`No src directory found at ${srcDir}`);
17548
17734
  }
17549
17735
  const bucketName = this.getDeploy(service).bucket?.trim();
@@ -17661,7 +17847,7 @@ var ServiceRuntimeRunner = class {
17661
17847
  const serverAddress = await resolveServerAddress(server);
17662
17848
  const templateName = serviceType === "ws" ? "nginx.ws.template" : "nginx.rest.template";
17663
17849
  const templatePath = this.resolveTemplatePath(templateName);
17664
- const template = (0, import_node_fs17.readFileSync)(templatePath, "utf8");
17850
+ const template = (0, import_node_fs18.readFileSync)(templatePath, "utf8");
17665
17851
  const host = `${subdomain}.${deploy.domain?.trim() || "pmate.chat"}`;
17666
17852
  const rendered = template.replace(/\$\{\{PORT\}\}/g, String(port)).replace(/\$\{\{HOSTS\}\}/g, host).replace(/\$\{\{HOST\}\}/g, host);
17667
17853
  const remotePath = `/etc/nginx/sites-enabled/${subdomain}.conf`;
@@ -17693,11 +17879,11 @@ var ServiceRuntimeRunner = class {
17693
17879
  }
17694
17880
  if (config.ensurePackageJson) {
17695
17881
  const appDir = this.resolveLocalDir(service, options.cwd);
17696
- const packageJsonPath = import_node_path22.default.join(appDir, "package.json");
17697
- if (!(0, import_node_fs17.existsSync)(packageJsonPath)) {
17882
+ const packageJsonPath = import_node_path23.default.join(appDir, "package.json");
17883
+ if (!(0, import_node_fs18.existsSync)(packageJsonPath)) {
17698
17884
  throw new Error(`Missing package.json at ${packageJsonPath}`);
17699
17885
  }
17700
- const pkgStat = (0, import_node_fs17.statSync)(packageJsonPath);
17886
+ const pkgStat = (0, import_node_fs18.statSync)(packageJsonPath);
17701
17887
  if (!pkgStat.isFile()) {
17702
17888
  throw new Error(`package.json is not a file at ${packageJsonPath}`);
17703
17889
  }
@@ -18120,9 +18306,9 @@ var ServiceRuntimeRunner = class {
18120
18306
  return tokens;
18121
18307
  }
18122
18308
  readPathValue(source, pathExpr) {
18123
- const path25 = pathExpr.split(".").map((segment) => segment.trim()).filter(Boolean);
18309
+ const path26 = pathExpr.split(".").map((segment) => segment.trim()).filter(Boolean);
18124
18310
  let cursor = source;
18125
- for (const segment of path25) {
18311
+ for (const segment of path26) {
18126
18312
  if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) {
18127
18313
  return void 0;
18128
18314
  }
@@ -18165,12 +18351,12 @@ var ServiceRuntimeRunner = class {
18165
18351
  }
18166
18352
  resolveTemplatePath(templateName) {
18167
18353
  const candidates = [
18168
- import_node_path22.default.join(__dirname, "..", "templates", templateName),
18169
- import_node_path22.default.join(__dirname, "..", "src", "templates", templateName),
18170
- import_node_path22.default.join(__dirname, "templates", templateName)
18354
+ import_node_path23.default.join(__dirname, "..", "templates", templateName),
18355
+ import_node_path23.default.join(__dirname, "..", "src", "templates", templateName),
18356
+ import_node_path23.default.join(__dirname, "templates", templateName)
18171
18357
  ];
18172
18358
  for (const candidate of candidates) {
18173
- if ((0, import_node_fs17.existsSync)(candidate)) {
18359
+ if ((0, import_node_fs18.existsSync)(candidate)) {
18174
18360
  return candidate;
18175
18361
  }
18176
18362
  }
@@ -18202,7 +18388,7 @@ var ServiceRuntimeRunner = class {
18202
18388
  });
18203
18389
  }
18204
18390
  loadPackageJson(packageJsonPath) {
18205
- const raw = (0, import_node_fs17.readFileSync)(packageJsonPath, "utf8");
18391
+ const raw = (0, import_node_fs18.readFileSync)(packageJsonPath, "utf8");
18206
18392
  const parsed = JSON.parse(raw);
18207
18393
  if (!parsed || typeof parsed !== "object") {
18208
18394
  throw new Error(`Invalid package.json at ${packageJsonPath}`);
@@ -18211,7 +18397,7 @@ var ServiceRuntimeRunner = class {
18211
18397
  }
18212
18398
  writePackageJson(packageJsonPath, data) {
18213
18399
  const serialized = JSON.stringify(data, null, 2);
18214
- (0, import_node_fs17.writeFileSync)(packageJsonPath, `${serialized}
18400
+ (0, import_node_fs18.writeFileSync)(packageJsonPath, `${serialized}
18215
18401
  `, "utf8");
18216
18402
  }
18217
18403
  runCommand(command, args, cwd) {
@@ -18551,7 +18737,7 @@ var ServiceCommandHandler = class {
18551
18737
  format: "dotenv"
18552
18738
  }
18553
18739
  );
18554
- const outputPath = import_node_path23.default.resolve(process.cwd(), args.output);
18740
+ const outputPath = import_node_path24.default.resolve(process.cwd(), args.output);
18555
18741
  await (0, import_promises5.writeFile)(outputPath, payload.content, "utf8");
18556
18742
  ui.success(`Wrote env file for ${args.appId} to ${outputPath}`);
18557
18743
  this.renderJson({ ...payload, output: outputPath }, args.json);
@@ -18580,11 +18766,11 @@ var ServiceCommandHandler = class {
18580
18766
  return (baseUrl || process.env.PMATE_APPS_API_BASE_URL || "https://apps-api.pmate.chat").replace(/\/+$/, "");
18581
18767
  }
18582
18768
  readJsonFile(filePath) {
18583
- const absolutePath = import_node_path23.default.resolve(process.cwd(), filePath);
18584
- if (!(0, import_node_fs18.existsSync)(absolutePath)) {
18769
+ const absolutePath = import_node_path24.default.resolve(process.cwd(), filePath);
18770
+ if (!(0, import_node_fs19.existsSync)(absolutePath)) {
18585
18771
  throw new Error(`JSON file not found: ${absolutePath}`);
18586
18772
  }
18587
- const raw = (0, import_node_fs18.readFileSync)(absolutePath, "utf8");
18773
+ const raw = (0, import_node_fs19.readFileSync)(absolutePath, "utf8");
18588
18774
  try {
18589
18775
  return JSON.parse(raw);
18590
18776
  } catch (error) {
@@ -19870,7 +20056,7 @@ function parseAppArgs(tokens) {
19870
20056
  const rawAction = rest.shift();
19871
20057
  const action = rawAction === "view" ? "view" : rawAction;
19872
20058
  if (!action) {
19873
- throw new Error("Missing app action. Use: app list|get|view|create|update|rename|move|delete");
20059
+ throw new Error("Missing app action. Use: app list|get|view|create|update|rename|move|icon|delete");
19874
20060
  }
19875
20061
  let baseUrl;
19876
20062
  let json = false;
@@ -19949,6 +20135,108 @@ function parseAppArgs(tokens) {
19949
20135
  }
19950
20136
  return { type: "app" /* App */, action, id, baseUrl, json };
19951
20137
  }
20138
+ if (action === "icon") {
20139
+ const rawRef = rest.shift();
20140
+ if (!rawRef || rawRef.startsWith("-")) {
20141
+ throw new Error(
20142
+ "Usage: app icon <ns:objectId> [--android|--ios|--size <w>x<h>|--<w>x<h>] [--format <png|webp|svg|ico>] [--output <path>] [--out-dir <path>] [--base-url <url>] [--json]"
20143
+ );
20144
+ }
20145
+ const id = parseNamespacedRef(rawRef, {
20146
+ entityLabel: "app ref"
20147
+ }).fullId;
20148
+ let preset;
20149
+ let width;
20150
+ let height;
20151
+ let format;
20152
+ let output;
20153
+ let outDir;
20154
+ while (rest.length > 0) {
20155
+ const token = rest.shift();
20156
+ const inlineSize = parseInlineSizeToken(token);
20157
+ if (inlineSize) {
20158
+ if (preset || width || height) {
20159
+ throw new Error("App icon export mode already set. Use exactly one of --android, --ios, --size <w>x<h>, or --<w>x<h>.");
20160
+ }
20161
+ width = inlineSize.width;
20162
+ height = inlineSize.height;
20163
+ continue;
20164
+ }
20165
+ switch (token) {
20166
+ case "--android":
20167
+ if (preset || width || height) {
20168
+ throw new Error("App icon export mode already set. Use exactly one of --android, --ios, --size <w>x<h>, or --<w>x<h>.");
20169
+ }
20170
+ preset = "android";
20171
+ break;
20172
+ case "--ios":
20173
+ if (preset || width || height) {
20174
+ throw new Error("App icon export mode already set. Use exactly one of --android, --ios, --size <w>x<h>, or --<w>x<h>.");
20175
+ }
20176
+ preset = "ios";
20177
+ break;
20178
+ case "--size": {
20179
+ if (preset || width || height) {
20180
+ throw new Error("App icon export mode already set. Use exactly one of --android, --ios, --size <w>x<h>, or --<w>x<h>.");
20181
+ }
20182
+ const size = parseSizeValue(requireValue5(rest, token));
20183
+ width = size.width;
20184
+ height = size.height;
20185
+ break;
20186
+ }
20187
+ case "--format": {
20188
+ const value = requireValue5(rest, token);
20189
+ if (value !== "png" && value !== "webp" && value !== "svg" && value !== "ico") {
20190
+ throw new Error(`Invalid app icon format "${value}". Use png, webp, svg, or ico.`);
20191
+ }
20192
+ format = value;
20193
+ break;
20194
+ }
20195
+ case "--output":
20196
+ output = requireValue5(rest, token);
20197
+ break;
20198
+ case "--out-dir":
20199
+ outDir = requireValue5(rest, token);
20200
+ break;
20201
+ case "--base-url":
20202
+ baseUrl = requireValue5(rest, token);
20203
+ break;
20204
+ case "--json":
20205
+ json = true;
20206
+ break;
20207
+ default:
20208
+ throw new Error(`Unknown app icon option: ${token}`);
20209
+ }
20210
+ }
20211
+ if (!preset && (!width || !height)) {
20212
+ throw new Error("Missing app icon export mode. Use one of --android, --ios, --size <w>x<h>, or --<w>x<h>.");
20213
+ }
20214
+ if (preset && output) {
20215
+ throw new Error("App icon preset export writes multiple files. Use --out-dir instead of --output.");
20216
+ }
20217
+ if (preset && !outDir) {
20218
+ throw new Error("App icon preset export requires --out-dir.");
20219
+ }
20220
+ if (!preset && outDir) {
20221
+ throw new Error("Single-size app icon export writes one file. Use --output instead of --out-dir.");
20222
+ }
20223
+ if (!preset && !output) {
20224
+ throw new Error("Single-size app icon export requires --output.");
20225
+ }
20226
+ return {
20227
+ type: "app" /* App */,
20228
+ action,
20229
+ id,
20230
+ preset,
20231
+ width,
20232
+ height,
20233
+ format,
20234
+ output,
20235
+ outDir,
20236
+ baseUrl,
20237
+ json
20238
+ };
20239
+ }
19952
20240
  if (action === "create") {
19953
20241
  let filePath;
19954
20242
  while (rest.length > 0) {
@@ -20091,6 +20379,26 @@ function parseBareRenameId2(value, entityName) {
20091
20379
  }
20092
20380
  return normalized;
20093
20381
  }
20382
+ function parseSizeValue(value) {
20383
+ const match = /^(\d+)x(\d+)$/i.exec(value.trim());
20384
+ if (!match) {
20385
+ throw new Error(`Invalid app icon size "${value}". Use <width>x<height>.`);
20386
+ }
20387
+ return {
20388
+ width: Number(match[1]),
20389
+ height: Number(match[2])
20390
+ };
20391
+ }
20392
+ function parseInlineSizeToken(token) {
20393
+ if (!token.startsWith("--")) {
20394
+ return null;
20395
+ }
20396
+ const value = token.slice(2);
20397
+ if (!/^\d+x\d+$/i.test(value)) {
20398
+ return null;
20399
+ }
20400
+ return parseSizeValue(value);
20401
+ }
20094
20402
 
20095
20403
  // src/serviceCommandParser.ts
20096
20404
  init_namespacedRef();
@@ -20982,7 +21290,7 @@ function requireValue10(tokens, option) {
20982
21290
 
20983
21291
  // src/index.ts
20984
21292
  var pkg = JSON.parse(
20985
- (0, import_node_fs19.readFileSync)(import_node_path24.default.resolve(__dirname, "..", "package.json"), "utf8")
21293
+ (0, import_node_fs20.readFileSync)(import_node_path25.default.resolve(__dirname, "..", "package.json"), "utf8")
20986
21294
  );
20987
21295
  var HELP_TEXT = [
20988
21296
  import_chalk4.default.bold(`pmate v${pkg.version}`),
@@ -21112,6 +21420,9 @@ var HELP_TEXT = [
21112
21420
  ` ${import_chalk4.default.gray(" update <ns:objectId> --file <path> [--base-url <url>] [--json]")}`,
21113
21421
  ` ${import_chalk4.default.gray(" rename --from <id> --to <id> --ns <namespace> [--base-url <url>] [--json]")}`,
21114
21422
  ` ${import_chalk4.default.gray(" move --from <id> --ns <namespace> --to-ns <namespace> [--to <id>] [--base-url <url>] [--json]")}`,
21423
+ ` ${import_chalk4.default.gray(" icon <ns:objectId> --android --out-dir <path> [--base-url <url>] [--json]")}`,
21424
+ ` ${import_chalk4.default.gray(" icon <ns:objectId> --ios --out-dir <path> [--base-url <url>] [--json]")}`,
21425
+ ` ${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]")}`,
21115
21426
  ` ${import_chalk4.default.gray(" delete <ns:objectId> [--base-url <url>] [--json]")}`,
21116
21427
  ` ${import_chalk4.default.gray(" default base URL: https://apps-api.pmate.chat")}`,
21117
21428
  ` ${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.7",
3
+ "version": "0.21.9",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -46,7 +46,9 @@
46
46
  "jsqr": "^1.4.0",
47
47
  "openai": "^6.16.0",
48
48
  "playwright": "^1.58.2",
49
+ "png-to-ico": "^3.0.1",
49
50
  "pngjs": "^7.0.0",
51
+ "sharp": "^0.34.5",
50
52
  "tslib": "^2.8.1",
51
53
  "yaml": "^2.8.2"
52
54
  },
@@ -56,8 +58,8 @@
56
58
  "@types/node": "^25.0.3",
57
59
  "@types/pngjs": "^6.0.5",
58
60
  "rimraf": "^6.1.2",
59
- "tsup": "^8.5.0",
60
61
  "ts-node": "^10.9.2",
62
+ "tsup": "^8.5.0",
61
63
  "typescript": "^5.9.3",
62
64
  "vitest": "^4.0.17"
63
65
  }