fleetlens 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +4 -0
  3. package/app/apps/web/.next/build-manifest.json +3 -3
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +24 -0
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  14. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/apps/web/.next/server/app/api/ask/route.js +3 -3
  17. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +4 -4
  19. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +4 -4
  21. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +2 -2
  23. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +4 -4
  25. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +2 -2
  27. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +4 -4
  29. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/settings/route.js +5 -4
  33. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/team/autostart/route/app-paths-manifest.json +3 -0
  35. package/app/apps/web/.next/server/app/api/team/autostart/route/build-manifest.json +9 -0
  36. package/app/apps/web/.next/server/app/api/team/autostart/route/server-reference-manifest.json +4 -0
  37. package/app/apps/web/.next/server/app/api/team/autostart/route.js +7 -0
  38. package/app/apps/web/.next/server/app/api/team/autostart/route.js.map +5 -0
  39. package/app/apps/web/.next/server/app/api/team/autostart/route.js.nft.json +1 -0
  40. package/app/apps/web/.next/server/app/api/team/autostart/route_client-reference-manifest.js +3 -0
  41. package/app/apps/web/.next/server/app/api/team/onboarding/start/route/app-paths-manifest.json +3 -0
  42. package/app/apps/web/.next/server/app/api/team/onboarding/start/route/build-manifest.json +9 -0
  43. package/app/apps/web/.next/server/app/api/team/onboarding/start/route/server-reference-manifest.json +4 -0
  44. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js +8 -0
  45. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js.map +5 -0
  46. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js.nft.json +1 -0
  47. package/app/apps/web/.next/server/app/api/team/onboarding/start/route_client-reference-manifest.js +3 -0
  48. package/app/apps/web/.next/server/app/api/team/sync-projects/route/app-paths-manifest.json +3 -0
  49. package/app/apps/web/.next/server/app/api/team/sync-projects/route/build-manifest.json +9 -0
  50. package/app/apps/web/.next/server/app/api/team/sync-projects/route/server-reference-manifest.json +4 -0
  51. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js +8 -0
  52. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js.map +5 -0
  53. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js.nft.json +1 -0
  54. package/app/apps/web/.next/server/app/api/team/sync-projects/route_client-reference-manifest.js +3 -0
  55. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  56. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  57. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  58. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  59. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  60. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  61. package/app/apps/web/.next/server/app/day/page.js +1 -1
  62. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  63. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  64. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  65. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  68. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  69. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  70. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  71. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  72. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  73. package/app/apps/web/.next/server/app/insights/page.js +1 -1
  74. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  75. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  76. package/app/apps/web/.next/server/app/page.js +1 -1
  77. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  78. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  79. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  80. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  81. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  82. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  83. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  84. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  85. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  86. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  87. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  88. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  89. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  90. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  91. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  92. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  93. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  94. package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -3
  95. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  96. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  97. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  98. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  100. package/app/apps/web/.next/server/app/settings/page.js +2 -2
  101. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  103. package/app/apps/web/.next/server/app/team/onboarding/page/app-paths-manifest.json +3 -0
  104. package/app/apps/web/.next/server/app/team/onboarding/page/build-manifest.json +17 -0
  105. package/app/apps/web/.next/server/app/team/onboarding/page/next-font-manifest.json +6 -0
  106. package/app/apps/web/.next/server/app/team/onboarding/page/react-loadable-manifest.json +1 -0
  107. package/app/apps/web/.next/server/app/team/onboarding/page/server-reference-manifest.json +4 -0
  108. package/app/apps/web/.next/server/app/team/onboarding/page.js +19 -0
  109. package/app/apps/web/.next/server/app/team/onboarding/page.js.map +5 -0
  110. package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -0
  111. package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +3 -0
  112. package/app/apps/web/.next/server/app/team/page.js +3 -2
  113. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  114. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  115. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  116. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  117. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  118. package/app/apps/web/.next/server/app-paths-manifest.json +4 -0
  119. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_team_onboarding_start_route_actions_0zeptl_.js +3 -0
  120. package/app/apps/web/.next/server/chunks/0fuv_zod_v4_classic_external_0.z.vap.js +39 -0
  121. package/app/apps/web/.next/server/chunks/[root-of-the-server]__038-.1r._.js +6 -0
  122. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0~utv~v._.js → [root-of-the-server]__03ewxxu._.js} +2 -2
  123. package/app/apps/web/.next/server/chunks/[root-of-the-server]__03rmbz6._.js +3 -0
  124. package/app/apps/web/.next/server/chunks/{[externals]__0o43ejh._.js → [root-of-the-server]__065lib8._.js} +2 -2
  125. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0e.f-t.._.js → [root-of-the-server]__0a0~7d6._.js} +2 -2
  126. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0gril0u._.js → [root-of-the-server]__0acikq.._.js} +2 -2
  127. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0et2sqb._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/{[externals]__09v47rg._.js → [root-of-the-server]__0jbc7tz._.js} +2 -2
  129. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0mq0or2._.js +3 -0
  130. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0mvl0qn._.js +3 -0
  131. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0n79hpm._.js → [root-of-the-server]__0ow1kn1._.js} +4 -4
  132. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~xt99s._.js +3 -0
  133. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_team_autostart_route_actions_10-4s71.js +3 -0
  134. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_team_sync-projects_route_actions_0r_2f5n.js +3 -0
  135. package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +4 -40
  136. package/app/apps/web/.next/server/chunks/packages_09wmnv.._.js +3 -0
  137. package/app/apps/web/.next/server/chunks/packages_0o6.gpk._.js +3 -0
  138. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  139. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  140. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0a7n44-._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0c00i76._.js +3 -0
  142. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0i.q-1h._.js +3 -0
  143. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0stu~~r._.js +3 -0
  144. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ufi3.w._.js +3 -0
  145. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  146. package/app/apps/web/.next/server/chunks/ssr/apps_web_0.97l5.._.js +3 -0
  147. package/app/apps/web/.next/server/chunks/ssr/{apps_web_app_settings_0u6z6rj._.js → apps_web_0q2x4a~._.js} +2 -2
  148. package/app/apps/web/.next/server/chunks/ssr/apps_web_114yo0v._.js +3 -0
  149. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_team_onboarding_page_actions_0pss~.k.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0c-tpft._.js +1 -1
  151. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  152. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  153. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  154. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_team-config_ts_10egg08._.js +39 -0
  155. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_0w2y7fz._.js +2 -2
  156. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0jxvcfi.js +2 -2
  157. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0s5yhs4.js +4 -0
  158. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  159. package/app/apps/web/.next/server/functions-config-manifest.json +3 -0
  160. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  161. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  162. package/app/apps/web/.next/server/pages/500.html +1 -1
  163. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  164. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  165. package/app/apps/web/.next/static/chunks/{0wwcx42ng8i2e.js → 0-3_u.90d79wi.js} +1 -1
  166. package/app/apps/web/.next/static/chunks/00a9oyw~h5i1p.js +1 -0
  167. package/app/apps/web/.next/static/chunks/040s42ypidk7s.css +1 -0
  168. package/app/apps/web/.next/static/chunks/06eiwghjkkbnh.js +1 -0
  169. package/app/apps/web/.next/static/chunks/06qeg4pt5jdqs.js +1 -0
  170. package/app/apps/web/.next/static/chunks/{0e_fzy0n8ij-o.js → 0u6gxpqhl0gc7.js} +1 -1
  171. package/app/apps/web/.next/static/chunks/18cj08-la37o-.js +1 -0
  172. package/app/apps/web/app/api/team/autostart/route.ts +41 -0
  173. package/app/apps/web/app/api/team/onboarding/start/route.ts +96 -0
  174. package/app/apps/web/app/api/team/sync-projects/route.ts +39 -0
  175. package/app/apps/web/app/globals.css +82 -0
  176. package/app/apps/web/app/settings/ai-features-form.tsx +1 -1
  177. package/app/apps/web/app/settings/page.tsx +21 -7
  178. package/app/apps/web/app/{settings → team}/force-sync-button.tsx +2 -4
  179. package/app/apps/web/app/team/onboarding/onboarding-wizard.tsx +503 -0
  180. package/app/apps/web/app/team/onboarding/page.tsx +28 -0
  181. package/app/apps/web/app/team/page.tsx +48 -17
  182. package/app/apps/web/app/{settings/team-connection-section.tsx → team/sync-activity-section.tsx} +6 -23
  183. package/app/apps/web/app/team/synced-projects-form.tsx +201 -0
  184. package/app/apps/web/app/team/synced-projects-section.tsx +37 -0
  185. package/app/apps/web/components/autostart-toggle.tsx +59 -0
  186. package/app/apps/web/components/project-sync-picker.tsx +109 -0
  187. package/app/apps/web/components/team-chip.tsx +1 -1
  188. package/app/apps/web/components/team-welcome-banner.tsx +1 -1
  189. package/app/apps/web/lib/autostart-data.ts +31 -0
  190. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  191. package/app/apps/web/lib/sync-projects-data.ts +36 -0
  192. package/app/apps/web/lib/team-config.ts +21 -2
  193. package/app/apps/web/package.json +1 -1
  194. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  195. package/dist/daemon-worker.js +150 -42
  196. package/dist/index.js +311 -100
  197. package/package.json +1 -1
  198. package/app/apps/web/.next/server/chunks/[root-of-the-server]__08agijy._.js +0 -3
  199. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0i06ito._.js +0 -3
  200. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0y5p73y._.js +0 -3
  201. package/app/apps/web/.next/server/chunks/packages_0c-sz9n._.js +0 -3
  202. package/app/apps/web/.next/server/chunks/packages_0wywuu.._.js +0 -3
  203. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__09yxy53._.js +0 -3
  204. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_hv6aq._.js +0 -3
  205. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0dh0q7b._.js +0 -3
  206. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv0m0q._.js +0 -3
  207. package/app/apps/web/.next/static/chunks/005ad7l-h1-.6.js +0 -1
  208. package/app/apps/web/.next/static/chunks/0h.ua.-myj.6z.js +0 -1
  209. package/app/apps/web/.next/static/chunks/0l4r0bqqtdju..css +0 -1
  210. /package/app/apps/web/.next/static/{3VbAChKD8EChlt8Tm5N5r → C4y6rare_ZCcXXQLc8ysA}/_buildManifest.js +0 -0
  211. /package/app/apps/web/.next/static/{3VbAChKD8EChlt8Tm5N5r → C4y6rare_ZCcXXQLc8ysA}/_clientMiddlewareManifest.js +0 -0
  212. /package/app/apps/web/.next/static/{3VbAChKD8EChlt8Tm5N5r → C4y6rare_ZCcXXQLc8ysA}/_ssgManifest.js +0 -0
@@ -2,12 +2,14 @@
2
2
  * Team-sync status page.
3
3
  *
4
4
  * Shows which team this machine's daemon is paired with and what data flows
5
- * over the wire on the next push. There is exactly one sync behavior: all
6
- * active projects and all LLM-enriched fields are shared — there is no
7
- * member-side gating. The page reads the same on-disk team-config.json the
8
- * daemon reads.
5
+ * over the wire on the next push. All LLM-enriched fields are shared for
6
+ * every synced project; which projects are synced is the member's own
7
+ * choice (Settings' Synced-projects selection, seeded by the onboarding
8
+ * wizard). The page reads the same on-disk team-config.json the daemon reads.
9
9
  */
10
10
  import { readTeamConfig, toTeamConfigView } from "@/lib/team-config";
11
+ import { SyncedProjectsSection } from "./synced-projects-section";
12
+ import { SyncActivitySection } from "./sync-activity-section";
11
13
 
12
14
  export const dynamic = "force-dynamic";
13
15
 
@@ -37,7 +39,7 @@ export default function TeamPage() {
37
39
  return (
38
40
  <main className="mx-auto max-w-3xl p-6 space-y-6">
39
41
  <h1 className="text-2xl font-semibold">Team sync</h1>
40
- <section className="border border-dashed rounded-lg p-6 text-sm space-y-3">
42
+ <section className="af-card border-dashed text-sm space-y-3">
41
43
  <p className="font-medium">Not paired with any team.</p>
42
44
  <p className="text-gray-600">
43
45
  To pair this machine with a Fleetlens team server, run from a
@@ -56,6 +58,12 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
56
58
  }
57
59
 
58
60
  const view = toTeamConfigView(config);
61
+ const sp = config.syncProjects;
62
+ const syncSummary = sp
63
+ ? sp.autoIncludeNew
64
+ ? `Syncing: all projects except ${sp.excluded.length} excluded`
65
+ : `Syncing: only ${sp.included.length} selected project${sp.included.length === 1 ? "" : "s"}`
66
+ : null;
59
67
 
60
68
  return (
61
69
  <main className="mx-auto max-w-3xl p-6 space-y-8">
@@ -66,7 +74,14 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
66
74
  </p>
67
75
  </header>
68
76
 
69
- <section className="border rounded-lg p-5 space-y-3">
77
+ {config.setupPending && (
78
+ <div className="border border-amber-400/40 bg-amber-400/10 rounded-lg p-4 text-sm">
79
+ Setup isn&rsquo;t finished — nothing is syncing yet.{" "}
80
+ <a className="underline font-medium" href="/team/onboarding">Finish onboarding</a>
81
+ </div>
82
+ )}
83
+
84
+ <section className="af-card space-y-3">
70
85
  <div className="flex items-baseline justify-between">
71
86
  <h2 className="text-lg font-medium">Pairing</h2>
72
87
  <span className="text-xs uppercase tracking-wide text-green-700 dark:text-green-400">
@@ -77,7 +92,11 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
77
92
  <dt className="text-gray-500">Team slug</dt>
78
93
  <dd className="font-mono">{view.teamSlug}</dd>
79
94
  <dt className="text-gray-500">Server</dt>
80
- <dd className="font-mono break-all">{view.serverUrl}</dd>
95
+ <dd className="font-mono break-all">
96
+ <a href={view.serverUrl} target="_blank" rel="noreferrer" className="hover:underline">
97
+ {view.serverUrl} <span aria-hidden>↗</span>
98
+ </a>
99
+ </dd>
81
100
  <dt className="text-gray-500">Member ID</dt>
82
101
  <dd className="font-mono break-all text-xs">{view.memberId}</dd>
83
102
  <dt className="text-gray-500">Bearer token</dt>
@@ -91,11 +110,18 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
91
110
  </dl>
92
111
  </section>
93
112
 
94
- <section className="border rounded-lg p-5 space-y-3">
113
+ <section className="af-card">
114
+ <SyncActivitySection />
115
+ </section>
116
+
117
+ <section className="af-card">
118
+ <SyncedProjectsSection />
119
+ </section>
120
+
121
+ <section className="af-card space-y-3">
95
122
  <h2 className="text-lg font-medium">What gets shared</h2>
96
123
  <p className="text-sm text-gray-500">
97
- Every push shares the same data across all your active projects.
98
- There is no per-project or per-field opt-out.
124
+ {syncSummary ?? "Syncing: all projects (no selection set)."}
99
125
  </p>
100
126
  <ul className="text-sm space-y-2">
101
127
  <li>
@@ -103,8 +129,8 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
103
129
  calls, turns, token totals.
104
130
  </li>
105
131
  <li>
106
- <strong>Rich rollup</strong> (Entry-derived counts) for all active
107
- projects: working-shape distribution, top skills, subagents
132
+ <strong>Rich rollup</strong> (Entry-derived counts) for each
133
+ synced project: working-shape distribution, top skills, subagents
108
134
  dispatched, long-autonomous turns, plan-mode usage, brainstorm
109
135
  warmups, tool errors, PRs/commits/pushes. Project labels only;
110
136
  never raw prompts or agent output.
@@ -119,13 +145,18 @@ fleetlens team join &lt;server-url&gt; &lt;invite-token&gt;
119
145
  model calls ride along with the push).
120
146
  </li>
121
147
  </ul>
122
- <p className="text-xs text-gray-500 pt-2">
123
- Never shared: first-user prompts, final-agent output, raw transcript
124
- text, tool inputs/outputs.
125
- </p>
148
+ <div className="af-panel p-4 text-sm space-y-2">
149
+ <div className="font-medium">What does NOT leave your machine</div>
150
+ <ul className="list-disc list-inside text-gray-600 space-y-1">
151
+ <li>Session transcripts, prompts, or assistant responses</li>
152
+ <li>Absolute paths, file contents, or tool-call payloads</li>
153
+ <li>Anything from projects you exclude above</li>
154
+ <li>Anything from sessions older than the start-of-day rollup window</li>
155
+ </ul>
156
+ </div>
126
157
  </section>
127
158
 
128
- <section className="border rounded-lg p-5 space-y-3">
159
+ <section className="af-card space-y-3">
129
160
  <h2 className="text-lg font-medium">Disconnect</h2>
130
161
  <p className="text-sm text-gray-600">
131
162
  To leave the team and stop all syncing, run:
@@ -8,17 +8,17 @@ const HEALTH_COLORS = {
8
8
  red: "var(--af-error, #ef4444)",
9
9
  } as const;
10
10
 
11
- export function TeamConnectionSection() {
11
+ export function SyncActivitySection() {
12
12
  const conn = readTeamConnection();
13
13
  if (!conn.paired) return null;
14
14
 
15
- const { team, member, lastPush, health } = conn;
15
+ const { lastPush, health } = conn;
16
16
  const cliAvailable = Boolean(process.env.FLEETLENS_CLI_BIN);
17
17
 
18
18
  return (
19
19
  <section id="team" className="space-y-4">
20
20
  <div className="flex items-baseline gap-3">
21
- <h2 className="text-lg font-medium">Team connection</h2>
21
+ <h2 className="text-lg font-medium">Sync activity</h2>
22
22
  <span
23
23
  aria-hidden
24
24
  style={{
@@ -29,12 +29,9 @@ export function TeamConnectionSection() {
29
29
  display: "inline-block",
30
30
  }}
31
31
  />
32
- <span className="text-sm text-gray-500">{team.name}</span>
33
32
  </div>
34
33
 
35
34
  <dl className="grid grid-cols-[8rem_1fr] gap-y-1 text-sm text-gray-600">
36
- <dt>Server</dt><dd className="font-mono text-xs">{team.serverUrl}</dd>
37
- <dt>Paired</dt><dd>{new Date(member.pairedAt).toLocaleString()}</dd>
38
35
  <dt>Last sync</dt>
39
36
  <dd suppressHydrationWarning>
40
37
  {lastPush.kind === "none"
@@ -44,7 +41,7 @@ export function TeamConnectionSection() {
44
41
  </dl>
45
42
 
46
43
  {lastPush.kind === "ok" && (
47
- <div className="rounded border border-gray-200 p-4 space-y-3 text-sm">
44
+ <div className="af-panel p-4 space-y-3 text-sm">
48
45
  <div className="text-xs uppercase tracking-wide text-gray-500">
49
46
  Last push
50
47
  {lastPush.payload.dailyRollup ? ` — ${lastPush.payload.dailyRollup.day}` : ""}
@@ -93,7 +90,7 @@ export function TeamConnectionSection() {
93
90
  <summary className="cursor-pointer text-gray-500 hover:text-gray-700">
94
91
  Show raw JSON payload
95
92
  </summary>
96
- <pre className="mt-2 rounded border border-gray-200 bg-gray-50 p-3 font-mono text-[11px] whitespace-pre-wrap overflow-x-auto">
93
+ <pre className="af-panel mt-2 p-3 font-mono text-[11px] whitespace-pre-wrap overflow-x-auto">
97
94
  {JSON.stringify(lastPush.payload, null, 2)}
98
95
  </pre>
99
96
  </details>
@@ -101,27 +98,13 @@ export function TeamConnectionSection() {
101
98
  )}
102
99
 
103
100
  {lastPush.kind === "error" && (
104
- <div className="rounded border border-red-300 bg-red-50 p-4 text-sm text-red-700">
101
+ <div className="af-panel-danger p-4 text-sm">
105
102
  <div className="font-medium">Last sync failed</div>
106
103
  <div>{lastPush.error}</div>
107
104
  </div>
108
105
  )}
109
106
 
110
107
  <ForceSyncButton cliAvailable={cliAvailable} />
111
-
112
- <div className="rounded border border-gray-200 p-4 text-sm space-y-2">
113
- <div className="font-medium">What does NOT leave your machine</div>
114
- <ul className="list-disc list-inside text-gray-600 space-y-1">
115
- <li>Session transcripts, prompts, or assistant responses</li>
116
- <li>Project names, paths, or repo information</li>
117
- <li>File contents or tool-call payloads</li>
118
- <li>Anything from sessions older than the start-of-day rollup window</li>
119
- </ul>
120
- </div>
121
-
122
- <p className="text-xs text-gray-500">
123
- To disconnect from this team, run <code>fleetlens team leave</code> in your terminal.
124
- </p>
125
108
  </section>
126
109
  );
127
110
  }
@@ -0,0 +1,201 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { ProjectSyncPicker } from "@/components/project-sync-picker";
4
+ import type { SyncProjectRow } from "@/lib/sync-projects-data";
5
+ import type { SyncProjects } from "@/lib/team-config";
6
+
7
+ const ts = () => new Date().toTimeString().slice(0, 8);
8
+
9
+ export function SyncedProjectsForm({
10
+ projects,
11
+ initial,
12
+ }: {
13
+ projects: SyncProjectRow[];
14
+ initial: SyncProjects;
15
+ }) {
16
+ const [value, setValue] = useState<SyncProjects>(initial);
17
+ // Last-persisted selection — the read-only summary renders from this, so a
18
+ // cancelled edit or in-flight change never lies about what's syncing.
19
+ const [saved, setSaved] = useState<SyncProjects>(initial);
20
+ const [editing, setEditing] = useState(false);
21
+ const [saving, setSaving] = useState(false);
22
+ const [savedMsg, setSavedMsg] = useState<string | null>(null);
23
+ const [log, setLog] = useState<{ text: string; tone: "default" | "warn" | "error" }[] | null>(null);
24
+ const logRef = useRef<HTMLDivElement>(null);
25
+
26
+ useEffect(() => {
27
+ logRef.current?.scrollTo({ top: logRef.current.scrollHeight });
28
+ }, [log]);
29
+
30
+ const append = (text: string, tone: "default" | "warn" | "error" = "default") =>
31
+ setLog((prev) => [...(prev ?? []), { text: `${ts()} ${text}`, tone }]);
32
+
33
+ // Same NDJSON→SSE stream the onboarding wizard consumes; a changed
34
+ // selection re-pushes FULL history so the server matches it.
35
+ async function streamResync() {
36
+ setLog([]);
37
+ append("re-pushing your full history under the new selection…");
38
+ try {
39
+ const res = await fetch("/api/team/onboarding/start", {
40
+ method: "POST",
41
+ headers: { "content-type": "application/json" },
42
+ body: JSON.stringify(value),
43
+ });
44
+ if (!res.ok || !res.body) {
45
+ const body = await res.json().catch(() => null);
46
+ append(body?.error ?? `Error ${res.status}`, "error");
47
+ return;
48
+ }
49
+ const reader = res.body.getReader();
50
+ const decoder = new TextDecoder();
51
+ let buf = "";
52
+ for (;;) {
53
+ const { done, value: chunk } = await reader.read();
54
+ if (done) break;
55
+ buf += decoder.decode(chunk, { stream: true });
56
+ const frames = buf.split("\n\n");
57
+ buf = frames.pop() ?? "";
58
+ for (const frame of frames) {
59
+ const data = frame.split("\n").find((l) => l.startsWith("data: "))?.slice(6);
60
+ if (!data) continue;
61
+ let ev;
62
+ try {
63
+ ev = JSON.parse(data);
64
+ } catch {
65
+ continue;
66
+ }
67
+ if (ev.type === "phase" && ev.phase === "activity") append(`pushing ${ev.totalDays} day(s), newest first…`);
68
+ else if (ev.type === "day")
69
+ append(
70
+ ev.outcome === "pushed"
71
+ ? `✓ ${ev.day} pushed (${ev.index}/${ev.total})`
72
+ : `${ev.outcome === "queued" ? "⚠" : "✗"} ${ev.day} ${ev.outcome} (${ev.index}/${ev.total})`,
73
+ ev.outcome === "pushed" ? "default" : ev.outcome === "queued" ? "warn" : "error",
74
+ );
75
+ else if (ev.type === "done") append(`done — ${ev.pushed} day(s) pushed`);
76
+ else if (ev.type === "error") append(ev.message ?? "sync error", "error");
77
+ }
78
+ }
79
+ append("server now matches your selection");
80
+ } catch (err) {
81
+ append(`stream failed: ${(err as Error).message} — the daemon finishes the re-push within ~5 min`, "warn");
82
+ }
83
+ }
84
+
85
+ async function handleSave() {
86
+ setSaving(true);
87
+ setSavedMsg(null);
88
+ setLog(null);
89
+ const res = await fetch("/api/team/sync-projects", {
90
+ method: "PUT",
91
+ headers: { "content-type": "application/json" },
92
+ body: JSON.stringify(value),
93
+ });
94
+ const body = await res.json().catch(() => null);
95
+ setSaving(false);
96
+ setSavedMsg(
97
+ res.ok ? (body?.resync ? "Saved — selection changed." : "Saved.") : (body?.error ?? `Error: ${res.status}`),
98
+ );
99
+ if (res.ok) {
100
+ setSaved(value);
101
+ setEditing(false);
102
+ }
103
+ if (body?.resync) void streamResync();
104
+ }
105
+
106
+ const isSynced = (name: string) =>
107
+ saved.excluded.includes(name) ? false : saved.included.includes(name) ? true : saved.autoIncludeNew;
108
+ const synced = projects.filter((p) => isSynced(p.name));
109
+ const privateCount = projects.length - synced.length;
110
+
111
+ return (
112
+ <div className="space-y-4">
113
+ {!editing && (
114
+ <div className="space-y-3">
115
+ <p className="text-sm">
116
+ Syncing <strong>{synced.length}</strong> of {projects.length} projects
117
+ {saved.autoIncludeNew ? " — new projects sync automatically." : " — new projects stay private."}
118
+ </p>
119
+ <div className="flex flex-wrap gap-1.5">
120
+ {synced.slice(0, 8).map((p) => (
121
+ <a
122
+ key={p.name}
123
+ href={`/projects/${encodeURIComponent(p.projectDir)}`}
124
+ className="af-chip"
125
+ >
126
+ {p.name}
127
+ </a>
128
+ ))}
129
+ {synced.length > 8 && (
130
+ <span className="px-2 py-0.5 text-xs text-gray-500">+{synced.length - 8} more</span>
131
+ )}
132
+ </div>
133
+ {privateCount > 0 && (
134
+ <p className="text-xs text-gray-500">
135
+ {privateCount} project{privateCount === 1 ? " stays" : "s stay"} private on this machine.
136
+ </p>
137
+ )}
138
+ <div className="flex items-center gap-3">
139
+ <button
140
+ type="button"
141
+ onClick={() => {
142
+ setValue(saved);
143
+ setSavedMsg(null);
144
+ setEditing(true);
145
+ }}
146
+ className="af-btn"
147
+ >
148
+ Edit selection
149
+ </button>
150
+ {savedMsg && <p className="text-sm">{savedMsg}</p>}
151
+ </div>
152
+ </div>
153
+ )}
154
+ {editing && (
155
+ <>
156
+ <ProjectSyncPicker projects={projects} value={value} onChange={setValue} />
157
+ <div className="flex items-center gap-3">
158
+ <button
159
+ type="button"
160
+ onClick={handleSave}
161
+ disabled={saving}
162
+ className="af-btn-primary"
163
+ >
164
+ {saving ? "Saving…" : "Save"}
165
+ </button>
166
+ <button
167
+ type="button"
168
+ onClick={() => {
169
+ setValue(saved);
170
+ setEditing(false);
171
+ setSavedMsg(null);
172
+ }}
173
+ disabled={saving}
174
+ className="af-btn"
175
+ >
176
+ Cancel
177
+ </button>
178
+ {savedMsg && <p className="text-sm">{savedMsg}</p>}
179
+ </div>
180
+ </>
181
+ )}
182
+ {log && (
183
+ <div
184
+ ref={logRef}
185
+ className="max-h-60 overflow-y-auto rounded-lg bg-gray-950 p-3 font-mono text-xs text-gray-200"
186
+ >
187
+ {log.map((line, i) => (
188
+ <div
189
+ key={i}
190
+ className={
191
+ line.tone === "error" ? "text-red-400" : line.tone === "warn" ? "text-amber-400" : undefined
192
+ }
193
+ >
194
+ {line.text}
195
+ </div>
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ );
201
+ }
@@ -0,0 +1,37 @@
1
+ import { readTeamConfig } from "@/lib/team-config";
2
+ import { listSyncProjectRows } from "@/lib/sync-projects-data";
3
+ import { SyncedProjectsForm } from "./synced-projects-form";
4
+
5
+ export async function SyncedProjectsSection() {
6
+ const config = readTeamConfig();
7
+ if (!config) return null;
8
+
9
+ if (config.setupPending) {
10
+ return (
11
+ <section className="space-y-2">
12
+ <h2 className="text-lg font-medium">Synced projects</h2>
13
+ <p className="text-sm text-gray-500">
14
+ Setup isn&rsquo;t finished yet — finish the{" "}
15
+ <a className="underline" href="/team/onboarding">onboarding wizard</a>{" "}
16
+ to choose which projects sync to your team.
17
+ </p>
18
+ </section>
19
+ );
20
+ }
21
+
22
+ const projects = await listSyncProjectRows();
23
+ return (
24
+ <section className="space-y-3">
25
+ <h2 className="text-lg font-medium">Synced projects</h2>
26
+ <p className="text-sm text-gray-500">
27
+ Choose which projects share metrics with your team. Saving a changed
28
+ selection re-pushes your full history so the server matches it —
29
+ excluded projects disappear, newly included ones backfill.
30
+ </p>
31
+ <SyncedProjectsForm
32
+ projects={projects}
33
+ initial={config.syncProjects ?? { autoIncludeNew: true, included: [], excluded: [] }}
34
+ />
35
+ </section>
36
+ );
37
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import type { AutostartState } from "@/lib/autostart-data";
4
+
5
+ export function AutostartToggle({ initial }: { initial: AutostartState }) {
6
+ const [state, setState] = useState(initial);
7
+ const [busy, setBusy] = useState(false);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ if (!state.supported) {
11
+ return (
12
+ <p className="text-sm text-gray-500">
13
+ Auto-start uses a macOS LaunchAgent and is macOS-only for now. Add{" "}
14
+ <code className="font-mono text-xs">fleetlens daemon start</code> to your login items manually.
15
+ </p>
16
+ );
17
+ }
18
+
19
+ async function toggle(enabled: boolean) {
20
+ setBusy(true);
21
+ setError(null);
22
+ const res = await fetch("/api/team/autostart", {
23
+ method: "PUT",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify({ enabled }),
26
+ });
27
+ const body = await res.json().catch(() => null);
28
+ setBusy(false);
29
+ if (!res.ok) {
30
+ setError(body?.error ?? `Error ${res.status}`);
31
+ return;
32
+ }
33
+ setState(body);
34
+ }
35
+
36
+ return (
37
+ <div className="space-y-2">
38
+ <label className="flex cursor-pointer items-start gap-2 text-sm">
39
+ <input
40
+ type="checkbox"
41
+ checked={state.installed}
42
+ disabled={busy}
43
+ onChange={(e) => void toggle(e.target.checked)}
44
+ />
45
+ <span>
46
+ Start Fleetlens at login (dashboard + usage daemon)
47
+ <span className="mt-0.5 block text-xs text-gray-500">
48
+ {state.installed
49
+ ? "Enabled — the dashboard and reporting come back after a reboot."
50
+ : state.optedOut
51
+ ? "Disabled — you opted out; joining a team won't re-enable it."
52
+ : "Disabled — pairing with a team enables this automatically."}
53
+ </span>
54
+ </span>
55
+ </label>
56
+ {error && <p className="text-sm text-red-600">{error}</p>}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import type { SyncProjectRow } from "@/lib/sync-projects-data";
5
+ import type { SyncProjects } from "@/lib/team-config";
6
+ import { formatRelative } from "@/lib/format";
7
+
8
+ /** Compact agent-time label for a picker row — not `formatDuration`'s "1h 30m"
9
+ * form, which reads noisy next to sessions/last-active in a dense list. */
10
+ function formatAgentTimeShort(ms: number): string {
11
+ if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
12
+ return `${Math.round((ms / 3_600_000) * 10) / 10}h`;
13
+ }
14
+
15
+ export function ProjectSyncPicker({
16
+ projects,
17
+ value,
18
+ onChange,
19
+ }: {
20
+ projects: SyncProjectRow[];
21
+ value: SyncProjects;
22
+ onChange: (v: SyncProjects) => void;
23
+ }) {
24
+ const [query, setQuery] = useState("");
25
+
26
+ function checked(name: string): boolean {
27
+ if (value.excluded.includes(name)) return false;
28
+ if (value.included.includes(name)) return true;
29
+ return value.autoIncludeNew;
30
+ }
31
+
32
+ function toggle(name: string, next: boolean) {
33
+ const included = value.included.filter((n) => n !== name);
34
+ const excluded = value.excluded.filter((n) => n !== name);
35
+ (next ? included : excluded).push(name);
36
+ onChange({ ...value, included, excluded });
37
+ }
38
+
39
+ const filtered = useMemo(() => {
40
+ const q = query.trim().toLowerCase();
41
+ if (!q) return projects;
42
+ return projects.filter((p) => p.name.toLowerCase().includes(q));
43
+ }, [projects, query]);
44
+
45
+ function selectAll(next: boolean) {
46
+ let included = [...value.included];
47
+ let excluded = [...value.excluded];
48
+ for (const p of filtered) {
49
+ included = included.filter((n) => n !== p.name);
50
+ excluded = excluded.filter((n) => n !== p.name);
51
+ (next ? included : excluded).push(p.name);
52
+ }
53
+ onChange({ ...value, included, excluded });
54
+ }
55
+
56
+ return (
57
+ <div className="space-y-3">
58
+ <div className="flex items-center gap-3">
59
+ <input
60
+ type="text"
61
+ value={query}
62
+ onChange={(e) => setQuery(e.target.value)}
63
+ placeholder="Filter projects…"
64
+ className="flex-1 text-sm"
65
+ />
66
+ <button type="button" onClick={() => selectAll(true)} className="text-xs text-gray-500 underline hover:text-gray-700">
67
+ {query.trim() ? "Select visible" : "Select all"}
68
+ </button>
69
+ <button type="button" onClick={() => selectAll(false)} className="text-xs text-gray-500 underline hover:text-gray-700">
70
+ {query.trim() ? "Deselect visible" : "Deselect all"}
71
+ </button>
72
+ </div>
73
+
74
+ <div className="af-panel max-h-96 divide-y overflow-y-auto">
75
+ {filtered.length === 0 && (
76
+ <div className="p-3 text-sm text-gray-500">No projects match &ldquo;{query}&rdquo;.</div>
77
+ )}
78
+ {filtered.map((p) => (
79
+ <label
80
+ key={p.name}
81
+ className="flex cursor-pointer items-center gap-3 p-3 text-sm af-row"
82
+ >
83
+ <input type="checkbox" checked={checked(p.name)} onChange={(e) => toggle(p.name, e.target.checked)} />
84
+ <div className="min-w-0 flex-1">
85
+ <div className="truncate font-mono">{p.name}</div>
86
+ <div className="text-xs text-gray-500">
87
+ {p.sessions} session{p.sessions === 1 ? "" : "s"} · {formatAgentTimeShort(p.agentTimeMs)}
88
+ {p.lastActiveMs !== null && <> · {formatRelative(new Date(p.lastActiveMs).toISOString())}</>}
89
+ {p.worktreeCount > 1 && <> · ×{p.worktreeCount} worktrees</>}
90
+ </div>
91
+ </div>
92
+ </label>
93
+ ))}
94
+ </div>
95
+
96
+ <label className="flex items-start gap-2 text-sm">
97
+ <input
98
+ type="checkbox"
99
+ checked={value.autoIncludeNew}
100
+ onChange={(e) => onChange({ ...value, autoIncludeNew: e.target.checked })}
101
+ />
102
+ <span>
103
+ Automatically sync projects that appear later
104
+ <span className="mt-0.5 block text-xs text-gray-500">Unchecked projects never leave this machine.</span>
105
+ </span>
106
+ </label>
107
+ </div>
108
+ );
109
+ }
@@ -53,7 +53,7 @@ export function TeamChip({ connection }: { connection: TeamConnection }) {
53
53
 
54
54
  return (
55
55
  <Link
56
- href="/settings#team"
56
+ href="/team"
57
57
  title={tooltipParts.join("\n")}
58
58
  style={{
59
59
  display: "flex",
@@ -46,7 +46,7 @@ export function TeamWelcomeBanner({
46
46
  activity totals and current cycle utilization to the team dashboard
47
47
  every 5 minutes. Transcripts, prompts, and project content never leave
48
48
  your machine.{" "}
49
- <Link href="/settings#team" style={{ color: "var(--af-accent)", textDecoration: "underline" }}>
49
+ <Link href="/team" style={{ color: "var(--af-accent)", textDecoration: "underline" }}>
50
50
  See exactly what&apos;s shared →
51
51
  </Link>
52
52
  </div>
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { cclensHome } from "@claude-lens/parser/fs";
5
+
6
+ // Mirrors packages/cli/src/commands/autostart.ts state (plist presence +
7
+ // ~/.cclens/autostart.json flags). Reads only — mutations go through the CLI
8
+ // binary so the plist always gets the CLI's own node/script paths baked in.
9
+
10
+ const PLIST = join(homedir(), "Library", "LaunchAgents", "com.fleetlens.daemon.plist");
11
+
12
+ export type AutostartState = {
13
+ supported: boolean;
14
+ installed: boolean;
15
+ optedOut: boolean;
16
+ };
17
+
18
+ export function readAutostartState(): AutostartState {
19
+ const supported = process.platform === "darwin";
20
+ const installed = supported && existsSync(PLIST);
21
+ let optedOut = false;
22
+ try {
23
+ const flags = JSON.parse(readFileSync(join(cclensHome(), "autostart.json"), "utf8")) as {
24
+ optedOut?: boolean;
25
+ };
26
+ optedOut = flags.optedOut === true;
27
+ } catch {
28
+ // no flags file — never opted out
29
+ }
30
+ return { supported, installed, optedOut };
31
+ }