hdoc-tools 0.61.0 → 0.62.1

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 (45) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +89 -75
  3. package/editor/dist/assets/index-Blewr90z.css +1 -1
  4. package/editor/dist/assets/index-BtxvGZHW.js +111 -111
  5. package/editor/dist/assets/spell.worker-sryEKmOj.js +13 -13
  6. package/editor/dist/index.html +14 -14
  7. package/hdoc-build-db.js +275 -275
  8. package/hdoc-build-embeddings.js +202 -202
  9. package/hdoc-build-pdf.js +232 -232
  10. package/hdoc-bump.js +125 -125
  11. package/hdoc-create.js +110 -110
  12. package/hdoc-db.js +114 -114
  13. package/hdoc-help.js +60 -60
  14. package/hdoc-install-browser.js +145 -145
  15. package/hdoc-mermaid.js +204 -204
  16. package/hdoc-module.js +1102 -1102
  17. package/hdoc-validate-config.js +355 -355
  18. package/hdoc-validate-interbook.js +321 -321
  19. package/hdoc-validate.js +1231 -1231
  20. package/hdoc-ver.js +45 -45
  21. package/package.json +12 -1
  22. package/templates/doc-header-non-git.html +19 -19
  23. package/templates/doc-header.html +26 -26
  24. package/templates/init/.github/workflows/hdocbuild_onpull.yml +16 -16
  25. package/templates/init/.github/workflows/hdocbuild_onpush.yml +15 -15
  26. package/templates/init/LICENSE +21 -21
  27. package/templates/init/README.md +9 -9
  28. package/templates/init/_hdocbook/index.md +4 -4
  29. package/templates/init/gitignore +8 -8
  30. package/templates/init/resources/README.md +2 -2
  31. package/templates/pdf/css/custom-block.css +90 -90
  32. package/templates/pdf/css/fonts.css +221 -221
  33. package/templates/pdf/css/hdocs-pdf.css +495 -495
  34. package/templates/pdf/css/vars.css +404 -404
  35. package/templates/pdf/template-footer.html +19 -19
  36. package/templates/pdf/template-header.html +37 -37
  37. package/templates/pdf/template.html +20 -20
  38. package/templates/pdf-header-non-git.html +12 -12
  39. package/templates/pdf-header.html +16 -16
  40. package/ui/content/invalid-hdocbook-json.html +6 -6
  41. package/ui/content/invalid-hdocbook-json.md +7 -7
  42. package/ui/css/theme-default/styles/components/content.css +124 -124
  43. package/ui/css/theme-default/styles/components/sidebar.css +182 -182
  44. package/ui/css/theme-default/styles/htldoc.layouts.css +310 -310
  45. package/ui/index.html +419 -419
package/hdoc-db.js CHANGED
@@ -1,114 +1,114 @@
1
- (() => {
2
- const cheerio = require("cheerio");
3
- const path = require("node:path");
4
- const hdoc = require(path.join(__dirname, "hdoc-module.js"));
5
-
6
- exports.create_table = (db, table_name, columns, virtual, fts5) => {
7
- const create_sql = ["CREATE"];
8
- if (virtual) create_sql.push("VIRTUAL");
9
- create_sql.push("TABLE");
10
- create_sql.push(table_name);
11
- if (fts5) create_sql.push("USING fts5(");
12
- else create_sql.push("(");
13
- for (let i = 0; i < columns.length; i++) {
14
- if (i !== 0) create_sql.push(`,${columns[i]}`);
15
- else create_sql.push(columns[i]);
16
- }
17
- create_sql.push(");");
18
- try {
19
- db.exec(create_sql.join("\n"));
20
- return null;
21
- } catch (e) {
22
- return e;
23
- }
24
- };
25
-
26
- exports.insert_record = (db, table, columns, values) => {
27
- const response = {
28
- success: false,
29
- row_id: 0,
30
- error: null,
31
- };
32
- const queryProps = [];
33
- queryProps.push(`INSERT INTO ${table}`);
34
- let cols = "(";
35
- let vals = "VALUES (";
36
- for (let i = 0; i < columns.length; i++) {
37
- if (i === 0) {
38
- cols += `${columns[i].replace("UNINDEXED", "").replace("INTEGER", "").trim()}`;
39
- vals += "?";
40
- } else {
41
- cols += `, ${columns[i].replace("UNINDEXED", "").replace("INTEGER", "").trim()}`;
42
- vals += ", ?";
43
- }
44
- }
45
- cols += ")";
46
- vals += ")";
47
- queryProps.push(cols);
48
- queryProps.push(vals);
49
-
50
- try {
51
- const stmt = db.prepare(queryProps.join(" "));
52
- const info = stmt.run(values);
53
- response.row_id = info.lastInsertRowid;
54
- response.success = true;
55
- } catch (e) {
56
- response.error = e;
57
- }
58
- return response;
59
- };
60
-
61
- exports.transform_html_for_index = (html_txt) => {
62
- const response = {
63
- fm_props: {},
64
- sections: [],
65
- };
66
-
67
- // Single parse covers frontmatter extraction, full-text, and preview —
68
- // previously three separate cheerio.load() calls.
69
- const $ = cheerio.load(html_txt, { decodeEntities: false });
70
-
71
- // Extract frontmatter properties from the leading HTML comment
72
- if ($._root?.children && Array.isArray($._root.children)) {
73
- for (const child of $._root.children) {
74
- if (child.type === "comment" && child.data?.startsWith("[[FRONTMATTER")) {
75
- for (const line of child.data.split(/\r?\n/)) {
76
- if (line.includes(":")) {
77
- const parts = line.split(/:(.*)/s);
78
- if (parts.length > 1) {
79
- const key = parts[0].trim().toLowerCase();
80
- let val = parts[1].trim();
81
- if (/^".*"$/.test(val)) val = val.slice(1, -1);
82
- if (key === "title") {
83
- val = val.replace(
84
- /&amp;|&lt;|&gt;|&quot;|&#39;|&apos;|&#(\d+);|&#x([0-9a-fA-F]+);/g,
85
- (m, dec, hex) => dec ? String.fromCharCode(+dec) : hex ? String.fromCharCode(parseInt(hex, 16)) : ({ "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'", "&apos;": "'" })[m],
86
- );
87
- }
88
- response.fm_props[key] = val;
89
- }
90
- }
91
- }
92
- break;
93
- }
94
- }
95
- }
96
-
97
- // Page chrome - the document-header block (breadcrumb bar, edit link,
98
- // title, "Article / date / N minutes to read / N contributors") is
99
- // rendered into every page and would otherwise pollute search index
100
- // term frequencies and embedding chunks. The title is indexed
101
- // separately from frontmatter, so nothing of value is lost.
102
- $(".document-header").remove();
103
-
104
- // Full-document plain text for search indexing
105
- const text = $("body").text();
106
-
107
- // Preview: first paragraph texts joined, then truncated
108
- let preview = $("p").map((_i, el) => $(el).text()).get().join("\n");
109
- preview = hdoc.truncate_string(preview, 200, true).replace(/(?:\r\n|\r|\n)/g, " ");
110
-
111
- response.sections.push({ text, preview });
112
- return response;
113
- };
114
- })();
1
+ (() => {
2
+ const cheerio = require("cheerio");
3
+ const path = require("node:path");
4
+ const hdoc = require(path.join(__dirname, "hdoc-module.js"));
5
+
6
+ exports.create_table = (db, table_name, columns, virtual, fts5) => {
7
+ const create_sql = ["CREATE"];
8
+ if (virtual) create_sql.push("VIRTUAL");
9
+ create_sql.push("TABLE");
10
+ create_sql.push(table_name);
11
+ if (fts5) create_sql.push("USING fts5(");
12
+ else create_sql.push("(");
13
+ for (let i = 0; i < columns.length; i++) {
14
+ if (i !== 0) create_sql.push(`,${columns[i]}`);
15
+ else create_sql.push(columns[i]);
16
+ }
17
+ create_sql.push(");");
18
+ try {
19
+ db.exec(create_sql.join("\n"));
20
+ return null;
21
+ } catch (e) {
22
+ return e;
23
+ }
24
+ };
25
+
26
+ exports.insert_record = (db, table, columns, values) => {
27
+ const response = {
28
+ success: false,
29
+ row_id: 0,
30
+ error: null,
31
+ };
32
+ const queryProps = [];
33
+ queryProps.push(`INSERT INTO ${table}`);
34
+ let cols = "(";
35
+ let vals = "VALUES (";
36
+ for (let i = 0; i < columns.length; i++) {
37
+ if (i === 0) {
38
+ cols += `${columns[i].replace("UNINDEXED", "").replace("INTEGER", "").trim()}`;
39
+ vals += "?";
40
+ } else {
41
+ cols += `, ${columns[i].replace("UNINDEXED", "").replace("INTEGER", "").trim()}`;
42
+ vals += ", ?";
43
+ }
44
+ }
45
+ cols += ")";
46
+ vals += ")";
47
+ queryProps.push(cols);
48
+ queryProps.push(vals);
49
+
50
+ try {
51
+ const stmt = db.prepare(queryProps.join(" "));
52
+ const info = stmt.run(values);
53
+ response.row_id = info.lastInsertRowid;
54
+ response.success = true;
55
+ } catch (e) {
56
+ response.error = e;
57
+ }
58
+ return response;
59
+ };
60
+
61
+ exports.transform_html_for_index = (html_txt) => {
62
+ const response = {
63
+ fm_props: {},
64
+ sections: [],
65
+ };
66
+
67
+ // Single parse covers frontmatter extraction, full-text, and preview —
68
+ // previously three separate cheerio.load() calls.
69
+ const $ = cheerio.load(html_txt, { decodeEntities: false });
70
+
71
+ // Extract frontmatter properties from the leading HTML comment
72
+ if ($._root?.children && Array.isArray($._root.children)) {
73
+ for (const child of $._root.children) {
74
+ if (child.type === "comment" && child.data?.startsWith("[[FRONTMATTER")) {
75
+ for (const line of child.data.split(/\r?\n/)) {
76
+ if (line.includes(":")) {
77
+ const parts = line.split(/:(.*)/s);
78
+ if (parts.length > 1) {
79
+ const key = parts[0].trim().toLowerCase();
80
+ let val = parts[1].trim();
81
+ if (/^".*"$/.test(val)) val = val.slice(1, -1);
82
+ if (key === "title") {
83
+ val = val.replace(
84
+ /&amp;|&lt;|&gt;|&quot;|&#39;|&apos;|&#(\d+);|&#x([0-9a-fA-F]+);/g,
85
+ (m, dec, hex) => dec ? String.fromCharCode(+dec) : hex ? String.fromCharCode(parseInt(hex, 16)) : ({ "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'", "&apos;": "'" })[m],
86
+ );
87
+ }
88
+ response.fm_props[key] = val;
89
+ }
90
+ }
91
+ }
92
+ break;
93
+ }
94
+ }
95
+ }
96
+
97
+ // Page chrome - the document-header block (breadcrumb bar, edit link,
98
+ // title, "Article / date / N minutes to read / N contributors") is
99
+ // rendered into every page and would otherwise pollute search index
100
+ // term frequencies and embedding chunks. The title is indexed
101
+ // separately from frontmatter, so nothing of value is lost.
102
+ $(".document-header").remove();
103
+
104
+ // Full-document plain text for search indexing
105
+ const text = $("body").text();
106
+
107
+ // Preview: first paragraph texts joined, then truncated
108
+ let preview = $("p").map((_i, el) => $(el).text()).get().join("\n");
109
+ preview = hdoc.truncate_string(preview, 200, true).replace(/(?:\r\n|\r|\n)/g, " ");
110
+
111
+ response.sections.push({ text, preview });
112
+ return response;
113
+ };
114
+ })();
package/hdoc-help.js CHANGED
@@ -1,60 +1,60 @@
1
- (() => {
2
- exports.run = () => {
3
- // STEVE: The purpose of this function is to output information about hdoc arguments
4
- const helpText = `
5
- Command Line Usage
6
-
7
- hdoc <command> [switches]
8
-
9
- Commands
10
-
11
- - build
12
- Performs a local build of the book, and outputs as a ZIP file.
13
- - Use the '--set-version 1.2.3' argument to set the version number of the built book.
14
- - Use the '--no-color' argument to remove any color control characters from the output.
15
- - Use the '--no-links' argument to skip link output to CLI during validation.
16
- - Use the '--no-embeddings' argument to skip semantic search embedding generation.
17
-
18
- - createDocs
19
- Creates folder structure and markdown documents as defined in the HDocBook navigation item links
20
-
21
- - edit
22
- Starts the local structure/content editor on port 3001 (bound to 127.0.0.1), serving the editor UI alongside a live preview of the content. Supports a -port N to use a different port.
23
- NOTE: This feature is experimental and not really for general use yet. GS
24
-
25
- - help
26
- Outputs available arguments and switches
27
-
28
- - init
29
- Initializes a new HDocBook project from a template, using runtime input variables
30
-
31
- - serve
32
- Starts a local web server on port 3000, serving the content. Supports a -port N to use a different port
33
-
34
- - stats
35
- Returns statistics regarding the book you are working on. Supports a -v switch for verbose output.
36
- The book statistics do not include counts for any externally hosted content injected into the book content using the [[INCLUDE]] tags.
37
-
38
- - validate
39
- Validates the book content.
40
- - Use the '--set-version 1.2.3' argument to set the version number of the built book.
41
- - Use the '--no-color' argument to remove any color control characters from the output.
42
- - Use the '--no-links' argument to skip link output to CLI during validation.
43
- - Use the '--quiet' argument to suppress most console output, and only output validation errors if they are found.
44
-
45
- - bump
46
- Updates the semantic version number of the current book. If no options are specified, then the default of patch is applied:
47
- - major - updates the major version of the book. i.e. - 1.4.5 would become 2.0.0
48
- - minor - updates the minor version of the book. i.e. - 1.4.5 would become 1.5.0
49
- - patch (default) - updates the patch version of the book. i.e. - 1.4.5 would become 1.4.6
50
-
51
- - ver
52
- Returns the version of the current book
53
-
54
- Example
55
-
56
- hdoc stats -v
57
- `;
58
- console.log(helpText);
59
- };
60
- })();
1
+ (() => {
2
+ exports.run = () => {
3
+ // STEVE: The purpose of this function is to output information about hdoc arguments
4
+ const helpText = `
5
+ Command Line Usage
6
+
7
+ hdoc <command> [switches]
8
+
9
+ Commands
10
+
11
+ - build
12
+ Performs a local build of the book, and outputs as a ZIP file.
13
+ - Use the '--set-version 1.2.3' argument to set the version number of the built book.
14
+ - Use the '--no-color' argument to remove any color control characters from the output.
15
+ - Use the '--no-links' argument to skip link output to CLI during validation.
16
+ - Use the '--no-embeddings' argument to skip semantic search embedding generation.
17
+
18
+ - createDocs
19
+ Creates folder structure and markdown documents as defined in the HDocBook navigation item links
20
+
21
+ - edit
22
+ Starts the local structure/content editor on port 3001 (bound to 127.0.0.1), serving the editor UI alongside a live preview of the content. Supports a -port N to use a different port.
23
+ NOTE: This feature is experimental and not really for general use yet. GS
24
+
25
+ - help
26
+ Outputs available arguments and switches
27
+
28
+ - init
29
+ Initializes a new HDocBook project from a template, using runtime input variables
30
+
31
+ - serve
32
+ Starts a local web server on port 3000, serving the content. Supports a -port N to use a different port
33
+
34
+ - stats
35
+ Returns statistics regarding the book you are working on. Supports a -v switch for verbose output.
36
+ The book statistics do not include counts for any externally hosted content injected into the book content using the [[INCLUDE]] tags.
37
+
38
+ - validate
39
+ Validates the book content.
40
+ - Use the '--set-version 1.2.3' argument to set the version number of the built book.
41
+ - Use the '--no-color' argument to remove any color control characters from the output.
42
+ - Use the '--no-links' argument to skip link output to CLI during validation.
43
+ - Use the '--quiet' argument to suppress most console output, and only output validation errors if they are found.
44
+
45
+ - bump
46
+ Updates the semantic version number of the current book. If no options are specified, then the default of patch is applied:
47
+ - major - updates the major version of the book. i.e. - 1.4.5 would become 2.0.0
48
+ - minor - updates the minor version of the book. i.e. - 1.4.5 would become 1.5.0
49
+ - patch (default) - updates the patch version of the book. i.e. - 1.4.5 would become 1.4.6
50
+
51
+ - ver
52
+ Returns the version of the current book
53
+
54
+ Example
55
+
56
+ hdoc stats -v
57
+ `;
58
+ console.log(helpText);
59
+ };
60
+ })();
@@ -1,145 +1,145 @@
1
- // Resilient browser provisioning for hdoc-tools.
2
- //
3
- // Replaces the old `puppeteer browsers install ...` one-liner postinstall.
4
- // Puppeteer's own bundled download (install.mjs) is disabled via
5
- // .puppeteerrc.cjs (skipDownload), so this script is the single, controlled
6
- // place Chrome + chrome-headless-shell are fetched.
7
- //
8
- // Why: on some Windows Server 2019 build agents the Chrome archive extracts
9
- // only partially (Defender quarantining binaries mid-extract, or a truncated
10
- // download behind a proxy). The first failure leaves the version folder on
11
- // disk, after which Puppeteer refuses to re-extract and every later install
12
- // reports "folder exists but executable missing" — a permanent dead end.
13
- //
14
- // This script makes provisioning idempotent and self-healing:
15
- // * skip when a valid executable already exists,
16
- // * delete any stale/partial version folder before (re)installing,
17
- // * retry the download a few times,
18
- // * verify the executable exists afterwards and fail loudly with concrete
19
- // remediation if it still does not.
20
-
21
- (async () => {
22
- const fs = require("node:fs");
23
- const path = require("node:path");
24
- const {
25
- install,
26
- computeExecutablePath,
27
- detectBrowserPlatform,
28
- Browser,
29
- } = require("@puppeteer/browsers");
30
-
31
- // Build id + cache dir come from the shared .puppeteerrc.cjs so install and
32
- // runtime launch can never drift apart.
33
- const puppeteerConfig = require(path.join(__dirname, ".puppeteerrc.cjs"));
34
- const CHROME_BUILD = puppeteerConfig.chromeBuild;
35
- const MAX_ATTEMPTS = 3;
36
-
37
- const RED = "\x1b[31m";
38
- const YELLOW = "\x1b[33m";
39
- const GREEN = "\x1b[32m";
40
- const RESET = "\x1b[0m";
41
-
42
- const log = (msg) => console.log(`[hdoc-tools] ${msg}`);
43
-
44
- // Resolve the cache directory the same way Puppeteer does at runtime:
45
- // PUPPETEER_CACHE_DIR wins, otherwise the fixed cacheDir declared in
46
- // .puppeteerrc.cjs. Reading the shared config (rather than recomputing from
47
- // os.homedir()) guarantees we install to exactly the path the browser is
48
- // later launched from — critical for `sudo npm i -g`, where postinstall runs
49
- // as root but `hdoc` runs as a normal user with a different home directory.
50
- const cacheDir = process.env.PUPPETEER_CACHE_DIR || puppeteerConfig.cacheDir;
51
-
52
- let platform;
53
- try {
54
- platform = detectBrowserPlatform();
55
- } catch (err) {
56
- console.error(
57
- `${RED}Unable to detect browser platform: ${err.message}${RESET}`,
58
- );
59
- process.exit(1);
60
- }
61
-
62
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
-
64
- const rmrf = (target) => {
65
- try {
66
- fs.rmSync(target, { recursive: true, force: true });
67
- } catch {
68
- /* best effort */
69
- }
70
- };
71
-
72
- // A usable install is one where the executable exists and is non-empty.
73
- const isUsable = (exePath) => {
74
- try {
75
- return fs.statSync(exePath).size > 0;
76
- } catch {
77
- return false;
78
- }
79
- };
80
-
81
- const provision = async (browser, label) => {
82
- const exePath = computeExecutablePath({
83
- browser,
84
- buildId: CHROME_BUILD,
85
- cacheDir,
86
- platform,
87
- });
88
- // chrome.exe -> chrome-win64 -> win64-<build>; nuke the whole build folder
89
- // so a partial extraction can never block a clean re-extract.
90
- const versionFolder = path.dirname(path.dirname(exePath));
91
-
92
- if (isUsable(exePath)) {
93
- log(`${label} ${CHROME_BUILD} already present, skipping download.`);
94
- return true;
95
- }
96
-
97
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
98
- rmrf(versionFolder); // clear any stale/partial extract first
99
- try {
100
- log(
101
- `Installing ${label} ${CHROME_BUILD} (attempt ${attempt}/${MAX_ATTEMPTS})...`,
102
- );
103
- await install({
104
- browser,
105
- buildId: CHROME_BUILD,
106
- cacheDir,
107
- platform,
108
- unpack: true,
109
- });
110
- } catch (err) {
111
- console.error(
112
- `${YELLOW} attempt ${attempt} failed: ${err.message}${RESET}`,
113
- );
114
- }
115
-
116
- if (isUsable(exePath)) {
117
- log(`${GREEN}${label} ${CHROME_BUILD} ready.${RESET}`);
118
- return true;
119
- }
120
-
121
- if (attempt < MAX_ATTEMPTS) await sleep(2000);
122
- }
123
-
124
- return false;
125
- };
126
-
127
- const chromeOk = await provision(Browser.CHROME, "Chrome");
128
- const shellOk = await provision(
129
- Browser.CHROMEHEADLESSSHELL,
130
- "chrome-headless-shell",
131
- );
132
-
133
- if (chromeOk && shellOk) process.exit(0);
134
-
135
- console.error(
136
- `\n${RED}Failed to provision a complete browser into:${RESET}\n ${cacheDir}\n\n` +
137
- "This is almost always one of:\n" +
138
- " 1. Antivirus (e.g. Windows Defender) quarantining Chrome files mid-extract.\n" +
139
- ` Fix: Add-MpPreference -ExclusionPath "${cacheDir}"\n` +
140
- " 2. A stale/partial cache folder. Fix: delete the chrome / chrome-headless-shell\n" +
141
- ` sub-folders under "${cacheDir}" and reinstall.\n` +
142
- " 3. A truncated download behind a proxy / TLS inspection. Check npm/HTTPS proxy config.\n",
143
- );
144
- process.exit(1);
145
- })();
1
+ // Resilient browser provisioning for hdoc-tools.
2
+ //
3
+ // Replaces the old `puppeteer browsers install ...` one-liner postinstall.
4
+ // Puppeteer's own bundled download (install.mjs) is disabled via
5
+ // .puppeteerrc.cjs (skipDownload), so this script is the single, controlled
6
+ // place Chrome + chrome-headless-shell are fetched.
7
+ //
8
+ // Why: on some Windows Server 2019 build agents the Chrome archive extracts
9
+ // only partially (Defender quarantining binaries mid-extract, or a truncated
10
+ // download behind a proxy). The first failure leaves the version folder on
11
+ // disk, after which Puppeteer refuses to re-extract and every later install
12
+ // reports "folder exists but executable missing" — a permanent dead end.
13
+ //
14
+ // This script makes provisioning idempotent and self-healing:
15
+ // * skip when a valid executable already exists,
16
+ // * delete any stale/partial version folder before (re)installing,
17
+ // * retry the download a few times,
18
+ // * verify the executable exists afterwards and fail loudly with concrete
19
+ // remediation if it still does not.
20
+
21
+ (async () => {
22
+ const fs = require("node:fs");
23
+ const path = require("node:path");
24
+ const {
25
+ install,
26
+ computeExecutablePath,
27
+ detectBrowserPlatform,
28
+ Browser,
29
+ } = require("@puppeteer/browsers");
30
+
31
+ // Build id + cache dir come from the shared .puppeteerrc.cjs so install and
32
+ // runtime launch can never drift apart.
33
+ const puppeteerConfig = require(path.join(__dirname, ".puppeteerrc.cjs"));
34
+ const CHROME_BUILD = puppeteerConfig.chromeBuild;
35
+ const MAX_ATTEMPTS = 3;
36
+
37
+ const RED = "\x1b[31m";
38
+ const YELLOW = "\x1b[33m";
39
+ const GREEN = "\x1b[32m";
40
+ const RESET = "\x1b[0m";
41
+
42
+ const log = (msg) => console.log(`[hdoc-tools] ${msg}`);
43
+
44
+ // Resolve the cache directory the same way Puppeteer does at runtime:
45
+ // PUPPETEER_CACHE_DIR wins, otherwise the fixed cacheDir declared in
46
+ // .puppeteerrc.cjs. Reading the shared config (rather than recomputing from
47
+ // os.homedir()) guarantees we install to exactly the path the browser is
48
+ // later launched from — critical for `sudo npm i -g`, where postinstall runs
49
+ // as root but `hdoc` runs as a normal user with a different home directory.
50
+ const cacheDir = process.env.PUPPETEER_CACHE_DIR || puppeteerConfig.cacheDir;
51
+
52
+ let platform;
53
+ try {
54
+ platform = detectBrowserPlatform();
55
+ } catch (err) {
56
+ console.error(
57
+ `${RED}Unable to detect browser platform: ${err.message}${RESET}`,
58
+ );
59
+ process.exit(1);
60
+ }
61
+
62
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
+
64
+ const rmrf = (target) => {
65
+ try {
66
+ fs.rmSync(target, { recursive: true, force: true });
67
+ } catch {
68
+ /* best effort */
69
+ }
70
+ };
71
+
72
+ // A usable install is one where the executable exists and is non-empty.
73
+ const isUsable = (exePath) => {
74
+ try {
75
+ return fs.statSync(exePath).size > 0;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
80
+
81
+ const provision = async (browser, label) => {
82
+ const exePath = computeExecutablePath({
83
+ browser,
84
+ buildId: CHROME_BUILD,
85
+ cacheDir,
86
+ platform,
87
+ });
88
+ // chrome.exe -> chrome-win64 -> win64-<build>; nuke the whole build folder
89
+ // so a partial extraction can never block a clean re-extract.
90
+ const versionFolder = path.dirname(path.dirname(exePath));
91
+
92
+ if (isUsable(exePath)) {
93
+ log(`${label} ${CHROME_BUILD} already present, skipping download.`);
94
+ return true;
95
+ }
96
+
97
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
98
+ rmrf(versionFolder); // clear any stale/partial extract first
99
+ try {
100
+ log(
101
+ `Installing ${label} ${CHROME_BUILD} (attempt ${attempt}/${MAX_ATTEMPTS})...`,
102
+ );
103
+ await install({
104
+ browser,
105
+ buildId: CHROME_BUILD,
106
+ cacheDir,
107
+ platform,
108
+ unpack: true,
109
+ });
110
+ } catch (err) {
111
+ console.error(
112
+ `${YELLOW} attempt ${attempt} failed: ${err.message}${RESET}`,
113
+ );
114
+ }
115
+
116
+ if (isUsable(exePath)) {
117
+ log(`${GREEN}${label} ${CHROME_BUILD} ready.${RESET}`);
118
+ return true;
119
+ }
120
+
121
+ if (attempt < MAX_ATTEMPTS) await sleep(2000);
122
+ }
123
+
124
+ return false;
125
+ };
126
+
127
+ const chromeOk = await provision(Browser.CHROME, "Chrome");
128
+ const shellOk = await provision(
129
+ Browser.CHROMEHEADLESSSHELL,
130
+ "chrome-headless-shell",
131
+ );
132
+
133
+ if (chromeOk && shellOk) process.exit(0);
134
+
135
+ console.error(
136
+ `\n${RED}Failed to provision a complete browser into:${RESET}\n ${cacheDir}\n\n` +
137
+ "This is almost always one of:\n" +
138
+ " 1. Antivirus (e.g. Windows Defender) quarantining Chrome files mid-extract.\n" +
139
+ ` Fix: Add-MpPreference -ExclusionPath "${cacheDir}"\n` +
140
+ " 2. A stale/partial cache folder. Fix: delete the chrome / chrome-headless-shell\n" +
141
+ ` sub-folders under "${cacheDir}" and reinstall.\n` +
142
+ " 3. A truncated download behind a proxy / TLS inspection. Check npm/HTTPS proxy config.\n",
143
+ );
144
+ process.exit(1);
145
+ })();