@co0ontty/wand 1.26.0 → 1.29.1

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/cli.js CHANGED
@@ -78,14 +78,20 @@ async function main() {
78
78
  break;
79
79
  }
80
80
  case "config:show": {
81
- // 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。
81
+ // 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。password 脱敏:
82
+ // 显示是否已自定义("<set>" / "change-me"),避免误把真密码截图分享出去;
83
+ // 想看真值就直接读 DB(sqlite3 wand.db "SELECT * FROM app_config WHERE key='password'")。
82
84
  const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
83
85
  const dbPath = resolveDatabasePath(configPath);
84
86
  ensureDatabaseFile(dbPath);
85
87
  const storage = new WandStorage(dbPath);
86
88
  try {
87
89
  const config = await loadConfigWithStorage(configPath, storage);
88
- process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
90
+ const display = {
91
+ ...config,
92
+ password: config.password === "change-me" ? "change-me" : "<set>",
93
+ };
94
+ process.stdout.write(`${JSON.stringify(display, null, 2)}\n`);
89
95
  }
90
96
  finally {
91
97
  storage.close();
@@ -109,6 +115,16 @@ async function main() {
109
115
  writePreferenceToStorage(config, storage, key, value);
110
116
  process.stdout.write(`[wand] Updated preference ${key} in ${dbPath}\n`);
111
117
  }
118
+ else if (key === "password") {
119
+ // password 走 SQLite,和 Web UI 设置面板保持同一个源。
120
+ // 历史上 setConfigValue("password") 只写 config.json,但登录用 dbPassword ?? config.password,
121
+ // 一旦 DB 里有值,写 JSON 完全不生效,命令静默返回成功 → 用户以为改了密码其实没改。
122
+ if (typeof value !== "string" || value.length < 6) {
123
+ throw new Error("password 长度至少为 6 个字符");
124
+ }
125
+ storage.setPassword(value);
126
+ process.stdout.write(`[wand] Updated password in ${dbPath}\n`);
127
+ }
112
128
  else {
113
129
  const nextConfig = setConfigValue(config, key, value);
114
130
  await saveConfig(configPath, nextConfig);
@@ -296,10 +312,10 @@ async function runAttach(live, configPath, useTui) {
296
312
  process.on("SIGTERM", onSignal);
297
313
  }
298
314
  function setConfigValue(config, key, value) {
299
- // 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,这里只处理 JSON 字段。
315
+ // 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,password 也走 DB
316
+ // (由 case "config:set" 直接处理),这里只剩纯 JSON 字段。
300
317
  switch (key) {
301
318
  case "host":
302
- case "password":
303
319
  case "shell":
304
320
  return {
305
321
  ...config,
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
  const DEFAULT_CONFIG_DIR = ".wand";
@@ -41,6 +41,7 @@ export const defaultConfig = () => ({
41
41
  shortcutLogMaxBytes: 10 * 1024 * 1024,
42
42
  language: "",
43
43
  android: defaultAndroidApkConfig(),
44
+ macos: defaultMacosDmgConfig(),
44
45
  cardDefaults: defaultCardExpandDefaults(),
45
46
  defaultModel: "",
46
47
  structuredRunner: "cli",
@@ -85,6 +86,27 @@ export function resolveConfigDir(configPath) {
85
86
  export function hasConfigFile(configPath) {
86
87
  return existsSync(configPath);
87
88
  }
89
+ /**
90
+ * 原子写入:先写 `<dir>/.<file>.tmp-<rand>`,再 rename 覆盖目标。
91
+ * 防止 kill -9 / 断电 / 磁盘满导致 config.json 半截损坏 → 下次启动 catch 路径
92
+ * 把 defaults 写回去 → appSecret 重生成 → 已分发的 APK appToken 全部作废。
93
+ */
94
+ async function atomicWriteFile(filePath, content) {
95
+ const dir = path.dirname(filePath);
96
+ const base = path.basename(filePath);
97
+ const tmpPath = path.join(dir, `.${base}.tmp-${crypto.randomBytes(6).toString("hex")}`);
98
+ await writeFile(tmpPath, content, "utf8");
99
+ try {
100
+ await rename(tmpPath, filePath);
101
+ }
102
+ catch (err) {
103
+ try {
104
+ await unlink(tmpPath);
105
+ }
106
+ catch { /* noop */ }
107
+ throw err;
108
+ }
109
+ }
88
110
  export async function ensureConfig(configPath) {
89
111
  const dir = path.dirname(configPath);
90
112
  await mkdir(dir, { recursive: true });
@@ -94,20 +116,20 @@ export async function ensureConfig(configPath) {
94
116
  const normalized = `${JSON.stringify(merged, null, 2)}\n`;
95
117
  // Only write if the file content actually changed
96
118
  if (raw.trimEnd() !== normalized.trimEnd()) {
97
- await writeFile(configPath, normalized, "utf8");
119
+ await atomicWriteFile(configPath, normalized);
98
120
  }
99
121
  return merged;
100
122
  }
101
123
  catch {
102
124
  const config = defaultConfig();
103
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
125
+ await atomicWriteFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
104
126
  return config;
105
127
  }
106
128
  }
107
129
  /** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
108
130
  export async function saveConfig(configPath, config) {
109
131
  await mkdir(path.dirname(configPath), { recursive: true });
110
- await writeFile(configPath, `${JSON.stringify(stripPreferenceFields(config), null, 2)}\n`, "utf8");
132
+ await atomicWriteFile(configPath, `${JSON.stringify(stripPreferenceFields(config), null, 2)}\n`);
111
133
  }
112
134
  /**
113
135
  * 启动期合并 JSON + DB 偏好。语义:
@@ -128,11 +150,21 @@ export async function loadConfigWithStorage(configPath, storage) {
128
150
  hadFile = true;
129
151
  }
130
152
  catch {
153
+ // 文件缺失 或 JSON 损坏:保留空 rawInput;reconcileAppSecret 会优先用 DB 里那份,
154
+ // 避免已分发 APK 被踢下线。
131
155
  rawInput = {};
132
156
  }
133
157
  migrateLegacyPreferencesToDb(rawInput, storage);
158
+ migrateLegacyPasswordToDb(rawInput, storage);
134
159
  const config = mergeWithDefaults(rawInput);
135
160
  applyStoragePreferences(config, storage);
161
+ // appSecret: DB 是权威源。
162
+ // - DB 有 → 直接覆盖 runtime config(即使 mergeWithDefaults 临时生成了新的,也以 DB 为准)
163
+ // - DB 无、config 有 → 老用户首次升级,把 config.json 里的 appSecret 落到 DB
164
+ // - 都没 → 用 config 当前那个(mergeWithDefaults 已经生成),落到 DB
165
+ reconcileAppSecret(config, storage);
166
+ // password: DB 优先映射到 runtime config,让 config:show 与 server.ts 都看到真值
167
+ applyStoragePassword(config, storage);
136
168
  // 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
137
169
  const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
138
170
  if (!hadFile || hasLegacyPrefs) {
@@ -140,6 +172,46 @@ export async function loadConfigWithStorage(configPath, storage) {
140
172
  }
141
173
  return config;
142
174
  }
175
+ /**
176
+ * 把 DB 里的 appSecret 同步到 runtime config,缺失时反向回填,保证 DB 永远有备份。
177
+ * 这条路径修掉了之前的 bug:mergeWithDefaults 在缺失 appSecret 时会随机生成一个新的,
178
+ * 一旦 config.json 因为任何原因丢字段(损坏/手动编辑/catch fallback),所有 APK appToken 立刻作废。
179
+ */
180
+ function reconcileAppSecret(config, storage) {
181
+ const dbSecret = storage.getAppSecret();
182
+ if (dbSecret && dbSecret.length >= 32) {
183
+ config.appSecret = dbSecret;
184
+ return;
185
+ }
186
+ if (config.appSecret && config.appSecret.length >= 32) {
187
+ storage.setAppSecret(config.appSecret);
188
+ return;
189
+ }
190
+ const fresh = crypto.randomBytes(32).toString("hex");
191
+ config.appSecret = fresh;
192
+ storage.setAppSecret(fresh);
193
+ }
194
+ /** 把 DB 里设置过的 password 映射到 runtime config 字段,让 config:show 不再展示 "change-me" 假象。 */
195
+ function applyStoragePassword(config, storage) {
196
+ const dbPassword = storage.getPassword();
197
+ if (dbPassword !== null) {
198
+ config.password = dbPassword;
199
+ }
200
+ }
201
+ /**
202
+ * 老用户如果曾经手动编辑 config.json 设过非默认密码,但从没用 Web UI 改过密码
203
+ * (DB 里没有 password 行),那个 JSON 字段就是当前生效密码。升级到 DB 权威源之前
204
+ * 必须把它搬到 DB,否则后续如果 saveConfig 路径剥离 password 字段或 JSON 被截断,
205
+ * 用户会被锁在门外。DB 已经有 password 行的情况下不覆盖。
206
+ */
207
+ function migrateLegacyPasswordToDb(rawInput, storage) {
208
+ if (storage.hasCustomPassword())
209
+ return;
210
+ const legacy = rawInput.password;
211
+ if (typeof legacy === "string" && legacy.length >= 6 && legacy !== "change-me") {
212
+ storage.setPassword(legacy);
213
+ }
214
+ }
143
215
  /** Build a JSON-safe view of WandConfig that excludes preference fields (which live in DB). */
144
216
  function stripPreferenceFields(config) {
145
217
  const out = { ...config };
@@ -301,6 +373,28 @@ function normalizeAndroidApkConfig(input) {
301
373
  : defaults.currentApkFile,
302
374
  };
303
375
  }
376
+ function defaultMacosDmgConfig() {
377
+ return {
378
+ enabled: false,
379
+ dmgDir: "macos",
380
+ currentDmgFile: "",
381
+ };
382
+ }
383
+ function normalizeMacosDmgConfig(input) {
384
+ if (!input || typeof input !== "object")
385
+ return undefined;
386
+ const defaults = defaultMacosDmgConfig();
387
+ const macosInput = input;
388
+ return {
389
+ enabled: typeof macosInput.enabled === "boolean" ? macosInput.enabled : defaults.enabled,
390
+ dmgDir: typeof macosInput.dmgDir === "string" && macosInput.dmgDir.trim()
391
+ ? macosInput.dmgDir.trim()
392
+ : defaults.dmgDir,
393
+ currentDmgFile: typeof macosInput.currentDmgFile === "string"
394
+ ? macosInput.currentDmgFile.trim()
395
+ : defaults.currentDmgFile,
396
+ };
397
+ }
304
398
  function normalizeStructuredChatPersona(input) {
305
399
  if (!input || typeof input !== "object")
306
400
  return undefined;
@@ -358,6 +452,7 @@ function mergeWithDefaults(input) {
358
452
  ? input.appSecret
359
453
  : crypto.randomBytes(32).toString("hex"),
360
454
  android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
455
+ macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
361
456
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
362
457
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
363
458
  structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
@@ -1,4 +1,5 @@
1
1
  import { GitStatusResult, PushResult, QuickCommitResult, TagHeadResult } from "./types.js";
2
+ export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
2
3
  export declare function getGitStatus(cwd: string): GitStatusResult;
3
4
  interface QuickCommitOptions {
4
5
  cwd: string;
@@ -11,8 +12,8 @@ interface QuickCommitOptions {
11
12
  push?: boolean;
12
13
  }
13
14
  export declare class QuickCommitError extends Error {
14
- readonly code: string;
15
- constructor(message: string, code: string);
15
+ readonly code: QuickCommitErrorCode;
16
+ constructor(message: string, code: QuickCommitErrorCode);
16
17
  }
17
18
  export interface GenerateCommitMessageResult {
18
19
  message: string;