anyray-connect 0.11.89 → 0.11.91
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 +364 -103
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.js +38 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.js +4 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcpServer.js +75 -7
- package/dist/commands/mcpServer.js.map +1 -1
- package/dist/commands/shared.js +38 -18
- package/dist/commands/shared.js.map +1 -1
- package/dist/tools/claudeDesktop.js +3 -0
- package/dist/tools/claudeDesktop.js.map +1 -1
- package/dist/tools/copilotChat.js +39 -1
- package/dist/tools/copilotChat.js.map +1 -1
- package/dist/tools/cursor.js +22 -12
- package/dist/tools/cursor.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/appQuit.js +44 -4
- package/dist/util/appQuit.js.map +1 -1
- package/dist/util/connectHealthReport.js +32 -1
- package/dist/util/connectHealthReport.js.map +1 -1
- package/dist/util/devVerify.js +9 -1
- package/dist/util/devVerify.js.map +1 -1
- package/dist/util/directHeartbeatMigration.js +2 -3
- package/dist/util/directHeartbeatMigration.js.map +1 -1
- package/dist/util/enrollLink.js +1 -0
- package/dist/util/enrollLink.js.map +1 -1
- package/dist/util/fleetPlan.js +67 -0
- package/dist/util/fleetPlan.js.map +1 -0
- package/dist/util/identity.js +26 -0
- package/dist/util/identity.js.map +1 -1
- package/dist/util/log.js +11 -4
- package/dist/util/log.js.map +1 -1
- package/dist/util/multiSelect.js +180 -0
- package/dist/util/multiSelect.js.map +1 -0
- package/dist/util/policySync.js +21 -51
- package/dist/util/policySync.js.map +1 -1
- package/dist/util/proxyService.js +13 -0
- package/dist/util/proxyService.js.map +1 -1
- package/dist/util/retrieval.js +38 -3
- package/dist/util/retrieval.js.map +1 -1
- package/dist/util/spinner.js +32 -0
- package/dist/util/spinner.js.map +1 -0
- package/dist/util/verify.js +87 -1
- package/dist/util/verify.js.map +1 -1
- package/dist/util/vscodeExtension.js +32 -0
- package/dist/util/vscodeExtension.js.map +1 -1
- package/dist/util/vscodeProcess.js +7 -0
- package/dist/util/vscodeProcess.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
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, DEFAULT_LOOPBACK_PORT, } from "./util/proxyService.js";
|
|
13
|
+
import { pickLoopbackPort, installProxyService, uninstallProxyService, probeHealth, proxyUnitFileExists, DEFAULT_LOOPBACK_PORT, } from "./util/proxyService.js";
|
|
14
|
+
import { stat } from "node:fs/promises";
|
|
14
15
|
import { isDraining } from "./util/drainMarker.js";
|
|
15
16
|
import { installRefreshScheduler, uninstallRefreshScheduler, } from "./util/refreshScheduler.js";
|
|
16
17
|
import { detectRemoteDevBackend, isContainerBackend, } from "./util/remoteContext.js";
|
|
@@ -32,8 +33,10 @@ import { checkGateway } from "./util/preflight.js";
|
|
|
32
33
|
import { maskKey } from "./util/redeem.js";
|
|
33
34
|
import { parseEnrollLink, isEnrollArg, resolveEnrollLink, } from "./util/enrollLink.js";
|
|
34
35
|
import { canReuseEnrolledCert, enrollAndVerify, enrollTokenHash, GatewayVerifyError, STABLE_KEY_HEARTBEAT_CAPABILITY, verifyWithGateway, } from "./util/devVerify.js";
|
|
35
|
-
import {
|
|
36
|
+
import { detectEnrollmentIdentity, } from "./util/identity.js";
|
|
36
37
|
import { parseSsoLink, ssoEnroll } from "./util/ssoEnroll.js";
|
|
38
|
+
import { canMultiSelect, multiSelect } from "./util/multiSelect.js";
|
|
39
|
+
import { withSpinner } from "./util/spinner.js";
|
|
37
40
|
import { recordMintSuccess, refreshIfDue, repathManagedTools, reserveMint, } from "./util/devRefresh.js";
|
|
38
41
|
import { runSelfUpdate } from "./util/selfUpdate.js";
|
|
39
42
|
import { migrateToDirectHeartbeat } from "./util/directHeartbeatMigration.js";
|
|
@@ -42,10 +45,10 @@ import { staleShellExportsForTargets } from "./tools/shellEnv.js";
|
|
|
42
45
|
import { effectiveSubscription, metadataHeaderValue } from "./types.js";
|
|
43
46
|
import { quitTargetKey, quitTargetLabel, relaunchResolvable, quitTargetSupported, relaunchApp, requestGracefulQuit, waitForQuit, } from "./util/appQuit.js";
|
|
44
47
|
import { bold, cyan, dim, fail, heading, info, note, ok, warn, } from "./util/log.js";
|
|
45
|
-
import { clearDevKey, clearEnrollment, hasEnrollment, loadProfile, recordManagedTools, saveProfile, saveDevKey, loadDevKey, stateDir, ensureStateDir, purgeRetiredFallbackState, } from "./util/persist.js";
|
|
48
|
+
import { clearDevKey, clearEnrollment, hasEnrollment, loadProfile, profilePath, recordManagedTools, saveProfile, saveDevKey, loadDevKey, stateDir, ensureStateDir, purgeRetiredFallbackState, } from "./util/persist.js";
|
|
46
49
|
import { revokeDeviceToken } from "./util/deviceToken.js";
|
|
47
50
|
import { isFleetLockedTool } from "./util/policySync.js";
|
|
48
|
-
import { confirm, isInteractive, prompt, resolveDisplayName, resolveTeam, } from "./commands/shared.js";
|
|
51
|
+
import { confirm, isInteractive, PromptCancelledError, prompt, resolveDisplayName, resolveTeam, } from "./commands/shared.js";
|
|
49
52
|
import { verifyConnection } from "./util/verify.js";
|
|
50
53
|
import { detectSeatState } from "./util/seatState.js";
|
|
51
54
|
import { isRemoteDevContext } from "./tools/copilotChat.js";
|
|
@@ -71,6 +74,42 @@ import { CLAUDE_DESKTOP_CREDENTIAL_ARG, } from "./tools/claudeDesktopCredentialP
|
|
|
71
74
|
import { platform } from "node:os";
|
|
72
75
|
const HELP = `${bold("anyray-connect")} — point your local coding tools at the Anyray gateway
|
|
73
76
|
|
|
77
|
+
${bold("Usage")}
|
|
78
|
+
npx anyray-connect [options]
|
|
79
|
+
npx anyray-connect <gateway-url> [options]
|
|
80
|
+
|
|
81
|
+
${bold("Subcommands")}
|
|
82
|
+
login Sign in through browser SSO
|
|
83
|
+
wizard Interactive guided setup (also the default on a TTY)
|
|
84
|
+
status Show the current gateway and connected tools
|
|
85
|
+
doctor Diagnose gateway reachability and tool configuration
|
|
86
|
+
sync Pull the team's shared skills and instructions
|
|
87
|
+
update Update Connect or configure automatic updates
|
|
88
|
+
|
|
89
|
+
${bold("Options")}
|
|
90
|
+
--gateway <url> Gateway origin
|
|
91
|
+
--enroll <link> Connect with a passwordless enrollment link or token
|
|
92
|
+
--sso <link> Connect with an enterprise SSO link or tenant id
|
|
93
|
+
--tools <ids> Comma-separated subset: ${REGISTRY.map((t) => t.id).join(", ")}
|
|
94
|
+
--user <id> Attribute spend to this user
|
|
95
|
+
--team <id> Attribute spend to this team
|
|
96
|
+
--dry-run Show what would change without writing anything
|
|
97
|
+
--yes, -y Don't prompt for confirmation (for scripted rollout)
|
|
98
|
+
--revert Undo a previous run and clear stored enrollment
|
|
99
|
+
--verbose Show the curl re-test recipe and revert hint
|
|
100
|
+
--help, -h Show this help
|
|
101
|
+
|
|
102
|
+
Run \`npx anyray-connect --help --all\` for every command and option.
|
|
103
|
+
|
|
104
|
+
${bold("What it does")}
|
|
105
|
+
Writes each tool's supported routing config plus the enrolled developer's
|
|
106
|
+
personal gateway key so requests flow through the gateway. The real provider
|
|
107
|
+
key stays server-side (ANYRAY_PROVIDER_KEY_*); your tools never hold it. Only
|
|
108
|
+
metadata (user/team) is attached — never prompt content.
|
|
109
|
+
~/.anyray (mode 600) remembers your gateway, name/team, personal key, and the
|
|
110
|
+
per-skill versions ${dim("sync")} last wrote — for zero-question re-runs.`;
|
|
111
|
+
const HELP_ALL = `${bold("anyray-connect")} — point your local coding tools at the Anyray gateway
|
|
112
|
+
|
|
74
113
|
${bold("Usage")}
|
|
75
114
|
npx anyray-connect <gateway-url> [options]
|
|
76
115
|
npx anyray-connect [options] (gateway from $ANYRAY_GATEWAY_URL, the ~/.anyray cache, or http://localhost:8787)
|
|
@@ -224,6 +263,7 @@ ${bold("Options")}
|
|
|
224
263
|
clear the stored enrollment. Reconnecting needs --sso (or a
|
|
225
264
|
fresh --enroll link). Cannot be combined with --sso or
|
|
226
265
|
--enroll. Restart any editor that was running.
|
|
266
|
+
--verbose Show the curl re-test recipe and revert hint after apply
|
|
227
267
|
--help, -h Show this help
|
|
228
268
|
|
|
229
269
|
${bold("What it does")}
|
|
@@ -237,16 +277,16 @@ ${bold("What it does")}
|
|
|
237
277
|
* The identity submitted with an `--enroll` redemption. A SHARED link binds no
|
|
238
278
|
* identity, and the control plane refuses an identity-less redemption (it has
|
|
239
279
|
* nothing to put in the DevCert / attribute spend to) — so when `--user` is
|
|
240
|
-
* absent,
|
|
241
|
-
*
|
|
280
|
+
* absent, use an email-first automatic identity with a human-readable
|
|
281
|
+
* fallback. Personal links ignore this server-side (pre-bound identity wins),
|
|
242
282
|
* and connect adopts the cert's user afterwards, so roster, cert, and spend
|
|
243
|
-
* attribution all carry one identity. `--user ''` is the documented
|
|
244
|
-
* sentinel (and `?? ` would pass it through), so blank values fall
|
|
245
|
-
* the CP refuses an identity-less shared redemption.
|
|
283
|
+
* attribution all carry one verified identity. `--user ''` is the documented
|
|
284
|
+
* cache-clear sentinel (and `?? ` would pass it through), so blank values fall
|
|
285
|
+
* back too — the CP refuses an identity-less shared redemption.
|
|
246
286
|
*/
|
|
247
287
|
export const enrollIdentity = (user) => {
|
|
248
288
|
const explicit = user?.trim();
|
|
249
|
-
return explicit ? explicit :
|
|
289
|
+
return explicit ? explicit : detectEnrollmentIdentity();
|
|
250
290
|
};
|
|
251
291
|
export const parseArgs = (argv) => {
|
|
252
292
|
const args = {
|
|
@@ -257,7 +297,9 @@ export const parseArgs = (argv) => {
|
|
|
257
297
|
revert: false,
|
|
258
298
|
force: false,
|
|
259
299
|
skipVerify: false,
|
|
300
|
+
verbose: false,
|
|
260
301
|
help: false,
|
|
302
|
+
helpAll: false,
|
|
261
303
|
};
|
|
262
304
|
for (let i = 0; i < argv.length; i++) {
|
|
263
305
|
const a = argv[i];
|
|
@@ -362,10 +404,23 @@ export const parseArgs = (argv) => {
|
|
|
362
404
|
case "--security-review":
|
|
363
405
|
args.securityReview = true;
|
|
364
406
|
break;
|
|
407
|
+
case "--verbose":
|
|
408
|
+
args.verbose = true;
|
|
409
|
+
break;
|
|
365
410
|
case "--help":
|
|
366
411
|
case "-h":
|
|
367
412
|
args.help = true;
|
|
368
413
|
break;
|
|
414
|
+
case "--help-all":
|
|
415
|
+
args.help = true;
|
|
416
|
+
args.helpAll = true;
|
|
417
|
+
break;
|
|
418
|
+
case "--all":
|
|
419
|
+
if (!argv.includes("--help") && !argv.includes("-h")) {
|
|
420
|
+
throw new Error(`unknown option: ${a}`);
|
|
421
|
+
}
|
|
422
|
+
args.helpAll = true;
|
|
423
|
+
break;
|
|
369
424
|
default:
|
|
370
425
|
// First non-flag token is the gateway URL; a second positional is an error.
|
|
371
426
|
if (!a.startsWith("-") && args.gateway === undefined) {
|
|
@@ -440,6 +495,18 @@ const printTarget = (target, adapters) => {
|
|
|
440
495
|
}
|
|
441
496
|
info(` Attribution: ${metadataHeaderValue(target.metadata) ?? dim("(none — pass --user)")}`);
|
|
442
497
|
};
|
|
498
|
+
const printSelectedTools = async (adapters, mode) => {
|
|
499
|
+
heading("Selected tools");
|
|
500
|
+
for (const adapter of adapters) {
|
|
501
|
+
const detected = await adapter.detect();
|
|
502
|
+
if (detected.installed) {
|
|
503
|
+
info(` ${adapter.title} ${dim(`(${adapter.id})`)} — ${mode === "plan" ? "will configure" : "would configure after enrollment"}${detected.detail ? `; ${detected.detail}` : ""}`);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
note(` ${adapter.title} ${dim(`(${adapter.id})`)} — not found${detected.detail ? `; ${detected.detail}` : ""}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
443
510
|
/**
|
|
444
511
|
* Reuse a cached key only when it has the required `ark_` shape and its locally
|
|
445
512
|
* recorded expiry, when present, is still in the future. Older profiles did not
|
|
@@ -521,13 +588,14 @@ explicitIds) => {
|
|
|
521
588
|
let changes = 0;
|
|
522
589
|
const manual = [];
|
|
523
590
|
const results = [];
|
|
591
|
+
const skipped = [];
|
|
524
592
|
for (const adapter of adapters) {
|
|
525
593
|
const detected = await adapter.detect();
|
|
526
|
-
heading(`${adapter.title} ${dim(`(${adapter.id})`)}`);
|
|
527
594
|
if (!detected.installed && !revert) {
|
|
528
|
-
|
|
595
|
+
skipped.push(adapter.title);
|
|
529
596
|
continue;
|
|
530
597
|
}
|
|
598
|
+
heading(`${adapter.title} ${dim(`(${adapter.id})`)}`);
|
|
531
599
|
try {
|
|
532
600
|
const result = revert
|
|
533
601
|
? await adapter.revert(opts)
|
|
@@ -567,6 +635,9 @@ explicitIds) => {
|
|
|
567
635
|
fail(` failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
568
636
|
}
|
|
569
637
|
}
|
|
638
|
+
if (skipped.length > 0) {
|
|
639
|
+
note(`Skipped ${skipped.length} not installed (${skipped.join(", ")})`);
|
|
640
|
+
}
|
|
570
641
|
// Upgrade cleanup, deliberately AFTER the adapters: every managed tool now
|
|
571
642
|
// names the current runtime, so the copies left by earlier npx-launched
|
|
572
643
|
// versions are unreferenced and can go. Nothing ever reaped them before, so an
|
|
@@ -752,55 +823,154 @@ export const relaunchApps = (closed, deps = {}) => {
|
|
|
752
823
|
* which the adapter must be named to get (see `ToolAdapter.changesBilling`).
|
|
753
824
|
*/
|
|
754
825
|
export const wizardSuggests = (adapter) => adapter.changesBilling !== true;
|
|
755
|
-
/**
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
* `wizard --user x` stays terse. The apply flow still does the real work (and
|
|
763
|
-
* its own preflight + persistence) — the wizard only gathers input.
|
|
764
|
-
*/
|
|
765
|
-
const runWizard = async (args, profile) => {
|
|
826
|
+
/** Gather first-run choices without writing configuration. */
|
|
827
|
+
export const runWizard = async (args, profile, deps = {}) => {
|
|
828
|
+
const enrollmentPresent = deps.hasEnrollment ?? hasEnrollment;
|
|
829
|
+
const supportsMultiSelect = deps.canMultiSelect ?? canMultiSelect;
|
|
830
|
+
const selectMany = deps.multiSelect ?? multiSelect;
|
|
831
|
+
const ask = deps.prompt ?? prompt;
|
|
832
|
+
const registry = deps.registry ?? REGISTRY;
|
|
766
833
|
info(bold("Anyray connect — guided setup"));
|
|
767
834
|
info(dim("Point your local coding tools at the Anyray gateway. Ctrl-C to abort."));
|
|
768
|
-
|
|
835
|
+
if (await enrollmentPresent()) {
|
|
836
|
+
info("Stored enrollment will be reused. Run with --revert to clear it.");
|
|
837
|
+
}
|
|
838
|
+
else if (!args.enroll && !args.sso && !args.gateway) {
|
|
839
|
+
heading("How do you want to connect?");
|
|
840
|
+
let method;
|
|
841
|
+
for (let attempt = 0; attempt < 3 && !method; attempt++) {
|
|
842
|
+
if (supportsMultiSelect()) {
|
|
843
|
+
const outcome = await selectMany({
|
|
844
|
+
title: "Choose a connection method",
|
|
845
|
+
single: true,
|
|
846
|
+
items: [
|
|
847
|
+
{ value: "enroll", label: "Paste an enrollment link", selected: true },
|
|
848
|
+
{ value: "sso", label: "Sign in with SSO" },
|
|
849
|
+
{ value: "gateway", label: "I already have a gateway URL" },
|
|
850
|
+
],
|
|
851
|
+
});
|
|
852
|
+
if (outcome.kind === "cancelled")
|
|
853
|
+
throw new PromptCancelledError();
|
|
854
|
+
method = outcome.values[0];
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
info(" 1. Paste an enrollment link");
|
|
858
|
+
info(" 2. Sign in with SSO");
|
|
859
|
+
info(" 3. I already have a gateway URL");
|
|
860
|
+
const choice = await ask("? Connection method (1-3)", "1");
|
|
861
|
+
method =
|
|
862
|
+
choice === "1"
|
|
863
|
+
? "enroll"
|
|
864
|
+
: choice === "2"
|
|
865
|
+
? "sso"
|
|
866
|
+
: choice === "3"
|
|
867
|
+
? "gateway"
|
|
868
|
+
: undefined;
|
|
869
|
+
}
|
|
870
|
+
if (!method)
|
|
871
|
+
warn("Choose one of the three connection methods.");
|
|
872
|
+
}
|
|
873
|
+
if (!method)
|
|
874
|
+
throw new PromptCancelledError();
|
|
875
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
876
|
+
if (method === "enroll") {
|
|
877
|
+
const answer = await ask("? Enrollment link or token");
|
|
878
|
+
if (answer && isEnrollArg(answer) && parseEnrollLink(answer)) {
|
|
879
|
+
args.enroll = answer;
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
warn("That is not a valid enrollment link or token. Try again.");
|
|
883
|
+
}
|
|
884
|
+
else if (method === "sso") {
|
|
885
|
+
const answer = await ask("? SSO link or tenant id");
|
|
886
|
+
if (answer && parseSsoLink(answer)) {
|
|
887
|
+
args.sso = answer;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
warn("That is not a valid SSO link or tenant id. Try again.");
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
const answer = await ask("? Gateway URL", profile.gateway || process.env.ANYRAY_GATEWAY_URL);
|
|
894
|
+
if (answer && isEnrollArg(answer)) {
|
|
895
|
+
args.enroll = answer;
|
|
896
|
+
info("That looks like an enrollment link — using passwordless enrollment.");
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
if (!answer)
|
|
901
|
+
throw new Error("missing gateway");
|
|
902
|
+
args.gateway = resolveOrigin(answer);
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
warn("That is not a valid http(s) gateway URL. Try again.");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (!args.enroll && !args.sso && !args.gateway) {
|
|
911
|
+
throw new PromptCancelledError();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
769
914
|
if (!args.tools || args.tools.length === 0) {
|
|
770
915
|
heading("Tools detected");
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const suggested = [];
|
|
775
|
-
for (const adapter of REGISTRY) {
|
|
916
|
+
const installed = [];
|
|
917
|
+
const absent = [];
|
|
918
|
+
for (const adapter of registry) {
|
|
776
919
|
const detected = await adapter.detect();
|
|
777
920
|
if (!detected.installed) {
|
|
778
|
-
|
|
921
|
+
absent.push(adapter);
|
|
779
922
|
continue;
|
|
780
923
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
924
|
+
installed.push(adapter);
|
|
925
|
+
}
|
|
926
|
+
const suggested = installed
|
|
927
|
+
.filter((adapter) => wizardSuggests(adapter))
|
|
928
|
+
.map((adapter) => adapter.id);
|
|
929
|
+
if (installed.length > 0) {
|
|
930
|
+
for (let attempt = 0; attempt < 3 && !args.tools; attempt++) {
|
|
931
|
+
let chosen;
|
|
932
|
+
if (supportsMultiSelect()) {
|
|
933
|
+
const outcome = await selectMany({
|
|
934
|
+
title: "Tools to configure",
|
|
935
|
+
items: installed.map((adapter) => ({
|
|
936
|
+
value: adapter.id,
|
|
937
|
+
label: `${adapter.title} (${adapter.id})`,
|
|
938
|
+
hint: wizardSuggests(adapter)
|
|
939
|
+
? undefined
|
|
940
|
+
: "moves inference spend from your subscription to the org API bill",
|
|
941
|
+
selected: wizardSuggests(adapter),
|
|
942
|
+
requiresExplicitChoice: !wizardSuggests(adapter),
|
|
943
|
+
})),
|
|
944
|
+
});
|
|
945
|
+
if (outcome.kind === "cancelled")
|
|
946
|
+
throw new PromptCancelledError();
|
|
947
|
+
chosen = outcome.values;
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
const pick = await ask("? Tools to configure (comma-separated, blank = all suggested)", suggested.join(",") || undefined);
|
|
951
|
+
chosen = (pick ?? "")
|
|
952
|
+
.split(",")
|
|
953
|
+
.map((value) => value.trim())
|
|
954
|
+
.filter(Boolean);
|
|
955
|
+
const installedIds = new Set(installed.map((adapter) => adapter.id));
|
|
956
|
+
const invalid = chosen.filter((id) => !installedIds.has(id));
|
|
957
|
+
if (invalid.length > 0) {
|
|
958
|
+
warn(`Unknown or unavailable tool id${invalid.length === 1 ? "" : "s"}. Choose from: ${installed.map((adapter) => adapter.id).join(", ")}.`);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (chosen.length === 0) {
|
|
963
|
+
warn("Choose at least one installed tool.");
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
args.tools = chosen;
|
|
787
967
|
}
|
|
968
|
+
if (!args.tools)
|
|
969
|
+
throw new PromptCancelledError();
|
|
970
|
+
}
|
|
971
|
+
if (absent.length > 0) {
|
|
972
|
+
note(`${absent.length} others not installed (${absent.map((adapter) => adapter.id).join(", ")})`);
|
|
788
973
|
}
|
|
789
|
-
const pick = await prompt("? Tools to configure (comma-separated, blank = all suggested)", suggested.join(",") || undefined);
|
|
790
|
-
const chosen = (pick ?? "")
|
|
791
|
-
.split(",")
|
|
792
|
-
.map((s) => s.trim())
|
|
793
|
-
.filter(Boolean);
|
|
794
|
-
if (chosen.length > 0)
|
|
795
|
-
args.tools = chosen;
|
|
796
|
-
}
|
|
797
|
-
// Gateway URL: default to the cached/env/default origin.
|
|
798
|
-
if (!args.gateway) {
|
|
799
|
-
heading("Gateway");
|
|
800
|
-
const fallback = profile.gateway || process.env.ANYRAY_GATEWAY_URL;
|
|
801
|
-
const answer = await prompt("? Gateway URL", fallback);
|
|
802
|
-
if (answer)
|
|
803
|
-
args.gateway = answer;
|
|
804
974
|
}
|
|
805
975
|
// Identity is resolved by the apply flow's resolveDisplayName/resolveTeam,
|
|
806
976
|
// which already prompt on a TTY — leave args.user/args.team untouched so the
|
|
@@ -819,7 +989,7 @@ const runVerify = async (target, toolIds) => {
|
|
|
819
989
|
heading("Verifying the connection");
|
|
820
990
|
// Pass the configured tool ids so the org-key remedy can be tool-specific
|
|
821
991
|
// (e.g. OpenCode's Claude provider has no personal-subscription fallback).
|
|
822
|
-
const verdict = await verifyConnection(target, { tools: toolIds });
|
|
992
|
+
const verdict = await withSpinner("Checking the gateway…", () => verifyConnection(target, { tools: toolIds }));
|
|
823
993
|
if (verdict.kind === "ok")
|
|
824
994
|
ok(` ${verdict.detail}`);
|
|
825
995
|
else if (verdict.kind === "unreachable" ||
|
|
@@ -839,13 +1009,37 @@ const runVerify = async (target, toolIds) => {
|
|
|
839
1009
|
const RECONCILE_RETRY_PROBE_TIMEOUT_MS = 5_000;
|
|
840
1010
|
const RECONCILE_PROBE_GAP_MS = 500;
|
|
841
1011
|
const RECONCILE_PROBE_ATTEMPTS = 3;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1012
|
+
const PROFILE_SETTLE_MS = 10 * 60 * 1000;
|
|
1013
|
+
const defaultProfileAgeMs = async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
const s = await stat(await profilePath());
|
|
1016
|
+
return Date.now() - s.mtimeMs;
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
return Number.POSITIVE_INFINITY;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
// Scheduler-tick self-heal, both directions: reinstall a daemon EDR killed
|
|
1023
|
+
// while refresh is on; reap a disowned unit file (it re-registers on login).
|
|
845
1024
|
export const reconcileProxyService = async (deps = {}) => {
|
|
846
1025
|
try {
|
|
847
1026
|
const profile = await (deps.load ?? loadProfile)();
|
|
848
|
-
if (profile.keyRefreshEnabled !== true
|
|
1027
|
+
if (profile.keyRefreshEnabled !== true) {
|
|
1028
|
+
if ((deps.draining ?? isDraining)())
|
|
1029
|
+
return;
|
|
1030
|
+
// Unit-file-gated: a port probe would reap the profile-less fleet
|
|
1031
|
+
// daemon (RFC 0007); a file-less zombie dies at reboot anyway.
|
|
1032
|
+
if (!(await (deps.unitExists ?? proxyUnitFileExists)()))
|
|
1033
|
+
return;
|
|
1034
|
+
// An apply persists the direct profile before repointing tools — only
|
|
1035
|
+
// sweep a settled profile so a racing tick never strands them.
|
|
1036
|
+
const age = await (deps.profileAgeMs ?? defaultProfileAgeMs)();
|
|
1037
|
+
if (age < PROFILE_SETTLE_MS)
|
|
1038
|
+
return;
|
|
1039
|
+
await (deps.uninstall ?? uninstallProxyService)(profile.fallbackPort ?? DEFAULT_LOOPBACK_PORT);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (!profile.gateway)
|
|
849
1043
|
return;
|
|
850
1044
|
// RFC 0010 §6 P4: while a verified signed tray app owns launch-at-login,
|
|
851
1045
|
// it — not launchd — respawns a dead daemon, so reinstalling the
|
|
@@ -1027,7 +1221,31 @@ const SUBCOMMANDS = new Set([
|
|
|
1027
1221
|
"sync",
|
|
1028
1222
|
"device-url",
|
|
1029
1223
|
]);
|
|
1030
|
-
|
|
1224
|
+
const runApplySafely = async (argv, forceWizard) => {
|
|
1225
|
+
try {
|
|
1226
|
+
return await runApplyCommand(argv, forceWizard);
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
if (error instanceof PromptCancelledError) {
|
|
1230
|
+
console.error(dim("Cancelled — nothing was written."));
|
|
1231
|
+
return 130;
|
|
1232
|
+
}
|
|
1233
|
+
throw error;
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
export const confirmApplyPlan = async (args, wizardRan, targetCount, deps = {}) => {
|
|
1237
|
+
if (args.yes || args.dryRun)
|
|
1238
|
+
return "proceed";
|
|
1239
|
+
if (!(deps.isInteractive ?? isInteractive)())
|
|
1240
|
+
return "non-interactive";
|
|
1241
|
+
// Flag-driven runs keep their original question so scripted use is unchanged.
|
|
1242
|
+
const verb = args.revert ? "remove Anyray config from" : "configure";
|
|
1243
|
+
const question = wizardRan
|
|
1244
|
+
? "Apply this configuration?"
|
|
1245
|
+
: `\n${verb} ${targetCount} tool target(s)?`;
|
|
1246
|
+
return (await (deps.confirm ?? confirm)(question)) ? "proceed" : "declined";
|
|
1247
|
+
};
|
|
1248
|
+
const dispatch = async (argv) => {
|
|
1031
1249
|
// Hidden hook entrypoint: Claude Code invokes `anyray-connect __anyray-hook`
|
|
1032
1250
|
// as a PostToolUse hook. It speaks the hook stdin/stdout JSON protocol, so it
|
|
1033
1251
|
// must run BEFORE any human-facing logging touches stdout. Never throws.
|
|
@@ -1247,7 +1465,7 @@ export const run = async (argv) => {
|
|
|
1247
1465
|
return 1;
|
|
1248
1466
|
}
|
|
1249
1467
|
if (parsed.help) {
|
|
1250
|
-
info(HELP);
|
|
1468
|
+
info(parsed.helpAll ? HELP_ALL : HELP);
|
|
1251
1469
|
return 0;
|
|
1252
1470
|
}
|
|
1253
1471
|
if (sub === "status")
|
|
@@ -1267,11 +1485,25 @@ export const run = async (argv) => {
|
|
|
1267
1485
|
if (sub === "device-url")
|
|
1268
1486
|
return runDeviceUrl();
|
|
1269
1487
|
// 'wizard' → apply flow with the wizard enabled.
|
|
1270
|
-
return
|
|
1488
|
+
return runApplySafely(rest, true);
|
|
1489
|
+
}
|
|
1490
|
+
return runApplySafely(argv, false);
|
|
1491
|
+
};
|
|
1492
|
+
// Not just apply: without this, index.ts dumps the raw error and exits 1.
|
|
1493
|
+
export const run = async (argv) => {
|
|
1494
|
+
try {
|
|
1495
|
+
return await dispatch(argv);
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
if (error instanceof PromptCancelledError) {
|
|
1499
|
+
console.error(dim("Cancelled."));
|
|
1500
|
+
return 130;
|
|
1501
|
+
}
|
|
1502
|
+
throw error;
|
|
1271
1503
|
}
|
|
1272
|
-
return runApplyCommand(argv, false);
|
|
1273
1504
|
};
|
|
1274
|
-
const runApplyCommand = async (argv, forceWizard) => {
|
|
1505
|
+
export const runApplyCommand = async (argv, forceWizard, deps = {}) => {
|
|
1506
|
+
const interactive = deps.isInteractive ?? isInteractive;
|
|
1275
1507
|
let args;
|
|
1276
1508
|
try {
|
|
1277
1509
|
args = parseArgs(argv);
|
|
@@ -1282,7 +1514,7 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1282
1514
|
return 1;
|
|
1283
1515
|
}
|
|
1284
1516
|
if (args.help) {
|
|
1285
|
-
info(HELP);
|
|
1517
|
+
info(args.helpAll ? HELP_ALL : HELP);
|
|
1286
1518
|
return 0;
|
|
1287
1519
|
}
|
|
1288
1520
|
// These commands have opposite effects. In particular, accepting
|
|
@@ -1372,8 +1604,10 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1372
1604
|
!args.dryRun &&
|
|
1373
1605
|
!args.enroll &&
|
|
1374
1606
|
!args.sso;
|
|
1375
|
-
|
|
1376
|
-
|
|
1607
|
+
let wizardRan = false;
|
|
1608
|
+
if ((forceWizard || bareInteractive) && interactive()) {
|
|
1609
|
+
args = await (deps.runWizard ?? runWizard)(args, profile);
|
|
1610
|
+
wizardRan = true;
|
|
1377
1611
|
}
|
|
1378
1612
|
let adapters;
|
|
1379
1613
|
try {
|
|
@@ -1390,21 +1624,32 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1390
1624
|
if (args.gateway && !args.enroll && isEnrollArg(args.gateway)) {
|
|
1391
1625
|
args.enroll = args.gateway;
|
|
1392
1626
|
args.gateway = undefined;
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1395
|
-
//
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1627
|
+
info("That looks like an enrollment link — treating it as --enroll.");
|
|
1628
|
+
}
|
|
1629
|
+
// Resolve enrollment info when the gateway is unknown or a cached cert might
|
|
1630
|
+
// be reused. Besides the gateway URL it carries a personal link's pre-bound
|
|
1631
|
+
// identity. For a shared link, the absence of that identity tells reuse logic
|
|
1632
|
+
// to compare the stored cert with today's automatic identity, so a legacy
|
|
1633
|
+
// `unknown` binding can self-heal on re-run. A fresh enrollment with an
|
|
1634
|
+
// explicit gateway keeps the old one-round-trip path. The lookup is read-only
|
|
1635
|
+
// and does not consume the token.
|
|
1399
1636
|
let enrollDeploymentId;
|
|
1400
|
-
|
|
1637
|
+
let enrollBoundIdentity;
|
|
1638
|
+
let enrollInfoResolved = false;
|
|
1639
|
+
if (args.enroll && (!args.gateway || profile.enrolledCert)) {
|
|
1401
1640
|
const enrollLink = parseEnrollLink(args.enroll);
|
|
1402
1641
|
if (enrollLink) {
|
|
1403
1642
|
try {
|
|
1404
|
-
const resolved = await resolveEnrollLink(enrollLink);
|
|
1405
|
-
|
|
1643
|
+
const resolved = await withSpinner("Resolving enrollment link…", () => resolveEnrollLink(enrollLink));
|
|
1644
|
+
const filledGateway = !args.gateway;
|
|
1645
|
+
if (filledGateway)
|
|
1646
|
+
args.gateway = resolved.gatewayUrl;
|
|
1406
1647
|
enrollDeploymentId = resolved.deploymentId;
|
|
1407
|
-
|
|
1648
|
+
enrollBoundIdentity = resolved.email;
|
|
1649
|
+
enrollInfoResolved = true;
|
|
1650
|
+
if (filledGateway) {
|
|
1651
|
+
note(`Enrollment link${resolved.org ? ` for ${resolved.org}` : ""}: gateway ${resolved.gatewayUrl}`);
|
|
1652
|
+
}
|
|
1408
1653
|
if (resolved.seatMode && !args.subscription) {
|
|
1409
1654
|
args.subscription = true;
|
|
1410
1655
|
note("Subscription org: using pass-through auth (--subscription) — your own seat sign-in is used.");
|
|
@@ -1415,7 +1660,9 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1415
1660
|
// one (e.g. a returning dev re-minting while the control plane is briefly
|
|
1416
1661
|
// unreachable) — the cache/env logic below fills it. Only fail when the
|
|
1417
1662
|
// gateway is genuinely unknown.
|
|
1418
|
-
const fallback =
|
|
1663
|
+
const fallback = args.gateway ||
|
|
1664
|
+
process.env.ANYRAY_GATEWAY_URL?.trim() ||
|
|
1665
|
+
profile.gateway;
|
|
1419
1666
|
if (fallback) {
|
|
1420
1667
|
note(`Could not resolve the gateway from the enrollment link — using ${fallback}.`);
|
|
1421
1668
|
}
|
|
@@ -1442,12 +1689,12 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1442
1689
|
return 1;
|
|
1443
1690
|
}
|
|
1444
1691
|
try {
|
|
1445
|
-
const result = await ssoEnroll({
|
|
1692
|
+
const result = await withSpinner("Waiting for SSO sign-in…", () => ssoEnroll({
|
|
1446
1693
|
cpOrigin: ssoLink.cpOrigin,
|
|
1447
1694
|
tenantId: ssoLink.tenantId,
|
|
1448
1695
|
gatewayUrl: args.gateway,
|
|
1449
1696
|
log: (m) => note(m),
|
|
1450
|
-
});
|
|
1697
|
+
}));
|
|
1451
1698
|
ssoEnrollment = {
|
|
1452
1699
|
cert: result.cert,
|
|
1453
1700
|
privateKeyPem: result.privateKeyPem,
|
|
@@ -1526,7 +1773,7 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1526
1773
|
// the window once we already hold a key.
|
|
1527
1774
|
await reserveMint();
|
|
1528
1775
|
const presentedTokenHash = enrollTokenHash(input.token);
|
|
1529
|
-
const storedValid = canReuseEnrolledCert(profile.enrolledCert, profile.enrolledCertExpiry, input.deploymentId, Date.now(), presentedTokenHash, profile.enrolledTokenHash);
|
|
1776
|
+
const storedValid = canReuseEnrolledCert(profile.enrolledCert, profile.enrolledCertExpiry, input.deploymentId, Date.now(), presentedTokenHash, profile.enrolledTokenHash, input.expectedUser);
|
|
1530
1777
|
const storedPrivateKey = storedValid ? await loadDevKey() : undefined;
|
|
1531
1778
|
const reuse = storedValid && storedPrivateKey && profile.enrolledCert
|
|
1532
1779
|
? {
|
|
@@ -1593,13 +1840,20 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1593
1840
|
}
|
|
1594
1841
|
try {
|
|
1595
1842
|
const machineId = args.machine ? deriveMachineId() : undefined;
|
|
1843
|
+
const detectedIdentity = enrollBoundIdentity ?? enrollIdentity(args.user);
|
|
1844
|
+
const expectedUser = args.machine
|
|
1845
|
+
? `machine:${machineId}`
|
|
1846
|
+
: enrollInfoResolved
|
|
1847
|
+
? detectedIdentity
|
|
1848
|
+
: undefined;
|
|
1596
1849
|
await runEnrollment({
|
|
1597
1850
|
cpOrigin: link.cpOrigin,
|
|
1598
1851
|
token: link.token,
|
|
1599
|
-
email:
|
|
1852
|
+
email: detectedIdentity,
|
|
1600
1853
|
team: args.team,
|
|
1601
1854
|
machineId,
|
|
1602
1855
|
deploymentId: enrollDeploymentId,
|
|
1856
|
+
expectedUser,
|
|
1603
1857
|
});
|
|
1604
1858
|
}
|
|
1605
1859
|
catch (error) {
|
|
@@ -1714,34 +1968,27 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
1714
1968
|
}
|
|
1715
1969
|
warn(` ${message}`);
|
|
1716
1970
|
note(" Dry run only — no placeholder configuration will be written.");
|
|
1717
|
-
|
|
1718
|
-
for (const adapter of adapters) {
|
|
1719
|
-
const detected = await adapter.detect();
|
|
1720
|
-
if (detected.installed) {
|
|
1721
|
-
info(` ${adapter.title} ${dim(`(${adapter.id})`)} — would configure after enrollment${detected.detail ? `; ${detected.detail}` : ""}`);
|
|
1722
|
-
}
|
|
1723
|
-
else {
|
|
1724
|
-
note(` ${adapter.title} ${dim(`(${adapter.id})`)} — not found${detected.detail ? `; ${detected.detail}` : ""}`);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1971
|
+
await printSelectedTools(adapters, "after-enrollment");
|
|
1727
1972
|
note("\nDry run complete — no files were written.");
|
|
1728
1973
|
return 0;
|
|
1729
1974
|
}
|
|
1730
1975
|
if (!args.revert)
|
|
1731
1976
|
printTarget(target, adapters);
|
|
1977
|
+
if (wizardRan && !args.revert) {
|
|
1978
|
+
await printSelectedTools(adapters, "plan");
|
|
1979
|
+
}
|
|
1732
1980
|
if (args.dryRun)
|
|
1733
1981
|
note("\nDry run — no files will be written.");
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
}
|
|
1982
|
+
const confirmation = await confirmApplyPlan(args, wizardRan, adapters.length, { isInteractive: interactive, confirm: deps.confirm });
|
|
1983
|
+
if (confirmation === "non-interactive") {
|
|
1984
|
+
fail("\nNon-interactive session — cannot ask for confirmation. Pass --yes to proceed.");
|
|
1985
|
+
return 1;
|
|
1986
|
+
}
|
|
1987
|
+
if (confirmation === "declined") {
|
|
1988
|
+
note(wizardRan
|
|
1989
|
+
? "Nothing was written. Re-run `npx anyray-connect wizard` when you're ready."
|
|
1990
|
+
: "Aborted. Nothing changed.");
|
|
1991
|
+
return 0;
|
|
1745
1992
|
}
|
|
1746
1993
|
// Preflight: never repoint a working tool at a gateway that isn't answering.
|
|
1747
1994
|
// Skipped for dry-run (writes nothing) and revert (removing config, not adding
|
|
@@ -2023,8 +2270,8 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
2023
2270
|
}
|
|
2024
2271
|
}
|
|
2025
2272
|
else if (args.revert && !args.dryRun) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2273
|
+
info("\nKeeping the stored enrollment — the tools you did not name still need it.");
|
|
2274
|
+
info("Run --revert with no --tools to remove it.");
|
|
2028
2275
|
}
|
|
2029
2276
|
// A named `--tools` list (the wizard writes its checklist here too) is the
|
|
2030
2277
|
// developer choosing these adapters by hand; the default sweep names none.
|
|
@@ -2165,7 +2412,21 @@ const runApplyCommand = async (argv, forceWizard) => {
|
|
|
2165
2412
|
// separately at install time (and by `doctor`).
|
|
2166
2413
|
await runVerify(target, adapters.map((a) => a.id));
|
|
2167
2414
|
}
|
|
2168
|
-
|
|
2415
|
+
heading("What's next");
|
|
2416
|
+
const configuredIds = new Set(results
|
|
2417
|
+
.filter((result) => result.status === "configured")
|
|
2418
|
+
.map((result) => result.id));
|
|
2419
|
+
const connected = adapters
|
|
2420
|
+
.filter((adapter) => configuredIds.has(adapter.id))
|
|
2421
|
+
.map((adapter) => adapter.title);
|
|
2422
|
+
info(connected.length > 0
|
|
2423
|
+
? ` Connected: ${connected.join(", ")}`
|
|
2424
|
+
: " Connected: no tools completed automatic configuration.");
|
|
2425
|
+
info(" Restart any editor that was already running — it loaded our config at startup.");
|
|
2426
|
+
info(" If something looks wrong: npx anyray-connect doctor");
|
|
2427
|
+
if (!args.verbose)
|
|
2428
|
+
return 0;
|
|
2429
|
+
info("\n Re-test by hand any time:");
|
|
2169
2430
|
const nativeCursorOnly = target.authMode !== "placeholder" &&
|
|
2170
2431
|
adapters.length === 1 &&
|
|
2171
2432
|
adapters[0]?.id === "cursor";
|