droid-patch 0.12.2 → 0.13.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/README.md CHANGED
@@ -67,13 +67,46 @@ npx droid-patch --skip-login -o /path/to/dir my-droid
67
67
  | `--standalone` | Standalone mode: mock non-LLM Factory APIs (use with `--websearch` or `--websearch-proxy`) |
68
68
  | `--reasoning-effort` | Enable reasoning effort UI selector for custom models (set to high) |
69
69
  | `--disable-telemetry` | Disable telemetry and Sentry error reporting |
70
- | `--spec-model-custom` | Enable custom models as spec model (show in UI selector + bypass validation) |
71
70
  | `--dry-run` | Verify patches without actually modifying the binary |
72
71
  | `-p, --path <path>` | Path to the droid binary (default: `~/.droid/bin/droid`) |
73
72
  | `-o, --output <dir>` | Output directory for patched binary (creates file without alias) |
74
73
  | `--no-backup` | Skip creating backup of original binary |
75
74
  | `-v, --verbose` | Enable verbose output |
76
75
 
76
+ ### Manage Custom Models
77
+
78
+ ```bash
79
+ # List all custom models
80
+ npx droid-patch list-models
81
+
82
+ # Add model interactively
83
+ npx droid-patch add-model
84
+
85
+ # Add model via command line
86
+ npx droid-patch add-model \
87
+ -m "claude-sonnet-4-20250514" \
88
+ -n "Sonnet [proxy]" \
89
+ -u "http://127.0.0.1:20002/droid" \
90
+ -k "your-api-key" \
91
+ -p "anthropic"
92
+
93
+ # Insert model at specific position
94
+ npx droid-patch add-model -i 0 # Interactive, insert at position 0
95
+
96
+ # Remove model (supports index, ID, or displayName)
97
+ npx droid-patch remove-model 0 # By index
98
+ npx droid-patch remove-model "custom:Sonnet-[proxy]-1" # By ID
99
+ npx droid-patch remove-model "Sonnet [proxy]" # By display name
100
+ ```
101
+
102
+ **Model ID Format**: `custom:{DisplayName}-{index}`
103
+
104
+ - Spaces in `DisplayName` are replaced with `-`
105
+ - `index` is the position in the array (starting from 0)
106
+ - Example: `displayName: "Opus [proxy]"` → `id: "custom:Opus-[proxy]-0"`
107
+
108
+ **Important**: When deleting or inserting models, subsequent model IDs are automatically updated (because index changes).
109
+
77
110
  ### Manage Aliases and Files
78
111
 
79
112
  ```bash
@@ -285,19 +318,25 @@ Configure your custom model in `~/.factory/settings.json`:
285
318
  "customModels": [
286
319
  {
287
320
  "model": "claude-sonnet-4-20250514",
288
- "id": "custom:Claude-Proxy-0",
321
+ "id": "custom:Opus-[proxy]-0",
289
322
  "baseUrl": "http://127.0.0.1:20002/droid",
290
323
  "apiKey": "your-api-key",
291
- "displayName": "Claude [proxy]",
324
+ "displayName": "Opus [proxy]",
292
325
  "provider": "anthropic"
293
326
  }
294
327
  ],
295
328
  "sessionDefaultSettings": {
296
- "model": "custom:Claude-Proxy-0"
329
+ "model": "custom:Opus-[proxy]-0"
297
330
  }
298
331
  }
299
332
  ```
300
333
 
334
+ **Important**: The `id` field must match the `displayName` pattern:
335
+
336
+ - Format: `custom:{DisplayName}-{index}` where spaces are replaced with `-`
337
+ - Example: `displayName: "Opus [proxy]"` → `id: "custom:Opus-[proxy]-0"`
338
+ - The trailing number (`-0`) is the index (starting from 0)
339
+
301
340
  ### `--reasoning-effort`
302
341
 
303
342
  Enables reasoning effort control for custom models by patching the binary to:
@@ -395,29 +434,6 @@ npx droid-patch --disable-telemetry droid-private
395
434
  npx droid-patch --is-custom --skip-login --disable-telemetry droid-private
396
435
  ```
397
436
 
398
- ### `--spec-model-custom`
399
-
400
- Enables custom models to be used as the spec model in spec mode.
401
-
402
- **Purpose**: By default, droid only allows official models (like Claude, GPT) as spec models. This patch allows custom models to appear in the spec model selector and bypasses the compatibility validation.
403
-
404
- **How it works**:
405
-
406
- - Shows all custom models in the spec model selector UI (bypasses the filter that hides them)
407
- - Bypasses the `compatibilityGroup` validation check (uses regex matching to handle minified function name changes across versions)
408
-
409
- **Compatibility**: Requires droid 0.45.0 or later (the compatibilityGroup feature was added in this version).
410
-
411
- **Usage**:
412
-
413
- ```bash
414
- # Enable custom models as spec model
415
- npx droid-patch --spec-model-custom droid-spec
416
-
417
- # Combine with other patches for full custom model support
418
- npx droid-patch --is-custom --reasoning-effort --spec-model-custom droid-custom-full
419
- ```
420
-
421
437
  ---
422
438
 
423
439
  ## WebSearch Configuration Guide
package/README.zh-CN.md CHANGED
@@ -67,13 +67,46 @@ npx droid-patch --skip-login -o /path/to/dir my-droid
67
67
  | `--standalone` | 独立模式:mock 非 LLM 的 Factory API(与 `--websearch` 或 `--websearch-proxy` 配合使用) |
68
68
  | `--reasoning-effort` | 为自定义模型启用推理强度 UI 选择器(设置为 high) |
69
69
  | `--disable-telemetry` | 禁用遥测数据上传和 Sentry 错误报告 |
70
- | `--spec-model-custom` | 允许自定义模型作为 spec 模型(在 UI 选择器中显示 + 绕过验证) |
71
70
  | `--dry-run` | 验证修补但不实际修改二进制文件 |
72
71
  | `-p, --path <path>` | droid 二进制文件路径(默认:`~/.droid/bin/droid`) |
73
72
  | `-o, --output <dir>` | 修补后二进制文件的输出目录(直接创建文件,不创建别名) |
74
73
  | `--no-backup` | 跳过创建原始二进制文件的备份 |
75
74
  | `-v, --verbose` | 启用详细输出 |
76
75
 
76
+ ### 管理自定义模型
77
+
78
+ ```bash
79
+ # 列出所有自定义模型
80
+ npx droid-patch list-models
81
+
82
+ # 交互式添加模型
83
+ npx droid-patch add-model
84
+
85
+ # 命令行添加模型
86
+ npx droid-patch add-model \
87
+ -m "claude-sonnet-4-20250514" \
88
+ -n "Sonnet [proxy]" \
89
+ -u "http://127.0.0.1:20002/droid" \
90
+ -k "your-api-key" \
91
+ -p "anthropic"
92
+
93
+ # 在指定位置插入模型
94
+ npx droid-patch add-model -i 0 # 交互式,插入到位置 0
95
+
96
+ # 删除模型(支持 index、ID 或 displayName)
97
+ npx droid-patch remove-model 0 # 按索引
98
+ npx droid-patch remove-model "custom:Sonnet-[proxy]-1" # 按 ID
99
+ npx droid-patch remove-model "Sonnet [proxy]" # 按显示名称
100
+ ```
101
+
102
+ **模型 ID 格式**:`custom:{DisplayName}-{index}`
103
+
104
+ - `DisplayName` 中的空格会被替换为 `-`
105
+ - `index` 是模型在数组中的位置(从 0 开始)
106
+ - 示例:`displayName: "Opus [proxy]"` → `id: "custom:Opus-[proxy]-0"`
107
+
108
+ **重要**:删除或插入模型时,后续模型的 ID 会自动更新(因为 index 会变化)。
109
+
77
110
  ### 管理别名和文件
78
111
 
79
112
  ```bash
@@ -395,29 +428,6 @@ npx droid-patch --disable-telemetry droid-private
395
428
  npx droid-patch --is-custom --skip-login --disable-telemetry droid-private
396
429
  ```
397
430
 
398
- ### `--spec-model-custom`
399
-
400
- 允许自定义模型作为 spec 模式中的 spec 模型使用。
401
-
402
- **用途**:默认情况下,droid 只允许官方模型(如 Claude、GPT)作为 spec 模型。此补丁使自定义模型出现在 spec 模型选择器中,并绕过兼容性验证。
403
-
404
- **工作原理**:
405
-
406
- - 在 spec 模型选择器 UI 中显示所有自定义模型(绕过隐藏它们的过滤器)
407
- - 绕过 `compatibilityGroup` 验证检查(使用正则表达式匹配以处理不同版本间混淆后的函数名变化)
408
-
409
- **兼容性**:需要 droid 0.45.0 或更高版本(compatibilityGroup 功能在此版本中添加)。
410
-
411
- **使用方法**:
412
-
413
- ```bash
414
- # 允许自定义模型作为 spec 模型
415
- npx droid-patch --spec-model-custom droid-spec
416
-
417
- # 与其他补丁组合以获得完整的自定义模型支持
418
- npx droid-patch --is-custom --reasoning-effort --spec-model-custom droid-custom-full
419
- ```
420
-
421
431
  ---
422
432
 
423
433
  ## WebSearch 配置指南
@@ -1,11 +1,12 @@
1
1
  import { styleText } from "node:util";
2
2
  import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
4
- import { homedir } from "node:os";
3
+ import { basename, delimiter, dirname, join } from "node:path";
4
+ import { homedir, platform } from "node:os";
5
5
  import { execSync } from "node:child_process";
6
6
  import { chmod, copyFile, mkdir, readFile, readdir, readlink, stat, symlink, unlink, writeFile } from "node:fs/promises";
7
7
 
8
8
  //#region src/patcher.ts
9
+ const IS_WINDOWS$1 = platform() === "win32";
9
10
  async function patchDroid(options) {
10
11
  const { inputPath, outputPath, patches, dryRun = false, backup = true, verbose = false } = options;
11
12
  const finalOutputPath = outputPath || `${inputPath}.patched`;
@@ -152,13 +153,24 @@ async function patchDroid(options) {
152
153
  console.log(styleText("white", "[*] Applying patches..."));
153
154
  const totalPatched = results.reduce((sum, r) => sum + (r.positions?.length || 0), 0);
154
155
  console.log(styleText("green", `[*] Applied ${totalPatched} patches`));
155
- await writeFile(finalOutputPath, workingBuffer);
156
- console.log(styleText("white", `[*] Patched binary saved: ${styleText("cyan", finalOutputPath)}`));
157
- await chmod(finalOutputPath, 493);
156
+ let actualOutputPath = finalOutputPath;
157
+ try {
158
+ await writeFile(finalOutputPath, workingBuffer);
159
+ } catch (error) {
160
+ if (error.code === "EBUSY" && IS_WINDOWS$1) {
161
+ const timestamp = Date.now();
162
+ const ext = finalOutputPath.endsWith(".exe") ? ".exe" : "";
163
+ actualOutputPath = `${finalOutputPath.replace(/\.exe$/, "").replace(/\.patched$/, "").replace(/-\d+$/, "")}-${timestamp}${ext ? ext : ".patched"}`;
164
+ console.log(styleText("yellow", `[!] Original file locked, saving to: ${actualOutputPath}`));
165
+ await writeFile(actualOutputPath, workingBuffer);
166
+ } else throw error;
167
+ }
168
+ console.log(styleText("white", `[*] Patched binary saved: ${styleText("cyan", actualOutputPath)}`));
169
+ await chmod(actualOutputPath, 493);
158
170
  console.log(styleText("gray", "[*] Set executable permission"));
159
171
  console.log();
160
172
  console.log(styleText("white", "[*] Verifying patches..."));
161
- const verifyBuffer = await readFile(finalOutputPath);
173
+ const verifyBuffer = await readFile(actualOutputPath);
162
174
  let allVerified = true;
163
175
  for (const patch of patches) {
164
176
  if (patch.regexPattern && patch.regexReplacement) {
@@ -206,7 +218,7 @@ async function patchDroid(options) {
206
218
  }
207
219
  return {
208
220
  success: allVerified,
209
- outputPath: finalOutputPath,
221
+ outputPath: actualOutputPath,
210
222
  results,
211
223
  patchedCount: totalPatched
212
224
  };
@@ -338,17 +350,16 @@ function formatPatches(patches) {
338
350
  if (patches.reasoningEffort) applied.push("reasoningEffort");
339
351
  if (patches.noTelemetry) applied.push("noTelemetry");
340
352
  if (patches.standalone) applied.push("standalone");
341
- if (patches.autoHigh) applied.push("autoHigh");
342
- if (patches.specModelCustom) applied.push("specModelCustom");
343
353
  return applied.length > 0 ? applied.join(", ") : "(none)";
344
354
  }
345
355
 
346
356
  //#endregion
347
357
  //#region src/alias.ts
358
+ const IS_WINDOWS = platform() === "win32";
348
359
  const DROID_PATCH_DIR = join(homedir(), ".droid-patch");
349
360
  const ALIASES_DIR = join(DROID_PATCH_DIR, "aliases");
350
361
  const BINS_DIR = join(DROID_PATCH_DIR, "bins");
351
- const COMMON_PATH_DIRS = [
362
+ const UNIX_PATH_DIRS = [
352
363
  join(homedir(), ".local/bin"),
353
364
  join(homedir(), "bin"),
354
365
  join(homedir(), ".bin"),
@@ -369,17 +380,23 @@ const COMMON_PATH_DIRS = [
369
380
  join(homedir(), ".volta/bin"),
370
381
  join(homedir(), ".fnm/current/bin")
371
382
  ];
383
+ const WINDOWS_PATH_DIRS = [
384
+ join(homedir(), ".droid-patch", "bin"),
385
+ join(homedir(), "scoop", "shims"),
386
+ join(homedir(), "AppData", "Local", "Programs", "bin")
387
+ ];
388
+ const COMMON_PATH_DIRS = IS_WINDOWS ? WINDOWS_PATH_DIRS : UNIX_PATH_DIRS;
372
389
  function ensureDirectories() {
373
390
  if (!existsSync(DROID_PATCH_DIR)) mkdirSync(DROID_PATCH_DIR, { recursive: true });
374
391
  if (!existsSync(ALIASES_DIR)) mkdirSync(ALIASES_DIR, { recursive: true });
375
392
  if (!existsSync(BINS_DIR)) mkdirSync(BINS_DIR, { recursive: true });
376
393
  }
377
394
  function checkPathInclusion() {
378
- return (process.env.PATH || "").split(":").includes(ALIASES_DIR);
395
+ return (process.env.PATH || "").split(delimiter).some((p) => p.toLowerCase() === ALIASES_DIR.toLowerCase());
379
396
  }
380
397
  function findWritablePathDir() {
381
- const pathDirs = (process.env.PATH || "").split(":");
382
- for (const dir of COMMON_PATH_DIRS) if (pathDirs.includes(dir)) try {
398
+ const pathDirs = (process.env.PATH || "").split(delimiter);
399
+ for (const dir of COMMON_PATH_DIRS) if (pathDirs.some((p) => p.toLowerCase() === dir.toLowerCase())) try {
383
400
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
384
401
  const testFile = join(dir, `.droid-patch-test-${Date.now()}`);
385
402
  writeFileSync(testFile, "");
@@ -391,6 +408,7 @@ function findWritablePathDir() {
391
408
  return null;
392
409
  }
393
410
  function getShellConfigPath() {
411
+ if (IS_WINDOWS) return "";
394
412
  switch (basename(process.env.SHELL || "/bin/bash")) {
395
413
  case "zsh": return join(homedir(), ".zshrc");
396
414
  case "bash": {
@@ -403,6 +421,7 @@ function getShellConfigPath() {
403
421
  }
404
422
  }
405
423
  function isPathConfigured(shellConfigPath) {
424
+ if (IS_WINDOWS || !shellConfigPath) return false;
406
425
  if (!existsSync(shellConfigPath)) return false;
407
426
  try {
408
427
  const content = readFileSync(shellConfigPath, "utf-8");
@@ -411,6 +430,37 @@ function isPathConfigured(shellConfigPath) {
411
430
  return false;
412
431
  }
413
432
  }
433
+ /**
434
+ * Add directory to Windows user PATH using setx command
435
+ * This modifies the user's PATH permanently (requires terminal restart)
436
+ */
437
+ function addToWindowsUserPath(dir) {
438
+ try {
439
+ let existingPath = "";
440
+ try {
441
+ const match = execSync("reg query \"HKCU\\Environment\" /v Path 2>nul", {
442
+ encoding: "utf-8",
443
+ stdio: [
444
+ "pipe",
445
+ "pipe",
446
+ "pipe"
447
+ ]
448
+ }).match(/Path\s+REG_(?:EXPAND_)?SZ\s+(.+)/);
449
+ existingPath = match ? match[1].trim() : "";
450
+ } catch {}
451
+ if (existingPath.split(";").map((p) => p.toLowerCase().trim()).includes(dir.toLowerCase())) return true;
452
+ execSync(`setx PATH "${existingPath ? `${existingPath};${dir}` : dir}"`, { stdio: "pipe" });
453
+ return true;
454
+ } catch {
455
+ return false;
456
+ }
457
+ }
458
+ /**
459
+ * Generate Windows .cmd launcher script
460
+ */
461
+ function generateWindowsLauncher(targetPath) {
462
+ return `@echo off\r\n"${targetPath}" %*\r\n`;
463
+ }
414
464
  function addPathToShellConfig(shellConfigPath, verbose = false) {
415
465
  const shellName = basename(process.env.SHELL || "/bin/bash");
416
466
  let exportLine;
@@ -428,6 +478,7 @@ function addPathToShellConfig(shellConfigPath, verbose = false) {
428
478
  async function createAlias(patchedBinaryPath, aliasName, verbose = false) {
429
479
  ensureDirectories();
430
480
  console.log(styleText("white", `[*] Creating alias: ${styleText("cyan", aliasName)}`));
481
+ if (IS_WINDOWS) return createWindowsAlias(patchedBinaryPath, aliasName, verbose);
431
482
  const writablePathDir = findWritablePathDir();
432
483
  if (writablePathDir) {
433
484
  const targetPath = join(writablePathDir, aliasName);
@@ -536,10 +587,103 @@ async function createAlias(patchedBinaryPath, aliasName, verbose = false) {
536
587
  binaryPath: binaryDest
537
588
  };
538
589
  }
590
+ /**
591
+ * Create alias on Windows using .cmd launcher and setx for PATH
592
+ */
593
+ /**
594
+ * Try to copy file, handling Windows file locking
595
+ * If target is locked, use a new filename with timestamp
596
+ */
597
+ async function copyFileWithLockHandling(src, dest, verbose = false) {
598
+ try {
599
+ await copyFile(src, dest);
600
+ return dest;
601
+ } catch (error) {
602
+ if (error.code === "EBUSY" && IS_WINDOWS) {
603
+ const timestamp = Date.now();
604
+ const ext = dest.endsWith(".exe") ? ".exe" : "";
605
+ const newDest = `${dest.replace(/\.exe$/, "").replace(/-\d+$/, "")}-${timestamp}${ext}`;
606
+ if (verbose) console.log(styleText("yellow", ` [!] File locked, using new path: ${newDest}`));
607
+ await copyFile(src, newDest);
608
+ return newDest;
609
+ }
610
+ throw error;
611
+ }
612
+ }
613
+ async function createWindowsAlias(patchedBinaryPath, aliasName, verbose = false) {
614
+ const binDir = join(DROID_PATCH_DIR, "bin");
615
+ if (!existsSync(binDir)) mkdirSync(binDir, { recursive: true });
616
+ const binaryDest = await copyFileWithLockHandling(patchedBinaryPath, join(BINS_DIR, `${aliasName}-patched.exe`), verbose);
617
+ if (verbose) console.log(styleText("gray", ` Stored binary: ${binaryDest}`));
618
+ const cmdPath = join(binDir, `${aliasName}.cmd`);
619
+ writeFileSync(cmdPath, generateWindowsLauncher(binaryDest));
620
+ if (verbose) console.log(styleText("gray", ` Created launcher: ${cmdPath}`));
621
+ const pathAdded = addToWindowsUserPath(binDir);
622
+ console.log(styleText("green", `[*] Created: ${cmdPath}`));
623
+ console.log();
624
+ if (pathAdded) {
625
+ if (checkPathInclusion()) {
626
+ console.log(styleText("green", "─".repeat(60)));
627
+ console.log(styleText(["green", "bold"], " ALIAS READY!"));
628
+ console.log(styleText("green", "─".repeat(60)));
629
+ console.log();
630
+ console.log(styleText("white", `The alias "${styleText(["cyan", "bold"], aliasName)}" is now available.`));
631
+ console.log(styleText("gray", `(Installed to: ${binDir})`));
632
+ return {
633
+ aliasPath: cmdPath,
634
+ binaryPath: binaryDest,
635
+ immediate: true
636
+ };
637
+ }
638
+ console.log(styleText("yellow", "─".repeat(60)));
639
+ console.log(styleText(["yellow", "bold"], " PATH Updated - Restart Terminal"));
640
+ console.log(styleText("yellow", "─".repeat(60)));
641
+ console.log();
642
+ console.log(styleText("white", "PATH has been updated. Please restart your terminal."));
643
+ console.log(styleText("white", `Then you can use "${styleText(["cyan", "bold"], aliasName)}" command directly.`));
644
+ console.log();
645
+ console.log(styleText("gray", `Installed to: ${binDir}`));
646
+ } else {
647
+ console.log(styleText("yellow", "─".repeat(60)));
648
+ console.log(styleText(["yellow", "bold"], " Manual PATH Configuration Required"));
649
+ console.log(styleText("yellow", "─".repeat(60)));
650
+ console.log();
651
+ console.log(styleText("white", "Add this directory to your PATH:"));
652
+ console.log(styleText("cyan", ` ${binDir}`));
653
+ console.log();
654
+ console.log(styleText("gray", "Or run directly:"));
655
+ console.log(styleText("cyan", ` "${cmdPath}"`));
656
+ }
657
+ return {
658
+ aliasPath: cmdPath,
659
+ binaryPath: binaryDest,
660
+ immediate: false
661
+ };
662
+ }
539
663
  async function removeAlias(aliasName) {
540
664
  console.log(styleText("white", `[*] Removing alias: ${styleText("cyan", aliasName)}`));
541
665
  let removed = false;
542
- for (const pathDir of COMMON_PATH_DIRS) {
666
+ if (IS_WINDOWS) {
667
+ const cmdPath = join(join(DROID_PATCH_DIR, "bin"), `${aliasName}.cmd`);
668
+ if (existsSync(cmdPath)) {
669
+ await unlink(cmdPath);
670
+ console.log(styleText("green", ` Removed: ${cmdPath}`));
671
+ removed = true;
672
+ }
673
+ const exePath = join(BINS_DIR, `${aliasName}-patched.exe`);
674
+ if (existsSync(exePath)) {
675
+ await unlink(exePath);
676
+ console.log(styleText("green", ` Removed binary: ${exePath}`));
677
+ removed = true;
678
+ }
679
+ const proxyWrapperCmd = join(join(DROID_PATCH_DIR, "proxy"), `${aliasName}.cmd`);
680
+ if (existsSync(proxyWrapperCmd)) {
681
+ await unlink(proxyWrapperCmd);
682
+ console.log(styleText("green", ` Removed wrapper: ${proxyWrapperCmd}`));
683
+ removed = true;
684
+ }
685
+ }
686
+ if (!IS_WINDOWS) for (const pathDir of COMMON_PATH_DIRS) {
543
687
  const pathSymlink = join(pathDir, aliasName);
544
688
  if (existsSync(pathSymlink)) try {
545
689
  if (lstatSync(pathSymlink).isSymbolicLink()) {
@@ -629,43 +773,87 @@ async function listAliases() {
629
773
  console.log(styleText("cyan", "═".repeat(60)));
630
774
  console.log();
631
775
  const aliases = [];
632
- for (const pathDir of COMMON_PATH_DIRS) {
633
- if (!existsSync(pathDir)) continue;
776
+ if (IS_WINDOWS) {
777
+ const binDir = join(DROID_PATCH_DIR, "bin");
778
+ if (existsSync(binDir)) try {
779
+ const files = readdirSync(binDir);
780
+ for (const file of files) if (file.endsWith(".cmd")) {
781
+ const aliasName = file.replace(/\.cmd$/, "");
782
+ const fullPath = join(binDir, file);
783
+ try {
784
+ const match = readFileSync(fullPath, "utf-8").match(/"([^"]+)"/);
785
+ const target = match ? match[1] : fullPath;
786
+ aliases.push({
787
+ name: aliasName,
788
+ target,
789
+ location: binDir,
790
+ immediate: checkPathInclusion()
791
+ });
792
+ } catch {
793
+ aliases.push({
794
+ name: aliasName,
795
+ target: fullPath,
796
+ location: binDir,
797
+ immediate: false
798
+ });
799
+ }
800
+ }
801
+ } catch {}
802
+ const proxyDir = join(DROID_PATCH_DIR, "proxy");
803
+ if (existsSync(proxyDir)) try {
804
+ const files = readdirSync(proxyDir);
805
+ for (const file of files) if (file.endsWith(".cmd")) {
806
+ const aliasName = file.replace(/\.cmd$/, "");
807
+ if (!aliases.find((a) => a.name === aliasName)) {
808
+ const fullPath = join(proxyDir, file);
809
+ aliases.push({
810
+ name: aliasName,
811
+ target: fullPath,
812
+ location: proxyDir,
813
+ immediate: false
814
+ });
815
+ }
816
+ }
817
+ } catch {}
818
+ } else {
819
+ for (const pathDir of COMMON_PATH_DIRS) {
820
+ if (!existsSync(pathDir)) continue;
821
+ try {
822
+ const files = readdirSync(pathDir);
823
+ for (const file of files) {
824
+ const fullPath = join(pathDir, file);
825
+ try {
826
+ if (lstatSync(fullPath).isSymbolicLink()) {
827
+ const target = await readlink(fullPath);
828
+ if (target.includes(".droid-patch/bins") || target.includes(".droid-patch/websearch") || target.includes(".droid-patch/proxy") || target.includes(".droid-patch/statusline")) aliases.push({
829
+ name: file,
830
+ target,
831
+ location: pathDir,
832
+ immediate: true
833
+ });
834
+ }
835
+ } catch {}
836
+ }
837
+ } catch {}
838
+ }
634
839
  try {
635
- const files = readdirSync(pathDir);
840
+ const files = readdirSync(ALIASES_DIR);
636
841
  for (const file of files) {
637
- const fullPath = join(pathDir, file);
842
+ const fullPath = join(ALIASES_DIR, file);
638
843
  try {
639
844
  if (lstatSync(fullPath).isSymbolicLink()) {
640
845
  const target = await readlink(fullPath);
641
- if (target.includes(".droid-patch/bins") || target.includes(".droid-patch/websearch") || target.includes(".droid-patch/proxy") || target.includes(".droid-patch/statusline")) aliases.push({
846
+ if (!aliases.find((a) => a.name === file)) aliases.push({
642
847
  name: file,
643
848
  target,
644
- location: pathDir,
645
- immediate: true
849
+ location: ALIASES_DIR,
850
+ immediate: false
646
851
  });
647
852
  }
648
853
  } catch {}
649
854
  }
650
855
  } catch {}
651
856
  }
652
- try {
653
- const files = readdirSync(ALIASES_DIR);
654
- for (const file of files) {
655
- const fullPath = join(ALIASES_DIR, file);
656
- try {
657
- if (lstatSync(fullPath).isSymbolicLink()) {
658
- const target = await readlink(fullPath);
659
- if (!aliases.find((a) => a.name === file)) aliases.push({
660
- name: file,
661
- target,
662
- location: ALIASES_DIR,
663
- immediate: false
664
- });
665
- }
666
- } catch {}
667
- }
668
- } catch {}
669
857
  if (aliases.length === 0) {
670
858
  console.log(styleText("gray", " No aliases configured."));
671
859
  console.log();
@@ -744,6 +932,7 @@ async function replaceOriginal(patchedBinaryPath, originalPath, verbose = false)
744
932
  async function createAliasForWrapper(wrapperPath, aliasName, verbose = false) {
745
933
  ensureDirectories();
746
934
  console.log(styleText("white", `[*] Creating alias: ${styleText("cyan", aliasName)}`));
935
+ if (IS_WINDOWS) return createWindowsWrapperAlias(wrapperPath, aliasName, verbose);
747
936
  const writablePathDir = findWritablePathDir();
748
937
  if (writablePathDir) {
749
938
  const targetPath = join(writablePathDir, aliasName);
@@ -819,6 +1008,58 @@ async function createAliasForWrapper(wrapperPath, aliasName, verbose = false) {
819
1008
  binaryPath: wrapperPath
820
1009
  };
821
1010
  }
1011
+ /**
1012
+ * Create Windows alias for wrapper script (.cmd pointing to wrapper .cmd)
1013
+ */
1014
+ async function createWindowsWrapperAlias(wrapperPath, aliasName, verbose = false) {
1015
+ const binDir = join(DROID_PATCH_DIR, "bin");
1016
+ if (!existsSync(binDir)) mkdirSync(binDir, { recursive: true });
1017
+ if (verbose) console.log(styleText("gray", ` Wrapper: ${wrapperPath}`));
1018
+ const cmdPath = join(binDir, `${aliasName}.cmd`);
1019
+ writeFileSync(cmdPath, generateWindowsLauncher(wrapperPath));
1020
+ if (verbose) console.log(styleText("gray", ` Created launcher: ${cmdPath}`));
1021
+ const pathAdded = addToWindowsUserPath(binDir);
1022
+ console.log(styleText("green", `[*] Created: ${cmdPath}`));
1023
+ console.log();
1024
+ if (pathAdded) {
1025
+ if (checkPathInclusion()) {
1026
+ console.log(styleText("green", "─".repeat(60)));
1027
+ console.log(styleText(["green", "bold"], " ALIAS READY!"));
1028
+ console.log(styleText("green", "─".repeat(60)));
1029
+ console.log();
1030
+ console.log(styleText("white", `The alias "${styleText(["cyan", "bold"], aliasName)}" is now available.`));
1031
+ console.log(styleText("gray", `(Installed to: ${binDir})`));
1032
+ return {
1033
+ aliasPath: cmdPath,
1034
+ binaryPath: wrapperPath,
1035
+ immediate: true
1036
+ };
1037
+ }
1038
+ console.log(styleText("yellow", "─".repeat(60)));
1039
+ console.log(styleText(["yellow", "bold"], " PATH Updated - Restart Terminal"));
1040
+ console.log(styleText("yellow", "─".repeat(60)));
1041
+ console.log();
1042
+ console.log(styleText("white", "PATH has been updated. Please restart your terminal."));
1043
+ console.log(styleText("white", `Then you can use "${styleText(["cyan", "bold"], aliasName)}" command directly.`));
1044
+ console.log();
1045
+ console.log(styleText("gray", `Installed to: ${binDir}`));
1046
+ } else {
1047
+ console.log(styleText("yellow", "─".repeat(60)));
1048
+ console.log(styleText(["yellow", "bold"], " Manual PATH Configuration Required"));
1049
+ console.log(styleText("yellow", "─".repeat(60)));
1050
+ console.log();
1051
+ console.log(styleText("white", "Add this directory to your PATH:"));
1052
+ console.log(styleText("cyan", ` ${binDir}`));
1053
+ console.log();
1054
+ console.log(styleText("gray", "Or run directly:"));
1055
+ console.log(styleText("cyan", ` "${cmdPath}"`));
1056
+ }
1057
+ return {
1058
+ aliasPath: cmdPath,
1059
+ binaryPath: wrapperPath,
1060
+ immediate: false
1061
+ };
1062
+ }
822
1063
  async function restoreOriginal(originalPath) {
823
1064
  ensureDirectories();
824
1065
  const latestBackupPath = join(BINS_DIR, "droid-original-latest");
@@ -940,9 +1181,6 @@ async function removeAliasesByFilter(filter) {
940
1181
  case "standalone":
941
1182
  if (!patches.standalone) matches = false;
942
1183
  break;
943
- case "spec-model-custom":
944
- if (!patches.specModelCustom) matches = false;
945
- break;
946
1184
  }
947
1185
  if (!matches) break;
948
1186
  }
@@ -1058,4 +1296,4 @@ async function clearAllAliases() {
1058
1296
 
1059
1297
  //#endregion
1060
1298
  export { removeAlias as a, restoreOriginal as c, listAllMetadata as d, loadAliasMetadata as f, listAliases as i, createMetadata as l, patchDroid as m, createAlias as n, removeAliasesByFilter as o, saveAliasMetadata as p, createAliasForWrapper as r, replaceOriginal as s, clearAllAliases as t, formatPatches as u };
1061
- //# sourceMappingURL=alias-DwnTg_u5.mjs.map
1299
+ //# sourceMappingURL=alias-12JqnRQZ.mjs.map