@macintacos/caret 1.1.1 → 1.2.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.
Files changed (98) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +8 -4
  3. package/bin/caret-launcher +36 -8
  4. package/dist/cli.js +1262 -339
  5. package/opencode/caret.core.ts +458 -0
  6. package/opencode/caret.plugin.ts +83 -456
  7. package/opencode/caret.plugin.v2.ts +243 -0
  8. package/opencode/caret.tui.ts +75 -0
  9. package/opencode/index.ts +6 -5
  10. package/opencode/permission.ts +102 -0
  11. package/package.json +2 -1
  12. package/ui/dist/assets/{angular-html-BECXXPyr.js → angular-html-BVXNFElL.js} +1 -1
  13. package/ui/dist/assets/{angular-ts-CJKAKY9F.js → angular-ts-BBdNNVOV.js} +1 -1
  14. package/ui/dist/assets/{apl-CCcfDDqZ.js → apl-DN-9LSnm.js} +1 -1
  15. package/ui/dist/assets/{astro-DKCnAnVR.js → astro-DuMkehjz.js} +1 -1
  16. package/ui/dist/assets/{blade-DQ_JdT2N.js → blade-B_5LVqnt.js} +1 -1
  17. package/ui/dist/assets/{c-D3TwszVj.js → c-5LpWn0DJ.js} +1 -1
  18. package/ui/dist/assets/{chapel-C-2sPcCX.js → chapel-qw3-lXMw.js} +1 -1
  19. package/ui/dist/assets/{cobol-BPmo_4f_.js → cobol-BAnr3Azd.js} +1 -1
  20. package/ui/dist/assets/{coffee-DMTsNDVt.js → coffee-CGRFpAgb.js} +1 -1
  21. package/ui/dist/assets/{cpp-BJr6_Y8R.js → cpp-olfuBwwP.js} +1 -1
  22. package/ui/dist/assets/{crystal-pIWESUkD.js → crystal-qe9b46tp.js} +1 -1
  23. package/ui/dist/assets/{css-DRd9Q4Li.js → css-DkXCh44l.js} +1 -1
  24. package/ui/dist/assets/{dist-aKlRxpD_.js → dist-B1RcczPQ.js} +1 -1
  25. package/ui/dist/assets/{dist-BGfsV7n2.js → dist-BnIbJslb.js} +1 -1
  26. package/ui/dist/assets/{dist-BJ7SLVNC.js → dist-BnbtGh0F.js} +1 -1
  27. package/ui/dist/assets/{dist-BFQBMwmz.js → dist-CFbPxPI-.js} +1 -1
  28. package/ui/dist/assets/{dist-DoJTqy3z.js → dist-CHZr2-kE.js} +1 -1
  29. package/ui/dist/assets/{dist-BNr0c85A.js → dist-CMOjVm1q.js} +1 -1
  30. package/ui/dist/assets/{dist-C6t67BjE.js → dist-CfHYS987.js} +1 -1
  31. package/ui/dist/assets/{dist-CU5aMlNo.js → dist-D8lZm-8I.js} +1 -1
  32. package/ui/dist/assets/{dist-CD25BkNS.js → dist-KWgwFCxG.js} +1 -1
  33. package/ui/dist/assets/{dist-RKKeH1zs.js → dist-iPRbc51X.js} +1 -1
  34. package/ui/dist/assets/{edge-DeNk4F2G.js → edge-hytDa3H3.js} +1 -1
  35. package/ui/dist/assets/{elixir-DHc-DOO8.js → elixir-DnjerbFh.js} +1 -1
  36. package/ui/dist/assets/{elm-CJRqEcvz.js → elm-Cy60APyX.js} +1 -1
  37. package/ui/dist/assets/{erb-BEWGTHDA.js → erb-Rclm0zaT.js} +1 -1
  38. package/ui/dist/assets/{git-rebase-DzlcWhg9.js → git-rebase-Be7W4GuM.js} +1 -1
  39. package/ui/dist/assets/{glimmer-js-B-aWCRIZ.js → glimmer-js-DAN9CyDo.js} +1 -1
  40. package/ui/dist/assets/{glimmer-ts-DcjmPyQn.js → glimmer-ts-Bd4Ltw2M.js} +1 -1
  41. package/ui/dist/assets/{glsl-C9FOT-Uv.js → glsl-DIrrPSk8.js} +1 -1
  42. package/ui/dist/assets/{graphql-BgKSoRp0.js → graphql-CBXqF557.js} +1 -1
  43. package/ui/dist/assets/{hack-BOedpUUE.js → hack-D_K_3nhB.js} +1 -1
  44. package/ui/dist/assets/{haml-Cpngw1vd.js → haml-g7mpgDGW.js} +1 -1
  45. package/ui/dist/assets/{handlebars-CpW1IyBg.js → handlebars-DRWwCyqd.js} +1 -1
  46. package/ui/dist/assets/{html-CV142D8s.js → html-Byvm88Y2.js} +1 -1
  47. package/ui/dist/assets/{html-derivative-Df7h1Nuw.js → html-derivative-DpLoxBsM.js} +1 -1
  48. package/ui/dist/assets/{http-Cneddv9N.js → http-DGq_mf9b.js} +1 -1
  49. package/ui/dist/assets/{hurl-D6TS55N6.js → hurl-qkfW8ihO.js} +1 -1
  50. package/ui/dist/assets/{index-Ci9yCNtV.js → index-BwtwrzNC.js} +4 -4
  51. package/ui/dist/assets/{java-BicD3zQH.js → java-BHSPy0gM.js} +1 -1
  52. package/ui/dist/assets/{javascript-l6vKHWBV.js → javascript-BGu3ZEma.js} +1 -1
  53. package/ui/dist/assets/{jinja-BJF2iot2.js → jinja-BkUiOuUL.js} +1 -1
  54. package/ui/dist/assets/{jison-DiXPFJAT.js → jison-DQ_FQMYl.js} +1 -1
  55. package/ui/dist/assets/{json-BDRuI4UV.js → json-BHyU93c5.js} +1 -1
  56. package/ui/dist/assets/{jsx-BvfdkIOy.js → jsx-DhxI-Xjj.js} +1 -1
  57. package/ui/dist/assets/{julia-bXAhreI5.js → julia-BpY5TdkA.js} +1 -1
  58. package/ui/dist/assets/{just-BOxCfXDg.js → just-Dy9X30SL.js} +1 -1
  59. package/ui/dist/assets/{latex-B9OiTBLF.js → latex-DnqjVTTq.js} +1 -1
  60. package/ui/dist/assets/{liquid-DuLKDeWE.js → liquid-DSLF1wIz.js} +1 -1
  61. package/ui/dist/assets/{lua-Dbh9FQm_.js → lua-CWQ1tn9q.js} +1 -1
  62. package/ui/dist/assets/{marko--tjEBgDM.js → marko-CVOaD8Lr.js} +1 -1
  63. package/ui/dist/assets/{mdc-ByNJpl3m.js → mdc-CorrsEtK.js} +1 -1
  64. package/ui/dist/assets/{nginx-D4wsmFK9.js → nginx--n2A1zuK.js} +1 -1
  65. package/ui/dist/assets/{nim-Cy8RxisP.js → nim-eb5p3yB7.js} +1 -1
  66. package/ui/dist/assets/{org-CAm7o61d.js → org-Cnlel0O3.js} +1 -1
  67. package/ui/dist/assets/{perl-BAvaFGKe.js → perl-DDWGMuBV.js} +1 -1
  68. package/ui/dist/assets/{php-DpAf3w7u.js → php-Bz2SRtKk.js} +1 -1
  69. package/ui/dist/assets/{pug-Dm7otyJU.js → pug-cGPMemK2.js} +1 -1
  70. package/ui/dist/assets/{qml-D4rWQQKC.js → qml-B4Hbmvdi.js} +1 -1
  71. package/ui/dist/assets/{r-DMKUX8qf.js → r-BuuNGonz.js} +1 -1
  72. package/ui/dist/assets/{razor-BIUJkuf_.js → razor-CHpWr94T.js} +1 -1
  73. package/ui/dist/assets/{regexp-D8W_pVHj.js → regexp-9s1znDWq.js} +1 -1
  74. package/ui/dist/assets/{rst-aFr0DP5H.js → rst-BBsXWDuP.js} +1 -1
  75. package/ui/dist/assets/{ruby-BzH6kUKr.js → ruby-pQqViN30.js} +1 -1
  76. package/ui/dist/assets/{sas-CDk9DyCV.js → sas-C4SSIJLx.js} +1 -1
  77. package/ui/dist/assets/{scss-DABbvU5W.js → scss-D8e130Cq.js} +1 -1
  78. package/ui/dist/assets/{shellscript-B0ObP3K-.js → shellscript-CHUzbqv1.js} +1 -1
  79. package/ui/dist/assets/{shellsession-2sv3Jq0j.js → shellsession-BulTL_bH.js} +1 -1
  80. package/ui/dist/assets/{soy-irSRV4oq.js → soy-zGj4V7Ec.js} +1 -1
  81. package/ui/dist/assets/{sql-DJCHoKvP.js → sql-CnJYFgwb.js} +1 -1
  82. package/ui/dist/assets/{stata-a8YnzbIc.js → stata-BXDojqs4.js} +1 -1
  83. package/ui/dist/assets/{surrealql-DdXPOg8s.js → surrealql-3hRFCpnH.js} +1 -1
  84. package/ui/dist/assets/{svelte-ClAKUEZR.js → svelte-C8TzlKVd.js} +1 -1
  85. package/ui/dist/assets/{templ-NUCvvUBA.js → templ-D5KJNb8h.js} +1 -1
  86. package/ui/dist/assets/{tex-DwZeSAfC.js → tex-szX76TU8.js} +1 -1
  87. package/ui/dist/assets/{ts-tags-CFJEHYY_.js → ts-tags-H1v1spsZ.js} +1 -1
  88. package/ui/dist/assets/{tsx-B934yX2T.js → tsx-CoyAoKjY.js} +1 -1
  89. package/ui/dist/assets/{twig-NFkkOj3w.js → twig-pqXRFgM9.js} +1 -1
  90. package/ui/dist/assets/{typescript-7bmMC_xU.js → typescript-CNl_4SRH.js} +1 -1
  91. package/ui/dist/assets/{typst-BQEjp9tn.js → typst-Dr_K1Jqf.js} +1 -1
  92. package/ui/dist/assets/{vue-JqD9ieOF.js → vue-Wl1CPRGX.js} +1 -1
  93. package/ui/dist/assets/{vue-html-CT4XQRX1.js → vue-html-F5NdFsoh.js} +1 -1
  94. package/ui/dist/assets/{vue-vine-DIR27Dmn.js → vue-vine-YWSKUX3v.js} +1 -1
  95. package/ui/dist/assets/{xml-DhsMbw-1.js → xml-crSJzq2-.js} +1 -1
  96. package/ui/dist/assets/{xsl-BvqsVUg9.js → xsl-D-YjJFxJ.js} +1 -1
  97. package/ui/dist/assets/{yaml-C8eWjK_Q.js → yaml-BZWjhwN4.js} +1 -1
  98. package/ui/dist/index.html +1 -1
package/dist/cli.js CHANGED
@@ -2748,6 +2748,43 @@ var init_edit = __esm(() => {
2748
2748
  });
2749
2749
 
2750
2750
  // node_modules/jsonc-parser/lib/esm/main.js
2751
+ function printParseErrorCode(code) {
2752
+ switch (code) {
2753
+ case 1:
2754
+ return "InvalidSymbol";
2755
+ case 2:
2756
+ return "InvalidNumberFormat";
2757
+ case 3:
2758
+ return "PropertyNameExpected";
2759
+ case 4:
2760
+ return "ValueExpected";
2761
+ case 5:
2762
+ return "ColonExpected";
2763
+ case 6:
2764
+ return "CommaExpected";
2765
+ case 7:
2766
+ return "CloseBraceExpected";
2767
+ case 8:
2768
+ return "CloseBracketExpected";
2769
+ case 9:
2770
+ return "EndOfFileExpected";
2771
+ case 10:
2772
+ return "InvalidCommentToken";
2773
+ case 11:
2774
+ return "UnexpectedEndOfComment";
2775
+ case 12:
2776
+ return "UnexpectedEndOfString";
2777
+ case 13:
2778
+ return "UnexpectedEndOfNumber";
2779
+ case 14:
2780
+ return "InvalidUnicode";
2781
+ case 15:
2782
+ return "InvalidEscapeCharacter";
2783
+ case 16:
2784
+ return "InvalidCharacter";
2785
+ }
2786
+ return "<unknown ParseErrorCode>";
2787
+ }
2751
2788
  function modify(text, path, value, options) {
2752
2789
  return setProperty(text, path, value, options);
2753
2790
  }
@@ -2771,12 +2808,13 @@ function applyEdits(text, edits) {
2771
2808
  }
2772
2809
  return text;
2773
2810
  }
2774
- var ScanError, SyntaxKind, parse3, ParseErrorCode;
2811
+ var createScanner2, ScanError, SyntaxKind, parse3, parseTree2, findNodeAtLocation2, ParseErrorCode;
2775
2812
  var init_main = __esm(() => {
2776
2813
  init_format();
2777
2814
  init_edit();
2778
2815
  init_scanner();
2779
2816
  init_parser();
2817
+ createScanner2 = createScanner;
2780
2818
  (function(ScanError) {
2781
2819
  ScanError[ScanError["None"] = 0] = "None";
2782
2820
  ScanError[ScanError["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
@@ -2806,6 +2844,8 @@ var init_main = __esm(() => {
2806
2844
  SyntaxKind[SyntaxKind["EOF"] = 17] = "EOF";
2807
2845
  })(SyntaxKind || (SyntaxKind = {}));
2808
2846
  parse3 = parse2;
2847
+ parseTree2 = parseTree;
2848
+ findNodeAtLocation2 = findNodeAtLocation;
2809
2849
  (function(ParseErrorCode) {
2810
2850
  ParseErrorCode[ParseErrorCode["InvalidSymbol"] = 1] = "InvalidSymbol";
2811
2851
  ParseErrorCode[ParseErrorCode["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
@@ -2827,7 +2867,7 @@ var init_main = __esm(() => {
2827
2867
  });
2828
2868
 
2829
2869
  // src/adapters/opencode/paths.ts
2830
- import { existsSync, readdirSync } from "fs";
2870
+ import { existsSync, readdirSync, realpathSync } from "fs";
2831
2871
  import { homedir as homedir3 } from "os";
2832
2872
  import { basename, dirname, join as join5 } from "path";
2833
2873
  function localPluginSpecifier(repoDir) {
@@ -2849,14 +2889,24 @@ function opencodeConfigDir() {
2849
2889
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
2850
2890
  return join5(xdg || join5(homedir3(), ".config"), "opencode");
2851
2891
  }
2852
- function resolveConfigFile(configDir) {
2853
- for (const name of CONFIG_FILENAMES) {
2892
+ function resolveConfigFile(configDir, names) {
2893
+ for (const name of names) {
2854
2894
  const p = join5(configDir, name);
2855
2895
  if (existsSync(p))
2856
2896
  return p;
2857
2897
  }
2858
2898
  return join5(configDir, "opencode.json");
2859
2899
  }
2900
+ function existingConfigFiles(configDir) {
2901
+ const seen = new Set;
2902
+ return CONFIG_FILENAMES.map((name) => join5(configDir, name)).filter((p) => existsSync(p)).filter((p) => {
2903
+ const real = realpathSync(p);
2904
+ if (seen.has(real))
2905
+ return false;
2906
+ seen.add(real);
2907
+ return true;
2908
+ });
2909
+ }
2860
2910
  function commandDir(configDir) {
2861
2911
  return join5(configDir, COMMAND_DIRNAME);
2862
2912
  }
@@ -2874,15 +2924,48 @@ function existingLegacyInstallFiles(configDir) {
2874
2924
  const commands = entries.filter((e) => !e.isDirectory() && e.name.startsWith(COMMAND_NAMESPACE)).filter((e) => e.name.endsWith(".md")).map((e) => join5(configDir, LEGACY_COMMAND_DIRNAME, e.name)).sort();
2875
2925
  return [...plugins, ...commands].filter((p) => existsSync(p));
2876
2926
  }
2877
- function opencodeCachePackagesDir() {
2927
+ function opencodeCacheRoot() {
2878
2928
  const xdg = process.env.XDG_CACHE_HOME?.trim();
2879
- return join5(xdg || join5(homedir3(), ".cache"), "opencode", "packages");
2929
+ return join5(xdg || join5(homedir3(), ".cache"), "opencode");
2930
+ }
2931
+ function opencodeCachePackagesDir() {
2932
+ return join5(opencodeCacheRoot(), "packages");
2933
+ }
2934
+ function opencodeNpmDir() {
2935
+ return join5(opencodeCacheRoot(), "npm");
2936
+ }
2937
+ function opencodeNpmCacheDir(pkg, version) {
2938
+ return join5(opencodeNpmDir(), `${pkg}@${version ?? "latest"}`);
2939
+ }
2940
+ function opencodeNpmLocalCacheDir(spec) {
2941
+ return join5(opencodeNpmDir(), spec);
2942
+ }
2943
+ function liveGenerationDir(dir) {
2944
+ let names;
2945
+ try {
2946
+ names = readdirSync(dir);
2947
+ } catch {
2948
+ return null;
2949
+ }
2950
+ const generations = names.filter((n) => /^\d+$/.test(n));
2951
+ if (generations.length === 0)
2952
+ return null;
2953
+ return join5(dir, generations.reduce((newest, name) => Number(name) > Number(newest) ? name : newest));
2880
2954
  }
2881
2955
  function opencodeCachePackageDir(specifier = CARET_PACKAGE) {
2882
2956
  return join5(opencodeCachePackagesDir(), specifier);
2883
2957
  }
2884
2958
  function existingOpencodeCachePackageDirs(pkg = CARET_PACKAGE) {
2885
- const bare = opencodeCachePackageDir(pkg);
2959
+ return [
2960
+ ...listCacheDirs(opencodeCachePackageDir(pkg)),
2961
+ ...listCacheDirs(join5(opencodeNpmDir(), pkg))
2962
+ ];
2963
+ }
2964
+ function opencodeRootPaths(pkg = CARET_PACKAGE) {
2965
+ const liveGenerations = listCacheDirs(join5(opencodeNpmDir(), pkg)).map(liveGenerationDir).filter((d) => d !== null);
2966
+ return [...listCacheDirs(opencodeCachePackageDir(pkg)), ...liveGenerations].map((d) => join5(d, "node_modules", pkg));
2967
+ }
2968
+ function listCacheDirs(bare) {
2886
2969
  const parent = dirname(bare);
2887
2970
  const leaf = basename(bare);
2888
2971
  let names;
@@ -2901,9 +2984,53 @@ var init_paths2 = __esm(() => {
2901
2984
  });
2902
2985
 
2903
2986
  // src/adapters/opencode/config-plugin.ts
2904
- function pluginArray(text) {
2987
+ import { isDeepStrictEqual } from "util";
2988
+ function formattingOf(text) {
2989
+ const firstProperty = parseTree2(text)?.children?.[0];
2990
+ const indent = firstProperty ? text.slice(text.lastIndexOf(`
2991
+ `, firstProperty.offset) + 1, firstProperty.offset) : "";
2992
+ if (indent.startsWith("\t"))
2993
+ return { insertSpaces: false, tabSize: 1 };
2994
+ return { insertSpaces: true, tabSize: /^ +$/.test(indent) ? indent.length : 2 };
2995
+ }
2996
+ function configParseError(text) {
2997
+ const { root, errors } = lenientParse(text);
2998
+ const [first] = errors;
2999
+ if (first !== undefined)
3000
+ return `${printParseErrorCode(first.error)} at offset ${first.offset}`;
3001
+ const isObject = typeof root === "object" && root !== null && !Array.isArray(root);
3002
+ return root === undefined || isObject ? null : "not a JSON object";
3003
+ }
3004
+ function lenientParse(text) {
3005
+ const errors = [];
3006
+ const root = parse3(text.replace(/^\uFEFF/, ""), errors, {
3007
+ allowTrailingComma: true,
3008
+ allowEmptyContent: true
3009
+ });
3010
+ return { root, errors };
3011
+ }
3012
+ function checked(existing, edited, intent) {
3013
+ const { key, value } = intent;
3014
+ const { [key]: _, ...rest } = lenientParse(existing).root ?? {};
3015
+ const intended = value === undefined ? rest : { ...rest, [key]: value };
3016
+ const { root, errors } = lenientParse(edited);
3017
+ if (errors.length > 0 || !isDeepStrictEqual(root ?? {}, intended)) {
3018
+ throw new Error(`editing "${key}" would not give the intended config; is "${key}" listed more than once?`);
3019
+ }
3020
+ return edited;
3021
+ }
3022
+ function pluginArray(text, key) {
2905
3023
  const cfg = parse3(text);
2906
- return Array.isArray(cfg?.plugin) ? cfg.plugin : [];
3024
+ const arr = cfg?.[key];
3025
+ return Array.isArray(arr) ? arr : [];
3026
+ }
3027
+ function itemSpec(item) {
3028
+ if (typeof item === "string")
3029
+ return item;
3030
+ if (typeof item === "object" && item !== null && "package" in item && typeof item.package === "string") {
3031
+ return item.package;
3032
+ }
3033
+ return null;
2907
3034
  }
2908
3035
  function splitPluginSpecifier(spec) {
2909
3036
  if (isLocalPluginSpecifier(spec))
@@ -2918,82 +3045,157 @@ function packageName(spec) {
2918
3045
  return splitPluginSpecifier(spec).pkg;
2919
3046
  }
2920
3047
  function entryNames(entry, pkg) {
2921
- return typeof entry === "string" && packageName(entry) === packageName(pkg);
3048
+ const spec = itemSpec(entry);
3049
+ return spec !== null && packageName(spec) === packageName(pkg);
2922
3050
  }
2923
- function addPluginToConfigText(existing, pkg) {
3051
+ function addPluginToConfigText(existing, pkg, key) {
2924
3052
  const text = existing ?? `{}
2925
3053
  `;
2926
- const current = parse3(text)?.plugin;
3054
+ const current = parse3(text)?.[key];
2927
3055
  if (Array.isArray(current)) {
2928
3056
  if (current.some((e) => entryNames(e, pkg)))
2929
3057
  return text;
2930
- const path = ["plugin", current.length];
3058
+ const path = [key, current.length];
2931
3059
  const edits = modify(text, path, pkg, {
2932
3060
  isArrayInsertion: true,
2933
- formattingOptions: FORMATTING
3061
+ formattingOptions: formattingOf(text)
2934
3062
  });
2935
- return applyEdits(text, edits);
3063
+ return checked(text, applyEdits(text, edits), { key, value: [...current, pkg] });
2936
3064
  }
2937
- const edits = modify(text, ["plugin"], [pkg], { formattingOptions: FORMATTING });
2938
- return applyEdits(text, edits);
2939
- }
2940
- function findPluginEntry(existing, pkg) {
2941
- if (existing === null)
2942
- return null;
2943
- const entry = pluginArray(existing).find((e) => entryNames(e, pkg));
2944
- return typeof entry === "string" ? entry : null;
3065
+ const edits = modify(text, [key], [pkg], { formattingOptions: formattingOf(text) });
3066
+ return checked(text, applyEdits(text, edits), { key, value: [pkg] });
2945
3067
  }
2946
- function pluginEntries(existing) {
2947
- return existing === null ? [] : pluginArray(existing).filter((e) => typeof e === "string");
3068
+ function pluginItemSpecs(existing, key) {
3069
+ return pluginArray(existing, key).map(itemSpec);
2948
3070
  }
2949
- function setPluginVersionInConfigText(existing, pkg, version) {
2950
- const arr = pluginArray(existing);
2951
- const i = arr.findIndex((e) => entryNames(e, pkg));
2952
- if (i === -1)
3071
+ function setPluginVersionInConfigText(existing, target) {
3072
+ const { pkg, version, key } = target;
3073
+ const arr = pluginArray(existing, key);
3074
+ const index = arr.findIndex((e) => entryNames(e, pkg));
3075
+ if (index === -1)
2953
3076
  return existing;
2954
- const next = `${packageName(arr[i])}@${version}`;
2955
- const edits = modify(existing, ["plugin", i], next, { formattingOptions: FORMATTING });
2956
- return applyEdits(existing, edits);
2957
- }
2958
- function removePluginFromConfigText(existing, pkg) {
2959
- const arr = pluginArray(existing);
2960
- if (!arr.some((e) => entryNames(e, pkg)))
3077
+ const spec = `${packageName(itemSpec(arr[index]) ?? pkg)}@${version}`;
3078
+ return setPluginItemSpec(existing, { key, index, spec });
3079
+ }
3080
+ function setPluginItemSpec(existing, target) {
3081
+ const { key, index, spec } = target;
3082
+ const arr = pluginArray(existing, key);
3083
+ const item = arr[index];
3084
+ const isObject = typeof item === "object" && item !== null;
3085
+ const path = isObject ? [key, index, "package"] : [key, index];
3086
+ const edits = modify(existing, path, spec, { formattingOptions: formattingOf(existing) });
3087
+ const swapped = isObject ? { ...item, package: spec } : spec;
3088
+ const next = arr.map((e, i) => i === index ? swapped : e);
3089
+ return checked(existing, applyEdits(existing, edits), { key, value: next });
3090
+ }
3091
+ function rewritePluginArray(existing, key, keep) {
3092
+ const arr = pluginArray(existing, key);
3093
+ const dropped = arr.flatMap((item, i) => keep(item, i) ? [] : [i]);
3094
+ if (dropped.length === 0)
2961
3095
  return existing;
2962
- const next = arr.filter((e) => !entryNames(e, pkg));
2963
- const edits = modify(existing, ["plugin"], next, { formattingOptions: FORMATTING });
2964
- return applyEdits(existing, edits);
3096
+ const next = arr.filter((_, i) => !dropped.includes(i));
3097
+ if (key === "plugins" && next.length === 0) {
3098
+ return checked(existing, deleteNode(existing, [key]), { key, value: undefined });
3099
+ }
3100
+ const edited = dropped.reduceRight((text, i) => deleteNode(text, [key, i]), existing);
3101
+ return checked(existing, edited, { key, value: next });
3102
+ }
3103
+ function commaAfter(text, from) {
3104
+ const scanner = createScanner2(text, true);
3105
+ scanner.setPosition(from);
3106
+ scanner.scan();
3107
+ return text[scanner.getTokenOffset()] === "," ? scanner.getTokenOffset() : null;
3108
+ }
3109
+ function wholeLine(text, start, end) {
3110
+ let lineStart = start;
3111
+ let lineEnd = end;
3112
+ while (lineStart > 0 && (text[lineStart - 1] === " " || text[lineStart - 1] === "\t"))
3113
+ lineStart--;
3114
+ while (lineEnd < text.length && (text[lineEnd] === " " || text[lineEnd] === "\t"))
3115
+ lineEnd++;
3116
+ const eol = text.startsWith(`\r
3117
+ `, lineEnd) ? 2 : text[lineEnd] === `
3118
+ ` ? 1 : 0;
3119
+ const aloneOnLine = (lineStart === 0 || text[lineStart - 1] === `
3120
+ `) && (lineEnd === text.length || eol > 0);
3121
+ return aloneOnLine ? { offset: lineStart, length: lineEnd + eol - lineStart, content: "" } : { offset: start, length: end - start, content: "" };
3122
+ }
3123
+ function deleteNode(text, path) {
3124
+ const root = parseTree2(text);
3125
+ const found = root && findNodeAtLocation2(root, path);
3126
+ const node = typeof path.at(-1) === "number" ? found : found?.parent;
3127
+ const siblings = node?.parent?.children;
3128
+ if (!node || !siblings)
3129
+ return text;
3130
+ const i = siblings.indexOf(node);
3131
+ const end = node.offset + node.length;
3132
+ const edits = [];
3133
+ const trailingComma = commaAfter(text, end);
3134
+ if (trailingComma !== null && text.slice(end, trailingComma).trim() === "") {
3135
+ edits.push(wholeLine(text, node.offset, trailingComma + 1));
3136
+ } else {
3137
+ edits.push(wholeLine(text, node.offset, end));
3138
+ const previousSibling = siblings[i - 1];
3139
+ const comma = trailingComma ?? (previousSibling ? commaAfter(text, previousSibling.offset + previousSibling.length) : null);
3140
+ if (comma !== null)
3141
+ edits.push({ offset: comma, length: 1, content: "" });
3142
+ }
3143
+ return applyEdits(text, edits);
2965
3144
  }
2966
- var FORMATTING;
3145
+ var PLUGIN_KEYS;
2967
3146
  var init_config_plugin = __esm(() => {
2968
3147
  init_main();
2969
3148
  init_paths2();
2970
- FORMATTING = { insertSpaces: true, tabSize: 2 };
3149
+ PLUGIN_KEYS = ["plugin", "plugins"];
2971
3150
  });
2972
3151
 
2973
3152
  // src/adapters/opencode/entries.ts
2974
3153
  import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
2975
- import { join as join6 } from "path";
3154
+ import { basename as basename2, join as join6 } from "path";
2976
3155
  function isCaretCheckout(dir) {
2977
3156
  return existsSync2(join6(dir, "opencode", "caret.plugin.ts"));
2978
3157
  }
2979
3158
  function caretEntries(text, isCheckout) {
2980
- return pluginEntries(text).filter((entry) => {
2981
- const path = localSpecifierPath(entry);
2982
- return path === undefined ? splitPluginSpecifier(entry).pkg === CARET_PACKAGE : isCheckout(path);
2983
- });
3159
+ if (text === null)
3160
+ return [];
3161
+ return PLUGIN_KEYS.flatMap((key) => pluginItemSpecs(text, key).flatMap((spec, index) => spec !== null && isCaretSpec(spec, isCheckout) ? [{ key, index, spec }] : []));
3162
+ }
3163
+ function isCaretSpec(spec, isCheckout) {
3164
+ const path = localSpecifierPath(spec);
3165
+ if (path === undefined)
3166
+ return splitPluginSpecifier(spec).pkg === CARET_PACKAGE;
3167
+ return isCaretTarball(path) || isCheckout(path);
3168
+ }
3169
+ function isCaretTarball(path) {
3170
+ const filename = basename2(path);
3171
+ return filename.startsWith(CARET_TARBALL_PREFIX) && filename.endsWith(".tgz");
2984
3172
  }
2985
- function readCaretEntry(configFile) {
2986
- return findPluginEntry(readConfigText(configFile), CARET_PACKAGE);
3173
+ function dropEntries(text, drop) {
3174
+ return PLUGIN_KEYS.reduce((acc, key) => {
3175
+ const droppedIndices = new Set(drop.filter((e) => e.key === key).map((e) => e.index));
3176
+ return droppedIndices.size === 0 ? acc : rewritePluginArray(acc, key, (_, i) => !droppedIndices.has(i));
3177
+ }, text);
2987
3178
  }
2988
- function readLoadedCaretEntry(configFile) {
2989
- return caretEntries(readConfigText(configFile), isCaretCheckout)[0] ?? null;
3179
+ function caretPackageEntry(text) {
3180
+ return caretEntries(text, () => false).find((e) => !isLocalPluginSpecifier(e.spec)) ?? null;
3181
+ }
3182
+ function readCaretEntries(configFiles, isCheckout) {
3183
+ return configFiles.flatMap((f) => caretEntries(readConfigText(f), isCheckout));
3184
+ }
3185
+ function readCaretEntry(configFiles) {
3186
+ return configFiles.map((f) => caretPackageEntry(readConfigText(f))).find((e) => e !== null) ?? null;
3187
+ }
3188
+ function readLoadedCaretEntry(configFiles) {
3189
+ return readCaretEntries(configFiles, isCaretCheckout)[0] ?? null;
2990
3190
  }
2991
3191
  function readConfigText(path) {
2992
3192
  return existsSync2(path) ? readFileSync3(path, "utf-8") : null;
2993
3193
  }
3194
+ var CARET_TARBALL_PREFIX;
2994
3195
  var init_entries = __esm(() => {
2995
3196
  init_config_plugin();
2996
3197
  init_paths2();
3198
+ CARET_TARBALL_PREFIX = `${CARET_PACKAGE.slice(1).replace("/", "-")}-`;
2997
3199
  });
2998
3200
 
2999
3201
  // src/lib/semver.ts
@@ -3015,6 +3217,126 @@ function isNewer(latest, current) {
3015
3217
  return a2 > b2;
3016
3218
  }
3017
3219
 
3220
+ // src/adapters/opencode/host.ts
3221
+ import { accessSync, constants, realpathSync as realpathSync2, statSync } from "fs";
3222
+ import { basename as basename3, delimiter, isAbsolute, join as join7 } from "path";
3223
+ function parseOpencodeVersion(stdout) {
3224
+ return parseVersionTriple(stdout.trim().replace(/^opencode /, ""));
3225
+ }
3226
+ function readOpencodeVersion({
3227
+ bin,
3228
+ timeoutMs = OPENCODE_VERSION_TIMEOUT_MS
3229
+ }) {
3230
+ try {
3231
+ const res = Bun.spawnSync([bin, "--version"], {
3232
+ timeout: timeoutMs,
3233
+ stdin: "ignore",
3234
+ stderr: "ignore"
3235
+ });
3236
+ return res.exitCode === 0 ? parseOpencodeVersion(res.stdout.toString()) : null;
3237
+ } catch {
3238
+ return null;
3239
+ }
3240
+ }
3241
+ function opencodeBinsOnPath(path) {
3242
+ const seen = new Set;
3243
+ return path.split(delimiter).filter((dir) => isAbsolute(dir)).map((dir) => join7(dir, "opencode")).filter((bin) => {
3244
+ const real = executableRealpath(bin);
3245
+ if (real === null || seen.has(real))
3246
+ return false;
3247
+ seen.add(real);
3248
+ return true;
3249
+ });
3250
+ }
3251
+ function executableRealpath(bin) {
3252
+ try {
3253
+ if (!statSync(bin).isFile())
3254
+ return null;
3255
+ accessSync(bin, constants.X_OK);
3256
+ return realpathSync2(bin);
3257
+ } catch {
3258
+ return null;
3259
+ }
3260
+ }
3261
+ function readOpencodeHosts(path = process.env.PATH ?? "", timeoutMs) {
3262
+ return opencodeBinsOnPath(path).map((bin) => ({
3263
+ bin,
3264
+ version: readOpencodeVersion({ bin, timeoutMs })
3265
+ }));
3266
+ }
3267
+ function sharedHost(hosts) {
3268
+ const firstVersion = hosts[0]?.version ?? null;
3269
+ if (firstVersion === null)
3270
+ return null;
3271
+ return hosts.every((h) => h.version !== null && isV2Host(h.version) === isV2Host(firstVersion)) ? firstVersion : null;
3272
+ }
3273
+ function describeHosts(hosts) {
3274
+ if (hosts.length === 0)
3275
+ return "no `opencode` on PATH";
3276
+ return hosts.map(({ bin, version }) => version === null ? `couldn't read \`${bin} --version\`` : `OpenCode ${version.join(".")} at ${bin}`).join(", ");
3277
+ }
3278
+ function isV2Host(version) {
3279
+ return version !== null && version[0] >= 2;
3280
+ }
3281
+ function pluginKeyFor(version) {
3282
+ return isV2Host(version) ? "plugins" : "plugin";
3283
+ }
3284
+ function hostConfigFilenames(host) {
3285
+ return host !== null && !isV2Host(host) ? CONFIG_FILENAMES : CONFIG_FILENAMES.filter((name) => name !== "config.json");
3286
+ }
3287
+ function loadedConfigFiles(configFiles, host) {
3288
+ const names = hostConfigFilenames(host);
3289
+ return configFiles.filter((p) => names.includes(basename3(p)));
3290
+ }
3291
+ function hostCheck(hosts, keys, ignoredFiles) {
3292
+ const base = { id: "opencode-host", title: "OpenCode host" };
3293
+ const found = describeHosts(hosts);
3294
+ if (hosts.every((h) => h.version === null)) {
3295
+ return { ...base, status: "unknown", detail: "", reason: found };
3296
+ }
3297
+ const faults = [];
3298
+ const remedies = [];
3299
+ for (const { bin, version } of hosts) {
3300
+ if (version === null || isV2Host(version))
3301
+ continue;
3302
+ const shown = version.join(".");
3303
+ if (isNewer(V1_FLOOR, shown)) {
3304
+ faults.push(`OpenCode ${shown} at ${bin} is older than ${V1_FLOOR}, the first that loads caret`);
3305
+ remedies.push(`upgrade OpenCode to ${V1_FLOOR} or later`);
3306
+ }
3307
+ }
3308
+ const wrongKey = keys.find((k) => k !== pluginKeyFor(sharedHost(hosts)));
3309
+ if (wrongKey === "plugins") {
3310
+ faults.push("caret is in `plugins`, which only OpenCode v2 loads, and not every `opencode` on PATH reads as v2 (v1 before 1.18.16 refuses to start with the key)");
3311
+ remedies.push("run `caret install`");
3312
+ } else if (wrongKey === "plugin") {
3313
+ faults.push("caret is in the legacy `plugin` key on OpenCode v2");
3314
+ remedies.push("run `caret install`");
3315
+ }
3316
+ if (ignoredFiles.length > 0) {
3317
+ const names = ignoredFiles.map((p) => basename3(p)).join(", ");
3318
+ faults.push(`caret is in ${names}, which OpenCode v2 doesn't load`);
3319
+ remedies.push("run `caret install`");
3320
+ }
3321
+ if (faults.length === 0) {
3322
+ return {
3323
+ ...base,
3324
+ status: "pass",
3325
+ detail: `${found} \u2014 loads caret from \`${keys.join("`, `")}\``
3326
+ };
3327
+ }
3328
+ return {
3329
+ ...base,
3330
+ status: "fail",
3331
+ detail: `${found} \u2014 ${faults.join("; ")}`,
3332
+ remedy: [...new Set(remedies)].join("; ")
3333
+ };
3334
+ }
3335
+ var OPENCODE_VERSION_TIMEOUT_MS = 5000, V1_FLOOR = "1.3.4";
3336
+ var init_host = __esm(() => {
3337
+ init_paths2();
3338
+ });
3339
+
3018
3340
  // node_modules/zod/v4/core/util.js
3019
3341
  function getEnumValues(entries) {
3020
3342
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -3656,9 +3978,12 @@ function config(newConfig) {
3656
3978
  Object.assign(globalConfig, newConfig);
3657
3979
  return globalConfig;
3658
3980
  }
3659
- var _a, _zodDesc, _E, $ZodAsyncError, $ZodEncodeError, globalConfig;
3981
+ var _a, NEVER, _zodDesc, _E, $ZodAsyncError, $ZodEncodeError, globalConfig;
3660
3982
  var init_core = __esm(() => {
3661
3983
  init_util2();
3984
+ NEVER = /* @__PURE__ */ Object.freeze({
3985
+ status: "aborted"
3986
+ });
3662
3987
  _zodDesc = { value: undefined, enumerable: false };
3663
3988
  _E = "captureStackTrace" in Error ? Error : null;
3664
3989
  $ZodAsyncError = class $ZodAsyncError extends Error {
@@ -9260,6 +9585,7 @@ var init_coerce = __esm(() => {
9260
9585
 
9261
9586
  // node_modules/zod/v4/classic/external.js
9262
9587
  var init_external = __esm(() => {
9588
+ init_core2();
9263
9589
  init_core2();
9264
9590
  init_locales();
9265
9591
  init_iso();
@@ -9282,7 +9608,7 @@ var package_default;
9282
9608
  var init_package = __esm(() => {
9283
9609
  package_default = {
9284
9610
  name: "@macintacos/caret",
9285
- version: "1.1.1",
9611
+ version: "1.2.0",
9286
9612
  license: "MIT",
9287
9613
  type: "module",
9288
9614
  description: "A Claude Code plugin that replaces the terminal plan-approval prompt with a local web UI for inline review, annotation, and approval.",
@@ -9333,6 +9659,7 @@ var init_package = __esm(() => {
9333
9659
  "@laststance/tailwind-suggest-canonical-classes": "^0.1.1",
9334
9660
  "@lezer/highlight": "^1.2.3",
9335
9661
  "@modelcontextprotocol/sdk": "^1.30.0",
9662
+ "@opencode/plugin": "^2.0.18",
9336
9663
  "@pierre/diffs": "1.3.6",
9337
9664
  "@pierre/trees": "1.0.0-beta.6",
9338
9665
  "@playwright/test": "^1.62.1",
@@ -9445,7 +9772,7 @@ var init_upstream = __esm(() => {
9445
9772
 
9446
9773
  // src/adapters/opencode/upgrade.ts
9447
9774
  import { existsSync as existsSync3, rmSync } from "fs";
9448
- import { join as join7 } from "path";
9775
+ import { join as join8 } from "path";
9449
9776
  function upgradeVerdict(input) {
9450
9777
  const { entry, cached, published } = input;
9451
9778
  if (published === null) {
@@ -9504,25 +9831,34 @@ function upgradeCheck(verdict) {
9504
9831
  }
9505
9832
  }
9506
9833
  async function readUpgradeVerdict(deps) {
9507
- const entry = readCaretEntry(deps.configFile);
9834
+ const entry = readCaretEntry(deps.configFiles);
9835
+ const cacheDir = deps.cacheDir ?? ((e) => caretCacheDir(e, deps.host));
9508
9836
  return upgradeVerdict({
9509
- entry,
9510
- cached: readEntryCachedVersion(entry, deps.cacheDir),
9837
+ entry: entry?.spec ?? null,
9838
+ cached: readEntryCachedVersion(entry, cacheDir),
9511
9839
  published: await (deps.published ?? publishedCaretVersion)()
9512
9840
  });
9513
9841
  }
9514
- function hasCaretPluginEntry(configFile) {
9515
- return readCaretEntry(configFile) !== null;
9842
+ function caretCacheDir(entry, host) {
9843
+ if (entry.key === "plugin" && !isV2Host(host ?? null)) {
9844
+ return opencodeCachePackageDir(entry.spec);
9845
+ }
9846
+ if (isLocalPluginSpecifier(entry.spec)) {
9847
+ return liveGenerationDir(opencodeNpmLocalCacheDir(entry.spec));
9848
+ }
9849
+ const { pkg, version } = splitPluginSpecifier(entry.spec);
9850
+ return liveGenerationDir(opencodeNpmCacheDir(pkg, version));
9516
9851
  }
9517
- function readEntryCachedVersion(entry, cacheDir = opencodeCachePackageDir) {
9518
- return entry === null ? null : readCachedCaretVersion(cacheDir(entry));
9852
+ function readEntryCachedVersion(entry, cacheDir = caretCacheDir) {
9853
+ const dir = entry === null ? null : cacheDir(entry);
9854
+ return dir === null ? null : readCachedCaretVersion(dir);
9519
9855
  }
9520
9856
  function readCachedCaretVersion(dir) {
9521
- const installed = readJsonFileSync(join7(dir, "node_modules", CARET_PACKAGE, "package.json"));
9857
+ const installed = readJsonFileSync(join8(dir, "node_modules", CARET_PACKAGE, "package.json"));
9522
9858
  if (typeof installed?.version === "string" && installed.version.length > 0) {
9523
9859
  return installed.version;
9524
9860
  }
9525
- const shim = readJsonFileSync(join7(dir, "package.json"));
9861
+ const shim = readJsonFileSync(join8(dir, "package.json"));
9526
9862
  const requested = shim?.dependencies?.[CARET_PACKAGE];
9527
9863
  return typeof requested === "string" && requested.length > 0 ? requested : null;
9528
9864
  }
@@ -9539,6 +9875,7 @@ function clearCachedCaret(dirs = existingOpencodeCachePackageDirs()) {
9539
9875
  var init_upgrade = __esm(() => {
9540
9876
  init_config_plugin();
9541
9877
  init_entries();
9878
+ init_host();
9542
9879
  init_paths2();
9543
9880
  init_json_file();
9544
9881
  init_upstream();
@@ -9546,7 +9883,7 @@ var init_upgrade = __esm(() => {
9546
9883
 
9547
9884
  // src/adapters/opencode/install.ts
9548
9885
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
9549
- import { join as join8 } from "path";
9886
+ import { join as join9 } from "path";
9550
9887
  function readOpencodeInstallState() {
9551
9888
  const dir = opencodeConfigDir();
9552
9889
  if (!existsSync4(dir)) {
@@ -9554,12 +9891,12 @@ function readOpencodeInstallState() {
9554
9891
  }
9555
9892
  let entry;
9556
9893
  try {
9557
- entry = readLoadedCaretEntry(resolveConfigFile(dir));
9894
+ entry = readLoadedCaretEntry(existingConfigFiles(dir));
9558
9895
  } catch {
9559
9896
  return {
9560
9897
  pluginVersion: "unknown",
9561
9898
  pluginEnabled: "unknown",
9562
- hookInUserSettings: readCaretInPluginArray(dir)
9899
+ hookInUserSettings: readCaretInPluginLists(dir)
9563
9900
  };
9564
9901
  }
9565
9902
  const cached = readEntryCachedVersion(entry);
@@ -9567,13 +9904,13 @@ function readOpencodeInstallState() {
9567
9904
  return {
9568
9905
  pluginVersion: version,
9569
9906
  pluginEnabled: version !== "unknown",
9570
- hookInUserSettings: readCaretInPluginArray(dir)
9907
+ hookInUserSettings: readCaretInPluginLists(dir)
9571
9908
  };
9572
9909
  }
9573
- function readCaretInPluginArray(dir) {
9910
+ function readCaretInPluginLists(dir) {
9574
9911
  let sawConfig = false;
9575
9912
  for (const name of CONFIG_FILENAMES) {
9576
- const path = join8(dir, name);
9913
+ const path = join9(dir, name);
9577
9914
  if (!existsSync4(path))
9578
9915
  continue;
9579
9916
  let text;
@@ -9851,6 +10188,9 @@ function daemonStderrLogFile() {
9851
10188
  function daemonLock() {
9852
10189
  return `${stateDir()}/daemon.lock`;
9853
10190
  }
10191
+ function daemonTokenFile() {
10192
+ return `${stateDir()}/daemon.token`;
10193
+ }
9854
10194
  function daemonBootMarker() {
9855
10195
  return `${stateDir()}/daemon.boot`;
9856
10196
  }
@@ -9880,7 +10220,7 @@ var init_paths3 = () => {};
9880
10220
 
9881
10221
  // src/ui/assets.ts
9882
10222
  import { readdirSync as readdirSync2 } from "fs";
9883
- import { dirname as dirname2, join as join9, relative } from "path";
10223
+ import { dirname as dirname2, join as join10, relative } from "path";
9884
10224
  import { fileURLToPath } from "url";
9885
10225
  function fromPathMap(map) {
9886
10226
  const paths = Object.keys(map).sort();
@@ -9896,7 +10236,7 @@ function walkDist(distDir) {
9896
10236
  const found = [];
9897
10237
  const walk = (dir) => {
9898
10238
  for (const ent of readdirSync2(dir, { withFileTypes: true })) {
9899
- const full = join9(dir, ent.name);
10239
+ const full = join10(dir, ent.name);
9900
10240
  if (ent.isDirectory())
9901
10241
  walk(full);
9902
10242
  else if (ent.isFile())
@@ -9934,7 +10274,7 @@ async function loadUiAssets() {
9934
10274
  if (fromDist)
9935
10275
  return fromDist;
9936
10276
  }
9937
- const besideDist = join9(dirname2(process.execPath), "ui");
10277
+ const besideDist = join10(dirname2(process.execPath), "ui");
9938
10278
  return assetsFromDist(besideDist);
9939
10279
  }
9940
10280
  var init_assets = () => {};
@@ -14434,18 +14774,18 @@ import {
14434
14774
  openSync,
14435
14775
  readdirSync as readdirSync3,
14436
14776
  readFileSync as readFileSync5,
14437
- statSync,
14777
+ statSync as statSync2,
14438
14778
  truncateSync,
14439
14779
  unlinkSync,
14440
14780
  writeFileSync
14441
14781
  } from "fs";
14442
- import { basename as basename2 } from "path";
14782
+ import { basename as basename4 } from "path";
14443
14783
  function stamp() {
14444
14784
  return new Date().toISOString().replace(/[-:.]/g, "");
14445
14785
  }
14446
14786
  function sizeOf(path) {
14447
14787
  try {
14448
- return statSync(path).size;
14788
+ return statSync2(path).size;
14449
14789
  } catch {
14450
14790
  return -1;
14451
14791
  }
@@ -14457,7 +14797,7 @@ function claimLock(lock) {
14457
14797
  } catch (e) {
14458
14798
  if (e.code !== "EEXIST")
14459
14799
  return false;
14460
- if (Date.now() - statSync(lock).mtimeMs >= STALE_LOCK_MS)
14800
+ if (Date.now() - statSync2(lock).mtimeMs >= STALE_LOCK_MS)
14461
14801
  unlinkSync(lock);
14462
14802
  return false;
14463
14803
  }
@@ -14476,7 +14816,7 @@ function rotateIfOversized(path, maxSize, keep) {
14476
14816
  try {
14477
14817
  if (sizeOf(path) <= maxSize)
14478
14818
  return;
14479
- const name = basename2(path, ".log");
14819
+ const name = basename4(path, ".log");
14480
14820
  const lock = `${logsDir()}/.rotate-${name}.lock`;
14481
14821
  if (!claimLock(lock))
14482
14822
  return;
@@ -14569,7 +14909,7 @@ function shortId(id) {
14569
14909
  }
14570
14910
  var CENSOR = "<redacted>", DENY_KEYS, MAX_DEPTH = 6;
14571
14911
  var init_core3 = __esm(() => {
14572
- DENY_KEYS = new Set(["plan", "prompt", "feedback"]);
14912
+ DENY_KEYS = new Set(["plan", "prompt", "feedback", "hostnames"]);
14573
14913
  });
14574
14914
 
14575
14915
  // src/redact/node.ts
@@ -14769,9 +15109,11 @@ var init_log = __esm(() => {
14769
15109
  });
14770
15110
 
14771
15111
  // src/config/settings.ts
14772
- import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
15112
+ import { readFileSync as readFileSync7, statSync as statSync3 } from "fs";
15113
+ import { isIP } from "net";
14773
15114
  function freeze(s) {
14774
15115
  Object.freeze(s.logging);
15116
+ Object.freeze(s.daemon.hostnames);
14775
15117
  Object.freeze(s.daemon);
14776
15118
  Object.freeze(s.review);
14777
15119
  Object.freeze(s.opencode);
@@ -14807,7 +15149,7 @@ function loadSettings(file = configFile(), isCompiled = isCompiledBinary()) {
14807
15149
  } catch {
14808
15150
  return DEFAULTS;
14809
15151
  }
14810
- return parseAndValidate(text, isCompiled) ?? DEFAULTS;
15152
+ return parseAndValidate(text, isCompiled) ?? FAIL_CLOSED_DEFAULTS;
14811
15153
  }
14812
15154
  function createSettings(file = configFile(), isCompiled = isCompiledBinary()) {
14813
15155
  let lastGood = DEFAULTS;
@@ -14816,7 +15158,7 @@ function createSettings(file = configFile(), isCompiled = isCompiledBinary()) {
14816
15158
  current() {
14817
15159
  let st;
14818
15160
  try {
14819
- st = statSync2(file);
15161
+ st = statSync3(file);
14820
15162
  } catch {
14821
15163
  return lastGood;
14822
15164
  }
@@ -14833,6 +15175,8 @@ function createSettings(file = configFile(), isCompiled = isCompiledBinary()) {
14833
15175
  lastSig = { mtimeMs: st.mtimeMs, size: st.size };
14834
15176
  if (next !== null)
14835
15177
  lastGood = next;
15178
+ else if (lastGood === DEFAULTS)
15179
+ lastGood = FAIL_CLOSED_DEFAULTS;
14836
15180
  return lastGood;
14837
15181
  }
14838
15182
  };
@@ -14842,15 +15186,24 @@ function diffSettings(prev, next) {
14842
15186
  for (const table of SETTINGS_TABLES) {
14843
15187
  const prevTable = prev[table];
14844
15188
  const nextTable = next[table];
14845
- for (const key of Object.keys(nextTable)) {
15189
+ const keys = new Set([...Object.keys(prevTable), ...Object.keys(nextTable)]);
15190
+ for (const key of keys) {
14846
15191
  const before = prevTable[key];
14847
15192
  const after = nextTable[key];
14848
- if (before !== after)
14849
- changes.push(`${table}.${key}: ${before} \u2192 ${after}`);
15193
+ const [beforeJson, afterJson] = [JSON.stringify(before), JSON.stringify(after)];
15194
+ if (beforeJson === afterJson)
15195
+ continue;
15196
+ if (DENY_KEYS.has(key))
15197
+ changes.push(`${table}.${key}: changed`);
15198
+ else
15199
+ changes.push(`${table}.${key}: ${shown(before, beforeJson)} \u2192 ${shown(after, afterJson)}`);
14850
15200
  }
14851
15201
  }
14852
15202
  return changes;
14853
15203
  }
15204
+ function shown(value, json) {
15205
+ return typeof value === "object" && value !== null ? String(json) : String(value);
15206
+ }
14854
15207
  function watchSettings(inner, onChange) {
14855
15208
  let prev = null;
14856
15209
  return {
@@ -14918,7 +15271,7 @@ function logMaxSize(s = settings().current()) {
14918
15271
  function logKeep(s = settings().current()) {
14919
15272
  return envValue("CARET_LOG_KEEP", LogKeep) ?? s.logging.keep;
14920
15273
  }
14921
- var Port, TimeoutS, IdleMs, HeartbeatMs, LogMaxSize, LogKeep, DevIntervalMs, Dev, SettingsSchema, DEFAULTS, SETTINGS_TABLES, singleton, ENV_VARS, envCache;
15274
+ var Port, TimeoutS, IdleMs, HeartbeatMs, LogMaxSize, LogKeep, Host, Hostname, DevIntervalMs, Dev, SettingsSchema, DEFAULTS, FAIL_CLOSED_DEFAULTS, SETTINGS_TABLES, singleton, ENV_VARS, envCache;
14922
15275
  var init_settings = __esm(() => {
14923
15276
  init_dist();
14924
15277
  init_zod();
@@ -14926,12 +15279,23 @@ var init_settings = __esm(() => {
14926
15279
  init_paths3();
14927
15280
  init_build_id();
14928
15281
  init_log();
15282
+ init_core3();
14929
15283
  Port = number2().int().positive();
14930
15284
  TimeoutS = number2().positive().lt(HOOK_TIMEOUT_S);
14931
15285
  IdleMs = number2().int().nonnegative();
14932
15286
  HeartbeatMs = number2().int().positive().lt(MAX_HEARTBEAT_MS);
14933
15287
  LogMaxSize = number2().int().min(MIN_LOG_MAX_SIZE).max(MAX_LOG_MAX_SIZE);
14934
15288
  LogKeep = number2().int().nonnegative();
15289
+ Host = string2().refine((h) => h === "localhost" || isIP(h) !== 0 && !h.includes("%"), "an IP address or localhost").transform((h) => h === "localhost" ? "127.0.0.1" : h);
15290
+ Hostname = string2().transform((raw, ctx) => {
15291
+ const lower = raw.toLowerCase();
15292
+ try {
15293
+ if (lower !== "" && new URL(`http://${raw}`).hostname === lower)
15294
+ return lower;
15295
+ } catch {}
15296
+ ctx.addIssue({ code: "custom", message: "a bare hostname or IP address" });
15297
+ return NEVER;
15298
+ });
14935
15299
  DevIntervalMs = number2().int().positive();
14936
15300
  Dev = object({
14937
15301
  port: Port.optional(),
@@ -14952,7 +15316,10 @@ var init_settings = __esm(() => {
14952
15316
  daemon: object({
14953
15317
  port: Port.default(DEFAULT_PORT),
14954
15318
  idle_ms: IdleMs.default(60000),
14955
- heartbeat_ms: HeartbeatMs.default(8000)
15319
+ heartbeat_ms: HeartbeatMs.default(8000),
15320
+ host: Host.default("127.0.0.1"),
15321
+ hostnames: array(Hostname).default([]),
15322
+ auth: _enum(["token", "none"]).optional()
14956
15323
  }).prefault({}),
14957
15324
  review: object({
14958
15325
  timeout_s: TimeoutS.default(3600)
@@ -14966,6 +15333,10 @@ var init_settings = __esm(() => {
14966
15333
  dev: Dev
14967
15334
  });
14968
15335
  DEFAULTS = freeze(SettingsSchema.parse({}));
15336
+ FAIL_CLOSED_DEFAULTS = freeze({
15337
+ ...DEFAULTS,
15338
+ daemon: { ...DEFAULTS.daemon, auth: "token" }
15339
+ });
14969
15340
  SETTINGS_TABLES = ["logging", "daemon", "review", "updates"];
14970
15341
  ENV_VARS = [
14971
15342
  ["CARET_PORT", Port],
@@ -14994,21 +15365,170 @@ var init_boot = __esm(() => {
14994
15365
  init_log();
14995
15366
  });
14996
15367
 
15368
+ // src/daemon/address.ts
15369
+ function isExposed(host) {
15370
+ return !(host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host));
15371
+ }
15372
+ function authEnabled(daemon) {
15373
+ return daemon.auth === undefined ? isExposed(daemon.host) : daemon.auth === "token";
15374
+ }
15375
+ function connectHostname(host) {
15376
+ if (host === "0.0.0.0")
15377
+ return "127.0.0.1";
15378
+ if (host === "::")
15379
+ return "[::1]";
15380
+ return new URL(`http://${host.includes(":") ? `[${host}]` : host}`).hostname;
15381
+ }
15382
+ function baseUrlFor(host, port) {
15383
+ return `http://${connectHostname(host)}:${port}`;
15384
+ }
15385
+ function daemonBaseUrl(s) {
15386
+ return baseUrlFor(s.daemon.host, getPort(s));
15387
+ }
15388
+ function localHostname(host) {
15389
+ const name = connectHostname(host);
15390
+ return name === "127.0.0.1" || name === "localhost" ? VANITY_HOST : name;
15391
+ }
15392
+ function publicHostname(daemon) {
15393
+ return daemon.hostnames[0] ?? localHostname(daemon.host);
15394
+ }
15395
+ function publicUrl(daemon, port) {
15396
+ return `http://${publicHostname(daemon)}:${port}`;
15397
+ }
15398
+ function loginLink(url, token) {
15399
+ const u = new URL(url);
15400
+ u.searchParams.set("token", token);
15401
+ return u.href;
15402
+ }
15403
+ var init_address = __esm(() => {
15404
+ init_constants();
15405
+ init_settings();
15406
+ });
15407
+
15408
+ // src/daemon/auth.ts
15409
+ import { timingSafeEqual } from "crypto";
15410
+ function authCookieName(port) {
15411
+ return `caret-auth-${port}`;
15412
+ }
15413
+ function matches(candidate, token) {
15414
+ if (!candidate)
15415
+ return false;
15416
+ const a = Buffer.from(candidate);
15417
+ const b = Buffer.from(token);
15418
+ return a.length === b.length && timingSafeEqual(a, b);
15419
+ }
15420
+ function bearerToken(req) {
15421
+ const header = req.headers.get("authorization");
15422
+ return header?.startsWith("Bearer ") ? header.slice(7) : null;
15423
+ }
15424
+ function reject(url, extra) {
15425
+ const headers = new Headers(extra);
15426
+ headers.set("WWW-Authenticate", AUTH_CHALLENGE);
15427
+ if (url.pathname.startsWith("/api/")) {
15428
+ return Response.json({ error: "caret daemon token required" }, { status: 401, headers });
15429
+ }
15430
+ headers.set("Content-Type", "text/html; charset=utf-8");
15431
+ return new Response(REJECTION_PAGE, { status: 401, headers });
15432
+ }
15433
+ function login(url, presented, auth) {
15434
+ const noReferrer = { "Referrer-Policy": "no-referrer" };
15435
+ if (!matches(presented, auth.token))
15436
+ return reject(url, noReferrer);
15437
+ const params = new URLSearchParams(url.search);
15438
+ params.delete("token");
15439
+ const query = params.size > 0 ? `?${params}` : "";
15440
+ const location = `/${url.pathname.replace(/^\/+/, "")}${query}`;
15441
+ const cookie = new Bun.Cookie(authCookieName(auth.port), auth.token, {
15442
+ httpOnly: true,
15443
+ sameSite: "lax",
15444
+ secure: auth.secure,
15445
+ path: "/",
15446
+ maxAge: COOKIE_MAX_AGE_S
15447
+ });
15448
+ return new Response(null, {
15449
+ status: 303,
15450
+ headers: { ...noReferrer, Location: location, "Set-Cookie": cookie.serialize() }
15451
+ });
15452
+ }
15453
+ function authGate(req, url, auth) {
15454
+ const presented = url.searchParams.get("token");
15455
+ if (req.method === "GET" && presented !== null)
15456
+ return login(url, presented, auth);
15457
+ const cookieToken = new Bun.CookieMap(req.headers.get("cookie") ?? "").get(authCookieName(auth.port));
15458
+ if (matches(bearerToken(req), auth.token) || matches(cookieToken, auth.token))
15459
+ return null;
15460
+ return reject(url);
15461
+ }
15462
+ var COOKIE_MAX_AGE_S = 34560000, REJECTION_PAGE, AUTH_CHALLENGE = 'Bearer realm="caret"';
15463
+ var init_auth = __esm(() => {
15464
+ REJECTION_PAGE = "<!doctype html><title>caret</title><p>This caret daemon requires sign-in. " + "Open caret's login link to continue.</p>";
15465
+ });
15466
+
15467
+ // src/daemon/token.ts
15468
+ import { randomBytes } from "crypto";
15469
+ import { linkSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
15470
+ import { dirname as dirname5 } from "path";
15471
+ function readToken(file) {
15472
+ try {
15473
+ return readFileSync8(file, "utf8").trim() || null;
15474
+ } catch {
15475
+ return null;
15476
+ }
15477
+ }
15478
+ function loadOrMintToken(file) {
15479
+ const existing = readToken(file);
15480
+ if (existing !== null)
15481
+ return existing;
15482
+ const token = randomBytes(32).toString("base64url");
15483
+ ensureStateDir(dirname5(file));
15484
+ const tmp = `${file}.${process.pid}.tmp`;
15485
+ writeFileSync3(tmp, token, { mode: 384 });
15486
+ try {
15487
+ linkSync(tmp, file);
15488
+ return token;
15489
+ } catch (e) {
15490
+ if (e.code !== "EEXIST")
15491
+ throw e;
15492
+ const winner = readToken(file);
15493
+ if (winner === null)
15494
+ throw new Error(`caret daemon token file ${file} exists but holds no readable token`);
15495
+ return winner;
15496
+ } finally {
15497
+ rmSync2(tmp, { force: true });
15498
+ }
15499
+ }
15500
+ var init_token = __esm(() => {
15501
+ init_paths3();
15502
+ });
15503
+
14997
15504
  // src/daemon/client.ts
15505
+ async function daemonFetch(url, init = {}) {
15506
+ const tokenFile = daemonTokenFile();
15507
+ const token = readToken(tokenFile);
15508
+ const headers = new Headers(init.headers);
15509
+ if (token !== null)
15510
+ headers.set("Authorization", `Bearer ${token}`);
15511
+ const res = await fetch(url, { ...init, headers });
15512
+ if (res.status !== 401 || res.headers.get("www-authenticate") !== AUTH_CHALLENGE)
15513
+ return res;
15514
+ throw new DaemonAuthError(token === null ? `the daemon requires a token, but ${tokenFile} does not exist or cannot be read; restart the caret daemon to mint one, or check that XDG_STATE_HOME matches the caret service's` : `the daemon rejected the token in ${tokenFile}; check that XDG_STATE_HOME matches the caret service's`);
15515
+ }
14998
15516
  async function httpHealth(baseUrl) {
14999
15517
  try {
15000
- const res = await fetch(`${baseUrl}/api/health`, {
15518
+ const res = await daemonFetch(`${baseUrl}/api/health`, {
15001
15519
  signal: AbortSignal.timeout(500)
15002
15520
  });
15003
15521
  if (!res.ok)
15004
15522
  return null;
15005
15523
  return await res.json();
15006
- } catch {
15524
+ } catch (e) {
15525
+ if (e instanceof DaemonAuthError)
15526
+ throw e;
15007
15527
  return null;
15008
15528
  }
15009
15529
  }
15010
15530
  async function postReview(baseUrl, input) {
15011
- const res = await fetch(`${baseUrl}/api/reviews`, {
15531
+ const res = await daemonFetch(`${baseUrl}/api/reviews`, {
15012
15532
  method: "POST",
15013
15533
  headers: { "Content-Type": "application/json" },
15014
15534
  body: JSON.stringify(input)
@@ -15023,7 +15543,7 @@ function versionSearch(version) {
15023
15543
  return version === undefined ? "" : `?version=${version}`;
15024
15544
  }
15025
15545
  async function expireReview(baseUrl, id, version) {
15026
- const res = await fetch(`${baseUrl}/api/reviews/${id}/expire${versionSearch(version)}`, {
15546
+ const res = await daemonFetch(`${baseUrl}/api/reviews/${id}/expire${versionSearch(version)}`, {
15027
15547
  method: "POST",
15028
15548
  signal: AbortSignal.timeout(1000)
15029
15549
  });
@@ -15032,7 +15552,7 @@ async function expireReview(baseUrl, id, version) {
15032
15552
  }
15033
15553
  }
15034
15554
  async function longPoll(baseUrl, id, version) {
15035
- const res = await fetch(`${baseUrl}/api/reviews/${id}/decision${versionSearch(version)}`);
15555
+ const res = await daemonFetch(`${baseUrl}/api/reviews/${id}/decision${versionSearch(version)}`);
15036
15556
  if (res.status === 204)
15037
15557
  return null;
15038
15558
  if (res.status === 409)
@@ -15042,13 +15562,13 @@ async function longPoll(baseUrl, id, version) {
15042
15562
  return await res.json();
15043
15563
  }
15044
15564
  async function listReviews(baseUrl) {
15045
- const res = await fetch(`${baseUrl}/api/reviews`, { signal: AbortSignal.timeout(1000) });
15565
+ const res = await daemonFetch(`${baseUrl}/api/reviews`, { signal: AbortSignal.timeout(1000) });
15046
15566
  if (!res.ok)
15047
15567
  throw new Error(`GET /api/reviews failed: ${res.status}`);
15048
15568
  return await res.json();
15049
15569
  }
15050
15570
  async function resolveReview(baseUrl, id, body) {
15051
- const res = await fetch(`${baseUrl}/api/reviews/${id}/resolve`, {
15571
+ const res = await daemonFetch(`${baseUrl}/api/reviews/${id}/resolve`, {
15052
15572
  method: "POST",
15053
15573
  headers: { "Content-Type": "application/json" },
15054
15574
  body: JSON.stringify(body),
@@ -15057,28 +15577,44 @@ async function resolveReview(baseUrl, id, body) {
15057
15577
  if (!res.ok)
15058
15578
  throw new Error(`POST /resolve failed: ${res.status}`);
15059
15579
  }
15580
+ var DaemonAuthError;
15581
+ var init_client = __esm(() => {
15582
+ init_paths3();
15583
+ init_auth();
15584
+ init_token();
15585
+ DaemonAuthError = class DaemonAuthError extends Error {
15586
+ };
15587
+ });
15060
15588
 
15061
15589
  // src/daemon/lifecycle.ts
15062
15590
  import {
15063
- accessSync,
15591
+ accessSync as accessSync2,
15064
15592
  chmodSync as chmodSync2,
15065
- constants,
15593
+ constants as constants2,
15066
15594
  existsSync as existsSync7,
15067
- linkSync,
15595
+ linkSync as linkSync2,
15068
15596
  openSync as openSync2,
15069
- readFileSync as readFileSync8,
15597
+ readFileSync as readFileSync9,
15070
15598
  renameSync as renameSync2,
15071
- statSync as statSync3,
15599
+ statSync as statSync4,
15072
15600
  unlinkSync as unlinkSync2,
15073
- writeFileSync as writeFileSync3
15601
+ writeFileSync as writeFileSync4
15074
15602
  } from "fs";
15075
- import { join as join10, normalize } from "path";
15603
+ import { createServer as createNetServer } from "net";
15604
+ import { join as join11, normalize } from "path";
15076
15605
  function isAddrInUse(e) {
15077
15606
  if (e && typeof e === "object" && "code" in e) {
15078
15607
  return e.code === "EADDRINUSE";
15079
15608
  }
15080
15609
  return e instanceof Error && /EADDRINUSE/.test(e.message);
15081
15610
  }
15611
+ function isAddrNotAvail(host) {
15612
+ return new Promise((resolve) => {
15613
+ const probe = createNetServer();
15614
+ probe.once("error", (e) => resolve(e.code === "EADDRNOTAVAIL"));
15615
+ probe.listen({ host, port: 0 }, () => probe.close(() => resolve(false)));
15616
+ });
15617
+ }
15082
15618
  function sameWorldPath(a, b) {
15083
15619
  return normalize(a) === normalize(b);
15084
15620
  }
@@ -15089,7 +15625,7 @@ async function ensureDaemon(deps, mode = "takeover") {
15089
15625
  const { timing } = deps;
15090
15626
  const windowEnd = timing.now() + timing.windowMs;
15091
15627
  const deadline = windowEnd + timing.reserveMs;
15092
- let windowSpent = mode === "successor" ? await awaitDrained(deps, windowEnd) : false;
15628
+ let windowSpent = mode === "successor" ? await awaitDrained(deps, windowEnd) : mode === "takeover" && await moveStray(deps, windowEnd);
15093
15629
  let supervised;
15094
15630
  let spawnedPid;
15095
15631
  let waitingOn;
@@ -15110,7 +15646,7 @@ async function ensureDaemon(deps, mode = "takeover") {
15110
15646
  if (deps.pinned || isNewer(h.version ?? "", deps.currentVersion))
15111
15647
  return deps.baseUrl;
15112
15648
  windowSpent = true;
15113
- if (await restartService(deps.service, h)) {
15649
+ if (await restartService(deps.service, h, "stale daemon build")) {
15114
15650
  await awaitSuccessor(deps, { prev: h, step: "service", until: windowEnd });
15115
15651
  }
15116
15652
  continue;
@@ -15125,6 +15661,11 @@ async function ensureDaemon(deps, mode = "takeover") {
15125
15661
  if (h && h.service !== "caret") {
15126
15662
  throw new Error(`port is held by a non-caret process \u2014 set CARET_PORT to a free port`);
15127
15663
  }
15664
+ if (mode !== "takeover") {
15665
+ const stray = await strayDaemon(deps.readLock(), deps.baseUrl, deps.health);
15666
+ if (stray)
15667
+ return stray.url;
15668
+ }
15128
15669
  if (!windowSpent && deps.service) {
15129
15670
  supervised ??= await supervisorExpected(deps.service);
15130
15671
  if (supervised) {
@@ -15206,9 +15747,9 @@ async function awaitSuccessor(deps, { prev, step, until }) {
15206
15747
  logWarn(step, "no daemon took the port in time", { instanceId: prev?.instanceId });
15207
15748
  return false;
15208
15749
  }
15209
- async function restartService(service, stale) {
15750
+ async function restartService(service, stale, reason) {
15210
15751
  const ctx = { instanceId: stale.instanceId, build: stale.build };
15211
- logInfo("service", "restarting service: stale daemon build", ctx);
15752
+ logInfo("service", `restarting service: ${reason}`, ctx);
15212
15753
  try {
15213
15754
  await service.restart();
15214
15755
  return true;
@@ -15217,27 +15758,74 @@ async function restartService(service, stale) {
15217
15758
  return false;
15218
15759
  }
15219
15760
  }
15761
+ async function strayDaemon(lock, baseUrl, health) {
15762
+ if (typeof lock?.host !== "string" || lock.instanceId === undefined)
15763
+ return null;
15764
+ let lockUrl;
15765
+ try {
15766
+ lockUrl = baseUrlFor(lock.host, lock.port);
15767
+ } catch {
15768
+ return null;
15769
+ }
15770
+ if (lockUrl === baseUrl)
15771
+ return null;
15772
+ const h = await health(lockUrl);
15773
+ if (h?.service !== "caret" || h.instanceId !== lock.instanceId)
15774
+ return null;
15775
+ if ((await health(baseUrl))?.instanceId === lock.instanceId)
15776
+ return null;
15777
+ return { url: lockUrl, health: h };
15778
+ }
15779
+ async function moveStray(deps, windowEnd) {
15780
+ const lock = deps.readLock();
15781
+ const stray = await strayDaemon(lock, deps.baseUrl, deps.health);
15782
+ if (!stray)
15783
+ return false;
15784
+ if (deps.service && (stray.health.supervised ?? stray.health.resident) === true) {
15785
+ if (await restartService(deps.service, stray.health, "daemon on a stale address")) {
15786
+ await awaitSuccessor(deps, { prev: null, step: "service", until: windowEnd });
15787
+ }
15788
+ return true;
15789
+ }
15790
+ const ctx = { instanceId: stray.health.instanceId };
15791
+ if (await deps.retire(stray.url, lock)) {
15792
+ logInfo("retire", "daemon on a stale address retiring", ctx);
15793
+ } else {
15794
+ logWarn("retire", "daemon on a stale address not retired", ctx);
15795
+ }
15796
+ return false;
15797
+ }
15220
15798
  async function supervisorExpected(service) {
15221
15799
  const status = await service.status().catch(() => null);
15222
15800
  return status?.keepsAlive === true;
15223
15801
  }
15224
15802
  async function vacatePort(deps) {
15803
+ try {
15804
+ return await vacate(deps);
15805
+ } catch (e) {
15806
+ if (e instanceof DaemonAuthError)
15807
+ return e.message;
15808
+ throw e;
15809
+ }
15810
+ }
15811
+ async function vacate(deps) {
15225
15812
  const deadline = deps.now() + deps.deadlineMs;
15226
15813
  let retired;
15814
+ const url = (await strayDaemon(deps.readLock(), deps.baseUrl, deps.health))?.url ?? deps.baseUrl;
15227
15815
  for (;; ) {
15228
- const h = await deps.health(deps.baseUrl);
15816
+ const h = await deps.health(url);
15229
15817
  if (h?.service !== "caret")
15230
15818
  return null;
15231
15819
  if (isForeignWorld(h, deps.currentStateDir))
15232
15820
  return FOREIGN_WORLD_ERROR;
15233
15821
  if ((h.supervised ?? h.resident) === true) {
15234
- return "caret's service already serves the review UI on this port \u2014 run `caret install` and answer \"I'll run it myself\" to serve it from a terminal instead";
15822
+ return "caret's service already serves the review UI \u2014 run `caret install` and answer \"I'll run it myself\" to serve it from a terminal instead";
15235
15823
  }
15236
15824
  if (deps.now() >= deadline)
15237
- return "the caret daemon on this port did not stop in time";
15825
+ return "the caret daemon did not stop in time";
15238
15826
  if (h.instanceId === undefined || h.instanceId !== retired) {
15239
- if (!await deps.retire(deps.baseUrl)) {
15240
- return "the caret daemon on this port could not be asked to stop";
15827
+ if (!await deps.retire(url)) {
15828
+ return "the caret daemon could not be asked to stop";
15241
15829
  }
15242
15830
  retired = h.instanceId;
15243
15831
  }
@@ -15260,7 +15848,7 @@ function fileBootMarker() {
15260
15848
  const write = (marker, place) => {
15261
15849
  const path = daemonBootMarker();
15262
15850
  const tmp = `${path}.${process.pid}.tmp`;
15263
- writeFileSync3(tmp, JSON.stringify(marker), { mode: 384 });
15851
+ writeFileSync4(tmp, JSON.stringify(marker), { mode: 384 });
15264
15852
  try {
15265
15853
  place(tmp, path);
15266
15854
  } finally {
@@ -15271,7 +15859,7 @@ function fileBootMarker() {
15271
15859
  claim: (marker) => {
15272
15860
  try {
15273
15861
  ensureStateDir();
15274
- write(marker, linkSync);
15862
+ write(marker, linkSync2);
15275
15863
  return true;
15276
15864
  } catch (e) {
15277
15865
  return e.code !== "EEXIST";
@@ -15312,13 +15900,16 @@ function removeOwnDaemonLock() {
15312
15900
  }
15313
15901
  async function retireDaemon(baseUrl, lock, currentStateDir, kill = (pid, sig) => process.kill(pid, sig)) {
15314
15902
  try {
15315
- const res = await fetch(`${baseUrl}/api/retire`, {
15903
+ const res = await daemonFetch(`${baseUrl}/api/retire`, {
15316
15904
  method: "POST",
15317
15905
  signal: AbortSignal.timeout(1000)
15318
15906
  });
15319
15907
  if (res.ok)
15320
15908
  return true;
15321
- } catch {}
15909
+ } catch (e) {
15910
+ if (e instanceof DaemonAuthError)
15911
+ throw e;
15912
+ }
15322
15913
  const sameWorld = lock?.stateDir === undefined || sameWorldPath(lock.stateDir, currentStateDir);
15323
15914
  if (lock && sameWorld && isPidAlive(lock.pid)) {
15324
15915
  try {
@@ -15372,7 +15963,7 @@ function isLauncherPinned() {
15372
15963
  }
15373
15964
  function readPinnedRoot() {
15374
15965
  try {
15375
- const text = readFileSync8(launcherPinnedRootFile(), "utf8");
15966
+ const text = readFileSync9(launcherPinnedRootFile(), "utf8");
15376
15967
  const nl = text.indexOf(`
15377
15968
  `);
15378
15969
  if (nl < 0)
@@ -15384,12 +15975,12 @@ function readPinnedRoot() {
15384
15975
  }
15385
15976
  function isRunnableRoot(root) {
15386
15977
  try {
15387
- accessSync(join10(root, "bin", "caret"), constants.X_OK);
15978
+ accessSync2(join11(root, "bin", "caret"), constants2.X_OK);
15388
15979
  try {
15389
- accessSync(join10(root, "bin", "caret-native"), constants.X_OK);
15980
+ accessSync2(join11(root, "bin", "caret-native"), constants2.X_OK);
15390
15981
  return true;
15391
15982
  } catch {
15392
- return statSync3(join10(root, "ui", "dist", "index.html")).isFile();
15983
+ return statSync4(join11(root, "ui", "dist", "index.html")).isFile();
15393
15984
  }
15394
15985
  } catch {
15395
15986
  return false;
@@ -15400,7 +15991,7 @@ async function prodEnsureDeps(s, service, reserveMs) {
15400
15991
  return {
15401
15992
  service: existsSync7(launcherServiceFile()) ? service() : undefined,
15402
15993
  pinned: isLauncherPinned(),
15403
- baseUrl: `http://localhost:${getPort(s)}`,
15994
+ baseUrl: daemonBaseUrl(s),
15404
15995
  currentBuild: await currentBuildId(),
15405
15996
  currentVersion: VERSION,
15406
15997
  currentStateDir: world,
@@ -15425,6 +16016,8 @@ var BOOT_MARKER_TTL_MS = 30000, FOREIGN_WORLD_ERROR = "port serves a different c
15425
16016
  var init_lifecycle = __esm(() => {
15426
16017
  init_paths3();
15427
16018
  init_settings();
16019
+ init_address();
16020
+ init_client();
15428
16021
  init_build_id();
15429
16022
  init_json_file();
15430
16023
  init_log();
@@ -16816,7 +17409,7 @@ __export(exports_dist, {
16816
17409
  import { styleText as styleText2, stripVTControlCharacters } from "util";
16817
17410
  import process$1 from "process";
16818
17411
  import { existsSync as existsSync11, lstatSync, readdirSync as readdirSync4 } from "fs";
16819
- import { dirname as dirname8, join as join16 } from "path";
17412
+ import { dirname as dirname9, join as join17 } from "path";
16820
17413
  function isUnicodeSupported() {
16821
17414
  if (process$1.platform !== "win32") {
16822
17415
  return process$1.env.TERM !== "linux";
@@ -17527,10 +18120,10 @@ ${n}
17527
18120
  return [];
17528
18121
  try {
17529
18122
  let i;
17530
- existsSync11(t) ? lstatSync(t).isDirectory() && (!e.directory || t.endsWith("/")) ? i = t : i = dirname8(t) : i = dirname8(t);
18123
+ existsSync11(t) ? lstatSync(t).isDirectory() && (!e.directory || t.endsWith("/")) ? i = t : i = dirname9(t) : i = dirname9(t);
17531
18124
  const c = t.length > 1 && t.endsWith("/") ? t.slice(0, -1) : t;
17532
18125
  return readdirSync4(i).map((r) => {
17533
- const n = join16(i, r), m = lstatSync(n);
18126
+ const n = join17(i, r), m = lstatSync(n);
17534
18127
  return {
17535
18128
  name: r,
17536
18129
  path: n,
@@ -33764,7 +34357,7 @@ init_dist();
33764
34357
  init_paths3();
33765
34358
  import { mkdir, readFile as readFile3, stat as stat2 } from "fs/promises";
33766
34359
  import { dirname as dirname4 } from "path";
33767
- import { isDeepStrictEqual } from "util";
34360
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
33768
34361
 
33769
34362
  // src/lib/atomic-write.ts
33770
34363
  import { realpath as realpath2, rename, rm, writeFile } from "fs/promises";
@@ -33858,7 +34451,7 @@ function withUpdatesCheck(text, check) {
33858
34451
  return null;
33859
34452
  if (after.updates?.check !== check)
33860
34453
  return null;
33861
- if (!isDeepStrictEqual(withoutUpdatesCheck(before), withoutUpdatesCheck(after)))
34454
+ if (!isDeepStrictEqual2(withoutUpdatesCheck(before), withoutUpdatesCheck(after)))
33862
34455
  return null;
33863
34456
  return next;
33864
34457
  }
@@ -33933,6 +34526,7 @@ function optedOut(text) {
33933
34526
 
33934
34527
  // src/commands/daemon.ts
33935
34528
  init_settings();
34529
+ init_address();
33936
34530
 
33937
34531
  // src/daemon/update-changes.ts
33938
34532
  var COMMIT_CAP = 50;
@@ -34056,14 +34650,17 @@ function buildDiagnostics(deps) {
34056
34650
  init_lifecycle();
34057
34651
 
34058
34652
  // src/daemon/server.ts
34059
- import { renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
34060
- import { dirname as dirname6 } from "path";
34653
+ import { renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "fs";
34654
+ import { dirname as dirname7 } from "path";
34061
34655
  init_constants();
34062
34656
  init_paths3();
34063
34657
  init_settings();
34658
+ init_address();
34659
+ init_auth();
34064
34660
 
34065
34661
  // src/daemon/guards.ts
34066
34662
  init_constants();
34663
+ init_address();
34067
34664
  var LIVE_CLIENT_WINDOW_MS = 120000;
34068
34665
  function isClientLive(lastPollAt, now, windowMs) {
34069
34666
  return lastPollAt !== 0 && now - lastPollAt < windowMs;
@@ -34071,38 +34668,53 @@ function isClientLive(lastPollAt, now, windowMs) {
34071
34668
  function isSafeMethod(method) {
34072
34669
  return method === "GET" || method === "HEAD";
34073
34670
  }
34074
- var OWN_HOSTNAMES = new Set(["127.0.0.1", "localhost", VANITY_HOST]);
34075
- function isOwnAuthority(authority, port) {
34671
+ var BUILT_IN_NAMES = ["127.0.0.1", "localhost", VANITY_HOST];
34672
+ var LOOPBACK_NAMES = ownNames("127.0.0.1", []);
34673
+ function ownNames(connectHostname, hostnames) {
34674
+ const anyPortSafe = (h) => !BUILT_IN_NAMES.includes(h) && isExposed(h.replace(/^\[(.*)\]$/, "$1"));
34675
+ return {
34676
+ exact: new Set([...BUILT_IN_NAMES, connectHostname]),
34677
+ anyPort: new Set(hostnames.filter(anyPortSafe))
34678
+ };
34679
+ }
34680
+ function isOwnAuthority(authority, port, names) {
34076
34681
  try {
34077
34682
  const u = new URL(`http://${authority}`);
34078
34683
  if (u.href !== `http://${u.host}/`)
34079
34684
  return false;
34080
- return OWN_HOSTNAMES.has(u.hostname) && (u.port || "80") === String(port);
34685
+ if (names.anyPort.has(u.hostname))
34686
+ return true;
34687
+ return names.exact.has(u.hostname) && (u.port || "80") === String(port);
34081
34688
  } catch {
34082
34689
  return false;
34083
34690
  }
34084
34691
  }
34085
- function isForeignHost(req, port) {
34692
+ function isForeignHost(req, port, names = LOOPBACK_NAMES) {
34086
34693
  const host = req.headers.get("host");
34087
- return host === null || !isOwnAuthority(host, port);
34694
+ return host === null || !isOwnAuthority(host, port, names);
34088
34695
  }
34089
- function isOwnOrigin(origin, port) {
34696
+ function isOwnOrigin(origin, port, names) {
34090
34697
  try {
34091
34698
  const u = new URL(origin);
34092
- return u.protocol === "http:" && isOwnAuthority(u.host, port);
34699
+ if (u.protocol === "https:")
34700
+ return names.anyPort.has(u.hostname);
34701
+ return u.protocol === "http:" && isOwnAuthority(u.host, port, names);
34093
34702
  } catch {
34094
34703
  return false;
34095
34704
  }
34096
34705
  }
34097
- function isCrossOrigin(req, port) {
34706
+ function isCrossOrigin(req, port, names = LOOPBACK_NAMES) {
34098
34707
  const origin = req.headers.get("origin");
34099
- if (origin && !isOwnOrigin(origin, port))
34708
+ if (origin && !isOwnOrigin(origin, port, names))
34100
34709
  return true;
34101
34710
  const site = req.headers.get("sec-fetch-site");
34102
34711
  if (site && site !== "same-origin" && site !== "none")
34103
34712
  return true;
34104
34713
  return false;
34105
34714
  }
34715
+ function isForwardedHttps(req, bindHost) {
34716
+ return !isExposed(bindHost) && req.headers.get("x-forwarded-proto") === "https";
34717
+ }
34106
34718
 
34107
34719
  // src/daemon/liveness.ts
34108
34720
  function createLiveness(deps) {
@@ -34271,6 +34883,7 @@ async function parseBody(req, schema) {
34271
34883
  }
34272
34884
 
34273
34885
  // src/daemon/server.ts
34886
+ init_token();
34274
34887
  init_build_id();
34275
34888
 
34276
34889
  // src/lib/cmux.ts
@@ -34304,13 +34917,13 @@ init_log();
34304
34917
 
34305
34918
  // src/plan/directory.ts
34306
34919
  import { readdir as readdir4 } from "fs/promises";
34307
- import { join as join12, relative as relative3, sep as sep4 } from "path";
34920
+ import { join as join13, relative as relative3, sep as sep4 } from "path";
34308
34921
 
34309
34922
  // src/plan/excerpt.ts
34310
34923
  init_constants();
34311
- import { readFileSync as readFileSync9 } from "fs";
34924
+ import { readFileSync as readFileSync10 } from "fs";
34312
34925
  import { readdir as readdir3, realpath as realpath3, stat as stat3 } from "fs/promises";
34313
- import { extname, isAbsolute, join as join11, relative as relative2, resolve, sep as sep3 } from "path";
34926
+ import { extname, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve, sep as sep3 } from "path";
34314
34927
  var EXCERPT_HEAD_LINES = 60;
34315
34928
  var MAX_EXCERPT_BYTES = 10 * 1024 * 1024;
34316
34929
  var MAX_SCAN_ENTRIES = 5000;
@@ -34401,16 +35014,16 @@ async function basenameSearch(cwdReal, name) {
34401
35014
  return null;
34402
35015
  if (entry.isFile()) {
34403
35016
  if (entry.name === name)
34404
- return join11(currentDir, entry.name);
35017
+ return join12(currentDir, entry.name);
34405
35018
  } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
34406
- queue.push(join11(currentDir, entry.name));
35019
+ queue.push(join12(currentDir, entry.name));
34407
35020
  }
34408
35021
  }
34409
35022
  }
34410
35023
  return null;
34411
35024
  }
34412
35025
  async function resolveInCwd(cwd, candidate) {
34413
- if (cwd === "" || !isAbsolute(cwd))
35026
+ if (cwd === "" || !isAbsolute2(cwd))
34414
35027
  return null;
34415
35028
  const cwdReal = await safeRealpath(cwd);
34416
35029
  if (cwdReal === null)
@@ -34450,7 +35063,7 @@ function readLines(abs, stats) {
34450
35063
  return memo2.lines;
34451
35064
  let content;
34452
35065
  try {
34453
- content = readFileSync9(abs, "utf-8");
35066
+ content = readFileSync10(abs, "utf-8");
34454
35067
  } catch {
34455
35068
  return null;
34456
35069
  }
@@ -34535,7 +35148,7 @@ async function listDirectory(cwd, root, path) {
34535
35148
  else if (d.isSymbolicLink())
34536
35149
  links.push(d);
34537
35150
  }
34538
- const targets = await Promise.all(links.map((d) => resolveInCwd(cwd, join12(target.path, d.name))));
35151
+ const targets = await Promise.all(links.map((d) => resolveInCwd(cwd, join13(target.path, d.name))));
34539
35152
  targets.forEach((hit, i) => {
34540
35153
  const link = links[i];
34541
35154
  if (hit !== null && link !== undefined)
@@ -34552,7 +35165,7 @@ async function listDirectory(cwd, root, path) {
34552
35165
 
34553
35166
  // src/plan/file-search.ts
34554
35167
  import { readdir as readdir5 } from "fs/promises";
34555
- import { isAbsolute as isAbsolute2, join as join13 } from "path";
35168
+ import { isAbsolute as isAbsolute3, join as join14 } from "path";
34556
35169
  var SEARCH_BUDGET = { dirents: 20000, results: 50 };
34557
35170
  var MAX_SEARCH_QUERY_CHARS = 128;
34558
35171
  function subsequence(haystack, needle) {
@@ -34587,7 +35200,7 @@ function partition(dirents, allowance) {
34587
35200
  return { files, dirs, scanned, cut: false };
34588
35201
  }
34589
35202
  async function searchFiles(cwd, query, budget = SEARCH_BUDGET) {
34590
- if (cwd === "" || !isAbsolute2(cwd))
35203
+ if (cwd === "" || !isAbsolute3(cwd))
34591
35204
  return null;
34592
35205
  const cwdReal = await safeRealpath(cwd);
34593
35206
  if (cwdReal === null)
@@ -34600,7 +35213,7 @@ async function searchFiles(cwd, query, budget = SEARCH_BUDGET) {
34600
35213
  const rel = queue.shift();
34601
35214
  let dirents;
34602
35215
  try {
34603
- dirents = await readdir5(join13(cwdReal, rel), { withFileTypes: true });
35216
+ dirents = await readdir5(join14(cwdReal, rel), { withFileTypes: true });
34604
35217
  } catch {
34605
35218
  continue;
34606
35219
  }
@@ -34673,7 +35286,7 @@ function createDecisions(log = noopLogger) {
34673
35286
  }
34674
35287
 
34675
35288
  // src/review/threading.ts
34676
- import { randomBytes } from "crypto";
35289
+ import { randomBytes as randomBytes2 } from "crypto";
34677
35290
 
34678
35291
  // node_modules/marked/lib/marked.esm.js
34679
35292
  function A() {
@@ -36055,13 +36668,13 @@ var un = x.lex;
36055
36668
  init_log();
36056
36669
 
36057
36670
  // src/plan/canonical-file.ts
36058
- import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync10, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
36671
+ import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync11, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
36059
36672
  function isPlanFile(path) {
36060
- return path.endsWith(".md") && existsSync8(path) && statSync4(path).isFile();
36673
+ return path.endsWith(".md") && existsSync8(path) && statSync5(path).isFile();
36061
36674
  }
36062
36675
  function readPlanFile(path) {
36063
36676
  try {
36064
- return isPlanFile(path) ? readFileSync10(path, "utf8") : undefined;
36677
+ return isPlanFile(path) ? readFileSync11(path, "utf8") : undefined;
36065
36678
  } catch {
36066
36679
  return;
36067
36680
  }
@@ -36079,11 +36692,11 @@ function guardedPlanFileWrite(planFilePath, log, failMsg, write) {
36079
36692
  }
36080
36693
  function writeCanonicalPlanFile(input, canonical, log) {
36081
36694
  return guardedPlanFileWrite(input.planFilePath, log, "plan file canonicalize failed", (p) => {
36082
- if (readFileSync10(p, "utf8") !== (input.plan ?? "")) {
36695
+ if (readFileSync11(p, "utf8") !== (input.plan ?? "")) {
36083
36696
  log.info("review", "plan file changed; rewrite skipped");
36084
36697
  return "changed";
36085
36698
  }
36086
- writeFileSync4(p, canonical);
36699
+ writeFileSync5(p, canonical);
36087
36700
  return "written";
36088
36701
  }) ?? "skipped";
36089
36702
  }
@@ -36100,10 +36713,10 @@ init_log();
36100
36713
  // src/plan/rumdl.ts
36101
36714
  init_paths3();
36102
36715
  import { createHash as createHash2 } from "crypto";
36103
- import { chmodSync as chmodSync3, existsSync as existsSync9, mkdirSync as mkdirSync2, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
36716
+ import { chmodSync as chmodSync3, existsSync as existsSync9, mkdirSync as mkdirSync2, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
36104
36717
  import { mkdtemp, rm as rm3 } from "fs/promises";
36105
36718
  import { tmpdir } from "os";
36106
- import { dirname as dirname5, join as join14 } from "path";
36719
+ import { dirname as dirname6, join as join15 } from "path";
36107
36720
  var RUMDL_VERSION = "0.2.73";
36108
36721
  var ASSETS = {
36109
36722
  "darwin-arm64": {
@@ -36146,17 +36759,17 @@ async function downloadRumdl(binPath, asset, fetchImpl = fetch) {
36146
36759
  if (actual !== asset.sha256) {
36147
36760
  throw new Error(`rumdl: checksum mismatch for ${asset.url} (expected ${asset.sha256}, got ${actual})`);
36148
36761
  }
36149
- const work = await mkdtemp(join14(tmpdir(), "caret-rumdl-"));
36762
+ const work = await mkdtemp(join15(tmpdir(), "caret-rumdl-"));
36150
36763
  try {
36151
- const archive = join14(work, "rumdl.tar.gz");
36764
+ const archive = join15(work, "rumdl.tar.gz");
36152
36765
  await Bun.write(archive, bytes);
36153
36766
  const tar = Bun.spawn(["tar", "-xzf", archive, "-C", work], { stderr: "pipe" });
36154
36767
  if (await tar.exited !== 0)
36155
36768
  throw new Error(`rumdl: failed to extract ${asset.url}`);
36156
- const extracted = join14(work, "rumdl");
36769
+ const extracted = join15(work, "rumdl");
36157
36770
  if (!existsSync9(extracted))
36158
36771
  throw new Error(`rumdl: binary not found in ${asset.url}`);
36159
- mkdirSync2(dirname5(binPath), { recursive: true });
36772
+ mkdirSync2(dirname6(binPath), { recursive: true });
36160
36773
  const tmp = `${binPath}.${process.pid}.tmp`;
36161
36774
  await Bun.write(tmp, Bun.file(extracted));
36162
36775
  chmodSync3(tmp, 493);
@@ -36185,7 +36798,7 @@ var acquisition = { cooldownUntil: 0 };
36185
36798
  async function ensureRumdl(install = installPinnedRumdl, now = Date.now, state = acquisition) {
36186
36799
  ensureStateDir(rumdlDir());
36187
36800
  const config = rumdlConfig();
36188
- writeFileSync5(config, RUMDL_CONFIG);
36801
+ writeFileSync6(config, RUMDL_CONFIG);
36189
36802
  const override = process.env.CARET_RUMDL_BIN?.trim();
36190
36803
  if (override && await installedVersion(override) === RUMDL_VERSION) {
36191
36804
  return { bin: override, config, installed: false };
@@ -36255,7 +36868,7 @@ async function formatPlanMarkdown(plan, log = noopLogger, doFormat = rumdlFormat
36255
36868
 
36256
36869
  // src/review/threading.ts
36257
36870
  function newReviewId() {
36258
- return randomBytes(8).toString("base64url");
36871
+ return randomBytes2(8).toString("base64url");
36259
36872
  }
36260
36873
  function deriveTitle(plan) {
36261
36874
  const tokens = new x({ gfm: true }).blockTokens(plan.replace(/\r\n?/g, `
@@ -36441,6 +37054,9 @@ function resolveOptions(opts) {
36441
37054
  routePlan: opts.routePlan ?? routeIncomingPlan,
36442
37055
  configPath: opts.configPath ?? configFile(),
36443
37056
  lockPath: opts.lockPath,
37057
+ tokenFile: opts.tokenFile,
37058
+ hostname: opts.hostname ?? "127.0.0.1",
37059
+ hostnames: opts.hostnames ?? [],
36444
37060
  buildId: opts.buildId,
36445
37061
  assetDigest: opts.assetDigest,
36446
37062
  commit: opts.commit,
@@ -36496,6 +37112,8 @@ function createServer(opts) {
36496
37112
  const { approveVariants, source, lockPath } = cfg;
36497
37113
  const { awaitDecision, resolveDecision, clearDecision, openDecisionCount, unreadDecisionCount } = createDecisions(log);
36498
37114
  const configWriter = createConfigWriter(configPath);
37115
+ const token = cfg.tokenFile === undefined ? null : loadOrMintToken(cfg.tokenFile);
37116
+ const names = ownNames(connectHostname(cfg.hostname), cfg.hostnames);
36499
37117
  function raceDecision(id, ms) {
36500
37118
  return new Promise((resolve) => {
36501
37119
  const t = setTimeout(() => resolve(null), ms);
@@ -36962,19 +37580,29 @@ function createServer(opts) {
36962
37580
  return notFound();
36963
37581
  }
36964
37582
  async function handle(req, self) {
36965
- const end = liveness.begin(req.method);
37583
+ let end;
36966
37584
  let failureLog = log;
36967
37585
  try {
36968
37586
  const port = self.port ?? -1;
36969
- if (isForeignHost(req, port)) {
37587
+ if (isForeignHost(req, port, names)) {
36970
37588
  return new Response("host not recognized", { status: 403 });
36971
37589
  }
36972
37590
  const url = new URL(req.url);
36973
37591
  const path = url.pathname;
36974
37592
  const method = req.method;
36975
- if (!isSafeMethod(method) && isCrossOrigin(req, port)) {
37593
+ if (token !== null) {
37594
+ const denied = authGate(req, url, {
37595
+ token,
37596
+ port,
37597
+ secure: isForwardedHttps(req, cfg.hostname)
37598
+ });
37599
+ if (denied)
37600
+ return denied;
37601
+ }
37602
+ if (!isSafeMethod(method) && isCrossOrigin(req, port, names)) {
36976
37603
  return new Response("cross-origin request blocked", { status: 403 });
36977
37604
  }
37605
+ end = liveness.begin(method);
36978
37606
  const idRoute = matchIdRoute(path);
36979
37607
  const route = idRoute && { ...idRoute, log: log.child({ reviewId: idRoute.id }) };
36980
37608
  if (route)
@@ -36988,16 +37616,16 @@ function createServer(opts) {
36988
37616
  } catch {}
36989
37617
  return new Response("internal error", { status: 500 });
36990
37618
  } finally {
36991
- end();
37619
+ end?.();
36992
37620
  }
36993
37621
  }
36994
37622
  const server = Bun.serve({
36995
37623
  port: opts.port ?? 0,
36996
- hostname: "127.0.0.1",
37624
+ hostname: cfg.hostname,
36997
37625
  idleTimeout: deriveIdleTimeoutSec(heartbeat),
36998
37626
  fetch: handle
36999
37627
  });
37000
- log.info("listen", `listening on 127.0.0.1:${server.port}`, {
37628
+ log.info("listen", `listening on ${cfg.hostname}:${server.port}`, {
37001
37629
  build: buildId,
37002
37630
  version: IDENTITY.version,
37003
37631
  commit,
@@ -37007,10 +37635,11 @@ function createServer(opts) {
37007
37635
  if (!lockPath)
37008
37636
  return;
37009
37637
  try {
37010
- ensureStateDir(dirname6(lockPath));
37638
+ ensureStateDir(dirname7(lockPath));
37011
37639
  const lock = {
37012
37640
  pid: process.pid,
37013
37641
  port: server.port ?? 0,
37642
+ host: cfg.hostname,
37014
37643
  build: buildId,
37015
37644
  version: IDENTITY.version,
37016
37645
  startedAt: Date.now(),
@@ -37018,7 +37647,7 @@ function createServer(opts) {
37018
37647
  instanceId
37019
37648
  };
37020
37649
  const tmp = `${lockPath}.tmp.${process.pid}`;
37021
- writeFileSync6(tmp, JSON.stringify(lock));
37650
+ writeFileSync7(tmp, JSON.stringify(lock));
37022
37651
  renameSync4(tmp, lockPath);
37023
37652
  } catch {}
37024
37653
  }
@@ -37042,11 +37671,14 @@ function createServer(opts) {
37042
37671
  return { port: server.port ?? 0, stop, drain: () => liveness.drain() };
37043
37672
  }
37044
37673
 
37674
+ // src/commands/daemon.ts
37675
+ init_token();
37676
+
37045
37677
  // src/daemon/update-check.ts
37046
37678
  init_paths3();
37047
37679
  init_json_file();
37048
- import { writeFileSync as writeFileSync7 } from "fs";
37049
- import { dirname as dirname7 } from "path";
37680
+ import { writeFileSync as writeFileSync8 } from "fs";
37681
+ import { dirname as dirname8 } from "path";
37050
37682
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
37051
37683
  var REFRESH_COMMAND = "bunx --no-cache @macintacos/caret@latest install --refresh";
37052
37684
  var REBUILD_COMMAND = "mise run build --install";
@@ -37114,8 +37746,8 @@ function fileUpdateCache(path) {
37114
37746
  read: () => asRecord(readJsonFileSync(path)),
37115
37747
  write: (entry) => {
37116
37748
  try {
37117
- ensureStateDir(dirname7(path));
37118
- writeFileSync7(path, JSON.stringify(entry, null, 2), { mode: 384 });
37749
+ ensureStateDir(dirname8(path));
37750
+ writeFileSync8(path, JSON.stringify(entry, null, 2), { mode: 384 });
37119
37751
  } catch {}
37120
37752
  }
37121
37753
  };
@@ -37182,7 +37814,7 @@ init_upstream();
37182
37814
  // src/review/store.ts
37183
37815
  init_paths3();
37184
37816
  import { readdir as readdir6, readFile as readFile5, stat as stat4 } from "fs/promises";
37185
- import { join as join15 } from "path";
37817
+ import { join as join16 } from "path";
37186
37818
  init_json_file();
37187
37819
  init_log();
37188
37820
  var STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
@@ -37192,7 +37824,7 @@ function createStore(dir, log = noopLogger) {
37192
37824
  function persist(review) {
37193
37825
  return writes.run(review.id, async () => {
37194
37826
  ensureStateDir(dir);
37195
- await writeFileAtomic(join15(dir, `${review.id}.json`), JSON.stringify(review, null, 2), {
37827
+ await writeFileAtomic(join16(dir, `${review.id}.json`), JSON.stringify(review, null, 2), {
37196
37828
  mode: 384
37197
37829
  });
37198
37830
  log.debug("store", `review persisted: ${shortId(review.id)}`, { reviewId: review.id });
@@ -37240,7 +37872,7 @@ function createStore(dir, log = noopLogger) {
37240
37872
  return review;
37241
37873
  },
37242
37874
  async persisted(id) {
37243
- return await readJsonFile(join15(dir, `${id}.json`)) ?? undefined;
37875
+ return await readJsonFile(join16(dir, `${id}.json`)) ?? undefined;
37244
37876
  },
37245
37877
  size() {
37246
37878
  return reviews.size;
@@ -37273,7 +37905,7 @@ function createStore(dir, log = noopLogger) {
37273
37905
  for (const file of files) {
37274
37906
  if (!file.endsWith(".json"))
37275
37907
  continue;
37276
- const path = join15(dir, file);
37908
+ const path = join16(dir, file);
37277
37909
  try {
37278
37910
  if ((await stat4(path)).mtimeMs < now - STALE_AFTER_MS)
37279
37911
  continue;
@@ -37372,6 +38004,14 @@ async function runDaemon(opts) {
37372
38004
  const buildId = await currentBuildId();
37373
38005
  const assetDigest = await buildHash(assets);
37374
38006
  let armedUpkeep = [];
38007
+ const tokenFile = authEnabled(boot.daemon) ? daemonTokenFile() : undefined;
38008
+ if (tokenFile !== undefined) {
38009
+ try {
38010
+ loadOrMintToken(tokenFile);
38011
+ } catch (e) {
38012
+ exitTerminal(`cannot use the daemon token file ${tokenFile}`, "daemon-token-unusable", e);
38013
+ }
38014
+ }
37375
38015
  try {
37376
38016
  server = createServer({
37377
38017
  store,
@@ -37381,6 +38021,9 @@ async function runDaemon(opts) {
37381
38021
  resident,
37382
38022
  assets,
37383
38023
  lockPath: daemonLock(),
38024
+ tokenFile,
38025
+ hostname: boot.daemon.host,
38026
+ hostnames: boot.daemon.hostnames,
37384
38027
  buildId,
37385
38028
  assetDigest,
37386
38029
  commit: built.commit,
@@ -37412,12 +38055,12 @@ async function runDaemon(opts) {
37412
38055
  log
37413
38056
  });
37414
38057
  } catch (e) {
37415
- if (isAddrInUse(e)) {
38058
+ if (isAddrInUse(e) && !await isAddrNotAvail(boot.daemon.host)) {
37416
38059
  process.stderr.write(`caret: another daemon won the port; exiting.
37417
38060
  `);
37418
38061
  process.exit(0);
37419
38062
  }
37420
- exitTerminal("cannot bind the daemon port", "daemon-bind-failed", e);
38063
+ exitTerminal(`cannot bind the daemon to ${boot.daemon.host}:${getPort(boot)}`, "daemon-bind-failed", e);
37421
38064
  }
37422
38065
  removeOwnBootMarker();
37423
38066
  const onFatal = (label) => (err) => {
@@ -37437,16 +38080,43 @@ async function runDaemon(opts) {
37437
38080
  upkeep.push({ name: "stderr-rotate", run: () => rotateDaemonStderr(svc.current()) });
37438
38081
  }
37439
38082
  armedUpkeep = startUpkeep({ tasks: upkeep, log });
37440
- return server.port;
38083
+ return { port: server.port, settings: boot };
37441
38084
  }
37442
38085
 
37443
38086
  // src/commands/doctor.ts
37444
38087
  init_adapters();
37445
- init_paths2();
37446
- init_upgrade();
37447
38088
  import { existsSync as existsSync12 } from "fs";
37448
38089
  import { release } from "os";
37449
38090
 
38091
+ // src/adapters/opencode/checks.ts
38092
+ init_entries();
38093
+ init_host();
38094
+ init_paths2();
38095
+ init_upgrade();
38096
+ async function readOpencodeChecks(deps) {
38097
+ if (readCaretEntries(deps.configFiles, isCaretCheckout).length === 0)
38098
+ return [];
38099
+ const hosts = deps.opencodeHosts();
38100
+ const v2Host = hosts.find((h) => isV2Host(h.version));
38101
+ const loadedFiles = v2Host === undefined ? deps.configFiles : loadedConfigFiles(deps.configFiles, v2Host.version);
38102
+ const ignored = deps.configFiles.filter((f) => !loadedFiles.includes(f) && readCaretEntries([f], isCaretCheckout).length > 0);
38103
+ const entries = readCaretEntries(loadedFiles, isCaretCheckout);
38104
+ const hostResult = hostCheck(hosts, [...new Set(entries.map((e) => e.key))], ignored);
38105
+ if (entries.length === 0 || entries.every((e) => isLocalPluginSpecifier(e.spec)))
38106
+ return [hostResult];
38107
+ const verdict = await readUpgradeVerdict({
38108
+ configFiles: loadedFiles,
38109
+ host: sharedHost(hosts),
38110
+ published: deps.published
38111
+ });
38112
+ return [hostResult, upgradeCheck(verdict)];
38113
+ }
38114
+
38115
+ // src/commands/doctor.ts
38116
+ init_host();
38117
+ init_paths2();
38118
+ init_upgrade();
38119
+
37450
38120
  // src/commands/install/ui.ts
37451
38121
  function isTerminal() {
37452
38122
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -37530,9 +38200,9 @@ function servicePlatform(platform = process.platform) {
37530
38200
 
37531
38201
  // src/service/launchd-manager.ts
37532
38202
  init_log();
37533
- import { mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
38203
+ import { mkdirSync as mkdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "fs";
37534
38204
  import { homedir as homedir6 } from "os";
37535
- import { join as join17 } from "path";
38205
+ import { join as join18 } from "path";
37536
38206
 
37537
38207
  // src/service/launchd.ts
37538
38208
  var EXIT_TIMEOUT_SEC = 20;
@@ -37593,10 +38263,10 @@ function commandError(tool, verb, result) {
37593
38263
  }
37594
38264
 
37595
38265
  // src/service/unit-file.ts
37596
- import { readFileSync as readFileSync11 } from "fs";
38266
+ import { readFileSync as readFileSync12 } from "fs";
37597
38267
  function unitUnchanged(path, text) {
37598
38268
  try {
37599
- return readFileSync11(path, "utf-8") === text;
38269
+ return readFileSync12(path, "utf-8") === text;
37600
38270
  } catch {
37601
38271
  return false;
37602
38272
  }
@@ -37608,14 +38278,14 @@ var RUNNING = [/^\s*state\s*=\s*running\b/m, /^\s*pid\s*=\s*\d+/m];
37608
38278
  var DISABLED2 = new RegExp(`"${LAUNCHD_LABEL.replace(/\./g, "\\.")}"\\s*=>\\s*(?:true|disabled)\\b`);
37609
38279
  var launchctlError = (action, result) => commandError("launchctl", action, result);
37610
38280
  function createLaunchdManager(deps = {}) {
37611
- const launchAgentsDir = deps.launchAgentsDir ?? join17(homedir6(), "Library", "LaunchAgents");
38281
+ const launchAgentsDir = deps.launchAgentsDir ?? join18(homedir6(), "Library", "LaunchAgents");
37612
38282
  const launchctl = deps.launchctl ?? spawnLaunchctl;
37613
38283
  const uid = deps.uid ?? process.getuid?.();
37614
38284
  if (uid === undefined)
37615
38285
  throw new Error("caret service: launchd needs a POSIX uid");
37616
38286
  const domain = `gui/${uid}`;
37617
38287
  const target = `${domain}/${LAUNCHD_LABEL}`;
37618
- const plistPath = join17(launchAgentsDir, `${LAUNCHD_LABEL}.plist`);
38288
+ const plistPath = join18(launchAgentsDir, `${LAUNCHD_LABEL}.plist`);
37619
38289
  async function readStatus() {
37620
38290
  const [printed, printDisabled] = await Promise.all([
37621
38291
  launchctl(["print", target]),
@@ -37644,7 +38314,7 @@ function createLaunchdManager(deps = {}) {
37644
38314
  }
37645
38315
  }
37646
38316
  mkdirSync3(launchAgentsDir, { recursive: true });
37647
- writeFileSync8(plistPath, text);
38317
+ writeFileSync9(plistPath, text);
37648
38318
  await launchctl(["bootout", target]);
37649
38319
  const bootstrapped = await launchctl(["bootstrap", domain, plistPath]);
37650
38320
  if (bootstrapped.code !== 0)
@@ -37652,7 +38322,7 @@ function createLaunchdManager(deps = {}) {
37652
38322
  },
37653
38323
  async uninstall() {
37654
38324
  await launchctl(["bootout", target]);
37655
- rmSync2(plistPath, { force: true });
38325
+ rmSync3(plistPath, { force: true });
37656
38326
  },
37657
38327
  status: readStatus,
37658
38328
  async restart() {
@@ -37665,9 +38335,9 @@ function createLaunchdManager(deps = {}) {
37665
38335
 
37666
38336
  // src/service/systemd-manager.ts
37667
38337
  init_log();
37668
- import { mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "fs";
38338
+ import { mkdirSync as mkdirSync4, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
37669
38339
  import { homedir as homedir7 } from "os";
37670
- import { join as join18 } from "path";
38340
+ import { join as join19 } from "path";
37671
38341
 
37672
38342
  // src/service/systemd.ts
37673
38343
  var START_LIMIT_INTERVAL_SEC = 60;
@@ -37709,10 +38379,10 @@ WantedBy=default.target
37709
38379
 
37710
38380
  // src/service/systemd-manager.ts
37711
38381
  function createSystemdManager(deps = {}) {
37712
- const configHome = process.env.XDG_CONFIG_HOME || join18(homedir7(), ".config");
37713
- const unitDir = deps.unitDir ?? join18(configHome, "systemd", "user");
38382
+ const configHome = process.env.XDG_CONFIG_HOME || join19(homedir7(), ".config");
38383
+ const unitDir = deps.unitDir ?? join19(configHome, "systemd", "user");
37714
38384
  const run = deps.run ?? runCommand;
37715
- const unitPath = join18(unitDir, SYSTEMD_UNIT);
38385
+ const unitPath = join19(unitDir, SYSTEMD_UNIT);
37716
38386
  const systemctl = (...args) => run(["systemctl", "--user", ...args]);
37717
38387
  async function probeSystemd() {
37718
38388
  const shown = await systemctl("show-environment");
@@ -37771,7 +38441,7 @@ function createSystemdManager(deps = {}) {
37771
38441
  }
37772
38442
  mkdirSync4(unitDir, { recursive: true });
37773
38443
  try {
37774
- writeFileSync9(unitPath, text);
38444
+ writeFileSync10(unitPath, text);
37775
38445
  const reloaded = await systemctl("daemon-reload");
37776
38446
  if (reloaded.code !== 0)
37777
38447
  throw commandError("systemctl", "daemon-reload", reloaded);
@@ -37783,14 +38453,14 @@ function createSystemdManager(deps = {}) {
37783
38453
  if (restarted.code !== 0)
37784
38454
  throw commandError("systemctl", "restart", restarted);
37785
38455
  } catch (err) {
37786
- rmSync3(unitPath, { force: true });
38456
+ rmSync4(unitPath, { force: true });
37787
38457
  throw err;
37788
38458
  }
37789
38459
  await enableLinger();
37790
38460
  },
37791
38461
  async uninstall() {
37792
38462
  await systemctl("disable", "--now", SYSTEMD_UNIT);
37793
- rmSync3(unitPath, { force: true });
38463
+ rmSync4(unitPath, { force: true });
37794
38464
  await systemctl("daemon-reload");
37795
38465
  },
37796
38466
  status: readStatus,
@@ -37826,34 +38496,36 @@ function prodService() {
37826
38496
  // src/commands/doctor.ts
37827
38497
  init_paths3();
37828
38498
  init_settings();
38499
+ init_address();
38500
+ init_client();
37829
38501
  init_lifecycle();
37830
38502
 
37831
38503
  // src/doctor/bundle.ts
37832
- import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
37833
- import { basename as basename3, join as join19 } from "path";
38504
+ import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
38505
+ import { basename as basename5, join as join20 } from "path";
37834
38506
  function stamp2(now) {
37835
38507
  return now.toISOString().replace(/[-:.]/g, "");
37836
38508
  }
37837
38509
  function bundlePath(stateDir, now) {
37838
- return join19(stateDir, `doctor-${stamp2(now)}.zip`);
38510
+ return join20(stateDir, `doctor-${stamp2(now)}.zip`);
37839
38511
  }
37840
38512
  function member(path, name) {
37841
38513
  try {
37842
- return { name, data: readFileSync12(path) };
38514
+ return { name, data: readFileSync13(path) };
37843
38515
  } catch {
37844
38516
  return null;
37845
38517
  }
37846
38518
  }
37847
38519
  var MAX_REVIEW_MEMBERS = 5000;
37848
38520
  function entriesFor(deps) {
37849
- const logs = deps.logPaths.map((p) => member(p, `logs/${basename3(p)}`));
38521
+ const logs = deps.logPaths.map((p) => member(p, `logs/${basename5(p)}`));
37850
38522
  let reviews;
37851
38523
  try {
37852
38524
  reviews = readdirSync5(deps.reviewsDir).filter((f) => f.endsWith(".json"));
37853
38525
  } catch {
37854
38526
  reviews = [];
37855
38527
  }
37856
- const records = reviews.slice(0, MAX_REVIEW_MEMBERS).map((f) => member(join19(deps.reviewsDir, f), `reviews/${f}`));
38528
+ const records = reviews.slice(0, MAX_REVIEW_MEMBERS).map((f) => member(join20(deps.reviewsDir, f), `reviews/${f}`));
37857
38529
  return [...logs, ...records].filter((e) => e !== null);
37858
38530
  }
37859
38531
  async function runBundle(opts, deps) {
@@ -37878,8 +38550,8 @@ init_paths3();
37878
38550
  init_json_file();
37879
38551
  init_log();
37880
38552
  init_node();
37881
- import { readdirSync as readdirSync6, statSync as statSync5 } from "fs";
37882
- import { basename as basename4, join as join20 } from "path";
38553
+ import { readdirSync as readdirSync6, statSync as statSync6 } from "fs";
38554
+ import { basename as basename6, join as join21 } from "path";
37883
38555
  var CARET_BIN = "caret-native";
37884
38556
  var ERROR_RECORD_FIELDS = ["time", "source", "step", "code", "reviewId", "sessionId"];
37885
38557
  async function safe(build) {
@@ -38161,7 +38833,7 @@ function parsePsLines(text) {
38161
38833
  const [, pidStr, comm] = m ?? [];
38162
38834
  if (pidStr === undefined || comm === undefined)
38163
38835
  continue;
38164
- const name = basename4(comm.trim());
38836
+ const name = basename6(comm.trim());
38165
38837
  if (name === CARET_BIN)
38166
38838
  out.push({ pid: Number(pidStr), name });
38167
38839
  }
@@ -38191,7 +38863,7 @@ function listReviewFiles() {
38191
38863
  continue;
38192
38864
  if (out.length >= 5000)
38193
38865
  break;
38194
- const raw = readJsonFileSync(join20(dir, file));
38866
+ const raw = readJsonFileSync(join21(dir, file));
38195
38867
  if (raw && typeof raw.id === "string" && typeof raw.status === "string") {
38196
38868
  out.push({ id: raw.id, status: raw.status });
38197
38869
  }
@@ -38202,7 +38874,7 @@ var TAIL_BYTES = 256 * 1024;
38202
38874
  async function logStats(path) {
38203
38875
  let size;
38204
38876
  try {
38205
- size = statSync5(path).size;
38877
+ size = statSync6(path).size;
38206
38878
  } catch {
38207
38879
  return { path, exists: false, size: 0, errors: 0, warns: 0 };
38208
38880
  }
@@ -38221,7 +38893,7 @@ async function readTail(path, size) {
38221
38893
  async function logErrorRecords(path) {
38222
38894
  let size;
38223
38895
  try {
38224
- size = statSync5(path).size;
38896
+ size = statSync6(path).size;
38225
38897
  } catch {
38226
38898
  return [];
38227
38899
  }
@@ -38467,7 +39139,7 @@ function runChecks(report, adapterChecks = []) {
38467
39139
  }
38468
39140
 
38469
39141
  // src/doctor/zip.ts
38470
- import { closeSync as closeSync2, openSync as openSync3, rmSync as rmSync4, writeSync } from "fs";
39142
+ import { closeSync as closeSync2, openSync as openSync3, rmSync as rmSync5, writeSync } from "fs";
38471
39143
  import { crc32 } from "zlib";
38472
39144
  var LOCAL_SIG = 67324752;
38473
39145
  var CENTRAL_SIG = 33639248;
@@ -38565,7 +39237,7 @@ function writeZip(path, entries, now) {
38565
39237
  }
38566
39238
  put(eocd(records.length, offset - directoryStart, directoryStart));
38567
39239
  } catch (e) {
38568
- rmSync4(path, { force: true });
39240
+ rmSync5(path, { force: true });
38569
39241
  throw e;
38570
39242
  } finally {
38571
39243
  closeSync2(fd);
@@ -38594,7 +39266,7 @@ function prodDoctorDeps(s) {
38594
39266
  reviewTimeoutMs: reviewTimeoutMs(s),
38595
39267
  heartbeatMs: heartbeatMs(s)
38596
39268
  }),
38597
- baseUrl: `http://localhost:${getPort(s)}`,
39269
+ baseUrl: daemonBaseUrl(s),
38598
39270
  health: httpHealth,
38599
39271
  serviceInstalled: () => existsSync12(launcherServiceFile()),
38600
39272
  serviceStatus: async () => prodService().manager.status(),
@@ -38619,10 +39291,10 @@ function prodDoctorDeps(s) {
38619
39291
  }
38620
39292
  async function readAdapterChecks() {
38621
39293
  try {
38622
- const config = resolveConfigFile(opencodeConfigDir());
38623
- if (!hasCaretPluginEntry(config))
38624
- return [];
38625
- return [upgradeCheck(await readUpgradeVerdict({ configFile: config }))];
39294
+ return await readOpencodeChecks({
39295
+ configFiles: existingConfigFiles(opencodeConfigDir()),
39296
+ opencodeHosts: readOpencodeHosts
39297
+ });
38626
39298
  } catch (e) {
38627
39299
  return [upgradeCheck({ kind: "unknown", reason: errorMessage(e) })];
38628
39300
  }
@@ -38685,27 +39357,27 @@ import { resolve as resolve3 } from "path";
38685
39357
 
38686
39358
  // src/commands/install/local.ts
38687
39359
  import {
38688
- accessSync as accessSync2,
38689
- constants as constants2,
39360
+ accessSync as accessSync3,
39361
+ constants as constants3,
38690
39362
  existsSync as existsSync13,
38691
39363
  mkdirSync as mkdirSync5,
38692
- rmSync as rmSync5,
38693
- statSync as statSync6,
39364
+ rmSync as rmSync6,
39365
+ statSync as statSync7,
38694
39366
  symlinkSync,
38695
- writeFileSync as writeFileSync10
39367
+ writeFileSync as writeFileSync11
38696
39368
  } from "fs";
38697
- import { join as join22, resolve as resolve2 } from "path";
39369
+ import { join as join23, resolve as resolve2 } from "path";
38698
39370
 
38699
39371
  // src/adapters/opencode/packaging.ts
38700
39372
  init_entries();
38701
- import { readdirSync as readdirSync7, readFileSync as readFileSync13 } from "fs";
38702
- import { dirname as dirname9, join as join21 } from "path";
39373
+ import { readdirSync as readdirSync7, readFileSync as readFileSync14 } from "fs";
39374
+ import { dirname as dirname10, join as join22 } from "path";
38703
39375
  function resolveCaretRoot() {
38704
39376
  const candidates = [];
38705
39377
  const script = process.argv[1];
38706
39378
  if (script)
38707
- candidates.push(join21(dirname9(script), ".."));
38708
- candidates.push(join21(dirname9(process.execPath), ".."));
39379
+ candidates.push(join22(dirname10(script), ".."));
39380
+ candidates.push(join22(dirname10(process.execPath), ".."));
38709
39381
  for (const root of candidates) {
38710
39382
  if (isCaretCheckout(root))
38711
39383
  return root;
@@ -38714,12 +39386,12 @@ function resolveCaretRoot() {
38714
39386
  }
38715
39387
  var DEMO_TEMPLATE = "templates/demo.md";
38716
39388
  function loadOpencodePackaging(root = resolveCaretRoot()) {
38717
- const binPath = join21(root, "bin", "caret");
38718
- const demoTemplate = readFileSync13(join21(root, DEMO_TEMPLATE), "utf-8");
38719
- const commandsDir = join21(root, "opencode", "commands");
39389
+ const binPath = join22(root, "bin", "caret");
39390
+ const demoTemplate = readFileSync14(join22(root, DEMO_TEMPLATE), "utf-8");
39391
+ const commandsDir = join22(root, "opencode", "commands");
38720
39392
  let commands = [];
38721
39393
  try {
38722
- commands = readdirSync7(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync13(join21(commandsDir, f), "utf-8") }));
39394
+ commands = readdirSync7(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync14(join22(commandsDir, f), "utf-8") }));
38723
39395
  } catch {
38724
39396
  commands = [];
38725
39397
  }
@@ -38729,17 +39401,17 @@ function loadOpencodePackaging(root = resolveCaretRoot()) {
38729
39401
  // src/commands/install/local.ts
38730
39402
  init_paths3();
38731
39403
  function devMarketplaceDir() {
38732
- return join22(stateDir(), "dev-marketplace");
39404
+ return join23(stateDir(), "dev-marketplace");
38733
39405
  }
38734
39406
  function resolveLocalCheckout(opts = {}, deps = {}) {
38735
39407
  const repoDir = resolve2(caretRoot(deps));
38736
- if (!existsSync13(join22(repoDir, ".claude-plugin", "marketplace.json"))) {
39408
+ if (!existsSync13(join23(repoDir, ".claude-plugin", "marketplace.json"))) {
38737
39409
  throw new Error(`--from-local must run from a caret checkout (no .claude-plugin/marketplace.json at ${repoDir}). A published caret installs with a plain \`caret install\`.`);
38738
39410
  }
38739
39411
  if (opts.requireArtifacts !== false) {
38740
39412
  const missing = [
38741
- isExecutable(join22(repoDir, "bin", "caret-native")) ? null : "bin/caret-native",
38742
- isDirectory(join22(repoDir, "bin", "ui")) ? null : "bin/ui"
39413
+ isExecutable(join23(repoDir, "bin", "caret-native")) ? null : "bin/caret-native",
39414
+ isDirectory(join23(repoDir, "bin", "ui")) ? null : "bin/ui"
38743
39415
  ].filter((m) => m !== null);
38744
39416
  if (missing.length > 0) {
38745
39417
  throw new Error(`--from-local reuses the build artifacts bin/caret-native + bin/ui, and ${repoDir} is missing ${missing.join(" + ")} \u2014 run \`mise run build\` first.`);
@@ -38757,10 +39429,10 @@ function caretRoot(deps) {
38757
39429
  }
38758
39430
  }
38759
39431
  function writeDevMarketplace(repoDir, outDir) {
38760
- rmSync5(outDir, { recursive: true, force: true });
38761
- mkdirSync5(join22(outDir, ".claude-plugin"), { recursive: true });
38762
- symlinkSync(resolve2(repoDir), join22(outDir, "caret"));
38763
- writeFileSync10(join22(outDir, ".claude-plugin", "marketplace.json"), `${JSON.stringify({
39432
+ rmSync6(outDir, { recursive: true, force: true });
39433
+ mkdirSync5(join23(outDir, ".claude-plugin"), { recursive: true });
39434
+ symlinkSync(resolve2(repoDir), join23(outDir, "caret"));
39435
+ writeFileSync11(join23(outDir, ".claude-plugin", "marketplace.json"), `${JSON.stringify({
38764
39436
  name: "caret",
38765
39437
  description: "Local caret dev build (generated by caret install --from-local).",
38766
39438
  owner: { name: "macintacos" },
@@ -38771,20 +39443,20 @@ function writeDevMarketplace(repoDir, outDir) {
38771
39443
  async function prewarmLocalBuild(repoDir, deps = {}) {
38772
39444
  const spawn = deps.spawn ?? runCaptured;
38773
39445
  try {
38774
- await spawn([join22(repoDir, "bin", "caret-native"), "prewarm"]);
39446
+ await spawn([join23(repoDir, "bin", "caret-native"), "prewarm"]);
38775
39447
  } catch {}
38776
39448
  }
38777
39449
  function isExecutable(path) {
38778
39450
  try {
38779
- accessSync2(path, constants2.X_OK);
38780
- return statSync6(path).isFile();
39451
+ accessSync3(path, constants3.X_OK);
39452
+ return statSync7(path).isFile();
38781
39453
  } catch {
38782
39454
  return false;
38783
39455
  }
38784
39456
  }
38785
39457
  function isDirectory(path) {
38786
39458
  try {
38787
- return statSync6(path).isDirectory();
39459
+ return statSync7(path).isDirectory();
38788
39460
  } catch {
38789
39461
  return false;
38790
39462
  }
@@ -39050,12 +39722,12 @@ async function runInstallClaudeTarget(opts, deps = {}) {
39050
39722
 
39051
39723
  // src/commands/install/opencode.ts
39052
39724
  init_config_plugin();
39053
- import { writeFileSync as writeFileSync12 } from "fs";
39054
- import { basename as basename5, join as join23 } from "path";
39725
+ import { writeFileSync as writeFileSync13 } from "fs";
39726
+ import { basename as basename7, join as join24 } from "path";
39055
39727
 
39056
39728
  // src/adapters/opencode/deploy.ts
39057
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
39058
- import { dirname as dirname10 } from "path";
39729
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
39730
+ import { dirname as dirname11 } from "path";
39059
39731
  function renderPlugin(source, opts) {
39060
39732
  return source.replaceAll("__CARET_VERSION__", () => opts.version).replaceAll("__CARET_BIN__", () => opts.binPath).replaceAll("__CARET_DEMO_TEMPLATE__", () => opts.demoTemplate);
39061
39733
  }
@@ -39063,8 +39735,8 @@ function deployFiles(files, opts) {
39063
39735
  const paths = [];
39064
39736
  for (const f of files) {
39065
39737
  if (!opts.dryRun) {
39066
- mkdirSync6(dirname10(f.path), { recursive: true });
39067
- writeFileSync11(f.path, f.contents);
39738
+ mkdirSync6(dirname11(f.path), { recursive: true });
39739
+ writeFileSync12(f.path, f.contents);
39068
39740
  }
39069
39741
  paths.push(f.path);
39070
39742
  }
@@ -39076,7 +39748,7 @@ function removeFiles(targets, opts) {
39076
39748
  if (!existsSync14(p))
39077
39749
  continue;
39078
39750
  if (!opts.dryRun)
39079
- rmSync6(p, { force: true });
39751
+ rmSync7(p, { force: true });
39080
39752
  paths.push(p);
39081
39753
  }
39082
39754
  return { paths, dryRun: opts.dryRun };
@@ -39084,6 +39756,7 @@ function removeFiles(targets, opts) {
39084
39756
 
39085
39757
  // src/commands/install/opencode.ts
39086
39758
  init_entries();
39759
+ init_host();
39087
39760
  init_paths2();
39088
39761
  init_upgrade();
39089
39762
 
@@ -39104,7 +39777,7 @@ var INSTALL_TARGETS = [
39104
39777
  {
39105
39778
  id: "opencode",
39106
39779
  label: "OpenCode",
39107
- hint: "add caret to the plugin array + deploy /caret:* commands",
39780
+ hint: "add caret to OpenCode's plugin list + deploy /caret:* commands",
39108
39781
  detect: (probe) => probe.hasCommand("opencode") || probe.hasPath(opencodeConfigDir())
39109
39782
  }
39110
39783
  ];
@@ -39166,42 +39839,146 @@ function sameEntryForm(entry, specifier) {
39166
39839
  return false;
39167
39840
  return entryIsLocal ? entry === specifier : true;
39168
39841
  }
39169
- function setCaretPluginEntry(text, specifier, isCheckout) {
39842
+ function keptEntry(entries, specifier) {
39843
+ const sameForm = entries.filter((e) => sameEntryForm(e.spec, specifier));
39844
+ return sameForm.find((e) => splitPluginSpecifier(e.spec).version !== null) ?? sameForm[0];
39845
+ }
39846
+ function setCaretPluginEntry(text, target) {
39847
+ const { spec, key, isCheckout } = target;
39170
39848
  if (text === null)
39171
- return addPluginToConfigText(null, specifier);
39172
- const pruned = caretEntries(text, isCheckout).filter((entry) => !sameEntryForm(entry, specifier)).reduce((acc, entry) => removePluginFromConfigText(acc, entry), text);
39173
- return addPluginToConfigText(pruned, specifier);
39849
+ return addPluginToConfigText(null, spec, key);
39850
+ const entries = caretEntries(text, isCheckout);
39851
+ const kept = entries.find((e) => e.key === key && e.spec === spec) ?? entries.find((e) => e.key === key);
39852
+ const others = entries.filter((e) => e !== kept);
39853
+ if (kept === undefined)
39854
+ return addPluginToConfigText(dropEntries(text, others), spec, key);
39855
+ const placed = kept.spec === spec ? text : setPluginItemSpec(text, { key, index: kept.index, spec });
39856
+ return dropEntries(placed, others);
39857
+ }
39858
+ function stripCaret(isCheckout) {
39859
+ return (text) => text === null ? null : dropEntries(text, caretEntries(text, isCheckout));
39860
+ }
39861
+ function bumpPin(version) {
39862
+ return (text) => {
39863
+ const entry = caretPackageEntry(text);
39864
+ return text === null || entry === null ? null : setPluginVersionInConfigText(text, { pkg: CARET_PACKAGE, version, key: entry.key });
39865
+ };
39866
+ }
39867
+ function hostLine({
39868
+ hosts,
39869
+ version,
39870
+ key,
39871
+ movedFrom
39872
+ }) {
39873
+ const loadedByAll = version === null ? ", which every OpenCode loads" : "";
39874
+ const found = `${describeHosts(hosts)} \u2014 writing caret to ${key}${loadedByAll}`;
39875
+ return movedFrom === null ? found : `${found} (moved from ${movedFrom})`;
39174
39876
  }
39175
39877
  async function runInstallOpencodeTarget(opts, deps = {}) {
39176
39878
  const dir = deps.configDir ?? opencodeConfigDir();
39177
- const pkg = deps.packaging ?? loadOpencodePackaging();
39178
- const ui = deps.ui ?? silentUI;
39179
- const isCheckout = deps.isCheckout ?? isCaretCheckout;
39180
- const configFile = resolveConfigFile(dir);
39181
- const commandPaths = pkg.commands.map((c) => join23(commandDir(dir), namespacedCommandFilename(c.name)));
39182
- const legacy = existingLegacyInstallFiles(dir);
39183
- const specifier = opts.local ? localPluginSpecifier(opts.local.repoDir) : CARET_PACKAGE;
39184
- if (opts.dryRun) {
39185
- const verb = opts.uninstall ? "remove" : "write";
39186
- const found = checks3(opts) ? ["", previewLine(await readVerdict(configFile, deps))] : [];
39187
- const entry = opts.uninstall ? [] : ["", `plugin entry: ${specifier}`];
39188
- const sweep = legacy.length === 0 ? [] : ["", "pre-array-install files to remove:", ...legacy];
39189
- ui.note([configFile, ...commandPaths, ...entry, ...sweep, ...found].join(`
39879
+ const setup = {
39880
+ dir,
39881
+ pkg: deps.packaging ?? loadOpencodePackaging(),
39882
+ ui: deps.ui ?? silentUI,
39883
+ isCheckout: deps.isCheckout ?? isCaretCheckout,
39884
+ configFiles: existingConfigFiles(dir),
39885
+ legacy: existingLegacyInstallFiles(dir)
39886
+ };
39887
+ if (opts.uninstall)
39888
+ return opts.dryRun ? previewUninstall(setup) : uninstallOpencode(setup);
39889
+ return opts.dryRun ? previewInstall(setup, opts, deps) : installOpencode(setup, opts, deps);
39890
+ }
39891
+ function readPlacement(setup, local, probe) {
39892
+ const specifier = local ? localPluginSpecifier(local.repoDir) : CARET_PACKAGE;
39893
+ const hosts = probe();
39894
+ const version = sharedHost(hosts);
39895
+ const key = pluginKeyFor(version);
39896
+ const allEntries = readCaretEntries(setup.configFiles, setup.isCheckout);
39897
+ const loadedFiles = loadedConfigFiles(setup.configFiles, version);
39898
+ const loadedEntries = readCaretEntries(loadedFiles, setup.isCheckout);
39899
+ const otherKey = key === "plugin" ? "plugins" : "plugin";
39900
+ const movedFrom = allEntries.some((e) => e.key === otherKey) ? otherKey : null;
39901
+ const writtenSpec = (keptEntry(loadedEntries, specifier) ?? keptEntry(allEntries, specifier))?.spec ?? specifier;
39902
+ const target = resolveConfigFile(setup.dir, hostConfigFilenames(version));
39903
+ return { specifier, hosts, version, key, movedFrom, writtenSpec, target, loadedFiles };
39904
+ }
39905
+ function installEdits({ configFiles, isCheckout }, { target, writtenSpec, key }) {
39906
+ return [
39907
+ ...configFiles.filter((path) => path !== target).map((path) => ({ path, transform: stripCaret(isCheckout) })),
39908
+ {
39909
+ path: target,
39910
+ transform: (text) => setCaretPluginEntry(text, { spec: writtenSpec, key, isCheckout })
39911
+ }
39912
+ ];
39913
+ }
39914
+ function uninstallEdits({
39915
+ configFiles,
39916
+ isCheckout
39917
+ }) {
39918
+ return configFiles.map((path) => ({ path, transform: stripCaret(isCheckout) }));
39919
+ }
39920
+ function ignoredConfigNote({ configFiles }, { version, target, loadedFiles }, changed) {
39921
+ const ignored = configFiles.filter((p) => !loadedFiles.includes(p));
39922
+ if (ignored.length === 0)
39923
+ return null;
39924
+ if (!ignored.some((p) => changed.includes(p)) && configFiles.includes(target))
39925
+ return null;
39926
+ const host = version === null ? "OpenCode" : `OpenCode v${version[0]}`;
39927
+ const names = ignored.map((p) => basename7(p)).join(", ");
39928
+ return `${host} ignores ${names} \u2014 caret goes in ${basename7(target)}`;
39929
+ }
39930
+ function commandPaths({ dir, pkg }) {
39931
+ return pkg.commands.map((c) => join24(commandDir(dir), namespacedCommandFilename(c.name)));
39932
+ }
39933
+ function previewNote(setup, verb, parts) {
39934
+ const { legacy, ui } = setup;
39935
+ const sweep = legacy.length === 0 ? [] : ["", "pre-array-install files to remove:", ...legacy];
39936
+ ui.note([...parts.configs, ...commandPaths(setup), ...parts.entry, ...sweep, ...parts.found].join(`
39190
39937
  `), `OpenCode \u2014 would ${verb}`);
39191
- return;
39192
- }
39193
- if (opts.uninstall) {
39194
- await ui.step("Removing caret from OpenCode's plugin array", async () => editConfig(configFile, (text) => text === null ? null : caretEntries(text, isCheckout).reduce((acc, entry) => removePluginFromConfigText(acc, entry), text)), (changed) => changed.length > 0 ? `Removed caret from ${basename5(configFile)}` : `caret was not in ${basename5(configFile)}`);
39195
- await ui.step("Removing the /caret:* command files", async () => removeFiles(commandPaths, { dryRun: false }), (removed) => `Removed ${removed.paths.length} command file(s) from ${dir}`);
39196
- await sweepLegacy(legacy, dir, ui);
39197
- return;
39198
- }
39199
- await ui.step(`Adding ${specifier} to OpenCode's plugin array`, async () => editConfig(configFile, (text) => setCaretPluginEntry(text, specifier, isCheckout)), (changed) => changed.length > 0 ? `Added ${specifier} to ${basename5(configFile)}` : `${specifier} was already in ${basename5(configFile)}`);
39200
- if (checks3(opts))
39201
- await upgradeStep(configFile, opts, deps, ui);
39938
+ }
39939
+ async function previewInstall(setup, opts, deps) {
39940
+ const placed = readPlacement(setup, opts.local, deps.opencodeHosts ?? readOpencodeHosts);
39941
+ const found = checksPublished(opts.local) ? [
39942
+ "",
39943
+ previewLine(await readVerdict({ configFiles: setup.configFiles, host: placed.version }, deps))
39944
+ ] : [];
39945
+ const entry = ["", `plugin entry: ${placed.specifier} \u2192 ${placed.key}`, hostLine(placed)];
39946
+ const changed = strictPlan(installEdits(setup, placed)).map((e) => e.path);
39947
+ const note = ignoredConfigNote(setup, placed, changed);
39948
+ if (note !== null)
39949
+ entry.push(note);
39950
+ const configs = [placed.target, ...changed.filter((p) => p !== placed.target)];
39951
+ previewNote(setup, "write", { configs, entry, found });
39952
+ }
39953
+ function previewUninstall(setup) {
39954
+ const configs = lenientPlan(uninstallEdits(setup), setup.ui).planned.map((e) => e.path);
39955
+ previewNote(setup, "remove", { configs, entry: [], found: [] });
39956
+ }
39957
+ async function uninstallOpencode(setup) {
39958
+ const { ui, dir, legacy } = setup;
39959
+ const { planned, uneditable } = lenientPlan(uninstallEdits(setup), ui);
39960
+ await ui.step("Removing caret from OpenCode's plugin and plugins keys", async () => writeConfigEdits(planned), (changed) => {
39961
+ if (changed.length > 0)
39962
+ return `Removed caret from ${changed.map((p) => basename7(p)).join(", ")}`;
39963
+ return uneditable.length > 0 ? "caret was not in any editable OpenCode config" : "caret was not in any OpenCode config";
39964
+ });
39965
+ await ui.step("Removing the /caret:* command files", async () => removeFiles(commandPaths(setup), { dryRun: false }), (removed) => `Removed ${removed.paths.length} command file(s) from ${dir}`);
39966
+ await sweepLegacy(legacy, dir, ui);
39967
+ }
39968
+ async function installOpencode(setup, opts, deps) {
39969
+ const { dir, pkg, ui, legacy } = setup;
39970
+ const placed = readPlacement(setup, opts.local, deps.opencodeHosts ?? readOpencodeHosts);
39971
+ const { specifier, key, writtenSpec, target } = placed;
39972
+ ui.info(hostLine(placed));
39973
+ const changed = await ui.step(`Adding ${specifier} to OpenCode's ${key} array`, async () => writeConfigEdits(strictPlan(installEdits(setup, placed))), (written) => installedLine(writtenSpec, target, written));
39974
+ const note = ignoredConfigNote(setup, placed, changed);
39975
+ if (note !== null)
39976
+ ui.info(note);
39977
+ if (checksPublished(opts.local))
39978
+ await upgradeStep(target, opts, deps, ui);
39202
39979
  await sweepLegacy(legacy, dir, ui);
39203
39980
  const files = pkg.commands.map((c) => ({
39204
- path: join23(commandDir(dir), namespacedCommandFilename(c.name)),
39981
+ path: join24(commandDir(dir), namespacedCommandFilename(c.name)),
39205
39982
  contents: renderPlugin(c.contents, {
39206
39983
  version: VERSION,
39207
39984
  binPath: pkg.binPath,
@@ -39215,18 +39992,18 @@ async function sweepLegacy(legacy, dir, ui) {
39215
39992
  return;
39216
39993
  await ui.step("Removing the pre-array-install files", async () => removeFiles(legacy, { dryRun: false }), (removed) => `Removed ${removed.paths.length} pre-array-install file(s) from ${dir}`);
39217
39994
  }
39218
- function checks3(opts) {
39219
- return !opts.uninstall && opts.local === undefined;
39995
+ function checksPublished(local) {
39996
+ return local === undefined;
39220
39997
  }
39221
39998
  function previewLine(verdict) {
39222
39999
  const line = upgradeVerdictLine(verdict);
39223
40000
  return verdict.kind === "unknown" ? `${line} (${verdict.reason})` : line;
39224
40001
  }
39225
- async function readVerdict(configFile, deps) {
39226
- return readUpgradeVerdict({ configFile, cacheDir: deps.cacheDir, published: deps.published });
40002
+ async function readVerdict(at, deps) {
40003
+ return readUpgradeVerdict({ ...at, cacheDir: deps.cacheDir, published: deps.published });
39227
40004
  }
39228
- async function upgradeStep(configFile, opts, deps, ui) {
39229
- const verdict = await ui.step("Checking OpenCode's caret version", () => readVerdict(configFile, deps), upgradeVerdictLine);
40005
+ async function upgradeStep(target, opts, deps, ui) {
40006
+ const verdict = await ui.step("Checking OpenCode's caret version", () => readVerdict({ configFiles: [target] }, deps), upgradeVerdictLine);
39230
40007
  if (verdict.kind === "unknown") {
39231
40008
  ui.warn(`Could not check OpenCode's caret version (${verdict.reason}) \u2014 nothing changed.`);
39232
40009
  return;
@@ -39247,15 +40024,69 @@ async function upgradeStep(configFile, opts, deps, ui) {
39247
40024
  await ui.step("Clearing OpenCode's cached caret", async () => (deps.clearCache ?? clearCachedCaret)(dirs), (cleared) => `Cleared ${cleared.length} cached ${cleared.length === 1 ? "copy" : "copies"} \u2014 OpenCode re-resolves on next start`);
39248
40025
  return;
39249
40026
  }
39250
- await ui.step(`Bumping ${CARET_PACKAGE} to ${verdict.published}`, async () => editConfig(configFile, (text) => text === null ? null : setPluginVersionInConfigText(text, CARET_PACKAGE, verdict.published)), (changed) => changed.length > 0 ? `Bumped the pin to ${CARET_PACKAGE}@${verdict.published}` : `Left ${basename5(configFile)} unchanged`);
40027
+ await ui.step(`Bumping ${CARET_PACKAGE} to ${verdict.published}`, async () => writeConfigEdits(strictPlan([{ path: target, transform: bumpPin(verdict.published) }])), (changed) => changed.length > 0 ? `Bumped the pin to ${CARET_PACKAGE}@${verdict.published}` : `Left ${basename7(target)} unchanged`);
39251
40028
  }
39252
- function editConfig(path, transform) {
39253
- const existing = readConfigText(path);
39254
- const next = transform(existing);
39255
- if (next === null || next === existing)
39256
- return [];
39257
- writeFileSync12(path, next);
39258
- return [path];
40029
+ function installedLine(spec, target, written) {
40030
+ const name = basename7(target);
40031
+ const line = written.includes(target) ? `Added ${spec} to ${name}` : `${spec} was already in ${name}`;
40032
+ const stripped = written.filter((p) => p !== target).map((p) => basename7(p));
40033
+ return stripped.length === 0 ? line : `${line} (removed it from ${stripped.join(", ")})`;
40034
+ }
40035
+ function planConfigEdits(edits) {
40036
+ const plan = { planned: [], uneditable: [] };
40037
+ for (const { path, transform } of edits) {
40038
+ let existing;
40039
+ try {
40040
+ existing = readConfigText(path);
40041
+ } catch (e) {
40042
+ plan.uneditable.push({ path, reason: errorMessage(e) });
40043
+ continue;
40044
+ }
40045
+ const parseError = existing === null ? null : configParseError(existing);
40046
+ if (parseError !== null) {
40047
+ plan.uneditable.push({ path, reason: parseError });
40048
+ continue;
40049
+ }
40050
+ let text;
40051
+ try {
40052
+ text = transform(existing);
40053
+ } catch (e) {
40054
+ plan.uneditable.push({ path, reason: errorMessage(e) });
40055
+ continue;
40056
+ }
40057
+ if (text !== null && text !== existing)
40058
+ plan.planned.push({ path, text });
40059
+ }
40060
+ return plan;
40061
+ }
40062
+ function strictPlan(edits) {
40063
+ const { planned, uneditable } = planConfigEdits(edits);
40064
+ if (uneditable.length > 0) {
40065
+ const files = uneditable.map(({ path, reason }) => `${path} (${reason})`).join(", ");
40066
+ throw new Error(`can't edit ${files} \u2014 caret changed nothing`);
40067
+ }
40068
+ return planned;
40069
+ }
40070
+ function lenientPlan(edits, ui) {
40071
+ const plan = planConfigEdits(edits);
40072
+ for (const { path, reason } of plan.uneditable) {
40073
+ ui.warn(`${path} can't be edited (${reason}) \u2014 caret leaves it alone`);
40074
+ }
40075
+ return plan;
40076
+ }
40077
+ function writeConfigEdits(planned) {
40078
+ const written = [];
40079
+ for (const { path, text } of planned) {
40080
+ try {
40081
+ writeFileSync13(path, text);
40082
+ } catch (e) {
40083
+ if (written.length === 0)
40084
+ throw e;
40085
+ throw new Error(`${errorMessage(e)} \u2014 already changed: ${written.join(", ")}`, { cause: e });
40086
+ }
40087
+ written.push(path);
40088
+ }
40089
+ return written;
39259
40090
  }
39260
40091
 
39261
40092
  // src/commands/install/launcher.ts
@@ -39269,13 +40100,13 @@ import {
39269
40100
  mkdirSync as mkdirSync7,
39270
40101
  mkdtempSync,
39271
40102
  readdirSync as readdirSync8,
39272
- readFileSync as readFileSync14,
40103
+ readFileSync as readFileSync15,
39273
40104
  renameSync as renameSync5,
39274
- rmSync as rmSync7,
39275
- statSync as statSync7,
39276
- writeFileSync as writeFileSync13
40105
+ rmSync as rmSync8,
40106
+ statSync as statSync8,
40107
+ writeFileSync as writeFileSync14
39277
40108
  } from "fs";
39278
- import { basename as basename6, dirname as dirname11, join as join24 } from "path";
40109
+ import { dirname as dirname12, join as join25 } from "path";
39279
40110
  init_paths2();
39280
40111
  init_paths3();
39281
40112
  init_lifecycle();
@@ -39286,34 +40117,34 @@ function publishedRoot(kind = buildKind(), root = resolveCaretRoot) {
39286
40117
  if (kind !== "bundle")
39287
40118
  return;
39288
40119
  const dir = root();
39289
- return existsSync16(join24(dir, "src", "cli.ts")) ? undefined : { root: dir, version: VERSION };
40120
+ return existsSync16(join25(dir, "src", "cli.ts")) ? undefined : { root: dir, version: VERSION };
39290
40121
  }
39291
40122
  function stageOwnedRoot(from) {
39292
40123
  if (!isRunnableRoot(from.root))
39293
40124
  return;
39294
- const dest = join24(ownedRootsDir(), from.version);
40125
+ const dest = join25(ownedRootsDir(), from.version);
39295
40126
  if (isRunnableRoot(dest))
39296
40127
  return;
39297
- const parsed = PackageFiles.safeParse(readJsonFileSync(join24(from.root, "package.json")));
40128
+ const parsed = PackageFiles.safeParse(readJsonFileSync(join25(from.root, "package.json")));
39298
40129
  if (!parsed.success)
39299
40130
  return;
39300
40131
  ensureStateDir(ownedRootsDir());
39301
- const tmp = mkdtempSync(join24(ownedRootsDir(), `.${from.version}.`));
40132
+ const tmp = mkdtempSync(join25(ownedRootsDir(), `.${from.version}.`));
39302
40133
  try {
39303
40134
  for (const entry of [...parsed.data.files, "package.json"]) {
39304
- const src = join24(from.root, entry);
40135
+ const src = join25(from.root, entry);
39305
40136
  if (!existsSync16(src))
39306
40137
  continue;
39307
- const target = join24(tmp, entry);
39308
- mkdirSync7(dirname11(target), { recursive: true });
40138
+ const target = join25(tmp, entry);
40139
+ mkdirSync7(dirname12(target), { recursive: true });
39309
40140
  cpSync(src, target, { recursive: true });
39310
40141
  }
39311
40142
  if (!isRunnableRoot(tmp))
39312
40143
  return;
39313
- rmSync7(dest, { recursive: true, force: true });
40144
+ rmSync8(dest, { recursive: true, force: true });
39314
40145
  renameSync5(tmp, dest);
39315
40146
  } finally {
39316
- rmSync7(tmp, { recursive: true, force: true });
40147
+ rmSync8(tmp, { recursive: true, force: true });
39317
40148
  }
39318
40149
  }
39319
40150
  function pruneOwnedRoots() {
@@ -39325,22 +40156,22 @@ function pruneOwnedRoots() {
39325
40156
  }
39326
40157
  const keep = pickLauncherRoot(null, listDirs(ownedRootsDir()).map((dir) => ({ dir, owned: true })))?.root;
39327
40158
  for (const name of entries) {
39328
- const path = join24(ownedRootsDir(), name);
40159
+ const path = join25(ownedRootsDir(), name);
39329
40160
  if (path === keep)
39330
40161
  continue;
39331
40162
  try {
39332
- rmSync7(path, { recursive: true, force: true });
40163
+ rmSync8(path, { recursive: true, force: true });
39333
40164
  } catch {}
39334
40165
  }
39335
40166
  }
39336
40167
  function installLauncher(deps = {}) {
39337
- const source = (deps.source ?? (() => join24(resolveCaretRoot(), "bin", "caret-launcher")))();
40168
+ const source = (deps.source ?? (() => join25(resolveCaretRoot(), "bin", "caret-launcher")))();
39338
40169
  if (deps.pinnedRoot === undefined) {
39339
40170
  const owned = (deps.ownedRoot ?? publishedRoot)();
39340
40171
  if (owned)
39341
40172
  stageOwnedRoot(owned);
39342
40173
  }
39343
- ensureStateDir(dirname11(launcherPath()));
40174
+ ensureStateDir(dirname12(launcherPath()));
39344
40175
  const tmp = `${launcherPath()}.${process.pid}.tmp`;
39345
40176
  copyFileSync(source, tmp);
39346
40177
  chmodSync4(tmp, 493);
@@ -39348,7 +40179,7 @@ function installLauncher(deps = {}) {
39348
40179
  const bunPath = deps.bunPath ?? (buildKind() === "binary" ? undefined : process.execPath);
39349
40180
  const unpinned = deps.pinnedRoot === undefined && existsSync16(launcherPinnedRootFile());
39350
40181
  if (unpinned)
39351
- rmSync7(launcherPinnedRootFile(), { force: true });
40182
+ rmSync8(launcherPinnedRootFile(), { force: true });
39352
40183
  if (bunPath === undefined && deps.serviceLabel === undefined && deps.pinnedRoot === undefined) {
39353
40184
  return { unpinned };
39354
40185
  }
@@ -39362,13 +40193,13 @@ function installLauncher(deps = {}) {
39362
40193
  return { unpinned };
39363
40194
  }
39364
40195
  function writeRecord(path, value) {
39365
- writeFileSync13(path, `${value}
40196
+ writeFileSync14(path, `${value}
39366
40197
  `, { mode: 384 });
39367
40198
  }
39368
40199
  function uninstallLauncher() {
39369
- rmSync7(dirname11(launcherPath()), { recursive: true, force: true });
39370
- rmSync7(launcherRecordDir(), { recursive: true, force: true });
39371
- rmSync7(ownedRootsDir(), { recursive: true, force: true });
40200
+ rmSync8(dirname12(launcherPath()), { recursive: true, force: true });
40201
+ rmSync8(launcherRecordDir(), { recursive: true, force: true });
40202
+ rmSync8(ownedRootsDir(), { recursive: true, force: true });
39372
40203
  }
39373
40204
  function listDirs(dir) {
39374
40205
  let names;
@@ -39377,28 +40208,28 @@ function listDirs(dir) {
39377
40208
  } catch {
39378
40209
  return [];
39379
40210
  }
39380
- return names.filter((n) => !n.startsWith(".")).sort().map((n) => join24(dir, n)).filter(isDir);
40211
+ return names.filter((n) => !n.startsWith(".")).sort().map((n) => join25(dir, n)).filter(isDir);
39381
40212
  }
39382
40213
  function isDir(path) {
39383
40214
  try {
39384
- return statSync7(path).isDirectory();
40215
+ return statSync8(path).isDirectory();
39385
40216
  } catch {
39386
40217
  return false;
39387
40218
  }
39388
40219
  }
39389
40220
  function launcherCandidateDirs() {
39390
- const opencode = listDirs(dirname11(opencodeCachePackageDir())).filter((d) => basename6(d).startsWith("caret")).map((d) => join24(d, "node_modules", "@macintacos", "caret")).filter(isDir);
40221
+ const opencodeRoots = opencodeRootPaths().filter(isDir);
39391
40222
  return [
39392
40223
  ...[
39393
- ...listDirs(join24(claudeConfigDir(), "plugins", "cache", "caret", "caret")),
39394
- ...opencode
40224
+ ...listDirs(join25(claudeConfigDir(), "plugins", "cache", "caret", "caret")),
40225
+ ...opencodeRoots
39395
40226
  ].map((dir) => ({ dir, owned: false })),
39396
40227
  ...listDirs(ownedRootsDir()).map((dir) => ({ dir, owned: true }))
39397
40228
  ];
39398
40229
  }
39399
40230
  function manifestVersion(root) {
39400
40231
  try {
39401
- const text = readFileSync14(join24(root, "package.json"), "utf8");
40232
+ const text = readFileSync15(join25(root, "package.json"), "utf8");
39402
40233
  return text.match(/^[^\S\n]*"version"[^\S\n]*:[^\S\n]*"([^"\n]*)"/m)?.[1];
39403
40234
  } catch {
39404
40235
  return;
@@ -39433,9 +40264,10 @@ function prodLauncherRoot() {
39433
40264
  }
39434
40265
 
39435
40266
  // src/commands/install/service.ts
39436
- init_constants();
39437
40267
  init_paths3();
39438
40268
  init_settings();
40269
+ init_address();
40270
+ init_client();
39439
40271
  init_lifecycle();
39440
40272
  init_build_id();
39441
40273
  function prodServiceWatch() {
@@ -39445,6 +40277,8 @@ var SETTLE_POLL_MS = 500;
39445
40277
  var SETTLE_WINDOW_MS = 30000;
39446
40278
  var SETTLE_ATTEMPTS = SETTLE_WINDOW_MS / SETTLE_POLL_MS;
39447
40279
  function classify(health, installerVersion, replaced) {
40280
+ if (health instanceof DaemonAuthError)
40281
+ return { kind: "unauthorized", message: health.message };
39448
40282
  if (health?.service !== "caret")
39449
40283
  return { kind: "silent" };
39450
40284
  const answeringVersion = health.version;
@@ -39457,10 +40291,19 @@ function classify(health, installerVersion, replaced) {
39457
40291
  return { kind: "silent" };
39458
40292
  return isNewer(installerVersion, answeringVersion) ? { kind: "stale", version: answeringVersion } : { kind: "ready", version: answeringVersion };
39459
40293
  }
40294
+ async function probe(baseUrl, watch) {
40295
+ try {
40296
+ return await watch.health(baseUrl);
40297
+ } catch (e) {
40298
+ if (e instanceof DaemonAuthError)
40299
+ return e;
40300
+ throw e;
40301
+ }
40302
+ }
39460
40303
  async function awaitServed(baseUrl, watch, replaced) {
39461
40304
  let served = { kind: "silent" };
39462
40305
  for (let attempt = 0;attempt < SETTLE_ATTEMPTS; attempt++) {
39463
- served = classify(await watch.health(baseUrl), watch.version, replaced);
40306
+ served = classify(await probe(baseUrl, watch), watch.version, replaced);
39464
40307
  if (served.kind === "ready")
39465
40308
  return served;
39466
40309
  await watch.sleep(SETTLE_POLL_MS);
@@ -39486,6 +40329,8 @@ ${log}`;
39486
40329
  case "unsupervised":
39487
40330
  return `A caret the service did not start${at(served.version)} holds the port.
39488
40331
  The service takes over once it exits.`;
40332
+ case "unauthorized":
40333
+ return `${served.message.charAt(0).toUpperCase()}${served.message.slice(1)}.`;
39489
40334
  case "silent":
39490
40335
  return `No caret daemon answered within ${SETTLE_WINDOW_MS / 1000} seconds.
39491
40336
  ${log}`;
@@ -39521,9 +40366,9 @@ async function uninstallService(opts, deps, ui) {
39521
40366
  }
39522
40367
  var SERVE_COMMAND = "bunx --no-cache @macintacos/caret@latest serve";
39523
40368
  async function reconcileService(opts, deps, ui) {
39524
- const port = getPort(loadSettings());
39525
- const reviewUrl = `http://${VANITY_HOST}:${port}`;
39526
- const baseUrl = `http://localhost:${port}`;
40369
+ const s = loadSettings();
40370
+ const reviewUrl = publicUrl(s.daemon, getPort(s));
40371
+ const baseUrl = daemonBaseUrl(s);
39527
40372
  if (opts.choice === "run-yourself")
39528
40373
  return runYourself({ ...opts, reviewUrl }, deps, ui);
39529
40374
  await withService(deps, ui, async (target) => {
@@ -39563,7 +40408,7 @@ async function reconcileService(opts, deps, ui) {
39563
40408
  }
39564
40409
  const cycles = opts.refresh || opts.pinnedRoot !== undefined || unpinned;
39565
40410
  const { watch } = deps;
39566
- const replaced = cycles && watch ? (await watch.health(baseUrl))?.instanceId : undefined;
40411
+ const replaced = cycles && watch ? await probe(baseUrl, watch).then((h) => h instanceof DaemonAuthError ? undefined : h?.instanceId) : undefined;
39567
40412
  if (cycles)
39568
40413
  await manager.restart();
39569
40414
  const settled = !watch || await settleService(baseUrl, watch, replaced, cycles, ui);
@@ -39745,6 +40590,52 @@ async function selectServiceChoice(opts, deps, ui) {
39745
40590
  return choice;
39746
40591
  }
39747
40592
 
40593
+ // src/commands/login-link.ts
40594
+ init_paths3();
40595
+ init_settings();
40596
+ init_address();
40597
+ init_token();
40598
+ function exposureWarning(daemon, port) {
40599
+ if (!isExposed(daemon.host) || authEnabled(daemon))
40600
+ return null;
40601
+ return `caret: warning: daemon.auth = "none" on an exposed daemon (daemon.host = ${daemon.host}) \u2014 anyone on the network who can reach port ${port} can read plans, read files under a review's working directory, and approve a plan.`;
40602
+ }
40603
+ function loginLinkOutcome(daemon, port, token, tokenFile) {
40604
+ const url = publicUrl(daemon, port);
40605
+ if (!authEnabled(daemon)) {
40606
+ const warning = exposureWarning(daemon, port);
40607
+ return {
40608
+ stderr: [
40609
+ `caret: daemon auth is off, so there is no login link \u2014 open ${url} instead.`,
40610
+ ...warning === null ? [] : [warning]
40611
+ ],
40612
+ code: 1
40613
+ };
40614
+ }
40615
+ if (token === null) {
40616
+ return {
40617
+ stderr: [
40618
+ `caret: no daemon token at ${tokenFile} yet \u2014 start the daemon (caret serve, or the caret service) to mint one.`
40619
+ ],
40620
+ code: 1
40621
+ };
40622
+ }
40623
+ return { stdout: loginLink(url, token), stderr: [], code: 0 };
40624
+ }
40625
+ function runLoginLink() {
40626
+ const s = loadSettings();
40627
+ const tokenFile = daemonTokenFile();
40628
+ const token = authEnabled(s.daemon) ? readToken(tokenFile) : null;
40629
+ const outcome = loginLinkOutcome(s.daemon, getPort(s), token, tokenFile);
40630
+ for (const line of outcome.stderr)
40631
+ process.stderr.write(`${line}
40632
+ `);
40633
+ if (outcome.stdout !== undefined)
40634
+ process.stdout.write(`${outcome.stdout}
40635
+ `);
40636
+ process.exitCode = outcome.code;
40637
+ }
40638
+
39748
40639
  // src/commands/prewarm.ts
39749
40640
  init_settings();
39750
40641
  init_lifecycle();
@@ -39768,11 +40659,14 @@ async function runPrewarm() {
39768
40659
  init_adapters();
39769
40660
  init_boot();
39770
40661
  init_settings();
40662
+ init_address();
40663
+ init_client();
39771
40664
  init_log();
39772
40665
 
39773
40666
  // src/review/orchestrate.ts
39774
- init_constants();
39775
40667
  init_paths3();
40668
+ init_address();
40669
+ init_client();
39776
40670
  init_log();
39777
40671
 
39778
40672
  // src/plan/format.ts
@@ -39813,7 +40707,11 @@ var REVIEW_FAILURE_CODES = {
39813
40707
  longPoll: "unexpected"
39814
40708
  };
39815
40709
  function reviewFailureCode(step, err) {
39816
- return err instanceof TimeoutError ? "review-timeout" : REVIEW_FAILURE_CODES[step];
40710
+ if (err instanceof TimeoutError)
40711
+ return "review-timeout";
40712
+ if (err instanceof DaemonAuthError)
40713
+ return "daemon-unauthorized";
40714
+ return REVIEW_FAILURE_CODES[step];
39817
40715
  }
39818
40716
  function withTimeout(p, ms, message) {
39819
40717
  return new Promise((resolve, reject) => {
@@ -39834,6 +40732,11 @@ async function expireAbandoned(expire, handle) {
39834
40732
  await expire(handle.baseUrl, handle.id, handle.version);
39835
40733
  } catch {}
39836
40734
  }
40735
+ function reviewUrl(baseUrl, hostname, id) {
40736
+ const u = new URL(baseUrl);
40737
+ u.hostname = hostname;
40738
+ return `${u.origin}/?review=${id}`;
40739
+ }
39837
40740
  async function runReview(parsed, deps) {
39838
40741
  let step = "parse";
39839
40742
  setLogContext({});
@@ -39876,12 +40779,12 @@ async function runReview(parsed, deps) {
39876
40779
  setLogContext({ ...ids, reviewId });
39877
40780
  deps.onPosted?.({ baseUrl, id, version, planFileCurrent });
39878
40781
  logDebug("review", `review created: ${shortId(id)}`);
39879
- const open = new URL(baseUrl);
39880
- open.hostname = VANITY_HOST;
39881
- const url = `${open.origin}/?review=${id}`;
39882
- if (!hasLiveClient)
39883
- deps.openBrowser(url);
39884
- deps.announceUrl(url);
40782
+ if (!hasLiveClient) {
40783
+ const localUrl = reviewUrl(baseUrl, deps.localHostname, id);
40784
+ const token = deps.loginToken();
40785
+ deps.openBrowser(token === null ? localUrl : loginLink(localUrl, token));
40786
+ }
40787
+ deps.announceUrl(reviewUrl(baseUrl, deps.publicHostname, id));
39885
40788
  step = "longPoll";
39886
40789
  const deadline = Date.now() + deps.timeoutMs;
39887
40790
  let decision;
@@ -39961,7 +40864,7 @@ async function runReconcileSubcommand() {
39961
40864
  const loaded = loadSettings();
39962
40865
  bootHookLogging(loaded);
39963
40866
  const adapter = selectAdapter();
39964
- const baseUrl = `http://localhost:${getPort(loaded)}`;
40867
+ const baseUrl = daemonBaseUrl(loaded);
39965
40868
  const stdin = await Bun.stdin.text();
39966
40869
  await runReconcile(parseHook(adapter.parseHookInput, stdin), prodReconcileDeps(baseUrl));
39967
40870
  } catch (err) {
@@ -39996,7 +40899,10 @@ init_adapters();
39996
40899
  init_boot();
39997
40900
  init_paths3();
39998
40901
  init_settings();
40902
+ init_address();
40903
+ init_client();
39999
40904
  init_lifecycle();
40905
+ init_token();
40000
40906
  init_log();
40001
40907
  function browserOpenCmd(platform, url) {
40002
40908
  return platform === "darwin" ? ["open", url] : platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
@@ -40029,6 +40935,9 @@ function prodReviewDeps(settings) {
40029
40935
  announceUrl: (url) => {
40030
40936
  process.stderr.write(reviewUrlLine(url));
40031
40937
  },
40938
+ localHostname: localHostname(settings.daemon.host),
40939
+ publicHostname: publicHostname(settings.daemon),
40940
+ loginToken: () => authEnabled(settings.daemon) && process.platform !== "win32" ? readToken(daemonTokenFile()) : null,
40032
40941
  readPane: readCmuxPane,
40033
40942
  timeoutMs: reviewTimeoutMs(settings),
40034
40943
  expire: expireReview
@@ -40083,18 +40992,21 @@ async function runReviewSubcommand() {
40083
40992
  }
40084
40993
 
40085
40994
  // src/commands/serve.ts
40086
- init_constants();
40087
40995
  init_paths3();
40088
40996
  init_settings();
40997
+ init_address();
40998
+ init_client();
40089
40999
  init_lifecycle();
41000
+ init_token();
40090
41001
  async function runServe() {
40091
41002
  const world = stateDir();
40092
41003
  const refusal = await vacatePort({
40093
- baseUrl: `http://localhost:${getPort()}`,
41004
+ baseUrl: daemonBaseUrl(settings().current()),
40094
41005
  currentStateDir: world,
40095
41006
  deadlineMs: DRAIN_DEADLINE_MS + 5000,
40096
41007
  health: httpHealth,
40097
41008
  retire: (baseUrl) => retireDaemon(baseUrl, readDaemonLock(), world),
41009
+ readLock: readDaemonLock,
40098
41010
  now: Date.now,
40099
41011
  sleep: Bun.sleep
40100
41012
  });
@@ -40104,8 +41016,18 @@ async function runServe() {
40104
41016
  process.exitCode = 1;
40105
41017
  return;
40106
41018
  }
40107
- const port = await runDaemon({ ephemeral: false, resident: true });
40108
- process.stdout.write(`caret is serving the review UI at http://${VANITY_HOST}:${port} \u2014 Ctrl+C stops it.
41019
+ const { port, settings: booted } = await runDaemon({ ephemeral: false, resident: true });
41020
+ const url = publicUrl(booted.daemon, port);
41021
+ process.stdout.write(`caret is serving the review UI at ${url} \u2014 Ctrl+C stops it.
41022
+ `);
41023
+ const token = authEnabled(booted.daemon) ? readToken(daemonTokenFile()) : null;
41024
+ if (token !== null) {
41025
+ process.stdout.write(`Open this link once on each device to log it in: ${loginLink(url, token)}
41026
+ `);
41027
+ }
41028
+ const warning = exposureWarning(booted.daemon, port);
41029
+ if (warning !== null)
41030
+ process.stderr.write(`${warning}
40109
41031
  `);
40110
41032
  }
40111
41033
 
@@ -42199,6 +43121,7 @@ function buildProgram() {
42199
43121
  await runDaemon({ ephemeral: opts.ephemeral ?? false, resident: isSupervised() });
42200
43122
  });
42201
43123
  program.command("serve").description("keep the review UI up in this terminal until Ctrl+C").action(() => runServe());
43124
+ program.command("login-link").description("print the link that logs a browser into the review UI").action(() => runLoginLink());
42202
43125
  program.command("prewarm").description("warm-start the daemon").action(() => runPrewarm());
42203
43126
  program.command("review").description("review a plan from stdin (ExitPlanMode hook)").action(() => runReviewSubcommand());
42204
43127
  program.command("mcp").description("serve caret's plan-review tool over MCP stdio (Claude Code plugin)").action(async () => (await Promise.resolve().then(() => (init_mcp2(), exports_mcp))).runMcpSubcommand());