@co0ontty/wand 4.5.0 → 4.6.1-beta.g27502fa

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "4c9bbce65ef02fdc9b8b5d2891e8839f0f2a74a2",
3
- "builtAt": "2026-07-15T13:06:11.510Z",
4
- "version": "4.5.0",
5
- "channel": "stable"
2
+ "commit": "27502faec54a3b247afebfe8c6c0be798082bd2a",
3
+ "builtAt": "2026-07-16T03:23:16.959Z",
4
+ "version": "4.6.1-beta.g27502fa",
5
+ "channel": "beta"
6
6
  }
@@ -29,6 +29,11 @@ export interface DistributionManagerOptions {
29
29
  fetch?: typeof fetch;
30
30
  now?: () => number;
31
31
  }
32
+ /**
33
+ * GitHub Release 正文还包含 Android/macOS/iOS 的安装指引;它们属于发布页,
34
+ * 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
35
+ */
36
+ export declare function extractUpdateSummary(releaseBody: string): string;
32
37
  export declare class DistributionManager {
33
38
  private readonly options;
34
39
  private readonly fetchImpl;
@@ -2,6 +2,14 @@ import { mkdir, readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
4
4
  const GITHUB_CACHE_TTL_MS = 10 * 60 * 1000;
5
+ /**
6
+ * GitHub Release 正文还包含 Android/macOS/iOS 的安装指引;它们属于发布页,
7
+ * 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
8
+ */
9
+ export function extractUpdateSummary(releaseBody) {
10
+ const summary = releaseBody.split(/\r?\n---\s*(?:\r?\n|$)/, 1)[0]?.trim() ?? "";
11
+ return summary.slice(0, 500);
12
+ }
5
13
  function asRecord(value) {
6
14
  return value && typeof value === "object" ? value : null;
7
15
  }
@@ -215,7 +223,7 @@ export class DistributionManager {
215
223
  downloadUrl: hit.asset.browser_download_url,
216
224
  fileName: hit.asset.name,
217
225
  size: hit.asset.size,
218
- ...(extension === ".apk" && hit.body ? { releaseNotes: hit.body.trim().slice(0, 500) } : {}),
226
+ ...(extension === ".apk" && hit.body ? { releaseNotes: extractUpdateSummary(hit.body) } : {}),
219
227
  };
220
228
  this.githubCache.set(extension, { asset, timestamp: this.now() });
221
229
  return asset;
@@ -81,6 +81,58 @@ async function resolvePushRemoteAsync(cwd) {
81
81
  catch { /* use origin */ }
82
82
  return "origin";
83
83
  }
84
+ function githubSshPushUrl(remoteUrl) {
85
+ try {
86
+ const parsed = new URL(remoteUrl);
87
+ if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "github.com")
88
+ return undefined;
89
+ const path = decodeURIComponent(parsed.pathname).replace(/^\/+|\/+$/g, "");
90
+ const parts = path.split("/");
91
+ if (parts.length !== 2 || parts.some((part) => !part))
92
+ return undefined;
93
+ return `git@github.com:${parts[0]}/${parts[1]}`;
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ function isHttpsCredentialError(error) {
100
+ return /could not read (?:User|Pass)name|authentication failed|terminal prompts disabled|credential[^\n]*(?:failed|missing)|http basic: access denied/i
101
+ .test(getGitErrorMessage(error));
102
+ }
103
+ async function resolveRemotePushUrl(cwd, remote) {
104
+ try {
105
+ return await runGitAsync(["remote", "get-url", "--push", remote], cwd);
106
+ }
107
+ catch {
108
+ return remote;
109
+ }
110
+ }
111
+ /**
112
+ * Push explicit refs through one transport seam. A Wand service deliberately disables interactive
113
+ * credential prompts, so a GitHub HTTPS remote that has no non-interactive credential would
114
+ * otherwise fail even when the same machine is already configured for GitHub SSH.
115
+ */
116
+ async function pushRemoteRefs(cwd, remote, options = [], refs = []) {
117
+ try {
118
+ await runGitAsync(["push", ...options, remote, ...refs], cwd, GIT_PUSH_TIMEOUT_MS);
119
+ return;
120
+ }
121
+ catch (error) {
122
+ if (!isHttpsCredentialError(error))
123
+ throw error;
124
+ const configuredUrl = await resolveRemotePushUrl(cwd, remote);
125
+ const sshUrl = githubSshPushUrl(configuredUrl);
126
+ if (!sshUrl)
127
+ throw error;
128
+ try {
129
+ await runGitAsync(["push", ...options, sshUrl, ...refs], cwd, GIT_PUSH_TIMEOUT_MS);
130
+ }
131
+ catch (sshError) {
132
+ throw new Error(`${getGitErrorMessage(error)}\nGitHub SSH 回退也失败:${getGitErrorMessage(sshError)}`, { cause: sshError });
133
+ }
134
+ }
135
+ }
84
136
  function unquotePath(raw) {
85
137
  if (raw.startsWith("\"") && raw.endsWith("\"")) {
86
138
  return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
@@ -751,21 +803,21 @@ async function doPush(opts) {
751
803
  try {
752
804
  if (pushCommits) {
753
805
  if (hasUpstream) {
754
- await runGitAsync(["push", recurseFlag], cwd, GIT_PUSH_TIMEOUT_MS);
806
+ await pushRemoteRefs(cwd, pushRemote, [recurseFlag]);
755
807
  }
756
808
  else {
757
- await runGitAsync(["push", "-u", recurseFlag, pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
809
+ await pushRemoteRefs(cwd, pushRemote, ["-u", recurseFlag], ["HEAD"]);
758
810
  }
759
811
  pushedCommits = true;
760
812
  }
761
813
  if (pushTags) {
762
814
  if (Array.isArray(pushTags)) {
763
815
  for (const name of pushTags) {
764
- await runGitAsync(["push", pushRemote, `refs/tags/${name}`], cwd, GIT_PUSH_TIMEOUT_MS);
816
+ await pushRemoteRefs(cwd, pushRemote, [], [`refs/tags/${name}`]);
765
817
  }
766
818
  }
767
819
  else {
768
- await runGitAsync(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
820
+ await pushRemoteRefs(cwd, pushRemote, ["--tags"]);
769
821
  }
770
822
  pushedTags = true;
771
823
  }
@@ -1012,7 +1064,7 @@ async function pushSubmodules(parentCwd, subInfos, opts) {
1012
1064
  continue;
1013
1065
  }
1014
1066
  try {
1015
- await runGitAsync(["push", info.remote, `HEAD:refs/heads/${info.branch}`], subCwd, GIT_PUSH_TIMEOUT_MS);
1067
+ await pushRemoteRefs(subCwd, info.remote, [], [`HEAD:refs/heads/${info.branch}`]);
1016
1068
  }
1017
1069
  catch (error) {
1018
1070
  errors.push(`submodule ${info.path} 推送失败:${getGitErrorMessage(error)}`);
@@ -1020,7 +1072,7 @@ async function pushSubmodules(parentCwd, subInfos, opts) {
1020
1072
  }
1021
1073
  if (opts.pushTags && opts.tagName) {
1022
1074
  try {
1023
- await runGitAsync(["push", info.remote, `refs/tags/${opts.tagName}`], subCwd, GIT_PUSH_TIMEOUT_MS);
1075
+ await pushRemoteRefs(subCwd, info.remote, [], [`refs/tags/${opts.tagName}`]);
1024
1076
  }
1025
1077
  catch (error) {
1026
1078
  errors.push(`submodule ${info.path} 推送 tag 失败:${getGitErrorMessage(error)}`);
@@ -1312,6 +1364,13 @@ export async function runQuickCommit(opts) {
1312
1364
  if (!tagName && autoTag) {
1313
1365
  tagName = await generateTagAfterCommit(cwd, language, message, ai);
1314
1366
  }
1367
+ let submodulePushInfos = submoduleOutcome.commits;
1368
+ if (submodule) {
1369
+ // The selected scope is the declared submodule set, not merely the submodules dirtied by this
1370
+ // request. A clean submodule can still have an unpushed HEAD or a pre-existing pointer change.
1371
+ const collected = await collectSubmodulesForPush(cwd);
1372
+ submodulePushInfos = collected.infos;
1373
+ }
1315
1374
  if (tagName) {
1316
1375
  try {
1317
1376
  await runGitAsync(["tag", tagName], cwd);
@@ -1319,9 +1378,10 @@ export async function runQuickCommit(opts) {
1319
1378
  catch (error) {
1320
1379
  throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
1321
1380
  }
1322
- // 纳入 submodule 时给刚提交的 submodule 打同名 tag(非致命,失败不阻断父仓库流程)。
1323
- if (submodule && submoduleOutcome.commits.length > 0) {
1324
- await tagSubmodules(cwd, submoduleOutcome.commits, tagName);
1381
+ // 纳入 submodule 时给全部声明的 submodule 当前 HEAD 打同名 tag。这样纯指针变化或
1382
+ // 已提前提交但尚未推送的 submodule 也不会漏掉发布 tag。
1383
+ if (submodule && submodulePushInfos.length > 0) {
1384
+ await tagSubmodules(cwd, submodulePushInfos, tagName);
1325
1385
  }
1326
1386
  }
1327
1387
  let pushed = false;
@@ -1330,10 +1390,10 @@ export async function runQuickCommit(opts) {
1330
1390
  // 纳入 submodule:先把各 submodule 的 HEAD(+ 同名 tag)分别推到各自远端分支,
1331
1391
  // 解决 detached HEAD 无法被父仓库 on-demand 递归推送的问题;父仓库随后用
1332
1392
  // recurse=no 单独推(submodule 已就绪)。否则父仓库用 recurse=check 做安全校验。
1333
- const includeSub = !!submodule && submoduleOutcome.commits.length > 0;
1393
+ const includeSub = !!submodule && submodulePushInfos.length > 0;
1334
1394
  let subPushErrors = [];
1335
1395
  if (includeSub) {
1336
- const subPush = await pushSubmodules(cwd, submoduleOutcome.commits, { pushTags: !!tagName, tagName });
1396
+ const subPush = await pushSubmodules(cwd, submodulePushInfos, { pushTags: !!tagName, tagName });
1337
1397
  subPushErrors = subPush.errors;
1338
1398
  }
1339
1399
  if (subPushErrors.length > 0) {