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/commands/start.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
import
|
|
4
|
+
import { error as errorStyle, warning as warningStyle, neutral, header, successMessage } from '../lib/theme.js';
|
|
5
5
|
import matter from 'gray-matter';
|
|
6
|
-
import { getConfig } from '../lib/config.js';
|
|
6
|
+
import { getConfig, getLogbookDirPath } from '../lib/config.js';
|
|
7
7
|
import { getActiveEntry } from '../lib/session.js';
|
|
8
|
+
import { getEntryPath } from '../lib/entry-paths.js';
|
|
8
9
|
import { LOCKFILE_NAME } from '../lib/lockfile.js';
|
|
9
10
|
function getGitUsername() {
|
|
10
11
|
try {
|
|
@@ -22,31 +23,31 @@ export async function start(id) {
|
|
|
22
23
|
// 1. Enforce single active entry
|
|
23
24
|
if (await fs.pathExists(lockfilePath)) {
|
|
24
25
|
const existing = await fs.readJson(lockfilePath);
|
|
25
|
-
console.error(
|
|
26
|
-
console.error(
|
|
26
|
+
console.error(errorStyle(`Error: Entry '${existing.slug}' is already active.`));
|
|
27
|
+
console.error(errorStyle(`Run 'logbook release' before starting a new entry.`));
|
|
27
28
|
process.exit(1);
|
|
28
29
|
}
|
|
29
30
|
// 2. Resolve folder by ID prefix (e.g. "LB-5" → "LB-5-jira-support")
|
|
30
31
|
const config = getConfig();
|
|
31
32
|
// If a bare number is given (e.g. "19"), auto-prepend the configured jiraPrefix
|
|
32
33
|
const resolvedId = /^\d+$/.test(id) && config.jiraPrefix ? `${config.jiraPrefix}-${id}` : id;
|
|
33
|
-
const logbookDir =
|
|
34
|
+
const logbookDir = getLogbookDirPath(config, cwd);
|
|
34
35
|
const allDirs = await fs.readdir(logbookDir);
|
|
35
36
|
const matchingDir = allDirs.find((d) => {
|
|
36
37
|
const prefix = d.split('-').slice(0, resolvedId.split('-').length).join('-');
|
|
37
38
|
return prefix.toLowerCase() === resolvedId.toLowerCase();
|
|
38
39
|
});
|
|
39
40
|
if (!matchingDir) {
|
|
40
|
-
console.error(
|
|
41
|
-
console.error(
|
|
41
|
+
console.error(errorStyle(`Error: No entry found matching ID '${resolvedId}' in ${config.logbookDir}/.`));
|
|
42
|
+
console.error(errorStyle(`Run 'logbook new <id> <slug>' to create it first.`));
|
|
42
43
|
process.exit(1);
|
|
43
44
|
}
|
|
44
45
|
const slug = matchingDir;
|
|
45
|
-
const entryDir =
|
|
46
|
+
const entryDir = getEntryPath(logbookDir, slug);
|
|
46
47
|
// Warn if branch-resolved entry is different
|
|
47
48
|
const activeFromBranch = await getActiveEntry({}, cwd);
|
|
48
49
|
if (activeFromBranch && activeFromBranch.source === 'branch' && activeFromBranch.slug !== slug) {
|
|
49
|
-
console.warn(
|
|
50
|
+
console.warn(warningStyle(`Warning: Your current Git branch resolves to active entry '${activeFromBranch.slug}', but you are manually starting '${slug}'.`));
|
|
50
51
|
}
|
|
51
52
|
// 3. Detect git prompter
|
|
52
53
|
const prompter = getGitUsername();
|
|
@@ -65,23 +66,27 @@ export async function start(id) {
|
|
|
65
66
|
let raw = await fs.readFile(indexPath, 'utf8');
|
|
66
67
|
const parsed = matter(raw);
|
|
67
68
|
// Inject dateStart only if the placeholder exists (preserve existing dateStart on re-start)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
raw = raw.replace('dateStart: [DATE_START]', `dateStart: ${lockData.startedAt}`);
|
|
69
|
+
if (raw.includes('dateStart: "[DATE_START]"')) {
|
|
70
|
+
raw = raw.replace('dateStart: "[DATE_START]"', `dateStart: ${lockData.startedAt}`);
|
|
71
71
|
}
|
|
72
72
|
// Inject prompter if available and not already set
|
|
73
|
-
if (prompter
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
if (prompter) {
|
|
74
|
+
if (raw.includes('prompter: "[PROMPTER]"')) {
|
|
75
|
+
raw = raw.replace('prompter: "[PROMPTER]"', `prompter: '${prompter.replace(/'/g, "''")}'`);
|
|
76
|
+
}
|
|
77
|
+
else if (!parsed.data.prompter) {
|
|
78
|
+
raw = raw.replace(/^(---\n[\s\S]*?)(^---$)/m, (_, frontmatter, closing) => {
|
|
79
|
+
if (frontmatter.includes('prompter:'))
|
|
80
|
+
return _;
|
|
81
|
+
return `${frontmatter}prompter: '${prompter.replace(/'/g, "''")}'\n${closing}`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
await fs.writeFile(indexPath, raw);
|
|
81
86
|
}
|
|
82
|
-
console.log(
|
|
87
|
+
console.log(successMessage('Started', `entry: ${slug}`));
|
|
83
88
|
if (prompter) {
|
|
84
|
-
console.log(
|
|
89
|
+
console.log(header(`Prompter set to: ${prompter}`));
|
|
85
90
|
}
|
|
86
|
-
console.log(
|
|
91
|
+
console.log(neutral(`Lockfile written to ${LOCKFILE_NAME}. Run 'logbook release' when done.`));
|
|
87
92
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function status(): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { getConfig, getLogbookDirPath } from '../lib/config.js';
|
|
3
|
+
import { getLogbookEntries } from '../utils/fs.js';
|
|
4
|
+
import { getActiveEntry } from '../lib/session.js';
|
|
5
|
+
import { parseTicketId } from '../utils/id.js';
|
|
6
|
+
import { error, neutral, highlight, bold, warning } from '../lib/theme.js';
|
|
7
|
+
import { getChangedLOC } from '../lib/git-helpers.js';
|
|
8
|
+
export async function status() {
|
|
9
|
+
const config = getConfig();
|
|
10
|
+
const logbookDir = getLogbookDirPath(config);
|
|
11
|
+
if (!(await fs.pathExists(logbookDir))) {
|
|
12
|
+
console.error(error(`Error: Logbook directory '${config.logbookDir}' not found. Run 'logbook init' first.`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Detect currently active entry (if any)
|
|
16
|
+
const active = await getActiveEntry();
|
|
17
|
+
const activeSlug = active?.slug;
|
|
18
|
+
const logbookEntries = await getLogbookEntries(logbookDir);
|
|
19
|
+
let totalCount = logbookEntries.length;
|
|
20
|
+
let draftCount = 0;
|
|
21
|
+
let doneCount = 0;
|
|
22
|
+
let activeCount = 0;
|
|
23
|
+
for (const entry of logbookEntries) {
|
|
24
|
+
const isActive = activeSlug === entry.slug;
|
|
25
|
+
if (isActive) {
|
|
26
|
+
activeCount++;
|
|
27
|
+
}
|
|
28
|
+
if (!entry.hasIndex) {
|
|
29
|
+
draftCount++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const { data, content } = entry;
|
|
33
|
+
// Check frontmatter field values for bracket-style unfilled placeholders
|
|
34
|
+
const dataValues = Object.values(data)
|
|
35
|
+
.filter((v) => typeof v === 'string')
|
|
36
|
+
.join('\n');
|
|
37
|
+
const frontmatterHasPlaceholders = dataValues.includes('[DATE_END]') ||
|
|
38
|
+
dataValues.includes('[DATE_START]') ||
|
|
39
|
+
dataValues.includes('[WRITE_SUMMARY_HERE]') ||
|
|
40
|
+
dataValues.includes('[PROMPTER]') ||
|
|
41
|
+
dataValues.includes('[HARNESS]') ||
|
|
42
|
+
dataValues.includes('[LLM]');
|
|
43
|
+
// Check body content for unfilled template hints (strip inline code spans to avoid false positives)
|
|
44
|
+
const contentWithoutCode = content.replace(/`[^`]*`/g, '');
|
|
45
|
+
const bodyHasPlaceholders = contentWithoutCode.includes('TODO:') ||
|
|
46
|
+
content.includes('Write a polished, highly readable "short story" of the change here.');
|
|
47
|
+
const hasPlaceholders = frontmatterHasPlaceholders || bodyHasPlaceholders;
|
|
48
|
+
if (hasPlaceholders) {
|
|
49
|
+
draftCount++;
|
|
50
|
+
}
|
|
51
|
+
else if (!isActive) {
|
|
52
|
+
doneCount++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Find latest entry (sorted descending by ticket id / slug)
|
|
56
|
+
const sortedEntries = [...logbookEntries].sort((a, b) => {
|
|
57
|
+
const idA = parseTicketId(a.slug);
|
|
58
|
+
const idB = parseTicketId(b.slug);
|
|
59
|
+
if (idA && idB) {
|
|
60
|
+
if (idA.prefix !== idB.prefix) {
|
|
61
|
+
return idB.prefix.localeCompare(idA.prefix);
|
|
62
|
+
}
|
|
63
|
+
return idB.number - idA.number;
|
|
64
|
+
}
|
|
65
|
+
return b.slug.localeCompare(a.slug);
|
|
66
|
+
});
|
|
67
|
+
const latestEntry = sortedEntries[0];
|
|
68
|
+
let latestText = neutral('None');
|
|
69
|
+
if (latestEntry) {
|
|
70
|
+
const latestTitle = latestEntry.data?.title || latestEntry.slug.replace(/^[A-Za-z]+-\d+-/, '').replace(/-/g, ' ');
|
|
71
|
+
latestText = `${highlight(latestEntry.slug)} - ${latestTitle}`;
|
|
72
|
+
}
|
|
73
|
+
// Print Active Task section
|
|
74
|
+
console.log(`\n${bold.underline('Logbook Status')}`);
|
|
75
|
+
console.log(`\n${bold('Active Task:')}`);
|
|
76
|
+
if (active) {
|
|
77
|
+
const activeDetail = logbookEntries.find((e) => e.slug === active.slug);
|
|
78
|
+
const activeTitle = activeDetail?.data?.title || active.slug.replace(/^[A-Za-z]+-\d+-/, '').replace(/-/g, ' ');
|
|
79
|
+
console.log(` ${bold('Slug/ID:')} ${highlight(active.slug)}`);
|
|
80
|
+
console.log(` ${bold('Title:')} ${activeTitle}`);
|
|
81
|
+
console.log(` ${bold('Source:')} ${warning(active.source)}`);
|
|
82
|
+
if (active.startedAt) {
|
|
83
|
+
console.log(` ${bold('Started:')} ${active.startedAt}`);
|
|
84
|
+
}
|
|
85
|
+
if (active.prompter) {
|
|
86
|
+
console.log(` ${bold('Prompter:')} ${active.prompter}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(` ${neutral('No active task detected.')}`);
|
|
91
|
+
}
|
|
92
|
+
// Calculate changed LOC
|
|
93
|
+
try {
|
|
94
|
+
const summary = await getChangedLOC(config.logbookDir);
|
|
95
|
+
const activeLOC = `${summary.total} (+${summary.insertions}, -${summary.deletions})`;
|
|
96
|
+
console.log(`\n${bold('Changed LOC Count:')} ${highlight(activeLOC)}\n`);
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
// Print Settings section
|
|
100
|
+
console.log(`\n${bold('Settings (.project-logbook):')}`);
|
|
101
|
+
console.log(` ${bold('Project Name:')} ${config.projectName}`);
|
|
102
|
+
console.log(` ${bold('Logbook Directory:')} ${config.logbookDir}`);
|
|
103
|
+
console.log(` ${bold('Output Directory:')} ${config.outputDir}`);
|
|
104
|
+
console.log(` ${bold('Primary Color:')} ${config.primaryColor}`);
|
|
105
|
+
if (config.jiraBaseUrl) {
|
|
106
|
+
console.log(` ${bold('Jira Base URL:')} ${config.jiraBaseUrl}`);
|
|
107
|
+
}
|
|
108
|
+
if (config.jiraPrefix) {
|
|
109
|
+
console.log(` ${bold('Jira Prefix:')} ${config.jiraPrefix}`);
|
|
110
|
+
}
|
|
111
|
+
if (config.tags?.allowed) {
|
|
112
|
+
console.log(` ${bold('Allowed Tags:')} ${config.tags.allowed.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
// Print Stats section
|
|
115
|
+
console.log(`\n${bold('Stats:')}`);
|
|
116
|
+
console.log(` ${bold('Total Entries:')} ${totalCount}`);
|
|
117
|
+
console.log(` ${bold('Active Tasks:')} ${activeCount}`);
|
|
118
|
+
console.log(` ${bold('Completed Done:')} ${doneCount}`);
|
|
119
|
+
console.log(` ${bold('Ticket Drafts:')} ${draftCount}`);
|
|
120
|
+
console.log(` ${bold('Latest Entry:')} ${latestText}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
package/dist/commands/steer.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import { warning as warningStyle, header, error as errorStyle, success, errorMessage, bold, highlight, } from '../lib/theme.js';
|
|
4
4
|
import { getTemplatesDir } from '../lib/migrations.js';
|
|
5
5
|
export async function steer() {
|
|
6
6
|
const steerPath = join(getTemplatesDir(), 'steer.txt');
|
|
7
7
|
if (!fs.existsSync(steerPath)) {
|
|
8
|
-
console.error(
|
|
8
|
+
console.error(errorMessage('Error', 'Steering template not found.'));
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
const protocol = fs.readFileSync(steerPath, 'utf8');
|
|
12
12
|
// Add some color to headers and mandatory parts
|
|
13
13
|
const formatted = protocol
|
|
14
|
-
.replace(/^PROJECT LOGBOOK:.+$/m, (m) =>
|
|
15
|
-
.replace(/^Phase \d:.+$/gm, (m) =>
|
|
16
|
-
.replace(/logbook \w+/g, (m) =>
|
|
17
|
-
.replace(/\w+\.md/g, (m) =>
|
|
18
|
-
.replace(/Mandatory:/g,
|
|
19
|
-
.replace(/Storytelling Mode:/g,
|
|
20
|
-
.replace(/Quality Gate:/g,
|
|
14
|
+
.replace(/^PROJECT LOGBOOK:.+$/m, (m) => bold(highlight(m)))
|
|
15
|
+
.replace(/^Phase \d:.+$/gm, (m) => bold(m))
|
|
16
|
+
.replace(/logbook \w+/g, (m) => success(m))
|
|
17
|
+
.replace(/\w+\.md/g, (m) => header(m))
|
|
18
|
+
.replace(/Mandatory:/g, warningStyle('Mandatory:'))
|
|
19
|
+
.replace(/Storytelling Mode:/g, warningStyle('Storytelling Mode:'))
|
|
20
|
+
.replace(/Quality Gate:/g, errorStyle('Quality Gate:'));
|
|
21
21
|
console.log(formatted);
|
|
22
22
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import
|
|
2
|
+
import { success, header } from '../lib/theme.js';
|
|
3
3
|
import { getProjectStatus } from '../lib/migrations.js';
|
|
4
4
|
export async function upgrade() {
|
|
5
5
|
const projectStatus = await getProjectStatus();
|
|
6
6
|
let updatedCount = 0;
|
|
7
7
|
for (const file of projectStatus) {
|
|
8
8
|
if (file.status === 'UP_TO_DATE') {
|
|
9
|
-
console.log(
|
|
9
|
+
console.log(header(`[UP TO DATE] ${file.name}`));
|
|
10
10
|
}
|
|
11
11
|
else {
|
|
12
12
|
await fs.writeFile(file.localPath, file.templateContent);
|
|
13
|
-
console.log(
|
|
13
|
+
console.log(success(`[${file.status === 'MISSING' ? 'CREATED' : 'UPDATED'}] ${file.name}`));
|
|
14
14
|
updatedCount++;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
if (updatedCount === 0) {
|
|
18
|
-
console.log(
|
|
18
|
+
console.log(header('\nEverything is already up to date.'));
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
|
-
console.log(
|
|
21
|
+
console.log(success(`\nSuccessfully upgraded ${updatedCount} files.`));
|
|
22
22
|
}
|
|
23
23
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
4
|
-
import { join, dirname } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
const pkgPath = join(__dirname, '..', 'package.json');
|
|
9
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
3
|
+
import { getPackageVersion } from './lib/package-version.js';
|
|
10
4
|
const program = new Command();
|
|
11
|
-
program.name('logbook').description('A CLI tool for managing project logbooks.').version(
|
|
5
|
+
program.name('logbook').description('A CLI tool for managing project logbooks.').version(getPackageVersion());
|
|
12
6
|
program
|
|
13
7
|
.command('init')
|
|
14
8
|
.description('Initialize .project-logbook config and /logbook folder')
|
|
@@ -74,6 +68,13 @@ program
|
|
|
74
68
|
const { list } = await import('./commands/list.js');
|
|
75
69
|
await list(options);
|
|
76
70
|
});
|
|
71
|
+
program
|
|
72
|
+
.command('status')
|
|
73
|
+
.description('Show current active logbook task, configuration settings, and statistics')
|
|
74
|
+
.action(async () => {
|
|
75
|
+
const { status } = await import('./commands/status.js');
|
|
76
|
+
await status();
|
|
77
|
+
});
|
|
77
78
|
program
|
|
78
79
|
.command('log <message>')
|
|
79
80
|
.description('Append a timestamped log entry to the active log.md (e.g. logbook log "Did a thing")')
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { unified } from 'unified';
|
|
4
|
-
import remarkParse from 'remark-parse';
|
|
5
|
-
import remarkGfm from 'remark-gfm';
|
|
6
|
-
import remarkRehype from 'remark-rehype';
|
|
7
|
-
import rehypeSlug from 'rehype-slug';
|
|
8
|
-
import rehypeFormat from 'rehype-format';
|
|
9
|
-
import rehypeStringify from 'rehype-stringify';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
10
3
|
import matter from 'gray-matter';
|
|
11
4
|
import { layout, postTemplate } from './templates.js';
|
|
12
5
|
import { getGitCommits } from './git-helpers.js';
|
|
6
|
+
import { visitHastElements } from './hast-helpers.js';
|
|
7
|
+
import { createGeneralMarkdownProcessor, createEntryMarkdownProcessor } from './markdown-processors.js';
|
|
8
|
+
import { isExternalUrl, isExternalUrlOrAnchor } from './url-helpers.js';
|
|
9
|
+
import { readFileIfExists, pathExistsOrNull } from '../utils/fs.js';
|
|
10
|
+
import { getEntryFilePaths } from './entry-paths.js';
|
|
13
11
|
import { mdToHtmlWithImages, extractImagePathsFromMarkdown, copyImages, rehypeRewriteImagePaths, } from './image-helpers.js';
|
|
14
12
|
/** Rehype plugin: rewrite relative .md links to /index.html equivalents. */
|
|
15
13
|
const rehypeRewriteMdLinks = () => {
|
|
16
14
|
return (tree) => {
|
|
17
|
-
|
|
15
|
+
visitHastElements(tree, 'a', (node) => {
|
|
18
16
|
const href = node.properties?.href;
|
|
19
17
|
if (typeof href !== 'string')
|
|
20
18
|
return;
|
|
21
|
-
if (
|
|
19
|
+
if (isExternalUrlOrAnchor(href))
|
|
22
20
|
return;
|
|
23
21
|
const mdMatch = href.match(/^(.*?)\.md(#.*)?$/i);
|
|
24
22
|
if (!mdMatch)
|
|
@@ -29,33 +27,8 @@ const rehypeRewriteMdLinks = () => {
|
|
|
29
27
|
});
|
|
30
28
|
};
|
|
31
29
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (child.type === 'element') {
|
|
35
|
-
if (child.tagName === 'a')
|
|
36
|
-
visitor(child);
|
|
37
|
-
visitLinks(child, visitor);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const processor = unified()
|
|
42
|
-
.use(remarkParse)
|
|
43
|
-
.use(remarkGfm)
|
|
44
|
-
.use(remarkRehype)
|
|
45
|
-
.use(rehypeSlug)
|
|
46
|
-
.use(rehypeRewriteMdLinks)
|
|
47
|
-
.use(rehypeFormat)
|
|
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);
|
|
30
|
+
const processor = createGeneralMarkdownProcessor(rehypeRewriteMdLinks);
|
|
31
|
+
const entryProcessor = createEntryMarkdownProcessor(rehypeRewriteMdLinks, rehypeRewriteImagePaths);
|
|
59
32
|
export async function mdToHtml(md) {
|
|
60
33
|
const result = await processor.process(md);
|
|
61
34
|
return result.toString();
|
|
@@ -103,18 +76,24 @@ async function renderLinkedMdFiles(markdownSources, entryPath, entryOutputDir, c
|
|
|
103
76
|
const href = raw.split('#')[0];
|
|
104
77
|
if (!href)
|
|
105
78
|
continue;
|
|
106
|
-
if (
|
|
79
|
+
if (isExternalUrl(href))
|
|
107
80
|
continue;
|
|
108
81
|
if (seen.has(href))
|
|
109
82
|
continue;
|
|
110
83
|
seen.add(href);
|
|
111
84
|
let sourcePath = join(entryPath, href);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (!(await fs.pathExists(sourcePath))) {
|
|
85
|
+
sourcePath = (await pathExistsOrNull(sourcePath)) ?? join(process.cwd(), href);
|
|
86
|
+
if (!sourcePath || !(await pathExistsOrNull(sourcePath))) {
|
|
115
87
|
console.warn(` Linked file not found, skipping: ${href}`);
|
|
116
88
|
continue;
|
|
117
89
|
}
|
|
90
|
+
// Path traversal protection: ensure resolved path stays within project boundaries
|
|
91
|
+
const resolvedPath = resolve(sourcePath);
|
|
92
|
+
const projectRoot = resolve(process.cwd());
|
|
93
|
+
if (!resolvedPath.startsWith(projectRoot + '/') && resolvedPath !== projectRoot) {
|
|
94
|
+
console.warn(` Path traversal blocked: ${href}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
118
97
|
const mdContent = await fs.readFile(sourcePath, 'utf8');
|
|
119
98
|
const bodyHtml = await mdToHtml(mdContent);
|
|
120
99
|
const title = href.replace(/\.md$/i, '');
|
|
@@ -135,12 +114,11 @@ async function renderLinkedMdFiles(markdownSources, entryPath, entryOutputDir, c
|
|
|
135
114
|
}
|
|
136
115
|
}
|
|
137
116
|
export async function renderPost(data, i, allEntries, ctx) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const logRaw = (await fs.pathExists(logPath)) ? await fs.readFile(logPath, 'utf8') : '';
|
|
117
|
+
const entryFilePaths = getEntryFilePaths(ctx.logbookDir, data.slug);
|
|
118
|
+
const entryPath = entryFilePaths.index.replace(/\/index\.md$/, '');
|
|
119
|
+
const { content, content: summaryContent } = matter(await fs.readFile(entryFilePaths.index, 'utf8'));
|
|
120
|
+
const ticketRaw = await readFileIfExists(entryFilePaths.ticket);
|
|
121
|
+
const logRaw = await readFileIfExists(entryFilePaths.log);
|
|
144
122
|
// Process with image collection for index.md content
|
|
145
123
|
const { html: storyHtml, imagePaths: storyImages } = await mdToHtmlWithImages(content.replace(/^#\s+.+$/m, '').trim());
|
|
146
124
|
// Collect images from all markdown sources
|
|
@@ -151,7 +129,7 @@ export async function renderPost(data, i, allEntries, ctx) {
|
|
|
151
129
|
const jiraUrl = ctx.config.jiraBaseUrl && data.ticket ? `${ctx.config.jiraBaseUrl}${data.ticket}` : undefined;
|
|
152
130
|
const prev = i > 0 ? allEntries[i - 1] : null;
|
|
153
131
|
const next = i < allEntries.length - 1 ? allEntries[i + 1] : null;
|
|
154
|
-
const
|
|
132
|
+
const { header: postHeader, content: postMainContent } = postTemplate({
|
|
155
133
|
...data,
|
|
156
134
|
title: data.title ?? data.slug,
|
|
157
135
|
harness: data.harness ?? '',
|
|
@@ -173,8 +151,8 @@ export async function renderPost(data, i, allEntries, ctx) {
|
|
|
173
151
|
description: descriptionRaw.substring(0, 160),
|
|
174
152
|
bodySlug: data.slug,
|
|
175
153
|
buildMeta,
|
|
176
|
-
header:
|
|
177
|
-
content:
|
|
154
|
+
header: postHeader,
|
|
155
|
+
content: postMainContent,
|
|
178
156
|
});
|
|
179
157
|
const entryOutputDir = join(ctx.outputDir, data.slug);
|
|
180
158
|
await fs.mkdirp(entryOutputDir);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LogbookConfig } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Set up the output directory: create it, copy static assets (favicon, styles, script).
|
|
4
|
+
*/
|
|
5
|
+
export declare function setupOutputDirectory(outputDir: string, config: LogbookConfig): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Process README.md if it exists and return rendered HTML + image paths.
|
|
8
|
+
*/
|
|
9
|
+
export declare function processReadme(projectRoot: string): Promise<{
|
|
10
|
+
html: string;
|
|
11
|
+
imagePaths: string[];
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Copy README images to the output directory.
|
|
15
|
+
*/
|
|
16
|
+
export declare function copyReadmeImages(projectRoot: string, outputDir: string, imagePaths: string[]): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Process the About content and return rendered HTML.
|
|
19
|
+
*/
|
|
20
|
+
export declare function processAboutContent(aboutContent: string): Promise<string>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for the build command.
|
|
3
|
+
* Breaks down the monolithic build() function into testable, reusable steps.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { warning } from './theme.js';
|
|
8
|
+
import { URL } from 'node:url';
|
|
9
|
+
import { mdToHtmlWithImages, copyImages } from './image-helpers.js';
|
|
10
|
+
import { getStyles } from './styles.js';
|
|
11
|
+
import { getClientScript } from './logbook-client.js';
|
|
12
|
+
import { mdToHtml } from './build-helpers.js';
|
|
13
|
+
/**
|
|
14
|
+
* Set up the output directory: create it, copy static assets (favicon, styles, script).
|
|
15
|
+
*/
|
|
16
|
+
export async function setupOutputDirectory(outputDir, config) {
|
|
17
|
+
await fs.remove(outputDir);
|
|
18
|
+
await fs.mkdirp(outputDir);
|
|
19
|
+
await fs.copyFile(new URL('../templates/favicon.svg', import.meta.url), join(outputDir, 'favicon.svg'));
|
|
20
|
+
await fs.writeFile(join(outputDir, 'style.css'), getStyles(config.primaryColor));
|
|
21
|
+
await fs.writeFile(join(outputDir, 'logbook.js'), getClientScript());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Process README.md if it exists and return rendered HTML + image paths.
|
|
25
|
+
*/
|
|
26
|
+
export async function processReadme(projectRoot) {
|
|
27
|
+
const readmePath = join(projectRoot, 'README.md');
|
|
28
|
+
let readmeHtml = '';
|
|
29
|
+
let readmeImages = [];
|
|
30
|
+
try {
|
|
31
|
+
if (await fs.pathExists(readmePath)) {
|
|
32
|
+
const readmeContent = await fs.readFile(readmePath, 'utf8');
|
|
33
|
+
const { html, imagePaths } = await mdToHtmlWithImages(readmeContent);
|
|
34
|
+
readmeHtml = html;
|
|
35
|
+
readmeImages = imagePaths;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.warn(warning(`Warning: Failed to read README.md: ${error instanceof Error ? error.message : error}`));
|
|
40
|
+
readmeHtml = '<p>README unavailable.</p>';
|
|
41
|
+
}
|
|
42
|
+
return { html: readmeHtml, imagePaths: readmeImages };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Copy README images to the output directory.
|
|
46
|
+
*/
|
|
47
|
+
export async function copyReadmeImages(projectRoot, outputDir, imagePaths) {
|
|
48
|
+
if (imagePaths.length > 0) {
|
|
49
|
+
await copyImages(projectRoot, outputDir, imagePaths);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Process the About content and return rendered HTML.
|
|
54
|
+
*/
|
|
55
|
+
export async function processAboutContent(aboutContent) {
|
|
56
|
+
return await mdToHtml(aboutContent);
|
|
57
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -12,3 +12,19 @@ export interface LogbookConfig {
|
|
|
12
12
|
}
|
|
13
13
|
export declare const DEFAULT_CONFIG: LogbookConfig;
|
|
14
14
|
export declare function getConfig(cwd?: string): LogbookConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Get the absolute path to the logbook directory.
|
|
17
|
+
* Convenience function to avoid repeating: join(process.cwd(), config.logbookDir)
|
|
18
|
+
* @param config - The logbook config
|
|
19
|
+
* @param cwd - Optional working directory (defaults to process.cwd())
|
|
20
|
+
* @returns Absolute path to the logbook directory
|
|
21
|
+
*/
|
|
22
|
+
export declare function getLogbookDirPath(config: LogbookConfig, cwd?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Get the absolute path to the output directory.
|
|
25
|
+
* Convenience function to avoid repeating: join(process.cwd(), config.outputDir)
|
|
26
|
+
* @param config - The logbook config
|
|
27
|
+
* @param cwd - Optional working directory (defaults to process.cwd())
|
|
28
|
+
* @returns Absolute path to the output directory
|
|
29
|
+
*/
|
|
30
|
+
export declare function getOutputDirPath(config: LogbookConfig, cwd?: string): string;
|
package/dist/lib/config.js
CHANGED
|
@@ -39,3 +39,23 @@ export function getConfig(cwd = process.cwd()) {
|
|
|
39
39
|
}
|
|
40
40
|
return DEFAULT_CONFIG;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the absolute path to the logbook directory.
|
|
44
|
+
* Convenience function to avoid repeating: join(process.cwd(), config.logbookDir)
|
|
45
|
+
* @param config - The logbook config
|
|
46
|
+
* @param cwd - Optional working directory (defaults to process.cwd())
|
|
47
|
+
* @returns Absolute path to the logbook directory
|
|
48
|
+
*/
|
|
49
|
+
export function getLogbookDirPath(config, cwd = process.cwd()) {
|
|
50
|
+
return join(cwd, config.logbookDir);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the absolute path to the output directory.
|
|
54
|
+
* Convenience function to avoid repeating: join(process.cwd(), config.outputDir)
|
|
55
|
+
* @param config - The logbook config
|
|
56
|
+
* @param cwd - Optional working directory (defaults to process.cwd())
|
|
57
|
+
* @returns Absolute path to the output directory
|
|
58
|
+
*/
|
|
59
|
+
export function getOutputDirPath(config, cwd = process.cwd()) {
|
|
60
|
+
return join(cwd, config.outputDir);
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry ID utilities for handling slug/ticket pairs.
|
|
3
|
+
* Normalizes the relationship between entry slug and optional ticket ID.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Get the display ID for an entry.
|
|
7
|
+
* Falls back to slug if ticket is not available.
|
|
8
|
+
* Used when displaying entries in lists, timelines, etc.
|
|
9
|
+
* @param ticket - Optional ticket ID (e.g., "LB-43", "PROJ-100")
|
|
10
|
+
* @param slug - Fallback slug (e.g., "LB-43-my-feature")
|
|
11
|
+
* @returns The ticket if available, otherwise the slug
|
|
12
|
+
*/
|
|
13
|
+
export declare function getEntryDisplayId(ticket: string | undefined, slug: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get the title/display name for an entry.
|
|
16
|
+
* Falls back to slug if title is not available.
|
|
17
|
+
* Used when displaying entries without a proper title.
|
|
18
|
+
* @param title - Optional title (e.g., "Implement feature X")
|
|
19
|
+
* @param slug - Fallback slug (e.g., "LB-43-my-feature")
|
|
20
|
+
* @returns The title if available, otherwise the slug
|
|
21
|
+
*/
|
|
22
|
+
export declare function getEntryDisplayTitle(title: string | undefined, slug: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry ID utilities for handling slug/ticket pairs.
|
|
3
|
+
* Normalizes the relationship between entry slug and optional ticket ID.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Get the display ID for an entry.
|
|
7
|
+
* Falls back to slug if ticket is not available.
|
|
8
|
+
* Used when displaying entries in lists, timelines, etc.
|
|
9
|
+
* @param ticket - Optional ticket ID (e.g., "LB-43", "PROJ-100")
|
|
10
|
+
* @param slug - Fallback slug (e.g., "LB-43-my-feature")
|
|
11
|
+
* @returns The ticket if available, otherwise the slug
|
|
12
|
+
*/
|
|
13
|
+
export function getEntryDisplayId(ticket, slug) {
|
|
14
|
+
return ticket || slug;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get the title/display name for an entry.
|
|
18
|
+
* Falls back to slug if title is not available.
|
|
19
|
+
* Used when displaying entries without a proper title.
|
|
20
|
+
* @param title - Optional title (e.g., "Implement feature X")
|
|
21
|
+
* @param slug - Fallback slug (e.g., "LB-43-my-feature")
|
|
22
|
+
* @returns The title if available, otherwise the slug
|
|
23
|
+
*/
|
|
24
|
+
export function getEntryDisplayTitle(title, slug) {
|
|
25
|
+
return title || slug;
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encapsulates knowledge of logbook entry folder structure.
|
|
3
|
+
* Provides consistent path construction for entry-related files.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Get the base directory path for a logbook entry.
|
|
7
|
+
* @param logbookDir - The root logbook directory (e.g., "/project/logbook")
|
|
8
|
+
* @param entrySlug - The entry's slug (e.g., "LB-43-my-feature")
|
|
9
|
+
* @returns Path to the entry folder (e.g., "/project/logbook/LB-43-my-feature")
|
|
10
|
+
*/
|
|
11
|
+
export declare function getEntryPath(logbookDir: string, entrySlug: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Get all standard entry file paths at once.
|
|
14
|
+
* Useful when you need to access index, ticket, and log files.
|
|
15
|
+
* @param logbookDir - The root logbook directory
|
|
16
|
+
* @param entrySlug - The entry's slug
|
|
17
|
+
* @returns Object with paths to index.md, ticket.md, and log.md
|
|
18
|
+
*/
|
|
19
|
+
export declare function getEntryFilePaths(logbookDir: string, entrySlug: string): {
|
|
20
|
+
index: string;
|
|
21
|
+
ticket: string;
|
|
22
|
+
log: string;
|
|
23
|
+
};
|