Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
/node_modules
/main.js
/lib/*.js
*.js.map
/dist
4 changes: 0 additions & 4 deletions .npmignore

This file was deleted.

71 changes: 71 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# AGENTS.md

## Repo structure

- `main.ts` — minimal module wrapper, re-exports `lib/cli`
- `lib/cli.ts` — main `Cli` class: entrypoint for all commands
- `lib/config.ts` — thin `configstore` wrapper keyed to package name (`@codehivetx/mourt`)
- `lib/plugin-taskw.ts` — spawns `task` and returns its exported JSON
- `bin/mourt` — CLI binary (plain JS, not TS); uses `minimist` with `Cli.opts`
- `package.json` declares `"bin": { "mourt": "bin/mourt" }`

TypeScript compiles to `dist/`, mirroring the source tree (`dist/main.js`,
`dist/lib/*.js`). Source stays in `lib/`, compiled JS in `dist/lib/`.

## Commands

```
npm run build # tsc — compiles to dist/
npm test # placeholder only: exits 1 because no tests exist yet
```

To run the CLI directly from the repo (requires a prior `npm run build`):

```
node bin/mourt -D dir=/path/to/data -m 'message'
node bin/mourt -l
node bin/mourt -D 'plugin.taskw.command=dock-warrior'
node bin/mourt -P dir # get current dir
```

## Gotchas

- **No tests exist yet.** `npm test` exits 1 by design (`echo "Error: no test specified" && exit 1`); nothing is broken.
- `bin/mourt` requires `../dist/lib/cli` — the *compiled* output, not `lib/cli.ts`. Build before running.
- `lib/config.ts` requires `../../package.json` because the compiled file lives at `dist/lib/`. Moving between source and compiled dirs means different relative depths.
- `lib/config.ts` returns a lazy `new Configstore(name)` — calling code must invoke the function: `require('./config')()`
- `require('../package.json')` resolves fine because `tsconfig` uses CommonJS. Don't "fix" it to an ESM import.
- `config.get('dir')` returns `undefined` until `-D dir=...` is set. `Cli.requireBaseDir()` throws a helpful error rather than letting `fs.opendir(undefined)` fail opaquely.
- Data is stored under the config dir set by `-D dir=...`, with date-subdirectories (`YYYY/MM/DD/`)
- `date` on disk is an **ISO string** (`date.toISOString()`), not a `Date` object — `JSON.stringify` would have coerced it anyway, so this is explicit. Read it back with `new Date(blob.date)`.
- `taskwarrior` is **optional** on disk — `doMessage` deletes the key when there are no active tasks.
- When listing (`-l`) with `-p project`, entries **without** a `taskwarrior` field are excluded (they can't be matched). Without `-p`, all entries are listed.
- Build output (`dist/`) is git-ignored and shipped via `package.json` `files`, which ships `dist/**/*.js` + `dist/**/*.d.ts` and deliberately excludes `.ts` sources and `.map` files. There is no `.npmignore` — the `files` field supersedes it.
- `main.ts` uses `module.exports`/`require` deliberately (CommonJS), matching the rest of the codebase and the `exports` map.

## Todo / Planned Fixes

### Critical
- [x] **`lib/plugin-taskw.ts`** crash risk — guard the `githubbody`/`annotations` deletes with `Array.isArray(j) && j.length > 0 && j[0]`
- [x] **`lib/plugin-taskw.ts`** — accumulate stderr into `err` so it reaches the rejection message (previously only printed, so `err` was always empty)
- [x] **`lib/cli.ts`** — `baseDir` was `undefined` when `dir` was never set, crashing `fs.opendir(undefined)` / `path.join`. Added `Cli.requireBaseDir()` used by `getList()` and `getDir()`
- [x] **`tsconfig.json`** — added `rootDir: "."` / `outDir: "dist"` (plus `declaration: true` for the `types` entries)

### Medium
- [x] **`package.json`** — added `exports`, `types`, and `files`; moved compiled entry to `dist/main.js`
- [x] **`bin/mourt`** — updated to require `../dist/lib/cli`; comment now states compiled-vs-source explicitly
- [x] **`lib/cli.ts`** — flattened the nested `if (proj && taskwarrior)` / `else` filter into two guard clauses (behavior unchanged)
- [x] **`lib/cli.ts`** — removed the unused `readFile` import
- [x] **`lib/cli.ts`** — `date` now explicitly stored as an ISO string; `MourtBlob` marks `taskwarrior`/`shortDate`/`localDate` optional to match what's actually on disk
- [x] **`lib/plugin-taskw.ts`** — removed the no-op `fetchTask` → `fetchData` wrapper; the exported function is now the implementation

### Low
- [x] **`AGENTS.md`** — clarified that `npm test` means "no tests yet", not "tests are broken"
- [x] **`tsconfig.json`** — dropped redundant `"module": "commonjs"` (already set by `@tsconfig/node16`)
- [x] **`.npmignore`** — deleted; superseded by the `files` field in `package.json`

### Remaining / Not Done
- [ ] **No test suite.** `npm test` still exits 1. Everything above was verified manually (CLI smoke tests + `npm pack` install test), but there is no regression coverage. Adding a runner (e.g. `node:test`) is the natural next step.
- [ ] **`main.ts` / codebase stays CommonJS.** Works fine and matches the `exports` map; converting to ESM would be a larger, riskier change with no current benefit.
- [ ] **`getList()` reads every JSON file** to filter by project — `// TODO: filter by date for speed` remains unaddressed.
- [ ] **`doMessage` has a `// TODO: override`** — unclear if the intent is to allow date/time override for backfilling entries.
6 changes: 3 additions & 3 deletions bin/mourt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
* SPDX-License-Identifier: Apache-2.0
*/

const Cli = require('../lib/cli');
// Plain JS on purpose: this is the published entrypoint, so it must not need
// a build step to be executable. It loads the *compiled* Cli from dist/.
const Cli = require('../dist/lib/cli');
const process = require('process');
const argv = require('minimist')(process.argv.slice(2), Cli.opts);

// This minimal entrypoint is kept as non-typescript…for now?

const cli = new Cli();
cli.run(argv)
.then(() => { },
Expand Down
45 changes: 29 additions & 16 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

const config : any = require('./config')();
const fs = require('fs').promises;
const { readFile } = require('fs');
const path = require('path');
const { fetchTask } = require('./plugin-taskw');
const chalk = require('chalk');
Expand All @@ -18,15 +17,19 @@ interface MourtBlob {
/**
* Short date such as 2022-03-04
*/
shortDate: string,
shortDate?: string,
/**
* TW blob
* TW blob. Absent when the entry was written with no active task.
*/
taskwarrior: any,
taskwarrior?: any,
/**
* String message
*/
message: string,
/**
* Date.toString() of the local time, for human-readable reads.
*/
localDate?: string,
};
class Cli {

Expand Down Expand Up @@ -74,7 +77,8 @@ class Cli {
const filep = path.join(dir, fn);
const taskwarrior = await fetchTask(config);
const o = {
date,
// Stored as an ISO string; MourtBlob.date is read back as a string.
date: date.toISOString(),
localDate: date.toString(),
taskwarrior,
message
Expand All @@ -96,20 +100,18 @@ class Cli {
async getList(argv: any): Promise<MourtBlob[]> {
// TODO: filter by date for speed
const proj = argv.project || null;
const allFiles = (await Cli.getAllJsonFiles(this.baseDir))
const baseDir = this.requireBaseDir();
const allFiles = (await Cli.getAllJsonFiles(baseDir))
.sort(); // sorted will give us ascending time
// .filter … 
const out: MourtBlob[] = [];
for (const f of allFiles) {
const d: MourtBlob = await Cli.readMourtFile(f);
const { taskwarrior } = d;
if (proj && taskwarrior) {
if (taskwarrior[0].project !== proj) {
// skip non matched
continue;
}
} else {
if (proj) continue; // not shown.
// With a project filter, entries that carry no taskwarrior data
// can't be matched, so they are excluded.
if (proj) {
if (!taskwarrior) continue;
if (taskwarrior[0].project !== proj) continue;
}
out.push(d);
}
Expand Down Expand Up @@ -201,15 +203,26 @@ class Cli {

async getDir(date: Date): Promise<string> {
const datePath = Cli.dateToDir(date);
const p = path.join(this.baseDir, datePath);
const p = path.join(this.requireBaseDir(), datePath);
await fs.mkdir(p, { recursive: true });
return p;
}

get baseDir() {
get baseDir(): string | undefined {
return config.get('dir');
}

/**
* baseDir, or an error if `dir` was never configured.
*/
requireBaseDir(): string {
const d = this.baseDir;
if (!d) {
throw Error('no data dir configured; set one with: mourt -D dir=/path/to/data');
}
return d;
}

static dateToDir(date: Date): string {
function pad(n: number) {
return String(n).padStart(2, '0');
Expand Down
3 changes: 2 additions & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Configstore = require('configstore');
const {name} = require('../package.json');
// Compiled output lives in dist/lib/, so the package.json sits two levels up.
const { name } = require('../../package.json');

module.exports = () => new Configstore(name);
26 changes: 15 additions & 11 deletions lib/plugin-taskw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

const {spawn} = require('child_process');

function fetchData(config: any) {
/**
* Run taskwarrior and return its exported tasks.
* Rejects if taskwarrior exits non-zero.
*/
module.exports.fetchTask = function fetchTask(config: any): Promise<any[]> {

function getTaskCommand(): string {
return config.get('plugin.taskw.command') || 'task';
Expand All @@ -21,23 +25,23 @@ function fetchData(config: any) {
let msg = '';
let err = '';
taskw.stdout.on('data', (data : any) => (msg = msg + data.toString('utf-8')));
taskw.stderr.on('data', (data : any) => console.error(data.toString('utf-8')));
taskw.stderr.on('data', (data : any) => {
const str = data.toString('utf-8');
err += str;
console.error(str);
});
taskw.on('close', (code : number) => {
if((code !== 0)) {
return reject(Error('Taskw: ' + (err || 'non zero exit')));
}
const j = JSON.parse(msg);
const j: any[] = JSON.parse(msg);
// this one is too big
delete (j || [])[0].githubbody;
delete (j || [])[0].annotations;
if (j && Array.isArray(j) && j.length > 0 && j[0]) {
delete j[0].githubbody;
delete j[0].annotations;
}

return resolve(j);
});
});
}

module.exports.fetchTask = async function fetchTask(config: any) {
const data = await fetchData(config);
// console.dir(data);
return data;
};
23 changes: 21 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@
"name": "@codehivetx/mourt",
"version": "1.2.2-0",
"description": "Extensible Diary/Journal Platform",
"main": "main.js",
"bin": "bin/mourt",
"main": "dist/main.js",
"types": "dist/main.d.ts",
"bin": {
"mourt": "bin/mourt"
},
"exports": {
".": {
"types": "./dist/main.d.ts",
"default": "./dist/main.js"
},
"./lib/cli": {
"types": "./dist/lib/cli.d.ts",
"default": "./dist/lib/cli.js"
},
"./package.json": "./package.json"
},
"files": [
"bin",
"dist/**/*.js",
"dist/**/*.d.ts"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"rootDir": ".",
"outDir": "dist",
"declaration": true,
"sourceMap": true
},
"exclude": [
Expand Down
Loading