@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.
package/dist/index.cjs CHANGED
@@ -205,8 +205,9 @@ async function dispatch(argv, deps) {
205
205
  io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
206
206
  return EXIT.USAGE;
207
207
  }
208
+ let code;
208
209
  try {
209
- return await command.run({ args, flags, io });
210
+ code = await command.run({ args, flags, io });
210
211
  } catch (err) {
211
212
  if (err instanceof MissingInputError) {
212
213
  io.errline(
@@ -217,6 +218,13 @@ async function dispatch(argv, deps) {
217
218
  io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
218
219
  return EXIT.ERROR;
219
220
  }
221
+ if (deps.notifyUpdate !== void 0) {
222
+ try {
223
+ await deps.notifyUpdate({ command: pathParts[0] ?? "", flags, io });
224
+ } catch {
225
+ }
226
+ }
227
+ return code;
220
228
  }
221
229
 
222
230
  // src/commands/agent.ts
@@ -372,6 +380,12 @@ function resolveApiUrl() {
372
380
  if (env !== void 0 && env.length > 0) return env;
373
381
  return readCliConfig().apiUrl ?? DEFAULT_API_URL;
374
382
  }
383
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
384
+ function resolveRegistryUrl() {
385
+ const env = process.env["npm_config_registry"];
386
+ if (env !== void 0 && env.length > 0) return env;
387
+ return DEFAULT_REGISTRY_URL;
388
+ }
375
389
 
376
390
  // src/diagnostics.ts
377
391
  var import_agent_core3 = require("@birdybeep/agent-core");
@@ -384,7 +398,7 @@ async function gatherIntegrations(adapters) {
384
398
  }))
385
399
  );
386
400
  }
387
- async function isLoggedIn(tokenOptions = {}) {
401
+ async function isPaired(tokenOptions = {}) {
388
402
  return await (0, import_agent_core3.getToken)(tokenOptions) !== null;
389
403
  }
390
404
  function localQueueDepth() {
@@ -421,13 +435,13 @@ function createDoctorCommand(deps = {}) {
421
435
  run: async (ctx) => {
422
436
  const checks = [];
423
437
  const apiUrl = resolveApiUrl();
424
- const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
438
+ const paired = await isPaired(deps.tokenOptions ?? {});
425
439
  checks.push(
426
- loggedIn ? { name: "Machine token", ok: true } : {
440
+ paired ? { name: "Machine token", ok: true } : {
427
441
  name: "Machine token",
428
442
  ok: false,
429
443
  detail: "No machine token found.",
430
- remedy: "Run `birdybeep login` to pair this machine."
444
+ remedy: "Run `birdybeep pair` to pair this machine."
431
445
  }
432
446
  );
433
447
  for (const adapter of adapters) {
@@ -555,12 +569,49 @@ function createHookCommand(deps = {}) {
555
569
  };
556
570
  }
557
571
 
558
- // src/commands/login.ts
559
- var import_agent_core7 = require("@birdybeep/agent-core");
572
+ // src/commands/logout.ts
573
+ var import_agent_core6 = require("@birdybeep/agent-core");
574
+ function createClearTokenCommand(spec, deps = {}) {
575
+ return {
576
+ name: spec.name,
577
+ summary: spec.summary,
578
+ usage: `birdybeep ${spec.name}`,
579
+ run: async (ctx) => {
580
+ await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
581
+ ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
582
+ return EXIT.OK;
583
+ }
584
+ };
585
+ }
586
+ function createLogoutCommand(deps = {}) {
587
+ return createClearTokenCommand(
588
+ {
589
+ name: "logout",
590
+ summary: "Remove the local machine token (same as `unpair`)",
591
+ humanMessage: "Logged out \u2014 the machine token was removed.",
592
+ jsonKey: "loggedOut"
593
+ },
594
+ deps
595
+ );
596
+ }
597
+ function createUnpairCommand(deps = {}) {
598
+ return createClearTokenCommand(
599
+ {
600
+ name: "unpair",
601
+ summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
602
+ humanMessage: "Unpaired \u2014 the machine token was removed.",
603
+ jsonKey: "unpaired"
604
+ },
605
+ deps
606
+ );
607
+ }
608
+
609
+ // src/commands/pair.ts
610
+ var import_agent_core8 = require("@birdybeep/agent-core");
560
611
  var import_uqr = require("uqr");
561
612
 
562
613
  // src/pairing.ts
563
- var import_agent_core6 = require("@birdybeep/agent-core");
614
+ var import_agent_core7 = require("@birdybeep/agent-core");
564
615
  function base(apiUrl) {
565
616
  return apiUrl.replace(/\/$/, "");
566
617
  }
@@ -576,7 +627,7 @@ async function pairStart(apiUrl, input, fetchImpl) {
576
627
  body: JSON.stringify(body)
577
628
  });
578
629
  if (!res.ok) throw new Error(`pairing could not be started (HTTP ${res.status})`);
579
- const parsed = import_agent_core6.pairStartResponseSchema.safeParse(await res.json());
630
+ const parsed = import_agent_core7.pairStartResponseSchema.safeParse(await res.json());
580
631
  if (!parsed.success) throw new Error("pairing start returned an unexpected response shape");
581
632
  return parsed.data;
582
633
  }
@@ -599,7 +650,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
599
650
  body: JSON.stringify(body)
600
651
  });
601
652
  if (res.ok) {
602
- const parsed = import_agent_core6.pairTokenResponseSchema.safeParse(await res.json());
653
+ const parsed = import_agent_core7.pairTokenResponseSchema.safeParse(await res.json());
603
654
  if (!parsed.success) return { status: "pending" };
604
655
  return {
605
656
  status: "paired",
@@ -612,7 +663,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
612
663
  errBody = await res.json();
613
664
  } catch {
614
665
  }
615
- const env = import_agent_core6.errorEnvelopeSchema.safeParse(errBody);
666
+ const env = import_agent_core7.errorEnvelopeSchema.safeParse(errBody);
616
667
  const code = env.success ? env.data.error.code : void 0;
617
668
  if (code === "validation_failed" || code === void 0 && res.status >= 400 && res.status < 500 && res.status !== 429) {
618
669
  return { status: "pending" };
@@ -625,27 +676,27 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
625
676
  }
626
677
 
627
678
  // src/version.ts
628
- var CLI_VERSION = "0.0.3".length > 0 ? "0.0.3" : "0.0.0";
679
+ var CLI_VERSION = "0.2.0".length > 0 ? "0.2.0" : "0.0.0";
629
680
 
630
- // src/commands/login.ts
681
+ // src/commands/pair.ts
631
682
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
632
683
  var HEARTBEAT_MS = 15e3;
633
684
  function renderQrMatrix(qrPayload) {
634
685
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
635
686
  }
636
- function createLoginCommand(deps = {}) {
687
+ function createPairCommand(deps = {}) {
637
688
  const fetchImpl = deps.fetchImpl ?? fetch;
638
689
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
639
690
  const clock = deps.now ?? (() => Date.now());
640
691
  const renderQr = deps.renderQr ?? renderQrMatrix;
641
692
  const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
642
693
  return {
643
- name: "login",
694
+ name: "pair",
644
695
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
645
- usage: "birdybeep login [--json]",
696
+ usage: "birdybeep pair [--json]",
646
697
  run: async (ctx) => {
647
698
  const apiUrl = resolveApiUrl();
648
- const identity = (0, import_agent_core7.getMachineIdentity)();
699
+ const identity = (0, import_agent_core8.getMachineIdentity)();
649
700
  const start = await pairStart(
650
701
  apiUrl,
651
702
  { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
@@ -706,11 +757,11 @@ function createLoginCommand(deps = {}) {
706
757
  if (paired === void 0 || paired.status !== "paired") {
707
758
  ctx.io.result({ paired: false, reason: "timeout" });
708
759
  ctx.io.errline(
709
- "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."
760
+ "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."
710
761
  );
711
762
  return EXIT.ERROR;
712
763
  }
713
- await (0, import_agent_core7.setToken)(paired.machineToken, deps.tokenOptions ?? {});
764
+ await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
714
765
  writeCliConfig({ apiUrl });
715
766
  ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
716
767
  paired: true,
@@ -721,21 +772,6 @@ function createLoginCommand(deps = {}) {
721
772
  };
722
773
  }
723
774
 
724
- // src/commands/logout.ts
725
- var import_agent_core8 = require("@birdybeep/agent-core");
726
- function createLogoutCommand(deps = {}) {
727
- return {
728
- name: "logout",
729
- summary: "Remove the local machine token",
730
- usage: "birdybeep logout",
731
- run: async (ctx) => {
732
- await (0, import_agent_core8.clearToken)(deps.tokenOptions ?? {});
733
- ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
734
- return EXIT.OK;
735
- }
736
- };
737
- }
738
-
739
775
  // src/commands/queue.ts
740
776
  var import_agent_core9 = require("@birdybeep/agent-core");
741
777
  function createQueueCommand() {
@@ -792,7 +828,7 @@ function createReportStatusCommand(deps = {}) {
792
828
  run: async (ctx) => {
793
829
  const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
794
830
  if (token === null) {
795
- ctx.io.errline("Not logged in \u2014 run `birdybeep login` first.");
831
+ ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
796
832
  return EXIT.ERROR;
797
833
  }
798
834
  const items = await gatherItems(adapters);
@@ -837,7 +873,7 @@ function createReportStatusCommand(deps = {}) {
837
873
  });
838
874
  } else if (outcome === "terminal") {
839
875
  ctx.io.errline(
840
- `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep login\`.`
876
+ `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep pair\`.`
841
877
  );
842
878
  } else {
843
879
  for (const e of effective) {
@@ -868,14 +904,14 @@ function createStatusCommand(deps = {}) {
868
904
  usage: "birdybeep status [--json]",
869
905
  run: async (ctx) => {
870
906
  const machine = machineIdentity();
871
- const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
907
+ const paired = await isPaired(deps.tokenOptions ?? {});
872
908
  const integrations = await gatherIntegrations(adapters);
873
909
  const depthBefore = localQueueDepth();
874
910
  const drain = await makeSender(resolveApiUrl()).drainNow();
875
911
  const depthAfter = localQueueDepth();
876
912
  const report = {
877
913
  machine,
878
- loggedIn,
914
+ paired,
879
915
  integrations,
880
916
  queue: { depthBefore, delivered: drain.delivered, depthAfter }
881
917
  };
@@ -883,16 +919,14 @@ function createStatusCommand(deps = {}) {
883
919
  ctx.io.result(report);
884
920
  } else {
885
921
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
886
- ctx.io.line(
887
- loggedIn ? "Login: paired" : "Login: NOT logged in \u2014 run `birdybeep login`"
888
- );
922
+ ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
889
923
  ctx.io.line("Integrations:");
890
924
  for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
891
925
  ctx.io.line(
892
926
  `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
893
927
  );
894
928
  }
895
- return loggedIn ? EXIT.OK : EXIT.ERROR;
929
+ return paired ? EXIT.OK : EXIT.ERROR;
896
930
  }
897
931
  };
898
932
  }
@@ -966,8 +1000,9 @@ function createTestCommand(deps = {}) {
966
1000
  // src/commands.ts
967
1001
  function buildCommands() {
968
1002
  return [
969
- createLoginCommand(),
1003
+ createPairCommand(),
970
1004
  createLogoutCommand(),
1005
+ createUnpairCommand(),
971
1006
  createStatusCommand(),
972
1007
  createTestCommand(),
973
1008
  createDoctorCommand(),
@@ -978,13 +1013,152 @@ function buildCommands() {
978
1013
  ];
979
1014
  }
980
1015
 
1016
+ // src/update-check.ts
1017
+ var import_node_fs3 = require("fs");
1018
+ var import_node_path2 = require("path");
1019
+ var import_agent_core13 = require("@birdybeep/agent-core");
1020
+ var PACKAGE_NAME = "@birdybeep/cli";
1021
+ var PACKAGE_PATH = "@birdybeep%2Fcli";
1022
+ var UPDATE_CACHE_FILE = "update-check.json";
1023
+ var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1024
+ var DEFAULT_TIMEOUT_MS = 1500;
1025
+ var SKIP_COMMANDS = /* @__PURE__ */ new Set(["hook", "report-status"]);
1026
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1027
+ function parseSemver(input) {
1028
+ const m = SEMVER_RE.exec(input.trim());
1029
+ if (m === null) return null;
1030
+ return {
1031
+ major: Number(m[1]),
1032
+ minor: Number(m[2]),
1033
+ patch: Number(m[3]),
1034
+ prerelease: m[4] !== void 0 ? m[4].split(".") : []
1035
+ };
1036
+ }
1037
+ function comparePrerelease(a, b) {
1038
+ if (a.length === 0 && b.length === 0) return 0;
1039
+ if (a.length === 0) return 1;
1040
+ if (b.length === 0) return -1;
1041
+ const len = Math.min(a.length, b.length);
1042
+ for (let i = 0; i < len; i++) {
1043
+ const ai = a[i];
1044
+ const bi = b[i];
1045
+ const aNum = /^\d+$/.test(ai);
1046
+ const bNum = /^\d+$/.test(bi);
1047
+ if (aNum && bNum) {
1048
+ const d = Number(ai) - Number(bi);
1049
+ if (d !== 0) return d < 0 ? -1 : 1;
1050
+ } else if (aNum) {
1051
+ return -1;
1052
+ } else if (bNum) {
1053
+ return 1;
1054
+ } else if (ai !== bi) {
1055
+ return ai < bi ? -1 : 1;
1056
+ }
1057
+ }
1058
+ if (a.length === b.length) return 0;
1059
+ return a.length < b.length ? -1 : 1;
1060
+ }
1061
+ function compareSemver(a, b) {
1062
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
1063
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
1064
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
1065
+ return comparePrerelease(a.prerelease, b.prerelease);
1066
+ }
1067
+ function isNewer(current, latest) {
1068
+ const cur = parseSemver(current);
1069
+ const lat = parseSemver(latest);
1070
+ return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1071
+ }
1072
+ function updateCachePath() {
1073
+ return (0, import_node_path2.join)((0, import_agent_core13.birdyBeepConfigDir)(), UPDATE_CACHE_FILE);
1074
+ }
1075
+ function readUpdateCache() {
1076
+ try {
1077
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(updateCachePath(), "utf8"));
1078
+ if (typeof parsed !== "object" || parsed === null) return null;
1079
+ const { checkedAt, latest } = parsed;
1080
+ if (typeof checkedAt !== "number") return null;
1081
+ if (latest !== null && typeof latest !== "string") return null;
1082
+ return { checkedAt, latest };
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+ function writeUpdateCache(cache) {
1088
+ (0, import_node_fs3.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1089
+ (0, import_node_fs3.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
1090
+ `, { mode: 384 });
1091
+ }
1092
+ async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {
1093
+ const url = `${registryUrl.replace(/\/+$/, "")}/${PACKAGE_PATH}/latest`;
1094
+ const controller = new AbortController();
1095
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1096
+ if (typeof timer.unref === "function") timer.unref();
1097
+ try {
1098
+ const res = await fetchImpl(url, {
1099
+ headers: { accept: "application/json" },
1100
+ signal: controller.signal
1101
+ });
1102
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
1103
+ const body = await res.json();
1104
+ if (typeof body.version !== "string" || body.version.length === 0) {
1105
+ throw new Error("registry response had no version");
1106
+ }
1107
+ return body.version;
1108
+ } finally {
1109
+ clearTimeout(timer);
1110
+ }
1111
+ }
1112
+ function renderNotice(current, latest) {
1113
+ return `a new version of birdybeep is available: ${current} \u2192 ${latest}
1114
+ upgrade with: npm install -g ${PACKAGE_NAME}@latest`;
1115
+ }
1116
+ async function maybeNotifyUpdate(opts) {
1117
+ try {
1118
+ if (opts.command !== void 0 && SKIP_COMMANDS.has(opts.command)) return;
1119
+ if (opts.flags.json || opts.flags.nonInteractive) return;
1120
+ const env = opts.env ?? process.env;
1121
+ if (env["BIRDYBEEP_NO_UPDATE_NOTIFIER"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) return;
1122
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1123
+ if (!isTTY) return;
1124
+ const current = opts.currentVersion ?? CLI_VERSION;
1125
+ const now = opts.now ?? Date.now();
1126
+ const intervalMs = opts.intervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
1127
+ const readCache = opts.readCache ?? readUpdateCache;
1128
+ const writeCache = opts.writeCache ?? writeUpdateCache;
1129
+ let cache = readCache();
1130
+ if (cache === null || now - cache.checkedAt >= intervalMs) {
1131
+ let latest = cache?.latest ?? null;
1132
+ try {
1133
+ latest = await fetchLatestVersion(
1134
+ opts.registryUrl ?? resolveRegistryUrl(),
1135
+ opts.fetchImpl ?? fetch,
1136
+ opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
1137
+ );
1138
+ } catch {
1139
+ }
1140
+ cache = { checkedAt: now, latest };
1141
+ try {
1142
+ writeCache(cache);
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ if (cache.latest !== null && isNewer(current, cache.latest)) {
1147
+ opts.io.errline(renderNotice(current, cache.latest));
1148
+ }
1149
+ } catch {
1150
+ }
1151
+ }
1152
+
981
1153
  // src/cli.ts
982
1154
  function runCli(argv, deps = {}) {
1155
+ const notifyUpdate = deps.updateCheck === false ? void 0 : (ctx) => maybeNotifyUpdate({ ...ctx, ...deps.updateCheck ?? {} });
983
1156
  return dispatch(argv, {
984
1157
  version: CLI_VERSION,
985
1158
  commands: deps.commands ?? buildCommands(),
986
1159
  stdout: deps.stdout ?? process.stdout,
987
1160
  stderr: deps.stderr ?? process.stderr,
1161
+ ...notifyUpdate !== void 0 ? { notifyUpdate } : {},
988
1162
  ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
989
1163
  });
990
1164
  }