project-logbook 0.3.3 → 0.3.4
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/dist/commands/build.js +32 -75
- package/dist/commands/init.js +11 -11
- package/dist/commands/lint.js +10 -11
- package/dist/commands/list.js +18 -18
- package/dist/commands/log.js +5 -5
- package/dist/commands/new.js +19 -20
- package/dist/commands/preview.js +9 -8
- package/dist/commands/release.js +6 -6
- package/dist/commands/start.js +26 -21
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +122 -0
- package/dist/commands/steer.js +9 -9
- package/dist/commands/upgrade.js +5 -5
- package/dist/index.js +9 -8
- package/dist/lib/build-helpers.js +28 -50
- package/dist/lib/build-steps.d.ts +20 -0
- package/dist/lib/build-steps.js +57 -0
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +20 -0
- package/dist/lib/entry-id.d.ts +22 -0
- package/dist/lib/entry-id.js +26 -0
- package/dist/lib/entry-paths.d.ts +23 -0
- package/dist/lib/entry-paths.js +55 -0
- package/dist/lib/git-helpers.d.ts +11 -1
- package/dist/lib/git-helpers.js +28 -26
- package/dist/lib/hast-helpers.d.ts +10 -0
- package/dist/lib/hast-helpers.js +22 -0
- package/dist/lib/html-attributes.d.ts +17 -0
- package/dist/lib/html-attributes.js +17 -0
- package/dist/lib/html-escape.d.ts +16 -0
- package/dist/lib/html-escape.js +38 -0
- package/dist/lib/image-helpers.js +26 -33
- package/dist/lib/lint-runner.js +5 -5
- package/dist/lib/markdown-processors.d.ts +18 -0
- package/dist/lib/markdown-processors.js +42 -0
- package/dist/lib/package-version.d.ts +5 -0
- package/dist/lib/package-version.js +16 -0
- package/dist/lib/styles.js +5 -2
- package/dist/lib/template-helpers.d.ts +1 -0
- package/dist/lib/template-helpers.js +35 -24
- package/dist/lib/template-types.d.ts +0 -2
- package/dist/lib/templates.d.ts +8 -2
- package/dist/lib/templates.js +50 -46
- package/dist/lib/theme.d.ts +37 -0
- package/dist/lib/theme.js +50 -0
- package/dist/lib/url-helpers.d.ts +13 -0
- package/dist/lib/url-helpers.js +27 -0
- package/dist/linters/diff-to-narrative.d.ts +6 -0
- package/dist/linters/diff-to-narrative.js +114 -0
- package/dist/linters/index.js +2 -0
- package/dist/templates/CONTRIBUTING.md +12 -3
- package/dist/templates/index.md +6 -6
- package/dist/templates/logbook-client.js +42 -16
- package/dist/templates/styles.css +5 -0
- package/dist/utils/date.d.ts +23 -1
- package/dist/utils/date.js +56 -15
- package/dist/utils/frontmatter.d.ts +26 -0
- package/dist/utils/frontmatter.js +37 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +23 -0
- package/package.json +2 -2
- package/src/templates/CONTRIBUTING.md +12 -3
- package/src/templates/index.md +6 -6
- package/src/templates/logbook-client.js +42 -16
- package/src/templates/styles.css +5 -0
package/dist/utils/date.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Parse and validate a date input (string or Date).
|
|
3
|
+
* Returns the Date object if valid, null if invalid.
|
|
4
|
+
*/
|
|
5
|
+
function parseDate(date) {
|
|
3
6
|
const d = new Date(date);
|
|
4
|
-
|
|
7
|
+
return isNaN(d.getTime()) ? null : d;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Format a date as absolute date and time (server-side).
|
|
11
|
+
* This is the primary format used in HTML data-date attributes.
|
|
12
|
+
* The client-side JavaScript will add the relative component (today, 2 days ago, etc).
|
|
13
|
+
* Format: "26 May 2026, 10:51"
|
|
14
|
+
* @param date - The date to format
|
|
15
|
+
* @returns Formatted date string with absolute date and time only
|
|
16
|
+
*/
|
|
17
|
+
export function formatAbsoluteDate(date) {
|
|
18
|
+
const d = parseDate(date);
|
|
19
|
+
if (!d)
|
|
20
|
+
return 'unknown';
|
|
21
|
+
const day = d.getDate();
|
|
22
|
+
const month = d.toLocaleString('en-US', { month: 'short' });
|
|
23
|
+
const year = d.getFullYear();
|
|
24
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
25
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
26
|
+
return `${day} ${month} ${year}, ${hours}:${minutes}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Format a date as a consistent, human-readable format.
|
|
30
|
+
* Combines relative time (today, yesterday, 2 days ago) with absolute date and time.
|
|
31
|
+
* Format: "today, 26 May 2026, 10:51"
|
|
32
|
+
* This is for server-side rendering when client-side JS is not available.
|
|
33
|
+
* Normally, the client-side JS will update formatAbsoluteDate() output with this format.
|
|
34
|
+
* @param date - The date to format
|
|
35
|
+
* @returns Formatted date string with relative and absolute components
|
|
36
|
+
*/
|
|
37
|
+
export function formatRelativeDate(date) {
|
|
38
|
+
const d = parseDate(date);
|
|
39
|
+
if (!d) {
|
|
5
40
|
return 'unknown date';
|
|
6
41
|
}
|
|
7
42
|
const now = new Date();
|
|
@@ -9,16 +44,15 @@ export function formatRelativeDate(date) {
|
|
|
9
44
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
10
45
|
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
|
11
46
|
const relative = diffDays === 0 ? 'today' : rtf.format(-diffDays, 'day');
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
month: 'long',
|
|
15
|
-
day: 'numeric',
|
|
16
|
-
});
|
|
47
|
+
// Format as "26 May 2026, 10:51"
|
|
48
|
+
const absolute = formatAbsoluteDate(d);
|
|
17
49
|
return `${relative}, ${absolute}`;
|
|
18
50
|
}
|
|
19
51
|
/** Format a date value as YYYY-MM-DD. */
|
|
20
52
|
export function formatIsoDate(value) {
|
|
21
|
-
const d =
|
|
53
|
+
const d = parseDate(value);
|
|
54
|
+
if (!d)
|
|
55
|
+
return 'invalid-date';
|
|
22
56
|
const yyyy = d.getFullYear();
|
|
23
57
|
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
24
58
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
@@ -26,8 +60,8 @@ export function formatIsoDate(value) {
|
|
|
26
60
|
}
|
|
27
61
|
/** Get "Month Year" string for a date (e.g., "May 2026"). */
|
|
28
62
|
export function getMonthYear(date) {
|
|
29
|
-
const d =
|
|
30
|
-
if (
|
|
63
|
+
const d = parseDate(date);
|
|
64
|
+
if (!d)
|
|
31
65
|
return 'Unknown Date';
|
|
32
66
|
return d.toLocaleDateString('en-US', {
|
|
33
67
|
year: 'numeric',
|
|
@@ -36,12 +70,19 @@ export function getMonthYear(date) {
|
|
|
36
70
|
}
|
|
37
71
|
/** Get a short "HH:mmh" string for sorting/display based on dateEnd or dateStart. */
|
|
38
72
|
export function getSortTime(dateStart, dateEnd) {
|
|
39
|
-
const dStart =
|
|
40
|
-
const dEnd = dateEnd ?
|
|
41
|
-
const date = dEnd
|
|
42
|
-
if (
|
|
73
|
+
const dStart = parseDate(dateStart);
|
|
74
|
+
const dEnd = dateEnd ? parseDate(dateEnd) : null;
|
|
75
|
+
const date = dEnd ? dEnd : dStart;
|
|
76
|
+
if (!date)
|
|
43
77
|
return '--:--h';
|
|
44
78
|
const hours = date.getHours().toString().padStart(2, '0');
|
|
45
79
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
46
80
|
return `${hours}:${minutes}h`;
|
|
47
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Format a timestamp for metadata/footer display (build time).
|
|
84
|
+
* Alias for formatAbsoluteDate for clarity in build context.
|
|
85
|
+
*/
|
|
86
|
+
export function formatDateTimeForDisplay(date) {
|
|
87
|
+
return formatAbsoluteDate(date);
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a frontmatter field that may be a scalar string or a YAML list of strings.
|
|
3
|
+
* Used when displaying frontmatter fields like 'harness', 'llm', 'prompter' in the timeline.
|
|
4
|
+
*
|
|
5
|
+
* @param v - A frontmatter value (string, array of strings, or other type)
|
|
6
|
+
* @returns A display string joining multiple values with ' + ', or undefined if empty
|
|
7
|
+
*/
|
|
8
|
+
export declare function toDisplayString(v: unknown): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Type guard: safely get a string field with optional fallback value.
|
|
11
|
+
* Used to defensively extract string fields from untyped YAML data.
|
|
12
|
+
*
|
|
13
|
+
* @param value - The value to check (may be unknown, string, or other type)
|
|
14
|
+
* @param fallback - Value to use if the input is not a string. If provided, return type is always string. If omitted, return type is string | undefined.
|
|
15
|
+
* @returns The value if it's a non-empty string, otherwise the fallback (or undefined if no fallback provided)
|
|
16
|
+
*/
|
|
17
|
+
export declare function asString(value: unknown): string | undefined;
|
|
18
|
+
export declare function asString(value: unknown, fallback: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Type guard: safely extract array of strings from a field that may be a string, array, or other type.
|
|
21
|
+
* Normalizes scalar strings to single-element arrays and filters out non-string array elements.
|
|
22
|
+
*
|
|
23
|
+
* @param value - The value to normalize (string, array, or other type)
|
|
24
|
+
* @returns Array of strings, or empty array if input is not convertible
|
|
25
|
+
*/
|
|
26
|
+
export declare function asStringArray(value: unknown): string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a frontmatter field that may be a scalar string or a YAML list of strings.
|
|
3
|
+
* Used when displaying frontmatter fields like 'harness', 'llm', 'prompter' in the timeline.
|
|
4
|
+
*
|
|
5
|
+
* @param v - A frontmatter value (string, array of strings, or other type)
|
|
6
|
+
* @returns A display string joining multiple values with ' + ', or undefined if empty
|
|
7
|
+
*/
|
|
8
|
+
export function toDisplayString(v) {
|
|
9
|
+
if (typeof v === 'string')
|
|
10
|
+
return v || undefined;
|
|
11
|
+
if (Array.isArray(v)) {
|
|
12
|
+
const joined = v.filter((x) => typeof x === 'string').join(' + ');
|
|
13
|
+
return joined || undefined;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export function asString(value, fallback) {
|
|
18
|
+
if (typeof value === 'string' && value)
|
|
19
|
+
return value;
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type guard: safely extract array of strings from a field that may be a string, array, or other type.
|
|
24
|
+
* Normalizes scalar strings to single-element arrays and filters out non-string array elements.
|
|
25
|
+
*
|
|
26
|
+
* @param value - The value to normalize (string, array, or other type)
|
|
27
|
+
* @returns Array of strings, or empty array if input is not convertible
|
|
28
|
+
*/
|
|
29
|
+
export function asStringArray(value) {
|
|
30
|
+
if (typeof value === 'string') {
|
|
31
|
+
return value ? [value] : [];
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.filter((item) => typeof item === 'string');
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
package/dist/utils/fs.d.ts
CHANGED
|
@@ -11,3 +11,16 @@ export interface EntryMeta {
|
|
|
11
11
|
* Filters for directories and parses index.md if it exists.
|
|
12
12
|
*/
|
|
13
13
|
export declare function getLogbookEntries(logbookDir: string): Promise<EntryMeta[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Safely read a file if it exists, otherwise return an empty string.
|
|
16
|
+
* @param filePath - Path to the file to read
|
|
17
|
+
* @returns File contents if the file exists, otherwise empty string
|
|
18
|
+
*/
|
|
19
|
+
export declare function readFileIfExists(filePath: string): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a file path exists, returning the path if it does.
|
|
22
|
+
* Useful for optional file resolution logic.
|
|
23
|
+
* @param filePath - Path to check
|
|
24
|
+
* @returns The path if it exists, otherwise null
|
|
25
|
+
*/
|
|
26
|
+
export declare function pathExistsOrNull(filePath: string): Promise<string | null>;
|
package/dist/utils/fs.js
CHANGED
|
@@ -36,3 +36,26 @@ export async function getLogbookEntries(logbookDir) {
|
|
|
36
36
|
}
|
|
37
37
|
return results;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Safely read a file if it exists, otherwise return an empty string.
|
|
41
|
+
* @param filePath - Path to the file to read
|
|
42
|
+
* @returns File contents if the file exists, otherwise empty string
|
|
43
|
+
*/
|
|
44
|
+
export async function readFileIfExists(filePath) {
|
|
45
|
+
if (await fs.pathExists(filePath)) {
|
|
46
|
+
return await fs.readFile(filePath, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if a file path exists, returning the path if it does.
|
|
52
|
+
* Useful for optional file resolution logic.
|
|
53
|
+
* @param filePath - Path to check
|
|
54
|
+
* @returns The path if it exists, otherwise null
|
|
55
|
+
*/
|
|
56
|
+
export async function pathExistsOrNull(filePath) {
|
|
57
|
+
if (await fs.pathExists(filePath)) {
|
|
58
|
+
return filePath;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-logbook",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "A command-line tool for project logbooks.",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"demo-app"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"pre": "npm run format && npm run lint && npm run test && npm run build && npm run knip && npm run madge && npm run dev build && npm run dev lint",
|
|
30
30
|
"next": "node scripts/create-ticket-from-backlog.js",
|
|
31
31
|
"link": "npm run build && npm link",
|
|
32
|
-
"open": "npm run build && node scripts/open-and-view-dist.js"
|
|
32
|
+
"open": "npm run dev build && node scripts/open-and-view-dist.js"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"logbook",
|
|
@@ -47,12 +47,21 @@ Only update the currently active logbook entry. Do not edit other existing entri
|
|
|
47
47
|
When you finish writing `index.md`, **remove the boilerplate link line** that the template inserts:
|
|
48
48
|
|
|
49
49
|
### 3. Quality Assurance
|
|
50
|
-
Before
|
|
50
|
+
Before finalizing your work, you should run the linter to verify there are no errors:
|
|
51
51
|
```bash
|
|
52
|
-
logbook release
|
|
53
52
|
logbook lint
|
|
54
53
|
```
|
|
55
|
-
|
|
54
|
+
|
|
55
|
+
You can also check the overall status of the logbook, configuration settings, and statistics (total tasks, done, and drafts) using:
|
|
56
|
+
```bash
|
|
57
|
+
logbook status
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The linter will warn you if the lockfile `.logbook-active` is still present, which is expected during active development.
|
|
61
|
+
|
|
62
|
+
Once the work is ready:
|
|
63
|
+
- **If you are an AI agent**: Do not release the entry. Hand back to the prompter, who will review and run `logbook release`.
|
|
64
|
+
- **If you are a human developer**: Run `logbook release` to remove the lockfile and finalize the entry.
|
|
56
65
|
|
|
57
66
|
The linter enforces rules to be followed.
|
|
58
67
|
|
package/src/templates/index.md
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
ticket: {{id}}
|
|
4
4
|
# The title should be a human-readable description of the work
|
|
5
5
|
title: {{title}}
|
|
6
|
-
prompter: [PROMPTER]
|
|
7
|
-
harness: [HARNESS]
|
|
8
|
-
llm: [LLM]
|
|
9
|
-
summary: [WRITE_SUMMARY_HERE]
|
|
6
|
+
prompter: "[PROMPTER]"
|
|
7
|
+
harness: "[HARNESS]"
|
|
8
|
+
llm: "[LLM]"
|
|
9
|
+
summary: "[WRITE_SUMMARY_HERE]"
|
|
10
10
|
# Tags for categorizing the change (must be from allowed list in .project-logbook)
|
|
11
11
|
tags: []
|
|
12
12
|
# Workspace(s) this change affects (must match package.json workspaces; leave empty for single-project)
|
|
13
13
|
# Example: workspaces: ["frontend", "shared"]
|
|
14
14
|
workspaces: []
|
|
15
15
|
# Set automatically by `logbook start`
|
|
16
|
-
dateStart: [DATE_START]
|
|
16
|
+
dateStart: "[DATE_START]"
|
|
17
17
|
# Set automatically by `logbook release`
|
|
18
|
-
dateEnd: [DATE_END]
|
|
18
|
+
dateEnd: "[DATE_END]"
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
## Summary
|
|
@@ -2,27 +2,40 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/* ── 1. Real-time relative dates ──────────────────────────────────────── */
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Calculate the relative date string (today, yesterday, 2 days ago, etc)
|
|
7
|
+
* Prepend the relative component to the locally-formatted absolute date.
|
|
8
|
+
*/
|
|
9
|
+
function getRelativeComponent(d) {
|
|
7
10
|
var now = new Date();
|
|
8
11
|
// Normalise both to midnight local time for day-diff calculation
|
|
9
12
|
var dDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
10
13
|
var nDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
11
14
|
var diffDays = Math.round((nDay - dDay) / 86400000);
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
if (diffDays ===
|
|
15
|
-
else if (diffDays
|
|
16
|
-
else if (diffDays <
|
|
17
|
-
else if (diffDays <
|
|
18
|
-
else if (diffDays <
|
|
19
|
-
else if (diffDays <
|
|
20
|
-
else if (diffDays <
|
|
21
|
-
else
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
if (diffDays === 0) return 'today';
|
|
17
|
+
else if (diffDays === 1) return 'yesterday';
|
|
18
|
+
else if (diffDays < 7) return diffDays + ' days ago';
|
|
19
|
+
else if (diffDays < 14) return '1 week ago';
|
|
20
|
+
else if (diffDays < 30) return Math.floor(diffDays / 7) + ' weeks ago';
|
|
21
|
+
else if (diffDays < 60) return '1 month ago';
|
|
22
|
+
else if (diffDays < 365) return Math.floor(diffDays / 30) + ' months ago';
|
|
23
|
+
else if (diffDays < 730) return '1 year ago';
|
|
24
|
+
else return Math.floor(diffDays / 365) + ' years ago';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format a Date object as absolute date and time in browser's local timezone.
|
|
29
|
+
* Format: "26 May 2026, 10:51"
|
|
30
|
+
*/
|
|
31
|
+
function formatAbsoluteComponent(d) {
|
|
32
|
+
var day = d.getDate();
|
|
33
|
+
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
34
|
+
var month = months[d.getMonth()];
|
|
35
|
+
var year = d.getFullYear();
|
|
36
|
+
var hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours();
|
|
37
|
+
var minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes();
|
|
38
|
+
return day + ' ' + month + ' ' + year + ', ' + hours + ':' + minutes;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
function updateRelativeDates() {
|
|
@@ -30,7 +43,20 @@
|
|
|
30
43
|
for (var i = 0; i < els.length; i++) {
|
|
31
44
|
var el = els[i];
|
|
32
45
|
var iso = el.getAttribute('data-date');
|
|
33
|
-
if (iso)
|
|
46
|
+
if (iso) {
|
|
47
|
+
var d = new Date(iso);
|
|
48
|
+
if (!isNaN(d.getTime())) {
|
|
49
|
+
// Format the absolute date in the user's browser/local timezone
|
|
50
|
+
var absolute = formatAbsoluteComponent(d);
|
|
51
|
+
// Prepend the relative component (e.g., "today")
|
|
52
|
+
var relative = getRelativeComponent(d);
|
|
53
|
+
if (relative) {
|
|
54
|
+
el.textContent = relative + ', ' + absolute;
|
|
55
|
+
} else {
|
|
56
|
+
el.textContent = absolute;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
34
60
|
}
|
|
35
61
|
}
|
|
36
62
|
|