project-logbook 0.3.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/README.md +34 -0
- package/dist/commands/build.d.ts +1 -0
- package/dist/commands/build.js +174 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +83 -0
- package/dist/commands/lint.d.ts +1 -0
- package/dist/commands/lint.js +78 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +127 -0
- package/dist/commands/log.d.ts +3 -0
- package/dist/commands/log.js +31 -0
- package/dist/commands/new.d.ts +1 -0
- package/dist/commands/new.js +135 -0
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +19 -0
- package/dist/commands/release.d.ts +3 -0
- package/dist/commands/release.js +66 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +87 -0
- package/dist/commands/steer.d.ts +1 -0
- package/dist/commands/steer.js +22 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -0
- package/dist/lib/about-content.d.ts +1 -0
- package/dist/lib/about-content.js +7 -0
- package/dist/lib/build-helpers.d.ts +15 -0
- package/dist/lib/build-helpers.js +171 -0
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/config.js +41 -0
- package/dist/lib/git-helpers.d.ts +16 -0
- package/dist/lib/git-helpers.js +93 -0
- package/dist/lib/image-helpers.d.ts +19 -0
- package/dist/lib/image-helpers.js +121 -0
- package/dist/lib/jira-helpers.d.ts +5 -0
- package/dist/lib/jira-helpers.js +5 -0
- package/dist/lib/lint-runner.d.ts +2 -0
- package/dist/lib/lint-runner.js +31 -0
- package/dist/lib/lint-types.d.ts +24 -0
- package/dist/lib/lint-types.js +1 -0
- package/dist/lib/lockfile.d.ts +6 -0
- package/dist/lib/lockfile.js +1 -0
- package/dist/lib/logbook-client.d.ts +5 -0
- package/dist/lib/logbook-client.js +11 -0
- package/dist/lib/migrations.d.ts +11 -0
- package/dist/lib/migrations.js +34 -0
- package/dist/lib/session.d.ts +16 -0
- package/dist/lib/session.js +74 -0
- package/dist/lib/styles.d.ts +1 -0
- package/dist/lib/styles.js +21 -0
- package/dist/lib/template-types.d.ts +118 -0
- package/dist/lib/template-types.js +1 -0
- package/dist/lib/templates.d.ts +14 -0
- package/dist/lib/templates.js +158 -0
- package/dist/linters/frontmatter.d.ts +3 -0
- package/dist/linters/frontmatter.js +68 -0
- package/dist/linters/index.d.ts +1 -0
- package/dist/linters/index.js +18 -0
- package/dist/linters/jira-prefix.d.ts +3 -0
- package/dist/linters/jira-prefix.js +34 -0
- package/dist/linters/links.d.ts +3 -0
- package/dist/linters/links.js +28 -0
- package/dist/linters/lockfile.d.ts +3 -0
- package/dist/linters/lockfile.js +20 -0
- package/dist/linters/placeholders.d.ts +3 -0
- package/dist/linters/placeholders.js +62 -0
- package/dist/linters/project-integrity.d.ts +3 -0
- package/dist/linters/project-integrity.js +29 -0
- package/dist/linters/readability.d.ts +3 -0
- package/dist/linters/readability.js +41 -0
- package/dist/linters/workspaces.d.ts +3 -0
- package/dist/linters/workspaces.js +26 -0
- package/dist/templates/ABOUT.md +23 -0
- package/dist/templates/CONTRIBUTING.md +78 -0
- package/dist/templates/favicon.svg +21 -0
- package/dist/templates/index.md +30 -0
- package/dist/templates/log.md +4 -0
- package/dist/templates/logbook-client.js +162 -0
- package/dist/templates/steer.txt +25 -0
- package/dist/templates/styles.css +641 -0
- package/dist/templates/ticket.md +4 -0
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +47 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +38 -0
- package/dist/utils/id.d.ts +7 -0
- package/dist/utils/id.js +36 -0
- package/dist/utils/slug.d.ts +2 -0
- package/dist/utils/slug.js +9 -0
- package/dist/utils/string.d.ts +2 -0
- package/dist/utils/string.js +4 -0
- package/package.json +69 -0
- package/src/templates/ABOUT.md +23 -0
- package/src/templates/CONTRIBUTING.md +78 -0
- package/src/templates/favicon.svg +21 -0
- package/src/templates/index.md +30 -0
- package/src/templates/log.md +4 -0
- package/src/templates/logbook-client.js +162 -0
- package/src/templates/steer.txt +25 -0
- package/src/templates/styles.css +641 -0
- package/src/templates/ticket.md +4 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function getWorkspaces(cwd?: string): string[];
|
|
2
|
+
export interface LogbookConfig {
|
|
3
|
+
projectName: string;
|
|
4
|
+
logbookDir: string;
|
|
5
|
+
outputDir: string;
|
|
6
|
+
primaryColor: string;
|
|
7
|
+
jiraBaseUrl?: string;
|
|
8
|
+
jiraPrefix?: string;
|
|
9
|
+
tags?: {
|
|
10
|
+
allowed: string[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_CONFIG: LogbookConfig;
|
|
14
|
+
export declare function getConfig(cwd?: string): LogbookConfig;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function getWorkspaces(cwd = process.cwd()) {
|
|
4
|
+
const pkgPath = join(cwd, 'package.json');
|
|
5
|
+
if (!existsSync(pkgPath))
|
|
6
|
+
return [];
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
9
|
+
const ws = pkg.workspaces;
|
|
10
|
+
if (Array.isArray(ws))
|
|
11
|
+
return ws.map(String);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const DEFAULT_CONFIG = {
|
|
19
|
+
projectName: 'My Project',
|
|
20
|
+
logbookDir: 'logbook',
|
|
21
|
+
outputDir: 'dist/logbook',
|
|
22
|
+
primaryColor: '#2563eb',
|
|
23
|
+
tags: {
|
|
24
|
+
allowed: ['#bugfix', '#feature', '#refactor'],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
export function getConfig(cwd = process.cwd()) {
|
|
28
|
+
const configPath = join(cwd, '.project-logbook');
|
|
29
|
+
if (existsSync(configPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const userConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
32
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
36
|
+
console.warn(`Warning: Failed to parse .project-logbook config — using defaults. (${errorMessage})`);
|
|
37
|
+
return DEFAULT_CONFIG;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return DEFAULT_CONFIG;
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GitCommit } from './template-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse the raw output of `git log --pretty=format:"%H|%s|%aI"` into GitCommit objects.
|
|
4
|
+
* Pure function (no I/O) — unit-testable without spawning git.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseGitLogOutput(raw: string, now?: Date): GitCommit[];
|
|
7
|
+
export declare function getGitCommits(dir: string, maxCount?: number): Promise<GitCommit[]>;
|
|
8
|
+
export declare function getGitTags(): Promise<{
|
|
9
|
+
name: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Synchronous branch detection — used in session resolution (not the build pipeline).
|
|
14
|
+
* Kept synchronous intentionally; simple-git is used for the heavier async build operations.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCurrentBranch(cwd?: string): string | undefined;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
/**
|
|
4
|
+
* Parse the raw output of `git log --pretty=format:"%H|%s|%aI"` into GitCommit objects.
|
|
5
|
+
* Pure function (no I/O) — unit-testable without spawning git.
|
|
6
|
+
*/
|
|
7
|
+
export function parseGitLogOutput(raw, now = new Date()) {
|
|
8
|
+
if (!raw.trim())
|
|
9
|
+
return [];
|
|
10
|
+
return raw
|
|
11
|
+
.split('\n')
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((line) => {
|
|
14
|
+
const [sha, message, timestamp] = line.split('|');
|
|
15
|
+
if (!sha || !message || !timestamp)
|
|
16
|
+
return null;
|
|
17
|
+
const relativeTime = formatRelativeTime(timestamp, now);
|
|
18
|
+
return { sha: sha.slice(0, 7), message, timestamp, relativeTime };
|
|
19
|
+
})
|
|
20
|
+
.filter((c) => c !== null);
|
|
21
|
+
}
|
|
22
|
+
function formatRelativeTime(isoTimestamp, now) {
|
|
23
|
+
const d = new Date(isoTimestamp);
|
|
24
|
+
if (isNaN(d.getTime()))
|
|
25
|
+
return isoTimestamp;
|
|
26
|
+
const diffMs = now.getTime() - d.getTime();
|
|
27
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
28
|
+
if (diffSec < 60)
|
|
29
|
+
return 'just now';
|
|
30
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
31
|
+
if (diffMin < 60)
|
|
32
|
+
return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
|
|
33
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
34
|
+
if (diffHour < 24)
|
|
35
|
+
return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`;
|
|
36
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
37
|
+
if (diffDay < 30)
|
|
38
|
+
return `${diffDay} day${diffDay === 1 ? '' : 's'} ago`;
|
|
39
|
+
const diffMonth = Math.floor(diffDay / 30);
|
|
40
|
+
if (diffMonth < 12)
|
|
41
|
+
return `${diffMonth} month${diffMonth === 1 ? '' : 's'} ago`;
|
|
42
|
+
const diffYear = Math.floor(diffMonth / 12);
|
|
43
|
+
return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
|
|
44
|
+
}
|
|
45
|
+
export async function getGitCommits(dir, maxCount = 100) {
|
|
46
|
+
try {
|
|
47
|
+
const git = simpleGit(process.cwd());
|
|
48
|
+
const raw = await git.raw(['log', '--follow', `-n`, String(maxCount), '--pretty=format:%H|%s|%aI', '--', dir]);
|
|
49
|
+
return parseGitLogOutput(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function getGitTags() {
|
|
56
|
+
try {
|
|
57
|
+
const git = simpleGit(process.cwd());
|
|
58
|
+
const raw = await git.raw(['tag', '-l', '--sort=-creatordate', '--format=%(creatordate:iso8601)|%(refname:short)']);
|
|
59
|
+
return raw
|
|
60
|
+
.split('\n')
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.map((line) => {
|
|
63
|
+
const idx = line.indexOf('|');
|
|
64
|
+
if (idx === -1)
|
|
65
|
+
return null;
|
|
66
|
+
const timestamp = line.slice(0, idx).trim();
|
|
67
|
+
const name = line.slice(idx + 1).trim();
|
|
68
|
+
if (!timestamp || !name)
|
|
69
|
+
return null;
|
|
70
|
+
return { name, timestamp };
|
|
71
|
+
})
|
|
72
|
+
.filter((t) => t !== null);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Synchronous branch detection — used in session resolution (not the build pipeline).
|
|
80
|
+
* Kept synchronous intentionally; simple-git is used for the heavier async build operations.
|
|
81
|
+
*/
|
|
82
|
+
export function getCurrentBranch(cwd = process.cwd()) {
|
|
83
|
+
try {
|
|
84
|
+
return (execSync('git rev-parse --abbrev-ref HEAD', {
|
|
85
|
+
encoding: 'utf8',
|
|
86
|
+
cwd,
|
|
87
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
88
|
+
}).trim() || undefined);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract image paths from markdown content
|
|
3
|
+
*/
|
|
4
|
+
export declare function extractImagePathsFromMarkdown(content: string): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Process markdown with image collection enabled
|
|
7
|
+
* Returns { html, imagePaths }
|
|
8
|
+
*/
|
|
9
|
+
export declare function mdToHtmlWithImages(md: string): Promise<{
|
|
10
|
+
html: string;
|
|
11
|
+
imagePaths: string[];
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Copy images from source directory to output directory
|
|
15
|
+
* @param sourceDir - Directory where images are referenced from (e.g., logbook entry folder)
|
|
16
|
+
* @param outputDir - Output directory where images should be copied (e.g., public/entry-slug/images)
|
|
17
|
+
* @param imagePaths - Array of image paths from markdown (e.g., ["./images/screenshot.png", "../assets/img.jpg", "/static/img.png"])
|
|
18
|
+
*/
|
|
19
|
+
export declare function copyImages(sourceDir: string, outputDir: string, imagePaths: string[]): Promise<void>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
|
+
import { unified } from 'unified';
|
|
4
|
+
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkRehype from 'remark-rehype';
|
|
6
|
+
import rehypeSlug from 'rehype-slug';
|
|
7
|
+
import rehypeFormat from 'rehype-format';
|
|
8
|
+
import rehypeStringify from 'rehype-stringify';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
/** Regex to match image links in markdown:  */
|
|
11
|
+
const imageLinkRegex = /!\[.*?\]\(([^)]+\.(?:png|jpe?g|gif|svg|webp|bmp|ico))\)/gi;
|
|
12
|
+
/**
|
|
13
|
+
* Extract image paths from markdown content
|
|
14
|
+
*/
|
|
15
|
+
export function extractImagePathsFromMarkdown(content) {
|
|
16
|
+
const paths = [];
|
|
17
|
+
let match;
|
|
18
|
+
imageLinkRegex.lastIndex = 0;
|
|
19
|
+
while ((match = imageLinkRegex.exec(content)) !== null) {
|
|
20
|
+
paths.push(match[1]);
|
|
21
|
+
}
|
|
22
|
+
return paths;
|
|
23
|
+
}
|
|
24
|
+
/** ImageProcessor: tracks images encountered during markdown-to-html conversion */
|
|
25
|
+
class ImageProcessor {
|
|
26
|
+
images = [];
|
|
27
|
+
reset() {
|
|
28
|
+
this.images = [];
|
|
29
|
+
}
|
|
30
|
+
getImagePaths() {
|
|
31
|
+
return [...this.images];
|
|
32
|
+
}
|
|
33
|
+
/** Rehype plugin: collect image paths from markdown */
|
|
34
|
+
rehypeCollectImages = () => {
|
|
35
|
+
return (tree) => {
|
|
36
|
+
visitImages(tree, (node) => {
|
|
37
|
+
const src = node.properties?.src;
|
|
38
|
+
if (typeof src !== 'string')
|
|
39
|
+
return;
|
|
40
|
+
// Skip external URLs and data URIs
|
|
41
|
+
if (/^[a-z][a-z\d+\-.]*:/i.test(src))
|
|
42
|
+
return;
|
|
43
|
+
if (src.startsWith('data:'))
|
|
44
|
+
return;
|
|
45
|
+
this.images.push(src);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const imageProcessor = new ImageProcessor();
|
|
51
|
+
function visitImages(node, visitor) {
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
if (child.type === 'element') {
|
|
54
|
+
if (child.tagName === 'img')
|
|
55
|
+
visitor(child);
|
|
56
|
+
visitImages(child, visitor);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Process markdown with image collection enabled
|
|
62
|
+
* Returns { html, imagePaths }
|
|
63
|
+
*/
|
|
64
|
+
export async function mdToHtmlWithImages(md) {
|
|
65
|
+
imageProcessor.reset();
|
|
66
|
+
const processorWithImages = unified()
|
|
67
|
+
.use(remarkParse)
|
|
68
|
+
.use(remarkRehype)
|
|
69
|
+
.use(imageProcessor.rehypeCollectImages)
|
|
70
|
+
.use(rehypeSlug)
|
|
71
|
+
.use(rehypeFormat)
|
|
72
|
+
.use(rehypeStringify);
|
|
73
|
+
const result = await processorWithImages.process(md);
|
|
74
|
+
return { html: result.toString(), imagePaths: imageProcessor.getImagePaths() };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Copy images from source directory to output directory
|
|
78
|
+
* @param sourceDir - Directory where images are referenced from (e.g., logbook entry folder)
|
|
79
|
+
* @param outputDir - Output directory where images should be copied (e.g., public/entry-slug/images)
|
|
80
|
+
* @param imagePaths - Array of image paths from markdown (e.g., ["./images/screenshot.png", "../assets/img.jpg", "/static/img.png"])
|
|
81
|
+
*/
|
|
82
|
+
export async function copyImages(sourceDir, outputDir, imagePaths) {
|
|
83
|
+
await fs.mkdirp(outputDir);
|
|
84
|
+
const processedPaths = new Set();
|
|
85
|
+
for (const imagePath of imagePaths) {
|
|
86
|
+
// Skip if already processed
|
|
87
|
+
if (processedPaths.has(imagePath))
|
|
88
|
+
continue;
|
|
89
|
+
processedPaths.add(imagePath);
|
|
90
|
+
let sourcePath = '';
|
|
91
|
+
let destRelPath = '';
|
|
92
|
+
// Handle absolute paths starting with /
|
|
93
|
+
if (imagePath.startsWith('/')) {
|
|
94
|
+
// Absolute path like /static/screenshot.jpg -> copy from project root to public/static/
|
|
95
|
+
const relativeToProjectRoot = imagePath.substring(1); // Remove leading /
|
|
96
|
+
sourcePath = join(process.cwd(), relativeToProjectRoot);
|
|
97
|
+
destRelPath = relativeToProjectRoot;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Resolve the image path relative to the source directory
|
|
101
|
+
let resolvedPath = imagePath;
|
|
102
|
+
if (!imagePath.startsWith('./') && !imagePath.startsWith('../')) {
|
|
103
|
+
resolvedPath = `./${imagePath}`;
|
|
104
|
+
}
|
|
105
|
+
const fullPath = join(sourceDir, resolvedPath);
|
|
106
|
+
sourcePath = fullPath;
|
|
107
|
+
// Get the relative path from source dir to maintain folder structure
|
|
108
|
+
destRelPath = relative(sourceDir, fullPath);
|
|
109
|
+
}
|
|
110
|
+
// Check if image exists
|
|
111
|
+
if (!(await fs.pathExists(sourcePath))) {
|
|
112
|
+
console.warn(chalk.yellow(` Image not found: ${sourcePath}`));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const destPath = join(outputDir, destRelPath);
|
|
116
|
+
// Create destination directory if needed
|
|
117
|
+
await fs.mkdirp(dirname(destPath));
|
|
118
|
+
// Copy the image
|
|
119
|
+
await fs.copyFile(sourcePath, destPath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces PREFIX-NNN occurrences in a commit message with Jira links.
|
|
3
|
+
* Only applied when both jiraBaseUrl and jiraPrefix are provided.
|
|
4
|
+
*/
|
|
5
|
+
export const linkJiraIds = (msg, base, prefix) => msg.replace(new RegExp(`(${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-\\d+)`, 'g'), (m) => `<a href="${base}${m}" target="_blank" rel="noopener noreferrer" class="commit-jira-link">${m}</a>`);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { linters } from '../linters/index.js';
|
|
3
|
+
export async function runLinters(context) {
|
|
4
|
+
const issues = [];
|
|
5
|
+
for (const linter of linters) {
|
|
6
|
+
try {
|
|
7
|
+
const result = await linter.check(context);
|
|
8
|
+
issues.push(...result);
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
console.error(chalk.red(`Error running linter '${linter.name}':`), err);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (issues.length === 0) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (context.entryName) {
|
|
18
|
+
console.log(chalk.blue(`\nEntry: ${context.entryName}`));
|
|
19
|
+
}
|
|
20
|
+
let hasErrors = false;
|
|
21
|
+
for (const issue of issues) {
|
|
22
|
+
if (issue.level === 'error') {
|
|
23
|
+
console.error(chalk.red(` [${issue.category.toUpperCase()}] ${issue.message}`));
|
|
24
|
+
hasErrors = true;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.warn(chalk.yellow(` [${issue.category.toUpperCase()}] ${issue.message}`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return !hasErrors;
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LogbookConfig } from './config.js';
|
|
2
|
+
export interface LintIssue {
|
|
3
|
+
level: 'error' | 'warning';
|
|
4
|
+
message: string;
|
|
5
|
+
category: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LinterContext {
|
|
8
|
+
config: LogbookConfig;
|
|
9
|
+
entryName?: string;
|
|
10
|
+
entryPath?: string;
|
|
11
|
+
indexPath?: string;
|
|
12
|
+
content?: string;
|
|
13
|
+
frontmatter?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface Linter {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
/**
|
|
19
|
+
* Run the linter.
|
|
20
|
+
* If entryName is provided, it's an entry-level check.
|
|
21
|
+
* Otherwise, it's a project-level check.
|
|
22
|
+
*/
|
|
23
|
+
check(context: LinterContext): Promise<LintIssue[]>;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LOCKFILE_NAME = '.logbook-active';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
/**
|
|
6
|
+
* Returns the client-side logbook.js helper script content.
|
|
7
|
+
* This is emitted as a standalone JS file into the build output.
|
|
8
|
+
*/
|
|
9
|
+
export function getClientScript() {
|
|
10
|
+
return readFileSync(join(__dirname, '../templates/logbook-client.js'), 'utf8');
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function getTemplatesDir(): string;
|
|
2
|
+
type FileStatus = 'UP_TO_DATE' | 'OUTDATED' | 'MISSING';
|
|
3
|
+
interface ProjectFileStatus {
|
|
4
|
+
name: string;
|
|
5
|
+
localPath: string;
|
|
6
|
+
status: FileStatus;
|
|
7
|
+
templateContent: string;
|
|
8
|
+
currentContent?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getProjectStatus(): Promise<ProjectFileStatus[]>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const CORE_FILES = [{ name: 'CONTRIBUTING.md', path: 'CONTRIBUTING.md' }];
|
|
6
|
+
export function getTemplatesDir() {
|
|
7
|
+
const possiblePaths = [join(__dirname, '../templates'), join(__dirname, '../../src/templates')];
|
|
8
|
+
for (const p of possiblePaths) {
|
|
9
|
+
if (fs.existsSync(p))
|
|
10
|
+
return p;
|
|
11
|
+
}
|
|
12
|
+
throw new Error('Templates directory not found.');
|
|
13
|
+
}
|
|
14
|
+
export async function getProjectStatus() {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const templatesDir = getTemplatesDir();
|
|
17
|
+
const results = [];
|
|
18
|
+
for (const file of CORE_FILES) {
|
|
19
|
+
const localPath = join(cwd, file.path);
|
|
20
|
+
const templatePath = join(templatesDir, file.name);
|
|
21
|
+
if (!fs.existsSync(templatePath))
|
|
22
|
+
continue;
|
|
23
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
24
|
+
if (!fs.existsSync(localPath)) {
|
|
25
|
+
results.push({ name: file.name, localPath, status: 'MISSING', templateContent });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const currentContent = fs.readFileSync(localPath, 'utf8');
|
|
29
|
+
const status = currentContent === templateContent ? 'UP_TO_DATE' : 'OUTDATED';
|
|
30
|
+
results.push({ name: file.name, localPath, status, templateContent, currentContent });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface ActiveEntryResult {
|
|
2
|
+
slug: string;
|
|
3
|
+
source: 'branch' | 'lockfile' | 'flag';
|
|
4
|
+
startedAt?: string;
|
|
5
|
+
prompter?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the currently active logbook entry using the following strategy:
|
|
9
|
+
* 1. If options.id is provided, checks if it matches an entry.
|
|
10
|
+
* 2. If the current Git branch name contains a ticket reference matching config.jiraPrefix or "LB", auto-resolves that entry.
|
|
11
|
+
* 3. Falls back to reading the local .logbook-active lockfile.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getActiveEntry(options?: {
|
|
14
|
+
id?: string;
|
|
15
|
+
}, cwd?: string): Promise<ActiveEntryResult | undefined>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getConfig } from './config.js';
|
|
4
|
+
import { getCurrentBranch } from './git-helpers.js';
|
|
5
|
+
import { LOCKFILE_NAME } from './lockfile.js';
|
|
6
|
+
import { getLogbookEntries } from '../utils/fs.js';
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the currently active logbook entry using the following strategy:
|
|
9
|
+
* 1. If options.id is provided, checks if it matches an entry.
|
|
10
|
+
* 2. If the current Git branch name contains a ticket reference matching config.jiraPrefix or "LB", auto-resolves that entry.
|
|
11
|
+
* 3. Falls back to reading the local .logbook-active lockfile.
|
|
12
|
+
*/
|
|
13
|
+
export async function getActiveEntry(options = {}, cwd = process.cwd()) {
|
|
14
|
+
const config = getConfig(cwd);
|
|
15
|
+
const logbookDir = join(cwd, config.logbookDir);
|
|
16
|
+
// 1. Early lockfile check
|
|
17
|
+
const lockfilePath = join(cwd, LOCKFILE_NAME);
|
|
18
|
+
if (await fs.pathExists(lockfilePath)) {
|
|
19
|
+
try {
|
|
20
|
+
const lock = await fs.readJson(lockfilePath);
|
|
21
|
+
return { slug: lock.slug, source: 'lockfile', startedAt: lock.startedAt, prompter: lock.prompter };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore corrupt lockfile
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// 2. Load logbook entries
|
|
28
|
+
let entries = [];
|
|
29
|
+
try {
|
|
30
|
+
entries = await getLogbookEntries(logbookDir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
entries = [];
|
|
34
|
+
}
|
|
35
|
+
const availableSlugs = entries.map((e) => e.slug);
|
|
36
|
+
// 1. Explicit ID flag
|
|
37
|
+
if (options.id) {
|
|
38
|
+
const resolvedId = options.id;
|
|
39
|
+
// Find entry matching ID prefix (e.g. "LB-35" or "19")
|
|
40
|
+
// Resolve short number to full prefix-number if possible
|
|
41
|
+
const fullId = /^\d+$/.test(resolvedId) && config.jiraPrefix ? `${config.jiraPrefix}-${resolvedId}` : resolvedId;
|
|
42
|
+
const matchingDir = availableSlugs.find((slug) => {
|
|
43
|
+
const prefix = slug.split('-').slice(0, fullId.split('-').length).join('-');
|
|
44
|
+
return prefix.toLowerCase() === fullId.toLowerCase();
|
|
45
|
+
});
|
|
46
|
+
if (matchingDir) {
|
|
47
|
+
return {
|
|
48
|
+
slug: matchingDir,
|
|
49
|
+
source: 'flag',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 2. Git Branch Resolution
|
|
54
|
+
const branchName = getCurrentBranch(cwd);
|
|
55
|
+
if (branchName) {
|
|
56
|
+
const jiraPrefix = config.jiraPrefix || 'LB';
|
|
57
|
+
const regex = new RegExp(`(?:^|\\/)(${jiraPrefix}-\\d+)(?:-|$)`, 'i');
|
|
58
|
+
const match = branchName.match(regex);
|
|
59
|
+
if (match) {
|
|
60
|
+
const id = match[1].toUpperCase();
|
|
61
|
+
const matchingDir = availableSlugs.find((slug) => {
|
|
62
|
+
const prefix = slug.split('-').slice(0, id.split('-').length).join('-');
|
|
63
|
+
return prefix.toLowerCase() === id.toLowerCase();
|
|
64
|
+
});
|
|
65
|
+
if (matchingDir) {
|
|
66
|
+
return {
|
|
67
|
+
slug: matchingDir,
|
|
68
|
+
source: 'branch',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getStyles(primaryColor: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
export function getStyles(primaryColor) {
|
|
6
|
+
const cssPath = join(__dirname, '../templates/styles.css');
|
|
7
|
+
const staticCss = readFileSync(cssPath, 'utf8');
|
|
8
|
+
const root = `:root {
|
|
9
|
+
--primary: ${primaryColor};
|
|
10
|
+
--primary-soft: ${primaryColor}15;
|
|
11
|
+
--bg: #fdfdfd;
|
|
12
|
+
--text: #1a1a1a;
|
|
13
|
+
--text-muted: #666666;
|
|
14
|
+
--border: #eeeeee;
|
|
15
|
+
--card-bg: #ffffff;
|
|
16
|
+
--max-width: 800px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
`;
|
|
20
|
+
return root + staticCss;
|
|
21
|
+
}
|