@towles/tool 0.0.11 → 0.0.13
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 +15 -10
- package/dist/index.mjs +326 -208
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -70,11 +70,16 @@ if that works, then you need to add the pnpm global bin directory to your PATH.
|
|
|
70
70
|
- [@anthropic-ai/claude-code](https://github.com/anthropic-ai/claude-code) - A library for interacting with the Claude code
|
|
71
71
|
- [zod](https://github.com/colinhacks/zod) - TypeScript-first schema validation
|
|
72
72
|
- [Consola](https://github.com/unjs/consola) console wrapper and colors
|
|
73
|
-
- [c12](https://github.com/unjs/c12) configuration loader and utilities
|
|
73
|
+
- ~~[c12](https://github.com/unjs/c12) configuration loader and utilities~~
|
|
74
|
+
- referted stayed to json config
|
|
74
75
|
- [rolldown-vite](https://voidzero.dev/posts/announcing-rolldown-vite) - A Vite plugin for rolling down your code
|
|
75
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.~~
|
|
76
77
|
- [prompts](https://github.com/terkelg/prompts) - A library for creating beautiful command-line prompts, with fuzzy search and other features.
|
|
77
|
-
- [
|
|
78
|
+
- [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
|
+
- ~~[ink](https://github.com/vadimdemedes/ink) - React for interactive command-line apps~~
|
|
80
|
+
- wanted hotkey support and more complex UI but this was overkill for this project.
|
|
81
|
+
- [publint](https://publint.dev/)
|
|
82
|
+
- [e18e.dev](https://e18e.dev/guide/resources.html)
|
|
78
83
|
|
|
79
84
|
## Document verbose and debug options
|
|
80
85
|
|
|
@@ -98,13 +103,13 @@ I'm using a lot of inspiration from [Anthony Fu](https://github.com/antfu) for t
|
|
|
98
103
|
|
|
99
104
|
<!-- Badges -->
|
|
100
105
|
|
|
101
|
-
[npm-version-src]: https://img.shields.io/npm/v/
|
|
102
|
-
[npm-version-href]: https://npmjs.com/package/
|
|
103
|
-
[npm-downloads-src]: https://img.shields.io/npm/dm/
|
|
104
|
-
[npm-downloads-href]: https://npmjs.com/package/
|
|
105
|
-
[bundle-src]: https://img.shields.io/bundlephobia/minzip/
|
|
106
|
-
[bundle-href]: https://bundlephobia.com/result?p
|
|
107
|
-
[license-src]: https://img.shields.io/github/license/
|
|
106
|
+
[npm-version-src]: https://img.shields.io/npm/v/@towles/tool?style=flat&colorA=080f12&colorB=1fa669
|
|
107
|
+
[npm-version-href]: https://npmjs.com/package/@towles/tool
|
|
108
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@towles/tool?style=flat&colorA=080f12&colorB=1fa669
|
|
109
|
+
[npm-downloads-href]: https://npmjs.com/package/@towles/tool
|
|
110
|
+
[bundle-src]: https://img.shields.io/bundlephobia/minzip/@towles/tool?style=flat&colorA=080f12&colorB=1fa669&label=minzip
|
|
111
|
+
[bundle-href]: https://bundlephobia.com/result?p=@towles/tool
|
|
112
|
+
[license-src]: https://img.shields.io/github/license/ChrisTowles/towles-tool.svg?style=flat&colorA=080f12&colorB=1fa669
|
|
108
113
|
[license-href]: https://github.com/ChrisTowles/towles-tool/blob/main/LICENSE.md
|
|
109
114
|
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
|
|
110
|
-
[jsdocs-href]: https://www.jsdocs.io/package/
|
|
115
|
+
[jsdocs-href]: https://www.jsdocs.io/package/@towles/tool
|
package/dist/index.mjs
CHANGED
|
@@ -1,116 +1,121 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
import
|
|
2
|
+
import process$1 from 'node:process';
|
|
3
|
+
import yargs from 'yargs';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
5
|
import consola from 'consola';
|
|
5
|
-
import { colors } from 'consola/utils';
|
|
6
6
|
import prompts from 'prompts';
|
|
7
7
|
import { execSync, exec } from 'node:child_process';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
8
9
|
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
10
|
import * as path from 'node:path';
|
|
10
11
|
import path__default from 'node:path';
|
|
11
|
-
import
|
|
12
|
+
import { promisify } from 'node:util';
|
|
13
|
+
import { colors } from 'consola/utils';
|
|
14
|
+
import { DateTime } from 'luxon';
|
|
15
|
+
import { z } from 'zod/v4';
|
|
12
16
|
import { homedir } from 'node:os';
|
|
13
|
-
import
|
|
14
|
-
import { updateConfig } from 'c12/update';
|
|
17
|
+
import stripJsonComments from 'strip-json-comments';
|
|
15
18
|
|
|
16
|
-
const version = "0.0.
|
|
19
|
+
const version = "0.0.13";
|
|
17
20
|
|
|
18
21
|
function execCommand(cmd, cwd) {
|
|
19
22
|
return execSync(cmd, { encoding: "utf8", cwd }).trim();
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
async function gitCommitCommand(
|
|
23
|
-
let statusOutput;
|
|
25
|
+
async function gitCommitCommand(context, messageArgs) {
|
|
24
26
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const untrackedFiles = lines.filter((line) => line.startsWith("??"));
|
|
34
|
-
if (lines.length === 0) {
|
|
35
|
-
consola.info("Working tree clean - nothing to commit");
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
consola.info("Git status:");
|
|
39
|
-
if (stagedFiles.length > 0) {
|
|
40
|
-
consola.info(colors.green("Staged files:"));
|
|
41
|
-
stagedFiles.forEach((file) => consola.info(` ${colors.green(file)}`));
|
|
42
|
-
}
|
|
43
|
-
if (unstagedFiles.length > 0) {
|
|
44
|
-
consola.info(colors.yellow("Modified files (not staged):"));
|
|
45
|
-
unstagedFiles.forEach((file) => consola.info(` ${colors.yellow(file)}`));
|
|
46
|
-
}
|
|
47
|
-
if (untrackedFiles.length > 0) {
|
|
48
|
-
consola.info(colors.red("Untracked files:"));
|
|
49
|
-
untrackedFiles.forEach((file) => consola.info(` ${colors.red(file)}`));
|
|
50
|
-
}
|
|
51
|
-
if (stagedFiles.length === 0) {
|
|
52
|
-
if (unstagedFiles.length > 0 || untrackedFiles.length > 0) {
|
|
53
|
-
const { shouldStage } = await prompts({
|
|
27
|
+
const status = await getGitStatus(context.cwd);
|
|
28
|
+
if (!status || status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0) {
|
|
29
|
+
consola.success("\u2713 Working tree clean - nothing to commit");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
displayGitStatus(status);
|
|
33
|
+
if (status.staged.length === 0) {
|
|
34
|
+
const shouldStage = await prompts({
|
|
54
35
|
type: "confirm",
|
|
55
|
-
name: "
|
|
56
|
-
message: "No files are staged.
|
|
36
|
+
name: "stage",
|
|
37
|
+
message: "No files are staged. Add all modified and untracked files?",
|
|
57
38
|
initial: true
|
|
58
39
|
});
|
|
59
|
-
if (shouldStage) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
name: "addAll",
|
|
63
|
-
message: "Add all modified and untracked files?",
|
|
64
|
-
initial: true
|
|
65
|
-
});
|
|
66
|
-
if (addAll) {
|
|
67
|
-
try {
|
|
68
|
-
execCommand("git add .", config.cwd);
|
|
69
|
-
consola.success("All files staged successfully");
|
|
70
|
-
} catch (error) {
|
|
71
|
-
consola.error("Failed to stage files");
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
consola.info(`Use ${colors.cyan("git add <file>...")} to stage specific files, then run the commit command again`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
consola.error(`No staged changes found to commit. Use ${colors.cyan("git add <file>...")} to stage changes before committing.`);
|
|
80
|
-
process.exit(1);
|
|
40
|
+
if (!shouldStage.stage) {
|
|
41
|
+
consola.info("Cancelled");
|
|
42
|
+
return;
|
|
81
43
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
44
|
+
consola.start("Staging files...");
|
|
45
|
+
await execCommand("git add .", context.cwd);
|
|
46
|
+
consola.success("Files staged");
|
|
85
47
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
message
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return;
|
|
48
|
+
let commitMessage = messageArgs?.join(" ") || "";
|
|
49
|
+
if (!commitMessage) {
|
|
50
|
+
const response = await prompts({
|
|
51
|
+
type: "text",
|
|
52
|
+
name: "message",
|
|
53
|
+
message: "Enter commit message:",
|
|
54
|
+
validate: (value) => value.trim().length > 0 || "Commit message cannot be empty"
|
|
55
|
+
});
|
|
56
|
+
if (!response.message) {
|
|
57
|
+
consola.info("Cancelled");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
commitMessage = response.message;
|
|
100
61
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
consola.info(`Running: ${colors.cyan(commandWithArgs)}`);
|
|
105
|
-
try {
|
|
106
|
-
execCommand(commandWithArgs, config.cwd);
|
|
107
|
-
consola.success("Commit created successfully!");
|
|
62
|
+
consola.start("Creating commit...");
|
|
63
|
+
await execCommand(`git commit -m "${commitMessage}"`, context.cwd);
|
|
64
|
+
consola.success("\u2713 Commit created successfully!");
|
|
108
65
|
} catch (error) {
|
|
109
|
-
consola.error("
|
|
110
|
-
consola.error(error);
|
|
66
|
+
consola.error("Git commit failed:", error instanceof Error ? error.message : String(error));
|
|
111
67
|
process.exit(1);
|
|
112
68
|
}
|
|
113
69
|
}
|
|
70
|
+
async function getGitStatus(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const output = await execCommand("git status --porcelain", cwd);
|
|
73
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
74
|
+
const staged = [];
|
|
75
|
+
const unstaged = [];
|
|
76
|
+
const untracked = [];
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
const status = line.substring(0, 2);
|
|
79
|
+
const file = line.substring(3);
|
|
80
|
+
if (status[0] !== " " && status[0] !== "?") {
|
|
81
|
+
staged.push(file);
|
|
82
|
+
}
|
|
83
|
+
if (status[1] !== " " && status[1] !== "?") {
|
|
84
|
+
unstaged.push(file);
|
|
85
|
+
}
|
|
86
|
+
if (status === "??") {
|
|
87
|
+
untracked.push(file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { staged, unstaged, untracked };
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function displayGitStatus(status) {
|
|
96
|
+
consola.info("Current git status:");
|
|
97
|
+
if (status.staged.length > 0) {
|
|
98
|
+
consola.success(`\u2713 Staged files (${status.staged.length}):`);
|
|
99
|
+
status.staged.slice(0, 5).forEach((file) => consola.log(` ${file}`));
|
|
100
|
+
if (status.staged.length > 5) {
|
|
101
|
+
consola.log(` ... and ${status.staged.length - 5} more`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (status.unstaged.length > 0) {
|
|
105
|
+
consola.warn(`M Modified files (${status.unstaged.length}):`);
|
|
106
|
+
status.unstaged.slice(0, 3).forEach((file) => consola.log(` ${file}`));
|
|
107
|
+
if (status.unstaged.length > 3) {
|
|
108
|
+
consola.log(` ... and ${status.unstaged.length - 3} more`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (status.untracked.length > 0) {
|
|
112
|
+
consola.error(`? Untracked files (${status.untracked.length}):`);
|
|
113
|
+
status.untracked.slice(0, 3).forEach((file) => consola.log(` ${file}`));
|
|
114
|
+
if (status.untracked.length > 3) {
|
|
115
|
+
consola.log(` ... and ${status.untracked.length - 3} more`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
114
119
|
|
|
115
120
|
function getMondayOfWeek(date) {
|
|
116
121
|
const newDate = new Date(date);
|
|
@@ -214,44 +219,71 @@ async function openInEditor({ editor, filePath }) {
|
|
|
214
219
|
consola.warn(`Could not open in editor : '${editor}'. Modify your editor in the config: examples include 'code', 'code-insiders', etc...`, ex);
|
|
215
220
|
}
|
|
216
221
|
}
|
|
217
|
-
function
|
|
222
|
+
function resolvePathTemplate(template, title, date) {
|
|
223
|
+
const dateTime = DateTime.fromJSDate(date, { zone: "utc" });
|
|
224
|
+
return template.replace(/\{([^}]+)\}/g, (match, token) => {
|
|
225
|
+
try {
|
|
226
|
+
if (token === "title") {
|
|
227
|
+
return title.toLowerCase().replace(/\s+/g, "-");
|
|
228
|
+
}
|
|
229
|
+
const result = dateTime.toFormat(token);
|
|
230
|
+
const isLikelyInvalid = token.includes("invalid") || result.length > 20 || // Very long results are likely garbage
|
|
231
|
+
result.length > token.length * 2 && /\d{10,}/.test(result) || // Contains very long numbers
|
|
232
|
+
result.includes("UTC");
|
|
233
|
+
if (isLikelyInvalid) {
|
|
234
|
+
consola.warn(`Invalid date format token: ${token}`);
|
|
235
|
+
return match;
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
consola.warn(`Invalid date format token: ${token}`);
|
|
240
|
+
return match;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function generateJournalFileInfoByType({ journalSettings, date = /* @__PURE__ */ new Date(), type = JOURNAL_TYPES.DAILY_NOTES, title }) {
|
|
218
245
|
const currentDate = new Date(date);
|
|
219
|
-
|
|
246
|
+
let templatePath = "";
|
|
247
|
+
let mondayDate = getMondayOfWeek(currentDate);
|
|
220
248
|
switch (type) {
|
|
221
249
|
case JOURNAL_TYPES.DAILY_NOTES: {
|
|
222
250
|
const monday = getMondayOfWeek(currentDate);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
templatePath = journalSettings.dailyPathTemplate;
|
|
252
|
+
mondayDate = monday;
|
|
253
|
+
break;
|
|
226
254
|
}
|
|
227
255
|
case JOURNAL_TYPES.MEETING: {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const fileName = `${dateStr}-${timeStr}-meeting${titleSlug}.md`;
|
|
232
|
-
const pathPrefix = [year, "meetings"];
|
|
233
|
-
return { pathPrefix, fileName, mondayDate: currentDate };
|
|
256
|
+
templatePath = journalSettings.meetingPathTemplate;
|
|
257
|
+
mondayDate = currentDate;
|
|
258
|
+
break;
|
|
234
259
|
}
|
|
235
260
|
case JOURNAL_TYPES.NOTE: {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const fileName = `${dateStr}-${timeStr}-note${titleSlug}.md`;
|
|
240
|
-
const pathPrefix = [year, "notes"];
|
|
241
|
-
return { pathPrefix, fileName, mondayDate: currentDate };
|
|
261
|
+
templatePath = journalSettings.notePathTemplate;
|
|
262
|
+
mondayDate = currentDate;
|
|
263
|
+
break;
|
|
242
264
|
}
|
|
243
265
|
default:
|
|
244
266
|
throw new Error(`Unknown journal type: ${type}`);
|
|
245
267
|
}
|
|
268
|
+
const resolvedPath = resolvePathTemplate(templatePath, title, currentDate);
|
|
269
|
+
return {
|
|
270
|
+
currentDate,
|
|
271
|
+
fullPath: resolvedPath,
|
|
272
|
+
mondayDate
|
|
273
|
+
};
|
|
246
274
|
}
|
|
247
|
-
async function createJournalFile({
|
|
275
|
+
async function createJournalFile({ context, type, title }) {
|
|
248
276
|
try {
|
|
249
277
|
const currentDate = /* @__PURE__ */ new Date();
|
|
250
|
-
const fileInfo = generateJournalFileInfoByType({
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
278
|
+
const fileInfo = generateJournalFileInfoByType({
|
|
279
|
+
journalSettings: context.settingsFile.settings.journalSettings,
|
|
280
|
+
date: currentDate,
|
|
281
|
+
type,
|
|
282
|
+
title
|
|
283
|
+
});
|
|
284
|
+
ensureDirectoryExists(path__default.dirname(fileInfo.fullPath));
|
|
285
|
+
if (existsSync(fileInfo.fullPath)) {
|
|
286
|
+
consola.info(`Opening existing ${type} file: ${colors.cyan(fileInfo.fullPath)}`);
|
|
255
287
|
} else {
|
|
256
288
|
let content;
|
|
257
289
|
switch (type) {
|
|
@@ -267,122 +299,208 @@ async function createJournalFile({ userConfig, type, title }) {
|
|
|
267
299
|
default:
|
|
268
300
|
throw new Error(`Unknown journal type: ${type}`);
|
|
269
301
|
}
|
|
270
|
-
|
|
271
|
-
|
|
302
|
+
consola.info(`Creating new ${type} file: ${colors.cyan(fileInfo.fullPath)}`);
|
|
303
|
+
writeFileSync(fileInfo.fullPath, content, "utf8");
|
|
272
304
|
}
|
|
273
|
-
await openInEditor({ editor:
|
|
305
|
+
await openInEditor({ editor: context.settingsFile.settings.preferredEditor, filePath: fileInfo.fullPath });
|
|
274
306
|
} catch (error) {
|
|
275
307
|
consola.warn(`Error creating ${type} file:`, error);
|
|
276
|
-
process.exit(1);
|
|
308
|
+
process$1.exit(1);
|
|
277
309
|
}
|
|
278
310
|
}
|
|
279
311
|
|
|
280
|
-
|
|
312
|
+
async function loadTowlesToolContext({
|
|
313
|
+
cwd,
|
|
314
|
+
settingsFile,
|
|
315
|
+
debug = false
|
|
316
|
+
}) {
|
|
317
|
+
return {
|
|
318
|
+
cwd,
|
|
319
|
+
settingsFile,
|
|
320
|
+
args: [],
|
|
321
|
+
// TODO: Load args from yargs
|
|
322
|
+
debug
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const AppInfo = {
|
|
281
327
|
toolName: "towles-tool"
|
|
282
328
|
};
|
|
283
329
|
|
|
284
|
-
function
|
|
285
|
-
consola.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
})
|
|
330
|
+
async function configCommand(context) {
|
|
331
|
+
consola.info("Configuration");
|
|
332
|
+
consola.log("");
|
|
333
|
+
consola.info(`Settings File: ${context.settingsFile.path}`);
|
|
334
|
+
consola.log("");
|
|
335
|
+
consola.warn("User Config:");
|
|
336
|
+
consola.log(` Daily Path Template: ${context.settingsFile.settings.journalSettings.dailyPathTemplate}`);
|
|
337
|
+
consola.log(` Meeting Path Template: ${context.settingsFile.settings.journalSettings.meetingPathTemplate}`);
|
|
338
|
+
consola.log(` Note Path Template: ${context.settingsFile.settings.journalSettings.notePathTemplate}`);
|
|
339
|
+
consola.log(` Editor: ${context.settingsFile.settings.preferredEditor}`);
|
|
340
|
+
consola.log("");
|
|
341
|
+
consola.warn("Working Directory:");
|
|
342
|
+
consola.log(` ${context.cwd}`);
|
|
291
343
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
344
|
+
|
|
345
|
+
const USER_SETTINGS_DIR = path.join(homedir(), ".config", AppInfo.toolName);
|
|
346
|
+
const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, `${AppInfo.toolName}.settings.json`);
|
|
347
|
+
const JournalSettingsSchema = z.object({
|
|
348
|
+
// https://moment.github.io/luxon/#/formatting?id=table-of-tokens
|
|
349
|
+
dailyPathTemplate: z.string().default(path.join(homedir(), "journal", "{yyyy}/{MM}/daily-notes/{yyyy}-{MM}-{dd}-daily-notes.md")),
|
|
350
|
+
meetingPathTemplate: z.string().default(path.join(homedir(), "journal", "{yyyy}/{MM}/meetings/{yyyy}-{MM}-{dd}-{title}.md")),
|
|
351
|
+
notePathTemplate: z.string().default(path.join(homedir(), "journal", "{yyyy}/{MM}/notes/{yyyy}-{MM}-{dd}-{title}.md"))
|
|
352
|
+
});
|
|
353
|
+
const UserSettingsSchema = z.object({
|
|
354
|
+
preferredEditor: z.string().default("code"),
|
|
355
|
+
journalSettings: JournalSettingsSchema
|
|
356
|
+
});
|
|
357
|
+
class LoadedSettings {
|
|
358
|
+
constructor(settingsFile) {
|
|
359
|
+
this.settingsFile = settingsFile;
|
|
295
360
|
}
|
|
361
|
+
settingsFile;
|
|
296
362
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
363
|
+
function createSettingsFile() {
|
|
364
|
+
const defaultSettings = UserSettingsSchema.parse({
|
|
365
|
+
// its odd, but but we have to get default value from each schema object, if we don't it failes.
|
|
366
|
+
// https://github.com/colinhacks/zod/discussions/1953
|
|
367
|
+
journalSettings: JournalSettingsSchema.parse({})
|
|
368
|
+
});
|
|
369
|
+
const settingsFile = {
|
|
370
|
+
path: USER_SETTINGS_PATH,
|
|
371
|
+
settings: defaultSettings
|
|
302
372
|
};
|
|
373
|
+
saveSettings(settingsFile);
|
|
374
|
+
consola.success(`Created settings file: ${USER_SETTINGS_PATH}`);
|
|
375
|
+
return defaultSettings;
|
|
303
376
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const defaults = getDefaultUserConfig();
|
|
310
|
-
const defaultConfigFolder = path.join(homedir(), ".config", constants.toolName);
|
|
311
|
-
const updateResult = await updateConfig({
|
|
312
|
-
cwd: defaultConfigFolder,
|
|
313
|
-
configFile: `${constants.toolName}.config`,
|
|
314
|
-
createExtension: ".ts",
|
|
315
|
-
async onCreate({ configFile: configFile2 }) {
|
|
316
|
-
const shallCreate = await consola.prompt(
|
|
317
|
-
`Do you want to initialize a new config in ${colors.cyan(configFile2)}?`,
|
|
318
|
-
{
|
|
319
|
-
type: "confirm",
|
|
320
|
-
default: true
|
|
321
|
-
}
|
|
322
|
-
);
|
|
323
|
-
if (shallCreate !== true) {
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
return `
|
|
327
|
-
// Default configuration for Towles Tool
|
|
328
|
-
// You can customize these values to fit your needs
|
|
329
|
-
// cwd: null means it will use the current working directory
|
|
330
|
-
export default ${JSON.stringify(defaults, null, 2)};
|
|
331
|
-
`;
|
|
332
|
-
},
|
|
333
|
-
async onUpdate(config) {
|
|
334
|
-
return config;
|
|
377
|
+
function saveSettings(settingsFile) {
|
|
378
|
+
try {
|
|
379
|
+
const dirPath = path.dirname(settingsFile.path);
|
|
380
|
+
if (!fs.existsSync(dirPath)) {
|
|
381
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
335
382
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
383
|
+
fs.writeFileSync(
|
|
384
|
+
settingsFile.path,
|
|
385
|
+
JSON.stringify(settingsFile.settings, null, 2),
|
|
386
|
+
"utf-8"
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
consola.error("Error saving user settings file:", error);
|
|
343
390
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
391
|
+
}
|
|
392
|
+
async function loadSettings() {
|
|
393
|
+
let userSettings = null;
|
|
394
|
+
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
|
395
|
+
const userContent = fs.readFileSync(USER_SETTINGS_PATH, "utf-8");
|
|
396
|
+
const parsedUserSettings = JSON.parse(stripJsonComments(userContent));
|
|
397
|
+
userSettings = UserSettingsSchema.parse(parsedUserSettings);
|
|
398
|
+
if (JSON.stringify(parsedUserSettings) !== JSON.stringify(userSettings)) {
|
|
399
|
+
consola.warn(`Settings file ${USER_SETTINGS_PATH} has been updated with default values.`);
|
|
400
|
+
const tempSettingsFile = {
|
|
401
|
+
path: USER_SETTINGS_PATH,
|
|
402
|
+
settings: userSettings
|
|
403
|
+
};
|
|
404
|
+
saveSettings(tempSettingsFile);
|
|
353
405
|
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
406
|
+
} else {
|
|
407
|
+
const confirmed = await consola.prompt(`Settings file not found. Create ${colors.cyan(USER_SETTINGS_PATH)}?`, {
|
|
408
|
+
type: "confirm"
|
|
409
|
+
});
|
|
410
|
+
if (!confirmed) {
|
|
411
|
+
throw new Error(`Settings file not found and user chose not to create it.`);
|
|
412
|
+
}
|
|
413
|
+
userSettings = createSettingsFile();
|
|
414
|
+
}
|
|
415
|
+
return new LoadedSettings(
|
|
416
|
+
{
|
|
417
|
+
path: USER_SETTINGS_PATH,
|
|
418
|
+
settings: userSettings
|
|
419
|
+
}
|
|
420
|
+
);
|
|
361
421
|
}
|
|
362
422
|
|
|
363
423
|
async function main() {
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
await createJournalFile({ userConfig: config.userConfig, type: JOURNAL_TYPES.DAILY_NOTES });
|
|
371
|
-
});
|
|
372
|
-
journalCmd.command("meeting [title]").description("Structured meeting notes with agenda and action items").action(async (title) => {
|
|
373
|
-
await createJournalFile({ userConfig: config.userConfig, type: JOURNAL_TYPES.MEETING, title });
|
|
374
|
-
});
|
|
375
|
-
journalCmd.command("note [title]").description("General-purpose notes with structured sections").action(async (title) => {
|
|
376
|
-
await createJournalFile({ userConfig: config.userConfig, type: JOURNAL_TYPES.NOTE, title });
|
|
377
|
-
});
|
|
378
|
-
program.command("git-commit [message...]").alias("gc").description("Git commit command with optional message").action(async (message) => {
|
|
379
|
-
await gitCommitCommand(config, message);
|
|
380
|
-
});
|
|
381
|
-
program.command("config").description("set or show configuration file.").action(async () => {
|
|
382
|
-
consola$1.log(colors.green("Showing configuration..."));
|
|
383
|
-
consola$1.log("Settings File:", config.configFile);
|
|
384
|
-
printJson(config);
|
|
424
|
+
const settings = await loadSettings();
|
|
425
|
+
const context = await loadTowlesToolContext({
|
|
426
|
+
cwd: process$1.cwd(),
|
|
427
|
+
settingsFile: settings.settingsFile,
|
|
428
|
+
debug: true
|
|
429
|
+
// later can be set to false in production or when not debugging
|
|
385
430
|
});
|
|
386
|
-
|
|
431
|
+
consola.info(`Using configuration from ${settings.settingsFile.path}`);
|
|
432
|
+
const parser = yargs(hideBin(process$1.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());
|
|
433
|
+
parser.command(
|
|
434
|
+
["journal", "j"],
|
|
435
|
+
"quickly create md files from templates files like daily-notes, meeting, notes, etc.",
|
|
436
|
+
(yargs2) => {
|
|
437
|
+
return yargs2.command(
|
|
438
|
+
["daily-notes", "today"],
|
|
439
|
+
"Weekly files with daily sections for ongoing work and notes",
|
|
440
|
+
{},
|
|
441
|
+
async () => {
|
|
442
|
+
await createJournalFile({ context, type: JOURNAL_TYPES.DAILY_NOTES, title: "" });
|
|
443
|
+
}
|
|
444
|
+
).command(
|
|
445
|
+
["meeting [title]", "m"],
|
|
446
|
+
"Structured meeting notes with agenda and action items",
|
|
447
|
+
(yargs3) => {
|
|
448
|
+
return yargs3.positional("title", {
|
|
449
|
+
type: "string",
|
|
450
|
+
describe: "Meeting title"
|
|
451
|
+
});
|
|
452
|
+
},
|
|
453
|
+
async (argv) => {
|
|
454
|
+
await createJournalFile({ context, type: JOURNAL_TYPES.MEETING, title: argv.title || "" });
|
|
455
|
+
}
|
|
456
|
+
).command(
|
|
457
|
+
["note [title]", "n"],
|
|
458
|
+
"General-purpose notes with structured sections",
|
|
459
|
+
(yargs3) => {
|
|
460
|
+
return yargs3.positional("title", {
|
|
461
|
+
type: "string",
|
|
462
|
+
describe: "Note title"
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
async (argv) => {
|
|
466
|
+
await createJournalFile({ context, type: JOURNAL_TYPES.NOTE, title: argv.title || "" });
|
|
467
|
+
}
|
|
468
|
+
).demandCommand(1, "You need to specify a journal subcommand").help();
|
|
469
|
+
},
|
|
470
|
+
() => {
|
|
471
|
+
parser.showHelp();
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
parser.command(
|
|
475
|
+
["git-commit [message...]", "gc"],
|
|
476
|
+
"Git commit command with optional message",
|
|
477
|
+
(yargs2) => {
|
|
478
|
+
return yargs2.positional("message", {
|
|
479
|
+
type: "string",
|
|
480
|
+
array: true,
|
|
481
|
+
describe: "Commit message words"
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
async (argv) => {
|
|
485
|
+
await gitCommitCommand(context, argv.message || []);
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
parser.command(
|
|
489
|
+
["config", "cfg"],
|
|
490
|
+
"set or show configuration file.",
|
|
491
|
+
{},
|
|
492
|
+
async () => {
|
|
493
|
+
await configCommand(context);
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
await parser.parse();
|
|
387
497
|
}
|
|
388
|
-
|
|
498
|
+
main().catch((error) => {
|
|
499
|
+
consola.error("An unexpected critical error occurred:");
|
|
500
|
+
if (error instanceof Error) {
|
|
501
|
+
consola.error(error.stack);
|
|
502
|
+
} else {
|
|
503
|
+
consola.error(String(error));
|
|
504
|
+
}
|
|
505
|
+
process$1.exit(1);
|
|
506
|
+
});
|
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.13",
|
|
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",
|
|
@@ -35,22 +35,21 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@anthropic-ai/claude-code": "^1.0.51",
|
|
37
37
|
"@anthropic-ai/sdk": "^0.56.0",
|
|
38
|
-
"@clack/prompts": "^0.11.0",
|
|
39
|
-
"c12": "^3.0.4",
|
|
40
|
-
"changelogen": "^0.6.2",
|
|
41
|
-
"commander": "^14.0.0",
|
|
42
38
|
"consola": "^3.4.2",
|
|
43
39
|
"fzf": "^0.5.2",
|
|
44
|
-
"
|
|
40
|
+
"luxon": "^3.7.1",
|
|
45
41
|
"neverthrow": "^8.2.0",
|
|
46
42
|
"prompts": "^2.4.2",
|
|
43
|
+
"strip-json-comments": "^5.0.2",
|
|
44
|
+
"yargs": "^17.7.2",
|
|
47
45
|
"zod": "^4.0.5"
|
|
48
46
|
},
|
|
49
47
|
"devDependencies": {
|
|
50
48
|
"@antfu/ni": "^25.0.0",
|
|
51
|
-
"@
|
|
49
|
+
"@types/luxon": "^3.6.2",
|
|
52
50
|
"@types/node": "^22.16.3",
|
|
53
51
|
"@types/prompts": "^2.4.9",
|
|
52
|
+
"@types/yargs": "^17.0.32",
|
|
54
53
|
"bumpp": "^10.2.0",
|
|
55
54
|
"lint-staged": "^15.5.2",
|
|
56
55
|
"oxlint": "^1.7.0",
|
|
@@ -79,11 +78,12 @@
|
|
|
79
78
|
"lint": "oxlint",
|
|
80
79
|
"lint:fix": "oxlint --fix",
|
|
81
80
|
"lint:fix_all": "oxlint --fix .",
|
|
81
|
+
"lint:package": "pnpm dlx publint --fix && pnpm dlx knip",
|
|
82
82
|
"release:local": "bumpp && pnpm publish --no-git-checks -r --access public",
|
|
83
83
|
"release": "bumpp && echo \"github action will run and publish to npm\"",
|
|
84
84
|
"start": "tsx src/index.ts",
|
|
85
85
|
"test": "vitest --run",
|
|
86
|
-
"test:watch": "vitest",
|
|
86
|
+
"test:watch": "CI=DisableCallingClaude vitest --watch",
|
|
87
87
|
"typecheck": "tsc --noEmit"
|
|
88
88
|
}
|
|
89
89
|
}
|