@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/bin.cjs CHANGED
@@ -169,8 +169,9 @@ async function dispatch(argv, deps) {
169
169
  io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
170
170
  return EXIT.USAGE;
171
171
  }
172
+ let code;
172
173
  try {
173
- return await command.run({ args, flags, io });
174
+ code = await command.run({ args, flags, io });
174
175
  } catch (err) {
175
176
  if (err instanceof MissingInputError) {
176
177
  io.errline(
@@ -181,6 +182,13 @@ async function dispatch(argv, deps) {
181
182
  io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
182
183
  return EXIT.ERROR;
183
184
  }
185
+ if (deps.notifyUpdate !== void 0) {
186
+ try {
187
+ await deps.notifyUpdate({ command: pathParts[0] ?? "", flags, io });
188
+ } catch {
189
+ }
190
+ }
191
+ return code;
184
192
  }
185
193
 
186
194
  // src/commands/agent.ts
@@ -336,6 +344,12 @@ function resolveApiUrl() {
336
344
  if (env !== void 0 && env.length > 0) return env;
337
345
  return readCliConfig().apiUrl ?? DEFAULT_API_URL;
338
346
  }
347
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
348
+ function resolveRegistryUrl() {
349
+ const env = process.env["npm_config_registry"];
350
+ if (env !== void 0 && env.length > 0) return env;
351
+ return DEFAULT_REGISTRY_URL;
352
+ }
339
353
 
340
354
  // src/diagnostics.ts
341
355
  var import_agent_core3 = require("@birdybeep/agent-core");
@@ -348,7 +362,7 @@ async function gatherIntegrations(adapters) {
348
362
  }))
349
363
  );
350
364
  }
351
- async function isLoggedIn(tokenOptions = {}) {
365
+ async function isPaired(tokenOptions = {}) {
352
366
  return await (0, import_agent_core3.getToken)(tokenOptions) !== null;
353
367
  }
354
368
  function localQueueDepth() {
@@ -385,13 +399,13 @@ function createDoctorCommand(deps = {}) {
385
399
  run: async (ctx) => {
386
400
  const checks = [];
387
401
  const apiUrl = resolveApiUrl();
388
- const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
402
+ const paired = await isPaired(deps.tokenOptions ?? {});
389
403
  checks.push(
390
- loggedIn ? { name: "Machine token", ok: true } : {
404
+ paired ? { name: "Machine token", ok: true } : {
391
405
  name: "Machine token",
392
406
  ok: false,
393
407
  detail: "No machine token found.",
394
- remedy: "Run `birdybeep login` to pair this machine."
408
+ remedy: "Run `birdybeep pair` to pair this machine."
395
409
  }
396
410
  );
397
411
  for (const adapter of adapters) {
@@ -519,12 +533,49 @@ function createHookCommand(deps = {}) {
519
533
  };
520
534
  }
521
535
 
522
- // src/commands/login.ts
523
- var import_agent_core7 = require("@birdybeep/agent-core");
536
+ // src/commands/logout.ts
537
+ var import_agent_core6 = require("@birdybeep/agent-core");
538
+ function createClearTokenCommand(spec, deps = {}) {
539
+ return {
540
+ name: spec.name,
541
+ summary: spec.summary,
542
+ usage: `birdybeep ${spec.name}`,
543
+ run: async (ctx) => {
544
+ await (0, import_agent_core6.clearToken)(deps.tokenOptions ?? {});
545
+ ctx.io.emit(spec.humanMessage, { [spec.jsonKey]: true });
546
+ return EXIT.OK;
547
+ }
548
+ };
549
+ }
550
+ function createLogoutCommand(deps = {}) {
551
+ return createClearTokenCommand(
552
+ {
553
+ name: "logout",
554
+ summary: "Remove the local machine token (same as `unpair`)",
555
+ humanMessage: "Logged out \u2014 the machine token was removed.",
556
+ jsonKey: "loggedOut"
557
+ },
558
+ deps
559
+ );
560
+ }
561
+ function createUnpairCommand(deps = {}) {
562
+ return createClearTokenCommand(
563
+ {
564
+ name: "unpair",
565
+ summary: "Unpair this machine \u2014 remove the local machine token (same as `logout`)",
566
+ humanMessage: "Unpaired \u2014 the machine token was removed.",
567
+ jsonKey: "unpaired"
568
+ },
569
+ deps
570
+ );
571
+ }
572
+
573
+ // src/commands/pair.ts
574
+ var import_agent_core8 = require("@birdybeep/agent-core");
524
575
  var import_uqr = require("uqr");
525
576
 
526
577
  // src/pairing.ts
527
- var import_agent_core6 = require("@birdybeep/agent-core");
578
+ var import_agent_core7 = require("@birdybeep/agent-core");
528
579
  function base(apiUrl) {
529
580
  return apiUrl.replace(/\/$/, "");
530
581
  }
@@ -540,7 +591,7 @@ async function pairStart(apiUrl, input, fetchImpl) {
540
591
  body: JSON.stringify(body)
541
592
  });
542
593
  if (!res.ok) throw new Error(`pairing could not be started (HTTP ${res.status})`);
543
- const parsed = import_agent_core6.pairStartResponseSchema.safeParse(await res.json());
594
+ const parsed = import_agent_core7.pairStartResponseSchema.safeParse(await res.json());
544
595
  if (!parsed.success) throw new Error("pairing start returned an unexpected response shape");
545
596
  return parsed.data;
546
597
  }
@@ -563,7 +614,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
563
614
  body: JSON.stringify(body)
564
615
  });
565
616
  if (res.ok) {
566
- const parsed = import_agent_core6.pairTokenResponseSchema.safeParse(await res.json());
617
+ const parsed = import_agent_core7.pairTokenResponseSchema.safeParse(await res.json());
567
618
  if (!parsed.success) return { status: "pending" };
568
619
  return {
569
620
  status: "paired",
@@ -576,7 +627,7 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
576
627
  errBody = await res.json();
577
628
  } catch {
578
629
  }
579
- const env = import_agent_core6.errorEnvelopeSchema.safeParse(errBody);
630
+ const env = import_agent_core7.errorEnvelopeSchema.safeParse(errBody);
580
631
  const code = env.success ? env.data.error.code : void 0;
581
632
  if (code === "validation_failed" || code === void 0 && res.status >= 400 && res.status < 500 && res.status !== 429) {
582
633
  return { status: "pending" };
@@ -589,27 +640,27 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint)
589
640
  }
590
641
 
591
642
  // src/version.ts
592
- var CLI_VERSION = "0.0.3".length > 0 ? "0.0.3" : "0.0.0";
643
+ var CLI_VERSION = "0.2.0".length > 0 ? "0.2.0" : "0.0.0";
593
644
 
594
- // src/commands/login.ts
645
+ // src/commands/pair.ts
595
646
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
596
647
  var HEARTBEAT_MS = 15e3;
597
648
  function renderQrMatrix(qrPayload) {
598
649
  return (0, import_uqr.renderUnicodeCompact)(qrPayload, { border: 2 });
599
650
  }
600
- function createLoginCommand(deps = {}) {
651
+ function createPairCommand(deps = {}) {
601
652
  const fetchImpl = deps.fetchImpl ?? fetch;
602
653
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
603
654
  const clock = deps.now ?? (() => Date.now());
604
655
  const renderQr = deps.renderQr ?? renderQrMatrix;
605
656
  const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
606
657
  return {
607
- name: "login",
658
+ name: "pair",
608
659
  summary: "Pair this machine with your BirdyBeep account (QR or manual)",
609
- usage: "birdybeep login [--json]",
660
+ usage: "birdybeep pair [--json]",
610
661
  run: async (ctx) => {
611
662
  const apiUrl = resolveApiUrl();
612
- const identity = (0, import_agent_core7.getMachineIdentity)();
663
+ const identity = (0, import_agent_core8.getMachineIdentity)();
613
664
  const start = await pairStart(
614
665
  apiUrl,
615
666
  { machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
@@ -670,11 +721,11 @@ function createLoginCommand(deps = {}) {
670
721
  if (paired === void 0 || paired.status !== "paired") {
671
722
  ctx.io.result({ paired: false, reason: "timeout" });
672
723
  ctx.io.errline(
673
- "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."
724
+ "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."
674
725
  );
675
726
  return EXIT.ERROR;
676
727
  }
677
- await (0, import_agent_core7.setToken)(paired.machineToken, deps.tokenOptions ?? {});
728
+ await (0, import_agent_core8.setToken)(paired.machineToken, deps.tokenOptions ?? {});
678
729
  writeCliConfig({ apiUrl });
679
730
  ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
680
731
  paired: true,
@@ -685,21 +736,6 @@ function createLoginCommand(deps = {}) {
685
736
  };
686
737
  }
687
738
 
688
- // src/commands/logout.ts
689
- var import_agent_core8 = require("@birdybeep/agent-core");
690
- function createLogoutCommand(deps = {}) {
691
- return {
692
- name: "logout",
693
- summary: "Remove the local machine token",
694
- usage: "birdybeep logout",
695
- run: async (ctx) => {
696
- await (0, import_agent_core8.clearToken)(deps.tokenOptions ?? {});
697
- ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
698
- return EXIT.OK;
699
- }
700
- };
701
- }
702
-
703
739
  // src/commands/queue.ts
704
740
  var import_agent_core9 = require("@birdybeep/agent-core");
705
741
  function createQueueCommand() {
@@ -756,7 +792,7 @@ function createReportStatusCommand(deps = {}) {
756
792
  run: async (ctx) => {
757
793
  const token = await (0, import_agent_core10.getToken)(deps.tokenOptions ?? {});
758
794
  if (token === null) {
759
- ctx.io.errline("Not logged in \u2014 run `birdybeep login` first.");
795
+ ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
760
796
  return EXIT.ERROR;
761
797
  }
762
798
  const items = await gatherItems(adapters);
@@ -801,7 +837,7 @@ function createReportStatusCommand(deps = {}) {
801
837
  });
802
838
  } else if (outcome === "terminal") {
803
839
  ctx.io.errline(
804
- `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep login\`.`
840
+ `Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep pair\`.`
805
841
  );
806
842
  } else {
807
843
  for (const e of effective) {
@@ -832,14 +868,14 @@ function createStatusCommand(deps = {}) {
832
868
  usage: "birdybeep status [--json]",
833
869
  run: async (ctx) => {
834
870
  const machine = machineIdentity();
835
- const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
871
+ const paired = await isPaired(deps.tokenOptions ?? {});
836
872
  const integrations = await gatherIntegrations(adapters);
837
873
  const depthBefore = localQueueDepth();
838
874
  const drain = await makeSender(resolveApiUrl()).drainNow();
839
875
  const depthAfter = localQueueDepth();
840
876
  const report = {
841
877
  machine,
842
- loggedIn,
878
+ paired,
843
879
  integrations,
844
880
  queue: { depthBefore, delivered: drain.delivered, depthAfter }
845
881
  };
@@ -847,16 +883,14 @@ function createStatusCommand(deps = {}) {
847
883
  ctx.io.result(report);
848
884
  } else {
849
885
  ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
850
- ctx.io.line(
851
- loggedIn ? "Login: paired" : "Login: NOT logged in \u2014 run `birdybeep login`"
852
- );
886
+ ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
853
887
  ctx.io.line("Integrations:");
854
888
  for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
855
889
  ctx.io.line(
856
890
  `Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
857
891
  );
858
892
  }
859
- return loggedIn ? EXIT.OK : EXIT.ERROR;
893
+ return paired ? EXIT.OK : EXIT.ERROR;
860
894
  }
861
895
  };
862
896
  }
@@ -930,8 +964,9 @@ function createTestCommand(deps = {}) {
930
964
  // src/commands.ts
931
965
  function buildCommands() {
932
966
  return [
933
- createLoginCommand(),
967
+ createPairCommand(),
934
968
  createLogoutCommand(),
969
+ createUnpairCommand(),
935
970
  createStatusCommand(),
936
971
  createTestCommand(),
937
972
  createDoctorCommand(),
@@ -942,13 +977,152 @@ function buildCommands() {
942
977
  ];
943
978
  }
944
979
 
980
+ // src/update-check.ts
981
+ var import_node_fs3 = require("fs");
982
+ var import_node_path2 = require("path");
983
+ var import_agent_core13 = require("@birdybeep/agent-core");
984
+ var PACKAGE_NAME = "@birdybeep/cli";
985
+ var PACKAGE_PATH = "@birdybeep%2Fcli";
986
+ var UPDATE_CACHE_FILE = "update-check.json";
987
+ var DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
988
+ var DEFAULT_TIMEOUT_MS = 1500;
989
+ var SKIP_COMMANDS = /* @__PURE__ */ new Set(["hook", "report-status"]);
990
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
991
+ function parseSemver(input) {
992
+ const m = SEMVER_RE.exec(input.trim());
993
+ if (m === null) return null;
994
+ return {
995
+ major: Number(m[1]),
996
+ minor: Number(m[2]),
997
+ patch: Number(m[3]),
998
+ prerelease: m[4] !== void 0 ? m[4].split(".") : []
999
+ };
1000
+ }
1001
+ function comparePrerelease(a, b) {
1002
+ if (a.length === 0 && b.length === 0) return 0;
1003
+ if (a.length === 0) return 1;
1004
+ if (b.length === 0) return -1;
1005
+ const len = Math.min(a.length, b.length);
1006
+ for (let i = 0; i < len; i++) {
1007
+ const ai = a[i];
1008
+ const bi = b[i];
1009
+ const aNum = /^\d+$/.test(ai);
1010
+ const bNum = /^\d+$/.test(bi);
1011
+ if (aNum && bNum) {
1012
+ const d = Number(ai) - Number(bi);
1013
+ if (d !== 0) return d < 0 ? -1 : 1;
1014
+ } else if (aNum) {
1015
+ return -1;
1016
+ } else if (bNum) {
1017
+ return 1;
1018
+ } else if (ai !== bi) {
1019
+ return ai < bi ? -1 : 1;
1020
+ }
1021
+ }
1022
+ if (a.length === b.length) return 0;
1023
+ return a.length < b.length ? -1 : 1;
1024
+ }
1025
+ function compareSemver(a, b) {
1026
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
1027
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
1028
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
1029
+ return comparePrerelease(a.prerelease, b.prerelease);
1030
+ }
1031
+ function isNewer(current, latest) {
1032
+ const cur = parseSemver(current);
1033
+ const lat = parseSemver(latest);
1034
+ return cur !== null && lat !== null && compareSemver(cur, lat) < 0;
1035
+ }
1036
+ function updateCachePath() {
1037
+ return (0, import_node_path2.join)((0, import_agent_core13.birdyBeepConfigDir)(), UPDATE_CACHE_FILE);
1038
+ }
1039
+ function readUpdateCache() {
1040
+ try {
1041
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(updateCachePath(), "utf8"));
1042
+ if (typeof parsed !== "object" || parsed === null) return null;
1043
+ const { checkedAt, latest } = parsed;
1044
+ if (typeof checkedAt !== "number") return null;
1045
+ if (latest !== null && typeof latest !== "string") return null;
1046
+ return { checkedAt, latest };
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+ function writeUpdateCache(cache) {
1052
+ (0, import_node_fs3.mkdirSync)((0, import_agent_core13.birdyBeepConfigDir)(), { recursive: true, mode: 448 });
1053
+ (0, import_node_fs3.writeFileSync)(updateCachePath(), `${JSON.stringify(cache)}
1054
+ `, { mode: 384 });
1055
+ }
1056
+ async function fetchLatestVersion(registryUrl, fetchImpl, timeoutMs) {
1057
+ const url = `${registryUrl.replace(/\/+$/, "")}/${PACKAGE_PATH}/latest`;
1058
+ const controller = new AbortController();
1059
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1060
+ if (typeof timer.unref === "function") timer.unref();
1061
+ try {
1062
+ const res = await fetchImpl(url, {
1063
+ headers: { accept: "application/json" },
1064
+ signal: controller.signal
1065
+ });
1066
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
1067
+ const body = await res.json();
1068
+ if (typeof body.version !== "string" || body.version.length === 0) {
1069
+ throw new Error("registry response had no version");
1070
+ }
1071
+ return body.version;
1072
+ } finally {
1073
+ clearTimeout(timer);
1074
+ }
1075
+ }
1076
+ function renderNotice(current, latest) {
1077
+ return `a new version of birdybeep is available: ${current} \u2192 ${latest}
1078
+ upgrade with: npm install -g ${PACKAGE_NAME}@latest`;
1079
+ }
1080
+ async function maybeNotifyUpdate(opts) {
1081
+ try {
1082
+ if (opts.command !== void 0 && SKIP_COMMANDS.has(opts.command)) return;
1083
+ if (opts.flags.json || opts.flags.nonInteractive) return;
1084
+ const env = opts.env ?? process.env;
1085
+ if (env["BIRDYBEEP_NO_UPDATE_NOTIFIER"] || env["NO_UPDATE_NOTIFIER"] || env["CI"]) return;
1086
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1087
+ if (!isTTY) return;
1088
+ const current = opts.currentVersion ?? CLI_VERSION;
1089
+ const now = opts.now ?? Date.now();
1090
+ const intervalMs = opts.intervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
1091
+ const readCache = opts.readCache ?? readUpdateCache;
1092
+ const writeCache = opts.writeCache ?? writeUpdateCache;
1093
+ let cache = readCache();
1094
+ if (cache === null || now - cache.checkedAt >= intervalMs) {
1095
+ let latest = cache?.latest ?? null;
1096
+ try {
1097
+ latest = await fetchLatestVersion(
1098
+ opts.registryUrl ?? resolveRegistryUrl(),
1099
+ opts.fetchImpl ?? fetch,
1100
+ opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
1101
+ );
1102
+ } catch {
1103
+ }
1104
+ cache = { checkedAt: now, latest };
1105
+ try {
1106
+ writeCache(cache);
1107
+ } catch {
1108
+ }
1109
+ }
1110
+ if (cache.latest !== null && isNewer(current, cache.latest)) {
1111
+ opts.io.errline(renderNotice(current, cache.latest));
1112
+ }
1113
+ } catch {
1114
+ }
1115
+ }
1116
+
945
1117
  // src/cli.ts
946
1118
  function runCli(argv, deps = {}) {
1119
+ const notifyUpdate = deps.updateCheck === false ? void 0 : (ctx) => maybeNotifyUpdate({ ...ctx, ...deps.updateCheck ?? {} });
947
1120
  return dispatch(argv, {
948
1121
  version: CLI_VERSION,
949
1122
  commands: deps.commands ?? buildCommands(),
950
1123
  stdout: deps.stdout ?? process.stdout,
951
1124
  stderr: deps.stderr ?? process.stderr,
1125
+ ...notifyUpdate !== void 0 ? { notifyUpdate } : {},
952
1126
  ...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
953
1127
  });
954
1128
  }