Create CLI working scaffolding

This commit is contained in:
2025-01-17 16:10:04 +01:00
parent f3b68dca33
commit a270ee4ae5
10 changed files with 136 additions and 20 deletions

35
package-lock.json generated
View File

@@ -9,8 +9,10 @@
"version": "0.0.1", "version": "0.0.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"commander": "^13.0.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"peggy": "^4.2.0" "peggy": "^4.2.0",
"yaml": "^2.7.0"
}, },
"bin": { "bin": {
"obsidian-tasks-reminder": "dist/index.js" "obsidian-tasks-reminder": "dist/index.js"
@@ -566,13 +568,12 @@
} }
}, },
"node_modules/commander": { "node_modules/commander": {
"version": "9.5.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": ">=18"
} }
}, },
"node_modules/dayjs": { "node_modules/dayjs": {
@@ -1053,6 +1054,16 @@
"tsc-alias": "dist/bin/index.js" "tsc-alias": "dist/bin/index.js"
} }
}, },
"node_modules/tsc-alias/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/tsx": { "node_modules/tsx": {
"version": "4.19.2", "version": "4.19.2",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
@@ -1093,6 +1104,18 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
},
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
} }
} }
} }

View File

@@ -20,7 +20,9 @@
"typescript": "^5.6.3" "typescript": "^5.6.3"
}, },
"dependencies": { "dependencies": {
"commander": "^13.0.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"peggy": "^4.2.0" "peggy": "^4.2.0",
"yaml": "^2.7.0"
} }
} }

View File

@@ -7,5 +7,5 @@ buildNpmPackage {
pname = "obsidian-tasks-reminder"; pname = "obsidian-tasks-reminder";
version = "0.0.1"; version = "0.0.1";
src = ./.; src = ./.;
npmDepsHash = "sha256-d8uZWYmroWoju976WXnCaYX+0uTLK/tc6hS/WgEHv/o="; npmDepsHash = "sha256-ofPAFnHbW+M0uQN/OXDLK+PQ3ls6AtD58/AOmSxn754=";
} }

47
src/cli/index.ts Normal file
View File

@@ -0,0 +1,47 @@
import { program } from "commander";
import { CLIOptions } from "../types/cli";
import { loadConfig } from "../config";
import { notify, scan } from "../runner";
const getOptions = () => program
.name("obsidian-tasks-reminder")
.version("0.0.1")
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
.option("-x, --set <arg>", "overrides the config option for this specific run (arg: <key>=<name>, i.e. backend.ntfy.enable=false", (v: string, prev: string[]) => prev.concat([v]), [])
.option("-s, --scan", "scans new tasks for future notifications and generates the database")
.option("-n, --notify", "reads the generated database and triggers notifications if any")
.parse()
.opts<CLIOptions>();
export const run = async () => {
const options = getOptions();
const config = loadConfig(options.config);
for (const override of options.set) {
const [path, value] = override.split("=");
const segments = path.trim().split(".")
let current: any = config;
segments.map(s => s.trim()).forEach((segment: string, idx) => {
if(!current[segment]) {
current[segment] = {};
}
if(idx === segments.length - 1) {
current[segment] = JSON.parse(value.trim());
}
current = current[segment];
});
}
if (options.scan) {
scan(config);
}
if (options.notify) {
notify(config);
}
}

8
src/config/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import fs from "fs";
import yaml from "yaml";
import { Config } from "../types/config";
export function loadConfig(file: string): Config {
const text = fs.readFileSync(file, 'utf-8');
return yaml.parse(text) as Config;
}

View File

@@ -5,7 +5,7 @@ import { Notification, NotificationDatabase } from "../types/notification";
/** /**
* Applies the mapper for each task from list, groups them by time and dumps the data into JSON formatted file. * Applies the mapper for each task from list, groups them by time and dumps the data into JSON formatted file.
*/ */
export function dumpDatabase(file: string, tasks: Task[], mapper: (task: Task) => Notification[]) { export function dumpDatabase(file: string, tasks: Task[], mapper: string) {
const data = serializeDatabase(tasks, mapper); const data = serializeDatabase(tasks, mapper);
fs.writeFileSync(file, data); fs.writeFileSync(file, data);
} }
@@ -13,8 +13,10 @@ export function dumpDatabase(file: string, tasks: Task[], mapper: (task: Task) =
/** /**
* Applies the mapper for each task from list, groups them by time and serializes into JSON format. * Applies the mapper for each task from list, groups them by time and serializes into JSON format.
*/ */
export function serializeDatabase(tasks: Task[], mapper: (task: Task) => Notification[]): string { export function serializeDatabase(tasks: Task[], mapper: string): string {
const output = tasks.flatMap(wrapWithTimeFiller(mapper)).reduce((acc, n) => { const transformer = new Function("$", `return ${mapper}`) as (task: Task) => Notification;
const output = tasks.map(wrapWithTimeFiller(transformer)).reduce((acc, n) => {
if (n.time) { if (n.time) {
(acc[n.time] = (acc[n.time] || [])).push(n); (acc[n.time] = (acc[n.time] || [])).push(n);
}; };
@@ -25,10 +27,13 @@ export function serializeDatabase(tasks: Task[], mapper: (task: Task) => Notific
return JSON.stringify(output); return JSON.stringify(output);
} }
function wrapWithTimeFiller(mapper: (task: Task) => Notification[]): (task: Task) => Notification[] { function wrapWithTimeFiller(mapper: (task: Task) => Notification): (task: Task) => Notification {
return (task: Task) => mapper(task) return (task: Task) => {
.map(notification => ({ const notification = mapper(task);
return {
...notification, ...notification,
time: task.reminder ?? notification.time, time: task.reminder ?? notification.time,
})); }
};
} }

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { run } from "./cli";
run();

15
src/runner/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import { loadTasks } from "../loader";
import { dumpDatabase } from "../database/serializer";
import { loadDatabase } from "../database/deserializer";
import { remind } from "../backend";
import { Config } from "../types/config";
export async function scan(config: Config) {
const tasks = await loadTasks(config.sources, config.query);
dumpDatabase(config.databaseFile, tasks, config.mapper);
}
export async function notify(config: Config) {
const db = loadDatabase(config.databaseFile);
remind(config, db);
}

6
src/types/cli.ts Normal file
View File

@@ -0,0 +1,6 @@
export type CLIOptions = {
config: string;
scan: boolean;
notify: boolean;
set: string[];
};

View File

@@ -1,12 +1,17 @@
export type Config = {
sources: string[];
query: string;
mapper: string;
databaseFile: string;
backend: Record<string, unknown>;
};
export type BackendSettings = { export type BackendSettings = {
enable?: boolean; enable?: boolean;
}; };
export type SupportedBackends = 'ntfy.sh'; export type SupportedBackends = 'ntfy.sh';
export type BackendConfig = { export type BackendConfig = {
backend: SupportedBackends; backend: SupportedBackends;
settings: BackendSettings; settings: BackendSettings;
} }
export type Config = {
backend: Record<string, unknown>;
};