@towles/tool 0.0.15 → 0.0.17
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 +1 -0
- package/dist/index.mjs +200 -32
- 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,
|
|
@@ -38,7 +41,7 @@ const USER_SETTINGS_DIR = path.join(homedir(), ".config", AppInfo.toolName);
|
|
|
38
41
|
const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, `${AppInfo.toolName}.settings.json`);
|
|
39
42
|
const JournalSettingsSchema = z.object({
|
|
40
43
|
// https://moment.github.io/luxon/#/formatting?id=table-of-tokens
|
|
41
|
-
dailyPathTemplate: z.string().default(path.join(homedir(), "journal", "{yyyy}/{MM}/daily-notes/{yyyy}-{MM}-{dd}-daily-notes.md")),
|
|
44
|
+
dailyPathTemplate: z.string().default(path.join(homedir(), "journal", "{monday:yyyy}/{monday:MM}/daily-notes/{monday:yyyy}-{monday:MM}-{monday:dd}-daily-notes.md")),
|
|
42
45
|
meetingPathTemplate: z.string().default(path.join(homedir(), "journal", "{yyyy}/{MM}/meetings/{yyyy}-{MM}-{dd}-{title}.md")),
|
|
43
46
|
notePathTemplate: z.string().default(path.join(homedir(), "journal", "{yyyy}/{MM}/notes/{yyyy}-{MM}-{dd}-{title}.md"))
|
|
44
47
|
});
|
|
@@ -52,12 +55,23 @@ class LoadedSettings {
|
|
|
52
55
|
}
|
|
53
56
|
settingsFile;
|
|
54
57
|
}
|
|
58
|
+
function createDefaultSettings() {
|
|
59
|
+
return UserSettingsSchema.parse({
|
|
60
|
+
// NOTE: yes its odd zod can't use defaults from objects nested but it appears to be the case.
|
|
61
|
+
journalSettings: JournalSettingsSchema.parse({})
|
|
62
|
+
});
|
|
63
|
+
}
|
|
55
64
|
function createSettingsFile() {
|
|
56
|
-
let userSettings =
|
|
65
|
+
let userSettings = createDefaultSettings();
|
|
57
66
|
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
|
58
67
|
const userContent = fs.readFileSync(USER_SETTINGS_PATH, "utf-8");
|
|
59
68
|
const parsedUserSettings = commentJson.parse(userContent);
|
|
60
69
|
userSettings = UserSettingsSchema.parse(parsedUserSettings);
|
|
70
|
+
} else {
|
|
71
|
+
saveSettings({
|
|
72
|
+
path: USER_SETTINGS_PATH,
|
|
73
|
+
settings: userSettings
|
|
74
|
+
});
|
|
61
75
|
}
|
|
62
76
|
return userSettings;
|
|
63
77
|
}
|
|
@@ -107,8 +121,13 @@ async function loadSettings() {
|
|
|
107
121
|
);
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
const version = "0.0.
|
|
124
|
+
const version = "0.0.17";
|
|
111
125
|
|
|
126
|
+
const JOURNAL_TYPES = {
|
|
127
|
+
DAILY_NOTES: "daily-notes",
|
|
128
|
+
MEETING: "meeting",
|
|
129
|
+
NOTE: "note"
|
|
130
|
+
};
|
|
112
131
|
async function parseArguments(argv) {
|
|
113
132
|
let parsedResult = null;
|
|
114
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());
|
|
@@ -121,7 +140,7 @@ async function parseArguments(argv) {
|
|
|
121
140
|
"Weekly files with daily sections for ongoing work and notes",
|
|
122
141
|
{},
|
|
123
142
|
(argv2) => {
|
|
124
|
-
parsedResult = { command: "journal", args: {
|
|
143
|
+
parsedResult = { command: "journal", args: { jouralType: "daily-notes", title: "" } };
|
|
125
144
|
}
|
|
126
145
|
).command(
|
|
127
146
|
["meeting [title]", "m"],
|
|
@@ -133,7 +152,7 @@ async function parseArguments(argv) {
|
|
|
133
152
|
}
|
|
134
153
|
},
|
|
135
154
|
(argv2) => {
|
|
136
|
-
parsedResult = { command: "journal", args: {
|
|
155
|
+
parsedResult = { command: "journal", args: { jouralType: "meeting", title: argv2.title || "" } };
|
|
137
156
|
}
|
|
138
157
|
).command(
|
|
139
158
|
["note [title]", "n"],
|
|
@@ -145,7 +164,7 @@ async function parseArguments(argv) {
|
|
|
145
164
|
}
|
|
146
165
|
},
|
|
147
166
|
(argv2) => {
|
|
148
|
-
parsedResult = { command: "journal", args: {
|
|
167
|
+
parsedResult = { command: "journal", args: { jouralType: "note", title: argv2.title || "" } };
|
|
149
168
|
}
|
|
150
169
|
).demandCommand(1, "You need to specify a journal subcommand").help();
|
|
151
170
|
},
|
|
@@ -167,6 +186,20 @@ async function parseArguments(argv) {
|
|
|
167
186
|
parsedResult = { command: "git-commit", args: { message: argv2.message } };
|
|
168
187
|
}
|
|
169
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
|
+
);
|
|
170
203
|
parser.command(
|
|
171
204
|
["config", "cfg"],
|
|
172
205
|
"set or show configuration file.",
|
|
@@ -310,11 +343,6 @@ function formatDate(date) {
|
|
|
310
343
|
}
|
|
311
344
|
|
|
312
345
|
const execAsync = promisify(exec);
|
|
313
|
-
const JOURNAL_TYPES = {
|
|
314
|
-
DAILY_NOTES: "daily-notes",
|
|
315
|
-
MEETING: "meeting",
|
|
316
|
-
NOTE: "note"
|
|
317
|
-
};
|
|
318
346
|
function ensureDirectoryExists(folderPath) {
|
|
319
347
|
if (!existsSync(folderPath)) {
|
|
320
348
|
consola.info(`Creating journal directory: ${colors.cyan(folderPath)}`);
|
|
@@ -382,13 +410,18 @@ async function openInEditor({ editor, filePath }) {
|
|
|
382
410
|
consola.warn(`Could not open in editor : '${editor}'. Modify your editor in the config: examples include 'code', 'code-insiders', etc...`, ex);
|
|
383
411
|
}
|
|
384
412
|
}
|
|
385
|
-
function resolvePathTemplate(template, title, date) {
|
|
413
|
+
function resolvePathTemplate(template, title, date, mondayDate) {
|
|
386
414
|
const dateTime = DateTime.fromJSDate(date, { zone: "utc" });
|
|
387
415
|
return template.replace(/\{([^}]+)\}/g, (match, token) => {
|
|
388
416
|
try {
|
|
389
417
|
if (token === "title") {
|
|
390
418
|
return title.toLowerCase().replace(/\s+/g, "-");
|
|
391
419
|
}
|
|
420
|
+
if (token.startsWith("monday:")) {
|
|
421
|
+
const mondayToken = token.substring(7);
|
|
422
|
+
const mondayDateTime = DateTime.fromJSDate(mondayDate, { zone: "utc" });
|
|
423
|
+
return mondayDateTime.toFormat(mondayToken);
|
|
424
|
+
}
|
|
392
425
|
const result = dateTime.toFormat(token);
|
|
393
426
|
const isLikelyInvalid = token.includes("invalid") || result.length > 20 || // Very long results are likely garbage
|
|
394
427
|
result.length > token.length * 2 && /\d{10,}/.test(result) || // Contains very long numbers
|
|
@@ -404,7 +437,7 @@ function resolvePathTemplate(template, title, date) {
|
|
|
404
437
|
}
|
|
405
438
|
});
|
|
406
439
|
}
|
|
407
|
-
function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */ new Date(), type
|
|
440
|
+
function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */ new Date(), type, title }) {
|
|
408
441
|
const currentDate = new Date(date);
|
|
409
442
|
let templatePath = "";
|
|
410
443
|
let mondayDate = getMondayOfWeek(currentDate);
|
|
@@ -426,9 +459,9 @@ function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */
|
|
|
426
459
|
break;
|
|
427
460
|
}
|
|
428
461
|
default:
|
|
429
|
-
throw new Error(`Unknown
|
|
462
|
+
throw new Error(`Unknown JournalType: ${type}`);
|
|
430
463
|
}
|
|
431
|
-
const resolvedPath = resolvePathTemplate(templatePath, title, currentDate);
|
|
464
|
+
const resolvedPath = resolvePathTemplate(templatePath, title, currentDate, mondayDate);
|
|
432
465
|
return {
|
|
433
466
|
currentDate,
|
|
434
467
|
fullPath: resolvedPath,
|
|
@@ -437,6 +470,11 @@ function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */
|
|
|
437
470
|
}
|
|
438
471
|
async function createJournalFile({ context, type, title }) {
|
|
439
472
|
try {
|
|
473
|
+
if (title.trim().length === 0 && (type === JOURNAL_TYPES.MEETING || type === JOURNAL_TYPES.NOTE)) {
|
|
474
|
+
title = await consola.prompt(`Enter ${type} title:`, {
|
|
475
|
+
type: "text"
|
|
476
|
+
});
|
|
477
|
+
}
|
|
440
478
|
const currentDate = /* @__PURE__ */ new Date();
|
|
441
479
|
const fileInfo = generateJournalFileInfoByType({
|
|
442
480
|
journalSettings: context.settingsFile.settings.journalSettings,
|
|
@@ -487,31 +525,161 @@ async function configCommand(context) {
|
|
|
487
525
|
consola.log(` ${context.cwd}`);
|
|
488
526
|
}
|
|
489
527
|
|
|
528
|
+
const isGithubCliInstalled = async () => {
|
|
529
|
+
try {
|
|
530
|
+
const proc = await exec$1(`gh`, ["--version"]);
|
|
531
|
+
return proc.stdout.indexOf("https://github.com/cli/cli") > 0;
|
|
532
|
+
} catch (e) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const getIssues = async ({ assignedToMe, cwd }) => {
|
|
537
|
+
let issues = [];
|
|
538
|
+
const flags = [
|
|
539
|
+
"issue",
|
|
540
|
+
"list",
|
|
541
|
+
"--json",
|
|
542
|
+
"labels,number,title,state"
|
|
543
|
+
];
|
|
544
|
+
if (assignedToMe) {
|
|
545
|
+
flags.push("--assignee");
|
|
546
|
+
flags.push("@me");
|
|
547
|
+
}
|
|
548
|
+
const result = await exec$1(`gh`, flags);
|
|
549
|
+
const striped = stripAnsi(result.stdout);
|
|
550
|
+
issues = JSON.parse(striped);
|
|
551
|
+
return issues;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const createBranch = async ({ branchName }) => {
|
|
555
|
+
const result = await exec$1(`git`, ["checkout", "-b", branchName]);
|
|
556
|
+
return result.stdout;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const getTerminalColumns = () => process.stdout?.columns || 80;
|
|
560
|
+
const limitText = (text, maxWidth) => {
|
|
561
|
+
if (text.length <= maxWidth)
|
|
562
|
+
return text;
|
|
563
|
+
return `${text.slice(0, maxWidth - 1)}${colors.dim("\u2026")}`;
|
|
564
|
+
};
|
|
565
|
+
function hexToRgb(hex) {
|
|
566
|
+
const cleanHex = hex.replace("#", "");
|
|
567
|
+
const r = Number.parseInt(cleanHex.slice(0, 2), 16);
|
|
568
|
+
const g = Number.parseInt(cleanHex.slice(2, 4), 16);
|
|
569
|
+
const b = Number.parseInt(cleanHex.slice(4, 6), 16);
|
|
570
|
+
return { r, g, b };
|
|
571
|
+
}
|
|
572
|
+
function printWithHexColor({ msg, hex }) {
|
|
573
|
+
const colorWithHex = hex.startsWith("#") ? hex : `#${hex}`;
|
|
574
|
+
const { r, g, b } = hexToRgb(colorWithHex);
|
|
575
|
+
const colorStart = `\x1B[38;2;${r};${g};${b}m`;
|
|
576
|
+
const colorEnd = "\x1B[0m";
|
|
577
|
+
return `${colorStart}${msg}${colorEnd}`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const checkPreqrequisites = async () => {
|
|
581
|
+
const cliInstalled = await isGithubCliInstalled();
|
|
582
|
+
if (!cliInstalled) {
|
|
583
|
+
consola.log("Github CLI not installed");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
function customTrimEnd(str, charsToTrim) {
|
|
588
|
+
let i = str.length - 1;
|
|
589
|
+
while (i >= 0 && charsToTrim.includes(str[i])) {
|
|
590
|
+
i--;
|
|
591
|
+
}
|
|
592
|
+
return str.substring(0, i + 1);
|
|
593
|
+
}
|
|
594
|
+
const createBranchNameFromIssue = (selectedIssue) => {
|
|
595
|
+
let slug = selectedIssue.title.toLowerCase();
|
|
596
|
+
slug = slug.trim();
|
|
597
|
+
slug = slug.replaceAll(" ", "-");
|
|
598
|
+
slug = slug.replace(/[^0-9a-zA-Z_]/g, "-");
|
|
599
|
+
slug = slug.replaceAll("--", "-");
|
|
600
|
+
slug = slug.replaceAll("--", "-");
|
|
601
|
+
slug = slug.replaceAll("--", "-");
|
|
602
|
+
slug = customTrimEnd(slug, ["-"]);
|
|
603
|
+
const branchName = `feature/${selectedIssue.number}-${slug}`;
|
|
604
|
+
return branchName;
|
|
605
|
+
};
|
|
606
|
+
async function githubBranchCommand(context, args) {
|
|
607
|
+
await checkPreqrequisites();
|
|
608
|
+
const assignedToMe = Boolean(args.assignedToMe);
|
|
609
|
+
consola.log("Assigned to me:", assignedToMe);
|
|
610
|
+
const currentIssues = await getIssues({ assignedToMe, cwd: context.cwd });
|
|
611
|
+
if (currentIssues.length === 0) {
|
|
612
|
+
consola.log(colors.yellow("No issues found, check assignments"));
|
|
613
|
+
process.exit(1);
|
|
614
|
+
} else {
|
|
615
|
+
consola.log(colors.green(`${currentIssues.length} Issues found assigned to you`));
|
|
616
|
+
}
|
|
617
|
+
let lineMaxLength = getTerminalColumns();
|
|
618
|
+
const longestNumber = Math.max(...currentIssues.map((i) => i.number.toString().length));
|
|
619
|
+
const longestLabels = Math.max(...currentIssues.map((i) => i.labels.map((x) => x.name).join(", ").length));
|
|
620
|
+
lineMaxLength = lineMaxLength > 130 ? 130 : lineMaxLength;
|
|
621
|
+
const descriptionLength = lineMaxLength - longestNumber - longestLabels - 15;
|
|
622
|
+
const choices = currentIssues.map(
|
|
623
|
+
(i) => {
|
|
624
|
+
const labelText = i.labels.map((l) => printWithHexColor({ msg: l.name, hex: l.color })).join(", ");
|
|
625
|
+
const labelTextNoColor = i.labels.map((l) => l.name).join(", ");
|
|
626
|
+
const labelStartpad = longestLabels - labelTextNoColor.length;
|
|
627
|
+
return {
|
|
628
|
+
title: i.number.toString(),
|
|
629
|
+
value: i.number,
|
|
630
|
+
description: `${limitText(i.title, descriptionLength).padEnd(descriptionLength)} ${"".padStart(labelStartpad)}${labelText}`
|
|
631
|
+
// pads to make sure the labels are aligned, no diea why padStart doesn't work on labelText
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
choices.push({ title: "Cancel", value: "cancel" });
|
|
636
|
+
const fzf = new Fzf(choices, {
|
|
637
|
+
selector: (item) => `${item.value} ${item.description}`,
|
|
638
|
+
casing: "case-insensitive"
|
|
639
|
+
});
|
|
640
|
+
try {
|
|
641
|
+
const result = await prompts({
|
|
642
|
+
name: "issueNumber",
|
|
643
|
+
message: "Github issue to create branch for:",
|
|
644
|
+
type: "autocomplete",
|
|
645
|
+
choices,
|
|
646
|
+
async suggest(input, choices2) {
|
|
647
|
+
consola.log(input);
|
|
648
|
+
const results = fzf.find(input);
|
|
649
|
+
return results.map((r) => choices2.find((c) => c.value === r.item.value));
|
|
650
|
+
}
|
|
651
|
+
}, {
|
|
652
|
+
// when escape is used just cancel
|
|
653
|
+
onCancel: () => {
|
|
654
|
+
consola.info(colors.dim("Canceled"));
|
|
655
|
+
process.exit(0);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
if (result.issueNumber === "cancel") {
|
|
659
|
+
consola.log(colors.dim("Canceled"));
|
|
660
|
+
process.exit(0);
|
|
661
|
+
}
|
|
662
|
+
const selectedIssue = currentIssues.find((i) => i.number === result.issueNumber);
|
|
663
|
+
consola.log(`Selected issue ${colors.green(selectedIssue.number)} - ${colors.green(selectedIssue.title)}`);
|
|
664
|
+
const branchName = createBranchNameFromIssue(selectedIssue);
|
|
665
|
+
createBranch({ branchName });
|
|
666
|
+
} catch (e) {
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
490
671
|
async function executeCommand(parsedArgs, context) {
|
|
491
672
|
switch (parsedArgs.command) {
|
|
492
673
|
case "journal": {
|
|
493
|
-
|
|
494
|
-
let journalType;
|
|
495
|
-
switch (subcommand) {
|
|
496
|
-
case "daily-notes":
|
|
497
|
-
case "today":
|
|
498
|
-
journalType = JOURNAL_TYPES.DAILY_NOTES;
|
|
499
|
-
break;
|
|
500
|
-
case "meeting":
|
|
501
|
-
journalType = JOURNAL_TYPES.MEETING;
|
|
502
|
-
break;
|
|
503
|
-
case "note":
|
|
504
|
-
journalType = JOURNAL_TYPES.NOTE;
|
|
505
|
-
break;
|
|
506
|
-
default:
|
|
507
|
-
throw new Error(`Unknown journal subcommand: ${subcommand}`);
|
|
508
|
-
}
|
|
509
|
-
await createJournalFile({ context, type: journalType, title: title || "" });
|
|
674
|
+
await createJournalFile({ context, type: parsedArgs.args.jouralType, title: parsedArgs.args.title || "" });
|
|
510
675
|
break;
|
|
511
676
|
}
|
|
512
677
|
case "git-commit":
|
|
513
678
|
await gitCommitCommand(context, parsedArgs.args.message);
|
|
514
679
|
break;
|
|
680
|
+
case "gh-branch":
|
|
681
|
+
await githubBranchCommand(context, parsedArgs.args);
|
|
682
|
+
break;
|
|
515
683
|
case "config":
|
|
516
684
|
await configCommand(context);
|
|
517
685
|
break;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towles/tool",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.17",
|
|
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
|
-
"
|
|
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",
|