project-logbook 0.3.1 → 0.3.3
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 -12
- package/dist/commands/build.js +58 -35
- package/dist/commands/lint.js +6 -14
- package/dist/index.js +3 -3
- package/dist/lib/build-helpers.d.ts +7 -0
- package/dist/lib/build-helpers.js +30 -6
- package/dist/lib/git-helpers.d.ts +11 -0
- package/dist/lib/git-helpers.js +28 -5
- package/dist/lib/image-helpers.d.ts +4 -0
- package/dist/lib/image-helpers.js +26 -0
- package/dist/lib/template-helpers.d.ts +8 -0
- package/dist/lib/template-helpers.js +78 -0
- package/dist/lib/template-types.d.ts +3 -0
- package/dist/lib/templates.js +4 -65
- package/dist/linters/frontmatter.js +21 -1
- package/dist/linters/placeholders.js +1 -3
- package/dist/templates/CONTRIBUTING.md +2 -2
- package/dist/templates/steer.txt +2 -2
- package/dist/templates/styles.css +52 -0
- package/dist/utils/timeline-helpers.d.ts +4 -0
- package/dist/utils/timeline-helpers.js +19 -0
- package/package.json +4 -2
- package/src/templates/CONTRIBUTING.md +2 -2
- package/src/templates/steer.txt +2 -2
- package/src/templates/styles.css +52 -0
package/README.md
CHANGED
|
@@ -17,18 +17,21 @@ Each entry in `logbook/` consists of:
|
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
```bash
|
|
20
|
-
npm i project-
|
|
20
|
+
npm i project-logbook -g
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
|
|
25
|
+
| Command | Description |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `logbook init` | Initialize configuration and directory. |
|
|
28
|
+
| `logbook new <id> <slug>` | Create a new entry folder with templates. |
|
|
29
|
+
| `logbook start <id>` | Mark a logbook entry as active (writes `.logbook-active` lockfile). The active entry is also auto-detected from the current Git branch — a branch named `feat/LB-123-my-feature` will automatically resolve to entry `LB-123`. |
|
|
30
|
+
| `logbook release` | Release the active logbook entry (removes `.logbook-active` lockfile). |
|
|
31
|
+
| `logbook log <message>` | Append a timestamped log entry to the active `log.md`. |
|
|
32
|
+
| `logbook list` | List all logbook entries with their status. |
|
|
33
|
+
| `logbook lint` | Validate structure, frontmatter, and internal links. |
|
|
34
|
+
| `logbook build` | Compile logbook entries into a static HTML site (default: `public/`). |
|
|
35
|
+
| `logbook preview` | Build and open the logbook in your default browser. |
|
|
36
|
+
| `logbook upgrade` | Synchronize core project files (like `CONTRIBUTING.md`) with latest templates. |
|
|
37
|
+
| `logbook steer` | Output agentic protocol for AI assistants. |
|
package/dist/commands/build.js
CHANGED
|
@@ -5,14 +5,25 @@ import { getConfig, getWorkspaces } from '../lib/config.js';
|
|
|
5
5
|
import { layout, timelineTemplate } from '../lib/templates.js';
|
|
6
6
|
import { getStyles } from '../lib/styles.js';
|
|
7
7
|
import { getClientScript } from '../lib/logbook-client.js';
|
|
8
|
-
import { mdToHtml, buildProjectMdFiles, renderPost } from '../lib/build-helpers.js';
|
|
8
|
+
import { mdToHtml, buildProjectMdFiles, renderPost, generateBuildMeta } from '../lib/build-helpers.js';
|
|
9
9
|
import { mdToHtmlWithImages, copyImages } from '../lib/image-helpers.js';
|
|
10
10
|
import { getGitCommits, getGitTags } from '../lib/git-helpers.js';
|
|
11
11
|
import { formatRelativeDate, getMonthYear, getSortTime } from '../utils/date.js';
|
|
12
12
|
import { getLogbookEntries } from '../utils/fs.js';
|
|
13
13
|
import { getAboutContent } from '../lib/about-content.js';
|
|
14
|
+
import { groupTimelineItems } from '../utils/timeline-helpers.js';
|
|
14
15
|
const pkg = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
15
16
|
const version = pkg.version;
|
|
17
|
+
/** Normalises a frontmatter field that may be a scalar string or a YAML list of strings. */
|
|
18
|
+
const toDisplayString = (v) => {
|
|
19
|
+
if (typeof v === 'string')
|
|
20
|
+
return v || undefined;
|
|
21
|
+
if (Array.isArray(v)) {
|
|
22
|
+
const joined = v.filter((x) => typeof x === 'string').join(' + ');
|
|
23
|
+
return joined || undefined;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
16
27
|
export async function build() {
|
|
17
28
|
const config = getConfig();
|
|
18
29
|
const logbookDir = join(process.cwd(), config.logbookDir);
|
|
@@ -102,9 +113,9 @@ export async function build() {
|
|
|
102
113
|
tags: Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === 'string') : [],
|
|
103
114
|
workspaces: entryWorkspaces,
|
|
104
115
|
title: typeof data.title === 'string' ? data.title : undefined,
|
|
105
|
-
harness:
|
|
106
|
-
llm:
|
|
107
|
-
prompter:
|
|
116
|
+
harness: toDisplayString(data.harness),
|
|
117
|
+
llm: toDisplayString(data.llm),
|
|
118
|
+
prompter: toDisplayString(data.prompter),
|
|
108
119
|
});
|
|
109
120
|
}
|
|
110
121
|
// Fetch global git commits and tags concurrently.
|
|
@@ -134,18 +145,49 @@ export async function build() {
|
|
|
134
145
|
return bEnd - aEnd;
|
|
135
146
|
});
|
|
136
147
|
await Promise.all(timelineEntries.map((entry, i) => renderPost(entry, i, timelineEntries, { logbookDir, outputDir, config, version, buildTime })));
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
// Build the full merged, chronologically sorted groups once, then paginate the flat item list.
|
|
149
|
+
const allGroups = groupTimelineItems(timelineEntries, commitItems, tagItems);
|
|
150
|
+
const flatItems = allGroups.flatMap((g) => g.items);
|
|
151
|
+
const pageSize = 50;
|
|
152
|
+
const totalPages = Math.max(1, Math.ceil(flatItems.length / pageSize));
|
|
153
|
+
for (let i = 0; i < totalPages; i++) {
|
|
154
|
+
const currentPage = i + 1;
|
|
155
|
+
const pageItems = flatItems.slice(i * pageSize, (i + 1) * pageSize);
|
|
156
|
+
// Re-group this page's items by month for rendering.
|
|
157
|
+
const pageGroups = [];
|
|
158
|
+
for (const item of pageItems) {
|
|
159
|
+
const month = item.monthGroup;
|
|
160
|
+
let group = pageGroups.find((g) => g.month === month);
|
|
161
|
+
if (!group) {
|
|
162
|
+
group = { month, items: [] };
|
|
163
|
+
pageGroups.push(group);
|
|
164
|
+
}
|
|
165
|
+
group.items.push(item);
|
|
166
|
+
}
|
|
167
|
+
const timelineContent = timelineTemplate({
|
|
168
|
+
projectName: config.projectName,
|
|
169
|
+
version,
|
|
170
|
+
buildTime,
|
|
171
|
+
groups: pageGroups,
|
|
172
|
+
readmeHtml,
|
|
173
|
+
aboutHtml,
|
|
174
|
+
jiraBaseUrl: config.jiraBaseUrl,
|
|
175
|
+
jiraPrefix: config.jiraPrefix,
|
|
176
|
+
currentPage,
|
|
177
|
+
totalPages,
|
|
178
|
+
}); // prettier-ignore
|
|
179
|
+
const buildMeta = generateBuildMeta(version, buildTime);
|
|
180
|
+
const timelineHtml = layout({
|
|
181
|
+
title: 'Timeline',
|
|
182
|
+
projectName: config.projectName,
|
|
183
|
+
basePath: './',
|
|
184
|
+
buildMeta,
|
|
185
|
+
header: timelineContent.split('</header>')[0] + '</header>',
|
|
186
|
+
content: timelineContent.split('</header>')[1],
|
|
187
|
+
});
|
|
188
|
+
const fileName = currentPage === 1 ? 'index.html' : `index-${currentPage}.html`;
|
|
189
|
+
await fs.writeFile(join(outputDir, fileName), timelineHtml);
|
|
190
|
+
}
|
|
149
191
|
await buildProjectMdFiles(process.cwd(), outputDir, [config.logbookDir, config.outputDir, 'node_modules'], {
|
|
150
192
|
config,
|
|
151
193
|
version,
|
|
@@ -153,22 +195,3 @@ export async function build() {
|
|
|
153
195
|
});
|
|
154
196
|
console.log(chalk.green(`\nSuccessfully built logbook to ${config.outputDir}/`));
|
|
155
197
|
}
|
|
156
|
-
function groupTimelineItems(entries, commits, tags) {
|
|
157
|
-
const toMs = (item) => {
|
|
158
|
-
if (item.kind === 'entry')
|
|
159
|
-
return item.dateEnd ? new Date(item.dateEnd).getTime() : new Date(item.dateStart).getTime();
|
|
160
|
-
return new Date(item.timestamp).getTime();
|
|
161
|
-
};
|
|
162
|
-
const all = [...entries, ...commits, ...tags];
|
|
163
|
-
all.sort((a, b) => toMs(b) - toMs(a));
|
|
164
|
-
const groups = [];
|
|
165
|
-
for (const item of all) {
|
|
166
|
-
let group = groups.find((g) => g.month === item.monthGroup);
|
|
167
|
-
if (!group) {
|
|
168
|
-
group = { month: item.monthGroup, items: [] };
|
|
169
|
-
groups.push(group);
|
|
170
|
-
}
|
|
171
|
-
group.items.push(item);
|
|
172
|
-
}
|
|
173
|
-
return groups;
|
|
174
|
-
}
|
package/dist/commands/lint.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function lint() {
|
|
|
20
20
|
// 2. Entry-level checks
|
|
21
21
|
const logbookEntries = await getLogbookEntries(logbookDir);
|
|
22
22
|
let passedCount = 0;
|
|
23
|
-
let skippedCount = 0;
|
|
23
|
+
let skippedCount = 0; // Re-introducing skippedCount
|
|
24
24
|
for (const entry of logbookEntries) {
|
|
25
25
|
if (!entry.hasIndex) {
|
|
26
26
|
console.error(chalk.red(` [MISSING] ${entry.slug}/index.md`));
|
|
@@ -62,17 +62,9 @@ export async function lint() {
|
|
|
62
62
|
* meaning it should be exempt from linting.
|
|
63
63
|
*/
|
|
64
64
|
function isDraft(entry) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
dataValues.includes('[WRITE_SUMMARY_HERE]') ||
|
|
71
|
-
dataValues.includes('[PROMPTER]') ||
|
|
72
|
-
dataValues.includes('[HARNESS]') ||
|
|
73
|
-
dataValues.includes('[LLM]');
|
|
74
|
-
const contentWithoutCode = entry.content.replace(/`[^`]*`/g, '');
|
|
75
|
-
const bodyHasPlaceholders = contentWithoutCode.includes('TODO:') ||
|
|
76
|
-
entry.content.includes('Write a polished, highly readable "short story" of the change here.');
|
|
77
|
-
return frontmatterHasPlaceholders || bodyHasPlaceholders;
|
|
65
|
+
const frontmatter = entry.data; // Correctly access the data property
|
|
66
|
+
// An entry is a draft if:
|
|
67
|
+
// 1. The 'dateStart' key is missing from frontmatter (i.e., undefined or null).
|
|
68
|
+
// 2. OR the string representation of 'dateStart' is '[DATE_START]'.
|
|
69
|
+
return !frontmatter?.dateStart || String(frontmatter.dateStart) === 'DATE_START';
|
|
78
70
|
}
|
package/dist/index.js
CHANGED
|
@@ -75,12 +75,12 @@ program
|
|
|
75
75
|
await list(options);
|
|
76
76
|
});
|
|
77
77
|
program
|
|
78
|
-
.command('log <message
|
|
78
|
+
.command('log <message>')
|
|
79
79
|
.description('Append a timestamped log entry to the active log.md (e.g. logbook log "Did a thing")')
|
|
80
80
|
.option('-i, --id <id>', 'Override active logbook entry ID')
|
|
81
|
-
.action(async (
|
|
81
|
+
.action(async (message, options) => {
|
|
82
82
|
const { log } = await import('./commands/log.js');
|
|
83
|
-
await log(
|
|
83
|
+
await log([message], options);
|
|
84
84
|
});
|
|
85
85
|
program
|
|
86
86
|
.command('steer')
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { LogbookConfig } from './config.js';
|
|
2
2
|
import type { TimelineEntry } from './template-types.js';
|
|
3
3
|
export declare function mdToHtml(md: string): Promise<string>;
|
|
4
|
+
/** Process entry markdown with image path rewriting */
|
|
5
|
+
export declare function mdToHtmlWithImagePathRewrite(md: string): Promise<string>;
|
|
4
6
|
export declare function buildProjectMdFiles(projectRoot: string, outputDir: string, excludeDirs: string[], ctx: {
|
|
5
7
|
config: LogbookConfig;
|
|
6
8
|
version: string;
|
|
@@ -13,3 +15,8 @@ export declare function renderPost(data: TimelineEntry, i: number, allEntries: T
|
|
|
13
15
|
version: string;
|
|
14
16
|
buildTime: string;
|
|
15
17
|
}): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Generate the standard footer metadata string.
|
|
20
|
+
* Always hardcoded to 'Project Logbook CLI' with link to npm.
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateBuildMeta(version: string, buildTime: string): string;
|
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { unified } from 'unified';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
5
6
|
import remarkRehype from 'remark-rehype';
|
|
6
7
|
import rehypeSlug from 'rehype-slug';
|
|
7
8
|
import rehypeFormat from 'rehype-format';
|
|
@@ -9,7 +10,7 @@ import rehypeStringify from 'rehype-stringify';
|
|
|
9
10
|
import matter from 'gray-matter';
|
|
10
11
|
import { layout, postTemplate } from './templates.js';
|
|
11
12
|
import { getGitCommits } from './git-helpers.js';
|
|
12
|
-
import { mdToHtmlWithImages, extractImagePathsFromMarkdown, copyImages } from './image-helpers.js';
|
|
13
|
+
import { mdToHtmlWithImages, extractImagePathsFromMarkdown, copyImages, rehypeRewriteImagePaths, } from './image-helpers.js';
|
|
13
14
|
/** Rehype plugin: rewrite relative .md links to /index.html equivalents. */
|
|
14
15
|
const rehypeRewriteMdLinks = () => {
|
|
15
16
|
return (tree) => {
|
|
@@ -39,15 +40,31 @@ function visitLinks(node, visitor) {
|
|
|
39
40
|
}
|
|
40
41
|
const processor = unified()
|
|
41
42
|
.use(remarkParse)
|
|
43
|
+
.use(remarkGfm)
|
|
42
44
|
.use(remarkRehype)
|
|
43
45
|
.use(rehypeSlug)
|
|
44
46
|
.use(rehypeRewriteMdLinks)
|
|
45
47
|
.use(rehypeFormat)
|
|
46
48
|
.use(rehypeStringify);
|
|
49
|
+
/** Processor for entry content with image path rewriting */
|
|
50
|
+
const entryProcessor = unified()
|
|
51
|
+
.use(remarkParse)
|
|
52
|
+
.use(remarkGfm)
|
|
53
|
+
.use(remarkRehype)
|
|
54
|
+
.use(rehypeSlug)
|
|
55
|
+
.use(rehypeRewriteMdLinks)
|
|
56
|
+
.use(rehypeRewriteImagePaths)
|
|
57
|
+
.use(rehypeFormat)
|
|
58
|
+
.use(rehypeStringify);
|
|
47
59
|
export async function mdToHtml(md) {
|
|
48
60
|
const result = await processor.process(md);
|
|
49
61
|
return result.toString();
|
|
50
62
|
}
|
|
63
|
+
/** Process entry markdown with image path rewriting */
|
|
64
|
+
export async function mdToHtmlWithImagePathRewrite(md) {
|
|
65
|
+
const result = await entryProcessor.process(md);
|
|
66
|
+
return result.toString();
|
|
67
|
+
}
|
|
51
68
|
export async function buildProjectMdFiles(projectRoot, outputDir, excludeDirs, ctx) {
|
|
52
69
|
const entries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
53
70
|
for (const entry of entries) {
|
|
@@ -60,7 +77,7 @@ export async function buildProjectMdFiles(projectRoot, outputDir, excludeDirs, c
|
|
|
60
77
|
const outDir = join(outputDir, stem);
|
|
61
78
|
const mdContent = await fs.readFile(sourcePath, 'utf8');
|
|
62
79
|
const bodyHtml = await mdToHtml(mdContent);
|
|
63
|
-
const buildMeta =
|
|
80
|
+
const buildMeta = generateBuildMeta(ctx.version, ctx.buildTime);
|
|
64
81
|
const pageHtml = layout({
|
|
65
82
|
title: stem,
|
|
66
83
|
projectName: ctx.config.projectName,
|
|
@@ -101,7 +118,7 @@ async function renderLinkedMdFiles(markdownSources, entryPath, entryOutputDir, c
|
|
|
101
118
|
const mdContent = await fs.readFile(sourcePath, 'utf8');
|
|
102
119
|
const bodyHtml = await mdToHtml(mdContent);
|
|
103
120
|
const title = href.replace(/\.md$/i, '');
|
|
104
|
-
const buildMeta =
|
|
121
|
+
const buildMeta = generateBuildMeta(ctx.version, ctx.buildTime);
|
|
105
122
|
const pageHtml = layout({
|
|
106
123
|
title,
|
|
107
124
|
projectName: ctx.config.projectName,
|
|
@@ -139,8 +156,8 @@ export async function renderPost(data, i, allEntries, ctx) {
|
|
|
139
156
|
title: data.title ?? data.slug,
|
|
140
157
|
harness: data.harness ?? '',
|
|
141
158
|
content: storyHtml,
|
|
142
|
-
ticketHtml: ticketRaw ? await
|
|
143
|
-
logHtml: logRaw ? await
|
|
159
|
+
ticketHtml: ticketRaw ? await mdToHtmlWithImagePathRewrite(ticketRaw) : '',
|
|
160
|
+
logHtml: logRaw ? await mdToHtmlWithImagePathRewrite(logRaw) : '',
|
|
144
161
|
commits,
|
|
145
162
|
version: ctx.version,
|
|
146
163
|
jiraUrl,
|
|
@@ -148,7 +165,7 @@ export async function renderPost(data, i, allEntries, ctx) {
|
|
|
148
165
|
nextEntry: next ? { slug: next.slug, title: next.title ?? '', ticket: next.ticket } : null,
|
|
149
166
|
});
|
|
150
167
|
const descriptionRaw = typeof data.summary === 'string' ? data.summary.replace(/[*#`]/g, '').trim() : '';
|
|
151
|
-
const buildMeta =
|
|
168
|
+
const buildMeta = generateBuildMeta(ctx.version, ctx.buildTime);
|
|
152
169
|
const postHtml = layout({
|
|
153
170
|
title: data.title ?? data.slug,
|
|
154
171
|
projectName: ctx.config.projectName,
|
|
@@ -169,3 +186,10 @@ export async function renderPost(data, i, allEntries, ctx) {
|
|
|
169
186
|
}
|
|
170
187
|
await renderLinkedMdFiles([summaryContent, ticketRaw, logRaw], entryPath, entryOutputDir, ctx);
|
|
171
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Generate the standard footer metadata string.
|
|
191
|
+
* Always hardcoded to 'Project Logbook CLI' with link to npm.
|
|
192
|
+
*/
|
|
193
|
+
export function generateBuildMeta(version, buildTime) {
|
|
194
|
+
return `Generated by <a href="https://www.npmjs.com/package/project-logbook" target="_blank" rel="noopener noreferrer">Project Logbook CLI</a> v${version} • ${buildTime}`;
|
|
195
|
+
}
|
|
@@ -2,9 +2,20 @@ import type { GitCommit } from './template-types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Parse the raw output of `git log --pretty=format:"%H|%s|%aI"` into GitCommit objects.
|
|
4
4
|
* Pure function (no I/O) — unit-testable without spawning git.
|
|
5
|
+
*
|
|
6
|
+
* Uses indexOf/lastIndexOf to delimit fields so commit messages containing `|`
|
|
7
|
+
* are preserved correctly instead of being silently truncated.
|
|
5
8
|
*/
|
|
6
9
|
export declare function parseGitLogOutput(raw: string, now?: Date): GitCommit[];
|
|
10
|
+
/**
|
|
11
|
+
* Fetch git commits that touched `dir`, up to `maxCount`.
|
|
12
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
13
|
+
*/
|
|
7
14
|
export declare function getGitCommits(dir: string, maxCount?: number): Promise<GitCommit[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch all git tags sorted by creation date (newest first).
|
|
17
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
18
|
+
*/
|
|
8
19
|
export declare function getGitTags(): Promise<{
|
|
9
20
|
name: string;
|
|
10
21
|
timestamp: string;
|
package/dist/lib/git-helpers.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { simpleGit } from 'simple-git';
|
|
3
|
+
function getGit() {
|
|
4
|
+
return simpleGit(process.cwd());
|
|
5
|
+
}
|
|
3
6
|
/**
|
|
4
7
|
* Parse the raw output of `git log --pretty=format:"%H|%s|%aI"` into GitCommit objects.
|
|
5
8
|
* Pure function (no I/O) — unit-testable without spawning git.
|
|
9
|
+
*
|
|
10
|
+
* Uses indexOf/lastIndexOf to delimit fields so commit messages containing `|`
|
|
11
|
+
* are preserved correctly instead of being silently truncated.
|
|
6
12
|
*/
|
|
7
13
|
export function parseGitLogOutput(raw, now = new Date()) {
|
|
8
14
|
if (!raw.trim())
|
|
@@ -11,7 +17,13 @@ export function parseGitLogOutput(raw, now = new Date()) {
|
|
|
11
17
|
.split('\n')
|
|
12
18
|
.filter(Boolean)
|
|
13
19
|
.map((line) => {
|
|
14
|
-
const
|
|
20
|
+
const firstPipe = line.indexOf('|');
|
|
21
|
+
const lastPipe = line.lastIndexOf('|');
|
|
22
|
+
if (firstPipe === -1 || firstPipe === lastPipe)
|
|
23
|
+
return null;
|
|
24
|
+
const sha = line.slice(0, firstPipe);
|
|
25
|
+
const message = line.slice(firstPipe + 1, lastPipe);
|
|
26
|
+
const timestamp = line.slice(lastPipe + 1);
|
|
15
27
|
if (!sha || !message || !timestamp)
|
|
16
28
|
return null;
|
|
17
29
|
const relativeTime = formatRelativeTime(timestamp, now);
|
|
@@ -42,20 +54,31 @@ function formatRelativeTime(isoTimestamp, now) {
|
|
|
42
54
|
const diffYear = Math.floor(diffMonth / 12);
|
|
43
55
|
return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
|
|
44
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch git commits that touched `dir`, up to `maxCount`.
|
|
59
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
60
|
+
*/
|
|
45
61
|
export async function getGitCommits(dir, maxCount = 100) {
|
|
46
62
|
try {
|
|
47
|
-
const
|
|
48
|
-
const raw = await git.raw(['log', '--follow', `-n`, String(maxCount), '--pretty=format:%H|%s|%aI', '--', dir]);
|
|
63
|
+
const raw = await getGit().raw(['log', '--follow', `-n`, String(maxCount), '--pretty=format:%H|%s|%aI', '--', dir]);
|
|
49
64
|
return parseGitLogOutput(raw);
|
|
50
65
|
}
|
|
51
66
|
catch {
|
|
52
67
|
return [];
|
|
53
68
|
}
|
|
54
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch all git tags sorted by creation date (newest first).
|
|
72
|
+
* Returns an empty array if git is unavailable or the directory is not a repo.
|
|
73
|
+
*/
|
|
55
74
|
export async function getGitTags() {
|
|
56
75
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
76
|
+
const raw = await getGit().raw([
|
|
77
|
+
'tag',
|
|
78
|
+
'-l',
|
|
79
|
+
'--sort=-creatordate',
|
|
80
|
+
'--format=%(creatordate:iso8601)|%(refname:short)',
|
|
81
|
+
]);
|
|
59
82
|
return raw
|
|
60
83
|
.split('\n')
|
|
61
84
|
.filter(Boolean)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { Root } from 'hast';
|
|
2
|
+
import type { Plugin } from 'unified';
|
|
1
3
|
/**
|
|
2
4
|
* Extract image paths from markdown content
|
|
3
5
|
*/
|
|
4
6
|
export declare function extractImagePathsFromMarkdown(content: string): string[];
|
|
7
|
+
/** Rehype plugin: rewrite relative image paths to use /images/ subfolder */
|
|
8
|
+
export declare const rehypeRewriteImagePaths: Plugin<[], Root>;
|
|
5
9
|
/**
|
|
6
10
|
* Process markdown with image collection enabled
|
|
7
11
|
* Returns { html, imagePaths }
|
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import { join, dirname, relative } from 'node:path';
|
|
3
3
|
import { unified } from 'unified';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
5
6
|
import remarkRehype from 'remark-rehype';
|
|
6
7
|
import rehypeSlug from 'rehype-slug';
|
|
7
8
|
import rehypeFormat from 'rehype-format';
|
|
@@ -57,6 +58,29 @@ function visitImages(node, visitor) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
/** Rehype plugin: rewrite relative image paths to use /images/ subfolder */
|
|
62
|
+
export const rehypeRewriteImagePaths = () => {
|
|
63
|
+
return (tree) => {
|
|
64
|
+
visitImages(tree, (node) => {
|
|
65
|
+
const src = node.properties?.src;
|
|
66
|
+
if (typeof src !== 'string')
|
|
67
|
+
return;
|
|
68
|
+
// Skip external URLs and data URIs
|
|
69
|
+
if (/^[a-z][a-z\d+\-.]*:/i.test(src))
|
|
70
|
+
return;
|
|
71
|
+
if (src.startsWith('data:'))
|
|
72
|
+
return;
|
|
73
|
+
// Skip already rewritten paths
|
|
74
|
+
if (src.startsWith('./images/') || src.startsWith('../images/'))
|
|
75
|
+
return;
|
|
76
|
+
// Skip absolute paths
|
|
77
|
+
if (src.startsWith('/'))
|
|
78
|
+
return;
|
|
79
|
+
// Rewrite relative paths to use /images/ subfolder
|
|
80
|
+
node.properties.src = `./images/${src}`;
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
};
|
|
60
84
|
/**
|
|
61
85
|
* Process markdown with image collection enabled
|
|
62
86
|
* Returns { html, imagePaths }
|
|
@@ -65,8 +89,10 @@ export async function mdToHtmlWithImages(md) {
|
|
|
65
89
|
imageProcessor.reset();
|
|
66
90
|
const processorWithImages = unified()
|
|
67
91
|
.use(remarkParse)
|
|
92
|
+
.use(remarkGfm)
|
|
68
93
|
.use(remarkRehype)
|
|
69
94
|
.use(imageProcessor.rehypeCollectImages)
|
|
95
|
+
.use(rehypeRewriteImagePaths)
|
|
70
96
|
.use(rehypeSlug)
|
|
71
97
|
.use(rehypeFormat)
|
|
72
98
|
.use(rehypeStringify);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TimelineEntry, TimelineCommitItem, TimelineTagItem, EntryLink, GitCommit } from './template-types.js';
|
|
2
|
+
export declare const navLink: (entry: EntryLink, dir: "prev" | "next") => string;
|
|
3
|
+
export declare const renderTagsAndWorkspaces: (tags: string[] | undefined, workspaces: string[] | undefined) => string;
|
|
4
|
+
export declare const renderCommits: (commits: GitCommit[] | undefined) => string;
|
|
5
|
+
export declare const renderTimelineEntryItem: (e: TimelineEntry) => string;
|
|
6
|
+
export declare const renderTimelineCommitItem: (c: TimelineCommitItem, jiraBaseUrl?: string, jiraPrefix?: string) => string;
|
|
7
|
+
export declare const renderTimelineTagItem: (t: TimelineTagItem) => string;
|
|
8
|
+
export declare const paginationLinks: (currentPage: number, totalPages: number) => string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { linkJiraIds } from './jira-helpers.js';
|
|
2
|
+
const html = (strings, ...values) => {
|
|
3
|
+
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
4
|
+
};
|
|
5
|
+
export const navLink = (entry, dir) => {
|
|
6
|
+
const label = dir === 'prev' ? '← Previous' : 'Next →';
|
|
7
|
+
const t = `${entry.ticket ? `${entry.ticket}: ` : ''}${entry.title}`;
|
|
8
|
+
return `<a href="../${entry.slug}/index.html" class="post-nav-link"><span class="post-nav-label">${label}</span><span class="post-nav-title">${t}</span></a>`;
|
|
9
|
+
};
|
|
10
|
+
const renderWorkspaces = (workspaces) => {
|
|
11
|
+
if (!workspaces || workspaces.length === 0)
|
|
12
|
+
return '';
|
|
13
|
+
return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
|
|
14
|
+
};
|
|
15
|
+
export const renderTagsAndWorkspaces = (tags, workspaces) => {
|
|
16
|
+
const tagsHtml = tags && tags.length > 0
|
|
17
|
+
? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
|
|
18
|
+
: '';
|
|
19
|
+
const wsHtml = renderWorkspaces(workspaces);
|
|
20
|
+
if (!tagsHtml && !wsHtml)
|
|
21
|
+
return '';
|
|
22
|
+
return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
|
|
23
|
+
};
|
|
24
|
+
// Renders a compact list of git commits to embed in the Technical Log tab.
|
|
25
|
+
export const renderCommits = (commits) => {
|
|
26
|
+
if (!commits || commits.length === 0)
|
|
27
|
+
return '';
|
|
28
|
+
const rows = commits
|
|
29
|
+
.map((c) => `<div class="commit-item"><span class="commit-sha">${c.sha}</span><span class="commit-message">${c.message}</span><span class="commit-time" title="${c.timestamp}">${c.relativeTime}</span></div>`)
|
|
30
|
+
.join('');
|
|
31
|
+
return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
|
|
32
|
+
};
|
|
33
|
+
export const renderTimelineEntryItem = (e) => html `<div class="timeline-item" data-entry-slug="${e.slug}">
|
|
34
|
+
<a class="timeline-card" href="./${e.slug}/index.html"
|
|
35
|
+
><div class="item-content">
|
|
36
|
+
<div class="item-meta">
|
|
37
|
+
<span data-date="${e.dateStart}">${e.displayDate}</span> • <span class="sort-time">${e.sortTime}</span> •
|
|
38
|
+
${e.llm ? ` ${e.llm} via ` : ''}${e.harness}${e.prompter ? ` / ${e.prompter}` : ''}
|
|
39
|
+
</div>
|
|
40
|
+
<h3 class="item-title">
|
|
41
|
+
<span class="new-badge" style="display:none;">NEW</span>${e.ticket ? `${e.ticket}: ` : ''}${e.title}
|
|
42
|
+
</h3>
|
|
43
|
+
<div class="item-summary">${e.summary}</div>
|
|
44
|
+
${renderTagsAndWorkspaces(e.tags, e.workspaces)}
|
|
45
|
+
</div>
|
|
46
|
+
<span class="item-arrow">›</span></a
|
|
47
|
+
>
|
|
48
|
+
</div>`;
|
|
49
|
+
export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
|
|
50
|
+
const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
|
|
51
|
+
return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="commit-sha">${c.sha}</span><span class="commit-message">${message}</span><span class="commit-time" title="${c.timestamp}" data-date="${c.timestamp}">${c.relativeTime}</span></div></div>`;
|
|
52
|
+
};
|
|
53
|
+
export const renderTimelineTagItem = (t) => {
|
|
54
|
+
const formatted = new Date(t.timestamp)
|
|
55
|
+
.toLocaleString('de-DE', {
|
|
56
|
+
year: 'numeric',
|
|
57
|
+
month: '2-digit',
|
|
58
|
+
day: '2-digit',
|
|
59
|
+
hour: '2-digit',
|
|
60
|
+
minute: '2-digit',
|
|
61
|
+
second: '2-digit',
|
|
62
|
+
hour12: false,
|
|
63
|
+
})
|
|
64
|
+
.replace(',', '');
|
|
65
|
+
return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${t.name}</span><span class="tag-chip-date" title="${t.timestamp}">${formatted}</span></div></div>`;
|
|
66
|
+
};
|
|
67
|
+
export const paginationLinks = (currentPage, totalPages) => {
|
|
68
|
+
let links = '';
|
|
69
|
+
if (currentPage > 1) {
|
|
70
|
+
const prevPageLink = currentPage === 2 ? 'index.html' : `index-${currentPage - 1}.html`;
|
|
71
|
+
links += `<a href="./${prevPageLink}" class="pagination-link pagination-link--prev">← Previous Page</a>`;
|
|
72
|
+
}
|
|
73
|
+
if (currentPage < totalPages) {
|
|
74
|
+
const nextPageLink = `index-${currentPage + 1}.html`;
|
|
75
|
+
links += `<a href="./${nextPageLink}" class="pagination-link pagination-link--next">Next Page →</a>`;
|
|
76
|
+
}
|
|
77
|
+
return links ? `<nav class="pagination-nav">${links}</nav>` : '';
|
|
78
|
+
};
|
|
@@ -18,6 +18,8 @@ export interface TimelineTemplateProps {
|
|
|
18
18
|
aboutHtml: string;
|
|
19
19
|
jiraBaseUrl?: string;
|
|
20
20
|
jiraPrefix?: string;
|
|
21
|
+
currentPage?: number;
|
|
22
|
+
totalPages?: number;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Configuration for a month group in the timeline
|
|
@@ -40,6 +42,7 @@ export interface TimelineEntry {
|
|
|
40
42
|
dateEnd?: string;
|
|
41
43
|
displayDate: string;
|
|
42
44
|
sortTime: string;
|
|
45
|
+
monthGroup: string;
|
|
43
46
|
harness?: string;
|
|
44
47
|
llm?: string;
|
|
45
48
|
prompter?: string;
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { navLink, renderTagsAndWorkspaces, renderCommits, renderTimelineEntryItem, renderTimelineCommitItem, renderTimelineTagItem, paginationLinks, } from './template-helpers.js';
|
|
2
2
|
const html = (strings, ...values) => {
|
|
3
3
|
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
4
4
|
};
|
|
@@ -28,70 +28,8 @@ export const layout = ({ title, header, content, projectName, basePath = './', d
|
|
|
28
28
|
<footer>${buildMeta ? `<p class="build-time">${buildMeta}</p>` : ''}</footer>
|
|
29
29
|
<script src="${basePath}logbook.js"></script>
|
|
30
30
|
</body></html>`;
|
|
31
|
-
const navLink = (entry, dir) => {
|
|
32
|
-
const label = dir === 'prev' ? '← Previous' : 'Next →';
|
|
33
|
-
const t = `${entry.ticket ? `${entry.ticket}: ` : ''}${entry.title}`;
|
|
34
|
-
return `<a href="../${entry.slug}/index.html" class="post-nav-link"><span class="post-nav-label">${label}</span><span class="post-nav-title">${t}</span></a>`;
|
|
35
|
-
};
|
|
36
|
-
const renderWorkspaces = (workspaces) => {
|
|
37
|
-
if (!workspaces || workspaces.length === 0)
|
|
38
|
-
return '';
|
|
39
|
-
return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
|
|
40
|
-
};
|
|
41
|
-
const renderTagsAndWorkspaces = (tags, workspaces) => {
|
|
42
|
-
const tagsHtml = tags && tags.length > 0
|
|
43
|
-
? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
|
|
44
|
-
: '';
|
|
45
|
-
const wsHtml = renderWorkspaces(workspaces);
|
|
46
|
-
if (!tagsHtml && !wsHtml)
|
|
47
|
-
return '';
|
|
48
|
-
return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
|
|
49
|
-
};
|
|
50
|
-
// Renders a compact list of git commits to embed in the Technical Log tab.
|
|
51
|
-
const renderCommits = (commits) => {
|
|
52
|
-
if (!commits || commits.length === 0)
|
|
53
|
-
return '';
|
|
54
|
-
const rows = commits
|
|
55
|
-
.map((c) => `<div class="commit-item"><span class="commit-sha">${c.sha}</span><span class="commit-message">${c.message}</span><span class="commit-time" title="${c.timestamp}">${c.relativeTime}</span></div>`)
|
|
56
|
-
.join('');
|
|
57
|
-
return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
|
|
58
|
-
};
|
|
59
|
-
const renderTimelineEntryItem = (e) => html `<div class="timeline-item" data-entry-slug="${e.slug}">
|
|
60
|
-
<a class="timeline-card" href="./${e.slug}/index.html"
|
|
61
|
-
><div class="item-content">
|
|
62
|
-
<div class="item-meta">
|
|
63
|
-
<span data-date="${e.dateStart}">${e.displayDate}</span> • <span class="sort-time">${e.sortTime}</span> •
|
|
64
|
-
${e.llm ? ` ${e.llm} via ` : ''}${e.harness}${e.prompter ? ` / ${e.prompter}` : ''}
|
|
65
|
-
</div>
|
|
66
|
-
<h3 class="item-title">
|
|
67
|
-
<span class="new-badge" style="display:none;">NEW</span>${e.ticket ? `${e.ticket}: ` : ''}${e.title}
|
|
68
|
-
</h3>
|
|
69
|
-
<div class="item-summary">${e.summary}</div>
|
|
70
|
-
${renderTagsAndWorkspaces(e.tags, e.workspaces)}
|
|
71
|
-
</div>
|
|
72
|
-
<span class="item-arrow">›</span></a
|
|
73
|
-
>
|
|
74
|
-
</div>`;
|
|
75
|
-
const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
|
|
76
|
-
const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
|
|
77
|
-
return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="commit-sha">${c.sha}</span><span class="commit-message">${message}</span><span class="commit-time" title="${c.timestamp}" data-date="${c.timestamp}">${c.relativeTime}</span></div></div>`;
|
|
78
|
-
};
|
|
79
|
-
const renderTimelineTagItem = (t) => {
|
|
80
|
-
const formatted = new Date(t.timestamp)
|
|
81
|
-
.toLocaleString('de-DE', {
|
|
82
|
-
year: 'numeric',
|
|
83
|
-
month: '2-digit',
|
|
84
|
-
day: '2-digit',
|
|
85
|
-
hour: '2-digit',
|
|
86
|
-
minute: '2-digit',
|
|
87
|
-
second: '2-digit',
|
|
88
|
-
hour12: false,
|
|
89
|
-
})
|
|
90
|
-
.replace(',', '');
|
|
91
|
-
return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${t.name}</span><span class="tag-chip-date" title="${t.timestamp}">${formatted}</span></div></div>`;
|
|
92
|
-
};
|
|
93
31
|
export const timelineTemplate = (props) => {
|
|
94
|
-
const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix } = props;
|
|
32
|
+
const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix, currentPage, totalPages } = props;
|
|
95
33
|
const timelineTab = html `<div class="timeline">
|
|
96
34
|
${groups
|
|
97
35
|
.map((g) => html `<section class="month-group">
|
|
@@ -107,9 +45,10 @@ export const timelineTemplate = (props) => {
|
|
|
107
45
|
.join('')}
|
|
108
46
|
</section>`)
|
|
109
47
|
.join('')}
|
|
48
|
+
${paginationLinks(currentPage, totalPages)}
|
|
110
49
|
</div>`;
|
|
111
50
|
return html `<header>
|
|
112
|
-
<h1>${projectName}</h1>
|
|
51
|
+
<h1><a href="./index.html">${projectName}</a></h1>
|
|
113
52
|
<p class="tagline">A brief summary of the recent changes to the project.</p>
|
|
114
53
|
</header>
|
|
115
54
|
${tabsComponent([
|
|
@@ -42,7 +42,23 @@ const linter = {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
// 3.
|
|
45
|
+
// 3. Validate specific placeholder values
|
|
46
|
+
const placeholderFields = ['prompter', 'harness', 'llm', 'summary'];
|
|
47
|
+
for (const field of placeholderFields) {
|
|
48
|
+
const value = frontmatter[field];
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value) {
|
|
51
|
+
if (typeof item === 'string' && (item === field.toUpperCase() || item === 'WRITE_SUMMARY_HERE')) {
|
|
52
|
+
issues.push({
|
|
53
|
+
level: 'error',
|
|
54
|
+
category: 'frontmatter',
|
|
55
|
+
message: `Placeholder value "${item}" found in field "${field}". Please replace with actual content.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 4. Tags validation
|
|
46
62
|
const allowedTags = config.tags?.allowed || [];
|
|
47
63
|
if (!frontmatter.tags || !Array.isArray(frontmatter.tags) || frontmatter.tags.length === 0) {
|
|
48
64
|
issues.push({
|
|
@@ -62,6 +78,10 @@ const linter = {
|
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
}
|
|
81
|
+
// TODO: Add conditional validation for body placeholders based on isDraft
|
|
82
|
+
// Currently the isDraft only checks frontmatter data for placeholders.
|
|
83
|
+
// If the body contains "TODO:" and isDraft is true, the current logic is to skip.
|
|
84
|
+
// However, if we want to lint placeholder values even in draft mode, we need a separate body linter.
|
|
65
85
|
return issues;
|
|
66
86
|
},
|
|
67
87
|
};
|
|
@@ -14,8 +14,6 @@ const linter = {
|
|
|
14
14
|
if (context.indexPath && (await fs.pathExists(context.indexPath))) {
|
|
15
15
|
rawFile = await fs.readFile(context.indexPath, 'utf8');
|
|
16
16
|
}
|
|
17
|
-
// Scan only the frontmatter block for bracket-style placeholders to avoid false
|
|
18
|
-
// positives when placeholder names are mentioned in the narrative body.
|
|
19
17
|
const frontmatterMatch = rawFile.match(/^---\n[\s\S]*?\n---/);
|
|
20
18
|
const frontmatterBlock = frontmatterMatch ? frontmatterMatch[0] : rawFile;
|
|
21
19
|
const frontmatterPlaceholders = [
|
|
@@ -37,7 +35,7 @@ const linter = {
|
|
|
37
35
|
});
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
// Check boilerplate body text against the full content (body only is fine here).
|
|
38
|
+
// Check boilerplate body text against the full content (body only is fine here) for all entries.
|
|
41
39
|
if (context.content.includes('TODO: ')) {
|
|
42
40
|
issues.push({
|
|
43
41
|
level: 'error',
|
|
@@ -37,10 +37,10 @@ As you code, keep the `log.md` updated in real-time using the `logbook log` comm
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
40
|
-
logbook log "Fixed the bug"
|
|
40
|
+
logbook log "Fixed the bug"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`.
|
|
43
|
+
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`. One message per call. If you encounter a bug or change your design, log it immediately. This is the most valuable file for future debugging.
|
|
44
44
|
|
|
45
45
|
Only update the currently active logbook entry. Do not edit other existing entries while working.
|
|
46
46
|
|
package/dist/templates/steer.txt
CHANGED
|
@@ -11,8 +11,8 @@ Phase 2: Execute & Trace
|
|
|
11
11
|
1. Use `log.md` as a live technical scratchpad.
|
|
12
12
|
2. Log your work in real-time using the `logbook log "<message>"` command. Record every major decision, error, and pivot. Do not reconstruct this at the end.
|
|
13
13
|
Example: logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
One message per call. For multiple entries, call the command once per message.
|
|
15
|
+
3. Stick to the active entry; never modify past entries in the logbook folder.
|
|
16
16
|
|
|
17
17
|
Phase 3: Synthesize
|
|
18
18
|
1. When implementation is finished, write the narrative in `index.md`.
|
|
@@ -11,6 +11,11 @@ body {
|
|
|
11
11
|
padding: 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.home-link {
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
color: inherit;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
header {
|
|
15
20
|
background: var(--card-bg);
|
|
16
21
|
border-bottom: 1px solid var(--border);
|
|
@@ -25,6 +30,11 @@ header h1 {
|
|
|
25
30
|
letter-spacing: -0.025em;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
header h1 a {
|
|
34
|
+
text-decoration: none !important;
|
|
35
|
+
color: black !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
.tagline {
|
|
29
39
|
color: var(--text-muted);
|
|
30
40
|
font-size: 1.125rem;
|
|
@@ -273,6 +283,44 @@ article h1 {
|
|
|
273
283
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
/* Pagination Links */
|
|
287
|
+
.pagination-nav {
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: space-between;
|
|
290
|
+
margin-top: 2rem;
|
|
291
|
+
padding-top: 1.5rem;
|
|
292
|
+
border-top: 1px solid var(--border);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.pagination-link {
|
|
296
|
+
display: inline-flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 0.5rem;
|
|
299
|
+
font-size: 0.9375rem;
|
|
300
|
+
font-weight: 500;
|
|
301
|
+
color: var(--primary);
|
|
302
|
+
text-decoration: none;
|
|
303
|
+
padding: 0.5rem 1rem;
|
|
304
|
+
border-radius: 2rem;
|
|
305
|
+
transition:
|
|
306
|
+
background 0.2s,
|
|
307
|
+
color 0.2s;
|
|
308
|
+
background: var(--primary-soft);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.pagination-link:hover {
|
|
312
|
+
background: var(--primary);
|
|
313
|
+
color: white;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.pagination-link--prev {
|
|
317
|
+
margin-right: auto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.pagination-link--next {
|
|
321
|
+
margin-left: auto;
|
|
322
|
+
}
|
|
323
|
+
|
|
276
324
|
.content-section {
|
|
277
325
|
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
278
326
|
font-size: 1.125rem;
|
|
@@ -286,6 +334,10 @@ article h1 {
|
|
|
286
334
|
margin-bottom: 1rem;
|
|
287
335
|
}
|
|
288
336
|
|
|
337
|
+
.content-section img {
|
|
338
|
+
max-width: 100%;
|
|
339
|
+
}
|
|
340
|
+
|
|
289
341
|
@keyframes fadeIn {
|
|
290
342
|
from {
|
|
291
343
|
opacity: 0;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TimelineEntry, TimelineCommitItem, TimelineTagItem, TimelineGroup } from '../lib/template-types.js';
|
|
2
|
+
export declare function groupTimelineItems(entries: (TimelineEntry & {
|
|
3
|
+
monthGroup: string;
|
|
4
|
+
})[], commits: TimelineCommitItem[], tags: TimelineTagItem[]): TimelineGroup[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function groupTimelineItems(entries, commits, tags) {
|
|
2
|
+
const toMs = (item) => {
|
|
3
|
+
if (item.kind === 'entry')
|
|
4
|
+
return item.dateEnd ? new Date(item.dateEnd).getTime() : new Date(item.dateStart).getTime();
|
|
5
|
+
return new Date(item.timestamp).getTime();
|
|
6
|
+
};
|
|
7
|
+
const all = [...entries, ...commits, ...tags];
|
|
8
|
+
all.sort((a, b) => toMs(b) - toMs(a));
|
|
9
|
+
const groups = [];
|
|
10
|
+
for (const item of all) {
|
|
11
|
+
let group = groups.find((g) => g.month === item.monthGroup);
|
|
12
|
+
if (!group) {
|
|
13
|
+
group = { month: item.monthGroup, items: [] };
|
|
14
|
+
groups.push(group);
|
|
15
|
+
}
|
|
16
|
+
group.items.push(item);
|
|
17
|
+
}
|
|
18
|
+
return groups;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-logbook",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "A command-line tool for project logbooks.",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"demo-app"
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"test": "vitest run",
|
|
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
|
-
"link": "npm run build && npm link"
|
|
31
|
+
"link": "npm run build && npm link",
|
|
32
|
+
"open": "npm run build && node scripts/open-and-view-dist.js"
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"logbook",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"rehype-format": "^5.0.1",
|
|
62
63
|
"rehype-slug": "^6.0.0",
|
|
63
64
|
"rehype-stringify": "^10.0.1",
|
|
65
|
+
"remark-gfm": "^4.0.1",
|
|
64
66
|
"remark-parse": "^11.0.0",
|
|
65
67
|
"remark-rehype": "^11.1.2",
|
|
66
68
|
"simple-git": "^3.36.0",
|
|
@@ -37,10 +37,10 @@ As you code, keep the `log.md` updated in real-time using the `logbook log` comm
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
40
|
-
logbook log "Fixed the bug"
|
|
40
|
+
logbook log "Fixed the bug"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`.
|
|
43
|
+
Each message is automatically prefixed with an ISO timestamp and appended as a bullet to the active `log.md`. One message per call. If you encounter a bug or change your design, log it immediately. This is the most valuable file for future debugging.
|
|
44
44
|
|
|
45
45
|
Only update the currently active logbook entry. Do not edit other existing entries while working.
|
|
46
46
|
|
package/src/templates/steer.txt
CHANGED
|
@@ -11,8 +11,8 @@ Phase 2: Execute & Trace
|
|
|
11
11
|
1. Use `log.md` as a live technical scratchpad.
|
|
12
12
|
2. Log your work in real-time using the `logbook log "<message>"` command. Record every major decision, error, and pivot. Do not reconstruct this at the end.
|
|
13
13
|
Example: logbook log "Investigated root cause — found issue in src/lib/config.ts"
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
One message per call. For multiple entries, call the command once per message.
|
|
15
|
+
3. Stick to the active entry; never modify past entries in the logbook folder.
|
|
16
16
|
|
|
17
17
|
Phase 3: Synthesize
|
|
18
18
|
1. When implementation is finished, write the narrative in `index.md`.
|
package/src/templates/styles.css
CHANGED
|
@@ -11,6 +11,11 @@ body {
|
|
|
11
11
|
padding: 0;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.home-link {
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
color: inherit;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
header {
|
|
15
20
|
background: var(--card-bg);
|
|
16
21
|
border-bottom: 1px solid var(--border);
|
|
@@ -25,6 +30,11 @@ header h1 {
|
|
|
25
30
|
letter-spacing: -0.025em;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
header h1 a {
|
|
34
|
+
text-decoration: none !important;
|
|
35
|
+
color: black !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
.tagline {
|
|
29
39
|
color: var(--text-muted);
|
|
30
40
|
font-size: 1.125rem;
|
|
@@ -273,6 +283,44 @@ article h1 {
|
|
|
273
283
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
/* Pagination Links */
|
|
287
|
+
.pagination-nav {
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: space-between;
|
|
290
|
+
margin-top: 2rem;
|
|
291
|
+
padding-top: 1.5rem;
|
|
292
|
+
border-top: 1px solid var(--border);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.pagination-link {
|
|
296
|
+
display: inline-flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 0.5rem;
|
|
299
|
+
font-size: 0.9375rem;
|
|
300
|
+
font-weight: 500;
|
|
301
|
+
color: var(--primary);
|
|
302
|
+
text-decoration: none;
|
|
303
|
+
padding: 0.5rem 1rem;
|
|
304
|
+
border-radius: 2rem;
|
|
305
|
+
transition:
|
|
306
|
+
background 0.2s,
|
|
307
|
+
color 0.2s;
|
|
308
|
+
background: var(--primary-soft);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.pagination-link:hover {
|
|
312
|
+
background: var(--primary);
|
|
313
|
+
color: white;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.pagination-link--prev {
|
|
317
|
+
margin-right: auto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.pagination-link--next {
|
|
321
|
+
margin-left: auto;
|
|
322
|
+
}
|
|
323
|
+
|
|
276
324
|
.content-section {
|
|
277
325
|
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
278
326
|
font-size: 1.125rem;
|
|
@@ -286,6 +334,10 @@ article h1 {
|
|
|
286
334
|
margin-bottom: 1rem;
|
|
287
335
|
}
|
|
288
336
|
|
|
337
|
+
.content-section img {
|
|
338
|
+
max-width: 100%;
|
|
339
|
+
}
|
|
340
|
+
|
|
289
341
|
@keyframes fadeIn {
|
|
290
342
|
from {
|
|
291
343
|
opacity: 0;
|