@yhong91/cpac 0.2.7 → 0.2.9
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 +17 -0
- package/dist/agents.js +57 -26
- package/dist/cpac.js +11 -2
- package/dist/proxy.js +28 -0
- package/dist/targets/claude.js +2 -2
- package/dist/util.js +74 -0
- package/package.json +1 -1
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
|
@@ -3,8 +3,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { MAX_SPAWN_MODELS, fetchCatalog, loadCatalogSlugs, pickSpawnModels, saveCodexSetup, saveSpawnModels, } from "./config.js";
|
|
7
|
-
import {
|
|
6
|
+
import { MAX_SPAWN_MODELS, fetchCatalog, loadCatalogSlugs, pickSpawnModels, readState, saveCodexSetup, saveSpawnModels, saveState, stateProxy, } from "./config.js";
|
|
7
|
+
import { ensureLoopbackProxy } from "./proxy.js";
|
|
8
|
+
import { diffModelSlugs, markModelsApplied, readModelIndex, refreshModelIndex, } from "./models.js";
|
|
8
9
|
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
9
10
|
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
10
11
|
import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
@@ -12,7 +13,7 @@ import { installPiConfig, isPiConfigInstalled, piAgentDir, piModelsPath, uninsta
|
|
|
12
13
|
import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
|
|
13
14
|
import { hermesConfigPaths, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
|
|
14
15
|
import { codebuddyHome, codebuddyModelsPath, installCodebuddyConfig, isCodebuddyConfigInstalled, uninstallCodebuddyConfig, } from "./targets/codebuddy.js";
|
|
15
|
-
import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, resolveApiKey, tabWizard, } from "./util.js";
|
|
16
|
+
import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, isWindows, objectValue, resolveApiKey, spawnAgent, spawnAgentSync, tabWizard, } from "./util.js";
|
|
16
17
|
const CODEX_CTX_STD = "Standard (default input budget, e.g. 200K~272K)";
|
|
17
18
|
const CODEX_CTX_MAX = "Max Context Window (lift to full upper bound, e.g. 921K~1M, 90% auto-compact limit)";
|
|
18
19
|
const CODEX_V2_ON = "Enabled (Default)";
|
|
@@ -135,11 +136,12 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
135
136
|
await target.install(config, false, options.v2Off, options.maxContext);
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
await ensureLauncherProxy(config, target, options);
|
|
138
140
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
139
141
|
const env = targetId === "zed"
|
|
140
142
|
? { ...process.env, CPAC_API_KEY: process.env.CPAC_API_KEY || "cpac" }
|
|
141
143
|
: undefined;
|
|
142
|
-
const child =
|
|
144
|
+
const child = spawnAgent(executable, args, { stdio: "inherit", env });
|
|
143
145
|
child.once("error", (error) => {
|
|
144
146
|
rejectPromise(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
145
147
|
? `${executable} not found`
|
|
@@ -163,7 +165,7 @@ export function detectClientVersion(binary) {
|
|
|
163
165
|
if (!/^[\w.-]+$/.test(binary))
|
|
164
166
|
return undefined;
|
|
165
167
|
try {
|
|
166
|
-
const res =
|
|
168
|
+
const res = spawnAgentSync(binary, ["--version"], {
|
|
167
169
|
encoding: "utf8",
|
|
168
170
|
timeout: 2000,
|
|
169
171
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -178,10 +180,40 @@ export function detectClientVersion(binary) {
|
|
|
178
180
|
}
|
|
179
181
|
return undefined;
|
|
180
182
|
}
|
|
183
|
+
async function ensureLauncherProxy(config, target, options) {
|
|
184
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
185
|
+
const before = readState(config.state_dir);
|
|
186
|
+
const previous = before ? stateProxy(before) : null;
|
|
187
|
+
if (!previous) {
|
|
188
|
+
await target.install(config, false, options.v2Off, options.maxContext);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
192
|
+
if (!started)
|
|
193
|
+
return;
|
|
194
|
+
saveState(config.state_dir, {
|
|
195
|
+
...before,
|
|
196
|
+
proxy_id: proxy.id,
|
|
197
|
+
proxy_pid: proxy.pid,
|
|
198
|
+
proxy_port: proxy.port,
|
|
199
|
+
proxy_fingerprint: fingerprint,
|
|
200
|
+
});
|
|
201
|
+
if (previous.port === proxy.port)
|
|
202
|
+
return;
|
|
203
|
+
await target.install(config, false, options.v2Off, options.maxContext);
|
|
204
|
+
}
|
|
181
205
|
function binaryOnPath(name) {
|
|
182
206
|
if (!/^[\w.-]+$/.test(name))
|
|
183
207
|
return false;
|
|
184
208
|
try {
|
|
209
|
+
if (isWindows) {
|
|
210
|
+
// `command -v` is a POSIX shell builtin; cmd.exe only has `where`.
|
|
211
|
+
return (spawnSync("where", [name], {
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
timeout: 3_000,
|
|
214
|
+
windowsHide: true,
|
|
215
|
+
}).status === 0);
|
|
216
|
+
}
|
|
185
217
|
return (spawnSync(`command -v ${name}`, { stdio: "ignore", shell: true }).status ===
|
|
186
218
|
0);
|
|
187
219
|
}
|
|
@@ -419,17 +451,21 @@ function syncTargetIds(config, requested, all) {
|
|
|
419
451
|
}
|
|
420
452
|
return selected.map((target) => target.id);
|
|
421
453
|
}
|
|
422
|
-
function
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
454
|
+
function logModelChanges(previous, next) {
|
|
455
|
+
const { added, removed } = diffModelSlugs(previous.map((model) => model.slug), next.map((model) => model.slug));
|
|
456
|
+
const addedSet = new Set(added);
|
|
457
|
+
const previousJson = new Map(previous.map((model) => [model.slug, JSON.stringify(model)]));
|
|
458
|
+
for (const slug of added)
|
|
459
|
+
console.log(`+ ${slug}`);
|
|
460
|
+
for (const slug of removed)
|
|
461
|
+
console.log(`- ${slug}`);
|
|
462
|
+
for (const model of next) {
|
|
463
|
+
if (addedSet.has(model.slug))
|
|
464
|
+
continue;
|
|
465
|
+
const before = previousJson.get(model.slug);
|
|
466
|
+
if (before !== undefined && before !== JSON.stringify(model))
|
|
467
|
+
console.log(`~ ${model.slug}`);
|
|
468
|
+
}
|
|
433
469
|
}
|
|
434
470
|
export async function runSync(config, requested, options) {
|
|
435
471
|
const ids = syncTargetIds(config, requested, options.all);
|
|
@@ -438,16 +474,11 @@ export async function runSync(config, requested, options) {
|
|
|
438
474
|
}
|
|
439
475
|
const apiKey = await resolveApiKey(config.api_key_env);
|
|
440
476
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
477
|
+
const previous = readModelIndex(config.state_dir);
|
|
441
478
|
const { index, changed } = refreshModelIndex(config.state_dir, catalog.bytes, new Date().toISOString(), !options.dryRun);
|
|
442
|
-
|
|
479
|
+
if (changed)
|
|
480
|
+
logModelChanges(previous?.models ?? [], index.models);
|
|
443
481
|
const pending = ids.filter((id) => index.applied[id] !== index.updated_at);
|
|
444
|
-
for (const id of ids) {
|
|
445
|
-
const applied = index.applied[id];
|
|
446
|
-
if (applied === index.updated_at)
|
|
447
|
-
console.log(` ${id}: unchanged`);
|
|
448
|
-
else if (applied)
|
|
449
|
-
console.log(` ${id}: ${formatModelVersion(applied)}`);
|
|
450
|
-
}
|
|
451
482
|
if (pending.length === 0)
|
|
452
483
|
return 0;
|
|
453
484
|
// ponytail: version is the model-list timestamp only. spawn_models, v2_off,
|
|
@@ -512,7 +543,7 @@ export async function runUninstall(config, options) {
|
|
|
512
543
|
return 0;
|
|
513
544
|
}
|
|
514
545
|
console.log("Uninstalling @yhong91/cpac");
|
|
515
|
-
const result =
|
|
546
|
+
const result = spawnAgentSync("npm", ["uninstall", "-g", "@yhong91/cpac"], {
|
|
516
547
|
stdio: "inherit",
|
|
517
548
|
});
|
|
518
549
|
if (result.status !== 0) {
|
|
@@ -573,7 +604,7 @@ export async function runUpgrade(config, checkOnly) {
|
|
|
573
604
|
const args = ["install", "-g", `@yhong91/cpac@${latest}`];
|
|
574
605
|
if (prefix)
|
|
575
606
|
args.splice(2, 0, `--prefix=${prefix}`);
|
|
576
|
-
const result =
|
|
607
|
+
const result = spawnAgentSync("npm", args, { stdio: "inherit" });
|
|
577
608
|
if (result.status !== 0) {
|
|
578
609
|
console.error("npm install failed");
|
|
579
610
|
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)) {
|
|
@@ -321,6 +346,9 @@ export async function ensureLoopbackProxy(config, apiKey) {
|
|
|
321
346
|
if (existing)
|
|
322
347
|
await stopProxyProcess(existing);
|
|
323
348
|
const proxy = await startProxyProcess(config, apiKey);
|
|
349
|
+
if (!existing) {
|
|
350
|
+
console.log(`loopback proxy listening on http://127.0.0.1:${proxy.port}/v1`);
|
|
351
|
+
}
|
|
324
352
|
return { proxy, fingerprint, started: true };
|
|
325
353
|
}
|
|
326
354
|
export async function runProxy(config) {
|
package/dist/targets/claude.js
CHANGED
|
@@ -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 =
|
|
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`;
|