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.
Files changed (65) hide show
  1. package/dist/commands/build.js +32 -75
  2. package/dist/commands/init.js +11 -11
  3. package/dist/commands/lint.js +10 -11
  4. package/dist/commands/list.js +18 -18
  5. package/dist/commands/log.js +5 -5
  6. package/dist/commands/new.js +19 -20
  7. package/dist/commands/preview.js +9 -8
  8. package/dist/commands/release.js +6 -6
  9. package/dist/commands/start.js +26 -21
  10. package/dist/commands/status.d.ts +1 -0
  11. package/dist/commands/status.js +122 -0
  12. package/dist/commands/steer.js +9 -9
  13. package/dist/commands/upgrade.js +5 -5
  14. package/dist/index.js +9 -8
  15. package/dist/lib/build-helpers.js +28 -50
  16. package/dist/lib/build-steps.d.ts +20 -0
  17. package/dist/lib/build-steps.js +57 -0
  18. package/dist/lib/config.d.ts +16 -0
  19. package/dist/lib/config.js +20 -0
  20. package/dist/lib/entry-id.d.ts +22 -0
  21. package/dist/lib/entry-id.js +26 -0
  22. package/dist/lib/entry-paths.d.ts +23 -0
  23. package/dist/lib/entry-paths.js +55 -0
  24. package/dist/lib/git-helpers.d.ts +11 -1
  25. package/dist/lib/git-helpers.js +28 -26
  26. package/dist/lib/hast-helpers.d.ts +10 -0
  27. package/dist/lib/hast-helpers.js +22 -0
  28. package/dist/lib/html-attributes.d.ts +17 -0
  29. package/dist/lib/html-attributes.js +17 -0
  30. package/dist/lib/html-escape.d.ts +16 -0
  31. package/dist/lib/html-escape.js +38 -0
  32. package/dist/lib/image-helpers.js +26 -33
  33. package/dist/lib/lint-runner.js +5 -5
  34. package/dist/lib/markdown-processors.d.ts +18 -0
  35. package/dist/lib/markdown-processors.js +42 -0
  36. package/dist/lib/package-version.d.ts +5 -0
  37. package/dist/lib/package-version.js +16 -0
  38. package/dist/lib/styles.js +5 -2
  39. package/dist/lib/template-helpers.d.ts +1 -0
  40. package/dist/lib/template-helpers.js +35 -24
  41. package/dist/lib/template-types.d.ts +0 -2
  42. package/dist/lib/templates.d.ts +8 -2
  43. package/dist/lib/templates.js +50 -46
  44. package/dist/lib/theme.d.ts +37 -0
  45. package/dist/lib/theme.js +50 -0
  46. package/dist/lib/url-helpers.d.ts +13 -0
  47. package/dist/lib/url-helpers.js +27 -0
  48. package/dist/linters/diff-to-narrative.d.ts +6 -0
  49. package/dist/linters/diff-to-narrative.js +114 -0
  50. package/dist/linters/index.js +2 -0
  51. package/dist/templates/CONTRIBUTING.md +12 -3
  52. package/dist/templates/index.md +6 -6
  53. package/dist/templates/logbook-client.js +42 -16
  54. package/dist/templates/styles.css +5 -0
  55. package/dist/utils/date.d.ts +23 -1
  56. package/dist/utils/date.js +56 -15
  57. package/dist/utils/frontmatter.d.ts +26 -0
  58. package/dist/utils/frontmatter.js +37 -0
  59. package/dist/utils/fs.d.ts +13 -0
  60. package/dist/utils/fs.js +23 -0
  61. package/package.json +2 -2
  62. package/src/templates/CONTRIBUTING.md +12 -3
  63. package/src/templates/index.md +6 -6
  64. package/src/templates/logbook-client.js +42 -16
  65. package/src/templates/styles.css +5 -0
@@ -1,7 +1,42 @@
1
- /** Format a date value as a relative time string (e.g., "today, May 16, 2026"). */
2
- export function formatRelativeDate(date) {
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
- if (isNaN(d.getTime())) {
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
- const absolute = d.toLocaleDateString('en-US', {
13
- year: 'numeric',
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 = new Date(value);
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 = new Date(date);
30
- if (isNaN(d.getTime()))
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 = new Date(dateStart);
40
- const dEnd = dateEnd ? new Date(dateEnd) : null;
41
- const date = dEnd && !isNaN(dEnd.getTime()) ? dEnd : dStart;
42
- if (isNaN(date.getTime()))
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
+ }
@@ -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",
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 submitting your work, you MUST release the active entry and run the linter:
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
- `logbook release` removes the `.logbook-active` lockfile. The linter will fail if the lockfile is still present, enforcing that no entry is left dangling in a committed state.
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
 
@@ -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
- function relativeDate(isoString) {
6
- var d = new Date(isoString);
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
- var relative;
14
- if (diffDays === 0) relative = 'today';
15
- else if (diffDays === 1) relative = 'yesterday';
16
- else if (diffDays < 7) relative = diffDays + ' days ago';
17
- else if (diffDays < 14) relative = '1 week ago';
18
- else if (diffDays < 30) relative = Math.floor(diffDays / 7) + ' weeks ago';
19
- else if (diffDays < 60) relative = '1 month ago';
20
- else if (diffDays < 365) relative = Math.floor(diffDays / 30) + ' months ago';
21
- else if (diffDays < 730) relative = '1 year ago';
22
- else relative = Math.floor(diffDays / 365) + ' years ago';
23
-
24
- var absolute = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
25
- return relative + ', ' + absolute;
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) el.textContent = relativeDate(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
 
@@ -33,6 +33,11 @@ header h1 {
33
33
  header h1 a {
34
34
  text-decoration: none !important;
35
35
  color: black !important;
36
+ transition: color 0.2s;
37
+ }
38
+
39
+ header h1 a:hover {
40
+ color: var(--primary) !important;
36
41
  }
37
42
 
38
43
  .tagline {