agentapprove 0.1.10 → 0.1.11
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 +464 -105
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2327,12 +2327,185 @@ var source_default = chalk;
|
|
|
2327
2327
|
|
|
2328
2328
|
// src/cli.ts
|
|
2329
2329
|
var import_qrcode_terminal = __toESM(require_main(), 1);
|
|
2330
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, readdirSync, statSync } from "fs";
|
|
2330
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, readdirSync as readdirSync2, statSync, lstatSync, realpathSync as realpathSync2, rmSync as rmSync2 } from "fs";
|
|
2331
2331
|
import { homedir, hostname, platform } from "os";
|
|
2332
|
-
import { join, dirname
|
|
2332
|
+
import { join, dirname as dirname2 } from "path";
|
|
2333
2333
|
import { execSync, spawnSync } from "child_process";
|
|
2334
2334
|
import { randomBytes, createHash } from "crypto";
|
|
2335
|
-
|
|
2335
|
+
|
|
2336
|
+
// src/uninstall-utils.ts
|
|
2337
|
+
import { existsSync, readdirSync, realpathSync, rmSync } from "fs";
|
|
2338
|
+
import { basename, dirname, sep } from "path";
|
|
2339
|
+
var INSTALLER_COMMAND_DESCRIPTIONS = {
|
|
2340
|
+
uninstall: "Remove hooks/plugins from agent configs and keep local Agent Approve state",
|
|
2341
|
+
purge: "Remove hooks/plugins and delete local Agent Approve state"
|
|
2342
|
+
};
|
|
2343
|
+
function isPathWithinBase(baseDir, candidatePath) {
|
|
2344
|
+
const safeBase = sanitizeAbsolutePath(baseDir);
|
|
2345
|
+
const safeCandidate = sanitizeAbsolutePath(candidatePath);
|
|
2346
|
+
if (!safeBase || !safeCandidate) {
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
return safeCandidate === safeBase || safeCandidate.startsWith(`${safeBase}${sep}`);
|
|
2350
|
+
}
|
|
2351
|
+
function safeJoinWithinBase(baseDir, ...parts) {
|
|
2352
|
+
const safeBase = sanitizeAbsolutePath(baseDir);
|
|
2353
|
+
if (!safeBase || parts.some((part) => !isSafeChildPathPart(part))) {
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
const childSegments = parts.flatMap(splitPathSegments);
|
|
2357
|
+
const candidatePath = appendPathSegments(safeBase, childSegments);
|
|
2358
|
+
if (!isPathWithinBase(safeBase, candidatePath)) {
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
return candidatePath;
|
|
2362
|
+
}
|
|
2363
|
+
function sanitizeAbsolutePath(path) {
|
|
2364
|
+
if (!path || path.includes("\x00")) {
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
const root = getPathRoot(path);
|
|
2368
|
+
if (!root) {
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
const segments = splitPathSegments(path);
|
|
2372
|
+
if (segments.includes("..")) {
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
if (root === sep) {
|
|
2376
|
+
return segments.length > 0 ? `${sep}${segments.join(sep)}` : sep;
|
|
2377
|
+
}
|
|
2378
|
+
const [, ...rest] = segments;
|
|
2379
|
+
return rest.length > 0 ? `${root}${rest.join(sep)}` : root;
|
|
2380
|
+
}
|
|
2381
|
+
function isSafeChildPathPart(part) {
|
|
2382
|
+
if (!part || part.includes("\x00") || getPathRoot(part)) {
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
const segments = splitPathSegments(part);
|
|
2386
|
+
if (segments.some((segment) => segment === "..")) {
|
|
2387
|
+
return false;
|
|
2388
|
+
}
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
function getPathRoot(path) {
|
|
2392
|
+
if (path.startsWith("/") || path.startsWith("\\")) {
|
|
2393
|
+
return sep;
|
|
2394
|
+
}
|
|
2395
|
+
const windowsDrive = path.match(/^[A-Za-z]:[\\/]/);
|
|
2396
|
+
if (windowsDrive) {
|
|
2397
|
+
return `${windowsDrive[0][0]}:${sep}`;
|
|
2398
|
+
}
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
function splitPathSegments(path) {
|
|
2402
|
+
return path.split(/[\\/]+/).filter(Boolean).filter((segment) => segment !== ".");
|
|
2403
|
+
}
|
|
2404
|
+
function appendPathSegments(basePath, childSegments) {
|
|
2405
|
+
if (childSegments.length === 0) {
|
|
2406
|
+
return basePath;
|
|
2407
|
+
}
|
|
2408
|
+
return basePath.endsWith(sep) ? `${basePath}${childSegments.join(sep)}` : `${basePath}${sep}${childSegments.join(sep)}`;
|
|
2409
|
+
}
|
|
2410
|
+
function createRemovalTarget(path, kind, recursive = false) {
|
|
2411
|
+
if (!existsSync(path)) {
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
let actualPath = path;
|
|
2415
|
+
let displayPath = path;
|
|
2416
|
+
try {
|
|
2417
|
+
actualPath = realpathSync(path);
|
|
2418
|
+
if (actualPath !== path) {
|
|
2419
|
+
displayPath = `${path} -> ${actualPath}`;
|
|
2420
|
+
}
|
|
2421
|
+
} catch {}
|
|
2422
|
+
return { path, actualPath, displayPath, kind, recursive };
|
|
2423
|
+
}
|
|
2424
|
+
function removeRemovalTarget(target) {
|
|
2425
|
+
if (!existsSync(target.path) && !existsSync(target.actualPath)) {
|
|
2426
|
+
return false;
|
|
2427
|
+
}
|
|
2428
|
+
try {
|
|
2429
|
+
rmSync(target.actualPath, { recursive: target.recursive, force: true });
|
|
2430
|
+
if (target.path !== target.actualPath) {
|
|
2431
|
+
rmSync(target.path, { recursive: false, force: true });
|
|
2432
|
+
}
|
|
2433
|
+
return true;
|
|
2434
|
+
} catch {
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function getOpenClawPluginTargets(openclawConfigPath) {
|
|
2439
|
+
const configDir = dirname(openclawConfigPath);
|
|
2440
|
+
const openclawPath = safeJoinWithinBase(configDir, "extensions", "openclaw");
|
|
2441
|
+
const legacyPath = safeJoinWithinBase(configDir, "extensions", "agentapprove");
|
|
2442
|
+
return [
|
|
2443
|
+
openclawPath ? createRemovalTarget(openclawPath, "plugin_artifact", true) : null,
|
|
2444
|
+
legacyPath ? createRemovalTarget(legacyPath, "plugin_artifact", true) : null
|
|
2445
|
+
].filter((target) => target !== null);
|
|
2446
|
+
}
|
|
2447
|
+
function getOpenCodePluginTargets(opencodeConfigDir) {
|
|
2448
|
+
const pluginPath = safeJoinWithinBase(opencodeConfigDir, "node_modules", "@agentapprove", "opencode");
|
|
2449
|
+
return [
|
|
2450
|
+
pluginPath ? createRemovalTarget(pluginPath, "plugin_artifact", true) : null
|
|
2451
|
+
].filter((target) => target !== null);
|
|
2452
|
+
}
|
|
2453
|
+
function collectBackupTargets(configPaths) {
|
|
2454
|
+
const targets = [];
|
|
2455
|
+
for (const configPath of configPaths) {
|
|
2456
|
+
const configDir = dirname(configPath);
|
|
2457
|
+
if (!existsSync(configDir)) {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
const configBase = basename(configPath);
|
|
2461
|
+
for (const entry of readdirSync(configDir)) {
|
|
2462
|
+
if (!entry.startsWith(`${configBase}.backup.`)) {
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
const backupPath = safeJoinWithinBase(configDir, entry);
|
|
2466
|
+
const target = backupPath ? createRemovalTarget(backupPath, "backup") : null;
|
|
2467
|
+
if (target) {
|
|
2468
|
+
targets.push(target);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
return targets;
|
|
2473
|
+
}
|
|
2474
|
+
function getManagedStateTargets(agentApproveDir) {
|
|
2475
|
+
const reversible = [
|
|
2476
|
+
safeJoinWithinBase(agentApproveDir, "env"),
|
|
2477
|
+
safeJoinWithinBase(agentApproveDir, "env.disabled"),
|
|
2478
|
+
safeJoinWithinBase(agentApproveDir, "hook-debug.log"),
|
|
2479
|
+
safeJoinWithinBase(agentApproveDir, "hooks"),
|
|
2480
|
+
safeJoinWithinBase(agentApproveDir, "install.sh"),
|
|
2481
|
+
safeJoinWithinBase(agentApproveDir, "copilot-cli-hooks.json")
|
|
2482
|
+
].map((path) => path ? createRemovalTarget(path, "local_state", path.endsWith(`${sep}hooks`)) : null).filter((target) => target !== null);
|
|
2483
|
+
const crypto = [
|
|
2484
|
+
safeJoinWithinBase(agentApproveDir, "e2e-key"),
|
|
2485
|
+
safeJoinWithinBase(agentApproveDir, "e2e-root-key"),
|
|
2486
|
+
safeJoinWithinBase(agentApproveDir, "e2e-server-key"),
|
|
2487
|
+
safeJoinWithinBase(agentApproveDir, "e2e-rotation.json")
|
|
2488
|
+
].map((path) => path ? createRemovalTarget(path, "crypto_material") : null).filter((target) => target !== null);
|
|
2489
|
+
if (existsSync(agentApproveDir)) {
|
|
2490
|
+
for (const entry of readdirSync(agentApproveDir)) {
|
|
2491
|
+
if (!/^e2e-key\.[^.]+\.bak$/.test(entry)) {
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
const cryptoPath = safeJoinWithinBase(agentApproveDir, entry);
|
|
2495
|
+
const target = cryptoPath ? createRemovalTarget(cryptoPath, "crypto_material") : null;
|
|
2496
|
+
if (target) {
|
|
2497
|
+
crypto.push(target);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return { reversible, crypto };
|
|
2502
|
+
}
|
|
2503
|
+
function shouldDeleteCryptoMaterial(purgeConfirmed, firstKeyConfirmation, secondKeyConfirmation) {
|
|
2504
|
+
return purgeConfirmed && firstKeyConfirmation && secondKeyConfirmation;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// src/cli.ts
|
|
2508
|
+
var VERSION = "0.1.11";
|
|
2336
2509
|
function getApiUrl() {
|
|
2337
2510
|
return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
|
|
2338
2511
|
}
|
|
@@ -2352,7 +2525,7 @@ function hasFlag2(flag) {
|
|
|
2352
2525
|
}
|
|
2353
2526
|
function updateEnvValue(key, value) {
|
|
2354
2527
|
const envPath = join(getAgentApproveDir(), "env");
|
|
2355
|
-
if (!
|
|
2528
|
+
if (!existsSync2(envPath))
|
|
2356
2529
|
return;
|
|
2357
2530
|
let content = readFileSync(envPath, "utf-8");
|
|
2358
2531
|
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
@@ -2380,7 +2553,7 @@ function getCommand() {
|
|
|
2380
2553
|
}
|
|
2381
2554
|
return filtered[0] || "install";
|
|
2382
2555
|
}
|
|
2383
|
-
var OPENCODE_PLUGIN_VERSION = "0.1.
|
|
2556
|
+
var OPENCODE_PLUGIN_VERSION = "0.1.7";
|
|
2384
2557
|
var OPENCLAW_PLUGIN_VERSION = "0.2.5";
|
|
2385
2558
|
var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
|
|
2386
2559
|
var AGENTS = {
|
|
@@ -2505,7 +2678,7 @@ function findGitBash() {
|
|
|
2505
2678
|
join(process.env.ProgramW6432 || "", "Git", "bin", "bash.exe")
|
|
2506
2679
|
].filter(Boolean);
|
|
2507
2680
|
for (const candidate of candidates) {
|
|
2508
|
-
if (
|
|
2681
|
+
if (existsSync2(candidate)) {
|
|
2509
2682
|
return candidate;
|
|
2510
2683
|
}
|
|
2511
2684
|
}
|
|
@@ -2513,7 +2686,7 @@ function findGitBash() {
|
|
|
2513
2686
|
const result = execSync("where bash", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
2514
2687
|
const firstLine = result.trim().split(`
|
|
2515
2688
|
`)[0];
|
|
2516
|
-
if (firstLine &&
|
|
2689
|
+
if (firstLine && existsSync2(firstLine)) {
|
|
2517
2690
|
return firstLine;
|
|
2518
2691
|
}
|
|
2519
2692
|
} catch {}
|
|
@@ -2550,7 +2723,7 @@ function getVSCodeSettingsPaths() {
|
|
|
2550
2723
|
return paths;
|
|
2551
2724
|
}
|
|
2552
2725
|
function findInstalledVSCodeVariants() {
|
|
2553
|
-
return getVSCodeSettingsPaths().filter(({ path }) =>
|
|
2726
|
+
return getVSCodeSettingsPaths().filter(({ path }) => existsSync2(dirname2(path)));
|
|
2554
2727
|
}
|
|
2555
2728
|
function stripJsonComments(content) {
|
|
2556
2729
|
let result = "";
|
|
@@ -2599,7 +2772,7 @@ function stripJsonComments(content) {
|
|
|
2599
2772
|
return result;
|
|
2600
2773
|
}
|
|
2601
2774
|
function readJsoncConfig(configPath) {
|
|
2602
|
-
if (!
|
|
2775
|
+
if (!existsSync2(configPath))
|
|
2603
2776
|
return {};
|
|
2604
2777
|
try {
|
|
2605
2778
|
const content = readFileSync(configPath, "utf-8");
|
|
@@ -2640,7 +2813,7 @@ function removeFromVSCodeHookLocations() {
|
|
|
2640
2813
|
const hookLocation = "~/.agentapprove/hooks";
|
|
2641
2814
|
const modified = [];
|
|
2642
2815
|
for (const { path: settingsPath, variant } of getVSCodeSettingsPaths()) {
|
|
2643
|
-
if (!
|
|
2816
|
+
if (!existsSync2(settingsPath))
|
|
2644
2817
|
continue;
|
|
2645
2818
|
try {
|
|
2646
2819
|
const config = readJsoncConfig(settingsPath);
|
|
@@ -2667,24 +2840,24 @@ function detectInstalledAgents() {
|
|
|
2667
2840
|
installed.push(id);
|
|
2668
2841
|
}
|
|
2669
2842
|
} else if (id === "copilot-cli") {
|
|
2670
|
-
if (
|
|
2843
|
+
if (existsSync2(join(homedir(), ".copilot"))) {
|
|
2671
2844
|
installed.push(id);
|
|
2672
2845
|
}
|
|
2673
2846
|
} else if (id === "openclaw") {
|
|
2674
|
-
if (
|
|
2847
|
+
if (existsSync2(join(homedir(), ".openclaw"))) {
|
|
2675
2848
|
installed.push(id);
|
|
2676
2849
|
}
|
|
2677
2850
|
} else if (id === "codex") {
|
|
2678
|
-
if (
|
|
2851
|
+
if (existsSync2(join(homedir(), ".codex"))) {
|
|
2679
2852
|
installed.push(id);
|
|
2680
2853
|
}
|
|
2681
2854
|
} else if (id === "opencode") {
|
|
2682
|
-
if (
|
|
2855
|
+
if (existsSync2(getOpenCodeConfigDir())) {
|
|
2683
2856
|
installed.push(id);
|
|
2684
2857
|
}
|
|
2685
2858
|
} else {
|
|
2686
|
-
const configDir =
|
|
2687
|
-
if (
|
|
2859
|
+
const configDir = dirname2(agent.configPath);
|
|
2860
|
+
if (existsSync2(configDir)) {
|
|
2688
2861
|
installed.push(id);
|
|
2689
2862
|
}
|
|
2690
2863
|
}
|
|
@@ -2694,9 +2867,124 @@ function detectInstalledAgents() {
|
|
|
2694
2867
|
function getAgentApproveDir() {
|
|
2695
2868
|
return join(homedir(), ".agentapprove");
|
|
2696
2869
|
}
|
|
2870
|
+
function uniqueRemovalTargets(targets) {
|
|
2871
|
+
const seen = new Set;
|
|
2872
|
+
const unique = [];
|
|
2873
|
+
for (const target of targets) {
|
|
2874
|
+
const key = `${target.kind}:${target.actualPath}`;
|
|
2875
|
+
if (seen.has(key)) {
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
seen.add(key);
|
|
2879
|
+
unique.push(target);
|
|
2880
|
+
}
|
|
2881
|
+
return unique;
|
|
2882
|
+
}
|
|
2883
|
+
function buildUninstallPlan() {
|
|
2884
|
+
const configuredAgents = detectInstalledAgents();
|
|
2885
|
+
const pluginArtifactTargets = uniqueRemovalTargets([
|
|
2886
|
+
...getOpenClawPluginTargets(AGENTS.openclaw.configPath),
|
|
2887
|
+
...getOpenCodePluginTargets(getOpenCodeConfigDir())
|
|
2888
|
+
]);
|
|
2889
|
+
return {
|
|
2890
|
+
configuredAgents,
|
|
2891
|
+
pluginArtifactTargets,
|
|
2892
|
+
backupTargets: uniqueRemovalTargets(collectBackupTargets(Object.values(AGENTS).map((agent) => agent.configPath))),
|
|
2893
|
+
managedState: getManagedStateTargets(getAgentApproveDir())
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function hasUninstallWork(mode, plan) {
|
|
2897
|
+
if (plan.configuredAgents.length > 0 || plan.pluginArtifactTargets.length > 0) {
|
|
2898
|
+
return true;
|
|
2899
|
+
}
|
|
2900
|
+
if (mode === "purge") {
|
|
2901
|
+
return plan.backupTargets.length > 0 || plan.managedState.reversible.length > 0 || plan.managedState.crypto.length > 0;
|
|
2902
|
+
}
|
|
2903
|
+
return false;
|
|
2904
|
+
}
|
|
2905
|
+
function printRemovalTargets(title, targets) {
|
|
2906
|
+
if (targets.length === 0) {
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
console.log(source_default.dim(` ${title}`));
|
|
2910
|
+
for (const target of targets) {
|
|
2911
|
+
console.log(source_default.dim(` - ${target.displayPath}`));
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
async function confirmUninstallPlan(mode, plan) {
|
|
2915
|
+
console.log();
|
|
2916
|
+
console.log(source_default.cyan(` ${mode === "purge" ? "Purge" : "Uninstall"} plan`));
|
|
2917
|
+
if (plan.configuredAgents.length > 0) {
|
|
2918
|
+
console.log(source_default.dim(` Agent configs: ${plan.configuredAgents.join(", ")}`));
|
|
2919
|
+
}
|
|
2920
|
+
printRemovalTargets("Plugin files to remove:", plan.pluginArtifactTargets);
|
|
2921
|
+
if (mode === "purge") {
|
|
2922
|
+
printRemovalTargets("Local Agent Approve files to remove:", plan.managedState.reversible);
|
|
2923
|
+
printRemovalTargets("Backups to remove:", plan.backupTargets);
|
|
2924
|
+
printRemovalTargets("Irreversible E2E material to remove:", plan.managedState.crypto);
|
|
2925
|
+
} else {
|
|
2926
|
+
console.log(source_default.dim(" Local Agent Approve state kept:"));
|
|
2927
|
+
console.log(source_default.dim(" - ~/.agentapprove/env and env.disabled"));
|
|
2928
|
+
console.log(source_default.dim(" - ~/.agentapprove/hooks, install.sh, and hook-debug.log"));
|
|
2929
|
+
console.log(source_default.dim(" - E2E keys and rotation metadata"));
|
|
2930
|
+
console.log(source_default.dim(" - OS credential-store token and config backups"));
|
|
2931
|
+
}
|
|
2932
|
+
console.log();
|
|
2933
|
+
const proceed = await ce({
|
|
2934
|
+
message: mode === "purge" ? "Proceed with purge? This removes agent hooks/plugins and local Agent Approve state." : "Proceed with uninstall? This removes agent hooks/plugins but keeps local Agent Approve state."
|
|
2935
|
+
});
|
|
2936
|
+
if (lD(proceed) || !proceed) {
|
|
2937
|
+
ge("Cancelled.");
|
|
2938
|
+
return false;
|
|
2939
|
+
}
|
|
2940
|
+
if (mode === "purge" && plan.managedState.crypto.length > 0) {
|
|
2941
|
+
const firstKeyConfirm = await ce({
|
|
2942
|
+
message: "Delete E2E keys and rotation metadata? You may lose access to encrypted local history on this machine."
|
|
2943
|
+
});
|
|
2944
|
+
if (lD(firstKeyConfirm) || !firstKeyConfirm) {
|
|
2945
|
+
v2.warn("Purge cancelled. Run `npx agentapprove uninstall` if you want to keep local keys and config.");
|
|
2946
|
+
return false;
|
|
2947
|
+
}
|
|
2948
|
+
const secondKeyConfirm = await ce({
|
|
2949
|
+
message: "Final confirmation: permanently delete the E2E keys and rotation metadata?"
|
|
2950
|
+
});
|
|
2951
|
+
if (lD(secondKeyConfirm) || !shouldDeleteCryptoMaterial(true, firstKeyConfirm === true, secondKeyConfirm === true)) {
|
|
2952
|
+
v2.warn("Purge cancelled. Run `npx agentapprove uninstall` if you want to keep local keys and config.");
|
|
2953
|
+
return false;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
return true;
|
|
2957
|
+
}
|
|
2958
|
+
function pruneEmptyDir(path) {
|
|
2959
|
+
if (!existsSync2(path)) {
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
try {
|
|
2963
|
+
if (readdirSync2(path).length === 0) {
|
|
2964
|
+
rmSync2(path, { recursive: false, force: true });
|
|
2965
|
+
}
|
|
2966
|
+
} catch {}
|
|
2967
|
+
}
|
|
2968
|
+
function finalizeAgentApproveDirPurge(agentApproveDir) {
|
|
2969
|
+
if (!existsSync2(agentApproveDir)) {
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
try {
|
|
2973
|
+
const stat = lstatSync(agentApproveDir);
|
|
2974
|
+
if (stat.isSymbolicLink()) {
|
|
2975
|
+
const actualDir = realpathSync2(agentApproveDir);
|
|
2976
|
+
if (existsSync2(actualDir) && readdirSync2(actualDir).length === 0) {
|
|
2977
|
+
rmSync2(actualDir, { recursive: false, force: true });
|
|
2978
|
+
}
|
|
2979
|
+
rmSync2(agentApproveDir, { recursive: false, force: true });
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
} catch {}
|
|
2983
|
+
pruneEmptyDir(agentApproveDir);
|
|
2984
|
+
}
|
|
2697
2985
|
function readExistingConfig() {
|
|
2698
2986
|
const envPath = join(getAgentApproveDir(), "env");
|
|
2699
|
-
if (!
|
|
2987
|
+
if (!existsSync2(envPath)) {
|
|
2700
2988
|
return null;
|
|
2701
2989
|
}
|
|
2702
2990
|
try {
|
|
@@ -2727,6 +3015,9 @@ function readExistingConfig() {
|
|
|
2727
3015
|
case "AGENTAPPROVE_E2E_MODE":
|
|
2728
3016
|
config.e2eMode = value;
|
|
2729
3017
|
break;
|
|
3018
|
+
case "AGENTAPPROVE_E2E_ENABLED":
|
|
3019
|
+
config.e2eEnabled = value === "true";
|
|
3020
|
+
break;
|
|
2730
3021
|
case "AGENTAPPROVE_FAIL_BEHAVIOR":
|
|
2731
3022
|
config.failBehavior = value;
|
|
2732
3023
|
break;
|
|
@@ -2739,11 +3030,11 @@ function readExistingConfig() {
|
|
|
2739
3030
|
}
|
|
2740
3031
|
function discoverE2EKeys() {
|
|
2741
3032
|
const dir = getAgentApproveDir();
|
|
2742
|
-
if (!
|
|
3033
|
+
if (!existsSync2(dir))
|
|
2743
3034
|
return [];
|
|
2744
3035
|
const keys = [];
|
|
2745
3036
|
const currentPath = join(dir, "e2e-key");
|
|
2746
|
-
if (
|
|
3037
|
+
if (existsSync2(currentPath)) {
|
|
2747
3038
|
try {
|
|
2748
3039
|
const hex = readFileSync(currentPath, "utf-8").trim();
|
|
2749
3040
|
if (hex.length === 64) {
|
|
@@ -2753,7 +3044,7 @@ function discoverE2EKeys() {
|
|
|
2753
3044
|
} catch {}
|
|
2754
3045
|
}
|
|
2755
3046
|
try {
|
|
2756
|
-
const files =
|
|
3047
|
+
const files = readdirSync2(dir).filter((f2) => f2.startsWith("e2e-key.") && f2.endsWith(".bak"));
|
|
2757
3048
|
for (const file of files) {
|
|
2758
3049
|
const fullPath = join(dir, file);
|
|
2759
3050
|
try {
|
|
@@ -2769,7 +3060,7 @@ function discoverE2EKeys() {
|
|
|
2769
3060
|
}
|
|
2770
3061
|
function readRotationConfig() {
|
|
2771
3062
|
const configPath = join(getAgentApproveDir(), "e2e-rotation.json");
|
|
2772
|
-
if (!
|
|
3063
|
+
if (!existsSync2(configPath))
|
|
2773
3064
|
return null;
|
|
2774
3065
|
try {
|
|
2775
3066
|
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -2783,11 +3074,11 @@ function writeRotationConfig(config) {
|
|
|
2783
3074
|
}
|
|
2784
3075
|
function ensureAgentApproveDir() {
|
|
2785
3076
|
const dir = getAgentApproveDir();
|
|
2786
|
-
if (!
|
|
3077
|
+
if (!existsSync2(dir)) {
|
|
2787
3078
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2788
3079
|
}
|
|
2789
3080
|
const hooksDir = join(dir, "hooks");
|
|
2790
|
-
if (!
|
|
3081
|
+
if (!existsSync2(hooksDir)) {
|
|
2791
3082
|
mkdirSync(hooksDir, { recursive: true, mode: 448 });
|
|
2792
3083
|
}
|
|
2793
3084
|
}
|
|
@@ -2796,14 +3087,14 @@ function migrateE2ERootKey() {
|
|
|
2796
3087
|
const keyPath = join(dir, "e2e-key");
|
|
2797
3088
|
const rootKeyPath = join(dir, "e2e-root-key");
|
|
2798
3089
|
const rotationPath = join(dir, "e2e-rotation.json");
|
|
2799
|
-
if (!
|
|
3090
|
+
if (!existsSync2(keyPath) || existsSync2(rootKeyPath))
|
|
2800
3091
|
return;
|
|
2801
3092
|
try {
|
|
2802
3093
|
const keyHex = readFileSync(keyPath, "utf-8").trim();
|
|
2803
3094
|
if (keyHex.length !== 64)
|
|
2804
3095
|
return;
|
|
2805
3096
|
writeFileSync(rootKeyPath, keyHex, { mode: 384 });
|
|
2806
|
-
if (!
|
|
3097
|
+
if (!existsSync2(rotationPath)) {
|
|
2807
3098
|
const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
|
|
2808
3099
|
writeRotationConfig({
|
|
2809
3100
|
rootKeyId: keyId,
|
|
@@ -2815,7 +3106,7 @@ function migrateE2ERootKey() {
|
|
|
2815
3106
|
} catch {}
|
|
2816
3107
|
}
|
|
2817
3108
|
function backupConfig(configPath) {
|
|
2818
|
-
if (!
|
|
3109
|
+
if (!existsSync2(configPath)) {
|
|
2819
3110
|
return null;
|
|
2820
3111
|
}
|
|
2821
3112
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
@@ -2824,7 +3115,7 @@ function backupConfig(configPath) {
|
|
|
2824
3115
|
return backupPath;
|
|
2825
3116
|
}
|
|
2826
3117
|
function readJsonConfig(configPath) {
|
|
2827
|
-
if (!
|
|
3118
|
+
if (!existsSync2(configPath)) {
|
|
2828
3119
|
return {};
|
|
2829
3120
|
}
|
|
2830
3121
|
try {
|
|
@@ -2835,21 +3126,21 @@ function readJsonConfig(configPath) {
|
|
|
2835
3126
|
}
|
|
2836
3127
|
}
|
|
2837
3128
|
function writeJsonConfig(configPath, config) {
|
|
2838
|
-
const dir =
|
|
2839
|
-
if (!
|
|
3129
|
+
const dir = dirname2(configPath);
|
|
3130
|
+
if (!existsSync2(dir)) {
|
|
2840
3131
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2841
3132
|
}
|
|
2842
3133
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
2843
3134
|
`);
|
|
2844
3135
|
}
|
|
2845
3136
|
function ensureCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.toml")) {
|
|
2846
|
-
const dir =
|
|
2847
|
-
if (!
|
|
3137
|
+
const dir = dirname2(configPath);
|
|
3138
|
+
if (!existsSync2(dir)) {
|
|
2848
3139
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2849
3140
|
}
|
|
2850
3141
|
const sectionHeader = "[features]";
|
|
2851
3142
|
const desiredLine = "codex_hooks = true";
|
|
2852
|
-
if (!
|
|
3143
|
+
if (!existsSync2(configPath)) {
|
|
2853
3144
|
writeFileSync(configPath, `${sectionHeader}
|
|
2854
3145
|
${desiredLine}
|
|
2855
3146
|
`, { mode: 384 });
|
|
@@ -2909,7 +3200,7 @@ ${desiredLine}
|
|
|
2909
3200
|
return { updated: true, backupPath };
|
|
2910
3201
|
}
|
|
2911
3202
|
function disableCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.toml")) {
|
|
2912
|
-
if (!
|
|
3203
|
+
if (!existsSync2(configPath)) {
|
|
2913
3204
|
return { updated: false, backupPath: null };
|
|
2914
3205
|
}
|
|
2915
3206
|
const original = readFileSync(configPath, "utf-8");
|
|
@@ -3212,13 +3503,13 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
3212
3503
|
const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
|
|
3213
3504
|
try {
|
|
3214
3505
|
let pkgJson = {};
|
|
3215
|
-
if (
|
|
3506
|
+
if (existsSync2(opencodePkgPath)) {
|
|
3216
3507
|
pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
|
|
3217
3508
|
}
|
|
3218
3509
|
if (!pkgJson.dependencies) {
|
|
3219
3510
|
pkgJson.dependencies = {};
|
|
3220
3511
|
}
|
|
3221
|
-
pkgJson.dependencies["@agentapprove/opencode"] =
|
|
3512
|
+
pkgJson.dependencies["@agentapprove/opencode"] = OPENCODE_PLUGIN_VERSION;
|
|
3222
3513
|
writeFileSync(opencodePkgPath, JSON.stringify(pkgJson, null, 2) + `
|
|
3223
3514
|
`);
|
|
3224
3515
|
} catch (err) {
|
|
@@ -3968,7 +4259,7 @@ async function installCommand() {
|
|
|
3968
4259
|
const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
|
|
3969
4260
|
const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
|
|
3970
4261
|
let e2eLine = "";
|
|
3971
|
-
if (
|
|
4262
|
+
if (existsSync2(e2eKeyPath2)) {
|
|
3972
4263
|
const keyHex = readFileSync(e2eKeyPath2, "utf-8").trim();
|
|
3973
4264
|
const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
|
|
3974
4265
|
e2eLine = `
|
|
@@ -4072,7 +4363,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
|
|
|
4072
4363
|
if (connectionMethod === "existing") {
|
|
4073
4364
|
token = existingConfig.token;
|
|
4074
4365
|
apiUrl = existingConfig.apiUrl || API_URL;
|
|
4075
|
-
const e2eKeyExists =
|
|
4366
|
+
const e2eKeyExists = existsSync2(join(getAgentApproveDir(), "e2e-key"));
|
|
4076
4367
|
useE2E = e2eKeyExists;
|
|
4077
4368
|
v2.success("Using existing token");
|
|
4078
4369
|
} else if (connectionMethod === "qr") {
|
|
@@ -4082,7 +4373,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
|
|
|
4082
4373
|
const existingKeyPath = join(agentApproveDir, "e2e-key");
|
|
4083
4374
|
let e2eUserKey = null;
|
|
4084
4375
|
if (useE2E) {
|
|
4085
|
-
if (
|
|
4376
|
+
if (existsSync2(existingKeyPath)) {
|
|
4086
4377
|
const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
|
|
4087
4378
|
const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
|
|
4088
4379
|
v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
|
|
@@ -4230,7 +4521,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4230
4521
|
downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files)`);
|
|
4231
4522
|
}
|
|
4232
4523
|
const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
|
|
4233
|
-
const hasE2EKey =
|
|
4524
|
+
const hasE2EKey = existsSync2(e2eKeyPath);
|
|
4234
4525
|
let installMode = "approval";
|
|
4235
4526
|
if (hasE2EKey) {
|
|
4236
4527
|
const modeChoice = await le({
|
|
@@ -4259,7 +4550,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4259
4550
|
v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
|
|
4260
4551
|
}
|
|
4261
4552
|
const envPath = join(getAgentApproveDir(), "env");
|
|
4262
|
-
if (
|
|
4553
|
+
if (existsSync2(envPath)) {
|
|
4263
4554
|
let envContent = readFileSync(envPath, "utf-8");
|
|
4264
4555
|
if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
|
|
4265
4556
|
envContent = envContent.replace(/AGENTAPPROVE_E2E_MODE=\w+/, `AGENTAPPROVE_E2E_MODE=${installMode}`);
|
|
@@ -4360,7 +4651,8 @@ Backups will be created with timestamp`, "Files to be modified");
|
|
|
4360
4651
|
npx agentapprove pair Link a new iOS device
|
|
4361
4652
|
npx agentapprove status Show current configuration
|
|
4362
4653
|
npx agentapprove disable Temporarily disable hooks
|
|
4363
|
-
npx agentapprove uninstall Remove
|
|
4654
|
+
npx agentapprove uninstall Remove hooks/plugins and keep local state
|
|
4655
|
+
npx agentapprove purge Remove hooks/plugins and delete local state
|
|
4364
4656
|
npx agentapprove restore Restore original configs`, "Useful commands");
|
|
4365
4657
|
ge(`${source_default.green("Agent Approve is ready!")} ${source_default.dim("Learn more at")} ${source_default.cyan("agentapprove.com")}`);
|
|
4366
4658
|
}
|
|
@@ -4369,7 +4661,7 @@ async function statusCommand() {
|
|
|
4369
4661
|
Agent Approve Status
|
|
4370
4662
|
`));
|
|
4371
4663
|
const envPath = join(getAgentApproveDir(), "env");
|
|
4372
|
-
if (!
|
|
4664
|
+
if (!existsSync2(envPath)) {
|
|
4373
4665
|
console.log(source_default.yellow(" Not configured. Run `npx agentapprove` to set up.\n"));
|
|
4374
4666
|
return;
|
|
4375
4667
|
}
|
|
@@ -4386,18 +4678,18 @@ async function statusCommand() {
|
|
|
4386
4678
|
}
|
|
4387
4679
|
const e2eKeyFile = join(getAgentApproveDir(), "e2e-key");
|
|
4388
4680
|
const e2eServerKeyFile = join(getAgentApproveDir(), "e2e-server-key");
|
|
4389
|
-
if (
|
|
4681
|
+
if (existsSync2(e2eKeyFile)) {
|
|
4390
4682
|
console.log(` ${source_default.green("✓")} E2E user key: installed`);
|
|
4391
4683
|
}
|
|
4392
|
-
if (
|
|
4684
|
+
if (existsSync2(e2eServerKeyFile)) {
|
|
4393
4685
|
console.log(` ${source_default.green("✓")} E2E server key: installed`);
|
|
4394
4686
|
}
|
|
4395
|
-
if (!
|
|
4687
|
+
if (!existsSync2(e2eKeyFile) && !existsSync2(e2eServerKeyFile)) {
|
|
4396
4688
|
console.log(` ${source_default.dim(" E2E encryption: not configured")}`);
|
|
4397
4689
|
}
|
|
4398
4690
|
console.log();
|
|
4399
4691
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4400
|
-
if (
|
|
4692
|
+
if (existsSync2(agent.configPath)) {
|
|
4401
4693
|
const config = readJsonConfig(agent.configPath);
|
|
4402
4694
|
if (agentId === "opencode") {
|
|
4403
4695
|
const pluginConfig = config.plugin;
|
|
@@ -4433,8 +4725,8 @@ async function statusCommand() {
|
|
|
4433
4725
|
async function disableCommand() {
|
|
4434
4726
|
const envPath = join(getAgentApproveDir(), "env");
|
|
4435
4727
|
const disabledPath = join(getAgentApproveDir(), "env.disabled");
|
|
4436
|
-
if (!
|
|
4437
|
-
if (
|
|
4728
|
+
if (!existsSync2(envPath)) {
|
|
4729
|
+
if (existsSync2(disabledPath)) {
|
|
4438
4730
|
console.log(source_default.yellow(`
|
|
4439
4731
|
Already disabled.
|
|
4440
4732
|
`));
|
|
@@ -4453,13 +4745,13 @@ async function disableCommand() {
|
|
|
4453
4745
|
async function enableCommand() {
|
|
4454
4746
|
const envPath = join(getAgentApproveDir(), "env");
|
|
4455
4747
|
const disabledPath = join(getAgentApproveDir(), "env.disabled");
|
|
4456
|
-
if (
|
|
4748
|
+
if (existsSync2(envPath)) {
|
|
4457
4749
|
console.log(source_default.yellow(`
|
|
4458
4750
|
Already enabled.
|
|
4459
4751
|
`));
|
|
4460
4752
|
return;
|
|
4461
4753
|
}
|
|
4462
|
-
if (!
|
|
4754
|
+
if (!existsSync2(disabledPath)) {
|
|
4463
4755
|
console.log(source_default.yellow("\n Not configured. Run `npx agentapprove` to set up.\n"));
|
|
4464
4756
|
return;
|
|
4465
4757
|
}
|
|
@@ -4469,12 +4761,22 @@ async function enableCommand() {
|
|
|
4469
4761
|
✓ Agent Approve enabled.
|
|
4470
4762
|
`));
|
|
4471
4763
|
}
|
|
4472
|
-
async function
|
|
4764
|
+
async function performUninstall(mode) {
|
|
4765
|
+
const plan = buildUninstallPlan();
|
|
4766
|
+
if (!hasUninstallWork(mode, plan)) {
|
|
4767
|
+
console.log(source_default.yellow(`
|
|
4768
|
+
Nothing to ${mode}. Run \`npx agentapprove install\` to set up Agent Approve.
|
|
4769
|
+
`));
|
|
4770
|
+
return;
|
|
4771
|
+
}
|
|
4772
|
+
if (!await confirmUninstallPlan(mode, plan)) {
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4473
4775
|
console.log(source_default.cyan(`
|
|
4474
|
-
Uninstalling Agent Approve
|
|
4776
|
+
${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
|
|
4475
4777
|
`));
|
|
4476
4778
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4477
|
-
if (!
|
|
4779
|
+
if (!existsSync2(agent.configPath))
|
|
4478
4780
|
continue;
|
|
4479
4781
|
const config = readJsonConfig(agent.configPath);
|
|
4480
4782
|
const hooksConfig = config[agent.hooksKey] ?? {};
|
|
@@ -4494,7 +4796,7 @@ async function uninstallCommand() {
|
|
|
4494
4796
|
}
|
|
4495
4797
|
const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
|
|
4496
4798
|
try {
|
|
4497
|
-
if (
|
|
4799
|
+
if (existsSync2(opencodePkgPath)) {
|
|
4498
4800
|
const pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
|
|
4499
4801
|
const deps = pkgJson.dependencies;
|
|
4500
4802
|
if (deps && deps["@agentapprove/opencode"]) {
|
|
@@ -4508,13 +4810,15 @@ async function uninstallCommand() {
|
|
|
4508
4810
|
const pluginsConfig = hooksConfig;
|
|
4509
4811
|
const entries = pluginsConfig.entries;
|
|
4510
4812
|
const installs = pluginsConfig.installs;
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4813
|
+
for (const key of ["openclaw", "agentapprove"]) {
|
|
4814
|
+
if (entries && entries[key]) {
|
|
4815
|
+
delete entries[key];
|
|
4816
|
+
modified = true;
|
|
4817
|
+
}
|
|
4818
|
+
if (installs && installs[key]) {
|
|
4819
|
+
delete installs[key];
|
|
4820
|
+
modified = true;
|
|
4821
|
+
}
|
|
4518
4822
|
}
|
|
4519
4823
|
} else {
|
|
4520
4824
|
for (const hook of agent.hooks) {
|
|
@@ -4539,8 +4843,8 @@ async function uninstallCommand() {
|
|
|
4539
4843
|
modified = true;
|
|
4540
4844
|
}
|
|
4541
4845
|
}
|
|
4542
|
-
if (hooksConfig
|
|
4543
|
-
delete hooksConfig
|
|
4846
|
+
if (hooksConfig.Prompt) {
|
|
4847
|
+
delete hooksConfig.Prompt;
|
|
4544
4848
|
modified = true;
|
|
4545
4849
|
}
|
|
4546
4850
|
}
|
|
@@ -4555,50 +4859,72 @@ async function uninstallCommand() {
|
|
|
4555
4859
|
}
|
|
4556
4860
|
}
|
|
4557
4861
|
}
|
|
4558
|
-
const envPath = join(getAgentApproveDir(), "env");
|
|
4559
|
-
if (existsSync(envPath)) {
|
|
4560
|
-
const { unlinkSync } = await import("fs");
|
|
4561
|
-
unlinkSync(envPath);
|
|
4562
|
-
console.log(` ${source_default.green("✓")} Removed configuration`);
|
|
4563
|
-
}
|
|
4564
|
-
if (deleteTokenFromKeychain()) {
|
|
4565
|
-
console.log(` ${source_default.green("✓")} Removed token from credential store`);
|
|
4566
|
-
}
|
|
4567
4862
|
const removedVSCode = removeFromVSCodeHookLocations();
|
|
4568
4863
|
if (removedVSCode.length > 0) {
|
|
4569
4864
|
for (const path of removedVSCode) {
|
|
4570
4865
|
console.log(` ${source_default.green("✓")} Removed hook path from ${path}`);
|
|
4571
4866
|
}
|
|
4572
4867
|
}
|
|
4573
|
-
const
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4868
|
+
for (const target of plan.pluginArtifactTargets) {
|
|
4869
|
+
if (!removeRemovalTarget(target)) {
|
|
4870
|
+
continue;
|
|
4871
|
+
}
|
|
4872
|
+
console.log(` ${source_default.green("✓")} Removed plugin files at ${target.displayPath}`);
|
|
4873
|
+
if (target.actualPath.endsWith("/node_modules/@agentapprove/opencode")) {
|
|
4874
|
+
pruneEmptyDir(dirname2(target.actualPath));
|
|
4875
|
+
pruneEmptyDir(dirname2(dirname2(target.actualPath)));
|
|
4876
|
+
}
|
|
4877
|
+
if (target.actualPath.includes("/extensions/")) {
|
|
4878
|
+
pruneEmptyDir(dirname2(target.actualPath));
|
|
4584
4879
|
}
|
|
4585
4880
|
}
|
|
4881
|
+
if (mode === "purge") {
|
|
4882
|
+
for (const target of plan.backupTargets) {
|
|
4883
|
+
if (removeRemovalTarget(target)) {
|
|
4884
|
+
console.log(` ${source_default.green("✓")} Removed backup ${target.displayPath}`);
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
for (const target of plan.managedState.reversible) {
|
|
4888
|
+
if (removeRemovalTarget(target)) {
|
|
4889
|
+
console.log(` ${source_default.green("✓")} Removed local file ${target.displayPath}`);
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
for (const target of plan.managedState.crypto) {
|
|
4893
|
+
if (removeRemovalTarget(target)) {
|
|
4894
|
+
console.log(` ${source_default.green("✓")} Removed E2E material ${target.displayPath}`);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
if (deleteTokenFromKeychain()) {
|
|
4898
|
+
console.log(` ${source_default.green("✓")} Removed token from credential store`);
|
|
4899
|
+
}
|
|
4900
|
+
finalizeAgentApproveDirPurge(getAgentApproveDir());
|
|
4901
|
+
}
|
|
4586
4902
|
console.log(source_default.green(`
|
|
4587
|
-
Agent Approve uninstalled.`));
|
|
4588
|
-
|
|
4589
|
-
|
|
4903
|
+
Agent Approve ${mode === "purge" ? "purged" : "uninstalled"}.`));
|
|
4904
|
+
if (mode === "purge") {
|
|
4905
|
+
console.log(source_default.dim(` Local Agent Approve state and backups were removed.
|
|
4590
4906
|
`));
|
|
4907
|
+
return;
|
|
4908
|
+
}
|
|
4909
|
+
console.log(source_default.dim(" Local Agent Approve config, logs, hooks, backups, and E2E keys were kept."));
|
|
4910
|
+
console.log(source_default.dim(" Run `npx agentapprove purge` if you want to remove all local Agent Approve traces.\n"));
|
|
4911
|
+
}
|
|
4912
|
+
async function uninstallCommand() {
|
|
4913
|
+
await performUninstall("uninstall");
|
|
4914
|
+
}
|
|
4915
|
+
async function purgeCommand() {
|
|
4916
|
+
await performUninstall("purge");
|
|
4591
4917
|
}
|
|
4592
4918
|
async function restoreCommand() {
|
|
4593
4919
|
console.log(source_default.cyan(`
|
|
4594
4920
|
Restoring original configurations...
|
|
4595
4921
|
`));
|
|
4596
|
-
const { readdirSync:
|
|
4922
|
+
const { readdirSync: readdirSync3, copyFileSync: copyFileSync2 } = await import("fs");
|
|
4597
4923
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4598
|
-
const configDir =
|
|
4599
|
-
if (!
|
|
4924
|
+
const configDir = dirname2(agent.configPath);
|
|
4925
|
+
if (!existsSync2(configDir))
|
|
4600
4926
|
continue;
|
|
4601
|
-
const files =
|
|
4927
|
+
const files = readdirSync3(configDir);
|
|
4602
4928
|
const backups = files.filter((f2) => f2.startsWith(agent.configPath.split("/").pop()) && f2.includes(".backup.")).sort().reverse();
|
|
4603
4929
|
if (backups.length > 0) {
|
|
4604
4930
|
const latestBackup = join(configDir, backups[0]);
|
|
@@ -4610,7 +4936,7 @@ async function restoreCommand() {
|
|
|
4610
4936
|
}
|
|
4611
4937
|
async function initRepoCommand() {
|
|
4612
4938
|
const cliHooksPath = join(getAgentApproveDir(), "copilot-cli-hooks.json");
|
|
4613
|
-
if (!
|
|
4939
|
+
if (!existsSync2(cliHooksPath)) {
|
|
4614
4940
|
console.log(source_default.yellow(`
|
|
4615
4941
|
GitHub Copilot CLI hooks not found. Run the installer first with GitHub Copilot CLI selected.`));
|
|
4616
4942
|
console.log(source_default.dim(` npx agentapprove install
|
|
@@ -4619,12 +4945,12 @@ async function initRepoCommand() {
|
|
|
4619
4945
|
}
|
|
4620
4946
|
let repoRoot = process.cwd();
|
|
4621
4947
|
let found = false;
|
|
4622
|
-
while (repoRoot !==
|
|
4623
|
-
if (
|
|
4948
|
+
while (repoRoot !== dirname2(repoRoot)) {
|
|
4949
|
+
if (existsSync2(join(repoRoot, ".git"))) {
|
|
4624
4950
|
found = true;
|
|
4625
4951
|
break;
|
|
4626
4952
|
}
|
|
4627
|
-
repoRoot =
|
|
4953
|
+
repoRoot = dirname2(repoRoot);
|
|
4628
4954
|
}
|
|
4629
4955
|
if (!found) {
|
|
4630
4956
|
console.log(source_default.yellow(`
|
|
@@ -4637,7 +4963,7 @@ async function initRepoCommand() {
|
|
|
4637
4963
|
console.log(source_default.cyan(`
|
|
4638
4964
|
Installing GitHub Copilot CLI hooks to ${targetFile}
|
|
4639
4965
|
`));
|
|
4640
|
-
if (
|
|
4966
|
+
if (existsSync2(targetFile)) {
|
|
4641
4967
|
console.log(source_default.yellow(" agentapprove.json already exists in .github/hooks/"));
|
|
4642
4968
|
const overwrite = await ce({
|
|
4643
4969
|
message: "Overwrite existing file?"
|
|
@@ -4648,7 +4974,7 @@ async function initRepoCommand() {
|
|
|
4648
4974
|
process.exit(0);
|
|
4649
4975
|
}
|
|
4650
4976
|
}
|
|
4651
|
-
if (!
|
|
4977
|
+
if (!existsSync2(targetDir)) {
|
|
4652
4978
|
mkdirSync(targetDir, { recursive: true });
|
|
4653
4979
|
}
|
|
4654
4980
|
copyFileSync(cliHooksPath, targetFile);
|
|
@@ -4666,13 +4992,14 @@ ${source_default.yellow("Usage:")}
|
|
|
4666
4992
|
|
|
4667
4993
|
${source_default.yellow("Commands:")}
|
|
4668
4994
|
${source_default.green("install")} Run the installation wizard (default)
|
|
4669
|
-
${source_default.green("pair")} Link a new iOS device
|
|
4670
|
-
${source_default.green("refresh")} Generate a new token
|
|
4995
|
+
${source_default.green("pair")} Link a new iOS device or repair E2E pairing
|
|
4996
|
+
${source_default.green("refresh")} Generate a new token and reuse current pairing context
|
|
4671
4997
|
${source_default.green("init-repo")} Add GitHub Copilot CLI hooks to current repo (.github/hooks/)
|
|
4672
4998
|
${source_default.green("status")} Show current configuration and installed hooks
|
|
4673
4999
|
${source_default.green("disable")} Temporarily disable hooks
|
|
4674
5000
|
${source_default.green("enable")} Re-enable hooks after disabling
|
|
4675
|
-
${source_default.green("uninstall")}
|
|
5001
|
+
${source_default.green("uninstall")} ${INSTALLER_COMMAND_DESCRIPTIONS.uninstall}
|
|
5002
|
+
${source_default.green("purge")} ${INSTALLER_COMMAND_DESCRIPTIONS.purge}
|
|
4676
5003
|
${source_default.green("restore")} Restore original configs from backups
|
|
4677
5004
|
${source_default.green("help")} Show this help message
|
|
4678
5005
|
|
|
@@ -4700,13 +5027,34 @@ async function refreshCommand() {
|
|
|
4700
5027
|
but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
|
|
4701
5028
|
let token = null;
|
|
4702
5029
|
let apiUrl = API_URL;
|
|
4703
|
-
const
|
|
5030
|
+
const installedAgents = detectInstalledAgents();
|
|
5031
|
+
const e2eEnabled = existingConfig.e2eEnabled !== false;
|
|
5032
|
+
const e2eKeyPath = join(getAgentApproveDir(), "e2e-key");
|
|
5033
|
+
let e2eUserKey = null;
|
|
5034
|
+
let e2eKeyId;
|
|
5035
|
+
if (e2eEnabled && existsSync2(e2eKeyPath)) {
|
|
5036
|
+
const existingKey = readFileSync(e2eKeyPath, "utf-8").trim();
|
|
5037
|
+
if (existingKey.length === 64) {
|
|
5038
|
+
e2eUserKey = existingKey;
|
|
5039
|
+
e2eKeyId = createHash("sha256").update(Buffer.from(existingKey, "hex")).digest("hex").slice(0, 8);
|
|
5040
|
+
v2.info(`Refresh will reuse E2E key ${e2eKeyId}. Use "npx agentapprove pair" if you need to choose a different key or repair pairing on another device.`);
|
|
5041
|
+
}
|
|
5042
|
+
} else {
|
|
5043
|
+
v2.info('Refresh updates the token only. Use "npx agentapprove pair" if you need to repair or change E2E pairing.');
|
|
5044
|
+
}
|
|
5045
|
+
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
|
|
4704
5046
|
if (!session || session.error) {
|
|
4705
5047
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
4706
5048
|
process.exit(1);
|
|
4707
5049
|
}
|
|
5050
|
+
const machineHost = hostname();
|
|
5051
|
+
let qrUrl = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
|
|
5052
|
+
if (e2eUserKey) {
|
|
5053
|
+
const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
|
|
5054
|
+
qrUrl += `&e2eKey=${e2eKeyBase64url}`;
|
|
5055
|
+
}
|
|
4708
5056
|
let qrDisplay = "";
|
|
4709
|
-
import_qrcode_terminal.default.generate(
|
|
5057
|
+
import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
|
|
4710
5058
|
qrDisplay = qr;
|
|
4711
5059
|
});
|
|
4712
5060
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
@@ -4741,10 +5089,16 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4741
5089
|
privacy,
|
|
4742
5090
|
retentionDays,
|
|
4743
5091
|
debugLog: existingConfig.debugLog,
|
|
5092
|
+
e2eMode: existingConfig.e2eMode,
|
|
5093
|
+
e2eEnabled,
|
|
4744
5094
|
failBehavior,
|
|
4745
5095
|
configSetAt
|
|
4746
5096
|
});
|
|
4747
5097
|
await storeTokenInKeychain(token);
|
|
5098
|
+
if (e2eEnabled && result.e2eServerKey) {
|
|
5099
|
+
const serverKeyPath = join(getAgentApproveDir(), "e2e-server-key");
|
|
5100
|
+
writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
|
|
5101
|
+
}
|
|
4748
5102
|
pushConfigToCloud({
|
|
4749
5103
|
apiUrl,
|
|
4750
5104
|
token,
|
|
@@ -4890,7 +5244,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
|
|
|
4890
5244
|
pairingSpinner.stop(`Paired! ${result.email ? `Account: ${result.email}` : ""}`);
|
|
4891
5245
|
if (e2eUserKey && e2eKeyId) {
|
|
4892
5246
|
const rootKeyPath = join(getAgentApproveDir(), "e2e-root-key");
|
|
4893
|
-
if (!
|
|
5247
|
+
if (!existsSync2(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
|
|
4894
5248
|
writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
|
|
4895
5249
|
}
|
|
4896
5250
|
if (!readRotationConfig()) {
|
|
@@ -4914,12 +5268,14 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
|
|
|
4914
5268
|
privacy: existingConfig?.privacy || result.privacy || "full",
|
|
4915
5269
|
retentionDays: existingConfig?.retentionDays ?? 30,
|
|
4916
5270
|
debugLog: existingConfig?.debugLog,
|
|
5271
|
+
e2eMode: existingConfig?.e2eMode,
|
|
5272
|
+
e2eEnabled: !!e2eUserKey,
|
|
4917
5273
|
failBehavior: existingConfig?.failBehavior || "ask",
|
|
4918
5274
|
configSetAt
|
|
4919
5275
|
});
|
|
4920
5276
|
await storeTokenInKeychain(result.token);
|
|
4921
5277
|
const hooksDir = join(getAgentApproveDir(), "hooks");
|
|
4922
|
-
if (
|
|
5278
|
+
if (existsSync2(hooksDir)) {
|
|
4923
5279
|
const updateSpinner = _2();
|
|
4924
5280
|
updateSpinner.start("Updating hook scripts with new token");
|
|
4925
5281
|
try {
|
|
@@ -4941,7 +5297,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
|
|
|
4941
5297
|
}
|
|
4942
5298
|
async function updateHookScriptsWithToken(token, apiUrl) {
|
|
4943
5299
|
const hooksDir = join(getAgentApproveDir(), "hooks");
|
|
4944
|
-
if (!
|
|
5300
|
+
if (!existsSync2(hooksDir)) {
|
|
4945
5301
|
throw new Error("Hooks directory not found");
|
|
4946
5302
|
}
|
|
4947
5303
|
const hookFiles = [
|
|
@@ -4964,7 +5320,7 @@ async function updateHookScriptsWithToken(token, apiUrl) {
|
|
|
4964
5320
|
];
|
|
4965
5321
|
for (const file of hookFiles) {
|
|
4966
5322
|
const filePath = join(hooksDir, file);
|
|
4967
|
-
if (
|
|
5323
|
+
if (existsSync2(filePath)) {
|
|
4968
5324
|
let content = readFileSync(filePath, "utf-8");
|
|
4969
5325
|
content = content.replace(/export AGENTAPPROVE_TOKEN=".*"/, `export AGENTAPPROVE_TOKEN="${token}"`);
|
|
4970
5326
|
content = content.replace(/export AGENTAPPROVE_API=".*"/, `export AGENTAPPROVE_API="${apiUrl}"`);
|
|
@@ -4996,6 +5352,9 @@ async function main() {
|
|
|
4996
5352
|
case "uninstall":
|
|
4997
5353
|
await uninstallCommand();
|
|
4998
5354
|
break;
|
|
5355
|
+
case "purge":
|
|
5356
|
+
await purgeCommand();
|
|
5357
|
+
break;
|
|
4999
5358
|
case "restore":
|
|
5000
5359
|
await restoreCommand();
|
|
5001
5360
|
break;
|