@yhong91/cpac 0.2.7 → 0.2.8

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
@@ -151,6 +151,23 @@ source ~/.zshrc # 使用引导实际显示的文件
151
151
  export CPA_API_KEY='...'
152
152
  ```
153
153
 
154
+ ### Windows
155
+
156
+ CPAC 会按平台自动选择处理方式:
157
+
158
+ - 密钥引导不写 shell profile(Windows 没有 `source` 语义)。引导会提示改用 `setx CPA_API_KEY "<your-key>"` 或「编辑账户的环境变量」持久化;也可以在 PowerShell 里直接执行:
159
+
160
+ ```powershell
161
+ [Environment]::SetEnvironmentVariable("CPA_API_KEY", "<your-key>", "User")
162
+ ```
163
+
164
+ 设置后需要重开终端。若通过 npm 全局安装,PowerShell 首次运行需要允许本地脚本:`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`。
165
+
166
+ - 检测 PATH 上的 agent 时使用 `where` 而非 `command -v`。
167
+ - 启动 agent 时自动解析 `.cmd` / `.bat` shim(npm 全局安装的 CLI),`.exe` 直接启动。
168
+ - `cpac upgrade` / `cpac uninstall` 内部调用 npm 时自动经 shell 启动 `npm.cmd`。
169
+ - 清理残留代理进程时通过 PowerShell CIM 读取进程命令行来确认身份,不会误杀无关进程。
170
+
154
171
  ## 命令
155
172
 
156
173
  ### 引导和帮助
package/dist/agents.js CHANGED
@@ -12,7 +12,7 @@ import { installPiConfig, isPiConfigInstalled, piAgentDir, piModelsPath, uninsta
12
12
  import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
13
13
  import { hermesConfigPaths, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
14
14
  import { codebuddyHome, codebuddyModelsPath, installCodebuddyConfig, isCodebuddyConfigInstalled, uninstallCodebuddyConfig, } from "./targets/codebuddy.js";
15
- import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, resolveApiKey, tabWizard, } from "./util.js";
15
+ import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, isWindows, objectValue, resolveApiKey, spawnAgent, spawnAgentSync, tabWizard, } from "./util.js";
16
16
  const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
17
17
  const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
18
18
  const CODEX_V2_ON = "Enabled (Default)";
@@ -139,7 +139,7 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
139
139
  const env = targetId === "zed"
140
140
  ? { ...process.env, CPAC_API_KEY: process.env.CPAC_API_KEY || "cpac" }
141
141
  : undefined;
142
- const child = spawn(executable, args, { stdio: "inherit", env });
142
+ const child = spawnAgent(executable, args, { stdio: "inherit", env });
143
143
  child.once("error", (error) => {
144
144
  rejectPromise(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
145
145
  ? `${executable} not found`
@@ -163,7 +163,7 @@ export function detectClientVersion(binary) {
163
163
  if (!/^[\w.-]+$/.test(binary))
164
164
  return undefined;
165
165
  try {
166
- const res = spawnSync(binary, ["--version"], {
166
+ const res = spawnAgentSync(binary, ["--version"], {
167
167
  encoding: "utf8",
168
168
  timeout: 2000,
169
169
  stdio: ["ignore", "pipe", "ignore"],
@@ -182,6 +182,14 @@ function binaryOnPath(name) {
182
182
  if (!/^[\w.-]+$/.test(name))
183
183
  return false;
184
184
  try {
185
+ if (isWindows) {
186
+ // `command -v` is a POSIX shell builtin; cmd.exe only has `where`.
187
+ return (spawnSync("where", [name], {
188
+ stdio: "ignore",
189
+ timeout: 3_000,
190
+ windowsHide: true,
191
+ }).status === 0);
192
+ }
185
193
  return (spawnSync(`command -v ${name}`, { stdio: "ignore", shell: true }).status ===
186
194
  0);
187
195
  }
@@ -512,7 +520,7 @@ export async function runUninstall(config, options) {
512
520
  return 0;
513
521
  }
514
522
  console.log("Uninstalling @yhong91/cpac");
515
- const result = spawnSync("npm", ["uninstall", "-g", "@yhong91/cpac"], {
523
+ const result = spawnAgentSync("npm", ["uninstall", "-g", "@yhong91/cpac"], {
516
524
  stdio: "inherit",
517
525
  });
518
526
  if (result.status !== 0) {
@@ -573,7 +581,7 @@ export async function runUpgrade(config, checkOnly) {
573
581
  const args = ["install", "-g", `@yhong91/cpac@${latest}`];
574
582
  if (prefix)
575
583
  args.splice(2, 0, `--prefix=${prefix}`);
576
- const result = spawnSync("npm", args, { stdio: "inherit" });
584
+ const result = spawnAgentSync("npm", args, { stdio: "inherit" });
577
585
  if (result.status !== 0) {
578
586
  console.error("npm install failed");
579
587
  return result.status ?? 1;
package/dist/cpac.js CHANGED
@@ -158,9 +158,15 @@ async function guide(config) {
158
158
  if (process.env[config.api_key_env]?.trim())
159
159
  return 0;
160
160
  const apiKey = await promptSecret(config.api_key_env);
161
+ process.env[config.api_key_env] = apiKey;
162
+ if (process.platform === "win32") {
163
+ // Windows has no shell profile to source; persist via setx instead of
164
+ // writing an export line no native shell would read.
165
+ console.log(`\nTo persist ${config.api_key_env} across terminals, run in a new terminal:\n setx ${config.api_key_env} "<your-key>"\n(or use Start > "Edit environment variables for your account").\nThe key is active for this session only until then.`);
166
+ return 0;
167
+ }
161
168
  const profile = shellProfile();
162
169
  saveApiKeyExport(profile, config.api_key_env, apiKey);
163
- process.env[config.api_key_env] = apiKey;
164
170
  console.log(`\nSaved ${config.api_key_env} to ${profile}. Open a new terminal or run:\n source ${profile}`);
165
171
  return 0;
166
172
  }
@@ -674,8 +680,11 @@ export async function main(args = process.argv.slice(2)) {
674
680
  const parsed = parseArgs(args);
675
681
  if (!parsed)
676
682
  return 0;
677
- if ("home" in parsed && parsed.home)
683
+ if ("home" in parsed && parsed.home) {
678
684
  process.env.HOME = parsed.home;
685
+ if (process.platform === "win32")
686
+ process.env.USERPROFILE = parsed.home;
687
+ }
679
688
  const config = loadConfig(parsed.configPath, true);
680
689
  if (parsed.command === "guide")
681
690
  return await guide(config);
package/dist/proxy.js CHANGED
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { createServer, request as httpRequest, } from "node:http";
5
5
  import { request as httpsRequest } from "node:https";
6
+ import { join } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { apiBase, proxyFingerprint, readState, saveState, stateProxy, } from "./config.js";
8
9
  import { CPACError, objectValue, resolveApiKey } from "./util.js";
@@ -246,6 +247,30 @@ export async function startProxyProcess(config, apiKey) {
246
247
  function processCommandArgs(pid) {
247
248
  if (!Number.isInteger(pid) || pid <= 0)
248
249
  return undefined;
250
+ if (process.platform === "win32") {
251
+ // No /proc and no ps on Windows; PowerShell CIM is the dependable way
252
+ // to read another process's command line (wmic is removed on newer
253
+ // Windows 11 builds). Only used to identify stale proxy processes.
254
+ const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
255
+ try {
256
+ const result = spawnSync(join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), [
257
+ "-NoProfile",
258
+ "-NonInteractive",
259
+ "-Command",
260
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`,
261
+ ], {
262
+ encoding: "utf8",
263
+ timeout: 5_000,
264
+ stdio: ["ignore", "pipe", "ignore"],
265
+ windowsHide: true,
266
+ });
267
+ const line = result.stdout?.trim();
268
+ return line ? line.split(/\s+/) : undefined;
269
+ }
270
+ catch {
271
+ return undefined;
272
+ }
273
+ }
249
274
  try {
250
275
  const proc = `/proc/${pid}/cmdline`;
251
276
  if (existsSync(proc)) {
@@ -4,7 +4,7 @@ import { createServer, request as httpRequest, } from "node:http";
4
4
  import { request as httpsRequest } from "node:https";
5
5
  import { apiBase, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, loadConfig, } from "../config.js";
6
6
  import { proxyHeaders, responseHeaders, upstreamUrl } from "../proxy.js";
7
- import { CPACError, atomicWrite, objectValue, resolveApiKey, tabWizard, } from "../util.js";
7
+ import { CPACError, atomicWrite, objectValue, resolveApiKey, spawnAgent, tabWizard, } from "../util.js";
8
8
  const CLAUDE_ALIAS_PREFIX = "claude-cpac--";
9
9
  // Claude Code accepts CLAUDE_CODE_AUTO_COMPACT_WINDOW in 100K–1M (binary-verified).
10
10
  // A single global window cannot be per-model; 350K is opencodex's user-approved
@@ -379,7 +379,7 @@ export async function runClaude(config, args, executable = "claude") {
379
379
  delete env.CLAUDE_CODE_USE_FOUNDRY;
380
380
  delete env.CLAUDE_CODE_USE_VERTEX;
381
381
  return await new Promise((resolve, reject) => {
382
- const child = spawn(executable, args, { env, stdio: "inherit" });
382
+ const child = spawnAgent(executable, args, { env, stdio: "inherit" });
383
383
  child.once("error", (error) => {
384
384
  stopProxy();
385
385
  reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
package/dist/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn, spawnSync, } from "node:child_process";
1
2
  import { chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { basename, dirname, join, parse } from "node:path";
@@ -9,6 +10,79 @@ export class CPACError extends Error {
9
10
  export function objectValue(value) {
10
11
  return typeof value === "object" && value !== null && !Array.isArray(value);
11
12
  }
13
+ export const isWindows = process.platform === "win32";
14
+ // Windows process launching: npm-installed CLIs are .cmd/.bat shims, and
15
+ // spawn without a shell only resolves .exe (CreateProcess), so those need a
16
+ // shell plus a correctly quoted command line. Everything else spawns directly.
17
+ function resolveWindowsExecutable(executable) {
18
+ if (!/^[\w.-]+$/.test(executable))
19
+ return undefined;
20
+ try {
21
+ const result = spawnSync("where", [executable], {
22
+ encoding: "utf8",
23
+ timeout: 3_000,
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ windowsHide: true,
26
+ });
27
+ if (result.status !== 0)
28
+ return undefined;
29
+ const lines = result.stdout
30
+ .split(/\r?\n/)
31
+ .map((entry) => entry.trim())
32
+ .filter(Boolean);
33
+ return (lines.find((entry) => /\.(exe|com|cmd|bat)$/i.test(entry)) || lines[0]);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ // Quotes a single argument for cmd.exe (MSDN rules): double backslashes that
40
+ // precede a quote, double trailing backslashes so they cannot escape the
41
+ // closing quote, and wrap tokens containing whitespace or quotes.
42
+ function cmdQuote(arg) {
43
+ if (arg !== "" && !/[\s"]/.test(arg))
44
+ return arg;
45
+ let out = "";
46
+ let backslashes = 0;
47
+ for (const ch of arg) {
48
+ if (ch === "\\") {
49
+ backslashes++;
50
+ continue;
51
+ }
52
+ if (ch === '"')
53
+ out += `${"\\".repeat(backslashes * 2 + 1)}"`;
54
+ else
55
+ out += `${"\\".repeat(backslashes)}${ch}`;
56
+ backslashes = 0;
57
+ }
58
+ return `"${out}${"\\".repeat(backslashes * 2)}"`;
59
+ }
60
+ function spawnAgentPlan(executable, args) {
61
+ if (!isWindows)
62
+ return { command: executable, args };
63
+ const resolved = resolveWindowsExecutable(executable);
64
+ if (!resolved)
65
+ return null; // let the plain spawn produce the ENOENT error
66
+ if (!/\.(cmd|bat)$/i.test(resolved))
67
+ return { command: resolved, args };
68
+ return { line: [resolved, ...args].map(cmdQuote).join(" ") };
69
+ }
70
+ export function spawnAgent(executable, args, options) {
71
+ const plan = spawnAgentPlan(executable, args);
72
+ if (plan === null)
73
+ return spawn(executable, args, options);
74
+ if ("line" in plan)
75
+ return spawn(plan.line, { ...options, shell: true });
76
+ return spawn(plan.command, plan.args, options);
77
+ }
78
+ export function spawnAgentSync(executable, args, options) {
79
+ const plan = spawnAgentPlan(executable, args);
80
+ if (plan === null)
81
+ return spawnSync(executable, args, options);
82
+ if ("line" in plan)
83
+ return spawnSync(plan.line, { ...options, shell: true });
84
+ return spawnSync(plan.command, plan.args, options);
85
+ }
12
86
  export function ensureCpacBackup(stateDir, agent, target, native = false) {
13
87
  const dest = join(stateDir, agent, basename(target));
14
88
  const sidecar = `${target}.cpac-backup`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {