@ibalzam/codejitsu-core 0.1.0 → 0.2.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.
@@ -1,201 +1,5 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import matter from 'gray-matter';
4
- import readingTime from 'reading-time';
5
-
6
- export interface FAQItem {
7
- question: string;
8
- answer: string;
9
- }
10
-
11
- export interface BlogPostFrontmatter {
12
- title: string;
13
- description: string;
14
- date: string;
15
- slug?: string;
16
- author?: string;
17
- image?: string;
18
- tags?: string[];
19
- faqs?: FAQItem[];
20
- }
21
-
22
- export interface BlogPostMetadata {
23
- slug: string;
24
- title: string;
25
- description: string;
26
- date: string;
27
- author?: string;
28
- image?: string;
29
- tags?: string[];
30
- readingTime: string;
31
- }
32
-
33
- export interface BlogPost extends BlogPostMetadata {
34
- faqs?: FAQItem[];
35
- content: string;
36
- }
37
-
38
- export interface BlogCategory {
39
- slug: string;
40
- tag: string;
41
- title: string;
42
- subtitle: string;
43
- metaDescription: string;
44
- }
45
-
46
- export interface BlogConfig {
47
- contentDir?: string;
48
- defaultAuthor?: string;
49
- categories?: BlogCategory[];
50
- }
51
-
52
- export interface BlogAPI {
53
- getAllPosts(): Promise<BlogPostMetadata[]>;
54
- getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]>;
55
- getFutureBlogSlugs(): Promise<string[]>;
56
- getAllPostSlugs(): Promise<string[]>;
57
- getPostBySlug(slug: string): Promise<BlogPost | null>;
58
- getAllTags(): Promise<string[]>;
59
- getPostsByTag(tag: string): Promise<BlogPostMetadata[]>;
60
- getAllCategorySlugs(): string[];
61
- getCategoryBySlug(slug: string): BlogCategory | undefined;
62
- getPostsByCategory(slug: string): Promise<BlogPostMetadata[]>;
63
- categories: BlogCategory[];
64
- }
65
-
66
- function getTodayUTC(): Date {
67
- const now = new Date();
68
- return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
69
- }
70
-
71
- function fileSlug(name: string): string {
72
- return name.replace(/\.md$/, '');
73
- }
74
-
75
- function canonicalSlugFor(name: string, fm: { slug?: string }): string {
76
- return fm.slug || fileSlug(name);
77
- }
78
-
79
- export function createBlog(config: BlogConfig = {}): BlogAPI {
80
- const contentDir = path.resolve(process.cwd(), config.contentDir ?? 'content/blog');
81
- const defaultAuthor = config.defaultAuthor;
82
- const categories = config.categories ?? [];
83
-
84
- function readAllFiles() {
85
- if (!fs.existsSync(contentDir)) return [];
86
- return fs.readdirSync(contentDir)
87
- .filter((n) => n.endsWith('.md'))
88
- .map((fileName) => {
89
- const raw = fs.readFileSync(path.join(contentDir, fileName), 'utf8');
90
- const parsed = matter(raw);
91
- const data = parsed.data as Partial<BlogPostFrontmatter>;
92
- return {
93
- fileName,
94
- fileSlug: fileSlug(fileName),
95
- canonicalSlug: canonicalSlugFor(fileName, data),
96
- data,
97
- content: parsed.content,
98
- };
99
- });
100
- }
101
-
102
- function toMetadata(f: ReturnType<typeof readAllFiles>[number]): BlogPostMetadata {
103
- return {
104
- slug: f.canonicalSlug,
105
- title: f.data.title ?? '',
106
- description: f.data.description ?? '',
107
- date: f.data.date ?? '',
108
- author: f.data.author ?? defaultAuthor,
109
- image: f.data.image,
110
- tags: f.data.tags,
111
- readingTime: readingTime(f.content).text,
112
- };
113
- }
114
-
115
- async function getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]> {
116
- return readAllFiles()
117
- .map(toMetadata)
118
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
119
- }
120
-
121
- async function getAllPosts(): Promise<BlogPostMetadata[]> {
122
- const today = getTodayUTC();
123
- const all = await getAllPostsIncludingFuture();
124
- return all.filter((p) => new Date(p.date) <= today);
125
- }
126
-
127
- async function getFutureBlogSlugs(): Promise<string[]> {
128
- const today = getTodayUTC();
129
- const slugs = new Set<string>();
130
- for (const f of readAllFiles()) {
131
- if (!f.data.date) continue;
132
- if (new Date(f.data.date) > today) {
133
- slugs.add(f.canonicalSlug);
134
- if (f.fileSlug !== f.canonicalSlug) slugs.add(f.fileSlug);
135
- }
136
- }
137
- return Array.from(slugs);
138
- }
139
-
140
- async function getAllPostSlugs(): Promise<string[]> {
141
- const slugs = new Set<string>();
142
- for (const f of readAllFiles()) {
143
- slugs.add(f.fileSlug);
144
- if (f.canonicalSlug !== f.fileSlug) slugs.add(f.canonicalSlug);
145
- }
146
- return Array.from(slugs);
147
- }
148
-
149
- async function getPostBySlug(slug: string): Promise<BlogPost | null> {
150
- const match = readAllFiles().find(
151
- (f) => f.canonicalSlug === slug || f.fileSlug === slug
152
- );
153
- if (!match) return null;
154
- return {
155
- ...toMetadata(match),
156
- faqs: match.data.faqs,
157
- content: match.content,
158
- };
159
- }
160
-
161
- async function getAllTags(): Promise<string[]> {
162
- const posts = await getAllPosts();
163
- const tags = new Set<string>();
164
- posts.forEach((p) => p.tags?.forEach((t) => tags.add(t)));
165
- return Array.from(tags).sort();
166
- }
167
-
168
- async function getPostsByTag(tag: string): Promise<BlogPostMetadata[]> {
169
- const posts = await getAllPosts();
170
- return posts.filter((p) => p.tags?.includes(tag));
171
- }
172
-
173
- function getAllCategorySlugs(): string[] {
174
- return categories.map((c) => c.slug);
175
- }
176
-
177
- function getCategoryBySlug(slug: string): BlogCategory | undefined {
178
- return categories.find((c) => c.slug === slug);
179
- }
180
-
181
- async function getPostsByCategory(slug: string): Promise<BlogPostMetadata[]> {
182
- const cat = getCategoryBySlug(slug);
183
- if (!cat) return [];
184
- const posts = await getAllPosts();
185
- return posts.filter((p) => p.tags?.includes(cat.tag));
186
- }
187
-
188
- return {
189
- getAllPosts,
190
- getAllPostsIncludingFuture,
191
- getFutureBlogSlugs,
192
- getAllPostSlugs,
193
- getPostBySlug,
194
- getAllTags,
195
- getPostsByTag,
196
- getAllCategorySlugs,
197
- getCategoryBySlug,
198
- getPostsByCategory,
199
- categories,
200
- };
201
- }
1
+ export * from './types.js';
2
+ export { createBlog } from './fs.js';
3
+ export type { FsBlogConfig } from './fs.js';
4
+ export { createBlogFromCollection } from './collection.js';
5
+ export type { CollectionBlogConfig } from './collection.js';
@@ -0,0 +1,71 @@
1
+ export interface FAQItem {
2
+ question: string;
3
+ answer: string;
4
+ }
5
+
6
+ export interface BlogPostFrontmatter {
7
+ title: string;
8
+ description: string;
9
+ date: string | Date;
10
+ slug?: string;
11
+ author?: string;
12
+ image?: string;
13
+ tags?: string[];
14
+ faqs?: FAQItem[];
15
+ draft?: boolean;
16
+ /** Additional fields. */
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export interface BlogPostMetadata {
21
+ slug: string;
22
+ title: string;
23
+ description: string;
24
+ /** ISO date string (YYYY-MM-DD or full ISO). */
25
+ date: string;
26
+ author?: string;
27
+ image?: string;
28
+ tags?: string[];
29
+ readingTime: string;
30
+ }
31
+
32
+ export interface BlogPost extends BlogPostMetadata {
33
+ faqs?: FAQItem[];
34
+ /** Raw markdown body (fs mode) or rendered HTML (CC mode). See per-function notes. */
35
+ content: string;
36
+ }
37
+
38
+ export interface BlogCategory {
39
+ slug: string;
40
+ tag: string;
41
+ title: string;
42
+ subtitle: string;
43
+ metaDescription: string;
44
+ }
45
+
46
+ export interface BlogAPI {
47
+ /** Published posts only (date <= today, not draft). Sorted newest first. */
48
+ getAllPosts(): Promise<BlogPostMetadata[]>;
49
+ /** All non-draft posts including future-dated ones. Sorted newest first. */
50
+ getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]>;
51
+ /** Slugs of non-draft posts with a future date. */
52
+ getFutureBlogSlugs(): Promise<string[]>;
53
+ /** Every slug needed for static path generation (includes future-dated, excludes drafts). */
54
+ getAllPostSlugs(): Promise<string[]>;
55
+ getPostBySlug(slug: string): Promise<BlogPost | null>;
56
+ getAllTags(): Promise<string[]>;
57
+ getPostsByTag(tag: string): Promise<BlogPostMetadata[]>;
58
+ getAllCategorySlugs(): string[];
59
+ getCategoryBySlug(slug: string): BlogCategory | undefined;
60
+ getPostsByCategory(slug: string): Promise<BlogPostMetadata[]>;
61
+ categories: BlogCategory[];
62
+ }
63
+
64
+ export interface CommonBlogConfig {
65
+ defaultAuthor?: string;
66
+ categories?: BlogCategory[];
67
+ /** Frontmatter field name for the date. Default 'date'. */
68
+ dateField?: string;
69
+ /** Frontmatter field name for the draft flag. Default null (no draft support). */
70
+ draftField?: string | null;
71
+ }
@@ -0,0 +1,27 @@
1
+ // Drop into `src/content.config.ts` (Astro). Adjust schema to your needs.
2
+ import { defineCollection, z } from 'astro:content';
3
+ import { glob } from 'astro/loaders';
4
+
5
+ const blog = defineCollection({
6
+ loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
7
+ schema: z.object({
8
+ title: z.string(),
9
+ description: z.string(),
10
+ pubDate: z.coerce.date(),
11
+ updatedDate: z.coerce.date().optional(),
12
+ author: z.string().default('editor'),
13
+ image: z.string().optional(),
14
+ tags: z.array(z.string()).default([]),
15
+ draft: z.boolean().default(false),
16
+ faqs: z
17
+ .array(
18
+ z.object({
19
+ question: z.string(),
20
+ answer: z.string(),
21
+ })
22
+ )
23
+ .optional(),
24
+ }),
25
+ });
26
+
27
+ export const collections = { blog };
@@ -0,0 +1,14 @@
1
+ // FS + gray-matter variant — use for non-Astro projects (Next.js, etc.).
2
+ // For Astro, see `blog.ts` (uses Content Collections).
3
+
4
+ import { createBlog, type BlogCategory } from '@ibalzam/codejitsu-core/blog';
5
+
6
+ const categories: BlogCategory[] = [];
7
+
8
+ export const blog = createBlog({
9
+ contentDir: 'content/blog',
10
+ dateField: 'date', // 'date' or 'pubDate' depending on your frontmatter
11
+ draftField: null, // set to 'draft' if your frontmatter uses it
12
+ defaultAuthor: 'TODO: Site Author',
13
+ categories,
14
+ });
@@ -1,4 +1,7 @@
1
- import { createBlog, type BlogCategory } from '@ibalzam/codejitsu-core/blog';
1
+ // Astro Content Collections variant recommended for Astro sites.
2
+ // For non-Astro projects, see `blog-fs.ts` (uses gray-matter directly).
3
+
4
+ import { createBlogFromCollection, type BlogCategory } from '@ibalzam/codejitsu-core/blog';
2
5
 
3
6
  const categories: BlogCategory[] = [
4
7
  // {
@@ -10,8 +13,13 @@ const categories: BlogCategory[] = [
10
13
  // },
11
14
  ];
12
15
 
13
- export const blog = createBlog({
14
- contentDir: 'content/blog',
16
+ export const blog = createBlogFromCollection({
17
+ collectionName: 'blog',
18
+ // Match the field name from your Astro CC schema (`src/content.config.ts`).
19
+ // Common choices: 'date' (default) or 'pubDate'.
20
+ dateField: 'pubDate',
21
+ // Set to null if your schema has no `draft` field.
22
+ draftField: 'draft',
15
23
  defaultAuthor: 'TODO: Site Author',
16
24
  categories,
17
25
  });
@@ -0,0 +1,121 @@
1
+ # Config module — instructions for Claude
2
+
3
+ The unified config drives every other module. When you set up a Codejitsu site, **always create `codejitsu.config.ts` first** — every other module reads from it.
4
+
5
+ ## What this module provides
6
+
7
+ - `defineConfig(config)` — identity helper that types your config (autocomplete + validation in editors).
8
+ - `loadConfig(cwd?)` — used internally by the CLIs to resolve the config from the site's working directory.
9
+ - `isModuleEnabled(config, name)` — utility for module-aware code paths.
10
+ - Full type definitions (`CodejitsuConfig`, `SiteConfig`, `BlogConfig`, etc.).
11
+
12
+ ## Wiring into a site
13
+
14
+ ### 1. Create `codejitsu.config.ts` at the site root
15
+
16
+ ```ts
17
+ import { defineConfig } from '@ibalzam/codejitsu-core/config';
18
+
19
+ export default defineConfig({
20
+ site: {
21
+ url: 'https://example.com',
22
+ name: 'Example',
23
+ titleSuffix: ' — Example',
24
+ defaultAuthor: 'editor',
25
+ defaultOgImage: '/og-image.webp',
26
+ locale: 'en-US',
27
+ business: {
28
+ telephone: '+1-555-555-5555',
29
+ email: 'hello@example.com',
30
+ address: { addressLocality: 'Las Vegas', addressRegion: 'NV', addressCountry: 'US' },
31
+ areaServed: ['Las Vegas', 'Henderson'],
32
+ },
33
+ },
34
+
35
+ blog: {
36
+ mode: 'collection', // Astro Content Collections
37
+ collectionName: 'blog',
38
+ dateField: 'pubDate', // pearl pattern
39
+ draftField: 'draft',
40
+ },
41
+
42
+ seo: {
43
+ sitemap: {
44
+ excludePatterns: [/\/lp\//],
45
+ },
46
+ defaultSchemas: ['localBusiness', 'website'],
47
+ },
48
+
49
+ images: {
50
+ sourceDir: 'public/assets/images',
51
+ defaultQuality: 82,
52
+ defaultMaxSize: 1376,
53
+ specialRules: {
54
+ 'logos/logo': { maxWidth: 400, quality: 35 },
55
+ },
56
+ },
57
+
58
+ llms: {
59
+ mode: 'content-scan',
60
+ contentScan: {
61
+ servicesDir: 'src/content/services',
62
+ locationsDir: 'src/content/locations',
63
+ pagesDir: 'src/pages',
64
+ },
65
+ blogDir: 'src/content/blog',
66
+ },
67
+
68
+ deploy: { cloudflarePagesName: 'example' },
69
+ });
70
+ ```
71
+
72
+ ### 2. Install jiti (only if using `.ts`)
73
+
74
+ ```bash
75
+ npm install -D jiti
76
+ ```
77
+
78
+ `.mjs` and `.json` configs work without jiti.
79
+
80
+ ### 3. The CLIs find it automatically
81
+
82
+ `codejitsu-optimize-images`, `codejitsu-llms`, and `codejitsu-check` all read from this one file. No more per-module config files.
83
+
84
+ ## Search order
85
+
86
+ The loader looks for, in order:
87
+ 1. `codejitsu.config.ts`
88
+ 2. `codejitsu.config.mts`
89
+ 3. `codejitsu.config.mjs`
90
+ 4. `codejitsu.config.js`
91
+ 5. `codejitsu.config.json`
92
+ 6. `codejitsu` key in `package.json`
93
+
94
+ First match wins. Stop searching after one is found.
95
+
96
+ ## Disabling a module
97
+
98
+ Two ways:
99
+
100
+ ```ts
101
+ // Explicit:
102
+ export default defineConfig({
103
+ site: {...},
104
+ blog: { enabled: false },
105
+ });
106
+
107
+ // Or omit the key entirely:
108
+ export default defineConfig({
109
+ site: {...},
110
+ // no blog → blog module is disabled
111
+ });
112
+ ```
113
+
114
+ The checklist runner uses `isModuleEnabled()` to skip checks for disabled modules.
115
+
116
+ ## What must NOT be done
117
+
118
+ - **Don't keep old per-module config files alongside the new one.** v0.2.0 hard-breaks `codejitsu-images.config.mjs` and `codejitsu-llms.config.mjs`. Delete them.
119
+ - **Don't store secrets in this config.** It ships to the client in some module configurations (e.g. business info → schema.org). Use env vars for anything sensitive.
120
+ - **Don't put computed values that depend on runtime state.** The config loads once at CLI start. If you need dynamic values (e.g. fetched from an API), do it inside the CLI's invocation, not in the config.
121
+ - **Don't duplicate `site.url` in module configs.** All modules read it from `site.url`. Setting it once means changing it once.
@@ -0,0 +1,18 @@
1
+ import type { CodejitsuConfig } from './types.js';
2
+
3
+ /**
4
+ * Identity helper that types your `codejitsu.config.ts` export.
5
+ *
6
+ * @example
7
+ * // codejitsu.config.ts
8
+ * import { defineConfig } from '@ibalzam/codejitsu-core/config';
9
+ *
10
+ * export default defineConfig({
11
+ * site: { url: 'https://example.com', name: 'Example' },
12
+ * blog: { mode: 'collection', dateField: 'pubDate', draftField: 'draft' },
13
+ * // ...
14
+ * });
15
+ */
16
+ export function defineConfig(config: CodejitsuConfig): CodejitsuConfig {
17
+ return config;
18
+ }
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export { defineConfig } from './define.js';
3
+ export { loadConfig, isModuleEnabled } from './load.js';
@@ -0,0 +1,96 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import type { CodejitsuConfig } from './types.js';
5
+
6
+ const CANDIDATES = [
7
+ 'codejitsu.config.ts',
8
+ 'codejitsu.config.mts',
9
+ 'codejitsu.config.mjs',
10
+ 'codejitsu.config.js',
11
+ 'codejitsu.config.json',
12
+ ];
13
+
14
+ /**
15
+ * Loads the Codejitsu config from the current working directory (or `cwd`).
16
+ *
17
+ * Search order:
18
+ * 1. `codejitsu.config.{ts,mts,mjs,js,json}` at cwd root.
19
+ * 2. `codejitsu` key in `package.json`.
20
+ *
21
+ * `.ts` and `.mts` files are loaded via `jiti` (peer-installable). If `jiti`
22
+ * isn't available, the loader skips `.ts` candidates and warns once.
23
+ *
24
+ * Throws if no config is found.
25
+ */
26
+ export async function loadConfig(cwd: string = process.cwd()): Promise<CodejitsuConfig> {
27
+ for (const name of CANDIDATES) {
28
+ const filePath = path.join(cwd, name);
29
+ if (!fs.existsSync(filePath)) continue;
30
+
31
+ if (name.endsWith('.json')) {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8')) as CodejitsuConfig;
33
+ }
34
+
35
+ if (name.endsWith('.ts') || name.endsWith('.mts')) {
36
+ const config = await loadWithJiti(filePath);
37
+ if (config) return config;
38
+ // jiti unavailable; fall through to other candidates.
39
+ continue;
40
+ }
41
+
42
+ // .mjs / .js — Node can load these directly.
43
+ const mod = await import(pathToFileURL(filePath).href);
44
+ return (mod.default ?? mod) as CodejitsuConfig;
45
+ }
46
+
47
+ // Fallback: package.json `codejitsu` key.
48
+ const pkgPath = path.join(cwd, 'package.json');
49
+ if (fs.existsSync(pkgPath)) {
50
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
51
+ if (pkg.codejitsu) return pkg.codejitsu as CodejitsuConfig;
52
+ }
53
+
54
+ throw new Error(
55
+ `No Codejitsu config found in ${cwd}. Create codejitsu.config.ts (or .mjs/.json) ` +
56
+ `at the site root, or add a "codejitsu" key to package.json.`
57
+ );
58
+ }
59
+
60
+ let jitiWarned = false;
61
+ async function loadWithJiti(filePath: string): Promise<CodejitsuConfig | null> {
62
+ try {
63
+ const jitiMod = await import('jiti');
64
+ const jitiFactory = (jitiMod as any).default ?? (jitiMod as any).createJiti ?? jitiMod;
65
+ const jiti = typeof jitiFactory === 'function' ? jitiFactory(process.cwd(), { interopDefault: true }) : null;
66
+ if (!jiti) {
67
+ throw new Error('Unexpected jiti API shape.');
68
+ }
69
+ const mod = await jiti.import(filePath, { default: true });
70
+ return (mod as any) as CodejitsuConfig;
71
+ } catch (err) {
72
+ if (!jitiWarned) {
73
+ const reason = err instanceof Error ? err.message : String(err);
74
+ console.warn(
75
+ `[@ibalzam/codejitsu-core] Could not load TypeScript config (${reason}). ` +
76
+ `Install \`jiti\` as a dev dependency, or rename to codejitsu.config.mjs.`
77
+ );
78
+ jitiWarned = true;
79
+ }
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Returns true if the named module is enabled in the config.
86
+ * A module is enabled if its key is present and not `false` and not `enabled: false`.
87
+ */
88
+ export function isModuleEnabled(
89
+ config: CodejitsuConfig,
90
+ module: 'blog' | 'seo' | 'images' | 'llms' | 'deploy'
91
+ ): boolean {
92
+ const value = config[module];
93
+ if (value === undefined || value === false) return false;
94
+ if (typeof value === 'object' && value !== null && (value as { enabled?: boolean }).enabled === false) return false;
95
+ return true;
96
+ }