@docubook/cli 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "DocuBook CLI tool that helps you initialize, update, and deploy documentation directly from your terminal.",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,113 +1,12 @@
1
- /* global fetch */
2
1
  import { program } from "commander";
3
2
  import { collectUserInput } from "./promptHandler.js";
4
3
  import { createProject } from "../installer/projectInstaller.js";
4
+ import { handleUpdate } from "./updateHandler.js";
5
5
  import log from "../utils/logger.js";
6
6
  import { renderWelcome, renderDone, renderError } from "../tui/renderer.js";
7
7
  import { CLIState } from "../tui/state.js";
8
8
  import { detectPackageManager, getPackageManagerInfo, getPackageManagerVersion } from "../utils/packageManagerDetect.js";
9
9
  import { getAvailableTemplates, getTemplate, getDefaultTemplate } from "../utils/templateDetect.js";
10
- import { execSync } from "child_process";
11
- import ora from "ora";
12
- import fs from "fs";
13
- import os from "os";
14
- import path from "path";
15
- import { lt } from "semver";
16
-
17
- // Helpers to show changelog once per installed version. Stores shown versions under
18
- // $HOME/.docubook_cli_seen_changelogs.json as a map: { "@docubook/cli": ["1.2.3"] }
19
- const _CHANGELOG_STORE = path.join(os.homedir(), ".docubook_cli_seen_changelogs.json");
20
- function _readChangelogStore() {
21
- try {
22
- const raw = fs.readFileSync(_CHANGELOG_STORE, "utf8");
23
- return JSON.parse(raw || "{}");
24
- } catch {
25
- return {};
26
- }
27
- }
28
- function _writeChangelogStore(obj) {
29
- try {
30
- fs.writeFileSync(_CHANGELOG_STORE, JSON.stringify(obj, null, 2), { mode: 0o600 });
31
- } catch {
32
- // non-fatal
33
- }
34
- }
35
-
36
- async function _fetchChangelogFromGitHub(version) {
37
- // Fetch CHANGELOG.md from the CLI release tag format: cli-v0.2.5
38
- const repo = "DocuBook/docubook";
39
- const bare = version.replace(/^v/, "");
40
- const tag = `cli-v${bare}`;
41
-
42
- const candidates = [
43
- `https://raw.githubusercontent.com/${repo}/${tag}/CHANGELOG.md`,
44
- `https://raw.githubusercontent.com/${repo}/${tag}/CHANGELOG.MD`,
45
- // Fallback to main branch
46
- `https://raw.githubusercontent.com/${repo}/main/CHANGELOG.md`,
47
- ];
48
-
49
- for (const url of candidates) {
50
- try {
51
- const res = await fetch(url);
52
- if (res && res.ok) return await res.text();
53
- } catch {
54
- // ignore and try next
55
- }
56
- }
57
- return null;
58
- }
59
-
60
- function _extractVersionSection(changelogText, version) {
61
- if (!changelogText) return null;
62
- const lines = changelogText.split(/\r?\n/);
63
- // Look for headings that include the version (e.g. "## v1.2.3" or "## 1.2.3")
64
- const headerRe = new RegExp(`^#{1,3}\\s*(?:v)?${version.replace(/\./g, "\\.")}(?:\\b|\\D)`, "i");
65
- let start = -1;
66
- for (let i = 0; i < lines.length; i++) {
67
- if (headerRe.test(lines[i])) {
68
- start = i;
69
- break;
70
- }
71
- }
72
- if (start === -1) return changelogText.slice(0, 2000); // fallback: return beginning of changelog
73
-
74
- let end = lines.length;
75
- for (let j = start + 1; j < lines.length; j++) {
76
- // Match the next version heading (level 2, ## but not ###)
77
- if (/^##\s*(?!#)/.test(lines[j])) {
78
- end = j;
79
- break;
80
- }
81
- }
82
- return lines.slice(start, end).join("\n");
83
- }
84
-
85
- async function showChangelogOnce(pkgName, version) {
86
- try {
87
- const store = _readChangelogStore();
88
- const seen = Array.isArray(store[pkgName]) ? store[pkgName] : [];
89
- if (seen.includes(version)) return;
90
-
91
- const changelog = await _fetchChangelogFromGitHub(version);
92
- if (!changelog) return;
93
-
94
- const section = _extractVersionSection(changelog, version);
95
- if (!section) return;
96
-
97
- // Print a concise changelog section
98
- console.log("\n===========================================================\n");
99
- console.log(section.trim());
100
- console.log("\nFor full changelog, visit:");
101
- console.log(` https://github.com/DocuBook/docubook/blob/main/CHANGELOG.md\n`);
102
-
103
- // Mark as shown
104
- store[pkgName] = Array.from(new Set([...seen, version]));
105
- _writeChangelogStore(store);
106
- } catch {
107
- // silent on any error - changelog is a nicety
108
- }
109
- }
110
-
111
10
 
112
11
  /**
113
12
  * Initializes the CLI program
@@ -123,59 +22,7 @@ export function initializeProgram(version) {
123
22
  .command("update")
124
23
  .description("Check for updates and install the latest DocuBook CLI globally")
125
24
  .action(async () => {
126
- const pkgName = "@docubook/cli";
127
- // declare spinner in outer scope so catch block can safely reference it
128
- let spinner;
129
- try {
130
- // Fetch package metadata from npm registry
131
- const encoded = encodeURIComponent(pkgName);
132
- spinner = ora('Checking for updates...').start();
133
- const res = await fetch(`https://registry.npmjs.org/${encoded}`);
134
- if (!res.ok) {
135
- spinner.fail(`Failed to fetch registry metadata (status ${res.status})`);
136
- throw new Error(`Failed to fetch registry metadata (status ${res.status})`);
137
- }
138
- const data = await res.json();
139
- const latest = data && data["dist-tags"] && data["dist-tags"].latest;
140
- if (!latest) {
141
- spinner.fail("Could not determine latest version from npm registry");
142
- throw new Error("Could not determine latest version from npm registry");
143
- }
144
-
145
- // Stop spinner and print a plain "Checking for updates..." line (no check mark)
146
- if (spinner && typeof spinner.stop === 'function') spinner.stop();
147
- console.log('Checking for updates...');
148
-
149
- if (!lt(version, latest)) {
150
- console.log(`No update needed, current version is ${version}, latest release is ${latest}`);
151
- return;
152
- }
153
-
154
- console.log(`Updating ${pkgName} from ${version} to ${latest}...`);
155
-
156
- // Use npm to install globally. This will stream stdout/stderr to the user.
157
- const cmd = `npm install -g ${pkgName}@${latest}`;
158
- try {
159
- execSync(cmd, { stdio: "inherit" });
160
- console.log(`Successfully updated to ${latest}`);
161
- // Try to show changelog for the newly installed version once
162
- try {
163
- await showChangelogOnce(pkgName, latest);
164
- } catch {
165
- // non-fatal
166
- }
167
- } catch (installErr) {
168
- // If install fails, provide a helpful message
169
- console.error(`Update failed: ${installErr.message || installErr}`);
170
- console.error(`Try running the following command manually:\n ${cmd}\nIf you see permissions errors, consider running with elevated privileges or using a Node version manager.`);
171
- process.exitCode = 1;
172
- }
173
- } catch (err) {
174
- // ensure spinner is stopped on error
175
- if (spinner && typeof spinner.stop === 'function') spinner.stop();
176
- console.error(err.message || err);
177
- process.exitCode = 1;
178
- }
25
+ await handleUpdate(version);
179
26
  });
180
27
 
181
28
  // Expose a `version` subcommand: `docubook version`
@@ -0,0 +1,332 @@
1
+ /* global fetch */
2
+ import { execSync } from "child_process";
3
+ import ora from "ora";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { lt } from "semver";
8
+
9
+ // Changelog store to avoid showing same version multiple times
10
+ const _CHANGELOG_STORE = path.join(os.homedir(), ".docubook_cli_seen_changelogs.json");
11
+
12
+ function _readChangelogStore() {
13
+ try {
14
+ const raw = fs.readFileSync(_CHANGELOG_STORE, "utf8");
15
+ return JSON.parse(raw || "{}");
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+
21
+ function _writeChangelogStore(obj) {
22
+ try {
23
+ fs.writeFileSync(_CHANGELOG_STORE, JSON.stringify(obj, null, 2), { mode: 0o600 });
24
+ } catch {
25
+ // non-fatal
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Detect which package manager was used to install this CLI globally
31
+ * Returns: 'npm', 'bun', 'yarn', or 'pnpm'
32
+ */
33
+ function detectInstalledPackageManager() {
34
+ try {
35
+ // Check npm_config_user_agent environment variable
36
+ const userAgent = process.env.npm_config_user_agent || "";
37
+ if (userAgent.includes("pnpm")) return "pnpm";
38
+ if (userAgent.includes("bun")) return "bun";
39
+ if (userAgent.includes("yarn")) return "yarn";
40
+ if (userAgent.includes("npm")) return "npm";
41
+
42
+ // Fallback: check what's available in PATH
43
+ try {
44
+ execSync("pnpm --version", { stdio: "ignore" });
45
+ return "pnpm";
46
+ } catch {
47
+ // try next
48
+ }
49
+
50
+ try {
51
+ execSync("bun --version", { stdio: "ignore" });
52
+ return "bun";
53
+ } catch {
54
+ // try next
55
+ }
56
+
57
+ try {
58
+ execSync("yarn --version", { stdio: "ignore" });
59
+ return "yarn";
60
+ } catch {
61
+ // try next
62
+ }
63
+
64
+ // Default to npm
65
+ return "npm";
66
+ } catch {
67
+ return "npm";
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Fetch release info from GitHub API
73
+ * Returns: { tag_name, name, body, created_at, html_url } or null
74
+ */
75
+ async function fetchLatestReleaseFromGitHub() {
76
+ try {
77
+ const owner = "DocuBook";
78
+ const repo = "docubook";
79
+ const url = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
80
+
81
+ const res = await fetch(url, {
82
+ headers: { "Accept": "application/vnd.github.v3+json" }
83
+ });
84
+
85
+ if (!res.ok) {
86
+ return null;
87
+ }
88
+
89
+ const data = await res.json();
90
+
91
+ // Ensure it's a cli release
92
+ if (!data.tag_name || !data.tag_name.startsWith("cli-v")) {
93
+ return null;
94
+ }
95
+
96
+ return {
97
+ tag_name: data.tag_name,
98
+ name: data.name,
99
+ body: data.body || "",
100
+ created_at: data.created_at,
101
+ html_url: data.html_url,
102
+ };
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Generate changelog from git commits between two versions
110
+ * Parses conventional commits and categorizes them
111
+ */
112
+ async function generateChangelogFromCommits(fromTag, toTag) {
113
+ try {
114
+ // Get commits between two tags
115
+ const cmd = `git log ${fromTag}..${toTag} --pretty=format:"%H|%s|%b" -- packages/cli/ 2>/dev/null || echo ""`;
116
+ const output = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
117
+
118
+ if (!output || output.trim() === "") {
119
+ return null;
120
+ }
121
+
122
+ const commits = output.trim().split("\n").filter(Boolean).map((line) => {
123
+ const [hash, subject] = line.split("|");
124
+ return { hash: hash.slice(0, 7), subject };
125
+ });
126
+
127
+ if (commits.length === 0) {
128
+ return null;
129
+ }
130
+
131
+ // Categorize commits by conventional commit type
132
+ const categories = {
133
+ added: [],
134
+ fixed: [],
135
+ improved: [],
136
+ deprecated: [],
137
+ removed: [],
138
+ };
139
+
140
+ commits.forEach(({ hash, subject }) => {
141
+ const repoUrl = "https://github.com/DocuBook/docubook/commit";
142
+ const link = `[${hash}](${repoUrl}/${hash})`;
143
+
144
+ if (subject.startsWith("feat") || subject.startsWith("feat:")) {
145
+ categories.added.push(`- ${subject.replace(/^feat(\(.*?\))?:\s*/, "")} ${link}`);
146
+ } else if (subject.startsWith("fix") || subject.startsWith("fix:")) {
147
+ categories.fixed.push(`- ${subject.replace(/^fix(\(.*?\))?:\s*/, "")} ${link}`);
148
+ } else if (subject.startsWith("perf") || subject.startsWith("refactor")) {
149
+ categories.improved.push(`- ${subject.replace(/^(perf|refactor)(\(.*?\))?:\s*/, "")} ${link}`);
150
+ } else if (subject.startsWith("deprecate")) {
151
+ categories.deprecated.push(`- ${subject.replace(/^deprecate(\(.*?\))?:\s*/, "")} ${link}`);
152
+ } else if (subject.startsWith("remove") || subject.startsWith("remove:")) {
153
+ categories.removed.push(`- ${subject.replace(/^remove(\(.*?\))?:\s*/, "")} ${link}`);
154
+ }
155
+ });
156
+
157
+ // Build markdown
158
+ let markdown = "";
159
+ if (categories.added.length > 0) {
160
+ markdown += `### Added\n${categories.added.join("\n")}\n\n`;
161
+ }
162
+ if (categories.fixed.length > 0) {
163
+ markdown += `### Fixed\n${categories.fixed.join("\n")}\n\n`;
164
+ }
165
+ if (categories.improved.length > 0) {
166
+ markdown += `### Improved\n${categories.improved.join("\n")}\n\n`;
167
+ }
168
+ if (categories.deprecated.length > 0) {
169
+ markdown += `### Deprecated\n${categories.deprecated.join("\n")}\n\n`;
170
+ }
171
+ if (categories.removed.length > 0) {
172
+ markdown += `### Removed\n${categories.removed.join("\n")}\n\n`;
173
+ }
174
+
175
+ return markdown.trim() || null;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Fetch and display changelog from GitHub release
183
+ * Uses git commits for structured changelog if available
184
+ */
185
+ async function showChangelogOnce(pkgName, version, releaseInfo) {
186
+ try {
187
+ const store = _readChangelogStore();
188
+ const seen = Array.isArray(store[pkgName]) ? store[pkgName] : [];
189
+ if (seen.includes(version)) return;
190
+
191
+ let changelog = "";
192
+
193
+ // Try to generate from git commits first
194
+ try {
195
+ // Get the previous release tag
196
+ const currentTag = `cli-v${version}`;
197
+ const prevTagCmd = `git describe --tags --abbrev=0 ${currentTag}^ 2>/dev/null || echo ""`;
198
+ const prevTag = execSync(prevTagCmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
199
+
200
+ if (prevTag && prevTag.startsWith("cli-v")) {
201
+ const generatedChangelog = await generateChangelogFromCommits(prevTag, currentTag);
202
+ if (generatedChangelog) {
203
+ changelog = generatedChangelog;
204
+ }
205
+ }
206
+ } catch {
207
+ // fallback to release body
208
+ }
209
+
210
+ // Fallback to release body if no commits found
211
+ if (!changelog && releaseInfo && releaseInfo.body) {
212
+ changelog = releaseInfo.body.slice(0, 2000);
213
+ }
214
+
215
+ if (!changelog) return;
216
+
217
+ // Print changelog
218
+ console.log("\n===========================================================\n");
219
+ console.log(`## ${releaseInfo?.tag_name || `cli-v${version}`}`);
220
+ console.log("");
221
+ console.log(changelog);
222
+ console.log("\nFull changelog, visit:");
223
+ console.log(` ${releaseInfo?.html_url || `https://github.com/DocuBook/docubook/releases/tag/cli-v${version}`}\n`);
224
+
225
+ // Mark as shown
226
+ store[pkgName] = Array.from(new Set([...seen, version]));
227
+ _writeChangelogStore(store);
228
+ } catch {
229
+ // silent on any error - changelog is a nicety
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Install package using detected package manager
235
+ * Supports npm, bun, yarn, and pnpm
236
+ */
237
+ function installGlobal(packageName, version, packageManager) {
238
+ let cmd;
239
+
240
+ switch (packageManager) {
241
+ case "pnpm":
242
+ cmd = `pnpm add -g ${packageName}@${version}`;
243
+ break;
244
+ case "bun":
245
+ cmd = `bun add -g ${packageName}@${version}`;
246
+ break;
247
+ case "yarn":
248
+ cmd = `yarn global add ${packageName}@${version}`;
249
+ break;
250
+ case "npm":
251
+ default:
252
+ cmd = `npm install -g ${packageName}@${version}`;
253
+ break;
254
+ }
255
+
256
+ execSync(cmd, { stdio: "inherit" });
257
+ }
258
+
259
+ /**
260
+ * Main update handler: check for updates and install if available
261
+ */
262
+ export async function handleUpdate(currentVersion) {
263
+ const pkgName = "@docubook/cli";
264
+ let spinner;
265
+
266
+ try {
267
+ // Detect package manager
268
+ const packageManager = detectInstalledPackageManager();
269
+
270
+ // Fetch package metadata from npm registry
271
+ const encoded = encodeURIComponent(pkgName);
272
+ spinner = ora("Checking for updates...").start();
273
+
274
+ const res = await fetch(`https://registry.npmjs.org/${encoded}`);
275
+ if (!res.ok) {
276
+ spinner.fail(`Failed to fetch registry metadata (status ${res.status})`);
277
+ throw new Error(`Failed to fetch registry metadata (status ${res.status})`);
278
+ }
279
+
280
+ const data = await res.json();
281
+ const latest = data && data["dist-tags"] && data["dist-tags"].latest;
282
+ if (!latest) {
283
+ spinner.fail("Could not determine latest version from npm registry");
284
+ throw new Error("Could not determine latest version from npm registry");
285
+ }
286
+
287
+ // Stop spinner and print a plain line
288
+ if (spinner && typeof spinner.stop === "function") spinner.stop();
289
+ console.log("Checking for updates...");
290
+
291
+ if (!lt(currentVersion, latest)) {
292
+ console.log(
293
+ `No update needed, current version is ${currentVersion}, latest release is ${latest}`
294
+ );
295
+ return;
296
+ }
297
+
298
+ console.log(
299
+ `Updating ${pkgName} from ${currentVersion} to ${latest} using ${packageManager}...`
300
+ );
301
+
302
+ // Fetch release info from GitHub
303
+ const releaseInfo = await fetchLatestReleaseFromGitHub(pkgName);
304
+
305
+ // Install using detected package manager
306
+ try {
307
+ installGlobal(pkgName, latest, packageManager);
308
+ console.log(`Successfully updated to ${latest}`);
309
+
310
+ // Try to show changelog for the newly installed version once
311
+ try {
312
+ await showChangelogOnce(pkgName, latest, releaseInfo);
313
+ } catch {
314
+ // non-fatal
315
+ }
316
+ } catch (installErr) {
317
+ // If install fails, provide a helpful message
318
+ const cmd = `${packageManager === "bun" ? "bun install -g" : packageManager === "yarn" ? "yarn global add" : "npm install -g"} ${pkgName}@${latest}`;
319
+ console.error(`Update failed: ${installErr.message || installErr}`);
320
+ console.error(
321
+ `Try running the following command manually:\n ${cmd}\n` +
322
+ `If you see permissions errors, consider running with elevated privileges or using a Node version manager.`
323
+ );
324
+ process.exitCode = 1;
325
+ }
326
+ } catch (err) {
327
+ // ensure spinner is stopped on error
328
+ if (spinner && typeof spinner.stop === "function") spinner.stop();
329
+ console.error(err.message || err);
330
+ process.exitCode = 1;
331
+ }
332
+ }