project-logbook 0.3.3 ā 0.4.0
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 +54 -76
- package/dist/commands/init.js +11 -11
- package/dist/commands/lint.js +27 -10
- 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 +138 -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 +30 -67
- package/dist/lib/build-steps.d.ts +20 -0
- package/dist/lib/build-steps.js +57 -0
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +27 -3
- 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 +32 -1
- package/dist/lib/git-helpers.js +119 -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 +22 -0
- package/dist/lib/markdown-processors.js +68 -0
- package/dist/lib/package-version.d.ts +5 -0
- package/dist/lib/package-version.js +16 -0
- package/dist/lib/rss.d.ts +29 -0
- package/dist/lib/rss.js +77 -0
- package/dist/lib/styles.js +5 -2
- package/dist/lib/template-helpers.d.ts +4 -3
- package/dist/lib/template-helpers.js +51 -27
- package/dist/lib/template-types.d.ts +2 -2
- package/dist/lib/templates.d.ts +12 -3
- package/dist/lib/templates.js +59 -52
- 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 +4 -0
- package/dist/linters/technical-log.d.ts +7 -0
- package/dist/linters/technical-log.js +72 -0
- package/dist/templates/CONTRIBUTING.md +12 -3
- package/dist/templates/index.md +10 -6
- package/dist/templates/log.md +5 -0
- package/dist/templates/logbook-client.js +42 -16
- package/dist/templates/steer.txt +21 -5
- package/dist/templates/styles.css +121 -0
- package/dist/utils/date.d.ts +30 -1
- package/dist/utils/date.js +68 -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/dist/utils/log-timeline.d.ts +69 -0
- package/dist/utils/log-timeline.js +218 -0
- package/package.json +4 -2
- package/src/templates/CONTRIBUTING.md +12 -3
- package/src/templates/index.md +10 -6
- package/src/templates/log.md +5 -0
- package/src/templates/logbook-client.js +42 -16
- package/src/templates/steer.txt +21 -5
- package/src/templates/styles.css +121 -0
package/dist/commands/build.js
CHANGED
|
@@ -1,73 +1,34 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
4
|
-
import { getConfig, getWorkspaces } from '../lib/config.js';
|
|
3
|
+
import { error, neutral, success } from '../lib/theme.js';
|
|
4
|
+
import { getConfig, getWorkspaces, getLogbookDirPath, getOutputDirPath } from '../lib/config.js';
|
|
5
5
|
import { layout, timelineTemplate } from '../lib/templates.js';
|
|
6
|
-
import {
|
|
7
|
-
import { getClientScript } from '../lib/logbook-client.js';
|
|
8
|
-
import { mdToHtml, buildProjectMdFiles, renderPost, generateBuildMeta } from '../lib/build-helpers.js';
|
|
9
|
-
import { mdToHtmlWithImages, copyImages } from '../lib/image-helpers.js';
|
|
6
|
+
import { buildProjectMdFiles, renderPost, generateBuildMeta } from '../lib/build-helpers.js';
|
|
10
7
|
import { getGitCommits, getGitTags } from '../lib/git-helpers.js';
|
|
11
|
-
import {
|
|
8
|
+
import { setupOutputDirectory, processReadme, copyReadmeImages, processAboutContent } from '../lib/build-steps.js';
|
|
9
|
+
import { formatAbsoluteDate, getMonthYear, getSortTime, formatDateTimeForDisplay } from '../utils/date.js';
|
|
12
10
|
import { getLogbookEntries } from '../utils/fs.js';
|
|
11
|
+
import { toDisplayString, asString, asStringArray } from '../utils/frontmatter.js';
|
|
13
12
|
import { getAboutContent } from '../lib/about-content.js';
|
|
13
|
+
import { getPackageVersion } from '../lib/package-version.js';
|
|
14
14
|
import { groupTimelineItems } from '../utils/timeline-helpers.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
};
|
|
15
|
+
import { getEntryDisplayId, getEntryDisplayTitle } from '../lib/entry-id.js';
|
|
16
|
+
import { generateRssFeed, validateRssXml } from '../lib/rss.js';
|
|
17
|
+
const version = getPackageVersion();
|
|
27
18
|
export async function build() {
|
|
28
19
|
const config = getConfig();
|
|
29
|
-
const logbookDir =
|
|
30
|
-
const outputDir =
|
|
31
|
-
const buildTime = new Date()
|
|
32
|
-
.toLocaleString('de-DE', {
|
|
33
|
-
year: 'numeric',
|
|
34
|
-
month: '2-digit',
|
|
35
|
-
day: '2-digit',
|
|
36
|
-
hour: '2-digit',
|
|
37
|
-
minute: '2-digit',
|
|
38
|
-
second: '2-digit',
|
|
39
|
-
hour12: false,
|
|
40
|
-
})
|
|
41
|
-
.replace(',', '');
|
|
20
|
+
const logbookDir = getLogbookDirPath(config);
|
|
21
|
+
const outputDir = getOutputDirPath(config);
|
|
22
|
+
const buildTime = formatDateTimeForDisplay(new Date());
|
|
42
23
|
if (!(await fs.pathExists(logbookDir))) {
|
|
43
|
-
console.error(
|
|
24
|
+
console.error(error(`Error: Logbook directory '${config.logbookDir}' not found.`));
|
|
44
25
|
return;
|
|
45
26
|
}
|
|
46
|
-
await
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await
|
|
50
|
-
await
|
|
51
|
-
const readmePath = join(process.cwd(), 'README.md');
|
|
52
|
-
let readmeHtml = '';
|
|
53
|
-
let readmeImages = [];
|
|
54
|
-
try {
|
|
55
|
-
if (await fs.pathExists(readmePath)) {
|
|
56
|
-
const readmeContent = await fs.readFile(readmePath, 'utf8');
|
|
57
|
-
const { html, imagePaths } = await mdToHtmlWithImages(readmeContent);
|
|
58
|
-
readmeHtml = html;
|
|
59
|
-
readmeImages = imagePaths;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
console.warn(chalk.yellow('Warning: Failed to read README.md:', error instanceof Error ? error.message : error));
|
|
64
|
-
readmeHtml = '<p>README unavailable.</p>';
|
|
65
|
-
}
|
|
66
|
-
// Copy README images to output
|
|
67
|
-
if (readmeImages.length > 0) {
|
|
68
|
-
await copyImages(process.cwd(), outputDir, readmeImages);
|
|
69
|
-
}
|
|
70
|
-
const aboutHtml = await mdToHtml(getAboutContent());
|
|
27
|
+
await setupOutputDirectory(outputDir, config);
|
|
28
|
+
const projectRoot = process.cwd();
|
|
29
|
+
const { html: readmeHtml, imagePaths: readmeImages } = await processReadme(projectRoot);
|
|
30
|
+
await copyReadmeImages(projectRoot, outputDir, readmeImages);
|
|
31
|
+
const aboutHtml = await processAboutContent(getAboutContent());
|
|
71
32
|
const logbookEntries = await getLogbookEntries(logbookDir);
|
|
72
33
|
const timelineEntries = [];
|
|
73
34
|
const availableWorkspaces = getWorkspaces();
|
|
@@ -82,9 +43,7 @@ export async function build() {
|
|
|
82
43
|
? firstParagraph.trim().substring(0, 200).replace(/[*#`]/g, '') + '...'
|
|
83
44
|
: 'No summary available.';
|
|
84
45
|
}
|
|
85
|
-
const entryWorkspaces =
|
|
86
|
-
? data.workspaces.filter((ws) => typeof ws === 'string' && availableWorkspaces.includes(ws))
|
|
87
|
-
: [];
|
|
46
|
+
const entryWorkspaces = asStringArray(data.workspaces).filter((ws) => availableWorkspaces.includes(ws));
|
|
88
47
|
const dateStartValue = data.dateStart;
|
|
89
48
|
const dateStart = typeof dateStartValue === 'string'
|
|
90
49
|
? dateStartValue
|
|
@@ -92,10 +51,10 @@ export async function build() {
|
|
|
92
51
|
? dateStartValue.toISOString()
|
|
93
52
|
: '';
|
|
94
53
|
// Skip DRAFT entries (no valid dateStart) ā they break the timeline
|
|
95
|
-
if (!dateStart || dateStart.includes('{{'))
|
|
54
|
+
if (!dateStart || dateStart.includes('{{') || dateStart === '[DATE_START]')
|
|
96
55
|
continue;
|
|
97
56
|
const dateEndValue = data.dateEnd;
|
|
98
|
-
const dateEnd = typeof dateEndValue === 'string'
|
|
57
|
+
const dateEnd = typeof dateEndValue === 'string' && dateEndValue !== '[DATE_END]'
|
|
99
58
|
? dateEndValue
|
|
100
59
|
: dateEndValue instanceof Date
|
|
101
60
|
? dateEndValue.toISOString()
|
|
@@ -105,14 +64,14 @@ export async function build() {
|
|
|
105
64
|
slug,
|
|
106
65
|
dateStart,
|
|
107
66
|
dateEnd,
|
|
108
|
-
displayDate:
|
|
67
|
+
displayDate: formatAbsoluteDate(dateStart),
|
|
109
68
|
sortTime: getSortTime(dateStart, dateEnd),
|
|
110
|
-
summary:
|
|
111
|
-
ticket:
|
|
69
|
+
summary: asString(summary, 'No summary available.') || '',
|
|
70
|
+
ticket: getEntryDisplayId(asString(data.ticket), slug),
|
|
112
71
|
monthGroup: getMonthYear(dateStart),
|
|
113
|
-
tags:
|
|
72
|
+
tags: asStringArray(data.tags),
|
|
114
73
|
workspaces: entryWorkspaces,
|
|
115
|
-
title:
|
|
74
|
+
title: getEntryDisplayTitle(asString(data.title), slug),
|
|
116
75
|
harness: toDisplayString(data.harness),
|
|
117
76
|
llm: toDisplayString(data.llm),
|
|
118
77
|
prompter: toDisplayString(data.prompter),
|
|
@@ -125,8 +84,7 @@ export async function build() {
|
|
|
125
84
|
sha: c.sha,
|
|
126
85
|
message: c.message,
|
|
127
86
|
timestamp: c.timestamp,
|
|
128
|
-
|
|
129
|
-
displayDate: formatRelativeDate(c.timestamp),
|
|
87
|
+
displayDate: formatAbsoluteDate(c.timestamp),
|
|
130
88
|
monthGroup: getMonthYear(c.timestamp),
|
|
131
89
|
sortTime: getSortTime(c.timestamp),
|
|
132
90
|
}));
|
|
@@ -134,11 +92,11 @@ export async function build() {
|
|
|
134
92
|
kind: 'tag',
|
|
135
93
|
name: t.name,
|
|
136
94
|
timestamp: t.timestamp,
|
|
137
|
-
displayDate:
|
|
95
|
+
displayDate: formatAbsoluteDate(t.timestamp),
|
|
138
96
|
monthGroup: getMonthYear(t.timestamp),
|
|
139
97
|
sortTime: getSortTime(t.timestamp),
|
|
140
98
|
}));
|
|
141
|
-
console.log(
|
|
99
|
+
console.log(neutral(`Found ${timelineEntries.length} entr${timelineEntries.length === 1 ? 'y' : 'ies'}, ${commitItems.length} commit${commitItems.length === 1 ? '' : 's'} (max 100), and ${tagItems.length} tag${tagItems.length === 1 ? '' : 's'}.`));
|
|
142
100
|
timelineEntries.sort((a, b) => {
|
|
143
101
|
const aEnd = a.dateEnd ? new Date(a.dateEnd).getTime() : new Date(a.dateStart).getTime();
|
|
144
102
|
const bEnd = b.dateEnd ? new Date(b.dateEnd).getTime() : new Date(b.dateStart).getTime();
|
|
@@ -164,7 +122,7 @@ export async function build() {
|
|
|
164
122
|
}
|
|
165
123
|
group.items.push(item);
|
|
166
124
|
}
|
|
167
|
-
const
|
|
125
|
+
const { header: timelineHeader, content: timelineMainContent } = timelineTemplate({
|
|
168
126
|
projectName: config.projectName,
|
|
169
127
|
version,
|
|
170
128
|
buildTime,
|
|
@@ -173,8 +131,10 @@ export async function build() {
|
|
|
173
131
|
aboutHtml,
|
|
174
132
|
jiraBaseUrl: config.jiraBaseUrl,
|
|
175
133
|
jiraPrefix: config.jiraPrefix,
|
|
134
|
+
repositoryUrl: config.repositoryUrl,
|
|
176
135
|
currentPage,
|
|
177
136
|
totalPages,
|
|
137
|
+
includeRssLink: currentPage === 1, // Only show RSS link on first page
|
|
178
138
|
}); // prettier-ignore
|
|
179
139
|
const buildMeta = generateBuildMeta(version, buildTime);
|
|
180
140
|
const timelineHtml = layout({
|
|
@@ -182,8 +142,9 @@ export async function build() {
|
|
|
182
142
|
projectName: config.projectName,
|
|
183
143
|
basePath: './',
|
|
184
144
|
buildMeta,
|
|
185
|
-
header:
|
|
186
|
-
content:
|
|
145
|
+
header: timelineHeader,
|
|
146
|
+
content: timelineMainContent,
|
|
147
|
+
includeRssLink: currentPage === 1, // Only show RSS link on first page
|
|
187
148
|
});
|
|
188
149
|
const fileName = currentPage === 1 ? 'index.html' : `index-${currentPage}.html`;
|
|
189
150
|
await fs.writeFile(join(outputDir, fileName), timelineHtml);
|
|
@@ -193,5 +154,22 @@ export async function build() {
|
|
|
193
154
|
version,
|
|
194
155
|
buildTime,
|
|
195
156
|
});
|
|
196
|
-
|
|
157
|
+
// Generate RSS feed
|
|
158
|
+
const rssConfig = {
|
|
159
|
+
title: `${config.projectName} - Recent Updates`,
|
|
160
|
+
description: `Latest logbook entries from ${config.projectName}`,
|
|
161
|
+
language: 'en',
|
|
162
|
+
feed_url: `${config.projectName.toLowerCase().replace(/\s+/g, '-')}/rss.xml`,
|
|
163
|
+
site_url: config.projectName,
|
|
164
|
+
generator: `Project Logbook CLI v${version}`,
|
|
165
|
+
};
|
|
166
|
+
const rssXml = generateRssFeed(timelineEntries, rssConfig);
|
|
167
|
+
// Basic validation
|
|
168
|
+
if (!validateRssXml(rssXml)) {
|
|
169
|
+
console.error(error('Error: Generated RSS feed failed basic validation.'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
await fs.writeFile(join(outputDir, 'rss.xml'), rssXml);
|
|
173
|
+
console.log(neutral(` Generated RSS feed with ${Math.min(50, timelineEntries.length)} entries`));
|
|
174
|
+
console.log(success(`\nSuccessfully built logbook to ${config.outputDir}/`));
|
|
197
175
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
4
|
import { DEFAULT_CONFIG } from '../lib/config.js';
|
|
6
5
|
import { getTemplatesDir } from '../lib/migrations.js';
|
|
6
|
+
import { warning, info, highlight, success } from '../lib/theme.js';
|
|
7
7
|
const rl = createInterface({
|
|
8
8
|
input: process.stdin,
|
|
9
9
|
output: process.stdout,
|
|
@@ -34,22 +34,22 @@ export async function init() {
|
|
|
34
34
|
const contributingPath = join(cwd, 'CONTRIBUTING.md');
|
|
35
35
|
// 1. Config file
|
|
36
36
|
if (await fs.pathExists(configPath)) {
|
|
37
|
-
console.log(
|
|
37
|
+
console.log(warning('Already initialized. .project-logbook exists.'));
|
|
38
38
|
rl.close();
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
console.log(
|
|
41
|
+
console.log(info('\nš Initializing project-logbook\n'));
|
|
42
42
|
// Detect project name from package.json
|
|
43
43
|
const detectedName = readPackageJsonName(cwd);
|
|
44
44
|
const defaultName = detectedName || DEFAULT_CONFIG.projectName;
|
|
45
|
-
const nameInput = await question(
|
|
45
|
+
const nameInput = await question(highlight(`Project name (press Enter for "${defaultName}"): `));
|
|
46
46
|
const projectName = nameInput || defaultName;
|
|
47
47
|
// Optional Jira config
|
|
48
|
-
const jiraPrefixInput = await question(
|
|
48
|
+
const jiraPrefixInput = await question(highlight('Jira project prefix (e.g. MYAPP, leave blank to skip): '));
|
|
49
49
|
const jiraPrefix = jiraPrefixInput || undefined;
|
|
50
50
|
let jiraBaseUrl;
|
|
51
51
|
if (jiraPrefix) {
|
|
52
|
-
const jiraUrlInput = await question(
|
|
52
|
+
const jiraUrlInput = await question(highlight('Jira base URL (e.g. https://yourorg.atlassian.net, leave blank to skip): '));
|
|
53
53
|
jiraBaseUrl = jiraUrlInput || undefined;
|
|
54
54
|
}
|
|
55
55
|
rl.close();
|
|
@@ -63,21 +63,21 @@ export async function init() {
|
|
|
63
63
|
if (jiraBaseUrl)
|
|
64
64
|
config.jiraBaseUrl = jiraBaseUrl;
|
|
65
65
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
66
|
-
console.log(
|
|
66
|
+
console.log(success('\nā Created .project-logbook config file.'));
|
|
67
67
|
// 2. Logbook directory
|
|
68
68
|
if (!(await fs.pathExists(logbookDir))) {
|
|
69
69
|
await fs.mkdirp(logbookDir);
|
|
70
|
-
console.log(
|
|
70
|
+
console.log(success(`ā Created ${DEFAULT_CONFIG.logbookDir}/ directory.`));
|
|
71
71
|
}
|
|
72
72
|
// 3. CONTRIBUTING.md (from template)
|
|
73
73
|
if (!(await fs.pathExists(contributingPath))) {
|
|
74
74
|
const templatePath = join(getTemplatesDir(), 'CONTRIBUTING.md');
|
|
75
75
|
if (await fs.pathExists(templatePath)) {
|
|
76
76
|
await fs.copyFile(templatePath, contributingPath);
|
|
77
|
-
console.log(
|
|
77
|
+
console.log(success('ā Created CONTRIBUTING.md with agentic workflow protocol.'));
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
// 4. Hint
|
|
81
|
-
console.log(
|
|
82
|
-
console.log(
|
|
81
|
+
console.log(info('\nš” Tip: Edit .project-logbook to customize primaryColor, add more tags, or configure Jira settings.'));
|
|
82
|
+
console.log(info(' Run `logbook new` to create your first entry.\n'));
|
|
83
83
|
}
|
package/dist/commands/lint.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
4
|
-
import { getConfig } from '../lib/config.js';
|
|
3
|
+
import { error as errorStyle, header, success as successStyle, neutral as neutralStyle } from '../lib/theme.js';
|
|
4
|
+
import { getConfig, getLogbookDirPath } from '../lib/config.js';
|
|
5
5
|
import { runLinters } from '../lib/lint-runner.js';
|
|
6
6
|
import { getLogbookEntries } from '../utils/fs.js';
|
|
7
|
+
import { getActiveEntry } from '../lib/session.js';
|
|
8
|
+
import { getLastLogEntry } from '../utils/log-timeline.js';
|
|
7
9
|
export async function lint() {
|
|
8
10
|
const config = getConfig();
|
|
9
|
-
const logbookDir =
|
|
11
|
+
const logbookDir = getLogbookDirPath(config);
|
|
10
12
|
if (!(await fs.pathExists(logbookDir))) {
|
|
11
|
-
console.error(
|
|
13
|
+
console.error(errorStyle(`Error: Logbook directory '${config.logbookDir}' not found.`));
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
16
|
let overallSuccess = true;
|
|
17
|
+
// Show last log entry nudge if an entry is active
|
|
18
|
+
const active = await getActiveEntry();
|
|
19
|
+
if (active) {
|
|
20
|
+
const logPath = join(logbookDir, active.slug, 'log.md');
|
|
21
|
+
if (await fs.pathExists(logPath)) {
|
|
22
|
+
const logContent = await fs.readFile(logPath, 'utf8');
|
|
23
|
+
const lastEntry = getLastLogEntry(logContent);
|
|
24
|
+
if (lastEntry) {
|
|
25
|
+
const truncated = lastEntry.message.length > 80 ? lastEntry.message.slice(0, 77) + '...' : lastEntry.message;
|
|
26
|
+
console.log(header(`Active entry: ${active.slug}`));
|
|
27
|
+
console.log(` Last Log: ${lastEntry.isoTimestamp}: ${truncated}`);
|
|
28
|
+
console.log(neutralStyle(` ā Use 'logbook log "<message>"' if there's anything to add.\n`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
// 1. Project-level checks
|
|
16
|
-
console.log(
|
|
33
|
+
console.log(header('Checking project integrity...'));
|
|
17
34
|
const projectSuccess = await runLinters({ config });
|
|
18
35
|
if (!projectSuccess)
|
|
19
36
|
overallSuccess = false;
|
|
@@ -23,7 +40,7 @@ export async function lint() {
|
|
|
23
40
|
let skippedCount = 0; // Re-introducing skippedCount
|
|
24
41
|
for (const entry of logbookEntries) {
|
|
25
42
|
if (!entry.hasIndex) {
|
|
26
|
-
console.error(
|
|
43
|
+
console.error(errorStyle(` [MISSING] ${entry.slug}/index.md`));
|
|
27
44
|
overallSuccess = false;
|
|
28
45
|
continue;
|
|
29
46
|
}
|
|
@@ -48,12 +65,12 @@ export async function lint() {
|
|
|
48
65
|
}
|
|
49
66
|
if (overallSuccess) {
|
|
50
67
|
const skippedNote = skippedCount > 0
|
|
51
|
-
?
|
|
68
|
+
? neutralStyle(` (${skippedCount} DRAFT ${skippedCount === 1 ? 'entry' : 'entries'} skipped)`)
|
|
52
69
|
: '';
|
|
53
|
-
console.log(
|
|
70
|
+
console.log(successStyle(`\nAll ${passedCount} entries passed linting!`) + skippedNote);
|
|
54
71
|
}
|
|
55
72
|
else {
|
|
56
|
-
console.log(
|
|
73
|
+
console.log(errorStyle('\nLinting failed with errors. If you are a LLM, try to fix the errors.'));
|
|
57
74
|
process.exit(1);
|
|
58
75
|
}
|
|
59
76
|
}
|
|
@@ -66,5 +83,5 @@ function isDraft(entry) {
|
|
|
66
83
|
// An entry is a draft if:
|
|
67
84
|
// 1. The 'dateStart' key is missing from frontmatter (i.e., undefined or null).
|
|
68
85
|
// 2. OR the string representation of 'dateStart' is '[DATE_START]'.
|
|
69
|
-
return !frontmatter?.dateStart || String(frontmatter.dateStart) === 'DATE_START';
|
|
86
|
+
return !frontmatter?.dateStart || String(frontmatter.dateStart) === '[DATE_START]';
|
|
70
87
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import {
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { getConfig } from '../lib/config.js';
|
|
2
|
+
import { getConfig, getLogbookDirPath } from '../lib/config.js';
|
|
5
3
|
import { getLogbookEntries } from '../utils/fs.js';
|
|
6
4
|
import { stripAnsi } from '../utils/string.js';
|
|
7
5
|
import { parseTicketId } from '../utils/id.js';
|
|
8
6
|
import { getActiveEntry } from '../lib/session.js';
|
|
7
|
+
import { asString } from '../utils/frontmatter.js';
|
|
8
|
+
import { error, neutral, warning, highlight, success, bold } from '../lib/theme.js';
|
|
9
9
|
export async function list(options = {}) {
|
|
10
10
|
const config = getConfig();
|
|
11
|
-
const logbookDir =
|
|
11
|
+
const logbookDir = getLogbookDirPath(config);
|
|
12
12
|
if (!(await fs.pathExists(logbookDir))) {
|
|
13
|
-
console.error(
|
|
13
|
+
console.error(error(`Error: Logbook directory '${config.logbookDir}' not found. Run 'logbook init' first.`));
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
16
|
// Detect currently active entry (if any)
|
|
@@ -35,9 +35,9 @@ export async function list(options = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
rows.push({
|
|
37
37
|
ticketId: draftTicketId,
|
|
38
|
-
title:
|
|
38
|
+
title: neutral(draftTitle),
|
|
39
39
|
prompter: '',
|
|
40
|
-
status:
|
|
40
|
+
status: warning('DRAFT'),
|
|
41
41
|
});
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
@@ -58,16 +58,16 @@ export async function list(options = {}) {
|
|
|
58
58
|
const bodyHasPlaceholders = contentWithoutCode.includes('TODO:') ||
|
|
59
59
|
content.includes('Write a polished, highly readable "short story" of the change here.');
|
|
60
60
|
const hasPlaceholders = frontmatterHasPlaceholders || bodyHasPlaceholders;
|
|
61
|
-
const status = isActive ?
|
|
61
|
+
const status = isActive ? highlight('ā ACTIVE') : hasPlaceholders ? warning('DRAFT') : success('DONE');
|
|
62
62
|
rows.push({
|
|
63
|
-
ticketId:
|
|
64
|
-
title:
|
|
65
|
-
prompter:
|
|
63
|
+
ticketId: asString(data.ticket) || neutral('-'),
|
|
64
|
+
title: asString(data.title) || neutral('(untitled)'),
|
|
65
|
+
prompter: asString(data.prompter) || neutral('-'),
|
|
66
66
|
status,
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
if (rows.length === 0) {
|
|
70
|
-
console.log(
|
|
70
|
+
console.log(warning('No logbook entries found. Use `logbook new <id> <slug>` to create one.'));
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
// Sort by ticket ID descending
|
|
@@ -97,10 +97,10 @@ export async function list(options = {}) {
|
|
|
97
97
|
};
|
|
98
98
|
const pad = (s, len) => s + ' '.repeat(Math.max(0, len - stripAnsi(s).length));
|
|
99
99
|
const header = [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
bold(pad('TICKET-ID', colWidths.ticketId)),
|
|
101
|
+
bold(pad('TITLE', colWidths.title)),
|
|
102
|
+
bold(pad('PROMPTER', colWidths.prompter)),
|
|
103
|
+
bold('STATUS'),
|
|
104
104
|
].join(' ');
|
|
105
105
|
const divider = [
|
|
106
106
|
'ā'.repeat(colWidths.ticketId),
|
|
@@ -110,7 +110,7 @@ export async function list(options = {}) {
|
|
|
110
110
|
].join(' ');
|
|
111
111
|
console.log('');
|
|
112
112
|
console.log(header);
|
|
113
|
-
console.log(
|
|
113
|
+
console.log(neutral(divider));
|
|
114
114
|
for (const row of displayedRows) {
|
|
115
115
|
console.log([
|
|
116
116
|
pad(row.ticketId, colWidths.ticketId),
|
|
@@ -123,5 +123,5 @@ export async function list(options = {}) {
|
|
|
123
123
|
const countText = displayedRows.length === totalEntries
|
|
124
124
|
? `${totalEntries} ${totalEntries === 1 ? 'entry' : 'entries'}`
|
|
125
125
|
: `Showing latest ${displayedRows.length} of ${totalEntries} entries (use --all to show all)`;
|
|
126
|
-
console.log(
|
|
126
|
+
console.log(neutral(`${countText} in ${config.logbookDir}/`));
|
|
127
127
|
}
|
package/dist/commands/log.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import { error as errorStyle, successMessage } from '../lib/theme.js';
|
|
4
4
|
import { getConfig } from '../lib/config.js';
|
|
5
5
|
import { getActiveEntry } from '../lib/session.js';
|
|
6
6
|
export async function log(messages, options = {}) {
|
|
@@ -8,14 +8,14 @@ export async function log(messages, options = {}) {
|
|
|
8
8
|
// 1. Require an active entry
|
|
9
9
|
const active = await getActiveEntry(options, cwd);
|
|
10
10
|
if (!active) {
|
|
11
|
-
console.error(
|
|
12
|
-
console.error(
|
|
11
|
+
console.error(errorStyle(`Error: No active logbook entry resolved.`));
|
|
12
|
+
console.error(errorStyle(`Activate an entry via 'logbook start <id>', checkout a feature branch, or pass --id.`));
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
const config = getConfig();
|
|
16
16
|
const logPath = join(cwd, config.logbookDir, active.slug, 'log.md');
|
|
17
17
|
if (!(await fs.pathExists(logPath))) {
|
|
18
|
-
console.error(
|
|
18
|
+
console.error(errorStyle(`Error: log.md not found`));
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
// 2. Build new lines: "- <ISO timestamp>: <message>"
|
|
@@ -26,6 +26,6 @@ export async function log(messages, options = {}) {
|
|
|
26
26
|
const separator = existing.endsWith('\n') ? '' : '\n';
|
|
27
27
|
await fs.writeFile(logPath, existing + separator + newLines + '\n');
|
|
28
28
|
for (const msg of messages) {
|
|
29
|
-
console.log(
|
|
29
|
+
console.log(successMessage('Logged', `${active.slug} (via ${active.source}): ${timestamp}: ${msg}`));
|
|
30
30
|
}
|
|
31
31
|
}
|
package/dist/commands/new.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import { join
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { join } from 'node:path';
|
|
4
3
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { getConfig, getLogbookDirPath } from '../lib/config.js';
|
|
5
|
+
import { getEntryPath } from '../lib/entry-paths.js';
|
|
7
6
|
import { getTemplatesDir } from '../lib/migrations.js';
|
|
7
|
+
import { header, warning as warningStyle, highlight, errorMessage, successMessage, warningMessage, } from '../lib/theme.js';
|
|
8
|
+
import { getPackageVersion } from '../lib/package-version.js';
|
|
8
9
|
import { parseTicketId, getNextId } from '../utils/id.js';
|
|
9
10
|
import { sanitiseSlug } from '../utils/slug.js';
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const rl = createInterface({
|
|
12
12
|
input: process.stdin,
|
|
13
13
|
output: process.stdout,
|
|
@@ -21,7 +21,7 @@ const question = (prompt) => {
|
|
|
21
21
|
};
|
|
22
22
|
export async function createNewEntry(id, slug) {
|
|
23
23
|
const config = getConfig();
|
|
24
|
-
const logbookDir =
|
|
24
|
+
const logbookDir = getLogbookDirPath(config);
|
|
25
25
|
if (!(await fs.pathExists(logbookDir))) {
|
|
26
26
|
await fs.mkdirp(logbookDir);
|
|
27
27
|
}
|
|
@@ -29,19 +29,19 @@ export async function createNewEntry(id, slug) {
|
|
|
29
29
|
let entrySlug = slug;
|
|
30
30
|
// Interactive mode if no parameters provided
|
|
31
31
|
if (!entryId || !entrySlug) {
|
|
32
|
-
console.log(
|
|
32
|
+
console.log(header('\nš Creating a new logbook entry (interactive mode)\n'));
|
|
33
33
|
// Determine if we have a jiraPrefix configured
|
|
34
34
|
const hasJiraPrefix = !!config.jiraPrefix;
|
|
35
35
|
if (!entryId) {
|
|
36
36
|
if (hasJiraPrefix) {
|
|
37
37
|
// Auto-generate ID based on jiraPrefix
|
|
38
38
|
const nextId = getNextId(logbookDir, config.jiraPrefix);
|
|
39
|
-
const input = await question(
|
|
39
|
+
const input = await question(highlight(`Enter entry ID (e.g. ${nextId}, press Enter for auto-generated): `));
|
|
40
40
|
if (input) {
|
|
41
41
|
const parsed = parseTicketId(input);
|
|
42
42
|
const bareNumber = /^\d+$/.test(input) ? parseInt(input, 10) : null;
|
|
43
43
|
if (parsed && parsed.prefix.toUpperCase() !== config.jiraPrefix?.toUpperCase()) {
|
|
44
|
-
console.warn(
|
|
44
|
+
console.warn(warningStyle(`Warning: Prefix '${parsed.prefix}' doesn't match configured '${config.jiraPrefix}'. Using configured prefix.`));
|
|
45
45
|
entryId = `${config.jiraPrefix}-${parsed.number}`;
|
|
46
46
|
}
|
|
47
47
|
else if (parsed) {
|
|
@@ -51,7 +51,7 @@ export async function createNewEntry(id, slug) {
|
|
|
51
51
|
entryId = `${config.jiraPrefix}-${bareNumber}`;
|
|
52
52
|
}
|
|
53
53
|
else {
|
|
54
|
-
console.warn(
|
|
54
|
+
console.warn(warningStyle('Invalid ID format. Using auto-generated ID.'));
|
|
55
55
|
entryId = nextId;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -61,7 +61,7 @@ export async function createNewEntry(id, slug) {
|
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
63
|
const nextId = getNextId(logbookDir, 'LB');
|
|
64
|
-
const input = await question(
|
|
64
|
+
const input = await question(highlight(`Enter entry ID (e.g. ${nextId}, press Enter for auto-generated): `));
|
|
65
65
|
if (input) {
|
|
66
66
|
const parsed = parseTicketId(input);
|
|
67
67
|
const bareNumber = /^\d+$/.test(input) ? parseInt(input, 10) : null;
|
|
@@ -72,7 +72,7 @@ export async function createNewEntry(id, slug) {
|
|
|
72
72
|
entryId = `LB-${bareNumber}`;
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
|
-
console.warn(
|
|
75
|
+
console.warn(warningStyle('Invalid ID format. Using auto-generated ID.'));
|
|
76
76
|
entryId = nextId;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -82,9 +82,9 @@ export async function createNewEntry(id, slug) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
if (!entrySlug) {
|
|
85
|
-
const input = await question(
|
|
85
|
+
const input = await question(highlight('Enter a short title (will be converted to slug): '));
|
|
86
86
|
if (!input) {
|
|
87
|
-
console.error(
|
|
87
|
+
console.error(errorMessage('Error', 'Title is required.'));
|
|
88
88
|
process.exit(1);
|
|
89
89
|
}
|
|
90
90
|
entrySlug = sanitiseSlug(input);
|
|
@@ -97,17 +97,16 @@ export async function createNewEntry(id, slug) {
|
|
|
97
97
|
// Sanitise slug
|
|
98
98
|
const sanitised = sanitiseSlug(entrySlug);
|
|
99
99
|
if (sanitised !== entrySlug) {
|
|
100
|
-
console.
|
|
100
|
+
console.warn(warningMessage('Notice', `Slug normalised from '${entrySlug}' to '${sanitised}'.`));
|
|
101
101
|
entrySlug = sanitised;
|
|
102
102
|
}
|
|
103
|
-
const entryDir =
|
|
103
|
+
const entryDir = getEntryPath(getLogbookDirPath(config), `${entryId}-${entrySlug}`);
|
|
104
104
|
if (await fs.pathExists(entryDir)) {
|
|
105
|
-
console.error(
|
|
105
|
+
console.error(errorMessage('Error', `Entry ${entryId}-${entrySlug} already exists.`));
|
|
106
106
|
process.exit(1);
|
|
107
107
|
}
|
|
108
108
|
await fs.mkdirp(entryDir);
|
|
109
|
-
const
|
|
110
|
-
const version = pkg.version;
|
|
109
|
+
const version = getPackageVersion();
|
|
111
110
|
const now = new Date();
|
|
112
111
|
const replacements = {
|
|
113
112
|
id: entryId,
|
|
@@ -131,5 +130,5 @@ export async function createNewEntry(id, slug) {
|
|
|
131
130
|
await fs.writeFile(join(entryDir, 'index.md'), fillTemplate(getTemplate('index.md')));
|
|
132
131
|
await fs.writeFile(join(entryDir, 'ticket.md'), fillTemplate(getTemplate('ticket.md')));
|
|
133
132
|
await fs.writeFile(join(entryDir, 'log.md'), fillTemplate(getTemplate('log.md')));
|
|
134
|
-
console.log(
|
|
133
|
+
console.log(successMessage('Created', `logbook entry in ${config.logbookDir}/${entryId}-${entrySlug}/`));
|
|
135
134
|
}
|
package/dist/commands/preview.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
4
|
-
import { getConfig } from '../lib/config.js';
|
|
3
|
+
import { header, error as errorStyle } from '../lib/theme.js';
|
|
4
|
+
import { getConfig, getOutputDirPath } from '../lib/config.js';
|
|
5
5
|
import { build } from './build.js';
|
|
6
6
|
export async function preview() {
|
|
7
7
|
const config = getConfig();
|
|
8
|
-
const outputDir =
|
|
8
|
+
const outputDir = getOutputDirPath(config);
|
|
9
9
|
const indexPath = join(outputDir, 'index.html');
|
|
10
|
-
console.log(
|
|
10
|
+
console.log(header('Building logbook before preview...'));
|
|
11
11
|
await build();
|
|
12
|
-
console.log(
|
|
12
|
+
console.log(header(`Opening preview: ${indexPath}`));
|
|
13
13
|
const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
14
|
-
|
|
14
|
+
// Use execFile with explicit args array to avoid shell interpolation
|
|
15
|
+
execFile(start, [indexPath], (err) => {
|
|
15
16
|
if (err) {
|
|
16
|
-
console.error(
|
|
17
|
+
console.error(errorStyle(`Failed to open preview: ${err.message}`));
|
|
17
18
|
}
|
|
18
19
|
});
|
|
19
20
|
}
|