@pmate/cli 0.24.25 → 0.24.27

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
@@ -913,6 +913,7 @@ type ServiceCommandArgs = {
913
913
  action: "deploy" | "stop";
914
914
  namespace: string;
915
915
  name: string;
916
+ env?: string;
916
917
  baseUrl?: string;
917
918
  json: boolean;
918
919
  } | {
@@ -920,6 +921,7 @@ type ServiceCommandArgs = {
920
921
  action: "init";
921
922
  namespace: string;
922
923
  name: string;
924
+ env: string;
923
925
  cwd?: string;
924
926
  baseUrl?: string;
925
927
  json: boolean;
@@ -943,6 +945,36 @@ type ServiceCommandArgs = {
943
945
  includeSecretValue: boolean;
944
946
  baseUrl?: string;
945
947
  json: boolean;
948
+ } | {
949
+ type: Command.Service;
950
+ action: "service-env-list";
951
+ appId: string;
952
+ baseUrl?: string;
953
+ json: boolean;
954
+ } | {
955
+ type: Command.Service;
956
+ action: "service-env-get";
957
+ appId: string;
958
+ envName: string;
959
+ baseUrl?: string;
960
+ json: boolean;
961
+ } | {
962
+ type: Command.Service;
963
+ action: "service-env-add";
964
+ appId: string;
965
+ envName: string;
966
+ baseUrl?: string;
967
+ json: boolean;
968
+ } | {
969
+ type: Command.Service;
970
+ action: "service-env-expose";
971
+ appId: string;
972
+ envName: string;
973
+ exposeType?: "https" | "websocket";
974
+ off: boolean;
975
+ domain?: string;
976
+ baseUrl?: string;
977
+ json: boolean;
946
978
  } | {
947
979
  type: Command.Service;
948
980
  action: "env-get";
@@ -1000,6 +1032,16 @@ type ServiceCommandArgs = {
1000
1032
  onlyKeys?: string[];
1001
1033
  baseUrl?: string;
1002
1034
  json: boolean;
1035
+ } | {
1036
+ type: Command.Service;
1037
+ action: "env-update-scope";
1038
+ appId: string;
1039
+ fromScope: string;
1040
+ toScope: string;
1041
+ overwrite: boolean;
1042
+ onlyKeys?: string[];
1043
+ baseUrl?: string;
1044
+ json: boolean;
1003
1045
  };
1004
1046
  type VercelProjectsCommandArgs = {
1005
1047
  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
@@ -18198,6 +18212,20 @@ var ServiceRegistryClient = class {
18198
18212
  }
18199
18213
  );
18200
18214
  }
18215
+ async getServiceEnv(serviceId, envName) {
18216
+ const payload = await this.request(
18217
+ "GET",
18218
+ `/services/by-id/${encodeURIComponent(serviceId)}/envs/${encodeURIComponent(envName)}`
18219
+ );
18220
+ return payload.item;
18221
+ }
18222
+ async appendDeployHistory(serviceId, input) {
18223
+ return this.requestWithBody(
18224
+ "POST",
18225
+ `/services/by-id/${encodeURIComponent(serviceId)}/deploy-history`,
18226
+ input
18227
+ );
18228
+ }
18201
18229
  async pullEnv(serviceId, options) {
18202
18230
  const pathname = `/services/by-id/${encodeURIComponent(serviceId)}/env/pull`;
18203
18231
  const response = await fetch(`${this.resolveBaseUrl()}${pathname}`, {
@@ -18313,6 +18341,12 @@ var ServiceRuntimeRunner = class {
18313
18341
  }
18314
18342
  async deploy(service, options = {}) {
18315
18343
  switch (this.getType(service)) {
18344
+ case "node-api":
18345
+ await this.deployNode(service, options);
18346
+ return;
18347
+ case "node-job":
18348
+ await this.deployScriptManaged(service);
18349
+ return;
18316
18350
  case "vite":
18317
18351
  await this.deployVite(service, options);
18318
18352
  return;
@@ -18320,14 +18354,14 @@ var ServiceRuntimeRunner = class {
18320
18354
  await this.deployStatic(service, options);
18321
18355
  return;
18322
18356
  case "node":
18323
- await this.deployNode(service);
18357
+ await this.deployNode(service, options);
18324
18358
  return;
18325
18359
  case "rust":
18326
18360
  case "python":
18327
18361
  await this.deployScriptManaged(service);
18328
18362
  return;
18329
18363
  case "fc":
18330
- await this.deployFc(service);
18364
+ await this.deployFc(service, options);
18331
18365
  return;
18332
18366
  case "ios":
18333
18367
  case "android":
@@ -18340,6 +18374,7 @@ var ServiceRuntimeRunner = class {
18340
18374
  }
18341
18375
  async stop(service) {
18342
18376
  switch (this.getType(service)) {
18377
+ case "node-job":
18343
18378
  case "rust":
18344
18379
  case "python":
18345
18380
  await this.stopScriptManaged(service);
@@ -18362,6 +18397,12 @@ var ServiceRuntimeRunner = class {
18362
18397
  case "static":
18363
18398
  await this.initStaticLike(service);
18364
18399
  return;
18400
+ case "node-api":
18401
+ await this.initServerApp(service, options, { ensurePackageJson: true });
18402
+ return;
18403
+ case "node-job":
18404
+ await this.initServerApp(service, options, { ensurePackageJson: false });
18405
+ return;
18365
18406
  case "node":
18366
18407
  await this.initServerApp(service, options, { ensurePackageJson: true });
18367
18408
  return;
@@ -18574,7 +18615,7 @@ var ServiceRuntimeRunner = class {
18574
18615
  }
18575
18616
  await AliyunService.uploadDirectory(srcDir, bucketName);
18576
18617
  }
18577
- async deployFc(service) {
18618
+ async deployFc(service, options = {}) {
18578
18619
  const fc = this.getFc(service);
18579
18620
  this.validateFcConfig(service, fc);
18580
18621
  const repoRoot = this.resolveRepoRoot(service);
@@ -18592,7 +18633,7 @@ var ServiceRuntimeRunner = class {
18592
18633
  await this.runCommand("pnpm", ["--filter", pkgName, "build"], repoRoot);
18593
18634
  const registry = new ServiceRegistryClient();
18594
18635
  const appId = `${service.namespace}:${service.name}`;
18595
- const stage = fc.deployStage ?? "prod";
18636
+ const stage = this.envNameToStage(options.envName ?? fc.deployStage ?? "prod");
18596
18637
  const requiredEnv = await this.syncFcRequiredEnv(service, registry, stage);
18597
18638
  const resolved = await registry.resolveEnv(appId, {
18598
18639
  stage,
@@ -18690,7 +18731,7 @@ var ServiceRuntimeRunner = class {
18690
18731
  url: this.buildFcUrl(fc)
18691
18732
  });
18692
18733
  }
18693
- async deployNode(service) {
18734
+ async deployNode(service, options = {}) {
18694
18735
  const network = this.getNetwork(service);
18695
18736
  const server = network.server?.trim();
18696
18737
  const repository = network.repository?.trim();
@@ -18717,7 +18758,7 @@ var ServiceRuntimeRunner = class {
18717
18758
  `pkg="${appDir}/package.json"`,
18718
18759
  `if [ -f "$pkg" ] && node -e 'const fs=require("node:fs"); const p=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.exit(p?.scripts?.build ? 0 : 1);' "$pkg"; then (cd "${appDir}" && pnpm run build); fi`
18719
18760
  ].join("; ");
18720
- const envSyncCommand = await this.buildNodeEnvSyncCommand(service.id, appDir);
18761
+ const envSyncCommand = await this.buildNodeEnvSyncCommand(service.id, appDir, options.envName);
18721
18762
  const command = [
18722
18763
  `cd "$HOME"`,
18723
18764
  `[ -d "${repoDir}/.git" ] || git clone "${gitUrl}" "${repoDir}"`,
@@ -18735,13 +18776,13 @@ var ServiceRuntimeRunner = class {
18735
18776
  ].join(" && ");
18736
18777
  await this.runRemoteCommand(server, command);
18737
18778
  }
18738
- async buildNodeEnvSyncCommand(serviceId, appDir) {
18779
+ async buildNodeEnvSyncCommand(serviceId, appDir, envName = "prod") {
18739
18780
  if (!this.envProvider) {
18740
18781
  return void 0;
18741
18782
  }
18742
18783
  let pulled;
18743
18784
  try {
18744
- pulled = await this.envProvider(serviceId, { stage: "prod", format: "dotenv" });
18785
+ pulled = await this.envProvider(serviceId, { stage: this.envNameToStage(envName), format: "dotenv" });
18745
18786
  } catch (error) {
18746
18787
  if (error instanceof ServiceEnvNotFoundError) {
18747
18788
  ui.warn(`No env found for ${serviceId}; skipping .env.local sync.`);
@@ -18963,6 +19004,12 @@ var ServiceRuntimeRunner = class {
18963
19004
  }
18964
19005
  return payload.data;
18965
19006
  }
19007
+ envNameToStage(envName) {
19008
+ if (envName === "dev" || envName === "preview" || envName === "prod") {
19009
+ return envName;
19010
+ }
19011
+ return "preview";
19012
+ }
18966
19013
  renderPage(page, options = {}) {
18967
19014
  ui.info("Page", {
18968
19015
  page: page.page,
@@ -19443,10 +19490,15 @@ var ServiceRuntimeRunner = class {
19443
19490
  return [];
19444
19491
  }
19445
19492
  const appId = `${service.namespace}:${service.name}`;
19446
- const items = await registry.listEnvItems(appId, {
19493
+ const scopedItems = await registry.listEnvItems(appId, {
19447
19494
  scope: stage,
19448
19495
  includeSecretMeta: true
19449
19496
  });
19497
+ const allItems = await registry.listEnvItems(appId, {
19498
+ scope: "all",
19499
+ includeSecretMeta: true
19500
+ });
19501
+ const items = [...allItems, ...scopedItems];
19450
19502
  const requiredEnv = Array.from(
19451
19503
  new Set(
19452
19504
  items.map((item) => item.key ?? item.name).filter((key) => typeof key === "string" && key.trim().length > 0).map((key) => key.trim())
@@ -19693,6 +19745,8 @@ var ServiceCommandHandler = class {
19693
19745
  return;
19694
19746
  }
19695
19747
  case "delete": {
19748
+ const service = await registry.getService(args.namespace, args.name);
19749
+ await this.deleteServiceResources(service);
19696
19750
  const record = await this.request(
19697
19751
  baseUrl,
19698
19752
  "DELETE",
@@ -19737,17 +19791,32 @@ var ServiceCommandHandler = class {
19737
19791
  }
19738
19792
  case "deploy": {
19739
19793
  const record = await registry.getService(args.namespace, args.name);
19740
- await runtime.deploy(record);
19794
+ const service = await this.materializeServiceEnv(registry, record, args.env);
19795
+ try {
19796
+ await runtime.deploy(service, { envName: args.env });
19797
+ await this.appendDeployHistory(registry, record.id, args.env, "success");
19798
+ } catch (error) {
19799
+ await this.appendDeployHistory(
19800
+ registry,
19801
+ record.id,
19802
+ args.env,
19803
+ "failed",
19804
+ error instanceof Error ? error.message : String(error)
19805
+ );
19806
+ throw error;
19807
+ }
19741
19808
  return;
19742
19809
  }
19743
19810
  case "stop": {
19744
19811
  const record = await registry.getService(args.namespace, args.name);
19745
- await runtime.stop(record);
19812
+ const service = args.env ? await this.materializeServiceEnv(registry, record, args.env) : record;
19813
+ await runtime.stop(service);
19746
19814
  return;
19747
19815
  }
19748
19816
  case "init": {
19749
19817
  const record = await registry.getService(args.namespace, args.name);
19750
- await runtime.init(record, { cwd: args.cwd });
19818
+ const service = await this.materializeServiceEnv(registry, record, args.env);
19819
+ await runtime.init(service, { cwd: args.cwd });
19751
19820
  return;
19752
19821
  }
19753
19822
  case "log": {
@@ -19863,6 +19932,188 @@ var ServiceCommandHandler = class {
19863
19932
  this.renderJson(payload, args.json);
19864
19933
  return;
19865
19934
  }
19935
+ case "env-update-scope": {
19936
+ const payload = await this.request(
19937
+ baseUrl,
19938
+ "POST",
19939
+ `${this.buildEnvBasePath(args.appId)}/update-scope`,
19940
+ {
19941
+ fromScope: args.fromScope,
19942
+ toScope: args.toScope,
19943
+ ...args.overwrite ? { overwrite: true } : {},
19944
+ ...args.onlyKeys?.length ? { onlyKeys: args.onlyKeys } : {}
19945
+ }
19946
+ );
19947
+ ui.success(`Updated env scope for ${args.appId}: ${args.fromScope} -> ${args.toScope}`);
19948
+ await this.syncFcRequiredEnvFromServiceEnv(registry, args.appId, args.toScope);
19949
+ this.renderJson(payload, args.json);
19950
+ return;
19951
+ }
19952
+ case "service-env-list": {
19953
+ const payload = await this.request(
19954
+ baseUrl,
19955
+ "GET",
19956
+ this.buildServiceEnvBasePath(args.appId)
19957
+ );
19958
+ this.renderJson(payload, true);
19959
+ return;
19960
+ }
19961
+ case "service-env-get": {
19962
+ const payload = await this.request(
19963
+ baseUrl,
19964
+ "GET",
19965
+ `${this.buildServiceEnvBasePath(args.appId)}/${encodeURIComponent(args.envName)}`
19966
+ );
19967
+ this.renderJson(payload, true);
19968
+ return;
19969
+ }
19970
+ case "service-env-add": {
19971
+ const payload = await this.request(
19972
+ baseUrl,
19973
+ "PUT",
19974
+ `${this.buildServiceEnvBasePath(args.appId)}/${encodeURIComponent(args.envName)}`,
19975
+ {
19976
+ name: args.envName
19977
+ }
19978
+ );
19979
+ ui.success(`Updated service env ${args.envName} for ${args.appId}`);
19980
+ this.renderJson(payload, args.json);
19981
+ return;
19982
+ }
19983
+ case "service-env-expose": {
19984
+ const payload = await this.request(
19985
+ baseUrl,
19986
+ "POST",
19987
+ `${this.buildServiceEnvBasePath(args.appId)}/${encodeURIComponent(args.envName)}/expose`,
19988
+ {
19989
+ ...args.exposeType ? { type: args.exposeType } : {},
19990
+ ...args.domain ? { domain: args.domain } : {},
19991
+ ...args.off ? { off: true } : {}
19992
+ }
19993
+ );
19994
+ ui.success(
19995
+ args.off ? `Disabled network for ${args.appId} ${args.envName}` : `Updated network for ${args.appId} ${args.envName}`
19996
+ );
19997
+ this.renderJson(payload, args.json);
19998
+ return;
19999
+ }
20000
+ }
20001
+ }
20002
+ async materializeServiceEnv(registry, service, envName) {
20003
+ if (!envName || !this.isDefinitionStyleService(service)) {
20004
+ return service;
20005
+ }
20006
+ const env = await registry.getServiceEnv(service.id, envName);
20007
+ return this.buildRuntimeServiceRecord(service, env);
20008
+ }
20009
+ isDefinitionStyleService(service) {
20010
+ return "repository" in service || "type" in service && (service.type === "node-api" || service.type === "node-job") || !("site" in service) && !("network" in service) && !("fc" in service) && !("runtimeType" in service);
20011
+ }
20012
+ buildRuntimeServiceRecord(service, env) {
20013
+ if (!("type" in service)) {
20014
+ return service;
20015
+ }
20016
+ const repository = service.repository?.trim();
20017
+ const dir = service.dir?.trim();
20018
+ const runtime = env.runtime;
20019
+ if (!runtime) {
20020
+ throw new Error(`Missing runtime for ${service.id} env ${env.name}`);
20021
+ }
20022
+ if (!repository) {
20023
+ throw new Error(`Missing service.repository for ${service.id}`);
20024
+ }
20025
+ if (!dir) {
20026
+ throw new Error(`Missing service.dir for ${service.id}`);
20027
+ }
20028
+ if (runtime.type === "ali-oss") {
20029
+ const domainParts = splitPrimaryDomain(env.network?.domain);
20030
+ return {
20031
+ id: service.id,
20032
+ namespace: service.namespace,
20033
+ name: service.name,
20034
+ type: service.type === "static" ? "static" : "vite",
20035
+ site: {
20036
+ exposeType: "static-site",
20037
+ repository,
20038
+ dir,
20039
+ bucket: requireString(runtime.bucket, `runtime.bucket for ${service.id} ${env.name}`),
20040
+ domain: domainParts.domain,
20041
+ ...domainParts.subdomain ? { subdomain: domainParts.subdomain } : {}
20042
+ }
20043
+ };
20044
+ }
20045
+ if (runtime.type === "ssh-pm2") {
20046
+ return {
20047
+ id: service.id,
20048
+ namespace: service.namespace,
20049
+ name: runtime.appName?.trim() || service.name,
20050
+ type: service.type === "node-api" ? "node-api" : "node",
20051
+ network: {
20052
+ exposeType: env.network?.type === "websocket" ? "websocket" : env.network ? "rest" : "internal",
20053
+ repository,
20054
+ dir,
20055
+ server: requireString(runtime.server, `runtime.server for ${service.id} ${env.name}`),
20056
+ ...runtime.port !== void 0 ? { port: runtime.port } : {},
20057
+ ...splitPrimaryDomain(env.network?.domain)
20058
+ }
20059
+ };
20060
+ }
20061
+ if (runtime.type === "ssh-script") {
20062
+ return {
20063
+ id: service.id,
20064
+ namespace: service.namespace,
20065
+ name: service.name,
20066
+ type: service.type === "node-job" ? "node-job" : service.type === "rust" ? "rust" : "python",
20067
+ network: {
20068
+ exposeType: env.network?.type === "websocket" ? "websocket" : env.network ? "rest" : "internal",
20069
+ repository,
20070
+ dir: runtime.cwd?.trim() || dir,
20071
+ server: requireString(runtime.server, `runtime.server for ${service.id} ${env.name}`),
20072
+ ...runtime.port !== void 0 ? { port: runtime.port } : {},
20073
+ ...splitPrimaryDomain(env.network?.domain)
20074
+ },
20075
+ runner: {
20076
+ script: requireString(runtime.command, `runtime.command for ${service.id} ${env.name}`)
20077
+ }
20078
+ };
20079
+ }
20080
+ if (runtime.type === "fc") {
20081
+ const domainParts = splitPrimaryDomain(env.network?.domain);
20082
+ return {
20083
+ id: service.id,
20084
+ namespace: service.namespace,
20085
+ name: service.name,
20086
+ type: "fc",
20087
+ fc: {
20088
+ provider: runtime.provider ?? "aliyun",
20089
+ region: requireString(runtime.region, `runtime.region for ${service.id} ${env.name}`),
20090
+ repository,
20091
+ dir,
20092
+ functionName: requireString(runtime.functionName, `runtime.functionName for ${service.id} ${env.name}`),
20093
+ ...runtime.configFile ? { configFile: runtime.configFile } : {},
20094
+ ...runtime.qualifier ? { qualifier: runtime.qualifier } : {},
20095
+ ...domainParts.domain ? { domain: domainParts.domain } : {},
20096
+ ...domainParts.subdomain ? { subdomain: domainParts.subdomain } : {},
20097
+ deployStage: env.name === "prod" || env.name === "preview" || env.name === "dev" ? env.name : "preview"
20098
+ }
20099
+ };
20100
+ }
20101
+ throw new Error(`Unsupported runtime ${runtime.type} for ${service.id} env ${env.name}`);
20102
+ }
20103
+ async appendDeployHistory(registry, serviceId, environment, status, message) {
20104
+ try {
20105
+ await registry.appendDeployHistory(serviceId, {
20106
+ environment,
20107
+ status,
20108
+ ...message ? { message } : {}
20109
+ });
20110
+ } catch (error) {
20111
+ ui.warn("Failed to append deploy history", {
20112
+ serviceId,
20113
+ environment,
20114
+ status,
20115
+ message: error instanceof Error ? error.message : String(error)
20116
+ });
19866
20117
  }
19867
20118
  }
19868
20119
  resolveBaseUrl(baseUrl) {
@@ -19905,6 +20156,9 @@ var ServiceCommandHandler = class {
19905
20156
  buildEnvBasePath(appId) {
19906
20157
  return `/services/by-id/${encodeURIComponent(appId)}/env`;
19907
20158
  }
20159
+ buildServiceEnvBasePath(appId) {
20160
+ return `/services/by-id/${encodeURIComponent(appId)}/envs`;
20161
+ }
19908
20162
  buildEnvPath(appId, query) {
19909
20163
  const params = new URLSearchParams();
19910
20164
  if (query.scope) {
@@ -20014,10 +20268,7 @@ var ServiceCommandHandler = class {
20014
20268
  if (!("type" in service) || service.type !== "fc" || !service.fc) {
20015
20269
  return;
20016
20270
  }
20017
- const items = await registry.listEnvItems(appId, {
20018
- scope,
20019
- includeSecretMeta: true
20020
- });
20271
+ const items = await this.listEffectiveEnvItems(registry, appId, scope);
20021
20272
  const requiredEnv = Array.from(
20022
20273
  new Set(
20023
20274
  items.map((item) => item.key ?? item.name).filter((key) => typeof key === "string" && key.trim().length > 0).map((key) => key.trim())
@@ -20039,6 +20290,20 @@ var ServiceCommandHandler = class {
20039
20290
  const normalizedRight = [...right].sort();
20040
20291
  return normalizedLeft.every((value, index) => value === normalizedRight[index]);
20041
20292
  }
20293
+ async listEffectiveEnvItems(registry, appId, scope) {
20294
+ const scopedItems = await registry.listEnvItems(appId, {
20295
+ scope,
20296
+ includeSecretMeta: true
20297
+ });
20298
+ if (scope === "all") {
20299
+ return scopedItems;
20300
+ }
20301
+ const allItems = await registry.listEnvItems(appId, {
20302
+ scope: "all",
20303
+ includeSecretMeta: true
20304
+ });
20305
+ return [...allItems, ...scopedItems];
20306
+ }
20042
20307
  renderList(items, asJson) {
20043
20308
  if (asJson) {
20044
20309
  ui.plain(JSON.stringify(items, null, 2));
@@ -20052,6 +20317,206 @@ var ServiceCommandHandler = class {
20052
20317
  ui.info(formatNamespacedDisplayId(item.namespace, item.name), item);
20053
20318
  }
20054
20319
  }
20320
+ async deleteServiceResources(service) {
20321
+ switch (this.getServiceType(service)) {
20322
+ case "vite":
20323
+ case "static":
20324
+ await this.deleteStaticResources(service);
20325
+ return;
20326
+ case "node":
20327
+ case "rust":
20328
+ case "python":
20329
+ await this.deleteServerResources(service);
20330
+ return;
20331
+ case "fc":
20332
+ await this.deleteFcResources(service);
20333
+ return;
20334
+ case "ios":
20335
+ case "android":
20336
+ ui.warn(`Service type "${this.getServiceType(service)}" has no runtime resources to delete`);
20337
+ return;
20338
+ default:
20339
+ throw new Error(`Unsupported service type for delete: ${this.getServiceType(service)}`);
20340
+ }
20341
+ }
20342
+ async deleteStaticResources(service) {
20343
+ const site = this.getStaticSiteConfig(service);
20344
+ const bucketName = site.bucket?.trim();
20345
+ const domainName = site.domain?.trim();
20346
+ if (!bucketName) {
20347
+ throw new Error(`Missing site.bucket for ${service.id}`);
20348
+ }
20349
+ if (!domainName) {
20350
+ throw new Error(`Missing site.domain for ${service.id}`);
20351
+ }
20352
+ const subdomain = site.subdomain?.trim();
20353
+ const rr = subdomain || "@";
20354
+ const cdnDomain = subdomain ? `${subdomain}.${domainName}` : domainName;
20355
+ const cname = await AliyunService.getCdnCname(cdnDomain);
20356
+ if (cname) {
20357
+ this.logDeletedRecords(
20358
+ `Removed CNAME record for ${cdnDomain}`,
20359
+ await AliyunService.deleteDomainRecords({
20360
+ domainName,
20361
+ rr,
20362
+ type: "CNAME",
20363
+ value: cname
20364
+ })
20365
+ );
20366
+ } else {
20367
+ ui.warn(`No CDN CNAME found for ${cdnDomain}; deleting matching CNAME records by host.`);
20368
+ this.logDeletedRecords(
20369
+ `Removed CNAME record for ${cdnDomain}`,
20370
+ await AliyunService.deleteDomainRecords({
20371
+ domainName,
20372
+ rr,
20373
+ type: "CNAME"
20374
+ })
20375
+ );
20376
+ }
20377
+ if (!subdomain) {
20378
+ this.logDeletedRecords(
20379
+ `Removed CNAME record for www.${domainName}`,
20380
+ await AliyunService.deleteDomainRecords({
20381
+ domainName,
20382
+ rr: "www",
20383
+ type: "CNAME",
20384
+ value: domainName
20385
+ })
20386
+ );
20387
+ }
20388
+ await AliyunService.deleteCdnDomain(cdnDomain);
20389
+ await AliyunService.deleteBucketAndContents(bucketName);
20390
+ }
20391
+ async deleteServerResources(service) {
20392
+ const network = this.getNetworkConfig(service);
20393
+ const subdomain = network.subdomain?.trim();
20394
+ if (!subdomain) {
20395
+ ui.warn(`No subdomain configured for ${service.id}; nothing to tear down on DNS/nginx.`);
20396
+ return;
20397
+ }
20398
+ const server = network.server?.trim();
20399
+ if (!server) {
20400
+ throw new Error(`Missing network.server for ${service.id}`);
20401
+ }
20402
+ await this.removeRemoteConfig(server, `/etc/nginx/sites-enabled/${subdomain}.conf`);
20403
+ ui.success(`Removed nginx config for ${subdomain} from ${server}`);
20404
+ await this.reloadNginx(server);
20405
+ ui.success(`Reloaded nginx on ${server}`);
20406
+ const domainName = network.domain?.trim() || "pmate.chat";
20407
+ const serverAddress = await resolveServerAddress(server);
20408
+ this.logDeletedRecords(
20409
+ `Removed A record for ${subdomain}.${domainName}`,
20410
+ await AliyunService.deleteDomainRecords({
20411
+ domainName,
20412
+ rr: subdomain,
20413
+ type: "A",
20414
+ value: serverAddress
20415
+ })
20416
+ );
20417
+ }
20418
+ async deleteFcResources(service) {
20419
+ const fc = this.getFcConfig(service);
20420
+ const domainName = fc.domain?.trim();
20421
+ if (!domainName) {
20422
+ ui.warn(`No fc.domain configured for ${service.id}; nothing to tear down on DNS/FC custom domain.`);
20423
+ return;
20424
+ }
20425
+ const region = fc.region?.trim();
20426
+ if (!region) {
20427
+ throw new Error(`Missing fc.region for ${service.id}`);
20428
+ }
20429
+ const subdomain = fc.subdomain?.trim();
20430
+ const rr = subdomain || "@";
20431
+ const host = subdomain ? `${subdomain}.${domainName}` : domainName;
20432
+ await AliyunService.deleteFcCustomDomain(host, region);
20433
+ const accountId = await AliyunService.getAccountId();
20434
+ this.logDeletedRecords(
20435
+ `Removed CNAME record for ${host}`,
20436
+ await AliyunService.deleteDomainRecords({
20437
+ domainName,
20438
+ rr,
20439
+ type: "CNAME",
20440
+ value: `${accountId}.${region}.fc.aliyuncs.com`
20441
+ })
20442
+ );
20443
+ }
20444
+ getServiceType(service) {
20445
+ return "runtimeType" in service ? service.runtimeType : service.type;
20446
+ }
20447
+ getStaticSiteConfig(service) {
20448
+ if ("runtimeType" in service) {
20449
+ return {
20450
+ bucket: service.deploy?.bucket,
20451
+ domain: service.deploy?.domain,
20452
+ subdomain: service.deploy?.subdomain
20453
+ };
20454
+ }
20455
+ return service.site ?? {};
20456
+ }
20457
+ getNetworkConfig(service) {
20458
+ if ("runtimeType" in service) {
20459
+ return {
20460
+ server: service.deploy?.server,
20461
+ domain: service.deploy?.domain,
20462
+ subdomain: service.deploy?.subdomain
20463
+ };
20464
+ }
20465
+ return service.network ?? {};
20466
+ }
20467
+ getFcConfig(service) {
20468
+ if ("runtimeType" in service) {
20469
+ return {
20470
+ region: void 0,
20471
+ domain: service.deploy?.domain,
20472
+ subdomain: service.deploy?.subdomain
20473
+ };
20474
+ }
20475
+ return service.fc ?? {};
20476
+ }
20477
+ logDeletedRecords(message, deletedCount) {
20478
+ if (deletedCount > 0) {
20479
+ ui.success(message, { deletedCount });
20480
+ return;
20481
+ }
20482
+ ui.warn(`${message} skipped`, { deletedCount });
20483
+ }
20484
+ removeRemoteConfig(server, remotePath) {
20485
+ return new Promise((resolve, reject) => {
20486
+ const target = `pmate@${server}`;
20487
+ const child = (0, import_node_child_process11.spawn)("ssh", [target, "sudo", "rm", "-f", remotePath], {
20488
+ stdio: ["ignore", "inherit", "inherit"]
20489
+ });
20490
+ child.on("error", (error) => {
20491
+ reject(error);
20492
+ });
20493
+ child.on("close", (code) => {
20494
+ if (code === 0) {
20495
+ resolve();
20496
+ return;
20497
+ }
20498
+ reject(new Error(`ssh to ${server} exited with code ${code}`));
20499
+ });
20500
+ });
20501
+ }
20502
+ reloadNginx(server) {
20503
+ return new Promise((resolve, reject) => {
20504
+ const target = `pmate@${server}`;
20505
+ const child = (0, import_node_child_process11.spawn)("ssh", [target, "sudo", "nginx", "-s", "reload"], {
20506
+ stdio: ["ignore", "inherit", "inherit"]
20507
+ });
20508
+ child.on("error", (error) => {
20509
+ reject(error);
20510
+ });
20511
+ child.on("close", (code) => {
20512
+ if (code === 0) {
20513
+ resolve();
20514
+ return;
20515
+ }
20516
+ reject(new Error(`nginx reload on ${server} exited with code ${code}`));
20517
+ });
20518
+ });
20519
+ }
20055
20520
  renderJson(value, asJson) {
20056
20521
  if (!asJson) {
20057
20522
  return;
@@ -20059,11 +20524,31 @@ var ServiceCommandHandler = class {
20059
20524
  ui.plain(JSON.stringify(value, null, 2));
20060
20525
  }
20061
20526
  };
20527
+ function requireString(value, field) {
20528
+ if (typeof value !== "string" || !value.trim()) {
20529
+ throw new Error(`Missing ${field}`);
20530
+ }
20531
+ return value.trim();
20532
+ }
20533
+ function splitPrimaryDomain(host) {
20534
+ const normalized = host?.trim();
20535
+ if (!normalized) {
20536
+ return { domain: "pmate.chat" };
20537
+ }
20538
+ const parts = normalized.split(".").filter(Boolean);
20539
+ if (parts.length <= 2) {
20540
+ return { domain: normalized };
20541
+ }
20542
+ return {
20543
+ subdomain: parts.slice(0, -2).join("."),
20544
+ domain: parts.slice(-2).join(".")
20545
+ };
20546
+ }
20062
20547
 
20063
20548
  // src/commands/VercelCommandHandler.ts
20064
- var import_node_child_process11 = require("child_process");
20549
+ var import_node_child_process12 = require("child_process");
20065
20550
  var import_node_util2 = require("util");
20066
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process11.execFile);
20551
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process12.execFile);
20067
20552
  function parseDeploymentTimestamp(deployment) {
20068
20553
  const candidates = [deployment.createdAt, deployment.readyAt, deployment.ready];
20069
20554
  for (const value of candidates) {
@@ -22307,10 +22792,14 @@ init_namespacedRef();
22307
22792
  function parseServiceArgs(tokens) {
22308
22793
  const rest = [...tokens];
22309
22794
  const rawAction = rest.shift();
22310
- if (rawAction && looksLikeNamespacedRef(rawAction) && rest[0] === "env") {
22795
+ if (rawAction && looksLikeNamespacedRef(rawAction) && rest[0] === "env-vars") {
22311
22796
  rest.shift();
22312
22797
  return parseServiceEnvArgs(rawAction, rest);
22313
22798
  }
22799
+ if (rawAction && looksLikeNamespacedRef(rawAction) && rest[0] === "env") {
22800
+ rest.shift();
22801
+ return parseServiceEnvDefinitionArgs(rawAction, rest);
22802
+ }
22314
22803
  const action = rawAction === "view" ? "view" : rawAction;
22315
22804
  if (!action) {
22316
22805
  throw new Error("Missing service action. Use: service list|get|view|create|update|rename|delete|deploy|stop|init|log");
@@ -22380,6 +22869,7 @@ function parseServiceArgs(tokens) {
22380
22869
  let history = false;
22381
22870
  let search;
22382
22871
  let follow = false;
22872
+ let env;
22383
22873
  while (rest.length > 0) {
22384
22874
  const token = rest.shift();
22385
22875
  switch (token) {
@@ -22392,6 +22882,12 @@ function parseServiceArgs(tokens) {
22392
22882
  }
22393
22883
  cwd = requireValue6(rest, token);
22394
22884
  break;
22885
+ case "--env":
22886
+ if (action !== "deploy" && action !== "init" && action !== "stop") {
22887
+ throw new Error(`Unknown service ${action} option: ${token}`);
22888
+ }
22889
+ env = requireValue6(rest, token);
22890
+ break;
22395
22891
  case "--page":
22396
22892
  page = Number.parseInt(requireValue6(rest, token), 10);
22397
22893
  break;
@@ -22419,10 +22915,16 @@ function parseServiceArgs(tokens) {
22419
22915
  return { type: "service" /* Service */, action, namespace, name, baseUrl, json, page, history, search, follow };
22420
22916
  }
22421
22917
  if (action === "deploy" || action === "stop") {
22422
- return { type: "service" /* Service */, action, namespace, name, baseUrl, json };
22918
+ if (action === "deploy" && !env) {
22919
+ throw new Error("Missing required option --env for service deploy");
22920
+ }
22921
+ return { type: "service" /* Service */, action, namespace, name, env, baseUrl, json };
22423
22922
  }
22424
22923
  if (action === "init") {
22425
- return { type: "service" /* Service */, action, namespace, name, cwd, baseUrl, json };
22924
+ if (!env) {
22925
+ throw new Error("Missing required option --env for service init");
22926
+ }
22927
+ return { type: "service" /* Service */, action, namespace, name, env, cwd, baseUrl, json };
22426
22928
  }
22427
22929
  return { type: "service" /* Service */, action, namespace, name, baseUrl, json };
22428
22930
  }
@@ -22590,11 +23092,111 @@ function parseBareRenameId3(value, entityName) {
22590
23092
  function looksLikeNamespacedRef(value) {
22591
23093
  return value.includes(":") && !value.startsWith("-");
22592
23094
  }
23095
+ function parseServiceEnvDefinitionArgs(appId, tokens) {
23096
+ const rest = [...tokens];
23097
+ const action = rest.shift();
23098
+ if (!action) {
23099
+ throw new Error("Usage: service <ns:service> env list|get|add|expose");
23100
+ }
23101
+ let baseUrl;
23102
+ let json = false;
23103
+ if (action === "list") {
23104
+ while (rest.length > 0) {
23105
+ const token = rest.shift();
23106
+ switch (token) {
23107
+ case "--base-url":
23108
+ baseUrl = requireValue6(rest, token);
23109
+ break;
23110
+ case "--json":
23111
+ json = true;
23112
+ break;
23113
+ default:
23114
+ throw new Error(`Unknown service env list option: ${token}`);
23115
+ }
23116
+ }
23117
+ return { type: "service" /* Service */, action: "service-env-list", appId, baseUrl, json };
23118
+ }
23119
+ if (action === "get" || action === "add") {
23120
+ const envName = rest.shift();
23121
+ if (!envName || envName.startsWith("-")) {
23122
+ throw new Error(`Usage: service <ns:service> env ${action} <env> [--base-url <url>] [--json]`);
23123
+ }
23124
+ while (rest.length > 0) {
23125
+ const token = rest.shift();
23126
+ switch (token) {
23127
+ case "--base-url":
23128
+ baseUrl = requireValue6(rest, token);
23129
+ break;
23130
+ case "--json":
23131
+ json = true;
23132
+ break;
23133
+ default:
23134
+ throw new Error(`Unknown service env ${action} option: ${token}`);
23135
+ }
23136
+ }
23137
+ return {
23138
+ type: "service" /* Service */,
23139
+ action: action === "get" ? "service-env-get" : "service-env-add",
23140
+ appId,
23141
+ envName,
23142
+ baseUrl,
23143
+ json
23144
+ };
23145
+ }
23146
+ if (action === "expose") {
23147
+ const envName = rest.shift();
23148
+ if (!envName || envName.startsWith("-")) {
23149
+ throw new Error("Usage: service <ns:service> env expose <env> [--type https|websocket] [--domain <domain>] [--off]");
23150
+ }
23151
+ let exposeType;
23152
+ let domain;
23153
+ let off = false;
23154
+ while (rest.length > 0) {
23155
+ const token = rest.shift();
23156
+ switch (token) {
23157
+ case "--type": {
23158
+ const value = requireValue6(rest, token);
23159
+ if (value !== "https" && value !== "websocket") {
23160
+ throw new Error("--type must be https or websocket");
23161
+ }
23162
+ exposeType = value;
23163
+ break;
23164
+ }
23165
+ case "--domain":
23166
+ domain = requireValue6(rest, token);
23167
+ break;
23168
+ case "--off":
23169
+ off = true;
23170
+ break;
23171
+ case "--base-url":
23172
+ baseUrl = requireValue6(rest, token);
23173
+ break;
23174
+ case "--json":
23175
+ json = true;
23176
+ break;
23177
+ default:
23178
+ throw new Error(`Unknown service env expose option: ${token}`);
23179
+ }
23180
+ }
23181
+ return {
23182
+ type: "service" /* Service */,
23183
+ action: "service-env-expose",
23184
+ appId,
23185
+ envName,
23186
+ exposeType,
23187
+ domain,
23188
+ off,
23189
+ baseUrl,
23190
+ json
23191
+ };
23192
+ }
23193
+ throw new Error(`Unknown service env action: ${action}`);
23194
+ }
22593
23195
  function parseServiceEnvArgs(appId, tokens) {
22594
23196
  const rest = [...tokens];
22595
23197
  const action = rest.shift();
22596
23198
  if (!action) {
22597
- throw new Error("Usage: service <ns:app> env list|get|set|unset|resolve|pull|copy-from");
23199
+ throw new Error("Usage: service <ns:app> env list|get|set|unset|resolve|pull|copy-from|update-scope");
22598
23200
  }
22599
23201
  let baseUrl;
22600
23202
  let json = false;
@@ -22606,6 +23208,7 @@ function parseServiceEnvArgs(appId, tokens) {
22606
23208
  const token = rest.shift();
22607
23209
  switch (token) {
22608
23210
  case "--scope":
23211
+ case "--env":
22609
23212
  scope = requireValue6(rest, token);
22610
23213
  break;
22611
23214
  case "--include-secret-meta":
@@ -22637,6 +23240,7 @@ function parseServiceEnvArgs(appId, tokens) {
22637
23240
  const token = rest.shift();
22638
23241
  switch (token) {
22639
23242
  case "--scope":
23243
+ case "--env":
22640
23244
  scope = requireValue6(rest, token);
22641
23245
  break;
22642
23246
  case "--include-secret-value":
@@ -22652,9 +23256,7 @@ function parseServiceEnvArgs(appId, tokens) {
22652
23256
  throw new Error(`Unknown service env get option: ${token}`);
22653
23257
  }
22654
23258
  }
22655
- if (!scope) {
22656
- throw new Error("Usage: service <ns:app> env get <key> --scope <scope> [--include-secret-value]");
22657
- }
23259
+ scope = scope ?? "all";
22658
23260
  return { type: "service" /* Service */, action: "env-get", appId, key, scope, includeSecretValue, baseUrl, json };
22659
23261
  }
22660
23262
  if (action === "set") {
@@ -22673,6 +23275,7 @@ function parseServiceEnvArgs(appId, tokens) {
22673
23275
  const token = rest.shift();
22674
23276
  switch (token) {
22675
23277
  case "--scope":
23278
+ case "--env":
22676
23279
  scope = requireValue6(rest, token);
22677
23280
  break;
22678
23281
  case "--secret":
@@ -22691,9 +23294,7 @@ function parseServiceEnvArgs(appId, tokens) {
22691
23294
  throw new Error(`Unknown service env set option: ${token}`);
22692
23295
  }
22693
23296
  }
22694
- if (!scope) {
22695
- throw new Error("Usage: service <ns:app> env set <key> [value] --scope <scope> [--secret] [--prompt]");
22696
- }
23297
+ scope = scope ?? "all";
22697
23298
  if (!value && !secret && !prompt) {
22698
23299
  throw new Error("Missing env value. Pass a value or use --prompt.");
22699
23300
  }
@@ -22709,6 +23310,7 @@ function parseServiceEnvArgs(appId, tokens) {
22709
23310
  const token = rest.shift();
22710
23311
  switch (token) {
22711
23312
  case "--scope":
23313
+ case "--env":
22712
23314
  scope = requireValue6(rest, token);
22713
23315
  break;
22714
23316
  case "--base-url":
@@ -22721,9 +23323,7 @@ function parseServiceEnvArgs(appId, tokens) {
22721
23323
  throw new Error(`Unknown service env unset option: ${token}`);
22722
23324
  }
22723
23325
  }
22724
- if (!scope) {
22725
- throw new Error("Usage: service <ns:app> env unset <key> --scope <scope>");
22726
- }
23326
+ scope = scope ?? "all";
22727
23327
  return { type: "service" /* Service */, action: "env-unset", appId, key, scope, baseUrl, json };
22728
23328
  }
22729
23329
  if (action === "resolve") {
@@ -22824,6 +23424,53 @@ function parseServiceEnvArgs(appId, tokens) {
22824
23424
  }
22825
23425
  return { type: "service" /* Service */, action: "env-copy-from", targetAppId: appId, sourceAppId, scope, includeSecret, overwrite, onlyKeys, baseUrl, json };
22826
23426
  }
23427
+ if (action === "update-scope") {
23428
+ let fromScope;
23429
+ let toScope;
23430
+ let overwrite = false;
23431
+ let onlyKeys;
23432
+ while (rest.length > 0) {
23433
+ const token = rest.shift();
23434
+ switch (token) {
23435
+ case "--from":
23436
+ case "--from-scope":
23437
+ fromScope = requireValue6(rest, token);
23438
+ break;
23439
+ case "--to":
23440
+ case "--to-scope":
23441
+ toScope = requireValue6(rest, token);
23442
+ break;
23443
+ case "--overwrite":
23444
+ overwrite = true;
23445
+ break;
23446
+ case "--only":
23447
+ onlyKeys = requireValue6(rest, token).split(",").map((item) => item.trim()).filter(Boolean);
23448
+ break;
23449
+ case "--base-url":
23450
+ baseUrl = requireValue6(rest, token);
23451
+ break;
23452
+ case "--json":
23453
+ json = true;
23454
+ break;
23455
+ default:
23456
+ throw new Error(`Unknown service env update-scope option: ${token}`);
23457
+ }
23458
+ }
23459
+ if (!fromScope || !toScope) {
23460
+ throw new Error("Usage: service <ns:app> env update-scope --from <scope> --to <scope> [--overwrite] [--only <key,key>] [--base-url <url>] [--json]");
23461
+ }
23462
+ return {
23463
+ type: "service" /* Service */,
23464
+ action: "env-update-scope",
23465
+ appId,
23466
+ fromScope,
23467
+ toScope,
23468
+ overwrite,
23469
+ onlyKeys,
23470
+ baseUrl,
23471
+ json
23472
+ };
23473
+ }
22827
23474
  throw new Error(`Unknown service env action: ${action}`);
22828
23475
  }
22829
23476
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.24.25",
3
+ "version": "0.24.27",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {