@junovy/bookstack-cli
Version:
A CLI for viewing, searching, importing, and exporting BookStack content.
1,144 lines • 57.4 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.program = void 0;
const commander_1 = require("commander");
const bookstack_client_1 = require("./bookstack-client");
const fs = __importStar(require("fs-extra"));
const config_1 = require("./config");
const axios_1 = require("axios");
const ui_1 = require("./ui");
const program = new commander_1.Command();
exports.program = program;
// Read version from package.json at runtime (both from src and dist)
function readVersion() {
try {
if (process.env.BOOKSTACK_CLI_VERSION) {
return process.env.BOOKSTACK_CLI_VERSION;
}
// dist/bookstack-cli.js -> ../package.json
// src/bookstack-cli.ts -> ../package.json
// In npm package, package.json is always present at root
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../package.json');
return pkg?.version || '0.0.0';
}
catch {
return process.env.BOOKSTACK_CLI_VERSION || '0.0.0';
}
}
program
.name("bookstack")
.description("An Automated CLI for viewing, managing, importing, and exporting content for BookStack")
.version(readVersion());
// 'help' manpage
program
.command("help")
.description("Show a concise CLI reference")
.action(async () => {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts?.noColor, quiet: !!globalOpts?.quiet });
const pad = (s, n = 18) => (s + ' '.repeat(n)).slice(0, n);
const lines = [];
lines.push(`${ui_1.c.bold('BookStack CLI')} – view, import, and export BookStack content`);
lines.push('');
lines.push(`${ui_1.c.bold('Usage')}`);
lines.push(' bookstack [global options] <command> [subcommand] [options]');
lines.push('');
lines.push(`${ui_1.c.bold('Global Options')}`);
lines.push(` ${pad('-u, --url <url>')}${ui_1.c.gray('BookStack base URL')}`);
lines.push(` ${pad('-i, --token-id <id>')}${ui_1.c.gray('API token ID')}`);
lines.push(` ${pad('-s, --token-secret <secret>')}${ui_1.c.gray('API token secret')}`);
lines.push(` ${pad('-c, --config <path>')}${ui_1.c.gray('Config file (auto-detected if omitted)')}`);
lines.push(` ${pad('-q, --quiet')}${ui_1.c.gray('Suppress non-essential output')}`);
lines.push(` ${pad('--no-color')}${ui_1.c.gray('Disable ANSI colors')}`);
lines.push('');
lines.push(`${ui_1.c.bold('Core Commands')}`);
lines.push(` ${pad('books list')}${ui_1.c.gray('List books (use --json for JSON)')}`);
lines.push(` ${pad('book show <book>')}${ui_1.c.gray('Show a book with contents (--json, --plain)')}`);
lines.push(` ${pad('book tree <book>')}${ui_1.c.gray('Tree of chapters/pages (--ids, --type, --json, --plain)')}`);
lines.push(` ${pad('book export <book>')}${ui_1.c.gray('Export book (markdown|html|plaintext|pdf)')}`);
lines.push(` ${pad('book export-contents <book>')}${ui_1.c.gray('Write chapter/page files to a folder')}`);
lines.push(` ${pad('chapters list --book <book>')}${ui_1.c.gray('List chapters for a book (--json)')}`);
lines.push(` ${pad('chapter show <chapter>')}${ui_1.c.gray('Show chapter and pages (--json, --plain)')}`);
lines.push(` ${pad('chapter export <chapter>')}${ui_1.c.gray('Export chapter (markdown|html|plaintext|pdf)')}`);
lines.push(` ${pad('pages list [--book <book>]')}${ui_1.c.gray('List pages (optionally by book) (--json)')}`);
lines.push(` ${pad('page show <page>')}${ui_1.c.gray('Show page metadata (--json)')}`);
lines.push(` ${pad('page export <page>')}${ui_1.c.gray('Export page (markdown|html|plaintext|pdf)')}`);
lines.push(` ${pad('shelves list')}${ui_1.c.gray('List shelves (--json)')}`);
lines.push(` ${pad('shelves show <shelf>')}${ui_1.c.gray('Show shelf and its books')}`);
lines.push(` ${pad('search <query>')}${ui_1.c.gray('Global search (supports rich filters, --json)')}`);
lines.push(` ${pad('find <query>')}${ui_1.c.gray('Quick ID lookup (wrapper around search)')}`);
lines.push(` ${pad('import <source>')}${ui_1.c.gray('Import files/dirs into a book (--chapter-from, --flatten)')}`);
lines.push(` ${pad('config init|show')}${ui_1.c.gray('Create or inspect local config')}`);
lines.push('');
lines.push(`${ui_1.c.bold('Search Filter Examples')}`);
lines.push(` ${ui_1.c.gray('# pages or chapters named intro since 2024')}`);
lines.push(' bookstack search cloud --type page,chapter --in-name intro --updated-after 2024-01-01');
lines.push(` ${ui_1.c.gray('# add tags and JSON output')}`);
lines.push(' bookstack search cloud --tag docs --tag-kv topic=storage --json');
lines.push('');
lines.push(`${ui_1.c.bold('Output Modes')}`);
lines.push(` ${pad('--json')}${ui_1.c.gray('Structured JSON output (suppresses spinners)')}`);
lines.push(` ${pad('--plain')}${ui_1.c.gray('Simpler bullet layout for tree/show')}`);
lines.push('');
lines.push(`${ui_1.c.bold('Config Sources (priority)')}`);
lines.push(` ${pad('CLI flags')}${ui_1.c.gray('--url, --token-id, --token-secret')}`);
lines.push(` ${pad('Env/.env')}${ui_1.c.gray('BOOKSTACK_URL, BOOKSTACK_TOKEN_ID, BOOKSTACK_TOKEN_SECRET')}`);
lines.push(` ${pad('Config files')}${ui_1.c.gray('bookstack-config.json; bookstack.config.(json|yaml|yml|toml); .bookstackrc*; package.json:bookstack')}`);
console.log(lines.join('\n'));
});
// Book command group
const bookCmd = program
.command("book")
.description("Manage and inspect a single book");
// Import command (root)
program
.command("import")
.description("Import content into BookStack")
.argument("<source>", "Source file or directory to import")
.option("-b, --book <name>", "Target book name or ID")
.option("-f, --format <format>", "Source format (markdown, html, json)", "markdown")
.option("--max-depth <n>", "Max recursion depth inside subdirectories (default: 10)", (v) => parseInt(v, 10), 10)
.option("--chapter-from <source>", "Chapter naming: dir|readme", "dir")
.option("--flatten", "Import all files directly into the book (no chapters)")
.option("--dry-run", "Show what would be imported without making changes")
.action(async (source, options) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts?.noColor, quiet: !!globalOpts?.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const { ImportCommand } = await Promise.resolve().then(() => __importStar(require("./commands/import")));
const importCmd = new ImportCommand(client);
await importCmd.execute(source, {
...options,
...globalOpts,
});
}
catch (error) {
handleAxiosError(error);
}
});
bookCmd
.command("show")
.description("Show details and contents of a book")
.argument("<book>", "Book identifier (ID, name, or slug)")
.option("--json", "Output JSON")
.option("--plain", "Plain text without tree glyphs")
.action(async (bookArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({
color: !globalOpts.noColor,
quiet: !!globalOpts.quiet || !!opts.json,
});
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const bookId = await resolveBookId(client, String(bookArg));
if (bookId == null) {
console.error(ui_1.c.red(`Book not found: ${bookArg}`));
process.exit(1);
}
const spin = (0, ui_1.createSpinner)("Fetching book…").start();
const book = await client.getBook(bookId);
spin.succeed("Fetched book");
const contents = book.contents || book.content || [];
const chapters = contents.filter((c) => c.type === "chapter");
const pages = contents.filter((p) => p.type === "page");
if (opts.json) {
const data = {
id: book.id,
slug: book.slug,
name: book.name,
description: book.description || undefined,
top_level_pages: pages.map((p) => ({
id: p.id,
name: p.name,
slug: p.slug,
})),
chapters: chapters.map((ch) => ({
id: ch.id,
name: ch.name,
slug: ch.slug,
pages: (Array.isArray(ch.pages) ? ch.pages : []).map((p) => ({
id: p.id,
name: p.name,
slug: p.slug,
})),
})),
};
console.log(JSON.stringify(data, null, 2));
return;
}
// Header
console.log(`${ui_1.c.bold(book.name)} ${ui_1.c.gray(`[${book.slug}]`)} ${ui_1.c.dim(`#${book.id}`)}`);
if (book.description) {
console.log(` ${ui_1.c.italic(book.description)}`);
}
if (pages.length) {
console.log(`\n${ui_1.c.bold(ui_1.c.cyan("Top‑level Pages"))}`);
pages.forEach((p, i) => {
const bullet = opts.plain
? "-"
: i === pages.length - 1 && chapters.length === 0
? "└─"
: "├─";
console.log(` ${bullet} ${ui_1.c.green(p.name)} ${ui_1.c.gray(`[${p.slug}]`)} ${ui_1.c.dim(`#${p.id}`)}`);
});
}
if (chapters.length) {
console.log(`\n${ui_1.c.bold(ui_1.c.cyan("Chapters"))}`);
chapters.forEach((ch, ci) => {
const isLast = ci === chapters.length - 1;
const branch = opts.plain ? "*" : isLast ? "└─" : "├─";
console.log(` ${branch} ${ui_1.c.yellow(ch.name)} ${ui_1.c.gray(`[${ch.slug}]`)} ${ui_1.c.dim(`#${ch.id}`)}`);
const chPages = Array.isArray(ch.pages) ? ch.pages : [];
chPages.forEach((p, pi) => {
const subBranch = opts.plain
? "-"
: pi === chPages.length - 1
? "└─"
: "├─";
const prefix = opts.plain ? " " : isLast ? " " : "│ ";
console.log(` ${prefix}${subBranch} ${ui_1.c.green(p.name)} ${ui_1.c.gray(`[${p.slug}]`)} ${ui_1.c.dim(`#${p.id}`)}`);
});
});
}
}
catch (error) {
handleAxiosError(error);
}
});
bookCmd
.command("export")
.description("Export a book to markdown, html, plaintext, or pdf")
.argument("<book>", "Book identifier (ID, name, or slug)")
.option("-f, --format <fmt>", "Export format: markdown|html|plaintext|pdf", "markdown")
.option("-o, --out <path>", "Output file path (defaults based on format)")
.option("--stdout", "Write text formats to stdout instead of a file")
.action(async (bookArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const bookId = await resolveBookId(client, String(bookArg));
if (bookId == null) {
console.error(`Book not found: ${bookArg}`);
process.exit(1);
}
const format = String(opts.format).toLowerCase();
const book = await client.getBook(bookId);
const slug = book.slug || `book-${bookId}`;
const defaultOut = (() => {
switch (format) {
case "markdown":
return `${slug}.md`;
case "html":
return `${slug}.html`;
case "plaintext":
return `${slug}.txt`;
case "pdf":
return `${slug}.pdf`;
default:
return `${slug}.out`;
}
})();
if (format === "pdf") {
console.log(ui_1.c.yellow("Note: PDF export can take longer to generate."));
const t0 = Date.now();
const spin = (0, ui_1.createSpinner)("Exporting book (pdf)…").start();
const bytes = await client.exportBookPdf(bookId);
spin.succeed("Exported book (pdf)");
const outPath = opts.out || defaultOut;
await fs.writeFile(outPath, bytes);
const elapsed = Date.now() - t0;
console.log(`Saved PDF export to ${outPath} (${(0, ui_1.formatBytes)(bytes.length)}, ${(0, ui_1.formatDuration)(elapsed)})`);
return;
}
if (!["markdown", "html", "plaintext"].includes(format)) {
console.error("Invalid format. Use one of: markdown, html, plaintext, pdf");
process.exit(1);
}
const t1 = Date.now();
const spin2 = (0, ui_1.createSpinner)(`Exporting book (${format})…`).start();
const text = await client.exportBook(bookId, format);
spin2.succeed(`Exported book (${format})`);
if (opts.stdout) {
process.stdout.write(text);
}
else {
const outPath = opts.out || defaultOut;
await fs.writeFile(outPath, text, "utf8");
const elapsed = Date.now() - t1;
const size = Buffer.byteLength(text, 'utf8');
console.log(`Saved ${format} export to ${outPath} (${(0, ui_1.formatBytes)(size)}, ${(0, ui_1.formatDuration)(elapsed)})`);
}
}
catch (error) {
handleAxiosError(error);
}
});
bookCmd
.command("export-contents")
.description("Export a book's chapters/pages to a folder structure")
.argument("<book>", "Book identifier (ID, name, or slug)")
.option("-d, --dir <path>", "Output directory (default: ./<book-slug>)")
.option("-f, --format <fmt>", "Content format: markdown|html|plaintext", "markdown")
.option("--dry-run", "Preview files without writing")
.action(async (bookArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const bookId = await resolveBookId(client, String(bookArg));
if (bookId == null) {
console.error(`Book not found: ${bookArg}`);
process.exit(1);
}
const book = await client.getBook(bookId);
const slug = book.slug || `book-${bookId}`;
const outRoot = opts.dir || `./${slug}`;
const fmt = String(opts.format).toLowerCase();
if (!["markdown", "html", "plaintext"].includes(fmt)) {
console.error("Invalid format. Use one of: markdown, html, plaintext");
process.exit(1);
}
const ext = fmt === "plaintext" ? "txt" : fmt === "markdown" ? "md" : "html";
const contents = book.contents || book.content || [];
const chapters = contents.filter((c) => c.type === "chapter");
const pages = contents.filter((p) => p.type === "page");
// Ensure root
if (!opts.dryRun)
await fs.ensureDir(outRoot);
console.log(`Exporting to ${outRoot} in ${fmt} format${opts.dryRun ? " (dry-run)" : ""}`);
// Export top-level pages
const total = pages.length +
chapters.reduce((n, ch) => n + (Array.isArray(ch.pages) ? ch.pages.length : 0), 0);
const bar = (0, ui_1.createProgressBar)(total, "Writing");
const t0 = Date.now();
let files = 0;
let bytes = 0;
for (const p of pages) {
const filename = `${sanitize(p.slug || p.name)}-${p.id}.${ext}`;
const outPath = require("path").join(outRoot, filename);
if (opts.dryRun) {
console.log(` Would write: ${outPath}`);
}
else {
const text = await client.exportPage(p.id, fmt);
await fs.writeFile(outPath, text, "utf8");
// console.log(` Wrote: ${outPath}`);
bytes += Buffer.byteLength(text, 'utf8');
}
files += 1;
bar.tick(1);
}
// Export chapters & pages
for (const ch of chapters) {
const chDir = require("path").join(outRoot, `${sanitize(ch.slug || ch.name)}-${ch.id}`);
if (opts.dryRun)
console.log(` Would ensure dir: ${chDir}`);
else
await fs.ensureDir(chDir);
const chPages = Array.isArray(ch.pages) ? ch.pages : [];
for (const p of chPages) {
const filename = `${sanitize(p.slug || p.name)}-${p.id}.${ext}`;
const outPath = require("path").join(chDir, filename);
if (opts.dryRun) {
console.log(` Would write: ${outPath}`);
}
else {
const text = await client.exportPage(p.id, fmt);
await fs.writeFile(outPath, text, "utf8");
// console.log(` Wrote: ${outPath}`);
bytes += Buffer.byteLength(text, 'utf8');
}
files += 1;
bar.tick(1);
}
}
bar.stop("\n");
const elapsed = Date.now() - t0;
if (opts.dryRun) {
console.log(ui_1.c.dim(`Dry-run summary: ${files} files would be written under ${outRoot}.`));
}
else {
console.log(`Wrote ${files} files to ${outRoot} (${(0, ui_1.formatBytes)(bytes)}, ${(0, ui_1.formatDuration)(elapsed)}).`);
}
}
catch (error) {
handleAxiosError(error);
}
});
bookCmd
.command("tree")
.description("Print a chapter/page tree for a book")
.argument("<book>", "Book identifier (ID, name, or slug)")
.option("--ids", "Include IDs in output")
.option("--type <types>", "Filter output: page|chapter or comma/pipe-separated")
.option("--json", "Output JSON (chapters/pages)")
.option("--plain", "Plain text without tree glyphs")
.action(async (bookArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({
color: !globalOpts.noColor,
quiet: !!globalOpts.quiet || !!opts.json,
});
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const bookId = await resolveBookId(client, String(bookArg));
if (bookId == null) {
console.error(`Book not found: ${bookArg}`);
process.exit(1);
}
const book = await client.getBook(bookId);
const showId = (label, id) => opts.ids && id ? ` (${label}: ${id})` : "";
const types = opts.type
? String(opts.type)
.toLowerCase()
.split(/[|,\s]+/)
.filter(Boolean)
: [];
const wantPages = types.length === 0 || types.includes("page") || types.includes("pages");
const wantChapters = types.length === 0 ||
types.includes("chapter") ||
types.includes("chapters");
const contents = book.contents || book.content || [];
const chapters = contents.filter((c) => c.type === "chapter");
const pages = contents.filter((p) => p.type === "page");
if (opts.json) {
const data = {
id: book.id,
slug: book.slug,
name: book.name,
pages: wantPages
? pages.map((p) => ({ id: p.id, name: p.name, slug: p.slug }))
: [],
chapters: wantChapters
? chapters.map((ch) => ({
id: ch.id,
name: ch.name,
slug: ch.slug,
pages: wantPages
? (Array.isArray(ch.pages) ? ch.pages : []).map((p) => ({
id: p.id,
name: p.name,
slug: p.slug,
}))
: [],
}))
: [],
};
console.log(JSON.stringify(data, null, 2));
return;
}
console.log(`${ui_1.c.bold(book.name)} ${ui_1.c.gray(`[${book.slug}]`)}${opts.ids ? ui_1.c.dim(` #${book.id}`) : ""}`);
if (wantPages) {
pages.forEach((p, i) => {
const branch = opts.plain
? "-"
: i === pages.length - 1 && (!wantChapters || chapters.length === 0)
? "└─"
: "├─";
const idSuffix = opts.ids ? ui_1.c.dim(` #${p.id}`) : "";
console.log(` ${branch} ${ui_1.c.green(p.name)} ${ui_1.c.gray(`[${p.slug}]`)}${idSuffix}`);
});
}
if (wantChapters) {
chapters.forEach((ch, ci) => {
const isLast = ci === chapters.length - 1;
const branch = opts.plain ? "*" : isLast ? "└─" : "├─";
const idSuffix = opts.ids ? ui_1.c.dim(` #${ch.id}`) : "";
console.log(` ${branch} ${ui_1.c.yellow(ch.name)} ${ui_1.c.gray(`[${ch.slug}]`)}${idSuffix}`);
if (wantPages) {
const chPages = (ch.pages || []);
chPages.forEach((p, pi) => {
const subBranch = opts.plain
? "-"
: pi === chPages.length - 1
? "└─"
: "├─";
const prefix = opts.plain ? " " : isLast ? " " : "│ ";
const pidSuffix = opts.ids ? ui_1.c.dim(` #${p.id}`) : "";
console.log(` ${prefix}${subBranch} ${ui_1.c.green(p.name)} ${ui_1.c.gray(`[${p.slug}]`)}${pidSuffix}`);
});
}
});
}
}
catch (error) {
handleAxiosError(error);
}
});
// Search command
program
.command("search")
.description("Search across BookStack content")
.argument("<query>", "Search query")
.option("-l, --limit <n>", "Limit number of results shown", (v) => parseInt(v, 10), 25)
.option("--type <types>", "Restrict types: page|chapter|book or comma/pipe-separated")
.option("--in-name <text>", "Require text in name")
.option("--in-body <text>", "Require text in body")
.option("--created-by <slug|me>", "Created by user slug or me")
.option("--updated-by <slug|me>", "Updated by user slug or me")
.option("--owned-by <slug|me>", "Owned by user slug or me")
.option("--created-after <YYYY-MM-DD>", "Created after date")
.option("--created-before <YYYY-MM-DD>", "Created before date")
.option("--updated-after <YYYY-MM-DD>", "Updated after date")
.option("--updated-before <YYYY-MM-DD>", "Updated before date")
.option("--is-restricted", "Only items with content-level permissions")
.option("--is-template", "Only page templates")
.option("--viewed-by-me", "Only content viewed by me")
.option("--not-viewed-by-me", "Only content not viewed by me")
.option("--sort-by <method>", "Sort method (last_commented)")
.option("--tag <name>", "Tag name filter [name]", collect, [])
.option("--tag-kv <name=value>", "Tag name/value filter [name=value]", collect, [])
.option("--json", "Output JSON")
.action(async (query, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet || !!opts.json });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const builtQuery = buildSearchQuery(query, opts);
const spin = (0, ui_1.createSpinner)("Searching…").start();
const results = await client.searchAll(builtQuery);
spin.succeed(`Found ${results.length} results`);
if (opts.json) {
console.log(JSON.stringify(results, null, 2));
return;
}
const lim = Math.max(1, opts.limit || 25);
const subset = results.slice(0, lim);
if (!subset.length) {
console.log(ui_1.c.dim("No results."));
return;
}
const typeColor = (t) => t === "page" ? ui_1.c.green : t === "chapter" ? ui_1.c.yellow : ui_1.c.cyan;
console.log(ui_1.c.bold("Results:"));
subset.forEach((r) => {
const bullet = "•";
const t = (r.type || "").toLowerCase();
const tcol = typeColor(t);
const id = r.id != null ? ui_1.c.dim(`#${r.id}`) : "";
const slug = r.slug ? ui_1.c.gray(`[${r.slug}]`) : "";
const ctx = t === "page"
? `${ui_1.c.cyan("book")}:${r.book_id}${r.chapter_id ? ` ${ui_1.c.cyan("chapter")}:${r.chapter_id}` : ""}`
: t === "chapter"
? `${ui_1.c.cyan("book")}:${r.book_id}`
: "";
const url = r.url ? ` ${ui_1.c.gray("->")} ${ui_1.c.gray(r.url)}` : "";
console.log(` ${bullet} ${tcol(`[${t || "item"}]`)} ${r.name} ${slug} ${id} ${ctx}${url}`.trim());
});
if (results.length > subset.length) {
console.log(ui_1.c.dim(`... ${results.length - subset.length} more not shown (use --limit to increase).`));
}
}
catch (error) {
handleAxiosError(error);
}
});
// Shelves commands
const shelvesCmd = program.command("shelves").description("Manage shelves");
shelvesCmd
.command("list")
.description("List shelves")
.action(async () => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const spin = (0, ui_1.createSpinner)("Fetching shelves…").start();
const shelves = await client.getShelves();
spin.succeed(`Fetched ${shelves.length} shelves`);
console.log("Shelves:");
shelves.forEach((s) => {
console.log(` ${s.id}: ${s.name} (${s.slug})`);
});
}
catch (error) {
handleAxiosError(error);
}
});
// Books commands
const booksCmd = program.command("books").description("Manage books");
booksCmd
.command("list")
.description("List books")
.action(async () => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const spin = (0, ui_1.createSpinner)("Fetching books…").start();
const books = await client.getBooks();
spin.succeed(`Fetched ${books.length} books`);
console.log(ui_1.c.bold("Books:"));
books.forEach((b) => console.log(` ${b.id}: ${b.name} (${b.slug})`));
}
catch (error) {
handleAxiosError(error);
}
});
// Chapters commands
const chaptersCmd = program.command("chapters").description("Manage chapters");
chaptersCmd
.command("list")
.description("List chapters in a book")
.option("--book <id|name|slug>", "Book to filter by (required)")
.action(async (opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
if (!opts.book) {
console.error("--book option is required");
process.exit(1);
}
const bookId = await resolveBookId(client, String(opts.book));
if (bookId == null) {
console.error(`Book not found: ${opts.book}`);
process.exit(1);
}
const spin = (0, ui_1.createSpinner)("Fetching chapters…").start();
const chapters = await client.getChapters(bookId);
spin.succeed(`Fetched ${chapters.length} chapters`);
console.log(ui_1.c.bold(`Chapters in book ${opts.book}:`));
chapters.forEach((ch) => console.log(` ${ch.id}: ${ch.name} (${ch.slug})`));
}
catch (error) {
handleAxiosError(error);
}
});
// Pages commands
const pagesCmd = program.command("pages").description("Manage pages");
pagesCmd
.command("list")
.description("List pages (optionally for a book)")
.option("--book <id|name|slug>", "Book to filter by")
.action(async (opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const spin = (0, ui_1.createSpinner)(opts.book ? "Fetching pages…" : "Fetching all pages…").start();
const pages = opts.book
? await (async () => {
const bookId = await resolveBookId(client, String(opts.book));
if (bookId == null) {
console.error(`Book not found: ${opts.book}`);
process.exit(1);
}
return client.getPages(bookId);
})()
: await client.getAllPages();
spin.succeed(`Fetched ${pages.length} pages`);
console.log(ui_1.c.bold(opts.book ? `Pages in book ${opts.book}:` : "All pages:"));
pages.forEach((p) => console.log(` ${p.id}: ${p.name} (${p.slug})`));
}
catch (error) {
handleAxiosError(error);
}
});
shelvesCmd
.command("show")
.description("Show details of a shelf and its books")
.argument("<shelf>", "Shelf identifier (ID, name, or slug)")
.action(async (shelfArg) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const shelfId = await resolveShelfId(client, String(shelfArg));
if (shelfId == null) {
console.error(`Shelf not found: ${shelfArg}`);
process.exit(1);
}
const spin = (0, ui_1.createSpinner)("Fetching shelf…").start();
const shelf = await client.getShelf(shelfId);
spin.succeed("Fetched shelf");
console.log(`${shelf.name} (ID: ${shelf.id}, slug: ${shelf.slug})`);
if (shelf.description)
console.log(`Description: ${shelf.description}`);
const books = (shelf.books || []);
if (books.length) {
console.log("Books:");
books.forEach((b) => console.log(` ${b.id}: ${b.name} (${b.slug})`));
}
else {
console.log("Books: (none)");
}
}
catch (error) {
handleAxiosError(error);
}
});
// Chapter commands
const chapterCmd = program.command("chapter").description("Manage chapters");
chapterCmd
.command("export")
.description("Export a chapter to markdown, html, plaintext, or pdf")
.argument("<chapter>", "Chapter identifier (ID, name, or slug)")
.option("-f, --format <fmt>", "Export format: markdown|html|plaintext|pdf", "markdown")
.option("-o, --out <path>", "Output file path (defaults based on format)")
.option("--stdout", "Write text formats to stdout instead of a file")
.action(async (chapterArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const chapterId = await resolveChapterId(client, String(chapterArg));
if (chapterId == null) {
console.error(`Chapter not found: ${chapterArg}`);
process.exit(1);
}
const format = String(opts.format).toLowerCase();
if (format === "pdf") {
console.log(ui_1.c.yellow("Note: PDF export can take longer to generate."));
const t0 = Date.now();
const spin = (0, ui_1.createSpinner)("Exporting chapter (pdf)…").start();
const bytes = await client.exportChapterPdf(chapterId);
spin.succeed("Exported chapter (pdf)");
const outPath = opts.out || `chapter-${chapterId}.pdf`;
await fs.writeFile(outPath, bytes);
const elapsed = Date.now() - t0;
console.log(`Saved PDF export to ${outPath} (${(0, ui_1.formatBytes)(bytes.length)}, ${(0, ui_1.formatDuration)(elapsed)})`);
return;
}
if (!["markdown", "html", "plaintext"].includes(format)) {
console.error("Invalid format. Use one of: markdown, html, plaintext, pdf");
process.exit(1);
}
const t1 = Date.now();
const spin2 = (0, ui_1.createSpinner)(`Exporting chapter (${format})…`).start();
const text = await client.exportChapter(chapterId, format);
spin2.succeed(`Exported chapter (${format})`);
if (opts.stdout)
process.stdout.write(text);
else {
const outPath = opts.out ||
`chapter-${chapterId}.${format === "plaintext" ? "txt" : format}`;
await fs.writeFile(outPath, text, "utf8");
const elapsed = Date.now() - t1;
const size = Buffer.byteLength(text, 'utf8');
console.log(`Saved ${format} export to ${outPath} (${(0, ui_1.formatBytes)(size)}, ${(0, ui_1.formatDuration)(elapsed)})`);
}
}
catch (error) {
handleAxiosError(error);
}
});
// Page commands
const pageCmd = program.command("page").description("Manage pages");
pageCmd
.command("export")
.description("Export a page to markdown, html, plaintext, or pdf")
.argument("<page>", "Page identifier (ID, name, or slug)")
.option("-f, --format <fmt>", "Export format: markdown|html|plaintext|pdf", "markdown")
.option("-o, --out <path>", "Output file path (defaults based on format)")
.option("--stdout", "Write text formats to stdout instead of a file")
.action(async (pageArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const pageId = await resolvePageId(client, String(pageArg));
if (pageId == null) {
console.error(`Page not found: ${pageArg}`);
process.exit(1);
}
const format = String(opts.format).toLowerCase();
if (format === "pdf") {
console.log(ui_1.c.yellow("Note: PDF export can take longer to generate."));
const t0 = Date.now();
const spin = (0, ui_1.createSpinner)("Exporting page (pdf)…").start();
const bytes = await client.exportPagePdf(pageId);
spin.succeed("Exported page (pdf)");
const outPath = opts.out || `page-${pageId}.pdf`;
await fs.writeFile(outPath, bytes);
const elapsed = Date.now() - t0;
console.log(`Saved PDF export to ${outPath} (${(0, ui_1.formatBytes)(bytes.length)}, ${(0, ui_1.formatDuration)(elapsed)})`);
return;
}
if (!["markdown", "html", "plaintext"].includes(format)) {
console.error("Invalid format. Use one of: markdown, html, plaintext, pdf");
process.exit(1);
}
const t1 = Date.now();
const spin2 = (0, ui_1.createSpinner)(`Exporting page (${format})…`).start();
const text = await client.exportPage(pageId, format);
spin2.succeed(`Exported page (${format})`);
if (opts.stdout)
process.stdout.write(text);
else {
const outPath = opts.out ||
`page-${pageId}.${format === "plaintext" ? "txt" : format}`;
await fs.writeFile(outPath, text, "utf8");
const elapsed = Date.now() - t1;
const size = Buffer.byteLength(text, 'utf8');
console.log(`Saved ${format} export to ${outPath} (${(0, ui_1.formatBytes)(size)}, ${(0, ui_1.formatDuration)(elapsed)})`);
}
}
catch (error) {
handleAxiosError(error);
}
});
chapterCmd
.command("show")
.description("Show details and pages of a chapter")
.argument("<chapter>", "Chapter identifier (ID, name, or slug)")
.option("--json", "Output JSON")
.option("--plain", "Plain text without tree glyphs")
.action(async (chapterArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const chapterId = await resolveChapterId(client, String(chapterArg));
if (chapterId == null) {
console.error(`Chapter not found: ${chapterArg}`);
process.exit(1);
}
const spin = (0, ui_1.createSpinner)("Fetching chapter…").start();
const ch = await client.getChapter(chapterId);
spin.succeed("Fetched chapter");
if (opts.json) {
let pages = [];
if (ch.book_id) {
pages = (await client.getPages(ch.book_id))
.filter((p) => p.chapter_id === ch.id)
.map((p) => ({ id: p.id, name: p.name, slug: p.slug }));
}
console.log(JSON.stringify({
id: ch.id,
slug: ch.slug,
name: ch.name,
description: ch.description || undefined,
pages,
}, null, 2));
return;
}
console.log(`${ui_1.c.bold(ch.name)} ${ui_1.c.gray(`[${ch.slug}]`)} ${ui_1.c.dim(`#${ch.id}`)}`);
if (ch.description)
console.log(` ${ui_1.c.italic(ch.description)}`);
if (!ch.book_id) {
console.log("Pages: (unknown book; skipping)");
return;
}
const pages = (await client.getPages(ch.book_id)).filter((p) => p.chapter_id === ch.id);
if (pages.length) {
console.log(`\n${ui_1.c.bold(ui_1.c.cyan("Pages"))}`);
pages.forEach((p, i) => {
const branch = opts.plain
? "-"
: i === pages.length - 1
? "└─"
: "├─";
console.log(` ${branch} ${ui_1.c.green(p.name)} ${ui_1.c.gray(`[${p.slug}]`)} ${ui_1.c.dim(`#${p.id}`)}`);
});
}
else {
console.log(ui_1.c.dim("Pages: (none)"));
}
}
catch (error) {
handleAxiosError(error);
}
});
pageCmd
.command("show")
.description("Show details of a page")
.argument("<page>", "Page identifier (ID, name, or slug)")
.option("--json", "Output JSON")
.action(async (pageArg, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const pageId = await resolvePageId(client, String(pageArg));
if (pageId == null) {
console.error(`Page not found: ${pageArg}`);
process.exit(1);
}
const spin = (0, ui_1.createSpinner)("Fetching page…").start();
const p = await client.getPage(pageId);
spin.succeed("Fetched page");
if (opts.json) {
console.log(JSON.stringify({
id: p.id,
slug: p.slug,
name: p.name,
book_id: p.book_id,
chapter_id: p.chapter_id || null,
}, null, 2));
return;
}
console.log(`${ui_1.c.bold(p.name)} ${ui_1.c.gray(`[${p.slug}]`)} ${ui_1.c.dim(`#${p.id}`)}`);
console.log(` ${ui_1.c.cyan("Book")}: ${p.book_id} ${p.chapter_id ? `${ui_1.c.cyan("Chapter")}: ${p.chapter_id}` : ""}`);
}
catch (error) {
handleAxiosError(error);
}
});
// Find helper: quick way to get IDs by fuzzy query
program
.command("find")
.description("Find items and print IDs (wrapper around search)")
.argument("<query>", "Search query (fuzzy)")
.option("--type <types>", "Restrict types: page|chapter|book or comma/pipe-separated")
.option("-l, --limit <n>", "Limit number of results shown", (v) => parseInt(v, 10), 50)
.action(async (query, opts) => {
try {
const globalOpts = program.opts();
(0, ui_1.configureUi)({ color: !globalOpts.noColor, quiet: !!globalOpts.quiet });
const config = await (0, config_1.resolveConfig)({
explicitPath: globalOpts.config,
cli: {
url: globalOpts.url,
tokenId: globalOpts.tokenId,
tokenSecret: globalOpts.tokenSecret,
},
});
const client = new bookstack_client_1.BookStackClient({
baseUrl: config.url || "",
tokenId: config.tokenId || "",
tokenSecret: config.tokenSecret || "",
});
const built = buildSearchQuery(query, { type: opts.type });
const results = await client.searchAll(built);
const lim = Math.max(1, opts.limit || 50);
if (!results.length) {
console.log("No results.");
return;
}
results.slice(0, lim).forEach((r) => {
const ctx = r.type === "page"
? `book_id=${r.book_id} chapter_id=${r.chapter_id || "-"}`
: r.type === "chapter"
? `book_id=${r.book_id}`
: "";
console.log(`${r.id}\t${r.type}\t${r.name}\t${r.slug}${ctx ? `\t${ctx}` : ""}`);
});
}
catch (error) {
handleAxiosError(error);
}
});
// Config command
program
.command("config")
.description("Manage configuration")
.argument("<action>", "Action (init, show)")
.action(async (action) => {
const configPath = program.opts().config || "./bo