@i4ctime/q-ring 0.14.1 → 0.15.0

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/index.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  tunnelList,
45
45
  tunnelRead,
46
46
  verifyAuditChain
47
- } from "./chunk-C2TFJ2EH.js";
47
+ } from "./chunk-MLBJCPX2.js";
48
48
 
49
49
  // src/cli/commands.ts
50
50
  import { Command, Help } from "commander";
@@ -764,6 +764,79 @@ var ProviderRegistry = class {
764
764
  return [...this.providers.values()];
765
765
  }
766
766
  };
767
+ function livenessProvider(cfg) {
768
+ return {
769
+ name: cfg.name,
770
+ description: cfg.description,
771
+ prefixes: cfg.prefixes,
772
+ async validate(value) {
773
+ const start = Date.now();
774
+ try {
775
+ const { statusCode } = await makeRequest(cfg.url, {
776
+ "User-Agent": "q-ring-validator/1.0",
777
+ ...cfg.headers(value)
778
+ });
779
+ const latencyMs = Date.now() - start;
780
+ if (statusCode === 200)
781
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: cfg.name };
782
+ if (statusCode === 401 || statusCode === 403)
783
+ return { valid: false, status: "invalid", message: `Invalid or revoked API key (${statusCode})`, latencyMs, provider: cfg.name };
784
+ if (statusCode === 429)
785
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: cfg.name };
786
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: cfg.name };
787
+ } catch (err) {
788
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: cfg.name };
789
+ }
790
+ }
791
+ };
792
+ }
793
+ var anthropicProvider = livenessProvider({
794
+ name: "anthropic",
795
+ description: "Anthropic API key validation",
796
+ prefixes: ["sk-ant-"],
797
+ url: "https://api.anthropic.com/v1/models?limit=1",
798
+ headers: (value) => ({ "x-api-key": value, "anthropic-version": "2023-06-01" })
799
+ });
800
+ var openrouterProvider = livenessProvider({
801
+ name: "openrouter",
802
+ description: "OpenRouter API key validation",
803
+ prefixes: ["sk-or-"],
804
+ url: "https://openrouter.ai/api/v1/key",
805
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
806
+ });
807
+ var googleAiProvider = livenessProvider({
808
+ name: "google-ai",
809
+ description: "Google AI (Gemini) API key validation",
810
+ prefixes: ["AIza"],
811
+ url: "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
812
+ headers: (value) => ({ "x-goog-api-key": value })
813
+ });
814
+ var groqProvider = livenessProvider({
815
+ name: "groq",
816
+ description: "Groq API key validation",
817
+ prefixes: ["gsk_"],
818
+ url: "https://api.groq.com/openai/v1/models",
819
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
820
+ });
821
+ var huggingfaceProvider = livenessProvider({
822
+ name: "huggingface",
823
+ description: "Hugging Face token validation",
824
+ prefixes: ["hf_"],
825
+ url: "https://huggingface.co/api/whoami-v2",
826
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
827
+ });
828
+ var elevenlabsProvider = livenessProvider({
829
+ name: "elevenlabs",
830
+ description: "ElevenLabs API key validation (explicit-only \u2014 set provider on the secret)",
831
+ url: "https://api.elevenlabs.io/v1/user",
832
+ headers: (value) => ({ "xi-api-key": value })
833
+ });
834
+ var vercelProvider = livenessProvider({
835
+ name: "vercel",
836
+ description: "Vercel token validation (explicit-only \u2014 set provider on the secret)",
837
+ url: "https://api.vercel.com/v2/user",
838
+ headers: (value) => ({ Authorization: `Bearer ${value}` })
839
+ });
767
840
  var openaiProvider = {
768
841
  name: "openai",
769
842
  description: "OpenAI API key validation",
@@ -890,7 +963,14 @@ var httpProvider = {
890
963
  }
891
964
  };
892
965
  var registry2 = new ProviderRegistry();
966
+ registry2.register(anthropicProvider);
967
+ registry2.register(openrouterProvider);
893
968
  registry2.register(openaiProvider);
969
+ registry2.register(googleAiProvider);
970
+ registry2.register(groqProvider);
971
+ registry2.register(huggingfaceProvider);
972
+ registry2.register(elevenlabsProvider);
973
+ registry2.register(vercelProvider);
894
974
  registry2.register(stripeProvider);
895
975
  registry2.register(githubProvider);
896
976
  registry2.register(awsProvider);
@@ -1984,10 +2064,9 @@ var RedactionTransform = class extends Transform {
1984
2064
  callback();
1985
2065
  }
1986
2066
  };
1987
- async function execCommand(opts) {
1988
- const profile = getProfile(opts.profile);
1989
- const fullCommand = [opts.command, ...opts.args].join(" ");
1990
- const policyDecision = checkExecPolicy(fullCommand, opts.projectPath);
2067
+ function enforceExecPolicy(profile, command, args, projectPath) {
2068
+ const fullCommand = [command, ...args].join(" ");
2069
+ const policyDecision = checkExecPolicy(fullCommand, projectPath);
1991
2070
  if (!policyDecision.allowed) {
1992
2071
  throw new Error(`Policy Denied: ${policyDecision.reason}`);
1993
2072
  }
@@ -2003,9 +2082,13 @@ async function execCommand(opts) {
2003
2082
  if (profile.allowCommands) {
2004
2083
  const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
2005
2084
  if (!allowed) {
2006
- throw new Error(`Exec profile "${profile.name}" does not allow command "${opts.command}"`);
2085
+ throw new Error(`Exec profile "${profile.name}" does not allow command "${command}"`);
2007
2086
  }
2008
2087
  }
2088
+ }
2089
+ async function execCommand(opts) {
2090
+ const profile = getProfile(opts.profile);
2091
+ enforceExecPolicy(profile, opts.command, opts.args, opts.projectPath);
2009
2092
  const envMap = {};
2010
2093
  for (const [k, v] of Object.entries(process.env)) {
2011
2094
  if (v !== void 0) envMap[k] = v;
@@ -2052,6 +2135,18 @@ async function execCommand(opts) {
2052
2135
  }
2053
2136
  }
2054
2137
  }
2138
+ return spawnRedacted({
2139
+ profile,
2140
+ command: opts.command,
2141
+ args: opts.args,
2142
+ envMap,
2143
+ secretsToRedact: [...secretsToRedact],
2144
+ captureOutput: opts.captureOutput,
2145
+ projectPath: opts.projectPath
2146
+ });
2147
+ }
2148
+ function spawnRedacted(opts) {
2149
+ const { profile, secretsToRedact, envMap } = opts;
2055
2150
  const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts.projectPath);
2056
2151
  return new Promise((resolve, reject) => {
2057
2152
  const networkTools = /* @__PURE__ */ new Set([
@@ -2492,7 +2587,7 @@ function registerToolingCommands(program2) {
2492
2587
  }
2493
2588
  });
2494
2589
  program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
2495
- const { startDashboardServer } = await import("./dashboard-PYYAED45.js");
2590
+ const { startDashboardServer } = await import("./dashboard-T2UG23KI.js");
2496
2591
  const { exec } = await import("child_process");
2497
2592
  const { platform } = await import("os");
2498
2593
  const port = Number(cmd.port);
@@ -3407,12 +3502,540 @@ ${SYMBOLS.lock} Approval Tokens
3407
3502
  });
3408
3503
  }
3409
3504
 
3410
- // src/cli/commands/doctor.ts
3411
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, rmSync } from "fs";
3412
- import { join as join3, delimiter } from "path";
3505
+ // src/core/run.ts
3506
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3507
+ import { join as join3, resolve as resolvePath } from "path";
3508
+
3509
+ // src/core/refs.ts
3510
+ var REF_PREFIX = "qring://";
3511
+ var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3512
+ var REF_PATTERN = /^qring:\/\/(global|project|)\/([^/?#]+)(?:\?([^#]*))?$/;
3513
+ function isRef(value) {
3514
+ return value.startsWith(REF_PREFIX);
3515
+ }
3516
+ function parseRef(raw) {
3517
+ if (!isRef(raw)) {
3518
+ throw new Error(`Not a qring:// reference: "${raw}"`);
3519
+ }
3520
+ const match = REF_PATTERN.exec(raw);
3521
+ if (!match) {
3522
+ const hostOnly = /^qring:\/\/([^/?#]+)\/?$/.exec(raw);
3523
+ if (hostOnly && KEY_PATTERN.test(hostOnly[1]) && !["global", "project"].includes(hostOnly[1])) {
3524
+ throw new Error(
3525
+ `Invalid ref "${raw}": the key belongs in the path, not the host (URL hosts are lowercased by parsers). Use "qring:///${hostOnly[1]}" (auto scope) or "qring://project/${hostOnly[1]}".`
3526
+ );
3527
+ }
3528
+ throw new Error(
3529
+ `Invalid ref "${raw}": expected qring://<global|project|>/KEY[?env=<env>]`
3530
+ );
3531
+ }
3532
+ const [, scopeRaw, key, query] = match;
3533
+ if (!KEY_PATTERN.test(key)) {
3534
+ throw new Error(
3535
+ `Invalid ref "${raw}": "${key}" is not a valid secret key (expected [A-Za-z_][A-Za-z0-9_]*)`
3536
+ );
3537
+ }
3538
+ let env;
3539
+ if (query) {
3540
+ for (const part of query.split("&")) {
3541
+ if (!part) continue;
3542
+ const eq = part.indexOf("=");
3543
+ const name = eq === -1 ? part : part.slice(0, eq);
3544
+ const value = eq === -1 ? "" : decodeURIComponent(part.slice(eq + 1));
3545
+ if (name === "env") {
3546
+ env = value;
3547
+ } else {
3548
+ throw new Error(`Invalid ref "${raw}": unknown query parameter "${name}"`);
3549
+ }
3550
+ }
3551
+ }
3552
+ return {
3553
+ scope: scopeRaw === "" ? void 0 : scopeRaw,
3554
+ key,
3555
+ env,
3556
+ raw
3557
+ };
3558
+ }
3559
+ function resolveRef(ref, opts = {}) {
3560
+ const scopes = ref.scope ? [ref.scope] : ["project", "global"];
3561
+ for (const scope of scopes) {
3562
+ const value = getSecret(ref.key, {
3563
+ ...opts,
3564
+ scope,
3565
+ env: ref.env ?? opts.env
3566
+ });
3567
+ if (value !== null) return value;
3568
+ }
3569
+ return null;
3570
+ }
3571
+ function resolveRefsInMap(map, opts = {}) {
3572
+ const resolved = {};
3573
+ const secretValues = [];
3574
+ const missing = [];
3575
+ for (const [name, value] of Object.entries(map)) {
3576
+ if (!isRef(value)) {
3577
+ resolved[name] = value;
3578
+ continue;
3579
+ }
3580
+ const ref = parseRef(value);
3581
+ const secret = resolveRef(ref, opts);
3582
+ if (secret === null) {
3583
+ missing.push({ name, ref });
3584
+ continue;
3585
+ }
3586
+ resolved[name] = secret;
3587
+ secretValues.push(secret);
3588
+ }
3589
+ return { resolved, secretValues, missing };
3590
+ }
3591
+
3592
+ // src/core/run.ts
3593
+ function buildRunPlan(opts) {
3594
+ const projectPath = opts.projectPath ?? process.cwd();
3595
+ const keyringOpts = {
3596
+ projectPath,
3597
+ env: opts.env,
3598
+ source: opts.source ?? "cli",
3599
+ silent: opts.silent
3600
+ };
3601
+ const envMap = {};
3602
+ for (const [k, v] of Object.entries(process.env)) {
3603
+ if (v !== void 0) envMap[k] = v;
3604
+ }
3605
+ const injected = [];
3606
+ const missingRequired = [];
3607
+ const missingOptional = [];
3608
+ const secretsToRedact = /* @__PURE__ */ new Set();
3609
+ if (opts.useManifest !== false) {
3610
+ const config = readProjectConfig(projectPath);
3611
+ for (const [key, entry] of Object.entries(config?.secrets ?? {})) {
3612
+ const value = resolveRef(
3613
+ { key, raw: `manifest:${key}` },
3614
+ keyringOpts
3615
+ );
3616
+ if (value === null) {
3617
+ if (entry.required !== false) missingRequired.push(key);
3618
+ else if (opts.strict) missingRequired.push(key);
3619
+ else missingOptional.push(key);
3620
+ continue;
3621
+ }
3622
+ envMap[key] = value;
3623
+ injected.push({ name: key, source: "manifest" });
3624
+ if (value.length > 5) secretsToRedact.add(value);
3625
+ }
3626
+ }
3627
+ const envFiles = opts.envFiles ?? (existsSync4(join3(projectPath, ".env")) ? [join3(projectPath, ".env")] : []);
3628
+ for (const file of envFiles) {
3629
+ const path = resolvePath(projectPath, file);
3630
+ if (!existsSync4(path)) {
3631
+ throw new Error(`Env file not found: ${path}`);
3632
+ }
3633
+ const parsed = Object.fromEntries(parseDotenv(readFileSync6(path, "utf8")));
3634
+ const { resolved, secretValues, missing } = resolveRefsInMap(parsed, keyringOpts);
3635
+ for (const [name, value] of Object.entries(resolved)) {
3636
+ envMap[name] = value;
3637
+ injected.push({
3638
+ name,
3639
+ source: isRef(parsed[name]) ? "env-file-ref" : "env-file"
3640
+ });
3641
+ }
3642
+ for (const value of secretValues) {
3643
+ if (value.length > 5) secretsToRedact.add(value);
3644
+ }
3645
+ for (const m of missing) missingRequired.push(m.name);
3646
+ }
3647
+ return {
3648
+ envFiles: envFiles.map((f) => resolvePath(projectPath, f)),
3649
+ injected,
3650
+ missingRequired,
3651
+ missingOptional,
3652
+ envMap,
3653
+ secretsToRedact: [...secretsToRedact]
3654
+ };
3655
+ }
3656
+ async function runCommand(opts) {
3657
+ const profile = getProfile(opts.profile);
3658
+ enforceExecPolicy(profile, opts.command, opts.args, opts.projectPath);
3659
+ const plan = buildRunPlan(opts);
3660
+ if (plan.missingRequired.length > 0) {
3661
+ throw new Error(
3662
+ `Missing required secrets: ${plan.missingRequired.join(", ")}. Store them with \`qring set <KEY>\` or drop them from the manifest/.env refs.`
3663
+ );
3664
+ }
3665
+ if (profile.stripEnvVars) {
3666
+ for (const key of profile.stripEnvVars) {
3667
+ delete plan.envMap[key];
3668
+ }
3669
+ }
3670
+ const result = await spawnRedacted({
3671
+ profile,
3672
+ command: opts.command,
3673
+ args: opts.args,
3674
+ envMap: plan.envMap,
3675
+ secretsToRedact: plan.secretsToRedact,
3676
+ captureOutput: opts.captureOutput,
3677
+ projectPath: opts.projectPath
3678
+ });
3679
+ return { ...result, plan };
3680
+ }
3681
+
3682
+ // src/core/setup.ts
3683
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3413
3684
  import { homedir } from "os";
3685
+ import { dirname, join as join4 } from "path";
3686
+ var EDITORS = ["cursor", "kiro", "claude"];
3687
+ var READ_ONLY_TOOLS = [
3688
+ "list_secrets",
3689
+ "has_secret",
3690
+ "inspect_secret",
3691
+ "detect_environment",
3692
+ "get_project_context",
3693
+ "list_providers",
3694
+ "list_hooks",
3695
+ "get_policy_summary",
3696
+ "audit_log",
3697
+ "verify_audit_chain",
3698
+ "health_check",
3699
+ "analyze_secrets"
3700
+ ];
3701
+ var SERVER_NAME = "q-ring";
3702
+ function serverEntry(editor) {
3703
+ switch (editor) {
3704
+ case "cursor":
3705
+ return { command: "qring-mcp", args: [], env: {} };
3706
+ case "kiro":
3707
+ return {
3708
+ command: "qring-mcp",
3709
+ args: [],
3710
+ env: {},
3711
+ disabled: false,
3712
+ autoApprove: READ_ONLY_TOOLS
3713
+ };
3714
+ case "claude":
3715
+ return { type: "stdio", command: "qring-mcp", args: [], env: {} };
3716
+ }
3717
+ }
3718
+ function configPathFor(editor, global, projectPath) {
3719
+ const home = homedir();
3720
+ switch (editor) {
3721
+ case "cursor":
3722
+ return global ? join4(home, ".cursor", "mcp.json") : join4(projectPath, ".cursor", "mcp.json");
3723
+ case "kiro":
3724
+ return global ? join4(home, ".kiro", "settings", "mcp.json") : join4(projectPath, ".kiro", "settings", "mcp.json");
3725
+ case "claude":
3726
+ if (global) {
3727
+ throw new Error(
3728
+ "Claude Code user-scoped MCP servers are registered via the claude CLI, not a config file.\nRun: claude mcp add --scope user q-ring qring-mcp\nOr install the plugin: /plugin marketplace add I4cTime/q-ring && /plugin install qring@q-ring"
3729
+ );
3730
+ }
3731
+ return join4(projectPath, ".mcp.json");
3732
+ }
3733
+ }
3734
+ function setupEditor(opts) {
3735
+ const projectPath = opts.projectPath ?? process.cwd();
3736
+ const configPath = configPathFor(opts.editor, opts.global ?? false, projectPath);
3737
+ const warnings = [];
3738
+ let config = {};
3739
+ if (existsSync5(configPath)) {
3740
+ const raw = readFileSync7(configPath, "utf8");
3741
+ try {
3742
+ config = JSON.parse(raw);
3743
+ } catch {
3744
+ throw new Error(
3745
+ `${configPath} exists but is not valid JSON \u2014 fix or remove it, then re-run.`
3746
+ );
3747
+ }
3748
+ }
3749
+ const servers = config.mcpServers ?? {};
3750
+ const desired = serverEntry(opts.editor);
3751
+ const existing = servers[SERVER_NAME];
3752
+ let action;
3753
+ if (existing === void 0) {
3754
+ action = existsSync5(configPath) ? "updated" : "created";
3755
+ servers[SERVER_NAME] = desired;
3756
+ } else if (JSON.stringify(existing) === JSON.stringify(desired)) {
3757
+ action = "unchanged";
3758
+ } else if (opts.force) {
3759
+ action = "updated";
3760
+ servers[SERVER_NAME] = desired;
3761
+ } else {
3762
+ action = "unchanged";
3763
+ warnings.push(
3764
+ `A different "${SERVER_NAME}" entry already exists in ${configPath} \u2014 left as-is (use --force to replace it).`
3765
+ );
3766
+ }
3767
+ config.mcpServers = servers;
3768
+ if (action !== "unchanged" && !opts.dryRun) {
3769
+ mkdirSync(dirname(configPath), { recursive: true });
3770
+ writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
3771
+ }
3772
+ return {
3773
+ editor: opts.editor,
3774
+ configPath,
3775
+ serverName: SERVER_NAME,
3776
+ action,
3777
+ dryRun: opts.dryRun ?? false,
3778
+ warnings
3779
+ };
3780
+ }
3781
+
3782
+ // src/core/push.ts
3783
+ import { spawnSync } from "child_process";
3784
+ var PUSH_TARGETS = ["github", "vercel", "cloudflare"];
3785
+ var TARGETS = {
3786
+ github: {
3787
+ binary: "gh",
3788
+ args: (key, opts) => [
3789
+ ["secret", "set", key, ...opts.repo ? ["--repo", opts.repo] : []]
3790
+ ],
3791
+ installHint: "install the GitHub CLI: https://cli.github.com (then `gh auth login`)"
3792
+ },
3793
+ vercel: {
3794
+ binary: "vercel",
3795
+ // One invocation per environment — `vercel env add` takes a single target.
3796
+ args: (key, opts) => (opts.vercelEnvs ?? ["production"]).map((env) => [
3797
+ "env",
3798
+ "add",
3799
+ key,
3800
+ env,
3801
+ "--force"
3802
+ ]),
3803
+ installHint: "install the Vercel CLI: npm i -g vercel (then `vercel link` in the project)"
3804
+ },
3805
+ cloudflare: {
3806
+ binary: "wrangler",
3807
+ args: (key) => [["secret", "put", key]],
3808
+ installHint: "install Wrangler: npm i -g wrangler (then `wrangler login`)"
3809
+ }
3810
+ };
3811
+ function binaryAvailable(binary) {
3812
+ const probe = spawnSync(binary, ["--version"], { stdio: "ignore", shell: false });
3813
+ return !probe.error;
3814
+ }
3815
+ function resolvePushKeys(opts) {
3816
+ if (opts.keys?.length) return opts.keys;
3817
+ const config = readProjectConfig(opts.projectPath);
3818
+ const manifestKeys = Object.keys(config?.secrets ?? {});
3819
+ if (manifestKeys.length === 0) {
3820
+ throw new Error(
3821
+ "Nothing to push: no --keys given and no secrets manifest in .q-ring.json. Declare the project's secrets there or pass --keys KEY1,KEY2."
3822
+ );
3823
+ }
3824
+ return manifestKeys;
3825
+ }
3826
+ function pushSecrets(opts) {
3827
+ const target = TARGETS[opts.target];
3828
+ const keys = resolvePushKeys(opts);
3829
+ if (!opts.dryRun && !binaryAvailable(target.binary)) {
3830
+ throw new Error(`"${target.binary}" CLI not found \u2014 ${target.installHint}`);
3831
+ }
3832
+ const result = { target: opts.target, pushed: [], failed: [], missing: [] };
3833
+ for (const key of keys) {
3834
+ const value = resolveRef(
3835
+ { key, raw: `push:${key}` },
3836
+ {
3837
+ projectPath: opts.projectPath,
3838
+ env: opts.env,
3839
+ source: opts.source ?? "cli",
3840
+ silent: opts.silent
3841
+ }
3842
+ );
3843
+ if (value === null) {
3844
+ result.missing.push(key);
3845
+ continue;
3846
+ }
3847
+ if (opts.dryRun) {
3848
+ result.pushed.push(key);
3849
+ continue;
3850
+ }
3851
+ let failedInvocation = null;
3852
+ for (const args of target.args(key, opts)) {
3853
+ const child = spawnSync(target.binary, args, {
3854
+ input: value,
3855
+ cwd: opts.projectPath,
3856
+ encoding: "utf8",
3857
+ shell: false
3858
+ });
3859
+ if (child.status !== 0) {
3860
+ failedInvocation = child.stderr?.trim() || child.error?.message || `exit ${child.status}`;
3861
+ break;
3862
+ }
3863
+ }
3864
+ if (failedInvocation) {
3865
+ result.failed.push({ key, error: failedInvocation });
3866
+ continue;
3867
+ }
3868
+ result.pushed.push(key);
3869
+ if (!opts.silent) {
3870
+ logAudit({
3871
+ action: "push",
3872
+ key,
3873
+ env: opts.env,
3874
+ source: opts.source ?? "cli",
3875
+ detail: `pushed to ${opts.target}${opts.repo ? ` (${opts.repo})` : ""}`
3876
+ });
3877
+ }
3878
+ }
3879
+ return result;
3880
+ }
3881
+
3882
+ // src/cli/commands/bridges.ts
3883
+ function registerBridgeCommands(program2) {
3884
+ program2.command("run <command...>").description(
3885
+ "Run a command with only declared secrets injected \u2014 .q-ring.json manifest keys plus qring:// refs from .env (output auto-redacted)"
3886
+ ).option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("--env-file <files...>", "Env file(s) to load (default: ./.env when present)").option("--no-manifest", "Skip .q-ring.json manifest keys").option("--profile <name>", "Exec profile: unrestricted, restricted, ci").option("--strict", "Missing optional manifest keys are fatal too").option("--dry-run", "Show what would be injected without running").action(async (commandArgs, cmd) => {
3887
+ const opts = {
3888
+ command: commandArgs[0],
3889
+ args: commandArgs.slice(1),
3890
+ projectPath: cmd.projectPath ?? process.cwd(),
3891
+ env: cmd.env,
3892
+ envFiles: cmd.envFile,
3893
+ useManifest: cmd.manifest !== false,
3894
+ profile: cmd.profile,
3895
+ strict: cmd.strict === true,
3896
+ source: "cli"
3897
+ };
3898
+ try {
3899
+ if (cmd.dryRun) {
3900
+ const plan2 = buildRunPlan({ ...opts, silent: true });
3901
+ console.log(`
3902
+ ${SYMBOLS.zap} ${c.bold("qring run")} ${c.dim("(dry run)")}`);
3903
+ if (plan2.envFiles.length > 0) {
3904
+ console.log(` Env files: ${plan2.envFiles.map((f) => c.cyan(f)).join(", ")}`);
3905
+ }
3906
+ for (const v of plan2.injected) {
3907
+ console.log(` ${c.green(SYMBOLS.check)} ${v.name} ${c.dim(`(${v.source})`)}`);
3908
+ }
3909
+ for (const name of plan2.missingOptional) {
3910
+ console.log(` ${c.yellow("?")} ${name} ${c.dim("(optional, missing)")}`);
3911
+ }
3912
+ for (const name of plan2.missingRequired) {
3913
+ console.log(` ${c.red(SYMBOLS.cross)} ${name} ${c.dim("(required, MISSING)")}`);
3914
+ }
3915
+ process.exit(plan2.missingRequired.length > 0 ? 1 : 0);
3916
+ }
3917
+ const { code, plan } = await runCommand(opts);
3918
+ if (plan.missingOptional.length > 0) {
3919
+ console.error(
3920
+ c.yellow(
3921
+ `${SYMBOLS.warning} Optional secrets not found: ${plan.missingOptional.join(", ")}`
3922
+ )
3923
+ );
3924
+ }
3925
+ process.exit(code);
3926
+ } catch (err) {
3927
+ console.error(
3928
+ c.red(
3929
+ `${SYMBOLS.cross} Run failed: ${err instanceof Error ? err.message : String(err)}`
3930
+ )
3931
+ );
3932
+ process.exit(1);
3933
+ }
3934
+ });
3935
+ program2.command("push <target>").description(
3936
+ `Push manifest secrets to a deployment platform via its own CLI (${PUSH_TARGETS.join(", ")}) \u2014 values travel over stdin, never argv`
3937
+ ).option("-k, --keys <keys>", "Comma-separated keys (default: .q-ring.json manifest)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context for superposition collapse").option("--repo <owner/name>", "GitHub repository (github target)").option(
3938
+ "--vercel-env <envs>",
3939
+ "Comma-separated Vercel environments (default: production)"
3940
+ ).option("--dry-run", "Show what would be pushed without pushing").option("--json", "Output as JSON").action((targetArg, cmd) => {
3941
+ if (!PUSH_TARGETS.includes(targetArg)) {
3942
+ console.error(
3943
+ c.red(`${SYMBOLS.cross} Unknown target "${targetArg}" \u2014 expected one of: ${PUSH_TARGETS.join(", ")}`)
3944
+ );
3945
+ process.exit(1);
3946
+ }
3947
+ try {
3948
+ const result = pushSecrets({
3949
+ target: targetArg,
3950
+ keys: cmd.keys?.split(",").map((k) => k.trim()),
3951
+ projectPath: cmd.projectPath ?? process.cwd(),
3952
+ env: cmd.env,
3953
+ repo: cmd.repo,
3954
+ vercelEnvs: cmd.vercelEnv?.split(",").map((e) => e.trim()),
3955
+ dryRun: cmd.dryRun === true,
3956
+ source: "cli"
3957
+ });
3958
+ if (wantsJsonOutput(program2, cmd)) {
3959
+ console.log(JSON.stringify(result, null, 2));
3960
+ process.exit(result.failed.length > 0 ? 1 : 0);
3961
+ }
3962
+ const prefix = cmd.dryRun ? c.dim("[dry-run] ") : "";
3963
+ console.log(`
3964
+ ${SYMBOLS.link} ${prefix}${c.bold(`qring push ${targetArg}`)}`);
3965
+ for (const key of result.pushed) {
3966
+ console.log(` ${c.green(SYMBOLS.check)} ${key}`);
3967
+ }
3968
+ for (const key of result.missing) {
3969
+ console.log(` ${c.yellow("?")} ${key} ${c.dim("(not in keyring \u2014 skipped)")}`);
3970
+ }
3971
+ for (const { key, error } of result.failed) {
3972
+ console.log(` ${c.red(SYMBOLS.cross)} ${key} ${c.dim(`\u2014 ${error}`)}`);
3973
+ }
3974
+ console.log(
3975
+ c.dim(
3976
+ ` ${result.pushed.length} pushed${cmd.dryRun ? " (dry run)" : ""}, ${result.missing.length} missing, ${result.failed.length} failed`
3977
+ )
3978
+ );
3979
+ process.exit(result.failed.length > 0 ? 1 : 0);
3980
+ } catch (err) {
3981
+ console.error(
3982
+ c.red(
3983
+ `${SYMBOLS.cross} Push failed: ${err instanceof Error ? err.message : String(err)}`
3984
+ )
3985
+ );
3986
+ process.exit(1);
3987
+ }
3988
+ });
3989
+ program2.command("setup <editor>").description(
3990
+ `Wire the q-ring MCP server into an editor's MCP config (${EDITORS.join(", ")})`
3991
+ ).option("-g, --global", "Write the per-user config instead of the project one").option("--project-path <path>", "Explicit project path").option("--force", "Replace an existing q-ring entry that differs").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action((editor, cmd) => {
3992
+ if (!EDITORS.includes(editor)) {
3993
+ console.error(
3994
+ c.red(`${SYMBOLS.cross} Unknown editor "${editor}" \u2014 expected one of: ${EDITORS.join(", ")}`)
3995
+ );
3996
+ process.exit(1);
3997
+ }
3998
+ try {
3999
+ const result = setupEditor({
4000
+ editor,
4001
+ global: cmd.global === true,
4002
+ projectPath: cmd.projectPath ?? process.cwd(),
4003
+ force: cmd.force === true,
4004
+ dryRun: cmd.dryRun === true
4005
+ });
4006
+ if (wantsJsonOutput(program2, cmd)) {
4007
+ console.log(JSON.stringify(result, null, 2));
4008
+ return;
4009
+ }
4010
+ const verb = result.action === "created" ? "Created" : result.action === "updated" ? "Updated" : "Already configured";
4011
+ const prefix = result.dryRun ? c.dim("[dry-run] ") : "";
4012
+ console.log(
4013
+ `
4014
+ ${SYMBOLS.check} ${prefix}${c.bold(verb)}: ${c.cyan(result.configPath)} ${c.dim(`(server "${result.serverName}")`)}`
4015
+ );
4016
+ for (const warning of result.warnings) {
4017
+ console.log(` ${c.yellow("!")} ${warning}`);
4018
+ }
4019
+ if (result.action !== "unchanged") {
4020
+ console.log(c.dim(` Restart ${editor} to pick up the MCP server. Verify with: qring doctor`));
4021
+ }
4022
+ } catch (err) {
4023
+ console.error(
4024
+ c.red(
4025
+ `${SYMBOLS.cross} Setup failed: ${err instanceof Error ? err.message : String(err)}`
4026
+ )
4027
+ );
4028
+ process.exit(1);
4029
+ }
4030
+ });
4031
+ }
4032
+
4033
+ // src/cli/commands/doctor.ts
4034
+ import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, rmSync } from "fs";
4035
+ import { join as join5, delimiter } from "path";
4036
+ import { homedir as homedir2 } from "os";
3414
4037
  function auditDir() {
3415
- return process.env.QRING_AUDIT_DIR ?? join3(homedir(), ".config", "q-ring");
4038
+ return process.env.QRING_AUDIT_DIR ?? join5(homedir2(), ".config", "q-ring");
3416
4039
  }
3417
4040
  function checkNode() {
3418
4041
  const major = Number(process.versions.node.split(".")[0]);
@@ -3452,8 +4075,8 @@ async function checkKeyringBackend() {
3452
4075
  function checkAuditLog() {
3453
4076
  const dir = auditDir();
3454
4077
  try {
3455
- const probeFile = join3(dir, ".doctor-probe");
3456
- writeFileSync5(probeFile, "ok");
4078
+ const probeFile = join5(dir, ".doctor-probe");
4079
+ writeFileSync6(probeFile, "ok");
3457
4080
  rmSync(probeFile);
3458
4081
  } catch (err) {
3459
4082
  return {
@@ -3480,8 +4103,8 @@ function checkAuditLog() {
3480
4103
  };
3481
4104
  }
3482
4105
  function checkManifest(projectPath) {
3483
- const manifestPath = join3(projectPath, ".q-ring.json");
3484
- if (!existsSync4(manifestPath)) {
4106
+ const manifestPath = join5(projectPath, ".q-ring.json");
4107
+ if (!existsSync6(manifestPath)) {
3485
4108
  return {
3486
4109
  name: "project manifest",
3487
4110
  status: "warn",
@@ -3489,7 +4112,7 @@ function checkManifest(projectPath) {
3489
4112
  };
3490
4113
  }
3491
4114
  try {
3492
- const config = JSON.parse(readFileSync6(manifestPath, "utf8"));
4115
+ const config = JSON.parse(readFileSync8(manifestPath, "utf8"));
3493
4116
  const declared = Object.keys(config.secrets ?? {}).length;
3494
4117
  const hasPolicy = !!config.policy;
3495
4118
  return {
@@ -3538,11 +4161,11 @@ function checkMcpBinary() {
3538
4161
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
3539
4162
  if (!dir) continue;
3540
4163
  for (const ext of exts) {
3541
- if (existsSync4(join3(dir, `qring-mcp${ext}`))) {
4164
+ if (existsSync6(join5(dir, `qring-mcp${ext}`))) {
3542
4165
  return {
3543
4166
  name: "qring-mcp binary",
3544
4167
  status: "ok",
3545
- detail: `found at ${join3(dir, `qring-mcp${ext}`)}`
4168
+ detail: `found at ${join5(dir, `qring-mcp${ext}`)}`
3546
4169
  };
3547
4170
  }
3548
4171
  }
@@ -3749,6 +4372,11 @@ var COMMAND_GROUPS = [
3749
4372
  symbol: SYMBOLS.zap,
3750
4373
  commands: ["exec", "scan", "lint", "status", "doctor", "completion"]
3751
4374
  },
4375
+ {
4376
+ name: "Bridges",
4377
+ symbol: SYMBOLS.link,
4378
+ commands: ["run", "setup", "push"]
4379
+ },
3752
4380
  {
3753
4381
  name: "Audit & Health",
3754
4382
  symbol: SYMBOLS.eye,
@@ -3889,6 +4517,7 @@ function createProgram() {
3889
4517
  registerHookCommands(program2);
3890
4518
  registerAgentCommands(program2);
3891
4519
  registerSecurityCommands(program2);
4520
+ registerBridgeCommands(program2);
3892
4521
  registerDoctorCommand(program2);
3893
4522
  registerCompletionCommand(program2);
3894
4523
  return program2;