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.
Files changed (102) hide show
  1. package/README.md +34 -0
  2. package/dist/commands/build.d.ts +1 -0
  3. package/dist/commands/build.js +174 -0
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.js +83 -0
  6. package/dist/commands/lint.d.ts +1 -0
  7. package/dist/commands/lint.js +78 -0
  8. package/dist/commands/list.d.ts +3 -0
  9. package/dist/commands/list.js +127 -0
  10. package/dist/commands/log.d.ts +3 -0
  11. package/dist/commands/log.js +31 -0
  12. package/dist/commands/new.d.ts +1 -0
  13. package/dist/commands/new.js +135 -0
  14. package/dist/commands/preview.d.ts +1 -0
  15. package/dist/commands/preview.js +19 -0
  16. package/dist/commands/release.d.ts +3 -0
  17. package/dist/commands/release.js +66 -0
  18. package/dist/commands/start.d.ts +1 -0
  19. package/dist/commands/start.js +87 -0
  20. package/dist/commands/steer.d.ts +1 -0
  21. package/dist/commands/steer.js +22 -0
  22. package/dist/commands/upgrade.d.ts +1 -0
  23. package/dist/commands/upgrade.js +23 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +92 -0
  26. package/dist/lib/about-content.d.ts +1 -0
  27. package/dist/lib/about-content.js +7 -0
  28. package/dist/lib/build-helpers.d.ts +15 -0
  29. package/dist/lib/build-helpers.js +171 -0
  30. package/dist/lib/config.d.ts +14 -0
  31. package/dist/lib/config.js +41 -0
  32. package/dist/lib/git-helpers.d.ts +16 -0
  33. package/dist/lib/git-helpers.js +93 -0
  34. package/dist/lib/image-helpers.d.ts +19 -0
  35. package/dist/lib/image-helpers.js +121 -0
  36. package/dist/lib/jira-helpers.d.ts +5 -0
  37. package/dist/lib/jira-helpers.js +5 -0
  38. package/dist/lib/lint-runner.d.ts +2 -0
  39. package/dist/lib/lint-runner.js +31 -0
  40. package/dist/lib/lint-types.d.ts +24 -0
  41. package/dist/lib/lint-types.js +1 -0
  42. package/dist/lib/lockfile.d.ts +6 -0
  43. package/dist/lib/lockfile.js +1 -0
  44. package/dist/lib/logbook-client.d.ts +5 -0
  45. package/dist/lib/logbook-client.js +11 -0
  46. package/dist/lib/migrations.d.ts +11 -0
  47. package/dist/lib/migrations.js +34 -0
  48. package/dist/lib/session.d.ts +16 -0
  49. package/dist/lib/session.js +74 -0
  50. package/dist/lib/styles.d.ts +1 -0
  51. package/dist/lib/styles.js +21 -0
  52. package/dist/lib/template-types.d.ts +118 -0
  53. package/dist/lib/template-types.js +1 -0
  54. package/dist/lib/templates.d.ts +14 -0
  55. package/dist/lib/templates.js +158 -0
  56. package/dist/linters/frontmatter.d.ts +3 -0
  57. package/dist/linters/frontmatter.js +68 -0
  58. package/dist/linters/index.d.ts +1 -0
  59. package/dist/linters/index.js +18 -0
  60. package/dist/linters/jira-prefix.d.ts +3 -0
  61. package/dist/linters/jira-prefix.js +34 -0
  62. package/dist/linters/links.d.ts +3 -0
  63. package/dist/linters/links.js +28 -0
  64. package/dist/linters/lockfile.d.ts +3 -0
  65. package/dist/linters/lockfile.js +20 -0
  66. package/dist/linters/placeholders.d.ts +3 -0
  67. package/dist/linters/placeholders.js +62 -0
  68. package/dist/linters/project-integrity.d.ts +3 -0
  69. package/dist/linters/project-integrity.js +29 -0
  70. package/dist/linters/readability.d.ts +3 -0
  71. package/dist/linters/readability.js +41 -0
  72. package/dist/linters/workspaces.d.ts +3 -0
  73. package/dist/linters/workspaces.js +26 -0
  74. package/dist/templates/ABOUT.md +23 -0
  75. package/dist/templates/CONTRIBUTING.md +78 -0
  76. package/dist/templates/favicon.svg +21 -0
  77. package/dist/templates/index.md +30 -0
  78. package/dist/templates/log.md +4 -0
  79. package/dist/templates/logbook-client.js +162 -0
  80. package/dist/templates/steer.txt +25 -0
  81. package/dist/templates/styles.css +641 -0
  82. package/dist/templates/ticket.md +4 -0
  83. package/dist/utils/date.d.ts +8 -0
  84. package/dist/utils/date.js +47 -0
  85. package/dist/utils/fs.d.ts +13 -0
  86. package/dist/utils/fs.js +38 -0
  87. package/dist/utils/id.d.ts +7 -0
  88. package/dist/utils/id.js +36 -0
  89. package/dist/utils/slug.d.ts +2 -0
  90. package/dist/utils/slug.js +9 -0
  91. package/dist/utils/string.d.ts +2 -0
  92. package/dist/utils/string.js +4 -0
  93. package/package.json +69 -0
  94. package/src/templates/ABOUT.md +23 -0
  95. package/src/templates/CONTRIBUTING.md +78 -0
  96. package/src/templates/favicon.svg +21 -0
  97. package/src/templates/index.md +30 -0
  98. package/src/templates/log.md +4 -0
  99. package/src/templates/logbook-client.js +162 -0
  100. package/src/templates/steer.txt +25 -0
  101. package/src/templates/styles.css +641 -0
  102. 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: ![alt](path) */
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 declare const linkJiraIds: (msg: string, base: string, prefix: string) => string;
@@ -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,2 @@
1
+ import { LinterContext } from './lint-types.js';
2
+ export declare function runLinters(context: LinterContext): Promise<boolean>;
@@ -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,6 @@
1
+ export declare const LOCKFILE_NAME = ".logbook-active";
2
+ export interface LockfileData {
3
+ slug: string;
4
+ startedAt: string;
5
+ prompter?: string;
6
+ }
@@ -0,0 +1 @@
1
+ export const LOCKFILE_NAME = '.logbook-active';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Returns the client-side logbook.js helper script content.
3
+ * This is emitted as a standalone JS file into the build output.
4
+ */
5
+ export declare function getClientScript(): string;
@@ -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
+ }