@pmate/cli 0.24.1 → 0.24.2

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.
Files changed (2) hide show
  1. package/dist/index.js +615 -104
  2. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -10136,12 +10136,84 @@ var LoginCommandHandler = class {
10136
10136
  // src/services/AliyunService.ts
10137
10137
  var import_alidns20150109 = __toESM(require("@alicloud/alidns20150109"));
10138
10138
  var import_cdn20180510 = __toESM(require("@alicloud/cdn20180510"));
10139
+ var import_fc20230330 = __toESM(require("@alicloud/fc20230330"));
10139
10140
  var import_openapi_client = require("@alicloud/openapi-client");
10141
+ var import_sts20150401 = __toESM(require("@alicloud/sts20150401"));
10140
10142
  var import_ali_oss = __toESM(require("ali-oss"));
10141
10143
  var import_node_crypto = require("crypto");
10142
10144
  var import_node_fs3 = require("fs");
10143
10145
  var import_node_path4 = __toESM(require("path"));
10144
10146
  var AliyunService = class {
10147
+ static async getAccountId() {
10148
+ const client = this.createStsClient();
10149
+ const response = await client.getCallerIdentity();
10150
+ const accountId = response.body?.accountId?.trim();
10151
+ if (!accountId) {
10152
+ throw new Error("Aliyun STS GetCallerIdentity did not return AccountId.");
10153
+ }
10154
+ return accountId;
10155
+ }
10156
+ static async getFcFunction(functionName, options) {
10157
+ const client = await this.createFcClient(options.region);
10158
+ const response = await client.getFunction(
10159
+ functionName,
10160
+ new import_fc20230330.GetFunctionRequest({
10161
+ ...options.qualifier ? { qualifier: options.qualifier } : {}
10162
+ })
10163
+ );
10164
+ return {
10165
+ functionName: response.body?.functionName,
10166
+ runtime: response.body?.runtime,
10167
+ state: response.body?.state,
10168
+ lastModifiedTime: response.body?.lastModifiedTime
10169
+ };
10170
+ }
10171
+ static async ensureFcCustomDomain(input) {
10172
+ const client = await this.createFcClient(input.region);
10173
+ const route = this.buildFcPathConfig(input);
10174
+ try {
10175
+ const response = await client.getCustomDomain(input.domainName);
10176
+ const existing = response.body;
10177
+ const routes = existing?.routeConfig?.routes ?? [];
10178
+ const routeIndex = routes.findIndex((item) => item?.path === route.path);
10179
+ if (routeIndex >= 0) {
10180
+ const current = routes[routeIndex];
10181
+ if (current?.functionName === route.functionName && (current?.qualifier || "LATEST") === route.qualifier) {
10182
+ ui.success(`FC custom domain route already exists: ${input.domainName}${route.path}`);
10183
+ return;
10184
+ }
10185
+ routes[routeIndex] = route;
10186
+ } else {
10187
+ routes.push(route);
10188
+ }
10189
+ await client.updateCustomDomain(
10190
+ input.domainName,
10191
+ new import_fc20230330.UpdateCustomDomainRequest({
10192
+ body: new import_fc20230330.UpdateCustomDomainInput({
10193
+ protocol: input.protocol ?? existing?.protocol ?? "HTTP",
10194
+ routeConfig: new import_fc20230330.RouteConfig({ routes })
10195
+ })
10196
+ })
10197
+ );
10198
+ ui.success(`Updated FC custom domain route: ${input.domainName}${route.path}`);
10199
+ return;
10200
+ } catch (error) {
10201
+ if (!this.isNotFoundError(error)) {
10202
+ throw error;
10203
+ }
10204
+ }
10205
+ await client.createCustomDomain(
10206
+ new import_fc20230330.CreateCustomDomainRequest({
10207
+ body: new import_fc20230330.CreateCustomDomainInput({
10208
+ domainName: input.domainName,
10209
+ protocol: input.protocol ?? "HTTP",
10210
+ authConfig: new import_fc20230330.AuthConfig({ authType: "anonymous", authInfo: "{}" }),
10211
+ routeConfig: new import_fc20230330.RouteConfig({ routes: [route] })
10212
+ })
10213
+ })
10214
+ );
10215
+ ui.success(`Created FC custom domain: ${input.domainName}`);
10216
+ }
10145
10217
  /** Add a DNS record via Aliyun DNS. */
10146
10218
  static async addRecord(input) {
10147
10219
  const userConfig = loadPmateConfig();
@@ -10797,6 +10869,48 @@ var AliyunService = class {
10797
10869
  return void 0;
10798
10870
  }
10799
10871
  }
10872
+ static buildFcPathConfig(input) {
10873
+ return new import_fc20230330.PathConfig({
10874
+ path: input.routePath ?? "/*",
10875
+ functionName: input.functionName,
10876
+ qualifier: input.qualifier ?? "LATEST",
10877
+ methods: ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"]
10878
+ });
10879
+ }
10880
+ static createStsClient() {
10881
+ const cfg = loadPmateConfig();
10882
+ const accessKeyId = cfg.aliyun?.ak?.trim();
10883
+ const accessKeySecret = cfg.aliyun?.sk?.trim();
10884
+ if (!accessKeyId) throw new Error("Missing aliyun.ak in ~/.pmate/config.yaml.");
10885
+ if (!accessKeySecret) throw new Error("Missing aliyun.sk in ~/.pmate/config.yaml.");
10886
+ return new import_sts20150401.default(
10887
+ new import_openapi_client.Config({
10888
+ accessKeyId,
10889
+ accessKeySecret,
10890
+ regionId: cfg.aliyun?.region?.trim() || "cn-hangzhou"
10891
+ })
10892
+ );
10893
+ }
10894
+ static async createFcClient(region) {
10895
+ const cfg = loadPmateConfig();
10896
+ const accessKeyId = cfg.aliyun?.ak?.trim();
10897
+ const accessKeySecret = cfg.aliyun?.sk?.trim();
10898
+ if (!accessKeyId) throw new Error("Missing aliyun.ak in ~/.pmate/config.yaml.");
10899
+ if (!accessKeySecret) throw new Error("Missing aliyun.sk in ~/.pmate/config.yaml.");
10900
+ const accountId = await this.getAccountId();
10901
+ return new import_fc20230330.default(
10902
+ new import_openapi_client.Config({
10903
+ accessKeyId,
10904
+ accessKeySecret,
10905
+ regionId: region,
10906
+ endpoint: `${accountId}.${region}.fc.aliyuncs.com`
10907
+ })
10908
+ );
10909
+ }
10910
+ static isNotFoundError(error) {
10911
+ const maybe = error;
10912
+ return maybe?.statusCode === 404 || maybe?.code === "DomainNameNotFound" || maybe?.code === "ResourceNotFound" || /not found|not exist/i.test(maybe?.message || "");
10913
+ }
10800
10914
  };
10801
10915
 
10802
10916
  // src/commands/AddDnsCommandHandler.ts
@@ -15932,10 +16046,12 @@ var CollectCommandHandler = class {
15932
16046
  return best?.page ?? null;
15933
16047
  }
15934
16048
  async extractThreadsFromConversationData(page) {
15935
- const result = await this.evaluateInPage(page, buildListThreadsExpr()).catch((error) => ({
15936
- ok: false,
15937
- reason: error instanceof Error ? error.message : String(error)
15938
- }));
16049
+ const result = await this.evaluateInPage(page, buildListThreadsExpr()).catch(
16050
+ (error) => ({
16051
+ ok: false,
16052
+ reason: error instanceof Error ? error.message : String(error)
16053
+ })
16054
+ );
15939
16055
  if (!result || !("ok" in result) || !result.ok || !Array.isArray(result.threads)) {
15940
16056
  throw new Error(
15941
16057
  result && "reason" in result && typeof result.reason === "string" ? result.reason : "unknown error"
@@ -15967,7 +16083,12 @@ var CollectCommandHandler = class {
15967
16083
  const result = await this.evaluateInPage(
15968
16084
  page,
15969
16085
  buildGetMessagesByConversationExpr(thread.conversationId, limit)
15970
- ).catch((error) => ({ ok: false, reason: error instanceof Error ? error.message : String(error) }));
16086
+ ).catch(
16087
+ (error) => ({
16088
+ ok: false,
16089
+ reason: error instanceof Error ? error.message : String(error)
16090
+ })
16091
+ );
15971
16092
  if (!result || !("ok" in result) || !result.ok || !Array.isArray(result.messages)) {
15972
16093
  return {
15973
16094
  error: result && "reason" in result && typeof result.reason === "string" ? result.reason : "unknown error"
@@ -17945,6 +18066,159 @@ var import_node_path22 = __toESM(require("path"));
17945
18066
  var import_node_child_process10 = require("child_process");
17946
18067
  var import_node_readline = require("readline");
17947
18068
  var import_chalk3 = __toESM(require("chalk"));
18069
+
18070
+ // src/services/ServiceRegistryClient.ts
18071
+ init_sessionStore();
18072
+ var ServiceRegistryNotFoundError = class extends Error {
18073
+ constructor(namespace, name) {
18074
+ super(`Service not found: ${namespace}/${name}`);
18075
+ this.name = "ServiceRegistryNotFoundError";
18076
+ }
18077
+ };
18078
+ var ServiceRegistryClient = class {
18079
+ constructor(baseUrl) {
18080
+ this.baseUrl = baseUrl;
18081
+ }
18082
+ async listServices(filters) {
18083
+ const searchParams = new URLSearchParams();
18084
+ const repository = normalizeRepository(filters?.repository);
18085
+ if (repository) {
18086
+ searchParams.set("repo", repository);
18087
+ }
18088
+ const server = filters?.server?.trim();
18089
+ if (server) {
18090
+ searchParams.set("server", server);
18091
+ }
18092
+ const payload = await this.request(
18093
+ "GET",
18094
+ `/services${searchParams.size > 0 ? `?${searchParams.toString()}` : ""}`
18095
+ );
18096
+ const namespace = filters?.namespace?.trim();
18097
+ return payload.items.filter((item) => {
18098
+ if (namespace && item.namespace !== namespace) {
18099
+ return false;
18100
+ }
18101
+ return true;
18102
+ });
18103
+ }
18104
+ async getService(namespace, name) {
18105
+ return this.request(
18106
+ "GET",
18107
+ `/services/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`
18108
+ );
18109
+ }
18110
+ async updateService(namespace, name, payload) {
18111
+ return this.requestWithBody(
18112
+ "PUT",
18113
+ `/services/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`,
18114
+ payload
18115
+ );
18116
+ }
18117
+ async listEnvItems(appId, options = {}) {
18118
+ const params = new URLSearchParams();
18119
+ if (options.scope) {
18120
+ params.set("scope", options.scope);
18121
+ }
18122
+ if (options.includeSecretMeta) {
18123
+ params.set("includeSecretMeta", "true");
18124
+ }
18125
+ const payload = await this.request(
18126
+ "GET",
18127
+ `/apps/by-id/${encodeURIComponent(appId)}/env${params.size > 0 ? `?${params.toString()}` : ""}`
18128
+ );
18129
+ return payload.items;
18130
+ }
18131
+ async resolveEnv(appId, options) {
18132
+ return this.requestWithBody(
18133
+ "POST",
18134
+ `/apps/by-id/${encodeURIComponent(appId)}/env/resolve`,
18135
+ {
18136
+ stage: options.stage,
18137
+ ...options.branch ? { branch: options.branch } : {},
18138
+ ...options.includeSecretValue ? { includeSecretValue: true } : {}
18139
+ }
18140
+ );
18141
+ }
18142
+ resolveBaseUrl() {
18143
+ return (this.baseUrl || process.env.PMATE_APPS_API_BASE_URL || "https://apps-api.pmate.chat").replace(/\/+$/, "");
18144
+ }
18145
+ readToken() {
18146
+ return readRequiredSession({
18147
+ preferredApp: DEFAULT_APP_ID,
18148
+ fallbackToActiveApp: true
18149
+ }).token;
18150
+ }
18151
+ async request(method, pathname) {
18152
+ const response = await fetch(`${this.resolveBaseUrl()}${pathname}`, {
18153
+ method,
18154
+ headers: {
18155
+ Authorization: `Bearer ${this.readToken()}`,
18156
+ Accept: "application/json"
18157
+ }
18158
+ });
18159
+ let payload = null;
18160
+ try {
18161
+ payload = await response.json();
18162
+ } catch {
18163
+ payload = null;
18164
+ }
18165
+ if (response.status === 404) {
18166
+ const segments = pathname.split("/").filter(Boolean);
18167
+ throw new ServiceRegistryNotFoundError(
18168
+ decodeURIComponent(segments[1] || "unknown"),
18169
+ decodeURIComponent(segments[2] || "unknown")
18170
+ );
18171
+ }
18172
+ if (!response.ok || !payload?.success || payload.data === void 0) {
18173
+ throw new Error(payload?.message || `Request failed with status ${response.status}`);
18174
+ }
18175
+ return payload.data;
18176
+ }
18177
+ async requestWithBody(method, pathname, body) {
18178
+ const response = await fetch(`${this.resolveBaseUrl()}${pathname}`, {
18179
+ method,
18180
+ headers: {
18181
+ Authorization: `Bearer ${this.readToken()}`,
18182
+ Accept: "application/json",
18183
+ "Content-Type": "application/json"
18184
+ },
18185
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
18186
+ });
18187
+ let payload = null;
18188
+ try {
18189
+ payload = await response.json();
18190
+ } catch {
18191
+ payload = null;
18192
+ }
18193
+ if (response.status === 404) {
18194
+ const segments = pathname.split("/").filter(Boolean);
18195
+ throw new ServiceRegistryNotFoundError(
18196
+ decodeURIComponent(segments[1] || "unknown"),
18197
+ decodeURIComponent(segments[2] || "unknown")
18198
+ );
18199
+ }
18200
+ if (!response.ok || !payload?.success || payload.data === void 0) {
18201
+ throw new Error(payload?.message || `Request failed with status ${response.status}`);
18202
+ }
18203
+ return payload.data;
18204
+ }
18205
+ };
18206
+ function normalizeRepository(repository) {
18207
+ if (!repository) {
18208
+ return void 0;
18209
+ }
18210
+ const trimmed = repository.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
18211
+ if (!trimmed) {
18212
+ return void 0;
18213
+ }
18214
+ const match = trimmed.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+)$/i);
18215
+ if (match) {
18216
+ return match[1];
18217
+ }
18218
+ return trimmed.replace(/^\/+/, "");
18219
+ }
18220
+
18221
+ // src/services/ServiceRuntimeRunner.ts
17948
18222
  var ServiceRuntimeRunner = class {
17949
18223
  async deploy(service, options = {}) {
17950
18224
  switch (this.getType(service)) {
@@ -17961,6 +18235,9 @@ var ServiceRuntimeRunner = class {
17961
18235
  case "python":
17962
18236
  await this.deployScriptManaged(service);
17963
18237
  return;
18238
+ case "fc":
18239
+ await this.deployFc(service);
18240
+ return;
17964
18241
  case "ios":
17965
18242
  case "android":
17966
18243
  throw new Error(
@@ -17983,6 +18260,9 @@ var ServiceRuntimeRunner = class {
17983
18260
  case "python":
17984
18261
  await this.initServerApp(service, options, { ensurePackageJson: false });
17985
18262
  return;
18263
+ case "fc":
18264
+ await this.initFc(service, options);
18265
+ return;
17986
18266
  case "ios":
17987
18267
  case "android":
17988
18268
  throw new Error(
@@ -18084,13 +18364,21 @@ var ServiceRuntimeRunner = class {
18084
18364
  return service.network;
18085
18365
  }
18086
18366
  const deploy = this.isLegacy(service) ? service.deploy ?? {} : {};
18087
- const exposeType = deploy.port === void 0 ? "internal" : deploy.serviceType === "ws" ? "websocket" : "rest";
18367
+ if (deploy.port === void 0) {
18368
+ return {
18369
+ exposeType: "internal",
18370
+ repository: String(deploy.repository ?? ""),
18371
+ dir: String(deploy.dir ?? ""),
18372
+ server: String(deploy.server ?? "")
18373
+ };
18374
+ }
18375
+ const exposeType = deploy.serviceType === "ws" ? "websocket" : "rest";
18088
18376
  return {
18089
18377
  exposeType,
18090
18378
  repository: String(deploy.repository ?? ""),
18091
18379
  dir: String(deploy.dir ?? ""),
18092
18380
  server: String(deploy.server ?? ""),
18093
- ...deploy.port !== void 0 ? { port: Number(deploy.port) } : {},
18381
+ port: Number(deploy.port),
18094
18382
  ...deploy.domain ? { domain: deploy.domain } : {},
18095
18383
  ...deploy.subdomain ? { subdomain: deploy.subdomain } : {}
18096
18384
  };
@@ -18104,17 +18392,28 @@ var ServiceRuntimeRunner = class {
18104
18392
  script: String(deploy.script ?? "")
18105
18393
  };
18106
18394
  }
18395
+ getFc(service) {
18396
+ if (!this.isLegacy(service) && service.fc) {
18397
+ return service.fc;
18398
+ }
18399
+ throw new Error(`Missing fc config for ${service.id}`);
18400
+ }
18107
18401
  resolveRepoRoot(service, cwd) {
18108
18402
  if (cwd) {
18109
18403
  return import_node_path22.default.resolve(process.cwd(), cwd);
18110
18404
  }
18111
- const repository = this.getType(service) === "vite" || this.getType(service) === "static" ? this.getSite(service).repository?.trim() : this.getType(service) === "ios" || this.getType(service) === "android" ? void 0 : this.getNetwork(service).repository?.trim();
18405
+ const repository = this.getType(service) === "vite" || this.getType(service) === "static" ? this.getSite(service).repository?.trim() : this.getType(service) === "fc" ? this.getFc(service).repository?.trim() : this.getType(service) === "ios" || this.getType(service) === "android" ? void 0 : this.getNetwork(service).repository?.trim();
18112
18406
  if (repository) {
18113
18407
  const repoName = repository.replace(/\/+$/, "").split("/").pop()?.replace(/\.git$/i, "");
18114
18408
  if (repoName) {
18115
- const candidate = import_node_path22.default.join(import_node_os8.default.homedir(), "dev", "pmate", repoName);
18116
- if ((0, import_node_fs17.existsSync)(candidate)) {
18117
- return candidate;
18409
+ const candidates = [
18410
+ import_node_path22.default.join(import_node_os8.default.homedir(), "dev", "pmate", repoName),
18411
+ import_node_path22.default.join(import_node_os8.default.homedir(), "dev", "pmate-envs", repoName)
18412
+ ];
18413
+ for (const candidate of candidates) {
18414
+ if ((0, import_node_fs17.existsSync)(candidate)) {
18415
+ return candidate;
18416
+ }
18118
18417
  }
18119
18418
  }
18120
18419
  }
@@ -18122,7 +18421,7 @@ var ServiceRuntimeRunner = class {
18122
18421
  }
18123
18422
  resolveLocalDir(service, cwd) {
18124
18423
  const repoRoot = this.resolveRepoRoot(service, cwd);
18125
- const dir = this.getType(service) === "vite" || this.getType(service) === "static" ? this.getSite(service).dir?.trim() : this.getType(service) === "ios" || this.getType(service) === "android" ? void 0 : this.getNetwork(service).dir?.trim();
18424
+ const dir = this.getType(service) === "vite" || this.getType(service) === "static" ? this.getSite(service).dir?.trim() : this.getType(service) === "fc" ? this.getFc(service).dir?.trim() : this.getType(service) === "ios" || this.getType(service) === "android" ? void 0 : this.getNetwork(service).dir?.trim();
18126
18425
  if (!dir) {
18127
18426
  throw new Error(`Missing service dir for ${service.id}`);
18128
18427
  }
@@ -18164,6 +18463,120 @@ var ServiceRuntimeRunner = class {
18164
18463
  }
18165
18464
  await AliyunService.uploadDirectory(srcDir, bucketName);
18166
18465
  }
18466
+ async deployFc(service) {
18467
+ const fc = this.getFc(service);
18468
+ this.validateFcConfig(service, fc);
18469
+ const repoRoot = this.resolveRepoRoot(service);
18470
+ const appDir = this.resolveLocalDir(service);
18471
+ const packageJsonPath = import_node_path22.default.join(appDir, "package.json");
18472
+ if (!(0, import_node_fs17.existsSync)(packageJsonPath)) {
18473
+ throw new Error(`No package.json found at ${packageJsonPath}`);
18474
+ }
18475
+ const pkg2 = this.loadPackageJson(packageJsonPath);
18476
+ const pkgName = pkg2.name?.trim();
18477
+ if (!pkgName) {
18478
+ throw new Error(`Missing package name in ${packageJsonPath}`);
18479
+ }
18480
+ await this.ensurePnpmWorkspaceRoot(repoRoot);
18481
+ await this.runCommand("pnpm", ["--filter", pkgName, "build"], repoRoot);
18482
+ const registry = new ServiceRegistryClient();
18483
+ const appId = `${service.namespace}:${service.name}`;
18484
+ const stage = fc.deployStage ?? "prod";
18485
+ const requiredEnv = await this.syncFcRequiredEnv(service, registry, stage);
18486
+ const resolved = await registry.resolveEnv(appId, {
18487
+ stage,
18488
+ includeSecretValue: true
18489
+ });
18490
+ const missing = requiredEnv.filter((key) => !this.hasResolvedEnvValue(resolved.values, key));
18491
+ if (missing.length > 0) {
18492
+ throw new Error(`Missing resolved env for ${service.id}: ${missing.join(", ")}`);
18493
+ }
18494
+ const deployEnv = {
18495
+ ...resolved.values,
18496
+ ...await this.resolveAliyunDeploymentEnv(),
18497
+ ...this.resolveFcNoProxyEnv()
18498
+ };
18499
+ const deployCommand = fc.deployCommand?.trim();
18500
+ if (deployCommand) {
18501
+ await this.runShellCommand(deployCommand, repoRoot, deployEnv);
18502
+ } else if (pkg2.scripts?.release) {
18503
+ await this.runCommandWithEnv("pnpm", ["--filter", pkgName, "release"], repoRoot, deployEnv);
18504
+ } else {
18505
+ await this.runCommandWithEnv("pnpm", ["release"], appDir, deployEnv);
18506
+ }
18507
+ const functionInfo = await AliyunService.getFcFunction(fc.functionName, {
18508
+ region: fc.region,
18509
+ qualifier: fc.qualifier
18510
+ });
18511
+ ui.success(`Deployed FC function ${fc.functionName}`, {
18512
+ region: fc.region,
18513
+ runtime: functionInfo.runtime,
18514
+ state: functionInfo.state,
18515
+ lastModifiedTime: functionInfo.lastModifiedTime,
18516
+ url: this.buildFcUrl(fc)
18517
+ });
18518
+ }
18519
+ async initFc(service, options) {
18520
+ const fc = this.getFc(service);
18521
+ this.validateFcConfig(service, fc);
18522
+ const repoRoot = this.resolveRepoRoot(service, options.cwd);
18523
+ const appDir = this.resolveLocalDir(service, options.cwd);
18524
+ await this.ensurePnpmWorkspaceRoot(repoRoot);
18525
+ const configPath = import_node_path22.default.join(appDir, fc.configFile ?? "s.yaml");
18526
+ if (!(0, import_node_fs17.existsSync)(configPath)) {
18527
+ throw new Error(`Missing FC config file at ${configPath}`);
18528
+ }
18529
+ const packageJsonPath = import_node_path22.default.join(appDir, "package.json");
18530
+ if (!(0, import_node_fs17.existsSync)(packageJsonPath)) {
18531
+ throw new Error(`No package.json found at ${packageJsonPath}`);
18532
+ }
18533
+ const rawConfig = (0, import_node_fs17.readFileSync)(configPath, "utf8");
18534
+ if (!rawConfig.includes(fc.functionName)) {
18535
+ throw new Error(`FC config ${configPath} does not mention functionName ${fc.functionName}`);
18536
+ }
18537
+ const accountId = await AliyunService.getAccountId();
18538
+ await AliyunService.getFcFunction(fc.functionName, {
18539
+ region: fc.region,
18540
+ qualifier: fc.qualifier
18541
+ });
18542
+ const host = this.buildFcHost(fc);
18543
+ if (host) {
18544
+ const cnameTarget = `${accountId}.${fc.region}.fc.aliyuncs.com`;
18545
+ const rr = fc.subdomain?.trim() || "@";
18546
+ const domain = fc.domain.trim();
18547
+ const existing = await AliyunService.getCnameRecordValue(domain, rr);
18548
+ if (existing) {
18549
+ if (this.normalizeDnsValue(existing) !== this.normalizeDnsValue(cnameTarget)) {
18550
+ throw new Error(
18551
+ `Existing CNAME for ${host} points to ${existing}, not ${cnameTarget}.`
18552
+ );
18553
+ }
18554
+ ui.success(`CNAME already exists: ${host} -> ${cnameTarget}`);
18555
+ } else {
18556
+ await AliyunService.addRecord({
18557
+ domainName: domain,
18558
+ rr,
18559
+ type: "CNAME",
18560
+ value: cnameTarget,
18561
+ ttl: 600
18562
+ });
18563
+ ui.success(`Added CNAME ${host} -> ${cnameTarget}`);
18564
+ }
18565
+ await AliyunService.ensureFcCustomDomain({
18566
+ domainName: host,
18567
+ region: fc.region,
18568
+ functionName: fc.functionName,
18569
+ routePath: fc.routePath,
18570
+ protocol: fc.protocol,
18571
+ qualifier: fc.qualifier
18572
+ });
18573
+ }
18574
+ ui.success(`Initialized FC service ${service.id}`, {
18575
+ functionName: fc.functionName,
18576
+ region: fc.region,
18577
+ url: this.buildFcUrl(fc)
18578
+ });
18579
+ }
18167
18580
  async deployNode(service) {
18168
18581
  const network = this.getNetwork(service);
18169
18582
  const server = network.server?.trim();
@@ -18828,9 +19241,154 @@ var ServiceRuntimeRunner = class {
18828
19241
  (0, import_node_fs17.writeFileSync)(packageJsonPath, `${serialized}
18829
19242
  `, "utf8");
18830
19243
  }
19244
+ validateFcConfig(service, fc) {
19245
+ if (fc.provider !== "aliyun") {
19246
+ throw new Error(`Unsupported fc.provider for ${service.id}: ${fc.provider}`);
19247
+ }
19248
+ if (!fc.region?.trim()) {
19249
+ throw new Error(`Missing fc.region for ${service.id}`);
19250
+ }
19251
+ if (!fc.repository?.trim()) {
19252
+ throw new Error(`Missing fc.repository for ${service.id}`);
19253
+ }
19254
+ if (!fc.dir?.trim()) {
19255
+ throw new Error(`Missing fc.dir for ${service.id}`);
19256
+ }
19257
+ if (!fc.functionName?.trim()) {
19258
+ throw new Error(`Missing fc.functionName for ${service.id}`);
19259
+ }
19260
+ if (fc.domain && !fc.domain.trim()) {
19261
+ throw new Error(`Invalid fc.domain for ${service.id}`);
19262
+ }
19263
+ if (fc.subdomain && !fc.domain?.trim()) {
19264
+ throw new Error(`Missing fc.domain for ${service.id} (required when fc.subdomain is set)`);
19265
+ }
19266
+ }
19267
+ async ensurePnpmWorkspaceRoot(repoRoot) {
19268
+ const workspacePath = import_node_path22.default.join(repoRoot, "pnpm-workspace.yaml");
19269
+ if (!(0, import_node_fs17.existsSync)(workspacePath)) {
19270
+ throw new Error(`Missing pnpm-workspace.yaml at ${workspacePath}`);
19271
+ }
19272
+ }
19273
+ async syncFcRequiredEnv(service, registry, stage) {
19274
+ if (this.isLegacy(service)) {
19275
+ return [];
19276
+ }
19277
+ const appId = `${service.namespace}:${service.name}`;
19278
+ const items = await registry.listEnvItems(appId, {
19279
+ scope: stage,
19280
+ includeSecretMeta: true
19281
+ });
19282
+ const requiredEnv = Array.from(
19283
+ new Set(
19284
+ items.map((item) => item.key ?? item.name).filter((key) => typeof key === "string" && key.trim().length > 0).map((key) => key.trim())
19285
+ )
19286
+ ).sort();
19287
+ const current = service.fc?.requiredEnv ?? [];
19288
+ if (this.sameStringSet(current, requiredEnv)) {
19289
+ return requiredEnv;
19290
+ }
19291
+ service.fc = {
19292
+ ...service.fc,
19293
+ requiredEnv
19294
+ };
19295
+ await registry.updateService(service.namespace, service.name, service);
19296
+ ui.success(`Synced fc.requiredEnv for ${service.id}`, requiredEnv);
19297
+ return requiredEnv;
19298
+ }
19299
+ sameStringSet(left, right) {
19300
+ if (left.length !== right.length) {
19301
+ return false;
19302
+ }
19303
+ const normalizedLeft = [...left].sort();
19304
+ const normalizedRight = [...right].sort();
19305
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
19306
+ }
19307
+ hasResolvedEnvValue(values, key) {
19308
+ return Object.prototype.hasOwnProperty.call(values, key) && values[key] !== void 0 && values[key] !== "";
19309
+ }
19310
+ buildFcHost(fc) {
19311
+ const domain = fc.domain?.trim();
19312
+ if (!domain) {
19313
+ return void 0;
19314
+ }
19315
+ const subdomain = fc.subdomain?.trim();
19316
+ return subdomain ? `${subdomain}.${domain}` : domain;
19317
+ }
19318
+ buildFcUrl(fc) {
19319
+ const host = this.buildFcHost(fc);
19320
+ return host ? `https://${host}` : void 0;
19321
+ }
19322
+ normalizeDnsValue(value) {
19323
+ return value.trim().replace(/\.$/, "").toLowerCase();
19324
+ }
19325
+ async resolveAliyunDeploymentEnv() {
19326
+ const config = loadPmateConfig();
19327
+ const accessKeyId = config.aliyun?.ak?.trim();
19328
+ const accessKeySecret = config.aliyun?.sk?.trim();
19329
+ if (!accessKeyId) {
19330
+ throw new Error("Missing aliyun.ak in ~/.pmate/config.yaml.");
19331
+ }
19332
+ if (!accessKeySecret) {
19333
+ throw new Error("Missing aliyun.sk in ~/.pmate/config.yaml.");
19334
+ }
19335
+ return {
19336
+ AccessKeyID: accessKeyId,
19337
+ AccessKeySecret: accessKeySecret,
19338
+ AccountID: await AliyunService.getAccountId()
19339
+ };
19340
+ }
19341
+ resolveFcNoProxyEnv() {
19342
+ const noProxy = /* @__PURE__ */ new Set(
19343
+ [
19344
+ "localhost",
19345
+ "127.0.0.1",
19346
+ "api.devsapp.cn",
19347
+ "devsapp.cn",
19348
+ "aliyuncs.com"
19349
+ ]
19350
+ );
19351
+ for (const value of [process.env.NO_PROXY, process.env.no_proxy]) {
19352
+ for (const item of value?.split(",") ?? []) {
19353
+ const trimmed = item.trim();
19354
+ if (trimmed) {
19355
+ noProxy.add(trimmed);
19356
+ }
19357
+ }
19358
+ }
19359
+ return {
19360
+ HTTP_PROXY: "",
19361
+ HTTPS_PROXY: "",
19362
+ ALL_PROXY: "",
19363
+ http_proxy: "",
19364
+ https_proxy: "",
19365
+ all_proxy: "",
19366
+ NO_PROXY: Array.from(noProxy).join(","),
19367
+ no_proxy: Array.from(noProxy).join(",")
19368
+ };
19369
+ }
18831
19370
  runCommand(command, args, cwd) {
19371
+ return this.runCommandWithEnv(command, args, cwd);
19372
+ }
19373
+ runCommandWithEnv(command, args, cwd, env) {
18832
19374
  return new Promise((resolve, reject) => {
18833
- const child = (0, import_node_child_process10.spawn)(command, args, { cwd, stdio: "inherit" });
19375
+ const child = (0, import_node_child_process10.spawn)(command, args, {
19376
+ cwd,
19377
+ stdio: "inherit",
19378
+ env: { ...process.env, ...env ?? {} }
19379
+ });
19380
+ child.on("error", reject);
19381
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited with code ${code}`)));
19382
+ });
19383
+ }
19384
+ runShellCommand(command, cwd, env) {
19385
+ return new Promise((resolve, reject) => {
19386
+ const child = (0, import_node_child_process10.spawn)(command, [], {
19387
+ cwd,
19388
+ stdio: "inherit",
19389
+ shell: true,
19390
+ env: { ...process.env, ...env ?? {} }
19391
+ });
18834
19392
  child.on("error", reject);
18835
19393
  child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited with code ${code}`)));
18836
19394
  });
@@ -18877,97 +19435,6 @@ var ServiceRuntimeRunner = class {
18877
19435
  }
18878
19436
  };
18879
19437
 
18880
- // src/services/ServiceRegistryClient.ts
18881
- init_sessionStore();
18882
- var ServiceRegistryNotFoundError = class extends Error {
18883
- constructor(namespace, name) {
18884
- super(`Service not found: ${namespace}/${name}`);
18885
- this.name = "ServiceRegistryNotFoundError";
18886
- }
18887
- };
18888
- var ServiceRegistryClient = class {
18889
- constructor(baseUrl) {
18890
- this.baseUrl = baseUrl;
18891
- }
18892
- async listServices(filters) {
18893
- const searchParams = new URLSearchParams();
18894
- const repository = normalizeRepository(filters?.repository);
18895
- if (repository) {
18896
- searchParams.set("repo", repository);
18897
- }
18898
- const server = filters?.server?.trim();
18899
- if (server) {
18900
- searchParams.set("server", server);
18901
- }
18902
- const payload = await this.request(
18903
- "GET",
18904
- `/services${searchParams.size > 0 ? `?${searchParams.toString()}` : ""}`
18905
- );
18906
- const namespace = filters?.namespace?.trim();
18907
- return payload.items.filter((item) => {
18908
- if (namespace && item.namespace !== namespace) {
18909
- return false;
18910
- }
18911
- return true;
18912
- });
18913
- }
18914
- async getService(namespace, name) {
18915
- return this.request(
18916
- "GET",
18917
- `/services/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`
18918
- );
18919
- }
18920
- resolveBaseUrl() {
18921
- return (this.baseUrl || process.env.PMATE_APPS_API_BASE_URL || "https://apps-api.pmate.chat").replace(/\/+$/, "");
18922
- }
18923
- readToken() {
18924
- return readRequiredSession({
18925
- preferredApp: DEFAULT_APP_ID,
18926
- fallbackToActiveApp: true
18927
- }).token;
18928
- }
18929
- async request(method, pathname) {
18930
- const response = await fetch(`${this.resolveBaseUrl()}${pathname}`, {
18931
- method,
18932
- headers: {
18933
- Authorization: `Bearer ${this.readToken()}`,
18934
- Accept: "application/json"
18935
- }
18936
- });
18937
- let payload = null;
18938
- try {
18939
- payload = await response.json();
18940
- } catch {
18941
- payload = null;
18942
- }
18943
- if (response.status === 404) {
18944
- const segments = pathname.split("/").filter(Boolean);
18945
- throw new ServiceRegistryNotFoundError(
18946
- decodeURIComponent(segments[1] || "unknown"),
18947
- decodeURIComponent(segments[2] || "unknown")
18948
- );
18949
- }
18950
- if (!response.ok || !payload?.success || payload.data === void 0) {
18951
- throw new Error(payload?.message || `Request failed with status ${response.status}`);
18952
- }
18953
- return payload.data;
18954
- }
18955
- };
18956
- function normalizeRepository(repository) {
18957
- if (!repository) {
18958
- return void 0;
18959
- }
18960
- const trimmed = repository.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
18961
- if (!trimmed) {
18962
- return void 0;
18963
- }
18964
- const match = trimmed.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+)$/i);
18965
- if (match) {
18966
- return match[1];
18967
- }
18968
- return trimmed.replace(/^\/+/, "");
18969
- }
18970
-
18971
19438
  // src/commands/ServiceCommandHandler.ts
18972
19439
  init_namespacedRef();
18973
19440
  var ServiceCommandHandler = class {
@@ -19122,6 +19589,7 @@ var ServiceCommandHandler = class {
19122
19589
  }
19123
19590
  );
19124
19591
  ui.success(`Updated env ${args.key} for ${args.appId}`);
19592
+ await this.syncFcRequiredEnvFromServiceEnv(registry, args.appId, args.scope);
19125
19593
  this.renderJson(payload, args.json);
19126
19594
  return;
19127
19595
  }
@@ -19134,6 +19602,7 @@ var ServiceCommandHandler = class {
19134
19602
  }).toString()}`
19135
19603
  );
19136
19604
  ui.success(`Deleted env ${payload.key} (${payload.scope}) from ${args.appId}`);
19605
+ await this.syncFcRequiredEnvFromServiceEnv(registry, args.appId, args.scope);
19137
19606
  this.renderJson(payload, args.json);
19138
19607
  return;
19139
19608
  }
@@ -19319,6 +19788,48 @@ var ServiceCommandHandler = class {
19319
19788
  );
19320
19789
  }
19321
19790
  }
19791
+ async syncFcRequiredEnvFromServiceEnv(registry, appId, scope) {
19792
+ const [namespace, name, extra] = appId.split(":");
19793
+ if (!namespace || !name || extra !== void 0) {
19794
+ return;
19795
+ }
19796
+ let service;
19797
+ try {
19798
+ service = await registry.getService(namespace, name);
19799
+ } catch (error) {
19800
+ if (error instanceof ServiceRegistryNotFoundError) {
19801
+ return;
19802
+ }
19803
+ throw error;
19804
+ }
19805
+ if (!("type" in service) || service.type !== "fc" || !service.fc) {
19806
+ return;
19807
+ }
19808
+ const items = await registry.listEnvItems(appId, {
19809
+ scope,
19810
+ includeSecretMeta: true
19811
+ });
19812
+ const requiredEnv = Array.from(
19813
+ new Set(
19814
+ items.map((item) => item.key ?? item.name).filter((key) => typeof key === "string" && key.trim().length > 0).map((key) => key.trim())
19815
+ )
19816
+ ).sort();
19817
+ const current = service.fc.requiredEnv ?? [];
19818
+ if (this.sameStringSet(current, requiredEnv)) {
19819
+ return;
19820
+ }
19821
+ service.fc = { ...service.fc, requiredEnv };
19822
+ await registry.updateService(namespace, name, service);
19823
+ ui.success(`Synced fc.requiredEnv for ${appId}`);
19824
+ }
19825
+ sameStringSet(left, right) {
19826
+ if (left.length !== right.length) {
19827
+ return false;
19828
+ }
19829
+ const normalizedLeft = [...left].sort();
19830
+ const normalizedRight = [...right].sort();
19831
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
19832
+ }
19322
19833
  renderList(items, asJson) {
19323
19834
  if (asJson) {
19324
19835
  ui.plain(JSON.stringify(items, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -36,7 +36,9 @@
36
36
  "dependencies": {
37
37
  "@alicloud/alidns20150109": "^4.0.0",
38
38
  "@alicloud/cdn20180510": "^8.0.0",
39
+ "@alicloud/fc20230330": "^4.7.4",
39
40
  "@alicloud/openapi-client": "^0.4.15",
41
+ "@alicloud/sts20150401": "^1.2.0",
40
42
  "@linear/sdk": "^70.0.0",
41
43
  "@types/ali-oss": "^6.16.13",
42
44
  "ajv": "^8.17.1",