anyray-connect 0.11.74 → 0.11.76
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 +628 -306
- package/dist/cli.js.map +1 -1
- package/dist/commands/desktop.js +6 -1
- package/dist/commands/desktop.js.map +1 -1
- package/dist/commands/doctor.js +90 -22
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hook.js +8 -0
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/migrate.js +17 -4
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/projectApply.js +267 -0
- package/dist/commands/projectApply.js.map +1 -0
- package/dist/commands/quitWatch.js +124 -0
- package/dist/commands/quitWatch.js.map +1 -0
- package/dist/commands/refreshProxy.js +258 -33
- package/dist/commands/refreshProxy.js.map +1 -1
- package/dist/commands/status.js +11 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/target.js +1 -1
- package/dist/target.js.map +1 -1
- package/dist/tools/claudeCode.js +23 -7
- package/dist/tools/claudeCode.js.map +1 -1
- package/dist/tools/claudeDesktop.js +232 -1
- package/dist/tools/claudeDesktop.js.map +1 -1
- package/dist/tools/claudeDesktopNative.js +9 -1
- package/dist/tools/claudeDesktopNative.js.map +1 -1
- package/dist/tools/claudeDesktopProfileNotice.js +28 -19
- package/dist/tools/claudeDesktopProfileNotice.js.map +1 -1
- package/dist/tools/codex.js +17 -6
- package/dist/tools/codex.js.map +1 -1
- package/dist/tools/copilotChat.js +22 -24
- package/dist/tools/copilotChat.js.map +1 -1
- package/dist/tools/copilotCli.js +149 -85
- package/dist/tools/copilotCli.js.map +1 -1
- package/dist/tools/cursor.js +126 -38
- package/dist/tools/cursor.js.map +1 -1
- package/dist/tools/opencode.js +354 -65
- package/dist/tools/opencode.js.map +1 -1
- package/dist/tools/sharedHooks.js +38 -0
- package/dist/tools/sharedHooks.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/appQuit.js +331 -0
- package/dist/util/appQuit.js.map +1 -0
- package/dist/util/codexConfig.js +22 -0
- package/dist/util/codexConfig.js.map +1 -1
- package/dist/util/desktopProfileMigrate.js +39 -4
- package/dist/util/desktopProfileMigrate.js.map +1 -1
- package/dist/util/devRefresh.js +128 -7
- package/dist/util/devRefresh.js.map +1 -1
- package/dist/util/devVerify.js +5 -1
- package/dist/util/devVerify.js.map +1 -1
- package/dist/util/historyStore.js +0 -0
- package/dist/util/historyStore.js.map +1 -1
- package/dist/util/pendingOps.js +416 -0
- package/dist/util/pendingOps.js.map +1 -0
- package/dist/util/persist.js +15 -3
- package/dist/util/persist.js.map +1 -1
- package/dist/util/proxyService.js +59 -2
- package/dist/util/proxyService.js.map +1 -1
- package/dist/util/refreshScheduler.js +25 -0
- package/dist/util/refreshScheduler.js.map +1 -1
- package/dist/util/remoteContext.js +55 -0
- package/dist/util/remoteContext.js.map +1 -0
- package/dist/util/seatState.js +28 -2
- package/dist/util/seatState.js.map +1 -1
- package/dist/util/serviceArtifacts.js +9 -0
- package/dist/util/serviceArtifacts.js.map +1 -1
- package/dist/util/verify.js +38 -7
- package/dist/util/verify.js.map +1 -1
- package/dist/util/vscodeProcess.js +119 -0
- package/dist/util/vscodeProcess.js.map +1 -0
- package/dist/util/vscodeState.js +6 -0
- package/dist/util/vscodeState.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/dist/commands/cursorWatch.js +0 -95
- package/dist/commands/cursorWatch.js.map +0 -1
- package/dist/util/cursorPending.js +0 -188
- package/dist/util/cursorPending.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
* personal gateway authentication, and content-free attribution.
|
|
11
11
|
*/
|
|
12
12
|
import { isPersonalClientKey, isInsecureRemoteOrigin, normalizeOrigin, resolveOrigin, resolveTarget, withLocalProxy, } from "./target.js";
|
|
13
|
-
import { pickLoopbackPort, installProxyService, uninstallProxyService, probeHealth, } from "./util/proxyService.js";
|
|
13
|
+
import { pickLoopbackPort, installProxyService, uninstallProxyService, probeHealth, DEFAULT_LOOPBACK_PORT, } from "./util/proxyService.js";
|
|
14
14
|
import { installRefreshScheduler, uninstallRefreshScheduler, } from "./util/refreshScheduler.js";
|
|
15
|
+
import { detectRemoteDevBackend, isContainerBackend, } from "./util/remoteContext.js";
|
|
15
16
|
import { runRefreshProxy } from "./commands/refreshProxy.js";
|
|
16
17
|
import { loopbackEngaged } from "./util/loopback.js";
|
|
17
|
-
import {
|
|
18
|
+
import { runQuitWatch } from "./commands/quitWatch.js";
|
|
18
19
|
import { runManaged } from "./commands/managed.js";
|
|
19
20
|
import { runPrintKey } from "./commands/printKey.js";
|
|
21
|
+
import { runProjectApply } from "./commands/projectApply.js";
|
|
20
22
|
import { runClaudeDesktopCredential, } from "./commands/claudeDesktopCredential.js";
|
|
21
23
|
import { runDesktop } from "./commands/desktop.js";
|
|
22
24
|
import { runRecallCommand, runRetrieveCommand } from "./commands/retrieval.js";
|
|
@@ -33,11 +35,13 @@ import { runSelfUpdate } from "./util/selfUpdate.js";
|
|
|
33
35
|
import { REGISTRY, byId } from "./tools/index.js";
|
|
34
36
|
import { staleShellExportsForTargets } from "./tools/shellEnv.js";
|
|
35
37
|
import { effectiveSubscription, metadataHeaderValue } from "./types.js";
|
|
38
|
+
import { quitTargetKey, quitTargetLabel, relaunchResolvable, quitTargetSupported, relaunchApp, requestGracefulQuit, waitForQuit, } from "./util/appQuit.js";
|
|
36
39
|
import { bold, cyan, dim, fail, heading, info, note, ok, warn, } from "./util/log.js";
|
|
37
40
|
import { clearDevKey, clearEnrollment, hasEnrollment, loadProfile, recordManagedTools, saveProfile, saveDevKey, loadDevKey, stateDir, ensureStateDir, purgeRetiredFallbackState, } from "./util/persist.js";
|
|
38
41
|
import { confirm, isInteractive, prompt, resolveDisplayName, resolveTeam, } from "./commands/shared.js";
|
|
39
42
|
import { verifyConnection } from "./util/verify.js";
|
|
40
43
|
import { detectSeatState } from "./util/seatState.js";
|
|
44
|
+
import { isRemoteDevContext } from "./tools/copilotChat.js";
|
|
41
45
|
import { runStatus } from "./commands/status.js";
|
|
42
46
|
import { runDoctor } from "./commands/doctor.js";
|
|
43
47
|
import { runSync } from "./commands/sync.js";
|
|
@@ -56,6 +60,7 @@ import { runAcpAgent } from "./commands/acpAgent.js";
|
|
|
56
60
|
import { deriveMachineId } from "./util/machineId.js";
|
|
57
61
|
import { relaunchFromDurableRuntime } from "./util/durableRuntime.js";
|
|
58
62
|
import { CLAUDE_DESKTOP_CREDENTIAL_ARG, } from "./tools/claudeDesktopCredentialProtocol.js";
|
|
63
|
+
import { platform } from "node:os";
|
|
59
64
|
const HELP = `${bold("anyray-connect")} — point your local coding tools at the Anyray gateway
|
|
60
65
|
|
|
61
66
|
${bold("Usage")}
|
|
@@ -70,9 +75,11 @@ ${bold("Subcommands")}
|
|
|
70
75
|
doctor Diagnose gateway reachability + tool config (incl. each tool's
|
|
71
76
|
retrieval MCP-server registration); exits non-zero on problems.
|
|
72
77
|
--json prints a shareable content-free report (send it to support);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
the auth smoke probe runs by DEFAULT (one tiny request) so an
|
|
79
|
+
all-clear means a request actually worked, not just that the
|
|
80
|
+
config looks right; --no-verify skips it for offline runs;
|
|
81
|
+
--verify additionally runs a retrieval-loop probe (which
|
|
82
|
+
never latches model retrieve capability);
|
|
76
83
|
--repair re-applies managed tools whose routing drifted
|
|
77
84
|
(only tools a previous apply recorded — a reverted tool
|
|
78
85
|
is never resurrected) and deletes the retired fallback
|
|
@@ -173,6 +180,12 @@ ${bold("Options")}
|
|
|
173
180
|
LiteLLM virtual key). Stored locally (mode 600) and in the
|
|
174
181
|
tool config, never sent to Anyray's control plane.
|
|
175
182
|
--tools <ids> Comma-separated subset: ${REGISTRY.map((t) => t.id).join(", ")}
|
|
183
|
+
--project Write committable, credential-free routing config into the
|
|
184
|
+
CURRENT repo (.claude/settings.json + .codex/config.toml)
|
|
185
|
+
instead of this machine's user config — so devcontainers,
|
|
186
|
+
Codespaces, SSH checkouts, and CI route without a per-machine
|
|
187
|
+
apply. Org API lane only; each machine still enrolls once.
|
|
188
|
+
Combine with --revert to remove it.
|
|
176
189
|
--dry-run Show what would change without writing anything
|
|
177
190
|
--yes, -y Don't prompt for confirmation (for scripted rollout)
|
|
178
191
|
--force Skip the gateway reachability check before writing
|
|
@@ -183,6 +196,15 @@ ${bold("Options")}
|
|
|
183
196
|
daemon fills a freshly re-minted key on each request so a
|
|
184
197
|
long session never 401s on an expired key mid-flight; this
|
|
185
198
|
forces the legacy lane that bakes the key once at launch.
|
|
199
|
+
--quit-apps Close the apps this run reconfigures — and reopen them when
|
|
200
|
+
it finishes — without asking. Only apps that are open AND
|
|
201
|
+
whose configuration actually changes; the quit is always the
|
|
202
|
+
polite one the app may refuse, never a force kill. An
|
|
203
|
+
interactive run is asked once and defaults to NO; --yes, a
|
|
204
|
+
non-TTY, --dry-run and the managed/MDM payloads close
|
|
205
|
+
nothing unless this flag is passed explicitly.
|
|
206
|
+
--no-quit-apps Never close anything, overriding --quit-apps. Apps that were
|
|
207
|
+
open keep their old routing until you restart them yourself.
|
|
186
208
|
--revert Undo a previous run: unpick every tool config we wrote and
|
|
187
209
|
clear the stored enrollment. Reconnecting needs --sso (or a
|
|
188
210
|
fresh --enroll link). Cannot be combined with --sso or
|
|
@@ -275,6 +297,9 @@ export const parseArgs = (argv) => {
|
|
|
275
297
|
.map((s) => s.trim())
|
|
276
298
|
.filter(Boolean);
|
|
277
299
|
break;
|
|
300
|
+
case "--project":
|
|
301
|
+
args.project = true;
|
|
302
|
+
break;
|
|
278
303
|
case "--dry-run":
|
|
279
304
|
args.dryRun = true;
|
|
280
305
|
break;
|
|
@@ -295,6 +320,12 @@ export const parseArgs = (argv) => {
|
|
|
295
320
|
case "--no-key-refresh":
|
|
296
321
|
args.noKeyRefresh = true;
|
|
297
322
|
break;
|
|
323
|
+
case "--quit-apps":
|
|
324
|
+
args.quitApps = true;
|
|
325
|
+
break;
|
|
326
|
+
case "--no-quit-apps":
|
|
327
|
+
args.noQuitApps = true;
|
|
328
|
+
break;
|
|
298
329
|
case "--revert":
|
|
299
330
|
args.revert = true;
|
|
300
331
|
break;
|
|
@@ -304,6 +335,9 @@ export const parseArgs = (argv) => {
|
|
|
304
335
|
case "--verify":
|
|
305
336
|
args.verify = true;
|
|
306
337
|
break;
|
|
338
|
+
case "--no-verify":
|
|
339
|
+
args.noVerify = true;
|
|
340
|
+
break;
|
|
307
341
|
case "--repair":
|
|
308
342
|
args.repair = true;
|
|
309
343
|
break;
|
|
@@ -482,6 +516,10 @@ explicitIds) => {
|
|
|
482
516
|
: await adapter.apply(target, {
|
|
483
517
|
...opts,
|
|
484
518
|
explicit: explicitIds?.has(adapter.id) === true,
|
|
519
|
+
// This loop prints every message below, so an adapter may take a
|
|
520
|
+
// side action on the developer's own files here. The background
|
|
521
|
+
// lanes call `apply` directly and never set this.
|
|
522
|
+
reportsToUser: true,
|
|
485
523
|
});
|
|
486
524
|
for (const m of result.messages)
|
|
487
525
|
info(` ${m}`);
|
|
@@ -530,6 +568,153 @@ export const manualStepsNotice = (titles) => {
|
|
|
530
568
|
"Then restart or reload the app as instructed; use `npx anyray-connect status` for the available route checks.",
|
|
531
569
|
];
|
|
532
570
|
};
|
|
571
|
+
/**
|
|
572
|
+
* The plan pass: which open apps would this apply actually affect?
|
|
573
|
+
*
|
|
574
|
+
* An editor caches its environment at launch and Cursor / Claude Desktop cannot
|
|
575
|
+
* have their stores rewritten while they are open, so a config written now does
|
|
576
|
+
* not take effect until the app restarts. Rather than printing "restart X" and
|
|
577
|
+
* hoping, Connect offers to do it — but only for an adapter that is
|
|
578
|
+
* installed, declares how its app is named to each platform
|
|
579
|
+
* (`quitGated.quitTarget`, read off the REGISTRY so this file never names an
|
|
580
|
+
* app), is running, and is NOT already pointed at this gateway.
|
|
581
|
+
*
|
|
582
|
+
* That last clause is what keeps a no-op re-run silent: nothing changes, so
|
|
583
|
+
* nobody's apps get closed.
|
|
584
|
+
*
|
|
585
|
+
* KNOWN BLIND SPOT, deliberately not guessed at: `pointsAt` reads the tool's
|
|
586
|
+
* CONFIG on disk, so it cannot see "configured, but the running process still
|
|
587
|
+
* holds the environment it cached at launch" — that app reads as correctly
|
|
588
|
+
* configured here and is left alone. `doctor` owns that case; inferring it
|
|
589
|
+
* would mean offering to close apps on every single re-run.
|
|
590
|
+
*
|
|
591
|
+
* Read-only and best-effort throughout: an adapter probe that throws simply
|
|
592
|
+
* drops out of the plan.
|
|
593
|
+
*/
|
|
594
|
+
export const planAppRestarts = async (adapters, origin,
|
|
595
|
+
// Explicit so a test can assert a platform deliberately. Reading the host's
|
|
596
|
+
// real platform made three of these tests pass on macOS and fail on Linux CI,
|
|
597
|
+
// because the fixture named no Linux spelling and the skip was invisible.
|
|
598
|
+
runtimePlatform = platform()) => {
|
|
599
|
+
const candidates = [];
|
|
600
|
+
// One APP, not one adapter. VS Code carries both Copilot adapters, so without
|
|
601
|
+
// this the developer is asked twice about the same window and it is closed
|
|
602
|
+
// twice — the second quit landing on an app that is already gone.
|
|
603
|
+
const claimedApps = new Set();
|
|
604
|
+
for (const adapter of adapters) {
|
|
605
|
+
const seam = adapter.quitGated;
|
|
606
|
+
const quitTarget = seam?.quitTarget;
|
|
607
|
+
if (!seam || !quitTarget)
|
|
608
|
+
continue;
|
|
609
|
+
// A target that doesn't name this platform can't be honoured here, and an
|
|
610
|
+
// offer we can't honour reads as a broken promise.
|
|
611
|
+
if (!quitTargetSupported(quitTarget, runtimePlatform))
|
|
612
|
+
continue;
|
|
613
|
+
// Never close what we cannot reopen (Flatpak/AppImage editors on Linux).
|
|
614
|
+
if (!relaunchResolvable(quitTarget, runtimePlatform))
|
|
615
|
+
continue;
|
|
616
|
+
const app = quitTargetKey(quitTarget, runtimePlatform);
|
|
617
|
+
if (claimedApps.has(app))
|
|
618
|
+
continue;
|
|
619
|
+
try {
|
|
620
|
+
const detected = await adapter.detect();
|
|
621
|
+
if (!detected.installed)
|
|
622
|
+
continue;
|
|
623
|
+
if (!seam.isRunning())
|
|
624
|
+
continue;
|
|
625
|
+
if ((await adapter.pointsAt?.(origin)) === true)
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Never let a probe failure turn into an offer to close someone's editor.
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
claimedApps.add(app);
|
|
633
|
+
candidates.push({
|
|
634
|
+
id: adapter.id,
|
|
635
|
+
// The APP's name, not the adapter's — see `quitTargetLabel`.
|
|
636
|
+
title: quitTargetLabel(quitTarget),
|
|
637
|
+
quitTarget,
|
|
638
|
+
isRunning: () => seam.isRunning(),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return candidates;
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* May this invocation close apps at all? Consent has to be present on the
|
|
645
|
+
* command line or at a prompt:
|
|
646
|
+
* - `--no-quit-apps` refuses, and wins over everything else.
|
|
647
|
+
* - `--dry-run` writes nothing, so it closes nothing (it reports the offer).
|
|
648
|
+
* - `--quit-apps` is an operator asking for it explicitly — the only way a
|
|
649
|
+
* scripted run gets it.
|
|
650
|
+
* - otherwise: an interactive TTY that was not handed `--yes` may ASK (the
|
|
651
|
+
* prompt itself defaults to no).
|
|
652
|
+
*
|
|
653
|
+
* `--yes`, a non-TTY and the managed/MDM payloads therefore close NOTHING by
|
|
654
|
+
* default. That is the hard rule: a buyer rolling Connect out to 500 seats must
|
|
655
|
+
* never have it close an app on someone's machine.
|
|
656
|
+
*/
|
|
657
|
+
export const quitAppsAllowed = (args, interactive) => {
|
|
658
|
+
if (args.noQuitApps === true)
|
|
659
|
+
return false;
|
|
660
|
+
if (args.dryRun)
|
|
661
|
+
return false;
|
|
662
|
+
if (args.quitApps === true)
|
|
663
|
+
return true;
|
|
664
|
+
return !args.yes && interactive;
|
|
665
|
+
};
|
|
666
|
+
/** The one-line offer. Returned as a string so it's testable without a TTY. */
|
|
667
|
+
export const appRestartOffer = (titles) => {
|
|
668
|
+
const which = titles.length === 1
|
|
669
|
+
? `${titles[0]} is open and won't pick this up until it restarts. Quit it now?`
|
|
670
|
+
: `${titles.slice(0, -1).join(", ")} and ${titles[titles.length - 1]} are open and won't pick this up until they restart. Quit them now?`;
|
|
671
|
+
return `\n${which}`;
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Ask each accepted app to quit and confirm it actually went away. Returns the
|
|
675
|
+
* ones Connect really closed — the exact set the relaunch is allowed to reopen.
|
|
676
|
+
*
|
|
677
|
+
* An app that refuses (an unsaved buffer, a modal) is a legitimate answer: say
|
|
678
|
+
* so plainly and carry on with the apply, which leaves the adapter's own
|
|
679
|
+
* queue-on-quit path to finish the job whenever the developer closes it. Never
|
|
680
|
+
* escalate to a force kill, never retry — these apps write state on exit.
|
|
681
|
+
*/
|
|
682
|
+
export const quitAppsNow = async (candidates, deps = {}) => {
|
|
683
|
+
const requestQuit = deps.requestQuit ?? ((target) => requestGracefulQuit(target));
|
|
684
|
+
const waitFor = deps.waitFor ?? ((isRunning) => waitForQuit(isRunning));
|
|
685
|
+
const closed = [];
|
|
686
|
+
for (const candidate of candidates) {
|
|
687
|
+
const delivered = requestQuit(candidate.quitTarget);
|
|
688
|
+
const gone = delivered && (await waitFor(candidate.isRunning));
|
|
689
|
+
if (gone) {
|
|
690
|
+
closed.push(candidate);
|
|
691
|
+
ok(` Closed ${candidate.title}.`);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
warn(` ${candidate.title} is still open — leaving it alone.`);
|
|
695
|
+
note(` Its configuration is written either way; ${candidate.title} picks it up the next time you close it.`);
|
|
696
|
+
}
|
|
697
|
+
return closed;
|
|
698
|
+
};
|
|
699
|
+
/**
|
|
700
|
+
* Reopen exactly what we closed, under the consent that closed it — never a
|
|
701
|
+
* second prompt, and never an app the developer closed themselves. Leaving
|
|
702
|
+
* someone's IDE shut is a worse outcome than never having offered.
|
|
703
|
+
*/
|
|
704
|
+
export const relaunchApps = (closed, deps = {}) => {
|
|
705
|
+
const relaunch = deps.relaunch ?? ((target) => relaunchApp(target));
|
|
706
|
+
const reopened = [];
|
|
707
|
+
for (const candidate of closed) {
|
|
708
|
+
if (relaunch(candidate.quitTarget)) {
|
|
709
|
+
reopened.push(candidate.title);
|
|
710
|
+
ok(` Reopened ${candidate.title}.`);
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
warn(` Could not reopen ${candidate.title} — start it when you're ready.`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return reopened;
|
|
717
|
+
};
|
|
533
718
|
/**
|
|
534
719
|
* May the wizard put this tool in its prefilled selection?
|
|
535
720
|
*
|
|
@@ -623,6 +808,46 @@ const runVerify = async (target, toolIds) => {
|
|
|
623
808
|
for (const line of verdict.remedy)
|
|
624
809
|
info(dim(` ${line}`));
|
|
625
810
|
};
|
|
811
|
+
/** Per-probe timeout for the reconcile retries — generous, a wedged daemon fails them all. */
|
|
812
|
+
const RECONCILE_RETRY_PROBE_TIMEOUT_MS = 5_000;
|
|
813
|
+
const RECONCILE_PROBE_GAP_MS = 500;
|
|
814
|
+
const RECONCILE_PROBE_ATTEMPTS = 3;
|
|
815
|
+
// Scheduler-tick self-heal: EDR kills/quarantines the daemon service; a silent
|
|
816
|
+
// port with key refresh enabled reinstalls it (the probe-first check is what
|
|
817
|
+
// leaves a live daemon alone — including a healthy one on an older build).
|
|
818
|
+
export const reconcileProxyService = async (deps = {}) => {
|
|
819
|
+
try {
|
|
820
|
+
const profile = await (deps.load ?? loadProfile)();
|
|
821
|
+
if (profile.keyRefreshEnabled !== true || !profile.gateway)
|
|
822
|
+
return;
|
|
823
|
+
const port = profile.fallbackPort ?? DEFAULT_LOOPBACK_PORT;
|
|
824
|
+
// Patience before the bounce: a 1.5s probe blip on a busy-but-alive daemon
|
|
825
|
+
// must not trigger a reinstall (greptile P1 on #1389). A quarantined daemon
|
|
826
|
+
// fails every probe instantly (connection refused), so healing stays prompt;
|
|
827
|
+
// a wedged one fails them all slowly and still heals this tick.
|
|
828
|
+
const probe = deps.probe ?? probeHealth;
|
|
829
|
+
const probeGapMs = deps.probeGapMs ?? RECONCILE_PROBE_GAP_MS;
|
|
830
|
+
for (let attempt = 0; attempt < RECONCILE_PROBE_ATTEMPTS; attempt++) {
|
|
831
|
+
const timeoutMs = attempt === 0 ? undefined : RECONCILE_RETRY_PROBE_TIMEOUT_MS;
|
|
832
|
+
if ((await probe(port, timeoutMs)) !== null)
|
|
833
|
+
return;
|
|
834
|
+
if (attempt < RECONCILE_PROBE_ATTEMPTS - 1 && probeGapMs > 0)
|
|
835
|
+
await new Promise((r) => setTimeout(r, probeGapMs));
|
|
836
|
+
}
|
|
837
|
+
const svc = await (deps.install ?? installProxyService)({
|
|
838
|
+
port,
|
|
839
|
+
remoteOrigin: new URL(profile.gateway).origin,
|
|
840
|
+
});
|
|
841
|
+
// Lands in the scheduler's log file — a permanent install failure must not
|
|
842
|
+
// retry silently twice an hour forever.
|
|
843
|
+
if (!svc.ok)
|
|
844
|
+
for (const m of svc.messages)
|
|
845
|
+
console.error(m);
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
// Malformed profile/gateway URL or install failure — next tick retries.
|
|
849
|
+
}
|
|
850
|
+
};
|
|
626
851
|
/**
|
|
627
852
|
* Install + health-check the loopback daemon, then return the target the tools
|
|
628
853
|
* should be pointed at. On health-check success the tools point at the loopback
|
|
@@ -635,7 +860,7 @@ export const setupLoopbackProxy = async (target, port, deps = {}) => {
|
|
|
635
860
|
const probe = deps.probe ?? probeHealth;
|
|
636
861
|
const pollDelayMs = deps.pollDelayMs ?? 300;
|
|
637
862
|
heading("Seamless key refresh");
|
|
638
|
-
const svc = await install();
|
|
863
|
+
const svc = await install({ port, remoteOrigin: target.origin });
|
|
639
864
|
for (const m of svc.messages)
|
|
640
865
|
info(` ${m}`);
|
|
641
866
|
// A freshly-installed supervisor may take a moment to bind, so poll. But
|
|
@@ -660,9 +885,13 @@ export const setupLoopbackProxy = async (target, port, deps = {}) => {
|
|
|
660
885
|
? // Install succeeded but the port never bound — a transient miss the
|
|
661
886
|
// next run retries against the same supervisor.
|
|
662
887
|
" It will retry on the next run; check the service logs if it persists."
|
|
663
|
-
:
|
|
664
|
-
//
|
|
665
|
-
|
|
888
|
+
: isContainerBackend(detectRemoteDevBackend())
|
|
889
|
+
? // Container: the OS schedule is skipped too (no service manager), so
|
|
890
|
+
// don't promise one — the helper + re-runs are what refresh here.
|
|
891
|
+
" In a container the key refreshes via Claude Code's helper and on each `anyray-connect` run instead."
|
|
892
|
+
: // No supervised daemon on this platform (svc.messages said why); the
|
|
893
|
+
// scheduled ~30-min re-mint still keeps the seat's key fresh.
|
|
894
|
+
" Your key still refreshes on a schedule — no daemon needed here."));
|
|
666
895
|
return target;
|
|
667
896
|
};
|
|
668
897
|
/**
|
|
@@ -810,6 +1039,7 @@ export const run = async (argv) => {
|
|
|
810
1039
|
// it just runs the gated refresh and exits 0.
|
|
811
1040
|
if (argv[0] === "__anyray-refresh") {
|
|
812
1041
|
await refreshIfDue();
|
|
1042
|
+
await reconcileProxyService();
|
|
813
1043
|
// Same tick keeps the binary itself current (util/selfUpdate.ts). Runs
|
|
814
1044
|
// AFTER the key re-mint — that's the credential-critical half — and never
|
|
815
1045
|
// throws, so a GitHub outage can't cost a seat its key.
|
|
@@ -826,12 +1056,14 @@ export const run = async (argv) => {
|
|
|
826
1056
|
if (argv[0] === "__anyray-fallback-proxy") {
|
|
827
1057
|
return runRefreshProxy();
|
|
828
1058
|
}
|
|
829
|
-
// Hidden entrypoint:
|
|
830
|
-
// running at apply
|
|
1059
|
+
// Hidden entrypoint: a quit-gated adapter spawns this DETACHED when its app
|
|
1060
|
+
// was running at apply time. It waits for that app to quit, completes the
|
|
831
1061
|
// queued operation through the normal adapter path, and exits. Silent +
|
|
832
|
-
// best-effort (never throws, never prints).
|
|
833
|
-
|
|
834
|
-
|
|
1062
|
+
// best-effort (never throws, never prints). Takes the adapter id, so it
|
|
1063
|
+
// serves Cursor's exact-row write and Claude Desktop's profile migration
|
|
1064
|
+
// through one mechanism.
|
|
1065
|
+
if (argv[0] === "__anyray-quit-watch") {
|
|
1066
|
+
return runQuitWatch(argv.slice(1));
|
|
835
1067
|
}
|
|
836
1068
|
// Hidden credential-helper protocol for Claude Desktop subscription mode.
|
|
837
1069
|
// Dispatch before durable-runtime relaunch and all human-facing logging:
|
|
@@ -928,6 +1160,7 @@ export const run = async (argv) => {
|
|
|
928
1160
|
gateway: parsed.gateway,
|
|
929
1161
|
json: parsed.json,
|
|
930
1162
|
verify: parsed.verify,
|
|
1163
|
+
noVerify: parsed.noVerify,
|
|
931
1164
|
repair: parsed.repair,
|
|
932
1165
|
securityReview: parsed.securityReview,
|
|
933
1166
|
});
|
|
@@ -960,6 +1193,22 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
960
1193
|
fail("--revert cannot be combined with --sso or --enroll. It removes Anyray configuration; re-run with --sso <link> --yes (without --revert) to configure a new CLI.");
|
|
961
1194
|
return 1;
|
|
962
1195
|
}
|
|
1196
|
+
if (args.project) {
|
|
1197
|
+
// Project scope writes a machine-independent artifact: enrollment is
|
|
1198
|
+
// per-machine (--sso/--enroll) and seat/BYO lanes are per-dev credentials —
|
|
1199
|
+
// none of them can ride a committed file.
|
|
1200
|
+
if (args.sso || args.enroll || args.subscription || args.upstream) {
|
|
1201
|
+
fail("--project cannot be combined with --sso/--enroll (per-machine enrollment) or --subscription/--upstream (per-dev credential lanes). It writes an org-lane, credential-free repo config.");
|
|
1202
|
+
return 1;
|
|
1203
|
+
}
|
|
1204
|
+
return runProjectApply({
|
|
1205
|
+
gateway: args.gateway,
|
|
1206
|
+
tools: args.tools,
|
|
1207
|
+
revert: args.revert,
|
|
1208
|
+
dryRun: args.dryRun,
|
|
1209
|
+
yes: args.yes,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
963
1212
|
if (Boolean(args.upstream) !== Boolean(args.upstreamKey)) {
|
|
964
1213
|
fail("--upstream and --upstream-key go together: the URL says where your personal key is valid, the key is what the gateway forwards there.");
|
|
965
1214
|
return 1;
|
|
@@ -1307,6 +1556,17 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1307
1556
|
info(` Codex (ChatGPT): ${seat.openai
|
|
1308
1557
|
? cyan("signed in → your subscription")
|
|
1309
1558
|
: dim("not signed in → org key")}`);
|
|
1559
|
+
// Seat probes are machine-scoped. On a remote box with nothing signed in,
|
|
1560
|
+
// "falls back to the org key" is a silent trap: the developer's sign-ins are
|
|
1561
|
+
// on their laptop, and a subscription-only deployment has no working org key
|
|
1562
|
+
// for that lane to use — so every tool we configure here would 401. Say it
|
|
1563
|
+
// BEFORE the adapters write, so the fix is obvious rather than archaeology.
|
|
1564
|
+
if (!seat.anthropic && !seat.openai && !seat.copilot && isRemoteDevContext()) {
|
|
1565
|
+
warn(" No subscription sign-ins found on this host. Sign-ins do not cross the");
|
|
1566
|
+
warn(" Remote-SSH hop — your laptop's are invisible here, and these tools run");
|
|
1567
|
+
warn(" HERE. Sign in on this machine (`copilot` → /login, Claude Code, Codex)");
|
|
1568
|
+
warn(" and re-run, or everything below routes on the org key instead.");
|
|
1569
|
+
}
|
|
1310
1570
|
}
|
|
1311
1571
|
const target = resolveTarget({
|
|
1312
1572
|
origin,
|
|
@@ -1386,317 +1646,379 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1386
1646
|
}
|
|
1387
1647
|
ok(`Gateway reachable (${probe.detail}).`);
|
|
1388
1648
|
}
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const preserveKeyRefresh = scopedApply &&
|
|
1427
|
-
args.noKeyRefresh !== true &&
|
|
1428
|
-
Boolean(profile.keyRefreshEnabled);
|
|
1429
|
-
// The refresh SCHEDULER is the universal trigger: a per-user launchd/systemd/
|
|
1430
|
-
// Task Scheduler task that runs `__anyray-refresh` every ~30 min to re-mint
|
|
1431
|
-
// the short-TTL ark_ key for EVERY configured tool. The loopback daemon above
|
|
1432
|
-
// only covers
|
|
1433
|
-
// CC/Codex (their traffic traverses it); Cursor/Copilot/Claude-Desktop/
|
|
1434
|
-
// shell-env read a static key from their own config with no daemon or hook to
|
|
1435
|
-
// renew it, so they 401 mid-session when it lapses — the exact bug this fixes.
|
|
1436
|
-
// Installed for every ENROLLED seat (an env-key seat has no cert to refresh),
|
|
1437
|
-
// gated by the same --no-key-refresh opt-out the daemon lane uses. It coexists
|
|
1438
|
-
// with the daemon: the shared refresh-state throttle stops the two triggers
|
|
1439
|
-
// double-minting, and if the daemon is ever down the scheduler's refresh
|
|
1440
|
-
// degrades those seats' tools to gateway-direct with a fresh key.
|
|
1441
|
-
const usingEnvironmentKey = Boolean(envClientKey && target.clientKey === envClientKey && !enrolledCert);
|
|
1442
|
-
const hasRefreshableCert = !usingEnvironmentKey && Boolean(enrolledCert ?? profile.enrolledCert);
|
|
1443
|
-
const wantRefreshScheduler = !args.revert && args.noKeyRefresh !== true && hasRefreshableCert;
|
|
1444
|
-
let applyTarget = target;
|
|
1445
|
-
const resolvedKeyRefreshEnabled = wantKeyRefresh || preserveKeyRefresh;
|
|
1446
|
-
// Hold the port when the loopback is wanted now, or preserved for the tools
|
|
1447
|
-
// this scoped apply didn't name; otherwise clear it so the persisted profile
|
|
1448
|
-
// and the teardown below agree the daemon is gone.
|
|
1449
|
-
let resolvedFallbackPort = resolvedKeyRefreshEnabled
|
|
1450
|
-
? profile.fallbackPort
|
|
1451
|
-
: undefined;
|
|
1452
|
-
if (wantKeyRefresh && !args.dryRun) {
|
|
1453
|
-
resolvedFallbackPort = profile.fallbackPort ?? (await pickLoopbackPort());
|
|
1454
|
-
}
|
|
1455
|
-
// ACP agents and Claude Desktop's credential helper intentionally persist no
|
|
1456
|
-
// static key: they load this owner-only profile when the app launches them.
|
|
1457
|
-
// Commit the complete target + enrollment pair before any adapter or loopback
|
|
1458
|
-
// service can register such a launcher. A persistence failure is fatal here,
|
|
1459
|
-
// while no tool configuration has changed yet.
|
|
1460
|
-
if (!args.revert && !args.dryRun) {
|
|
1461
|
-
// An explicit pre-minted environment key is a complete, non-refreshable
|
|
1462
|
-
// credential source (see `usingEnvironmentKey` above). Never retain a
|
|
1463
|
-
// possibly unrelated enrollment cert or expiry beside it: a later helper
|
|
1464
|
-
// refresh could otherwise replace the operator-supplied identity with the
|
|
1465
|
-
// old certificate's identity.
|
|
1466
|
-
const nextProfile = {
|
|
1467
|
-
...profile,
|
|
1468
|
-
gateway: target.origin,
|
|
1469
|
-
name: resolvedUser,
|
|
1470
|
-
team: resolvedTeam,
|
|
1471
|
-
clientKey: target.clientKey ?? profile.clientKey,
|
|
1472
|
-
enrolledCert: usingEnvironmentKey
|
|
1473
|
-
? undefined
|
|
1474
|
-
: (enrolledCert ?? profile.enrolledCert),
|
|
1475
|
-
enrolledCertExpiry: usingEnvironmentKey
|
|
1476
|
-
? undefined
|
|
1477
|
-
: (enrolledCertExpiry ?? profile.enrolledCertExpiry),
|
|
1478
|
-
enrolledTokenHash: usingEnvironmentKey
|
|
1479
|
-
? undefined
|
|
1480
|
-
: (enrolledTokenHash ?? profile.enrolledTokenHash),
|
|
1481
|
-
ssoLink: usingEnvironmentKey
|
|
1482
|
-
? undefined
|
|
1483
|
-
: ssoEnrollment
|
|
1484
|
-
? enrolledSsoLink
|
|
1485
|
-
: enrolledCert
|
|
1486
|
-
? undefined
|
|
1487
|
-
: profile.ssoLink,
|
|
1488
|
-
clientKeyExpiresAt: usingEnvironmentKey
|
|
1489
|
-
? undefined
|
|
1490
|
-
: (clientKeyExpiresAt ??
|
|
1491
|
-
(target.clientKey === profile.clientKey?.trim()
|
|
1492
|
-
? profile.clientKeyExpiresAt
|
|
1493
|
-
: undefined)),
|
|
1494
|
-
authMode: target.authMode,
|
|
1495
|
-
byoUpstream: target.byoUpstream,
|
|
1496
|
-
keyRefreshEnabled: resolvedKeyRefreshEnabled,
|
|
1497
|
-
refreshSchedulerEnabled: wantRefreshScheduler,
|
|
1498
|
-
fallbackPort: resolvedFallbackPort,
|
|
1499
|
-
};
|
|
1500
|
-
try {
|
|
1501
|
-
await persistApplyProfile(profile, nextProfile, enrolledPrivateKeyPem, undefined, usingEnvironmentKey);
|
|
1502
|
-
}
|
|
1503
|
-
catch {
|
|
1504
|
-
fail("Could not save the owner-only Anyray runtime profile. No tool configuration was changed.");
|
|
1505
|
-
return 1;
|
|
1649
|
+
// Quit-aware apply, planned BEFORE anything is written and after the preflight
|
|
1650
|
+
// (no point closing an editor for a gateway that turned out to be down). The
|
|
1651
|
+
// plan only contains apps that are open and that this run actually changes —
|
|
1652
|
+
// see `planAppRestarts` for the "already pointed at us" filter and for what it
|
|
1653
|
+
// deliberately cannot see. Revert is out of scope: the filter is apply-shaped
|
|
1654
|
+
// (a revert has work precisely when the tool IS pointed at us), and the
|
|
1655
|
+
// adapters' queue-on-quit path already covers a revert that finds an app open.
|
|
1656
|
+
const quitPlan = args.revert
|
|
1657
|
+
? []
|
|
1658
|
+
: await planAppRestarts(adapters, target.origin);
|
|
1659
|
+
let closedByConnect = [];
|
|
1660
|
+
/**
|
|
1661
|
+
* Give back every app we took away — from ANY exit after the quit, not just
|
|
1662
|
+
* the happy one. Two failure paths below return before the apply finishes,
|
|
1663
|
+
* and each of them used to leave the developer's editors closed with nothing
|
|
1664
|
+
* ever reopening them. Closing someone's editor is only defensible if
|
|
1665
|
+
* reopening it is unconditional.
|
|
1666
|
+
*/
|
|
1667
|
+
const reopenClosed = () => {
|
|
1668
|
+
if (closedByConnect.length === 0)
|
|
1669
|
+
return;
|
|
1670
|
+
heading("Reopening");
|
|
1671
|
+
relaunchApps(closedByConnect);
|
|
1672
|
+
closedByConnect = [];
|
|
1673
|
+
};
|
|
1674
|
+
if (quitPlan.length > 0) {
|
|
1675
|
+
const titles = quitPlan.map((candidate) => candidate.title);
|
|
1676
|
+
const plural = titles.length === 1 ? "it" : "them";
|
|
1677
|
+
heading("Open apps");
|
|
1678
|
+
// One gate decides whether anything closes, so a path that must not act
|
|
1679
|
+
// cannot drift out of step with the one that may.
|
|
1680
|
+
if (!quitAppsAllowed(args, isInteractive())) {
|
|
1681
|
+
// Headless-complete: report, never act. Dry-run says what it would offer.
|
|
1682
|
+
const hint = args.noQuitApps === true ? "" : " (or pass --quit-apps)";
|
|
1683
|
+
note(args.dryRun
|
|
1684
|
+
? ` Would offer to quit ${titles.join(", ")} — dry run, nothing was closed.`
|
|
1685
|
+
: ` ${titles.join(", ")} ${titles.length === 1 ? "is" : "are"} open; restart ${plural} to pick this up${hint}.`);
|
|
1506
1686
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1687
|
+
else {
|
|
1688
|
+
// `--quit-apps` is consent already given; otherwise ask once, default NO.
|
|
1689
|
+
const agreed = args.quitApps === true || (await confirm(appRestartOffer(titles)));
|
|
1690
|
+
if (agreed)
|
|
1691
|
+
closedByConnect = await quitAppsNow(quitPlan);
|
|
1692
|
+
else
|
|
1693
|
+
note(` Leaving ${plural} open — restart when convenient.`);
|
|
1513
1694
|
}
|
|
1514
1695
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
if (await hasEnrollment()) {
|
|
1534
|
-
heading("Enrollment");
|
|
1535
|
-
info(" would remove the stored gateway credential");
|
|
1696
|
+
// Everything from here on runs with the developer's editors CLOSED, so
|
|
1697
|
+
// the reopen has to be in a `finally`. Guarding only the explicit early
|
|
1698
|
+
// returns was not enough: any throw in between — a filesystem error, an
|
|
1699
|
+
// adapter probe, the loopback setup — would unwind past it and leave
|
|
1700
|
+
// them shut. Closing someone's editor is only defensible if reopening
|
|
1701
|
+
// it happens on every path out.
|
|
1702
|
+
try {
|
|
1703
|
+
// Adapters now keep durable helper/journal files below ~/.anyray. Existing
|
|
1704
|
+
// global-install users may still have the historical plain-file profile at
|
|
1705
|
+
// that path, so migrate it once before any apply/revert mutation. Dry-run
|
|
1706
|
+
// stays strictly read-only.
|
|
1707
|
+
if (!args.dryRun) {
|
|
1708
|
+
try {
|
|
1709
|
+
await ensureStateDir();
|
|
1710
|
+
}
|
|
1711
|
+
catch {
|
|
1712
|
+
fail("Could not prepare the owner-only Anyray state directory.");
|
|
1713
|
+
return 1;
|
|
1536
1714
|
}
|
|
1537
1715
|
}
|
|
1538
|
-
|
|
1716
|
+
// Seamless key refresh: ON by default for subscription/passthrough Claude Code
|
|
1717
|
+
// + Codex — the only tools whose traffic actually traverses the loopback. They
|
|
1718
|
+
// bake a short-TTL client key as a STATIC header read once at launch, so
|
|
1719
|
+
// without the daemon a long session 401s the moment the key lapses mid-flight,
|
|
1720
|
+
// curable only by a re-run + a tool relaunch. The daemon's surface is
|
|
1721
|
+
// deliberately tiny (fill the empty key slot, forward the bytes unchanged) —
|
|
1722
|
+
// that is what lets it default on where the retired provider-direct lane could
|
|
1723
|
+
// not. --no-key-refresh forces the legacy direct static-key lane. Other tools
|
|
1724
|
+
// (native Cursor, the Copilot extension, ACP launchers) rebuild their own
|
|
1725
|
+
// target and bypass the loopback, so they are unaffected either way — see the
|
|
1726
|
+
// scope boundary in commands/refreshProxy.ts.
|
|
1727
|
+
const keyRefreshSeatSelected = (adapters.some((a) => a.id === "claude-code") &&
|
|
1728
|
+
effectiveSubscription(target, "anthropic")) ||
|
|
1729
|
+
(adapters.some((a) => a.id === "codex") &&
|
|
1730
|
+
effectiveSubscription(target, "openai"));
|
|
1731
|
+
const wantKeyRefresh = !args.revert && args.noKeyRefresh !== true && keyRefreshSeatSelected;
|
|
1732
|
+
// A scoped apply (`--tools x`) that doesn't itself engage the loopback must
|
|
1733
|
+
// not tear down the DEFAULT-ON key-refresh lane the tools it did NOT name
|
|
1734
|
+
// still depend on — the apply-side mirror of the scoped-revert credential rule
|
|
1735
|
+
// below. Without this, `anyray-connect --tools cursor` after a full connect
|
|
1736
|
+
// would uninstall the refresh daemon and drop Claude Code / Codex back to the
|
|
1737
|
+
// 401-prone static-key lane. An explicit --no-key-refresh still wins, and a
|
|
1738
|
+
// full, unscoped apply falls through to the normal want-driven teardown below.
|
|
1739
|
+
const scopedApply = !args.revert && (args.tools?.length ?? 0) > 0;
|
|
1740
|
+
const preserveKeyRefresh = scopedApply &&
|
|
1741
|
+
args.noKeyRefresh !== true &&
|
|
1742
|
+
Boolean(profile.keyRefreshEnabled);
|
|
1743
|
+
// The refresh SCHEDULER is the universal trigger: a per-user launchd/systemd/
|
|
1744
|
+
// Task Scheduler task that runs `__anyray-refresh` every ~30 min to re-mint
|
|
1745
|
+
// the short-TTL ark_ key for EVERY configured tool. The loopback daemon above
|
|
1746
|
+
// only covers
|
|
1747
|
+
// CC/Codex (their traffic traverses it); Cursor/Copilot/Claude-Desktop/
|
|
1748
|
+
// shell-env read a static key from their own config with no daemon or hook to
|
|
1749
|
+
// renew it, so they 401 mid-session when it lapses — the exact bug this fixes.
|
|
1750
|
+
// Installed for every ENROLLED seat (an env-key seat has no cert to refresh),
|
|
1751
|
+
// gated by the same --no-key-refresh opt-out the daemon lane uses. It coexists
|
|
1752
|
+
// with the daemon: the shared refresh-state throttle stops the two triggers
|
|
1753
|
+
// double-minting, and if the daemon is ever down the scheduler's refresh
|
|
1754
|
+
// degrades those seats' tools to gateway-direct with a fresh key.
|
|
1755
|
+
const usingEnvironmentKey = Boolean(envClientKey && target.clientKey === envClientKey && !enrolledCert);
|
|
1756
|
+
const hasRefreshableCert = !usingEnvironmentKey && Boolean(enrolledCert ?? profile.enrolledCert);
|
|
1757
|
+
const wantRefreshScheduler = !args.revert && args.noKeyRefresh !== true && hasRefreshableCert;
|
|
1758
|
+
let applyTarget = target;
|
|
1759
|
+
const resolvedKeyRefreshEnabled = wantKeyRefresh || preserveKeyRefresh;
|
|
1760
|
+
// Hold the port when the loopback is wanted now, or preserved for the tools
|
|
1761
|
+
// this scoped apply didn't name; otherwise clear it so the persisted profile
|
|
1762
|
+
// and the teardown below agree the daemon is gone.
|
|
1763
|
+
let resolvedFallbackPort = resolvedKeyRefreshEnabled
|
|
1764
|
+
? profile.fallbackPort
|
|
1765
|
+
: undefined;
|
|
1766
|
+
if (wantKeyRefresh && !args.dryRun) {
|
|
1767
|
+
resolvedFallbackPort = profile.fallbackPort ?? (await pickLoopbackPort());
|
|
1768
|
+
}
|
|
1769
|
+
// ACP agents and Claude Desktop's credential helper intentionally persist no
|
|
1770
|
+
// static key: they load this owner-only profile when the app launches them.
|
|
1771
|
+
// Commit the complete target + enrollment pair before any adapter or loopback
|
|
1772
|
+
// service can register such a launcher. A persistence failure is fatal here,
|
|
1773
|
+
// while no tool configuration has changed yet.
|
|
1774
|
+
if (!args.revert && !args.dryRun) {
|
|
1775
|
+
// An explicit pre-minted environment key is a complete, non-refreshable
|
|
1776
|
+
// credential source (see `usingEnvironmentKey` above). Never retain a
|
|
1777
|
+
// possibly unrelated enrollment cert or expiry beside it: a later helper
|
|
1778
|
+
// refresh could otherwise replace the operator-supplied identity with the
|
|
1779
|
+
// old certificate's identity.
|
|
1780
|
+
const nextProfile = {
|
|
1781
|
+
...profile,
|
|
1782
|
+
gateway: target.origin,
|
|
1783
|
+
name: resolvedUser,
|
|
1784
|
+
team: resolvedTeam,
|
|
1785
|
+
clientKey: target.clientKey ?? profile.clientKey,
|
|
1786
|
+
enrolledCert: usingEnvironmentKey
|
|
1787
|
+
? undefined
|
|
1788
|
+
: (enrolledCert ?? profile.enrolledCert),
|
|
1789
|
+
enrolledCertExpiry: usingEnvironmentKey
|
|
1790
|
+
? undefined
|
|
1791
|
+
: (enrolledCertExpiry ?? profile.enrolledCertExpiry),
|
|
1792
|
+
enrolledTokenHash: usingEnvironmentKey
|
|
1793
|
+
? undefined
|
|
1794
|
+
: (enrolledTokenHash ?? profile.enrolledTokenHash),
|
|
1795
|
+
ssoLink: usingEnvironmentKey
|
|
1796
|
+
? undefined
|
|
1797
|
+
: ssoEnrollment
|
|
1798
|
+
? enrolledSsoLink
|
|
1799
|
+
: enrolledCert
|
|
1800
|
+
? undefined
|
|
1801
|
+
: profile.ssoLink,
|
|
1802
|
+
clientKeyExpiresAt: usingEnvironmentKey
|
|
1803
|
+
? undefined
|
|
1804
|
+
: (clientKeyExpiresAt ??
|
|
1805
|
+
(target.clientKey === profile.clientKey?.trim()
|
|
1806
|
+
? profile.clientKeyExpiresAt
|
|
1807
|
+
: undefined)),
|
|
1808
|
+
authMode: target.authMode,
|
|
1809
|
+
byoUpstream: target.byoUpstream,
|
|
1810
|
+
keyRefreshEnabled: resolvedKeyRefreshEnabled,
|
|
1811
|
+
refreshSchedulerEnabled: wantRefreshScheduler,
|
|
1812
|
+
fallbackPort: resolvedFallbackPort,
|
|
1813
|
+
};
|
|
1539
1814
|
try {
|
|
1540
|
-
|
|
1541
|
-
heading("Enrollment");
|
|
1542
|
-
ok(" Removed the stored gateway credential.");
|
|
1543
|
-
note(" Reconnect any time with `npx anyray-connect@latest --sso`.");
|
|
1544
|
-
}
|
|
1815
|
+
await persistApplyProfile(profile, nextProfile, enrolledPrivateKeyPem, undefined, usingEnvironmentKey);
|
|
1545
1816
|
}
|
|
1546
|
-
catch
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1817
|
+
catch {
|
|
1818
|
+
fail("Could not save the owner-only Anyray runtime profile. No tool configuration was changed.");
|
|
1819
|
+
return 1;
|
|
1820
|
+
}
|
|
1821
|
+
// This run minted a key (--enroll / --sso). Share that with the
|
|
1822
|
+
// auto-refresher's bookkeeping: with no successful mint on record, the next
|
|
1823
|
+
// detached refresh sees a credential "not rotated in hours", re-mints
|
|
1824
|
+
// immediately, and rotates the key out from under the session just set up.
|
|
1825
|
+
if (clientKeyExpiresAt && !usingEnvironmentKey) {
|
|
1826
|
+
await recordMintSuccess({ expiresAt: clientKeyExpiresAt });
|
|
1554
1827
|
}
|
|
1555
1828
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
note("\nKeeping the stored enrollment — the tools you did not name still need it.");
|
|
1559
|
-
note("Run --revert with no --tools to remove it.");
|
|
1560
|
-
}
|
|
1561
|
-
// A named `--tools` list (the wizard writes its checklist here too) is the
|
|
1562
|
-
// developer choosing these adapters by hand; the default sweep names none.
|
|
1563
|
-
const { changes, manual, results } = await runApply(adapters, applyTarget, opts, args.revert, new Set(args.tools ?? []));
|
|
1564
|
-
// Keep the managed-tool record in step: apply records landed tools, revert
|
|
1565
|
-
// removes them, a full revert clears the record. Drift repair (`doctor
|
|
1566
|
-
// --repair`, the detached refresh) only ever re-applies recorded ids, so
|
|
1567
|
-
// this is what makes an intentional revert stick (#1031).
|
|
1568
|
-
if (!args.dryRun) {
|
|
1569
|
-
await recordManagedTools(results, args.revert, fullRevert).catch(() => undefined);
|
|
1570
|
-
}
|
|
1571
|
-
// Tear down the loopback daemon whenever it isn't wanted but a previous run
|
|
1572
|
-
// left it installed — covers an explicit --revert, --no-key-refresh, or a
|
|
1573
|
-
// subscription→api-key switch that no longer needs the refresh lane. Runs
|
|
1574
|
-
// AFTER the adapters above re-pointed tools straight at the gateway, so
|
|
1575
|
-
// uninstalling the loopback service strands nothing.
|
|
1576
|
-
if (!resolvedKeyRefreshEnabled &&
|
|
1577
|
-
!args.dryRun &&
|
|
1578
|
-
(loopbackEngaged(profile) || profile.fallbackPort)) {
|
|
1579
|
-
heading("Local loopback proxy");
|
|
1580
|
-
const svc = await uninstallProxyService(profile.fallbackPort);
|
|
1581
|
-
for (const m of svc.messages)
|
|
1582
|
-
info(` ${m}`);
|
|
1583
|
-
// Re-read: on a --revert `clearEnrollment` already stripped the credential
|
|
1584
|
-
// from disk, so spreading the pre-run `profile` snapshot would write it
|
|
1585
|
-
// straight back. loadProfile() reflects what is actually persisted now.
|
|
1586
|
-
await saveProfile({
|
|
1587
|
-
...(await loadProfile()),
|
|
1588
|
-
keyRefreshEnabled: false,
|
|
1589
|
-
fallbackPort: undefined,
|
|
1590
|
-
});
|
|
1591
|
-
ok(" Removed the local loopback proxy.");
|
|
1592
|
-
}
|
|
1593
|
-
// Reconcile the universal refresh scheduler: (re)install it for an enrolled
|
|
1594
|
-
// seat, or tear down one a prior run left when the seat no longer wants it (a
|
|
1595
|
-
// full revert dropped the enrollment, or --no-key-refresh opted out). A SCOPED
|
|
1596
|
-
// revert (`--tools x --revert`) leaves it — the tools it did not name still
|
|
1597
|
-
// depend on the refresh. Independent of the daemon teardown above: a
|
|
1598
|
-
// Cursor-only seat has the scheduler but never had a daemon.
|
|
1599
|
-
await reconcileRefreshScheduler({
|
|
1600
|
-
want: wantRefreshScheduler,
|
|
1601
|
-
announcePrimary: !wantKeyRefresh,
|
|
1602
|
-
scopedRevert: Boolean(args.revert) && !fullRevert,
|
|
1603
|
-
dryRun: Boolean(args.dryRun),
|
|
1604
|
-
wasEnabled: Boolean(profile.refreshSchedulerEnabled),
|
|
1605
|
-
});
|
|
1606
|
-
heading("Summary");
|
|
1607
|
-
if (args.dryRun) {
|
|
1608
|
-
note("Dry run complete — re-run without --dry-run to apply.");
|
|
1609
|
-
}
|
|
1610
|
-
else if (args.revert) {
|
|
1611
|
-
ok(`Reverted (${changes} file(s) touched).`);
|
|
1612
|
-
// Editors read our hooks at startup and keep them in memory, so one that was
|
|
1613
|
-
// already open goes on running them until it restarts. Say so — otherwise
|
|
1614
|
-
// the leftover hook lines read as a revert that missed something.
|
|
1615
|
-
note("Restart any editor or agent that was running — it loaded our hooks at startup.");
|
|
1616
|
-
// The same "gone from disk, still live in memory" trap, but for the shell
|
|
1617
|
-
// exports — and this one doesn't just look wrong, it BREAKS the dev's tools
|
|
1618
|
-
// (see tools/shellEnv `staleShellExports`). We can prove it: the vars are in
|
|
1619
|
-
// our own env. Say it loudly and hand over the exact command.
|
|
1620
|
-
const priorTargets = [
|
|
1621
|
-
target,
|
|
1622
|
-
...(profile.fallbackPort
|
|
1623
|
-
? [withLocalProxy(target, profile.fallbackPort)]
|
|
1624
|
-
: []),
|
|
1625
|
-
];
|
|
1626
|
-
const stale = staleShellExportsForTargets(priorTargets);
|
|
1627
|
-
if (stale.length > 0) {
|
|
1628
|
-
heading("This terminal still points at the gateway");
|
|
1629
|
-
warn(` Still exported here: ${stale.join(", ")}`);
|
|
1630
|
-
warn(" Removing the block from your shell profile cannot reach a shell");
|
|
1631
|
-
warn(" that already ran it. Until these are gone, your tools keep");
|
|
1632
|
-
warn(" reaching the gateway and now fail with 401.");
|
|
1633
|
-
info("\n Fix this terminal:");
|
|
1634
|
-
info(dim(` unset ${stale.join(" ")}`));
|
|
1635
|
-
info("\n Every other open terminal needs the same — or just open a new one.");
|
|
1636
|
-
info(" VS Code injects a copy it captured when IT launched, so its");
|
|
1637
|
-
info(" terminals stay stale even if you open a new one: quit the app");
|
|
1638
|
-
info(" entirely (Cmd+Q). Reloading the window keeps the old copy.");
|
|
1829
|
+
if (wantKeyRefresh && !args.dryRun) {
|
|
1830
|
+
applyTarget = await setupLoopbackProxy(target, resolvedFallbackPort);
|
|
1639
1831
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
//
|
|
1648
|
-
//
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1832
|
+
// Drop the enrollment credential FIRST, before a single config is unpicked —
|
|
1833
|
+
// see util/persist `clearEnrollment` for why a revert that leaves it behind
|
|
1834
|
+
// undoes itself. Order matters: `refreshIfDue` re-reads the profile on every
|
|
1835
|
+
// run, so clearing up front means a hook that fires *during* the revert finds
|
|
1836
|
+
// nothing to mint and leaves our work alone. Clearing last would leave a
|
|
1837
|
+
// window — config already removed, cert still live — where that hook restores
|
|
1838
|
+
// the configs and we then strip the cert they now depend on, stranding the
|
|
1839
|
+
// tools on a key that can never refresh.
|
|
1840
|
+
//
|
|
1841
|
+
// A partial revert (`--tools`) is scoped to the tools it names, so it must
|
|
1842
|
+
// leave the credential alone: the tools it did NOT touch still need to
|
|
1843
|
+
// refresh their key.
|
|
1844
|
+
const fullRevert = args.revert && (args.tools?.length ?? 0) === 0;
|
|
1845
|
+
if (fullRevert) {
|
|
1846
|
+
if (args.dryRun) {
|
|
1847
|
+
if (await hasEnrollment()) {
|
|
1848
|
+
heading("Enrollment");
|
|
1849
|
+
info(" would remove the stored gateway credential");
|
|
1657
1850
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1851
|
+
}
|
|
1852
|
+
else {
|
|
1853
|
+
try {
|
|
1854
|
+
if (await clearEnrollment()) {
|
|
1855
|
+
heading("Enrollment");
|
|
1856
|
+
ok(" Removed the stored gateway credential.");
|
|
1857
|
+
note(" Reconnect any time with `npx anyray-connect@latest --sso`.");
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
catch (error) {
|
|
1861
|
+
// Local disk failure — say so plainly and keep going: the tool configs
|
|
1862
|
+
// below still come out, and a half-reverted install is worth more than
|
|
1863
|
+
// a stack trace. Never echo the error value (it can quote a path).
|
|
1864
|
+
heading("Enrollment");
|
|
1865
|
+
warn(" Could not remove the stored gateway credential.");
|
|
1866
|
+
warn(` Delete ${stateDir()} by hand, or the key-refresh hook may restore your tool config.`);
|
|
1867
|
+
void error;
|
|
1660
1868
|
}
|
|
1661
1869
|
}
|
|
1662
1870
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
note("\nSkipped the connection smoke test (--skip-verify).");
|
|
1871
|
+
else if (args.revert && !args.dryRun) {
|
|
1872
|
+
note("\nKeeping the stored enrollment — the tools you did not name still need it.");
|
|
1873
|
+
note("Run --revert with no --tools to remove it.");
|
|
1667
1874
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1875
|
+
// A named `--tools` list (the wizard writes its checklist here too) is the
|
|
1876
|
+
// developer choosing these adapters by hand; the default sweep names none.
|
|
1877
|
+
const { changes, manual, results } = await runApply(adapters, applyTarget, opts, args.revert, new Set(args.tools ?? []));
|
|
1878
|
+
// Keep the managed-tool record in step: apply records landed tools, revert
|
|
1879
|
+
// removes them, a full revert clears the record. Drift repair (`doctor
|
|
1880
|
+
// --repair`, the detached refresh) only ever re-applies recorded ids, so
|
|
1881
|
+
// this is what makes an intentional revert stick (#1031).
|
|
1882
|
+
if (!args.dryRun) {
|
|
1883
|
+
await recordManagedTools(results, args.revert, fullRevert).catch(() => undefined);
|
|
1884
|
+
}
|
|
1885
|
+
// Tear down the loopback daemon whenever it isn't wanted but a previous run
|
|
1886
|
+
// left it installed — covers an explicit --revert, --no-key-refresh, or a
|
|
1887
|
+
// subscription→api-key switch that no longer needs the refresh lane. Runs
|
|
1888
|
+
// AFTER the adapters above re-pointed tools straight at the gateway, so
|
|
1889
|
+
// uninstalling the loopback service strands nothing.
|
|
1890
|
+
if (!resolvedKeyRefreshEnabled &&
|
|
1891
|
+
!args.dryRun &&
|
|
1892
|
+
(loopbackEngaged(profile) || profile.fallbackPort)) {
|
|
1893
|
+
heading("Local loopback proxy");
|
|
1894
|
+
const svc = await uninstallProxyService(profile.fallbackPort);
|
|
1895
|
+
for (const m of svc.messages)
|
|
1896
|
+
info(` ${m}`);
|
|
1897
|
+
// Re-read: on a --revert `clearEnrollment` already stripped the credential
|
|
1898
|
+
// from disk, so spreading the pre-run `profile` snapshot would write it
|
|
1899
|
+
// straight back. loadProfile() reflects what is actually persisted now.
|
|
1900
|
+
await saveProfile({
|
|
1901
|
+
...(await loadProfile()),
|
|
1902
|
+
keyRefreshEnabled: false,
|
|
1903
|
+
fallbackPort: undefined,
|
|
1904
|
+
});
|
|
1905
|
+
ok(" Removed the local loopback proxy.");
|
|
1906
|
+
}
|
|
1907
|
+
// Reconcile the universal refresh scheduler: (re)install it for an enrolled
|
|
1908
|
+
// seat, or tear down one a prior run left when the seat no longer wants it (a
|
|
1909
|
+
// full revert dropped the enrollment, or --no-key-refresh opted out). A SCOPED
|
|
1910
|
+
// revert (`--tools x --revert`) leaves it — the tools it did not name still
|
|
1911
|
+
// depend on the refresh. Independent of the daemon teardown above: a
|
|
1912
|
+
// Cursor-only seat has the scheduler but never had a daemon.
|
|
1913
|
+
await reconcileRefreshScheduler({
|
|
1914
|
+
want: wantRefreshScheduler,
|
|
1915
|
+
announcePrimary: !wantKeyRefresh,
|
|
1916
|
+
scopedRevert: Boolean(args.revert) && !fullRevert,
|
|
1917
|
+
dryRun: Boolean(args.dryRun),
|
|
1918
|
+
wasEnabled: Boolean(profile.refreshSchedulerEnabled),
|
|
1919
|
+
});
|
|
1920
|
+
// Reopen here on the happy path so it reads in the right order, before the
|
|
1921
|
+
// summary. `reopenClosed` clears its list, so the `finally` below is a
|
|
1922
|
+
// no-op once this has run — it exists for the paths that never reach here.
|
|
1923
|
+
reopenClosed();
|
|
1924
|
+
heading("Summary");
|
|
1925
|
+
if (args.dryRun) {
|
|
1926
|
+
note("Dry run complete — re-run without --dry-run to apply.");
|
|
1676
1927
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1928
|
+
else if (args.revert) {
|
|
1929
|
+
ok(`Reverted (${changes} file(s) touched).`);
|
|
1930
|
+
// Editors read our hooks at startup and keep them in memory, so one that was
|
|
1931
|
+
// already open goes on running them until it restarts. Say so — otherwise
|
|
1932
|
+
// the leftover hook lines read as a revert that missed something.
|
|
1933
|
+
note("Restart any editor or agent that was running — it loaded our hooks at startup.");
|
|
1934
|
+
// The same "gone from disk, still live in memory" trap, but for the shell
|
|
1935
|
+
// exports — and this one doesn't just look wrong, it BREAKS the dev's tools
|
|
1936
|
+
// (see tools/shellEnv `staleShellExports`). We can prove it: the vars are in
|
|
1937
|
+
// our own env. Say it loudly and hand over the exact command.
|
|
1938
|
+
const priorTargets = [
|
|
1939
|
+
target,
|
|
1940
|
+
...(profile.fallbackPort
|
|
1941
|
+
? [withLocalProxy(target, profile.fallbackPort)]
|
|
1942
|
+
: []),
|
|
1943
|
+
];
|
|
1944
|
+
const stale = staleShellExportsForTargets(priorTargets);
|
|
1945
|
+
if (stale.length > 0) {
|
|
1946
|
+
heading("This terminal still points at the gateway");
|
|
1947
|
+
warn(` Still exported here: ${stale.join(", ")}`);
|
|
1948
|
+
warn(" Removing the block from your shell profile cannot reach a shell");
|
|
1949
|
+
warn(" that already ran it. Until these are gone, your tools keep");
|
|
1950
|
+
warn(" reaching the gateway and now fail with 401.");
|
|
1951
|
+
info("\n Fix this terminal:");
|
|
1952
|
+
info(dim(` unset ${stale.join(" ")}`));
|
|
1953
|
+
info("\n Every other open terminal needs the same — or just open a new one.");
|
|
1954
|
+
info(" VS Code injects a copy it captured when IT launched, so its");
|
|
1955
|
+
info(" terminals stay stale even if you open a new one: quit the app");
|
|
1956
|
+
info(" entirely (Cmd+Q). Reloading the window keeps the old copy.");
|
|
1957
|
+
}
|
|
1685
1958
|
}
|
|
1686
1959
|
else {
|
|
1687
|
-
|
|
1688
|
-
//
|
|
1689
|
-
|
|
1690
|
-
//
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1960
|
+
ok(`Done (${changes} file(s) written).`);
|
|
1961
|
+
// Tools whose config we can't script (Claude desktop, JetBrains, Windsurf)
|
|
1962
|
+
// are the easiest thing to overlook — the loop printed their steps, but a
|
|
1963
|
+
// dev skimming for the green "Done" can miss that no file was actually
|
|
1964
|
+
// written for them. Re-surface them loudly, and on an interactive run make
|
|
1965
|
+
// the dev explicitly acknowledge before we move on. Scripted runs
|
|
1966
|
+
// (--yes / non-TTY) just get the reminder — never a hanging prompt.
|
|
1967
|
+
if (manual.length > 0) {
|
|
1968
|
+
heading("Action required — manual steps");
|
|
1969
|
+
for (const line of manualStepsNotice(manual))
|
|
1970
|
+
warn(line);
|
|
1971
|
+
if (isInteractive() && !args.yes) {
|
|
1972
|
+
const acknowledged = await confirm("Have you applied the manual settings shown above?");
|
|
1973
|
+
if (acknowledged) {
|
|
1974
|
+
ok("Acknowledged.");
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
warn("Apply them before using the tool, or it will keep sending traffic straight to the provider (unattributed).");
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
// Smoke-test the connection so a routing/sign-in problem surfaces NOW with a
|
|
1982
|
+
// concrete fix, not as a cryptic 4xx on the dev's first real request.
|
|
1983
|
+
if (args.skipVerify) {
|
|
1984
|
+
note("\nSkipped the connection smoke test (--skip-verify).");
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
// MUST probe `target` (the remote gateway), NOT `applyTarget` (the local
|
|
1988
|
+
// key-refresh daemon the tools were pointed at). The verify classifiers
|
|
1989
|
+
// sniff response bodies, and a daemon-synthesized 502 ("gateway
|
|
1990
|
+
// unreachable") would confuse them into reporting a routing fault the
|
|
1991
|
+
// gateway itself never returned. The daemon hop is health-checked
|
|
1992
|
+
// separately at install time (and by `doctor`).
|
|
1993
|
+
await runVerify(target, adapters.map((a) => a.id));
|
|
1994
|
+
}
|
|
1995
|
+
info("\nRe-test by hand any time:");
|
|
1996
|
+
const nativeCursorOnly = target.authMode !== "placeholder" &&
|
|
1997
|
+
adapters.length === 1 &&
|
|
1998
|
+
adapters[0]?.id === "cursor";
|
|
1999
|
+
if (nativeCursorOnly) {
|
|
2000
|
+
info(dim(` curl -sS ${target.origin}/connect/savings \\\n` +
|
|
2001
|
+
` -H "x-anyray-api-key: $(anyray-connect print-key)"`));
|
|
2002
|
+
info(dim(" Validate Opus itself inside Cursor: keep the Team login, select a native Claude Opus model, and run an Agent prompt."));
|
|
2003
|
+
}
|
|
2004
|
+
else {
|
|
2005
|
+
// A minimal chat completion is the one request every API-routed gateway
|
|
2006
|
+
// serves. `anyray-default` lets gateway policy choose model/provider.
|
|
2007
|
+
info(dim(
|
|
2008
|
+
// Never echo a personal key — point at the tool config it landed in.
|
|
2009
|
+
` curl -sS ${target.openaiBaseUrl}/chat/completions \\\n` +
|
|
2010
|
+
` -H "Content-Type: application/json" \\\n` +
|
|
2011
|
+
` -H "Authorization: Bearer ${target.clientKey
|
|
2012
|
+
? `<your personal key (${maskKey(target.clientKey)}) — see your tool config>`
|
|
2013
|
+
: "<enrollment required>"}" \\\n` +
|
|
2014
|
+
` -d '{"model":"anyray-default","messages":[{"role":"user","content":"ping"}],"max_tokens":16}'`));
|
|
2015
|
+
}
|
|
2016
|
+
info(dim(" Revert: npx anyray-connect --revert"));
|
|
1697
2017
|
}
|
|
1698
|
-
|
|
2018
|
+
return 0;
|
|
2019
|
+
}
|
|
2020
|
+
finally {
|
|
2021
|
+
reopenClosed();
|
|
1699
2022
|
}
|
|
1700
|
-
return 0;
|
|
1701
2023
|
};
|
|
1702
2024
|
//# sourceMappingURL=cli.js.map
|