@towles/tool 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.mjs +259 -28
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -75,6 +75,7 @@ if that works, then you need to add the pnpm global bin directory to your PATH.
75
75
  - [rolldown-vite](https://voidzero.dev/posts/announcing-rolldown-vite) - A Vite plugin for rolling down your code
76
76
  - ~~[zx](https://github.com/google/zx) google created library to write shell scripts in a more powerful and expressive way via the Anthropic API.~~
77
77
  - [prompts](https://github.com/terkelg/prompts) - A library for creating beautiful command-line prompts, with fuzzy search and other features.
78
+ - had to patch it so `esc` cancels the selection with [pnpm-patch-i](https://github.com/antfu/pnpm-patch-i)
78
79
  - [yargs](https://github.com/yargs/yargs) - A modern, feature-rich command-line argument parser with enhanced error handling, TypeScript support, and flexible command configuration.
79
80
  - ~~[ink](https://github.com/vadimdemedes/ink) - React for interactive command-line apps~~
80
81
  - wanted hotkey support and more complex UI but this was overkill for this project.
package/dist/index.mjs CHANGED
@@ -15,6 +15,9 @@ import prompts from 'prompts';
15
15
  import { execSync, exec } from 'node:child_process';
16
16
  import { promisify } from 'node:util';
17
17
  import { DateTime } from 'luxon';
18
+ import { Fzf } from 'fzf';
19
+ import stripAnsi from 'strip-ansi';
20
+ import { exec as exec$1 } from 'tinyexec';
18
21
 
19
22
  async function loadTowlesToolContext({
20
23
  cwd,
@@ -118,8 +121,13 @@ async function loadSettings() {
118
121
  );
119
122
  }
120
123
 
121
- const version = "0.0.16";
124
+ const version = "0.0.18";
122
125
 
126
+ const JOURNAL_TYPES = {
127
+ DAILY_NOTES: "daily-notes",
128
+ MEETING: "meeting",
129
+ NOTE: "note"
130
+ };
123
131
  async function parseArguments(argv) {
124
132
  let parsedResult = null;
125
133
  const parser = yargs(hideBin(argv)).scriptName(AppInfo.toolName).usage("Usage: $0 <command> [options]").version(version).demandCommand(1, "You need at least one command").recommendCommands().strict().help().wrap(yargs().terminalWidth());
@@ -132,7 +140,7 @@ async function parseArguments(argv) {
132
140
  "Weekly files with daily sections for ongoing work and notes",
133
141
  {},
134
142
  (argv2) => {
135
- parsedResult = { command: "journal", args: { subcommand: "daily-notes", title: "" } };
143
+ parsedResult = { command: "journal", args: { jouralType: "daily-notes", title: "" } };
136
144
  }
137
145
  ).command(
138
146
  ["meeting [title]", "m"],
@@ -144,7 +152,7 @@ async function parseArguments(argv) {
144
152
  }
145
153
  },
146
154
  (argv2) => {
147
- parsedResult = { command: "journal", args: { subcommand: "meeting", title: argv2.title || "" } };
155
+ parsedResult = { command: "journal", args: { jouralType: "meeting", title: argv2.title || "" } };
148
156
  }
149
157
  ).command(
150
158
  ["note [title]", "n"],
@@ -156,7 +164,7 @@ async function parseArguments(argv) {
156
164
  }
157
165
  },
158
166
  (argv2) => {
159
- parsedResult = { command: "journal", args: { subcommand: "note", title: argv2.title || "" } };
167
+ parsedResult = { command: "journal", args: { jouralType: "note", title: argv2.title || "" } };
160
168
  }
161
169
  ).demandCommand(1, "You need to specify a journal subcommand").help();
162
170
  },
@@ -178,6 +186,20 @@ async function parseArguments(argv) {
178
186
  parsedResult = { command: "git-commit", args: { message: argv2.message } };
179
187
  }
180
188
  );
189
+ parser.command(
190
+ ["gh-branch [assignedToMe...]", "branch", "br"],
191
+ "Create git branch from github issue",
192
+ {
193
+ assignedToMe: {
194
+ type: "boolean",
195
+ describe: "filter issues based on if assigned to you by default",
196
+ default: false
197
+ }
198
+ },
199
+ (argv2) => {
200
+ parsedResult = { command: "gh-branch", args: { assignedToMe: argv2.assignedToMe } };
201
+ }
202
+ );
181
203
  parser.command(
182
204
  ["config", "cfg"],
183
205
  "set or show configuration file.",
@@ -186,6 +208,14 @@ async function parseArguments(argv) {
186
208
  parsedResult = { command: "config", args: {} };
187
209
  }
188
210
  );
211
+ parser.command(
212
+ ["weather", "w"],
213
+ "Show current weather for Cincinnati, OH",
214
+ {},
215
+ () => {
216
+ parsedResult = { command: "weather", args: {} };
217
+ }
218
+ );
189
219
  await parser.parse();
190
220
  if (!parsedResult) {
191
221
  throw new Error("No command was parsed");
@@ -321,11 +351,6 @@ function formatDate(date) {
321
351
  }
322
352
 
323
353
  const execAsync = promisify(exec);
324
- const JOURNAL_TYPES = {
325
- DAILY_NOTES: "daily-notes",
326
- MEETING: "meeting",
327
- NOTE: "note"
328
- };
329
354
  function ensureDirectoryExists(folderPath) {
330
355
  if (!existsSync(folderPath)) {
331
356
  consola.info(`Creating journal directory: ${colors.cyan(folderPath)}`);
@@ -420,7 +445,7 @@ function resolvePathTemplate(template, title, date, mondayDate) {
420
445
  }
421
446
  });
422
447
  }
423
- function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */ new Date(), type = JOURNAL_TYPES.DAILY_NOTES, title }) {
448
+ function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */ new Date(), type, title }) {
424
449
  const currentDate = new Date(date);
425
450
  let templatePath = "";
426
451
  let mondayDate = getMondayOfWeek(currentDate);
@@ -442,7 +467,7 @@ function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */
442
467
  break;
443
468
  }
444
469
  default:
445
- throw new Error(`Unknown journal type: ${type}`);
470
+ throw new Error(`Unknown JournalType: ${type}`);
446
471
  }
447
472
  const resolvedPath = resolvePathTemplate(templatePath, title, currentDate, mondayDate);
448
473
  return {
@@ -508,34 +533,240 @@ async function configCommand(context) {
508
533
  consola.log(` ${context.cwd}`);
509
534
  }
510
535
 
536
+ const isGithubCliInstalled = async () => {
537
+ try {
538
+ const proc = await exec$1(`gh`, ["--version"]);
539
+ return proc.stdout.indexOf("https://github.com/cli/cli") > 0;
540
+ } catch (e) {
541
+ return false;
542
+ }
543
+ };
544
+ const getIssues = async ({ assignedToMe, cwd }) => {
545
+ let issues = [];
546
+ const flags = [
547
+ "issue",
548
+ "list",
549
+ "--json",
550
+ "labels,number,title,state"
551
+ ];
552
+ if (assignedToMe) {
553
+ flags.push("--assignee");
554
+ flags.push("@me");
555
+ }
556
+ const result = await exec$1(`gh`, flags);
557
+ const striped = stripAnsi(result.stdout);
558
+ issues = JSON.parse(striped);
559
+ return issues;
560
+ };
561
+
562
+ const createBranch = async ({ branchName }) => {
563
+ const result = await exec$1(`git`, ["checkout", "-b", branchName]);
564
+ return result.stdout;
565
+ };
566
+
567
+ const getTerminalColumns = () => process.stdout?.columns || 80;
568
+ const limitText = (text, maxWidth) => {
569
+ if (text.length <= maxWidth)
570
+ return text;
571
+ return `${text.slice(0, maxWidth - 1)}${colors.dim("\u2026")}`;
572
+ };
573
+ function hexToRgb(hex) {
574
+ const cleanHex = hex.replace("#", "");
575
+ const r = Number.parseInt(cleanHex.slice(0, 2), 16);
576
+ const g = Number.parseInt(cleanHex.slice(2, 4), 16);
577
+ const b = Number.parseInt(cleanHex.slice(4, 6), 16);
578
+ return { r, g, b };
579
+ }
580
+ function printWithHexColor({ msg, hex }) {
581
+ const colorWithHex = hex.startsWith("#") ? hex : `#${hex}`;
582
+ const { r, g, b } = hexToRgb(colorWithHex);
583
+ const colorStart = `\x1B[38;2;${r};${g};${b}m`;
584
+ const colorEnd = "\x1B[0m";
585
+ return `${colorStart}${msg}${colorEnd}`;
586
+ }
587
+
588
+ const checkPreqrequisites = async () => {
589
+ const cliInstalled = await isGithubCliInstalled();
590
+ if (!cliInstalled) {
591
+ consola.log("Github CLI not installed");
592
+ process.exit(1);
593
+ }
594
+ };
595
+ function customTrimEnd(str, charsToTrim) {
596
+ let i = str.length - 1;
597
+ while (i >= 0 && charsToTrim.includes(str[i])) {
598
+ i--;
599
+ }
600
+ return str.substring(0, i + 1);
601
+ }
602
+ const createBranchNameFromIssue = (selectedIssue) => {
603
+ let slug = selectedIssue.title.toLowerCase();
604
+ slug = slug.trim();
605
+ slug = slug.replaceAll(" ", "-");
606
+ slug = slug.replace(/[^0-9a-zA-Z_]/g, "-");
607
+ slug = slug.replaceAll("--", "-");
608
+ slug = slug.replaceAll("--", "-");
609
+ slug = slug.replaceAll("--", "-");
610
+ slug = customTrimEnd(slug, ["-"]);
611
+ const branchName = `feature/${selectedIssue.number}-${slug}`;
612
+ return branchName;
613
+ };
614
+ async function githubBranchCommand(context, args) {
615
+ await checkPreqrequisites();
616
+ const assignedToMe = Boolean(args.assignedToMe);
617
+ consola.log("Assigned to me:", assignedToMe);
618
+ const currentIssues = await getIssues({ assignedToMe, cwd: context.cwd });
619
+ if (currentIssues.length === 0) {
620
+ consola.log(colors.yellow("No issues found, check assignments"));
621
+ process.exit(1);
622
+ } else {
623
+ consola.log(colors.green(`${currentIssues.length} Issues found assigned to you`));
624
+ }
625
+ let lineMaxLength = getTerminalColumns();
626
+ const longestNumber = Math.max(...currentIssues.map((i) => i.number.toString().length));
627
+ const longestLabels = Math.max(...currentIssues.map((i) => i.labels.map((x) => x.name).join(", ").length));
628
+ lineMaxLength = lineMaxLength > 130 ? 130 : lineMaxLength;
629
+ const descriptionLength = lineMaxLength - longestNumber - longestLabels - 15;
630
+ const choices = currentIssues.map(
631
+ (i) => {
632
+ const labelText = i.labels.map((l) => printWithHexColor({ msg: l.name, hex: l.color })).join(", ");
633
+ const labelTextNoColor = i.labels.map((l) => l.name).join(", ");
634
+ const labelStartpad = longestLabels - labelTextNoColor.length;
635
+ return {
636
+ title: i.number.toString(),
637
+ value: i.number,
638
+ description: `${limitText(i.title, descriptionLength).padEnd(descriptionLength)} ${"".padStart(labelStartpad)}${labelText}`
639
+ // pads to make sure the labels are aligned, no diea why padStart doesn't work on labelText
640
+ };
641
+ }
642
+ );
643
+ choices.push({ title: "Cancel", value: "cancel" });
644
+ const fzf = new Fzf(choices, {
645
+ selector: (item) => `${item.value} ${item.description}`,
646
+ casing: "case-insensitive"
647
+ });
648
+ try {
649
+ const result = await prompts({
650
+ name: "issueNumber",
651
+ message: "Github issue to create branch for:",
652
+ type: "autocomplete",
653
+ choices,
654
+ async suggest(input, choices2) {
655
+ consola.log(input);
656
+ const results = fzf.find(input);
657
+ return results.map((r) => choices2.find((c) => c.value === r.item.value));
658
+ }
659
+ }, {
660
+ // when escape is used just cancel
661
+ onCancel: () => {
662
+ consola.info(colors.dim("Canceled"));
663
+ process.exit(0);
664
+ }
665
+ });
666
+ if (result.issueNumber === "cancel") {
667
+ consola.log(colors.dim("Canceled"));
668
+ process.exit(0);
669
+ }
670
+ const selectedIssue = currentIssues.find((i) => i.number === result.issueNumber);
671
+ consola.log(`Selected issue ${colors.green(selectedIssue.number)} - ${colors.green(selectedIssue.title)}`);
672
+ const branchName = createBranchNameFromIssue(selectedIssue);
673
+ createBranch({ branchName });
674
+ } catch (e) {
675
+ process.exit(1);
676
+ }
677
+ }
678
+
679
+ const CINCINNATI_LATITUDE = 39.1031;
680
+ const CINCINNATI_LONGITUDE = -84.512;
681
+ const WEATHER_DESCRIPTIONS = {
682
+ 0: { description: "Clear sky", emoji: "\u2600\uFE0F" },
683
+ 1: { description: "Mainly clear", emoji: "\u{1F324}\uFE0F" },
684
+ 2: { description: "Partly cloudy", emoji: "\u26C5" },
685
+ 3: { description: "Overcast", emoji: "\u2601\uFE0F" },
686
+ 45: { description: "Fog", emoji: "\u{1F32B}\uFE0F" },
687
+ 48: { description: "Depositing rime fog", emoji: "\u{1F32B}\uFE0F" },
688
+ 51: { description: "Light drizzle", emoji: "\u{1F326}\uFE0F" },
689
+ 53: { description: "Moderate drizzle", emoji: "\u{1F326}\uFE0F" },
690
+ 55: { description: "Dense drizzle", emoji: "\u{1F327}\uFE0F" },
691
+ 56: { description: "Light freezing drizzle", emoji: "\u{1F328}\uFE0F" },
692
+ 57: { description: "Dense freezing drizzle", emoji: "\u{1F328}\uFE0F" },
693
+ 61: { description: "Slight rain", emoji: "\u{1F327}\uFE0F" },
694
+ 63: { description: "Moderate rain", emoji: "\u{1F327}\uFE0F" },
695
+ 65: { description: "Heavy rain", emoji: "\u{1F327}\uFE0F" },
696
+ 66: { description: "Light freezing rain", emoji: "\u{1F328}\uFE0F" },
697
+ 67: { description: "Heavy freezing rain", emoji: "\u{1F328}\uFE0F" },
698
+ 71: { description: "Slight snow fall", emoji: "\u{1F328}\uFE0F" },
699
+ 73: { description: "Moderate snow fall", emoji: "\u2744\uFE0F" },
700
+ 75: { description: "Heavy snow fall", emoji: "\u2744\uFE0F" },
701
+ 77: { description: "Snow grains", emoji: "\u{1F328}\uFE0F" },
702
+ 80: { description: "Slight rain showers", emoji: "\u{1F326}\uFE0F" },
703
+ 81: { description: "Moderate rain showers", emoji: "\u{1F327}\uFE0F" },
704
+ 82: { description: "Violent rain showers", emoji: "\u26C8\uFE0F" },
705
+ 85: { description: "Slight snow showers", emoji: "\u{1F328}\uFE0F" },
706
+ 86: { description: "Heavy snow showers", emoji: "\u2744\uFE0F" },
707
+ 95: { description: "Thunderstorm", emoji: "\u26C8\uFE0F" },
708
+ 96: { description: "Thunderstorm with slight hail", emoji: "\u26C8\uFE0F" },
709
+ 99: { description: "Thunderstorm with heavy hail", emoji: "\u26C8\uFE0F" }
710
+ };
711
+ function getWindDirection(degrees) {
712
+ const directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
713
+ const index = Math.round(degrees / 22.5) % 16;
714
+ return directions[index];
715
+ }
716
+ function formatWeatherData(data) {
717
+ const { current_weather: weather, current_weather_units: units } = data;
718
+ const weatherInfo = WEATHER_DESCRIPTIONS[weather.weathercode] || {
719
+ description: "Unknown",
720
+ emoji: "\u2753"
721
+ };
722
+ const windDirection = getWindDirection(weather.winddirection);
723
+ const timeOfDay = weather.is_day ? "Day" : "Night";
724
+ consola.info("\u{1F30E} Current Weather in Cincinnati, OH");
725
+ consola.log("");
726
+ consola.log(`${weatherInfo.emoji} ${weatherInfo.description}`);
727
+ consola.log(`\u{1F321}\uFE0F Temperature: ${weather.temperature}${units.temperature}`);
728
+ consola.log(`\u{1F4A8} Wind: ${weather.windspeed} ${units.windspeed} ${windDirection}`);
729
+ consola.log(`\u{1F550} Time: ${new Date(weather.time).toLocaleTimeString()} (${timeOfDay})`);
730
+ }
731
+ async function weatherCommand(context) {
732
+ try {
733
+ consola.start("Fetching current weather for Cincinnati...");
734
+ const url = `https://api.open-meteo.com/v1/forecast?latitude=${CINCINNATI_LATITUDE}&longitude=${CINCINNATI_LONGITUDE}&current_weather=true&temperature_unit=fahrenheit&windspeed_unit=mph&timezone=America/New_York`;
735
+ const response = await fetch(url);
736
+ if (!response.ok) {
737
+ throw new Error(`Weather API request failed: ${response.status} ${response.statusText}`);
738
+ }
739
+ const data = await response.json();
740
+ if (!data.current_weather) {
741
+ throw new Error("No weather data received from API");
742
+ }
743
+ consola.success("Weather data retrieved successfully!");
744
+ consola.log("");
745
+ formatWeatherData(data);
746
+ } catch (error) {
747
+ consola.error("Failed to fetch weather data:", error instanceof Error ? error.message : String(error));
748
+ process.exit(1);
749
+ }
750
+ }
751
+
511
752
  async function executeCommand(parsedArgs, context) {
512
753
  switch (parsedArgs.command) {
513
754
  case "journal": {
514
- const { subcommand, title } = parsedArgs.args;
515
- let journalType;
516
- switch (subcommand) {
517
- case "daily-notes":
518
- case "today":
519
- journalType = JOURNAL_TYPES.DAILY_NOTES;
520
- break;
521
- case "meeting":
522
- journalType = JOURNAL_TYPES.MEETING;
523
- break;
524
- case "note":
525
- journalType = JOURNAL_TYPES.NOTE;
526
- break;
527
- default:
528
- throw new Error(`Unknown journal subcommand: ${subcommand}`);
529
- }
530
- await createJournalFile({ context, type: journalType, title: title || "" });
755
+ await createJournalFile({ context, type: parsedArgs.args.jouralType, title: parsedArgs.args.title || "" });
531
756
  break;
532
757
  }
533
758
  case "git-commit":
534
759
  await gitCommitCommand(context, parsedArgs.args.message);
535
760
  break;
761
+ case "gh-branch":
762
+ await githubBranchCommand(context, parsedArgs.args);
763
+ break;
536
764
  case "config":
537
765
  await configCommand(context);
538
766
  break;
767
+ case "weather":
768
+ await weatherCommand();
769
+ break;
539
770
  default:
540
771
  throw new Error(`Unknown command: ${parsedArgs.command}`);
541
772
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
3
  "type": "module",
4
- "version": "0.0.16",
4
+ "version": "0.0.18",
5
5
  "description": "One off quality of life scripts that I use on a daily basis.",
6
6
  "author": "Chris Towles <Chris.Towles.Dev@gmail.com>",
7
7
  "license": "MIT",
@@ -37,11 +37,13 @@
37
37
  "@anthropic-ai/sdk": "^0.56.0",
38
38
  "@clack/prompts": "^0.11.0",
39
39
  "comment-json": "^4.2.5",
40
+ "consola": "^3.4.2",
40
41
  "fzf": "^0.5.2",
41
42
  "luxon": "^3.7.1",
42
43
  "neverthrow": "^8.2.0",
43
44
  "prompts": "^2.4.2",
44
- "consola": "^3.4.2",
45
+ "strip-ansi": "^7.1.0",
46
+ "tinyexec": "^0.3.2",
45
47
  "yargs": "^17.7.2",
46
48
  "zod": "^4.0.5"
47
49
  },
@@ -55,7 +57,6 @@
55
57
  "lint-staged": "^15.5.2",
56
58
  "oxlint": "^1.7.0",
57
59
  "simple-git-hooks": "^2.13.0",
58
- "tinyexec": "^0.3.2",
59
60
  "tsx": "^4.20.3",
60
61
  "typescript": "^5.8.3",
61
62
  "unbuild": "^3.5.0",