@birdybeep/cli 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -166,8 +166,9 @@ async function dispatch(argv, deps) {
166
166
  io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
167
167
  return EXIT.USAGE;
168
168
  }
169
+ let code;
169
170
  try {
170
- return await command.run({ args, flags, io });
171
+ code = await command.run({ args, flags, io });
171
172
  } catch (err) {
172
173
  if (err instanceof MissingInputError) {
173
174
  io.errline(
@@ -178,10 +179,17 @@ async function dispatch(argv, deps) {
178
179
  io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
179
180
  return EXIT.ERROR;
180
181
  }
182
+ if (deps.notifyUpdate !== void 0) {
183
+ try {
184
+ await deps.notifyUpdate({ command: pathParts[0] ?? "", flags, io });
185
+ } catch {
186
+ }
187
+ }
188
+ return code;
181
189
  }
182
190
 
183
191
  // src/version.ts
184
- var CLI_VERSION = "0.0.3".length > 0 ? "0.0.3" : "0.0.0";
192
+ var CLI_VERSION = "0.2.0".length > 0 ? "0.2.0" : "0.0.0";
185
193
 
186
194
  // src/commands/agent.ts
187
195
  import { claudeCodeAdapter } from "@birdybeep/claude-code";
@@ -341,6 +349,12 @@ function resolveApiUrl() {
341
349
  if (env !== void 0 && env.length > 0) return env;
342
350
  return readCliConfig().apiUrl ?? DEFAULT_API_URL;
343
351
  }
352
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
353
+ function resolveRegistryUrl() {
354
+ const env = process.env["npm_config_registry"];
355
+ if (env !== void 0 && env.length > 0) return env;
356
+ return DEFAULT_REGISTRY_URL;
357
+ }
344
358
 
345
359
  // src/diagnostics.ts
346
360
  import {
@@ -357,7 +371,7 @@ async function gatherIntegrations(adapters) {
357
371
  }))
358
372
  );
359
373
  }
360
- async function isLoggedIn(tokenOptions = {}) {
374
+ async function isPaired(tokenOptions = {}) {
361
375
  return await getToken(tokenOptions) !== null;
362
376
  }
363
377
  function localQueueDepth() {
@@ -394,13 +408,13 @@ function createDoctorCommand(deps = {}) {
394
408
  run: async (ctx) => {
395
409
  const checks = [];
396
410
  const apiUrl = resolveApiUrl();
397
- const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
411
+ const paired = await isPaired(deps.tokenOptions ?? {});
398
412
  checks.push(
399
- loggedIn ? { name: "Machine token", ok: true } : {
413
+ paired ? { name: "Machine token", ok: true } : {
400
414
  name: "Machine token",
401
415
  ok: false,
402
416
  detail: "No machine token found.",
403
- remedy: "Run `birdybeep login` to pair this machine."
417
+ remedy: "Run `birdybeep pair` to pair this machine."
404
418
  }
405
419
  );
406
420
  for (const adapter of adapters) {
@@ -530,7 +544,44 @@ function createHookCommand(deps = {}) {
530
544
  };
531
545
  }
532
546
 
533
- // src/commands/login.ts
547
+ // src/commands/logout.ts
548
+ import { clearToken } from "@birdybeep/agent-core";
549
+ function createClearTokenCommand(spec, deps = {}) {
550
+ return {
551
+ name: spec.name,
552
+ summary: spec.summary,
553
+ usage: `birdybeep ${spec.name}`,
554
+ run: async (ctx) => {
555
+ await clearToken(deps.tokenOptions ?? {});
556
+ ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
557
+ return EXIT.OK;
558
+ }
559
+ };
560
+ }
561
+ function createLogoutCommand(deps = {}) {
562
+ return createClearTokenCommand(
563
+ {
564
+ name: "logout",
565
+ summary: "Remove the local machine token (same as `unpair`)",
566
+ humanMessage: "Logged out \u2014 the machine token was removed.",
567
+ jsonKey: "loggedOut"
568
+ },
569
+ deps
570
+ );
571
+ }
572
+ function createUnpairCommand(deps = {}) {
573
+ return createClearTokenCommand(
574
+ {
575
+ name: "unpair",
576
+ summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
577
+ humanMessage: "Unpaired \u2014 the machine token was removed.",
578
+ jsonKey: "unpaired"
579
+ },
580
+ deps
581
+ );
582
+ }
583
+
584
+ // src/commands/pair.ts
534
585
  import { getMachineIdentity as getMachineIdentity2, setToken } from "@birdybeep/agent-core";
535
586
  import { renderUnicodeCompact } from "uqr";
536
587
 
@@ -603,22 +654,22 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
603
654
  return { status: "error", code: code ?? "unknown", message, retryable: true };
604
655
  }
605
656
 
606
- // src/commands/login.ts
657
+ // src/commands/pair.ts
607
658
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
608
659
  var HEARTBEAT_MS = 15e3;
609
660
  function renderQrMatrix(qrPayload) {
610
661
  return renderUnicodeCompact(qrPayload, { border: 2 });
611
662
  }
612
- function createLoginCommand(deps = {}) {
663
+ function createPairCommand(deps = {}) {
613
664
  const fetchImpl = deps.fetchImpl ?? fetch;
614
665
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
615
666
  const clock = deps.now ?? (() => Date.now());
616
667
  const renderQr = deps.renderQr ?? renderQrMatrix;
617
668
  const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
618
669
  return {
619
- name: "login",
670
+ name: "pair",
620
671
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
621
- usage: "birdybeep login [--json]",
672
+ usage: "birdybeep pair [--json]",
622
673
  run: async (ctx) => {
623
674
  const apiUrl = resolveApiUrl();
624
675
  const identity = getMachineIdentity2();
@@ -682,7 +733,7 @@ function createLoginCommand(deps = {}) {
682
733
  if (paired === void 0 || paired.status !== "paired") {
683
734
  ctx.io.result({ paired: false, reason: "timeout" });
684
735
  ctx.io.errline(
685
- "Pairing timed out before you approved it. In the BirdyBeep app, tap \u201Cpair a machine\u201D, scan the QR (or enter the code), then run `birdybeep login` again."
736
+ "Pairing timed out before you approved it. In the BirdyBeep app, tap \u201Cpair a machine\u201D, scan the QR (or enter the code), then run `birdybeep pair` again."
686
737
  );
687
738
  return EXIT.ERROR;
688
739
  }
@@ -697,21 +748,6 @@ function createLoginCommand(deps = {}) {
697
748
  };
698
749
  }
699
750
 
700
- // src/commands/logout.ts
701
- import { clearToken } from "@birdybeep/agent-core";
702
- function createLogoutCommand(deps = {}) {
703
- return {
704
- name: "logout",
705
- summary: "Remove the local machine token",
706
- usage: "birdybeep logout",
707
- run: async (ctx) => {
708
- await clearToken(deps.tokenOptions ?? {});
709
- ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
710
- return EXIT.OK;
711
- }
712
- };
713
- }
714
-
715
751
  // src/commands/queue.ts
716
752
  import { LocalEventQueue as LocalEventQueue2 } from "@birdybeep/agent-core";
717
753
  function createQueueCommand() {
@@ -772,7 +808,7 @@ function createReportStatusCommand(deps = {}) {
772
808
  run: async (ctx) => {
773
809
  const token = await getToken2(deps.tokenOptions ?? {});
774
810
  if (token === null) {
775
- ctx.io.errline("Not logged in \u2014 run `birdybeep login` first.");
811
+ ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
776
812
  return EXIT.ERROR;
777
813
  }
778
814
  const items = await gatherItems(adapters);
@@ -817,7 +853,7 @@ function createReportStatusCommand(deps = {}) {
817
853
  });
818
854
  } else if (outcome === "terminal") {
819
855
  ctx.io.errline(
820
- `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep login\`.`
856
+ `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep pair\`.`
821
857
  );
822
858
  } else {
823
859
  for (const e of effective) {
@@ -850,14 +886,14 @@ function createStatusCommand(deps = {}) {
850
886
  usage: "birdybeep status [--json]",
851
887
  run: async (ctx) => {
852
888
  const machine = machineIdentity();
853
- const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
889
+ const paired = await isPaired(deps.tokenOptions ?? {});
854
890
  const integrations = await gatherIntegrations(adapters);
855
891
  const depthBefore = localQueueDepth();
856
892
  const drain = await makeSender(resolveApiUrl()).drainNow();
857
893
  const depthAfter = localQueueDepth();
858
894
  const report = {
859
895
  machine,
860
- loggedIn,
896
+ paired,
861
897
  integrations,
862
898
  queue: { depthBefore, delivered: drain.delivered, depthAfter }
863
899
  };
@@ -865,16 +901,14 @@ function createStatusCommand(deps = {}) {
865
901
  ctx.io.result(report);
866
902
  } else {
867
903
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
868
- ctx.io.line(
869
- loggedIn ? "Login: paired" : "Login: NOT logged in \u2014 run `birdybeep login`"
870
- );
904
+ ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
871
905
  ctx.io.line("Integrations:");
872
906
  for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
873
907
  ctx.io.line(
874
908
  `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
875
909
  );
876
910
  }
877
- return loggedIn ? EXIT.OK : EXIT.ERROR;
911
+ return paired ? EXIT.OK : EXIT.ERROR;
878
912
  }
879
913
  };
880
914
  }
@@ -952,8 +986,9 @@ function createTestCommand(deps = {}) {
952
986
  // src/commands.ts
953
987
  function buildCommands() {
954
988
  return [
955
- createLoginCommand(),
989
+ createPairCommand(),
956
990
  createLogoutCommand(),
991
+ createUnpairCommand(),
957
992
  createStatusCommand(),
958
993
  createTestCommand(),
959
994
  createDoctorCommand(),
@@ -964,13 +999,152 @@ function buildCommands() {
964
999
  ];
965
1000
  }
966
1001
 
1002
+ // src/update-check.ts
1003
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1004
+ import { join as join2 } from "path";
1005
+ import { birdyBeepConfigDir as birdyBeepConfigDir3 } from "@birdybeep/agent-core";
1006
+ var PACKAGE_NAME = "@birdybeep/cli";
1007
+ var PACKAGE_PATH = "@birdybeep%2Fcli";
1008
+ var UPDATE_CACHE_FILE = "update-check.json";
1009
+ var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1010
+ var DEFAULT_TIMEOUT_MS = 1500;
1011
+ var SKIP_COMMANDS = /* @__PURE__ */ new Set(["hook", "report-status"]);
1012
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1013
+ function parseSemver(input) {
1014
+ const m = SEMVER_RE.exec(input.trim());
1015
+ if (m === null) return null;
1016
+ return {
1017
+ major: Number(m[1]),
1018
+ minor: Number(m[2]),
1019
+ patch: Number(m[3]),
1020
+ prerelease: m[4] !== void 0 ? m[4].split(".") : []
1021
+ };
1022
+ }
1023
+ function comparePrerelease(a, b) {
1024
+ if (a.length === 0 && b.length === 0) return 0;
1025
+ if (a.length === 0) return 1;
1026
+ if (b.length === 0) return -1;
1027
+ const len = Math.min(a.length, b.length);
1028
+ for (let i = 0; i < len; i++) {
1029
+ const ai = a[i];
1030
+ const bi = b[i];
1031
+ const aNum = /^\d+$/.test(ai);
1032
+ const bNum = /^\d+$/.test(bi);
1033
+ if (aNum && bNum) {
1034
+ const d = Number(ai) - Number(bi);
1035
+ if (d !== 0) return d < 0 ? -1 : 1;
1036
+ } else if (aNum) {
1037
+ return -1;
1038
+ } else if (bNum) {
1039
+ return 1;
1040
+ } else if (ai !== bi) {
1041
+ return ai < bi ? -1 : 1;
1042
+ }
1043
+ }
1044
+ if (a.length === b.length) return 0;
1045
+ return a.length < b.length ? -1 : 1;
1046
+ }
1047
+ function compareSemver(a, b) {
1048
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
1049
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
1050
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
1051
+ return comparePrerelease(a.prerelease, b.prerelease);
1052
+ }
1053
+ function isNewer(current, latest) {
1054
+ const cur = parseSemver(current);
1055
+ const lat = parseSemver(latest);
1056
+ return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1057
+ }
1058
+ function updateCachePath() {
1059
+ return join2(birdyBeepConfigDir3(), UPDATE_CACHE_FILE);
1060
+ }
1061
+ function readUpdateCache() {
1062
+ try {
1063
+ const parsed = JSON.parse(readFileSync2(updateCachePath(), "utf8"));
1064
+ if (typeof parsed !== "object" || parsed === null) return null;
1065
+ const { checkedAt, latest } = parsed;
1066
+ if (typeof checkedAt !== "number") return null;
1067
+ if (latest !== null && typeof latest !== "string") return null;
1068
+ return { checkedAt, latest };
1069
+ } catch {
1070
+ return null;
1071
+ }
1072
+ }
1073
+ function writeUpdateCache(cache) {
1074
+ mkdirSync3(birdyBeepConfigDir3(), { recursive: true, mode: 448 });
1075
+ writeFileSync2(updateCachePath(), `${JSON.stringify(cache)}
1076
+ `, { mode: 384 });
1077
+ }
1078
+ async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {
1079
+ const url = `${registryUrl.replace(/\/+$/, "")}/${PACKAGE_PATH}/latest`;
1080
+ const controller = new AbortController();
1081
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1082
+ if (typeof timer.unref === "function") timer.unref();
1083
+ try {
1084
+ const res = await fetchImpl(url, {
1085
+ headers: { accept: "application/json" },
1086
+ signal: controller.signal
1087
+ });
1088
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
1089
+ const body = await res.json();
1090
+ if (typeof body.version !== "string" || body.version.length === 0) {
1091
+ throw new Error("registry response had no version");
1092
+ }
1093
+ return body.version;
1094
+ } finally {
1095
+ clearTimeout(timer);
1096
+ }
1097
+ }
1098
+ function renderNotice(current, latest) {
1099
+ return `a new version of birdybeep is available: ${current} \u2192 ${latest}
1100
+ upgrade with: npm install -g ${PACKAGE_NAME}@latest`;
1101
+ }
1102
+ async function maybeNotifyUpdate(opts) {
1103
+ try {
1104
+ if (opts.command !== void 0 && SKIP_COMMANDS.has(opts.command)) return;
1105
+ if (opts.flags.json || opts.flags.nonInteractive) return;
1106
+ const env = opts.env ?? process.env;
1107
+ if (env["BIRDYBEEP_NO_UPDATE_NOTIFIER"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) return;
1108
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1109
+ if (!isTTY) return;
1110
+ const current = opts.currentVersion ?? CLI_VERSION;
1111
+ const now = opts.now ?? Date.now();
1112
+ const intervalMs = opts.intervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
1113
+ const readCache = opts.readCache ?? readUpdateCache;
1114
+ const writeCache = opts.writeCache ?? writeUpdateCache;
1115
+ let cache = readCache();
1116
+ if (cache === null || now - cache.checkedAt >= intervalMs) {
1117
+ let latest = cache?.latest ?? null;
1118
+ try {
1119
+ latest = await fetchLatestVersion(
1120
+ opts.registryUrl ?? resolveRegistryUrl(),
1121
+ opts.fetchImpl ?? fetch,
1122
+ opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
1123
+ );
1124
+ } catch {
1125
+ }
1126
+ cache = { checkedAt: now, latest };
1127
+ try {
1128
+ writeCache(cache);
1129
+ } catch {
1130
+ }
1131
+ }
1132
+ if (cache.latest !== null && isNewer(current, cache.latest)) {
1133
+ opts.io.errline(renderNotice(current, cache.latest));
1134
+ }
1135
+ } catch {
1136
+ }
1137
+ }
1138
+
967
1139
  // src/cli.ts
968
1140
  function runCli(argv, deps = {}) {
1141
+ const notifyUpdate = deps.updateCheck === false ? void 0 : (ctx) => maybeNotifyUpdate({ ...ctx, ...deps.updateCheck ?? {} });
969
1142
  return dispatch(argv, {
970
1143
  version: CLI_VERSION,
971
1144
  commands: deps.commands ?? buildCommands(),
972
1145
  stdout: deps.stdout ?? process.stdout,
973
1146
  stderr: deps.stderr ?? process.stderr,
1147
+ ...notifyUpdate !== void 0 ? { notifyUpdate } : {},
974
1148
  ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
975
1149
  });
976
1150
  }
@@ -986,4 +1160,4 @@ export {
986
1160
  buildCommands,
987
1161
  runCli
988
1162
  };
989
- //# sourceMappingURL=chunk-6VRCYDN5.js.map
1163
+ //# sourceMappingURL=chunk-OCS5IDYI.js.map