@usepowerplant/cli 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,8 +22,11 @@ re-run at any time — finished steps are skipped with a note:
22
22
  2. **Install the background service.** The sync service watches your local
23
23
  coding-agent sessions (Claude Code, Codex, and Cursor) and uploads the
24
24
  ones in scope. If recent sessions name repositories under a macOS-protected
25
- location such as Documents, init lists the location and asks before reading
26
- Git metadata there; declining leaves those sessions local. The service
25
+ location such as Documents, init lists the location and asks once before
26
+ reading Git metadata there; declining leaves those sessions local, and a
27
+ yes covers locations that appear later (init still explains before macOS
28
+ asks). A location macOS refused is re-checked on every init until you
29
+ enable it in System Settings. The service
27
30
  starts on login and keeps itself running. On macOS, init also installs
28
31
  the Powerplant menu bar app from this package into `~/Applications` and
29
32
  launches it; the app registers itself to start at login (turn that off
@@ -131,9 +134,11 @@ then restarts itself onto it. The choice is per machine and only an
131
134
  explicit yes counts: a machine set up before the question existed stays
132
135
  off until someone turns it on. `powerplant status` and `powerplant doctor`
133
136
  report the current choice, and `powerplant update --automatic on` or
134
- `--automatic off`, or the menu bar's More menu, change it. A release that
135
- fails to install is not retried by itself; the menu bar shows the failure
136
- with a retry and the update log, and the next release clears it.
137
+ `--automatic off`, or the menu bar's More menu, change it. Turning it on
138
+ (there, or at init's question) makes the sync check within seconds, so a
139
+ release already out installs now rather than at the next hourly check. A
140
+ release that fails to install is not retried by itself; the menu bar shows
141
+ the failure with a retry and the update log, and the next release clears it.
137
142
 
138
143
  Powerplant also publishes the oldest version it still supports. When yours
139
144
  is older, `powerplant status`, `powerplant doctor`, the menu bar app, and
package/dist/cli.js CHANGED
@@ -55485,20 +55485,210 @@ function workspaceClaimsAnyRepo(policy) {
55485
55485
  return policy.repos !== null ? policy.repos.length > 0 : policy.owners.length > 0;
55486
55486
  }
55487
55487
 
55488
+ // ../core/src/encodedImages.ts
55489
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
55490
+ function isEncodedImage(data, mediaType) {
55491
+ if (data.length < 16 || data.length % 4 !== 0) return false;
55492
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
55493
+ const body = data.slice(0, data.length - padding);
55494
+ if (/[^A-Za-z0-9+/]/.test(body)) return false;
55495
+ const last = BASE64_ALPHABET.indexOf(body.at(-1));
55496
+ if (padding === 2 && (last & 15) !== 0 || padding === 1 && (last & 3) !== 0) return false;
55497
+ switch (mediaType) {
55498
+ case "image/png":
55499
+ return isPng(atob(data));
55500
+ case "image/jpeg":
55501
+ return isJpeg(atob(data));
55502
+ case "image/gif":
55503
+ return isGif(atob(data));
55504
+ case "image/webp":
55505
+ return isWebp(atob(data));
55506
+ default:
55507
+ return false;
55508
+ }
55509
+ }
55510
+ function u16(data, offset) {
55511
+ return data.charCodeAt(offset) << 8 | data.charCodeAt(offset + 1);
55512
+ }
55513
+ function u32(data, offset) {
55514
+ return u16(data, offset) * 65536 + u16(data, offset + 2) >>> 0;
55515
+ }
55516
+ function little32(data, offset) {
55517
+ return (data.charCodeAt(offset) | data.charCodeAt(offset + 1) << 8 | data.charCodeAt(offset + 2) << 16 | data.charCodeAt(offset + 3) << 24) >>> 0;
55518
+ }
55519
+ var CRC_TABLE = Array.from({ length: 256 }, (_, value) => {
55520
+ for (let bit = 0; bit < 8; bit++) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
55521
+ return value >>> 0;
55522
+ });
55523
+ function crc32(data, start, end) {
55524
+ let crc = 4294967295;
55525
+ for (let i = start; i < end; i++)
55526
+ crc = CRC_TABLE[(crc ^ data.charCodeAt(i)) & 255] ^ crc >>> 8;
55527
+ return (crc ^ 4294967295) >>> 0;
55528
+ }
55529
+ function isPng(data) {
55530
+ if (!data.startsWith("\x89PNG\r\n\n")) return false;
55531
+ let offset = 8;
55532
+ let imageData = false;
55533
+ let dataEnded = false;
55534
+ let palette = false;
55535
+ let indexed = false;
55536
+ while (offset + 12 <= data.length) {
55537
+ const size = u32(data, offset);
55538
+ const type = data.slice(offset + 4, offset + 8);
55539
+ const end = offset + 8 + size;
55540
+ if (size > 2147483647 || end + 4 > data.length || !/^[A-Za-z]{2}[A-Z][A-Za-z]$/.test(type))
55541
+ return false;
55542
+ if (u32(data, end) !== crc32(data, offset + 4, end)) return false;
55543
+ if (offset === 8) {
55544
+ if (type !== "IHDR" || size !== 13 || !u32(data, offset + 8) || !u32(data, offset + 12))
55545
+ return false;
55546
+ const depth = data.charCodeAt(offset + 16);
55547
+ const color = data.charCodeAt(offset + 17);
55548
+ const validDepth = color === 0 ? [1, 2, 4, 8, 16] : color === 3 ? [1, 2, 4, 8] : [8, 16];
55549
+ if (![0, 2, 3, 4, 6].includes(color) || !validDepth.includes(depth)) return false;
55550
+ if (data.charCodeAt(offset + 18) !== 0 || data.charCodeAt(offset + 19) !== 0 || data.charCodeAt(offset + 20) > 1)
55551
+ return false;
55552
+ indexed = color === 3;
55553
+ } else if (type === "IHDR") return false;
55554
+ else if (type === "PLTE") {
55555
+ if (imageData || palette || size === 0 || size > 768 || size % 3 !== 0) return false;
55556
+ palette = true;
55557
+ } else if (type === "IDAT") {
55558
+ if (dataEnded || indexed && !palette) return false;
55559
+ imageData = imageData || size > 0;
55560
+ } else if (type === "IEND") return imageData && size === 0 && end + 4 === data.length;
55561
+ else if (type[0] === type[0].toUpperCase()) return false;
55562
+ if (imageData && type !== "IDAT") dataEnded = true;
55563
+ offset = end + 4;
55564
+ }
55565
+ return false;
55566
+ }
55567
+ function isJpeg(data) {
55568
+ if (!data.startsWith("\xFF\xD8")) return false;
55569
+ let offset = 2;
55570
+ let frame = false;
55571
+ let scan = false;
55572
+ while (offset < data.length) {
55573
+ if (data.charCodeAt(offset++) !== 255) return false;
55574
+ while (data.charCodeAt(offset) === 255) offset++;
55575
+ const marker = data.charCodeAt(offset++);
55576
+ if (marker === 217) return frame && scan && offset === data.length;
55577
+ if (marker === 0 || marker === 216 || marker >= 208 && marker <= 215) return false;
55578
+ if (marker === 1) continue;
55579
+ if (offset + 2 > data.length) return false;
55580
+ const size = u16(data, offset);
55581
+ if (size < 2 || offset + size > data.length) return false;
55582
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
55583
+ if (size < 11 || size !== 8 + 3 * data.charCodeAt(offset + 7) || !u16(data, offset + 3) || !u16(data, offset + 5))
55584
+ return false;
55585
+ frame = true;
55586
+ }
55587
+ if (marker !== 218) {
55588
+ offset += size;
55589
+ continue;
55590
+ }
55591
+ if (!frame || size < 8 || size !== 6 + 2 * data.charCodeAt(offset + 2)) return false;
55592
+ offset += size;
55593
+ const start = offset;
55594
+ for (; ; ) {
55595
+ const next = data.indexOf("\xFF", offset);
55596
+ if (next < 0 || next + 1 >= data.length) return false;
55597
+ const following = data.charCodeAt(next + 1);
55598
+ if (following === 0 || following >= 208 && following <= 215) offset = next + 2;
55599
+ else if (following === 255) offset = next + 1;
55600
+ else {
55601
+ offset = next;
55602
+ break;
55603
+ }
55604
+ }
55605
+ if (offset <= start) return false;
55606
+ scan = true;
55607
+ }
55608
+ return false;
55609
+ }
55610
+ function skipGifBlocks(data, offset) {
55611
+ while (offset < data.length) {
55612
+ const size = data.charCodeAt(offset++);
55613
+ if (size === 0) return offset;
55614
+ offset += size;
55615
+ }
55616
+ return -1;
55617
+ }
55618
+ function isGif(data) {
55619
+ if (!data.startsWith("GIF87a") && !data.startsWith("GIF89a") || data.length < 13) return false;
55620
+ if (!(data.charCodeAt(6) | data.charCodeAt(7)) || !(data.charCodeAt(8) | data.charCodeAt(9)))
55621
+ return false;
55622
+ const packed = data.charCodeAt(10);
55623
+ let offset = 13 + (packed & 128 ? 3 * (2 << (packed & 7)) : 0);
55624
+ let image = false;
55625
+ while (offset < data.length) {
55626
+ const marker = data.charCodeAt(offset++);
55627
+ if (marker === 59) return image && offset === data.length;
55628
+ if (marker === 33) offset = skipGifBlocks(data, offset + 1);
55629
+ else if (marker === 44) {
55630
+ if (offset + 9 > data.length || !(data.charCodeAt(offset + 4) | data.charCodeAt(offset + 5)) || !(data.charCodeAt(offset + 6) | data.charCodeAt(offset + 7)))
55631
+ return false;
55632
+ const local = data.charCodeAt(offset + 8);
55633
+ offset += 9 + (local & 128 ? 3 * (2 << (local & 7)) : 0);
55634
+ const codeSize = data.charCodeAt(offset++);
55635
+ if (codeSize < 2 || codeSize > 8 || offset >= data.length || data.charCodeAt(offset) === 0)
55636
+ return false;
55637
+ offset = skipGifBlocks(data, offset);
55638
+ image = true;
55639
+ } else return false;
55640
+ if (offset < 0) return false;
55641
+ }
55642
+ return false;
55643
+ }
55644
+ function isWebp(data) {
55645
+ return data.startsWith("RIFF") && data.slice(8, 12) === "WEBP" && little32(data, 4) + 8 === data.length && webpChunks(data, 12, data.length, true);
55646
+ }
55647
+ function webpChunks(data, start, end, allowFrames) {
55648
+ let image = false;
55649
+ let offset = start;
55650
+ while (offset + 8 <= end) {
55651
+ const type = data.slice(offset, offset + 4);
55652
+ const size = little32(data, offset + 4);
55653
+ const body = offset + 8;
55654
+ const next = body + size + size % 2;
55655
+ if (next > end) return false;
55656
+ if (type === "VP8 ") {
55657
+ if (size < 11 || data.charCodeAt(body) & 1 || data.slice(body + 3, body + 6) !== "\x9D*")
55658
+ return false;
55659
+ if (!((data.charCodeAt(body + 6) | data.charCodeAt(body + 7) << 8) & 16383) || !((data.charCodeAt(body + 8) | data.charCodeAt(body + 9) << 8) & 16383))
55660
+ return false;
55661
+ image = true;
55662
+ } else if (type === "VP8L") {
55663
+ if (size < 6 || data.charCodeAt(body) !== 47 || data.charCodeAt(body + 4) & 224)
55664
+ return false;
55665
+ image = true;
55666
+ } else if (type === "VP8X") {
55667
+ if (size !== 10 || !allowFrames) return false;
55668
+ } else if (type === "ANMF") {
55669
+ if (!allowFrames || size < 16 || !webpChunks(data, body + 16, body + size, false))
55670
+ return false;
55671
+ image = true;
55672
+ }
55673
+ offset = next;
55674
+ }
55675
+ return image && offset === end;
55676
+ }
55677
+
55488
55678
  // ../core/src/transcriptImages.ts
55489
55679
  var IMAGE_DATA_URI_RE = /^data:(image\/[\w.+-]+);base64,([A-Za-z0-9+/=_-]*)$/;
55490
55680
  function isImageDataUri(value) {
55491
- return value.startsWith("data:image/") && IMAGE_DATA_URI_RE.test(value);
55681
+ if (!value.startsWith("data:image/")) return false;
55682
+ const match = IMAGE_DATA_URI_RE.exec(value);
55683
+ return match !== null && isEncodedImage(match[2], match[1]);
55492
55684
  }
55493
55685
  function isImageBase64Payload(record2, key) {
55494
- if (typeof record2[key] !== "string") return false;
55495
- if (key === "data") return record2.type === "base64" && isImageMediaType(record2.media_type);
55496
- if (key === "base64") return isImageMediaType(record2.type);
55686
+ const data = record2[key];
55687
+ if (typeof data !== "string") return false;
55688
+ if (key === "data" && record2.type === "base64") return isEncodedImage(data, record2.media_type);
55689
+ if (key === "base64") return isEncodedImage(data, record2.type);
55497
55690
  return false;
55498
55691
  }
55499
- function isImageMediaType(value) {
55500
- return typeof value === "string" && value.startsWith("image/");
55501
- }
55502
55692
  function describeImageBlock(block2) {
55503
55693
  if (block2.type !== "image") return null;
55504
55694
  const source = asRecord(block2.source);
@@ -56388,6 +56578,38 @@ function scrubBreadcrumb(breadcrumb) {
56388
56578
  return breadcrumb;
56389
56579
  }
56390
56580
 
56581
+ // ../core/src/freshness.ts
56582
+ function describeDataAge(generatedAt, now) {
56583
+ const ageMs = now.getTime() - generatedAt.getTime();
56584
+ if (ageMs < 0) return "just now";
56585
+ const minutes = Math.floor(ageMs / 6e4);
56586
+ if (minutes < 1) return "just now";
56587
+ if (minutes < 60) return `${minutes}m ago`;
56588
+ const hours = Math.floor(minutes / 60);
56589
+ if (hours < 24) return `${hours}h ago`;
56590
+ const days = Math.floor(hours / 24);
56591
+ return `${days}d ago`;
56592
+ }
56593
+
56594
+ // ../core/src/onboarding.ts
56595
+ var SAME_HOUR_MS = 60 * 60 * 1e3;
56596
+ function describeSyncSignals(member, now, noUpload = "no sessions uploaded yet") {
56597
+ const session = member.lastSessionActivityAt;
56598
+ const poll = member.lastCheckedInAt;
56599
+ const error62 = member.lastSyncErrorAt;
56600
+ const paused = member.syncPaused === true;
56601
+ if (session === null && poll === null && error62 === null) return null;
56602
+ const age = (at) => describeDataAge(new Date(at), now);
56603
+ const apart = (a, b2) => b2 === null || Math.abs(Date.parse(a) - Date.parse(b2)) > SAME_HOUR_MS;
56604
+ const parts = [session === null ? noUpload : `last uploaded session ${age(session)}`];
56605
+ if (error62 !== null) parts.push(`reported sync error ${age(error62)}`);
56606
+ if (paused) parts.push("syncing paused");
56607
+ const pollSays = error62 !== null ? apart(poll ?? error62, error62) : paused || apart(poll ?? "", session);
56608
+ if (poll !== null && pollSays) parts.push(`last poll ${age(poll)}`);
56609
+ const line = parts.join(", ");
56610
+ return `${line.charAt(0).toUpperCase()}${line.slice(1)}.`;
56611
+ }
56612
+
56391
56613
  // ../core/src/sessionIngest.ts
56392
56614
  var SESSION_SOURCES = [
56393
56615
  "claude-code",
@@ -83335,9 +83557,20 @@ var storedCredentialsSchema = external_exports.object({
83335
83557
  * (`powerplant folders add`). */
83336
83558
  scopePaths: external_exports.array(external_exports.string()).optional(),
83337
83559
  /** macOS privacy roots the member explicitly allowed Powerplant to inspect
83338
- * for Git metadata. This does not widen capture: org policy still decides
83339
- * which discovered repositories upload. */
83560
+ * for Git metadata, and that macOS let it read. This does not widen
83561
+ * capture: org policy still decides which discovered repositories upload. */
83340
83562
  repoDiscoveryRoots: external_exports.array(external_exports.string()).optional(),
83563
+ /** When the member answered yes to Git metadata scans of macOS-protected
83564
+ * session folders (ISO 8601). One answer for this enrollment: later init
83565
+ * runs scan a newly seen root after an explanation, without asking again.
83566
+ * Absent = never asked, or never answered yes. */
83567
+ repoDiscoveryConsentedAt: external_exports.string().optional(),
83568
+ /** Protected roots the member allowed but macOS refused to let Powerplant
83569
+ * read (a declined Files and Folders dialog). macOS remembers that refusal
83570
+ * and shows no second dialog, so init re-reads these roots on every run
83571
+ * without a question and moves a root to repoDiscoveryRoots once the
83572
+ * member enables it in System Settings. */
83573
+ repoDiscoveryBlockedRoots: external_exports.array(external_exports.string()).optional(),
83341
83574
  /** When this enrollment's deep first sync completed; absent = still owed,
83342
83575
  * so the next sync sweeps DEEP_FIRST_SYNC_DAYS instead of the rolling window. */
83343
83576
  deepSyncedAt: external_exports.string().optional()
@@ -83423,7 +83656,7 @@ import os6 from "node:os";
83423
83656
  // ../../apps/collector/src/http.ts
83424
83657
  var import_undici = __toESM(require_undici(), 1);
83425
83658
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
83426
- var cliVersion = true ? "0.4.7" : "0.0.0-checkout";
83659
+ var cliVersion = true ? "0.4.8" : "0.0.0-checkout";
83427
83660
  function cliVersionHeaders() {
83428
83661
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
83429
83662
  }
@@ -83608,8 +83841,8 @@ function unconsentedProtectedRoot(candidate, environment = {}) {
83608
83841
  );
83609
83842
  return consented ? null : protectedRoot;
83610
83843
  }
83611
- var DENIED = { kind: "denied" };
83612
83844
  var NONE = { kind: "none" };
83845
+ var deniedAt = (p) => ({ kind: "denied", path: p });
83613
83846
  function consentGate(candidate, environment) {
83614
83847
  const root = unconsentedProtectedRoot(candidate, environment);
83615
83848
  return root === null ? null : { kind: "unconsented", root, path: path2.resolve(candidate) };
@@ -83618,7 +83851,7 @@ function readProbe(p) {
83618
83851
  try {
83619
83852
  return { kind: "found", value: readFileSync3(p, "utf8") };
83620
83853
  } catch (cause) {
83621
- return isPermissionError(cause) ? DENIED : NONE;
83854
+ return isPermissionError(cause) ? deniedAt(p) : NONE;
83622
83855
  }
83623
83856
  }
83624
83857
  function gitDirReferenceProbe(cwd, environment) {
@@ -83628,7 +83861,7 @@ function gitDirReferenceProbe(cwd, environment) {
83628
83861
  for (; ; ) {
83629
83862
  const gitPath = path2.join(dir, ".git");
83630
83863
  const kind = statKind(gitPath);
83631
- if (kind === "denied") return DENIED;
83864
+ if (kind === "denied") return deniedAt(gitPath);
83632
83865
  if (kind === "dir") return { kind: "found", value: gitPath };
83633
83866
  if (kind === "file") {
83634
83867
  const pointer = readProbe(gitPath);
@@ -83652,7 +83885,7 @@ function gitDirsProbe(cwd, environment) {
83652
83885
  const gitDirGate = consentGate(gitDir, environment);
83653
83886
  if (gitDirGate) return gitDirGate;
83654
83887
  const commondir = readProbe(path2.join(gitDir, "commondir"));
83655
- if (commondir.kind === "denied") return DENIED;
83888
+ if (commondir.kind === "denied") return commondir;
83656
83889
  const pointer = commondir.kind === "found" ? commondir.value.trim() : "";
83657
83890
  const commonDir = pointer ? path2.resolve(gitDir, pointer) : gitDir;
83658
83891
  const commonDirGate = consentGate(commonDir, environment);
@@ -83887,10 +84120,15 @@ function tokenRejected(route) {
83887
84120
  `${route} rejected this machine's token (revoked or unknown). Re-enroll this machine (powerplant enroll, or pnpm collector enroll from the checkout).`
83888
84121
  );
83889
84122
  }
83890
- async function fetchCapturePolicy(apiUrl, machineToken) {
84123
+ function capturePolicyUrl(apiUrl, request2) {
84124
+ const url2 = new URL(`${apiUrl}/api/collector/capture-policy`);
84125
+ if (request2.paused !== void 0) url2.searchParams.set("paused", String(request2.paused));
84126
+ return url2.toString();
84127
+ }
84128
+ async function fetchCapturePolicy(apiUrl, machineToken, request2 = {}) {
83891
84129
  const response = await fetchWithRetry(
83892
84130
  "fetching the org's capture policy",
83893
- `${apiUrl}/api/collector/capture-policy`,
84131
+ capturePolicyUrl(apiUrl, request2),
83894
84132
  { headers: { authorization: `Bearer ${machineToken}` } }
83895
84133
  );
83896
84134
  if (response.status === 401) {
@@ -83921,6 +84159,19 @@ async function fetchCapturePolicy(apiUrl, machineToken) {
83921
84159
  );
83922
84160
  return result.data;
83923
84161
  }
84162
+ var CHECK_IN_INTERVAL_MS = 30 * 6e4;
84163
+ async function reportCheckIn(apiUrl, machineToken, paused, fetcher = fetch) {
84164
+ try {
84165
+ const response = await fetcher(capturePolicyUrl(apiUrl, { paused }), {
84166
+ headers: { ...cliVersionHeaders(), authorization: `Bearer ${machineToken}` },
84167
+ signal: AbortSignal.timeout(3e3)
84168
+ });
84169
+ await response.body?.cancel();
84170
+ return response.ok;
84171
+ } catch {
84172
+ return false;
84173
+ }
84174
+ }
83924
84175
  async function uploadSessions(apiUrl, machineToken, items) {
83925
84176
  let upserted = 0;
83926
84177
  let unchanged = 0;
@@ -84461,7 +84712,6 @@ function removeFolder(config2, rawPath, enrollCommand, log3, deps = defaultDeps)
84461
84712
 
84462
84713
  // ../../apps/collector/src/diagnostics.ts
84463
84714
  import { createHmac } from "node:crypto";
84464
- var DIAGNOSTIC_INTERVAL_MS = 30 * 6e4;
84465
84715
  function observeFileDiagnostic(observer, file2) {
84466
84716
  try {
84467
84717
  observer?.(file2);
@@ -84579,8 +84829,7 @@ function createDiagnosticsReporter(credentials, send = createDiagnosticSender())
84579
84829
  let pending = Promise.resolve();
84580
84830
  let enrollment = null;
84581
84831
  let lastIssue = null;
84582
- let paused = false;
84583
- const report2 = (detailed) => {
84832
+ const report2 = () => {
84584
84833
  try {
84585
84834
  const current = credentials();
84586
84835
  if (!current) return;
@@ -84589,29 +84838,20 @@ function createDiagnosticsReporter(credentials, send = createDiagnosticSender())
84589
84838
  lastIssue = null;
84590
84839
  }
84591
84840
  const matching = pass !== null && passEnrollments.get(pass) === current.token ? pass : null;
84592
- if (detailed) {
84593
- if (matching === null) return;
84594
- const issue2 = issueSignature(matching);
84595
- if (issue2 === lastIssue) return;
84596
- lastIssue = issue2;
84597
- }
84598
- const body = collectorDiagnosticSchema.parse({
84599
- state: matching?.outcome === "running" ? "syncing" : paused ? "paused" : "idle",
84600
- pass: detailed ? matching : null
84601
- });
84841
+ if (matching === null) return;
84842
+ const issue2 = issueSignature(matching);
84843
+ if (issue2 === lastIssue) return;
84844
+ lastIssue = issue2;
84845
+ const body = collectorDiagnosticSchema.parse({ state: "idle", pass: matching });
84602
84846
  pending = send(current, body).catch(() => void 0);
84603
84847
  } catch {
84604
84848
  }
84605
84849
  };
84606
84850
  return {
84607
- heartbeat: (isPaused = false) => {
84608
- paused = isPaused;
84609
- report2(false);
84610
- },
84611
84851
  flush: () => pending,
84612
84852
  onDiagnostics: (diagnostic) => {
84613
84853
  pass = diagnostic;
84614
- if (diagnostic.outcome !== "running") report2(true);
84854
+ if (diagnostic.outcome !== "running") report2();
84615
84855
  }
84616
84856
  };
84617
84857
  }
@@ -86296,25 +86536,33 @@ function groupProtectedSessions(sessions, environment = {}) {
86296
86536
  if (!session.cwd) continue;
86297
86537
  const cwd = path10.resolve(session.cwd);
86298
86538
  const directRoot = macPrivacyProtectedRoot(cwd, environment);
86299
- const gitDir = directRoot === null ? resolveGitDirReference(cwd, environment) : null;
86300
- const root = directRoot ?? (gitDir ? macPrivacyProtectedRoot(gitDir, environment) : null);
86301
- if (root === null) continue;
86302
- const folder = directRoot !== null || gitDir === null ? cwd : checkoutOfGitDir(gitDir);
86303
- tally.add(root, folder, cwd);
86539
+ if (directRoot !== null) tally.add(directRoot, cwd, cwd);
86540
+ const gitDir = resolveGitDirReference(cwd, environment);
86541
+ const checkoutRoot = gitDir ? macPrivacyProtectedRoot(gitDir, environment) : null;
86542
+ if (checkoutRoot !== null && checkoutRoot !== directRoot) {
86543
+ tally.add(checkoutRoot, checkoutOfGitDir(gitDir), cwd);
86544
+ }
86304
86545
  }
86305
86546
  return tally.entries();
86306
86547
  }
86307
86548
  var defaultAllowDeps = {
86308
86549
  load: loadCredentials,
86309
86550
  persist: saveCredentials,
86310
- probeRepo: repoProber
86551
+ probeRepo: repoProber,
86552
+ now: Date.now
86311
86553
  };
86554
+ function protectedRootReview(credentials, root) {
86555
+ const resolved = path10.resolve(root);
86556
+ if ((credentials.repoDiscoveryRoots ?? []).includes(resolved)) return "allowed";
86557
+ if ((credentials.repoDiscoveryBlockedRoots ?? []).includes(resolved)) return "blocked";
86558
+ return "unreviewed";
86559
+ }
86312
86560
  function allowProtectedRepoDiscovery(config2, found, environment = {}, deps = defaultAllowDeps) {
86313
86561
  const enrollment = deps.load(config2.appEnv);
86314
86562
  if (enrollment === null) {
86315
86563
  throw new ExpectedCliError("This machine must be enrolled before protected folders can be reviewed.");
86316
86564
  }
86317
- const candidateRoots = found.map((entry) => path10.resolve(entry.root));
86565
+ const candidateRoots = rootsOf(found);
86318
86566
  const invalid = candidateRoots.find(
86319
86567
  (root) => macPrivacyProtectedRoot(root, environment) !== root
86320
86568
  );
@@ -86335,49 +86583,88 @@ function allowProtectedRepoDiscovery(config2, found, environment = {}, deps = de
86335
86583
  needsReview: []
86336
86584
  };
86337
86585
  const needsReview = new ProtectedRootTally(environment);
86586
+ const refusedElsewhere = new ProtectedRootTally(environment);
86338
86587
  for (const entry of found) {
86588
+ const root = path10.resolve(entry.root);
86339
86589
  const probes = entry.folders.flatMap(
86340
86590
  (folder) => folder.cwds.map((cwd) => ({ cwd, probe: probeOf(cwd), folder }))
86341
86591
  );
86592
+ let deniedHere = false;
86342
86593
  let unconsented = 0;
86343
86594
  const countedFolderTargets = /* @__PURE__ */ new Set();
86595
+ const countOnce = (target, sessionCount) => {
86596
+ const counted = countedFolderTargets.has(target);
86597
+ countedFolderTargets.add(target);
86598
+ return counted ? 0 : sessionCount;
86599
+ };
86344
86600
  for (const { cwd, probe, folder } of probes) {
86601
+ if (probe.kind === "denied") {
86602
+ const refusedRoot = macPrivacyProtectedRoot(probe.path, environment);
86603
+ if (refusedRoot === root) deniedHere = true;
86604
+ else if (refusedRoot !== null) {
86605
+ const checkout2 = checkoutOfGitDir(probe.path);
86606
+ const target2 = [folder.path, refusedRoot, checkout2].join("\0");
86607
+ refusedElsewhere.add(refusedRoot, checkout2, cwd, countOnce(target2, folder.sessionCount));
86608
+ }
86609
+ continue;
86610
+ }
86345
86611
  if (probe.kind !== "unconsented") continue;
86346
86612
  unconsented += 1;
86347
86613
  const checkout = checkoutOfGitDir(probe.path);
86348
86614
  const target = [folder.path, probe.root, checkout].join("\0");
86349
- needsReview.add(
86350
- probe.root,
86351
- checkout,
86352
- cwd,
86353
- countedFolderTargets.has(target) ? 0 : folder.sessionCount
86354
- );
86355
- countedFolderTargets.add(target);
86615
+ needsReview.add(probe.root, checkout, cwd, countOnce(target, folder.sessionCount));
86356
86616
  }
86357
86617
  if (probes.some(({ probe }) => probe.kind === "found")) outcome.enabled.push(entry);
86358
- else if (probes.some(({ probe }) => probe.kind === "denied")) outcome.blocked.push(entry);
86618
+ else if (deniedHere) outcome.blocked.push(entry);
86359
86619
  else if (unconsented > 0) outcome.unreviewed.push(entry);
86360
86620
  else outcome.unmatched.push(entry);
86361
86621
  }
86622
+ const blockedRoots = new Set(rootsOf(outcome.blocked));
86623
+ outcome.blocked.push(
86624
+ ...refusedElsewhere.entries().filter((entry) => !blockedRoots.has(entry.root))
86625
+ );
86362
86626
  outcome.needsReview = needsReview.entries();
86363
- const consented = [...outcome.enabled, ...outcome.unmatched];
86364
- if (consented.length === 0) return outcome;
86365
86627
  const credentials = deps.load(config2.appEnv);
86366
86628
  if (credentials === null || credentials.token !== enrollment.token || credentials.enrolledAt !== enrollment.enrolledAt) {
86367
86629
  throw new ExpectedCliError(
86368
- "This machine's enrollment changed while the folders were being reviewed, so the approval was not saved, run `powerplant init` again to review them."
86630
+ "This machine's enrollment changed while the folders were being reviewed, so nothing was saved, run `powerplant init` again to review them."
86369
86631
  );
86370
86632
  }
86371
- const allowedProtectedRoots = [
86633
+ const next = credentialsAfterRepoDiscovery(credentials, outcome, deps.now);
86634
+ if (next !== null) deps.persist(config2.appEnv, next);
86635
+ return outcome;
86636
+ }
86637
+ function rootsOf(entries) {
86638
+ return entries.map((entry) => path10.resolve(entry.root));
86639
+ }
86640
+ function credentialsAfterRepoDiscovery(credentials, outcome, now) {
86641
+ const readable = rootsOf([...outcome.enabled, ...outcome.unmatched]);
86642
+ const denied = rootsOf(outcome.blocked).filter((root) => !readable.includes(root));
86643
+ const allowedRoots = [
86372
86644
  .../* @__PURE__ */ new Set([
86373
- ...credentials.repoDiscoveryRoots ?? [],
86374
- ...consented.map((entry) => path10.resolve(entry.root))
86645
+ ...(credentials.repoDiscoveryRoots ?? []).filter((root) => !denied.includes(root)),
86646
+ ...readable
86375
86647
  ])
86376
86648
  ];
86377
- if (allowedProtectedRoots.length !== (credentials.repoDiscoveryRoots ?? []).length) {
86378
- deps.persist(config2.appEnv, { ...credentials, repoDiscoveryRoots: allowedProtectedRoots });
86379
- }
86380
- return outcome;
86649
+ const blockedRoots = [
86650
+ .../* @__PURE__ */ new Set([
86651
+ ...(credentials.repoDiscoveryBlockedRoots ?? []).filter((root) => !readable.includes(root)),
86652
+ ...denied
86653
+ ])
86654
+ ];
86655
+ const unchanged = credentials.repoDiscoveryConsentedAt !== void 0 && sameList(allowedRoots, credentials.repoDiscoveryRoots ?? []) && sameList(blockedRoots, credentials.repoDiscoveryBlockedRoots ?? []);
86656
+ if (unchanged) return null;
86657
+ return {
86658
+ ...credentials,
86659
+ repoDiscoveryConsentedAt: credentials.repoDiscoveryConsentedAt ?? new Date(now()).toISOString(),
86660
+ // Both lists clear on empty: keeping a stale allowed list would read a
86661
+ // root as consent after macOS refused it.
86662
+ repoDiscoveryRoots: allowedRoots.length > 0 ? allowedRoots : void 0,
86663
+ repoDiscoveryBlockedRoots: blockedRoots.length > 0 ? blockedRoots : void 0
86664
+ };
86665
+ }
86666
+ function sameList(a, b2) {
86667
+ return a.length === b2.length && a.every((root, index) => root === b2[index]);
86381
86668
  }
86382
86669
 
86383
86670
  // ../../apps/collector/src/hookInstall.ts
@@ -86709,6 +86996,9 @@ var collectorStatusSchema = external_exports.object({
86709
86996
  /** Last pass that completed without error — everything eligible had been
86710
86997
  * uploaded as of this time. */
86711
86998
  lastSyncAt: external_exports.string().optional(),
86999
+ /** Last pass that upserted at least one session; missing = no session
87000
+ * has uploaded in this daemon's lifetime, not "never". */
87001
+ lastUploadAt: external_exports.string().optional(),
86712
87002
  consecutiveFailures: external_exports.number().int().nonnegative()
86713
87003
  });
86714
87004
  var controlSchema = external_exports.object({
@@ -86725,7 +87015,8 @@ var controlSchema = external_exports.object({
86725
87015
  function interpretControlChange(previous, current) {
86726
87016
  const transitioned = previous.paused !== current.paused;
86727
87017
  const nudge = !current.paused && (transitioned || current.resumedAt !== previous.resumedAt);
86728
- return { transitioned, nudge };
87018
+ const updateOptIn = current.autoUpdate === true && previous.autoUpdate !== true;
87019
+ return { transitioned, nudge, updateOptIn };
86729
87020
  }
86730
87021
  function collectorStateDir(homeDir = os14.homedir()) {
86731
87022
  return path13.join(homeDir, ".config", "powerplant", "state");
@@ -86748,6 +87039,10 @@ function writeAtomically(file2, payload) {
86748
87039
  function writeCollectorStatus(appEnv, status, homeDir) {
86749
87040
  writeAtomically(statusFilePath(appEnv, homeDir), JSON.stringify(status, null, 2));
86750
87041
  }
87042
+ function carriedUploadClock(previous, machineId) {
87043
+ if (previous.file !== "ok" || machineId === void 0) return void 0;
87044
+ return previous.status.machineId === machineId ? previous.status.lastUploadAt : void 0;
87045
+ }
86751
87046
  function readCollectorStatus(appEnv, homeDir) {
86752
87047
  let raw;
86753
87048
  try {
@@ -87856,6 +88151,10 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87856
88151
  const presentRoots = storeRoots.filter(({ dir }) => existsSync6(dir)).map(({ dir }) => dir);
87857
88152
  const initialControl = readCollectorControl(config2.appEnv, home);
87858
88153
  const pausedAtMs = initialControl.paused ? Date.parse(initialControl.pausedAt ?? "") : Number.NaN;
88154
+ const lastUploadAt = carriedUploadClock(
88155
+ readCollectorStatus(config2.appEnv, home),
88156
+ credentials?.machineId
88157
+ );
87859
88158
  let status = {
87860
88159
  pid: process.pid,
87861
88160
  version: runtime.version,
@@ -87871,6 +88170,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87871
88170
  },
87872
88171
  watchedRoots: presentRoots,
87873
88172
  paused: initialControl.paused,
88173
+ ...lastUploadAt !== void 0 && { lastUploadAt },
87874
88174
  consecutiveFailures: 0
87875
88175
  };
87876
88176
  let statusWriteFailed = false;
@@ -87889,9 +88189,19 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87889
88189
  };
87890
88190
  writeStatus({});
87891
88191
  const diagnostics = createDiagnosticsReporter(() => loadCredentials(config2.appEnv));
87892
- diagnostics.heartbeat(initialControl.paused);
87893
- const diagnosticHeartbeat = setInterval(() => diagnostics.heartbeat(status.paused), DIAGNOSTIC_INTERVAL_MS);
87894
- diagnosticHeartbeat.unref?.();
88192
+ const checkIn = () => {
88193
+ let current;
88194
+ try {
88195
+ current = loadCredentials(config2.appEnv);
88196
+ } catch {
88197
+ return;
88198
+ }
88199
+ if (current === null) return;
88200
+ void reportCheckIn(current.apiUrl, current.token, status.paused);
88201
+ };
88202
+ checkIn();
88203
+ const checkInTimer = setInterval(checkIn, CHECK_IN_INTERVAL_MS);
88204
+ checkInTimer.unref?.();
87895
88205
  const heartbeat = setInterval(() => writeStatus({}), HEARTBEAT_INTERVAL_MS);
87896
88206
  heartbeat.unref?.();
87897
88207
  const noticeIfUnsupported = async () => {
@@ -87989,6 +88299,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87989
88299
  ...result.diagnostic && { diagnostic: result.diagnostic }
87990
88300
  },
87991
88301
  ...result.ok && { lastSyncAt: new Date(result.at).toISOString() },
88302
+ ...result.ok && result.upserted > 0 && { lastUploadAt: new Date(result.at).toISOString() },
87992
88303
  consecutiveFailures: result.ok ? 0 : status.consecutiveFailures + 1
87993
88304
  }),
87994
88305
  reportError: reportCollectorError,
@@ -88020,16 +88331,21 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
88020
88331
  let lastControl = initialControl;
88021
88332
  const controlPoll = setInterval(() => {
88022
88333
  const control = readCollectorControl(config2.appEnv, home);
88023
- const { transitioned, nudge } = interpretControlChange(lastControl, control);
88334
+ const { transitioned, nudge, updateOptIn } = interpretControlChange(lastControl, control);
88024
88335
  lastControl = control;
88025
88336
  if (transitioned) {
88026
88337
  log3(control.paused ? "syncing paused via control file" : "syncing resumed");
88027
88338
  writeStatus({ paused: control.paused });
88339
+ checkIn();
88028
88340
  }
88029
88341
  if (nudge) {
88030
88342
  log3("running a catch-up pass to cover the pause");
88031
88343
  loop.nudge();
88032
88344
  }
88345
+ if (updateOptIn) {
88346
+ log3("automatic updates turned on, checking for a release now");
88347
+ void checkForUpdate();
88348
+ }
88033
88349
  }, CONTROL_POLL_MS);
88034
88350
  controlPoll.unref?.();
88035
88351
  if (runtime.entryPath) {
@@ -89852,7 +90168,7 @@ import { accessSync, constants as constants2 } from "node:fs";
89852
90168
  import path23 from "node:path";
89853
90169
 
89854
90170
  // src/version.ts
89855
- var CLI_VERSION = true ? "0.4.7" : "0.0.0-checkout";
90171
+ var CLI_VERSION = true ? "0.4.8" : "0.0.0-checkout";
89856
90172
  var CLI_RELEASE_BUILD = true ? true : false;
89857
90173
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
89858
90174
 
@@ -90424,6 +90740,9 @@ async function confirmResume(deps, log3, indent) {
90424
90740
  }
90425
90741
  }
90426
90742
 
90743
+ // src/protectedFolders.ts
90744
+ var MACOS_BLOCKED_FOLDER_REMEDY = "To allow it, open System Settings > Privacy & Security > Files and Folders, enable the folder for node, then run `powerplant init`.";
90745
+
90427
90746
  // src/init.ts
90428
90747
  function syncNote(credentials) {
90429
90748
  const org = credentials.orgName ?? "your organization";
@@ -90468,7 +90787,7 @@ async function runInit(deps) {
90468
90787
  }
90469
90788
  log3("");
90470
90789
  const orgName = credentials.orgName;
90471
- let repoDiscoveryEnabled = false;
90790
+ let repoDiscoveryDidWork = false;
90472
90791
  let protectedFoldersNeedAttention = false;
90473
90792
  let installed = false;
90474
90793
  let menubarDidWork = false;
@@ -90481,56 +90800,68 @@ async function runInit(deps) {
90481
90800
  "Checking recent session locations\u2026",
90482
90801
  deps.findProtectedSessions
90483
90802
  );
90484
- const alreadyAllowed = new Set(credentials.repoDiscoveryRoots ?? []);
90485
- const needsReview = protectedSessions.filter((entry) => !alreadyAllowed.has(entry.root));
90486
- if (needsReview.length > 0) {
90487
- protectedFoldersNeedAttention = true;
90488
- log3(" Coding agents recorded recent sessions in folders macOS is currently hiding from Powerplant:");
90489
- logProtectedFolders(log3, needsReview);
90490
- log3(" Powerplant will scan only Git metadata in those recorded session folders.");
90491
- if (deps.promptAllowed) {
90492
- const allowed = await confirmProtectedRepoDiscovery(deps);
90493
- if (allowed) {
90494
- log3(
90495
- ' macOS may label Powerplant as "node" because Powerplant runs on Node.js.'
90496
- );
90803
+ const reviewOf = (entry) => protectedRootReview(credentials, entry.root);
90804
+ const blocked = protectedSessions.filter((entry) => reviewOf(entry) === "blocked");
90805
+ const fresh = protectedSessions.filter((entry) => reviewOf(entry) === "unreviewed");
90806
+ if (blocked.length > 0 || fresh.length > 0) {
90807
+ const org = orgName ?? "your organization";
90808
+ const consented = credentials.repoDiscoveryConsentedAt !== void 0;
90809
+ const toProbe = [];
90810
+ let needsAttention = false;
90811
+ if (fresh.length > 0) {
90812
+ log3(" Coding agents recorded recent sessions in folders macOS is currently hiding from Powerplant:");
90813
+ logProtectedFolders(log3, fresh);
90814
+ if (!deps.promptAllowed) {
90815
+ needsAttention = true;
90816
+ logProtectedSessionsStayLocal(log3, fresh);
90817
+ log3(` Run \`powerplant init\` in a terminal to ${consented ? "scan" : "review"} them.`);
90818
+ } else {
90497
90819
  log3(
90498
- ` If permission is added, Powerplant will only read Git metadata in these folders to check for ${orgName ?? "your organization's"} sessions.`
90820
+ consented ? ` You already allowed Git metadata scans. Checking those folders for ${org}'s work repositories.` : ` If you allow it, Powerplant reads only Git metadata there, to find ${org}'s work repositories.`
90499
90821
  );
90500
- const result = await deps.activity(
90822
+ if (consented || await confirmProtectedRepoDiscovery(deps)) toProbe.push(...fresh);
90823
+ else {
90824
+ needsAttention = true;
90825
+ logProtectedSessionsStayLocal(log3, fresh);
90826
+ }
90827
+ }
90828
+ }
90829
+ if (blocked.length > 0) {
90830
+ if (deps.promptAllowed) {
90831
+ log3(` Checking whether macOS still blocks ${displayRoots(blocked)}.`);
90832
+ toProbe.push(...blocked);
90833
+ } else {
90834
+ needsAttention = true;
90835
+ logBlockedByMacOS(log3, blocked);
90836
+ }
90837
+ }
90838
+ if (toProbe.length > 0) {
90839
+ log3(' macOS may label Powerplant as "node" because Powerplant runs on Node.js.');
90840
+ const probe = async (roots) => {
90841
+ const result2 = await deps.activity(
90501
90842
  "Scanning recorded session folders for Git repositories\u2026",
90502
- () => deps.allowProtectedRepoDiscovery(needsReview)
90843
+ () => deps.allowProtectedRepoDiscovery(roots)
90503
90844
  );
90504
- let needsAttention = reportProtectedRepoDiscovery(log3, result);
90505
- if (result.enabled.length > 0) repoDiscoveryEnabled = true;
90506
- if (result.needsReview.length > 0) {
90507
- log3(" Some of those sessions use Git checkouts in other folders macOS is hiding from Powerplant:");
90508
- logProtectedFolders(log3, result.needsReview);
90509
- const again = await confirmProtectedRepoDiscovery(deps);
90510
- if (again) {
90511
- const second = await deps.activity(
90512
- "Scanning recorded session folders for Git repositories\u2026",
90513
- () => deps.allowProtectedRepoDiscovery([...result.unreviewed, ...result.needsReview])
90514
- );
90515
- needsAttention = reportProtectedRepoDiscovery(log3, second) || needsAttention;
90516
- if (second.enabled.length > 0) repoDiscoveryEnabled = true;
90517
- if (second.unreviewed.length > 0) {
90518
- logProtectedSessionsStayLocal(log3, second.unreviewed);
90519
- needsAttention = true;
90520
- }
90521
- } else {
90522
- logProtectedSessionsStayLocal(log3, result.needsReview);
90523
- needsAttention = true;
90524
- }
90845
+ needsAttention = reportProtectedRepoDiscovery(log3, result2) || needsAttention;
90846
+ const unblocked = result2.unmatched.filter((entry) => reviewOf(entry) === "blocked");
90847
+ if (unblocked.length > 0) {
90848
+ log3(` \u2713 macOS now lets Powerplant read ${displayRoots(unblocked)}. No work repositories were found there.`);
90849
+ }
90850
+ if (result2.enabled.length > 0 || unblocked.length > 0) repoDiscoveryDidWork = true;
90851
+ return result2;
90852
+ };
90853
+ const result = await probe(toProbe);
90854
+ if (result.needsReview.length > 0) {
90855
+ log3(" Some of those sessions use Git checkouts in other folders macOS is hiding from Powerplant:");
90856
+ logProtectedFolders(log3, result.needsReview);
90857
+ const second = await probe([...result.unreviewed, ...result.needsReview]);
90858
+ if (second.unreviewed.length > 0) {
90859
+ logProtectedSessionsStayLocal(log3, second.unreviewed);
90860
+ needsAttention = true;
90525
90861
  }
90526
- protectedFoldersNeedAttention = needsAttention;
90527
- } else {
90528
- logProtectedSessionsStayLocal(log3, needsReview);
90529
90862
  }
90530
- } else {
90531
- logProtectedSessionsStayLocal(log3, needsReview);
90532
- log3(" Run `powerplant init` in a terminal to review them.");
90533
90863
  }
90864
+ protectedFoldersNeedAttention = needsAttention;
90534
90865
  }
90535
90866
  }
90536
90867
  if (deps.platform !== "darwin") {
@@ -90570,7 +90901,7 @@ async function runInit(deps) {
90570
90901
  log3("");
90571
90902
  const connectDidWork = outcome !== null && outcome.connected + outcome.configured > outcome.unchanged;
90572
90903
  const connectSettled = outcome !== null && outcome.found > 0 && outcome.pending === 0 && outcome.failed === 0 && outcome.skipped === 0 && outcome.unchanged === outcome.found && !connectDidWork;
90573
- if (!enrolled && !installed && !menubarDidWork && !repoDiscoveryEnabled && !protectedFoldersNeedAttention && pauseOffer === "not-paused" && connectSettled) {
90904
+ if (!enrolled && !installed && !menubarDidWork && !repoDiscoveryDidWork && !protectedFoldersNeedAttention && pauseOffer === "not-paused" && connectSettled) {
90574
90905
  log3("Everything is already set up.");
90575
90906
  return;
90576
90907
  }
@@ -90647,24 +90978,22 @@ function logProtectedFolders(log3, roots) {
90647
90978
  }
90648
90979
  function reportProtectedRepoDiscovery(log3, result) {
90649
90980
  if (result.enabled.length > 0) {
90650
- log3(
90651
- ` \u2713 Repository scanning enabled for ${result.enabled.map((entry) => entry.displayRoot).join(", ")}`
90652
- );
90653
- }
90654
- if (result.blocked.length > 0) {
90655
- log3(
90656
- ` ! macOS blocked Powerplant from reading ${result.blocked.map((entry) => entry.displayRoot).join(", ")}.`
90657
- );
90658
- log3(
90659
- " Sessions there will stay local. To allow it, open System Settings > Privacy & Security > Files and Folders, enable the folder for node, then rerun `powerplant init`."
90660
- );
90981
+ log3(` \u2713 Repository scanning enabled for ${displayRoots(result.enabled)}`);
90661
90982
  }
90983
+ if (result.blocked.length > 0) logBlockedByMacOS(log3, result.blocked);
90662
90984
  return result.blocked.length > 0;
90663
90985
  }
90986
+ function logBlockedByMacOS(log3, roots) {
90987
+ log3(` ! macOS blocked Powerplant from reading ${displayRoots(roots)}.`);
90988
+ log3(` Sessions there will stay local. ${MACOS_BLOCKED_FOLDER_REMEDY}`);
90989
+ }
90990
+ function displayRoots(roots) {
90991
+ return roots.map((entry) => entry.displayRoot).join(", ");
90992
+ }
90664
90993
  function logProtectedSessionsStayLocal(log3, found) {
90665
90994
  const total = found.reduce((sum2, entry) => sum2 + entry.sessionCount, 0);
90666
90995
  log3(
90667
- ` ! Repository matching for ${total} recent session${total === 1 ? "" : "s"} under ${found.map((entry) => entry.displayRoot).join(", ")} was not enabled.`
90996
+ ` ! Repository matching for ${total} recent session${total === 1 ? "" : "s"} under ${displayRoots(found)} was not enabled.`
90668
90997
  );
90669
90998
  log3(" Sessions outside folders you explicitly added will stay local.");
90670
90999
  }
@@ -91313,8 +91642,9 @@ function renderStatus(i) {
91313
91642
  );
91314
91643
  lines.push(`Syncing: ${describeStoredCapture(i.credentials)}.`);
91315
91644
  for (const entry of i.protectedSessions ?? []) {
91645
+ const sessions = `${entry.sessionCount} recent agent session${entry.sessionCount === 1 ? "" : "s"}`;
91316
91646
  lines.push(
91317
- `\u26A0 Repository matching is not enabled for ${entry.sessionCount} recent agent session${entry.sessionCount === 1 ? "" : "s"} under ${entry.displayRoot}. Sessions outside folders you explicitly added are staying local, run \`powerplant init\` to review Git repository scanning.`
91647
+ protectedRootReview(i.credentials, entry.root) === "blocked" ? `\u26A0 macOS is blocking Powerplant from reading ${entry.displayRoot} (${sessions}). Those sessions are staying local. ${MACOS_BLOCKED_FOLDER_REMEDY}` : `\u26A0 Repository matching is not enabled for ${sessions} under ${entry.displayRoot}. Sessions outside folders you explicitly added are staying local, run \`powerplant init\` to review Git repository scanning.`
91318
91648
  );
91319
91649
  }
91320
91650
  }
@@ -91366,9 +91696,17 @@ function renderDaemon(i) {
91366
91696
  ` The daemon is on ${status.version}, this CLI is ${i.cliVersion}, it restarts onto the installed version within minutes, or now via: powerplant restart`
91367
91697
  );
91368
91698
  }
91369
- lines.push(
91370
- status.lastSyncAt !== void 0 ? ` Last successful sync: ${ago(i.now - Date.parse(status.lastSyncAt))}.` : " No successful sync yet in this run."
91699
+ const signals = describeSyncSignals(
91700
+ {
91701
+ lastSessionActivityAt: status.lastUploadAt ?? null,
91702
+ lastCheckedInAt: status.lastPass?.at ?? null,
91703
+ syncPaused: null,
91704
+ lastSyncErrorAt: status.lastPass && !status.lastPass.ok ? status.lastPass.at : null
91705
+ },
91706
+ new Date(i.now),
91707
+ "last upload unmeasured"
91371
91708
  );
91709
+ lines.push(signals === null ? " No pass has run yet in this run." : ` ${signals}`);
91372
91710
  if (status.lastPass && !status.lastPass.ok) {
91373
91711
  lines.push(
91374
91712
  ` \u26A0 Last pass failed: ${status.lastPass.error ?? "unknown error"} (${status.consecutiveFailures} consecutive failure${status.consecutiveFailures === 1 ? "" : "s"}).`
@@ -91395,7 +91733,10 @@ function statusJson(i) {
91395
91733
  },
91396
91734
  protectedRepoDiscoveryNeeded: (i.protectedSessions ?? []).map((entry) => ({
91397
91735
  displayRoot: entry.displayRoot,
91398
- sessionCount: entry.sessionCount
91736
+ sessionCount: entry.sessionCount,
91737
+ // The member said yes and macOS refused: System Settings, not init,
91738
+ // is the fix, so a tool rendering this must not say "review".
91739
+ blockedByMacOS: i.credentials !== null && protectedRootReview(i.credentials, entry.root) === "blocked"
91399
91740
  })),
91400
91741
  ...i.credentialsError !== void 0 && { credentialsError: i.credentialsError },
91401
91742
  service: i.service,
@@ -91990,7 +92331,11 @@ ${USAGE}`);
91990
92331
  // Enrollment's own narration (org, token location, capture boundary)
91991
92332
  // indents under stage 1.
91992
92333
  enroll: () => enroll2((line) => terminal.log(` ${line}`)),
91993
- findProtectedSessions: () => findRecentProtectedSessions(),
92334
+ // Discovery may read a worktree pointer under a root the store
92335
+ // already allows, so a revoked checkout root still surfaces.
92336
+ findProtectedSessions: () => findRecentProtectedSessions({
92337
+ environment: { allowedProtectedRoots: loadCredentials(config2.appEnv)?.repoDiscoveryRoots ?? [] }
92338
+ }),
91994
92339
  promptAllowed: pickerOn,
91995
92340
  promptLine: realPromptLine,
91996
92341
  allowProtectedRepoDiscovery: (found) => allowProtectedRepoDiscovery(config2, found),
@@ -92064,12 +92409,14 @@ The access you granted these agents stays on your Powerplant account until you r
92064
92409
  }
92065
92410
  case "status": {
92066
92411
  const stored = loadCredentialsSafely(config2);
92067
- const allowedProtectedRoots = new Set(stored.credentials?.repoDiscoveryRoots ?? []);
92412
+ const credentials = stored.credentials;
92068
92413
  const [latestVersion, oldestSupportedVersion, protectedSessions] = await Promise.all([
92069
- fetchAvailableVersion(config2, stored.credentials),
92070
- oldestSupportedFor(stored.credentials),
92071
- process.platform === "darwin" && stored.credentials ? findRecentProtectedSessions().then(
92072
- (found) => found.filter((entry) => !allowedProtectedRoots.has(entry.root))
92414
+ fetchAvailableVersion(config2, credentials),
92415
+ oldestSupportedFor(credentials),
92416
+ process.platform === "darwin" && credentials ? findRecentProtectedSessions({
92417
+ environment: { allowedProtectedRoots: credentials.repoDiscoveryRoots ?? [] }
92418
+ }).then(
92419
+ (found) => found.filter((entry) => protectedRootReview(credentials, entry.root) !== "allowed")
92073
92420
  ) : []
92074
92421
  ]);
92075
92422
  const inputs = {
@@ -92212,7 +92559,7 @@ The access you granted these agents stays on your Powerplant account until you r
92212
92559
  const on = parseAutomaticFlag(values.automatic);
92213
92560
  setAutomaticUpdates(config2.appEnv, on);
92214
92561
  console.log(
92215
- on ? "Automatic updates on: the background sync installs each new release within an hour of publication." : "Automatic updates off: powerplant status and the menu bar still say when a release is available."
92562
+ on ? "Automatic updates on: the background sync checks for a release now and installs each new one within an hour of publication." : "Automatic updates off: powerplant status and the menu bar still say when a release is available."
92216
92563
  );
92217
92564
  return;
92218
92565
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usepowerplant/cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },