@pmate/cli 0.24.25 → 0.24.26

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
@@ -1000,6 +1000,16 @@ type ServiceCommandArgs = {
1000
1000
  onlyKeys?: string[];
1001
1001
  baseUrl?: string;
1002
1002
  json: boolean;
1003
+ } | {
1004
+ type: Command.Service;
1005
+ action: "env-update-scope";
1006
+ appId: string;
1007
+ fromScope: string;
1008
+ toScope: string;
1009
+ overwrite: boolean;
1010
+ onlyKeys?: string[];
1011
+ baseUrl?: string;
1012
+ json: boolean;
1003
1013
  };
1004
1014
  type VercelProjectsCommandArgs = {
1005
1015
  type: Command.Vercel;
package/dist/index.js CHANGED
@@ -10050,6 +10050,19 @@ var AliyunService = class {
10050
10050
  );
10051
10051
  ui.success(`Created FC custom domain: ${input.domainName}`);
10052
10052
  }
10053
+ static async deleteFcCustomDomain(domainName, region) {
10054
+ const client = await this.createFcClient(region);
10055
+ try {
10056
+ await client.deleteCustomDomain(domainName);
10057
+ ui.success(`Deleted FC custom domain: ${domainName}`);
10058
+ } catch (error) {
10059
+ if (this.isNotFoundError(error)) {
10060
+ ui.warn(`FC custom domain not found: ${domainName}`);
10061
+ return;
10062
+ }
10063
+ throw error;
10064
+ }
10065
+ }
10053
10066
  /** Add a DNS record via Aliyun DNS. */
10054
10067
  static async addRecord(input) {
10055
10068
  const userConfig = loadPmateConfig();
@@ -18110,6 +18123,7 @@ function readNamespaceFromPayload(payload) {
18110
18123
  var import_node_fs20 = require("fs");
18111
18124
  var import_promises6 = require("fs/promises");
18112
18125
  var import_node_path24 = __toESM(require("path"));
18126
+ var import_node_child_process11 = require("child_process");
18113
18127
  var import_promises7 = require("readline/promises");
18114
18128
 
18115
18129
  // src/services/ServiceRuntimeRunner.ts
@@ -19443,10 +19457,15 @@ var ServiceRuntimeRunner = class {
19443
19457
  return [];
19444
19458
  }
19445
19459
  const appId = `${service.namespace}:${service.name}`;
19446
- const items = await registry.listEnvItems(appId, {
19460
+ const scopedItems = await registry.listEnvItems(appId, {
19447
19461
  scope: stage,
19448
19462
  includeSecretMeta: true
19449
19463
  });
19464
+ const allItems = await registry.listEnvItems(appId, {
19465
+ scope: "all",
19466
+ includeSecretMeta: true
19467
+ });
19468
+ const items = [...allItems, ...scopedItems];
19450
19469
  const requiredEnv = Array.from(
19451
19470
  new Set(
19452
19471
  items.map((item) => item.key ?? item.name).filter((key) => typeof key === "string" && key.trim().length > 0).map((key) => key.trim())
@@ -19693,6 +19712,8 @@ var ServiceCommandHandler = class {
19693
19712
  return;
19694
19713
  }
19695
19714
  case "delete": {
19715
+ const service = await registry.getService(args.namespace, args.name);
19716
+ await this.deleteServiceResources(service);
19696
19717
  const record = await this.request(
19697
19718
  baseUrl,
19698
19719
  "DELETE",
@@ -19863,6 +19884,23 @@ var ServiceCommandHandler = class {
19863
19884
  this.renderJson(payload, args.json);
19864
19885
  return;
19865
19886
  }
19887
+ case "env-update-scope": {
19888
+ const payload = await this.request(
19889
+ baseUrl,
19890
+ "POST",
19891
+ `${this.buildEnvBasePath(args.appId)}/update-scope`,
19892
+ {
19893
+ fromScope: args.fromScope,
19894
+ toScope: args.toScope,
19895
+ ...args.overwrite ? { overwrite: true } : {},
19896
+ ...args.onlyKeys?.length ? { onlyKeys: args.onlyKeys } : {}
19897
+ }
19898
+ );
19899
+ ui.success(`Updated env scope for ${args.appId}: ${args.fromScope} -> ${args.toScope}`);
19900
+ await this.syncFcRequiredEnvFromServiceEnv(registry, args.appId, args.toScope);
19901
+ this.renderJson(payload, args.json);
19902
+ return;
19903
+ }
19866
19904
  }
19867
19905
  }
19868
19906
  resolveBaseUrl(baseUrl) {
@@ -20014,10 +20052,7 @@ var ServiceCommandHandler = class {
20014
20052
  if (!("type" in service) || service.type !== "fc" || !service.fc) {
20015
20053
  return;
20016
20054
  }
20017
- const items = await registry.listEnvItems(appId, {
20018
- scope,
20019
- includeSecretMeta: true
20020
- });
20055
+ const items = await this.listEffectiveEnvItems(registry, appId, scope);
20021
20056
  const requiredEnv = Array.from(
20022
20057
  new Set(
20023
20058
  items.map((item) => item.key ?? item.name).filter((key) => typeof key === "string" && key.trim().length > 0).map((key) => key.trim())
@@ -20039,6 +20074,20 @@ var ServiceCommandHandler = class {
20039
20074
  const normalizedRight = [...right].sort();
20040
20075
  return normalizedLeft.every((value, index) => value === normalizedRight[index]);
20041
20076
  }
20077
+ async listEffectiveEnvItems(registry, appId, scope) {
20078
+ const scopedItems = await registry.listEnvItems(appId, {
20079
+ scope,
20080
+ includeSecretMeta: true
20081
+ });
20082
+ if (scope === "all") {
20083
+ return scopedItems;
20084
+ }
20085
+ const allItems = await registry.listEnvItems(appId, {
20086
+ scope: "all",
20087
+ includeSecretMeta: true
20088
+ });
20089
+ return [...allItems, ...scopedItems];
20090
+ }
20042
20091
  renderList(items, asJson) {
20043
20092
  if (asJson) {
20044
20093
  ui.plain(JSON.stringify(items, null, 2));
@@ -20052,6 +20101,206 @@ var ServiceCommandHandler = class {
20052
20101
  ui.info(formatNamespacedDisplayId(item.namespace, item.name), item);
20053
20102
  }
20054
20103
  }
20104
+ async deleteServiceResources(service) {
20105
+ switch (this.getServiceType(service)) {
20106
+ case "vite":
20107
+ case "static":
20108
+ await this.deleteStaticResources(service);
20109
+ return;
20110
+ case "node":
20111
+ case "rust":
20112
+ case "python":
20113
+ await this.deleteServerResources(service);
20114
+ return;
20115
+ case "fc":
20116
+ await this.deleteFcResources(service);
20117
+ return;
20118
+ case "ios":
20119
+ case "android":
20120
+ ui.warn(`Service type "${this.getServiceType(service)}" has no runtime resources to delete`);
20121
+ return;
20122
+ default:
20123
+ throw new Error(`Unsupported service type for delete: ${this.getServiceType(service)}`);
20124
+ }
20125
+ }
20126
+ async deleteStaticResources(service) {
20127
+ const site = this.getStaticSiteConfig(service);
20128
+ const bucketName = site.bucket?.trim();
20129
+ const domainName = site.domain?.trim();
20130
+ if (!bucketName) {
20131
+ throw new Error(`Missing site.bucket for ${service.id}`);
20132
+ }
20133
+ if (!domainName) {
20134
+ throw new Error(`Missing site.domain for ${service.id}`);
20135
+ }
20136
+ const subdomain = site.subdomain?.trim();
20137
+ const rr = subdomain || "@";
20138
+ const cdnDomain = subdomain ? `${subdomain}.${domainName}` : domainName;
20139
+ const cname = await AliyunService.getCdnCname(cdnDomain);
20140
+ if (cname) {
20141
+ this.logDeletedRecords(
20142
+ `Removed CNAME record for ${cdnDomain}`,
20143
+ await AliyunService.deleteDomainRecords({
20144
+ domainName,
20145
+ rr,
20146
+ type: "CNAME",
20147
+ value: cname
20148
+ })
20149
+ );
20150
+ } else {
20151
+ ui.warn(`No CDN CNAME found for ${cdnDomain}; deleting matching CNAME records by host.`);
20152
+ this.logDeletedRecords(
20153
+ `Removed CNAME record for ${cdnDomain}`,
20154
+ await AliyunService.deleteDomainRecords({
20155
+ domainName,
20156
+ rr,
20157
+ type: "CNAME"
20158
+ })
20159
+ );
20160
+ }
20161
+ if (!subdomain) {
20162
+ this.logDeletedRecords(
20163
+ `Removed CNAME record for www.${domainName}`,
20164
+ await AliyunService.deleteDomainRecords({
20165
+ domainName,
20166
+ rr: "www",
20167
+ type: "CNAME",
20168
+ value: domainName
20169
+ })
20170
+ );
20171
+ }
20172
+ await AliyunService.deleteCdnDomain(cdnDomain);
20173
+ await AliyunService.deleteBucketAndContents(bucketName);
20174
+ }
20175
+ async deleteServerResources(service) {
20176
+ const network = this.getNetworkConfig(service);
20177
+ const subdomain = network.subdomain?.trim();
20178
+ if (!subdomain) {
20179
+ ui.warn(`No subdomain configured for ${service.id}; nothing to tear down on DNS/nginx.`);
20180
+ return;
20181
+ }
20182
+ const server = network.server?.trim();
20183
+ if (!server) {
20184
+ throw new Error(`Missing network.server for ${service.id}`);
20185
+ }
20186
+ await this.removeRemoteConfig(server, `/etc/nginx/sites-enabled/${subdomain}.conf`);
20187
+ ui.success(`Removed nginx config for ${subdomain} from ${server}`);
20188
+ await this.reloadNginx(server);
20189
+ ui.success(`Reloaded nginx on ${server}`);
20190
+ const domainName = network.domain?.trim() || "pmate.chat";
20191
+ const serverAddress = await resolveServerAddress(server);
20192
+ this.logDeletedRecords(
20193
+ `Removed A record for ${subdomain}.${domainName}`,
20194
+ await AliyunService.deleteDomainRecords({
20195
+ domainName,
20196
+ rr: subdomain,
20197
+ type: "A",
20198
+ value: serverAddress
20199
+ })
20200
+ );
20201
+ }
20202
+ async deleteFcResources(service) {
20203
+ const fc = this.getFcConfig(service);
20204
+ const domainName = fc.domain?.trim();
20205
+ if (!domainName) {
20206
+ ui.warn(`No fc.domain configured for ${service.id}; nothing to tear down on DNS/FC custom domain.`);
20207
+ return;
20208
+ }
20209
+ const region = fc.region?.trim();
20210
+ if (!region) {
20211
+ throw new Error(`Missing fc.region for ${service.id}`);
20212
+ }
20213
+ const subdomain = fc.subdomain?.trim();
20214
+ const rr = subdomain || "@";
20215
+ const host = subdomain ? `${subdomain}.${domainName}` : domainName;
20216
+ await AliyunService.deleteFcCustomDomain(host, region);
20217
+ const accountId = await AliyunService.getAccountId();
20218
+ this.logDeletedRecords(
20219
+ `Removed CNAME record for ${host}`,
20220
+ await AliyunService.deleteDomainRecords({
20221
+ domainName,
20222
+ rr,
20223
+ type: "CNAME",
20224
+ value: `${accountId}.${region}.fc.aliyuncs.com`
20225
+ })
20226
+ );
20227
+ }
20228
+ getServiceType(service) {
20229
+ return "runtimeType" in service ? service.runtimeType : service.type;
20230
+ }
20231
+ getStaticSiteConfig(service) {
20232
+ if ("runtimeType" in service) {
20233
+ return {
20234
+ bucket: service.deploy?.bucket,
20235
+ domain: service.deploy?.domain,
20236
+ subdomain: service.deploy?.subdomain
20237
+ };
20238
+ }
20239
+ return service.site ?? {};
20240
+ }
20241
+ getNetworkConfig(service) {
20242
+ if ("runtimeType" in service) {
20243
+ return {
20244
+ server: service.deploy?.server,
20245
+ domain: service.deploy?.domain,
20246
+ subdomain: service.deploy?.subdomain
20247
+ };
20248
+ }
20249
+ return service.network ?? {};
20250
+ }
20251
+ getFcConfig(service) {
20252
+ if ("runtimeType" in service) {
20253
+ return {
20254
+ region: void 0,
20255
+ domain: service.deploy?.domain,
20256
+ subdomain: service.deploy?.subdomain
20257
+ };
20258
+ }
20259
+ return service.fc ?? {};
20260
+ }
20261
+ logDeletedRecords(message, deletedCount) {
20262
+ if (deletedCount > 0) {
20263
+ ui.success(message, { deletedCount });
20264
+ return;
20265
+ }
20266
+ ui.warn(`${message} skipped`, { deletedCount });
20267
+ }
20268
+ removeRemoteConfig(server, remotePath) {
20269
+ return new Promise((resolve, reject) => {
20270
+ const target = `pmate@${server}`;
20271
+ const child = (0, import_node_child_process11.spawn)("ssh", [target, "sudo", "rm", "-f", remotePath], {
20272
+ stdio: ["ignore", "inherit", "inherit"]
20273
+ });
20274
+ child.on("error", (error) => {
20275
+ reject(error);
20276
+ });
20277
+ child.on("close", (code) => {
20278
+ if (code === 0) {
20279
+ resolve();
20280
+ return;
20281
+ }
20282
+ reject(new Error(`ssh to ${server} exited with code ${code}`));
20283
+ });
20284
+ });
20285
+ }
20286
+ reloadNginx(server) {
20287
+ return new Promise((resolve, reject) => {
20288
+ const target = `pmate@${server}`;
20289
+ const child = (0, import_node_child_process11.spawn)("ssh", [target, "sudo", "nginx", "-s", "reload"], {
20290
+ stdio: ["ignore", "inherit", "inherit"]
20291
+ });
20292
+ child.on("error", (error) => {
20293
+ reject(error);
20294
+ });
20295
+ child.on("close", (code) => {
20296
+ if (code === 0) {
20297
+ resolve();
20298
+ return;
20299
+ }
20300
+ reject(new Error(`nginx reload on ${server} exited with code ${code}`));
20301
+ });
20302
+ });
20303
+ }
20055
20304
  renderJson(value, asJson) {
20056
20305
  if (!asJson) {
20057
20306
  return;
@@ -20061,9 +20310,9 @@ var ServiceCommandHandler = class {
20061
20310
  };
20062
20311
 
20063
20312
  // src/commands/VercelCommandHandler.ts
20064
- var import_node_child_process11 = require("child_process");
20313
+ var import_node_child_process12 = require("child_process");
20065
20314
  var import_node_util2 = require("util");
20066
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process11.execFile);
20315
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process12.execFile);
20067
20316
  function parseDeploymentTimestamp(deployment) {
20068
20317
  const candidates = [deployment.createdAt, deployment.readyAt, deployment.ready];
20069
20318
  for (const value of candidates) {
@@ -22594,7 +22843,7 @@ function parseServiceEnvArgs(appId, tokens) {
22594
22843
  const rest = [...tokens];
22595
22844
  const action = rest.shift();
22596
22845
  if (!action) {
22597
- throw new Error("Usage: service <ns:app> env list|get|set|unset|resolve|pull|copy-from");
22846
+ throw new Error("Usage: service <ns:app> env list|get|set|unset|resolve|pull|copy-from|update-scope");
22598
22847
  }
22599
22848
  let baseUrl;
22600
22849
  let json = false;
@@ -22824,6 +23073,53 @@ function parseServiceEnvArgs(appId, tokens) {
22824
23073
  }
22825
23074
  return { type: "service" /* Service */, action: "env-copy-from", targetAppId: appId, sourceAppId, scope, includeSecret, overwrite, onlyKeys, baseUrl, json };
22826
23075
  }
23076
+ if (action === "update-scope") {
23077
+ let fromScope;
23078
+ let toScope;
23079
+ let overwrite = false;
23080
+ let onlyKeys;
23081
+ while (rest.length > 0) {
23082
+ const token = rest.shift();
23083
+ switch (token) {
23084
+ case "--from":
23085
+ case "--from-scope":
23086
+ fromScope = requireValue6(rest, token);
23087
+ break;
23088
+ case "--to":
23089
+ case "--to-scope":
23090
+ toScope = requireValue6(rest, token);
23091
+ break;
23092
+ case "--overwrite":
23093
+ overwrite = true;
23094
+ break;
23095
+ case "--only":
23096
+ onlyKeys = requireValue6(rest, token).split(",").map((item) => item.trim()).filter(Boolean);
23097
+ break;
23098
+ case "--base-url":
23099
+ baseUrl = requireValue6(rest, token);
23100
+ break;
23101
+ case "--json":
23102
+ json = true;
23103
+ break;
23104
+ default:
23105
+ throw new Error(`Unknown service env update-scope option: ${token}`);
23106
+ }
23107
+ }
23108
+ if (!fromScope || !toScope) {
23109
+ throw new Error("Usage: service <ns:app> env update-scope --from <scope> --to <scope> [--overwrite] [--only <key,key>] [--base-url <url>] [--json]");
23110
+ }
23111
+ return {
23112
+ type: "service" /* Service */,
23113
+ action: "env-update-scope",
23114
+ appId,
23115
+ fromScope,
23116
+ toScope,
23117
+ overwrite,
23118
+ onlyKeys,
23119
+ baseUrl,
23120
+ json
23121
+ };
23122
+ }
22827
23123
  throw new Error(`Unknown service env action: ${action}`);
22828
23124
  }
22829
23125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.24.25",
3
+ "version": "0.24.26",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {