@ibalzam/codejitsu-core 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/bin/codejitsu.mjs +45 -0
- package/modules/cli/src/blog.mjs +99 -0
- package/modules/cli/src/format.mjs +65 -0
- package/package.json +2 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runBlog } from '../modules/cli/src/blog.mjs';
|
|
3
|
+
|
|
4
|
+
const subcommand = process.argv[2];
|
|
5
|
+
|
|
6
|
+
const COMMANDS = {
|
|
7
|
+
'blog:list': () => runBlog('blog:list'),
|
|
8
|
+
'blog:drafts': () => runBlog('blog:drafts'),
|
|
9
|
+
// Aliases for the existing standalone bins
|
|
10
|
+
llms: () => import('../modules/llms/bin/generate.mjs'),
|
|
11
|
+
'optimize-images': () => import('../modules/images/bin/optimize.mjs'),
|
|
12
|
+
check: () => import('../checklist/bin/run.mjs'),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
16
|
+
printHelp();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const handler = COMMANDS[subcommand];
|
|
21
|
+
if (!handler) {
|
|
22
|
+
console.error(`Unknown subcommand: ${subcommand}\n`);
|
|
23
|
+
printHelp();
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await handler();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printHelp() {
|
|
35
|
+
console.log(`\nUsage: codejitsu <subcommand>\n`);
|
|
36
|
+
console.log(`Subcommands:`);
|
|
37
|
+
console.log(` blog:list List every non-draft post with publish status + URL + image check`);
|
|
38
|
+
console.log(` blog:drafts List future-dated (pending) posts only`);
|
|
39
|
+
console.log(``);
|
|
40
|
+
console.log(` llms Generate public/llms.txt and public/llms-full.txt`);
|
|
41
|
+
console.log(` optimize-images Optimize images per codejitsu.config`);
|
|
42
|
+
console.log(` check Run sitewide checklist`);
|
|
43
|
+
console.log(``);
|
|
44
|
+
console.log(`All commands read codejitsu.config.{ts,mjs,json} from the current directory.`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { loadConfig, isModuleEnabled } from '../../config/src/load.mjs';
|
|
4
|
+
import { createBlog } from '../../blog/src/fs.js';
|
|
5
|
+
import { c, table } from './format.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `codejitsu blog:list` — show every non-draft post (published + pending).
|
|
9
|
+
* `codejitsu blog:drafts` — show only future-dated (pending) posts.
|
|
10
|
+
*/
|
|
11
|
+
export async function runBlog(subcommand) {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const config = await loadConfig(cwd);
|
|
14
|
+
|
|
15
|
+
if (!isModuleEnabled(config, 'blog')) {
|
|
16
|
+
console.error(c.red('blog module is disabled in codejitsu.config'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const blogCfg = config.blog && typeof config.blog === 'object' ? config.blog : {};
|
|
21
|
+
const contentDir = blogCfg.contentDir ?? 'src/content/blog';
|
|
22
|
+
const dateField = blogCfg.dateField ?? 'date';
|
|
23
|
+
const draftField = blogCfg.draftField ?? null;
|
|
24
|
+
|
|
25
|
+
const blog = createBlog({
|
|
26
|
+
contentDir,
|
|
27
|
+
dateField,
|
|
28
|
+
draftField,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const today = todayUTC();
|
|
32
|
+
const siteUrl = config.site.url.replace(/\/$/, '');
|
|
33
|
+
|
|
34
|
+
if (subcommand === 'blog:drafts') {
|
|
35
|
+
const future = await blog.getAllPostsIncludingFuture();
|
|
36
|
+
const drafts = future.filter((p) => new Date(p.date) > today);
|
|
37
|
+
printPosts(drafts, { siteUrl, contentDir, today, kind: 'drafts' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// blog:list — published + pending, sorted newest first
|
|
42
|
+
const all = await blog.getAllPostsIncludingFuture();
|
|
43
|
+
printPosts(all, { siteUrl, contentDir, today, kind: 'all' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function todayUTC() {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function daysBetween(future, today) {
|
|
52
|
+
return Math.round((future.getTime() - today.getTime()) / 86_400_000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printPosts(posts, { siteUrl, contentDir, today, kind }) {
|
|
56
|
+
if (posts.length === 0) {
|
|
57
|
+
console.log(c.dim(kind === 'drafts' ? 'No pending posts.' : 'No posts.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cwd = process.cwd();
|
|
62
|
+
const publicDir = path.join(cwd, 'public');
|
|
63
|
+
|
|
64
|
+
const rows = posts.map((p) => {
|
|
65
|
+
const postDate = new Date(p.date);
|
|
66
|
+
const isFuture = postDate > today;
|
|
67
|
+
const days = isFuture ? `+${daysBetween(postDate, today)}d` : 'live';
|
|
68
|
+
|
|
69
|
+
const url = `${siteUrl}/blog/${p.slug}/`;
|
|
70
|
+
const imgStatus = formatImageStatus(p.image, publicDir);
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
isFuture ? c.yellow(days) : c.green(days),
|
|
74
|
+
p.date,
|
|
75
|
+
imgStatus,
|
|
76
|
+
url,
|
|
77
|
+
];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
table(['STATUS', 'DATE', 'IMG', 'URL'], rows);
|
|
81
|
+
|
|
82
|
+
const published = posts.filter((p) => new Date(p.date) <= today).length;
|
|
83
|
+
const pending = posts.length - published;
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(
|
|
86
|
+
`${c.bold(String(posts.length))} posts · ` +
|
|
87
|
+
`${c.green(`${published} live`)} · ` +
|
|
88
|
+
`${c.yellow(`${pending} pending`)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatImageStatus(image, publicDir) {
|
|
93
|
+
if (!image) return c.gray('—');
|
|
94
|
+
const rel = image.startsWith('/') ? image.slice(1) : image;
|
|
95
|
+
const fullPath = path.join(publicDir, rel);
|
|
96
|
+
if (fs.existsSync(fullPath)) return c.green('✓');
|
|
97
|
+
return c.red(`✗ ${image}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Tiny ANSI color + table helpers. No deps.
|
|
2
|
+
|
|
3
|
+
const isTTY = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4
|
+
|
|
5
|
+
function wrap(code) {
|
|
6
|
+
return isTTY ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => s;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const c = {
|
|
10
|
+
dim: wrap('2'),
|
|
11
|
+
bold: wrap('1'),
|
|
12
|
+
red: wrap('31'),
|
|
13
|
+
green: wrap('32'),
|
|
14
|
+
yellow: wrap('33'),
|
|
15
|
+
blue: wrap('34'),
|
|
16
|
+
magenta: wrap('35'),
|
|
17
|
+
cyan: wrap('36'),
|
|
18
|
+
gray: wrap('90'),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ansiRe = /\x1b\[[0-9;]*m/g;
|
|
22
|
+
|
|
23
|
+
function visibleLength(s) {
|
|
24
|
+
return s.replace(ansiRe, '').length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function padCol(s, width) {
|
|
28
|
+
const diff = width - visibleLength(s);
|
|
29
|
+
return diff > 0 ? s + ' '.repeat(diff) : s;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function truncate(s, width) {
|
|
33
|
+
if (visibleLength(s) <= width) return s;
|
|
34
|
+
const plain = s.replace(ansiRe, '');
|
|
35
|
+
return plain.slice(0, width - 1) + '…';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Print a table.
|
|
40
|
+
*
|
|
41
|
+
* @param {string[]} headers
|
|
42
|
+
* @param {string[][]} rows
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @param {number[]} [opts.maxWidths]
|
|
45
|
+
*/
|
|
46
|
+
export function table(headers, rows, opts = {}) {
|
|
47
|
+
const cols = headers.length;
|
|
48
|
+
const widths = new Array(cols).fill(0);
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < cols; i++) {
|
|
51
|
+
widths[i] = visibleLength(headers[i]);
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
widths[i] = Math.max(widths[i], visibleLength(row[i] ?? ''));
|
|
54
|
+
}
|
|
55
|
+
if (opts.maxWidths?.[i]) {
|
|
56
|
+
widths[i] = Math.min(widths[i], opts.maxWidths[i]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(headers.map((h, i) => c.bold(padCol(h, widths[i]))).join(' '));
|
|
61
|
+
console.log(widths.map((w) => c.gray('─'.repeat(w))).join(' '));
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
console.log(row.map((cell, i) => padCol(truncate(cell ?? '', widths[i]), widths[i])).join(' '));
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibalzam/codejitsu-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared core for Codejitsu Astro sites — reusable code and Claude-facing instructions for blog, SEO, images, deploy, and llms.txt.",
|
|
6
6
|
"keywords": [
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"./package.json": "./package.json"
|
|
69
69
|
},
|
|
70
70
|
"bin": {
|
|
71
|
+
"codejitsu": "./bin/codejitsu.mjs",
|
|
71
72
|
"codejitsu-llms": "./modules/llms/bin/generate.mjs",
|
|
72
73
|
"codejitsu-optimize-images": "./modules/images/bin/optimize.mjs",
|
|
73
74
|
"codejitsu-check": "./checklist/bin/run.mjs"
|