@pagepocket/cli 0.13.0 → 0.14.5
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/dist/commands/archive.js +17 -20
- package/dist/index.js +1 -0
- package/dist/utils/strategy-vars.js +117 -0
- package/package.json +21 -18
package/dist/commands/archive.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ConfigService } from "../services/config-service.js";
|
|
|
6
6
|
import { loadConfiguredPlugins } from "../services/load-configured-plugins.js";
|
|
7
7
|
import { StrategyService } from "../services/strategy/strategy-service.js";
|
|
8
8
|
import { loadStrategyUnits, resolveStrategy } from "../utils/archive-strategy.js";
|
|
9
|
+
import { parseEnvVars, resolveStrategyVars } from "../utils/strategy-vars.js";
|
|
9
10
|
export default class ArchiveCommand extends Command {
|
|
10
11
|
static description = "Archive a web page as an offline snapshot.";
|
|
11
12
|
static args = {
|
|
@@ -18,35 +19,29 @@ export default class ArchiveCommand extends Command {
|
|
|
18
19
|
help: Flags.help({
|
|
19
20
|
char: "h"
|
|
20
21
|
}),
|
|
21
|
-
timeout: Flags.integer({
|
|
22
|
-
char: "t",
|
|
23
|
-
description: "Network idle duration in milliseconds before capture stops",
|
|
24
|
-
required: false
|
|
25
|
-
}),
|
|
26
|
-
maxDuration: Flags.integer({
|
|
27
|
-
description: "Hard max capture duration in milliseconds",
|
|
28
|
-
required: false
|
|
29
|
-
}),
|
|
30
22
|
strategy: Flags.string({
|
|
31
23
|
description: "Run an installed or built-in strategy (unit pipeline) by name"
|
|
24
|
+
}),
|
|
25
|
+
env: Flags.string({
|
|
26
|
+
char: "e",
|
|
27
|
+
description: "Strategy variable in key:value format (e.g. -e \"timeout:3000\")",
|
|
28
|
+
multiple: true
|
|
32
29
|
})
|
|
33
30
|
};
|
|
34
31
|
async run() {
|
|
35
32
|
const { args, flags } = await this.parse(ArchiveCommand);
|
|
36
33
|
const targetUrl = args.url;
|
|
37
|
-
const timeoutMs = typeof flags.timeout === "number" ? flags.timeout : undefined;
|
|
38
|
-
const maxDurationMs = typeof flags.maxDuration === "number" ? flags.maxDuration : undefined;
|
|
39
34
|
const strategyName = typeof flags.strategy === "string" ? flags.strategy.trim() : undefined;
|
|
40
35
|
const configService = new ConfigService();
|
|
41
36
|
const name = strategyName && strategyName.length > 0 ? strategyName : "default";
|
|
42
37
|
const strategyService = new StrategyService(configService);
|
|
43
38
|
strategyService.ensureConfigFileExists();
|
|
44
39
|
const strategy = resolveStrategy({ strategyName: name, strategyService });
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
40
|
+
const envVars = parseEnvVars(flags.env ?? []);
|
|
41
|
+
const resolvedStrategyFile = resolveStrategyVars(strategy.strategyFile, envVars);
|
|
42
|
+
const resolvedStrategy = { ...strategy, strategyFile: resolvedStrategyFile };
|
|
43
|
+
const units = await loadStrategyUnits({ strategy: resolvedStrategy, configService });
|
|
44
|
+
const captureOptions = resolvedStrategy.strategyFile.pipeline.captureOptions;
|
|
50
45
|
const pagepocket = PagePocket.fromURL(targetUrl);
|
|
51
46
|
let spinner = ora();
|
|
52
47
|
let activeUnitId = "";
|
|
@@ -69,13 +64,15 @@ export default class ArchiveCommand extends Command {
|
|
|
69
64
|
let result;
|
|
70
65
|
try {
|
|
71
66
|
result = await pagepocket.capture({
|
|
72
|
-
...(typeof
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
...(typeof captureOptions?.timeoutMs === "number"
|
|
68
|
+
? { timeoutMs: captureOptions.timeoutMs }
|
|
69
|
+
: {}),
|
|
70
|
+
...(typeof captureOptions?.maxDurationMs === "number"
|
|
71
|
+
? { maxDurationMs: captureOptions.maxDurationMs }
|
|
75
72
|
: {}),
|
|
76
73
|
blacklist: [...ga, ...ns],
|
|
77
74
|
units,
|
|
78
|
-
plugins: await loadConfiguredPlugins()
|
|
75
|
+
plugins: await loadConfiguredPlugins()
|
|
79
76
|
});
|
|
80
77
|
}
|
|
81
78
|
catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const VAR_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]*))?\}\}/g;
|
|
2
|
+
/**
|
|
3
|
+
* Parse `-e` flag values into a typed variable map.
|
|
4
|
+
*
|
|
5
|
+
* Each raw arg must be in `key:value` format. The key is the variable name,
|
|
6
|
+
* everything after the first colon is the raw value string.
|
|
7
|
+
*
|
|
8
|
+
* Value type inference:
|
|
9
|
+
* - Parseable as finite number → number
|
|
10
|
+
* - `"true"` / `"false"` → boolean
|
|
11
|
+
* - Otherwise → string
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* parseEnvVars(["timeout:3000", "overwrite:true", "outDir:./snapshots"])
|
|
15
|
+
* // => { timeout: 3000, overwrite: true, outDir: "./snapshots" }
|
|
16
|
+
*/
|
|
17
|
+
export const parseEnvVars = (rawArgs) => {
|
|
18
|
+
const vars = {};
|
|
19
|
+
for (const raw of rawArgs) {
|
|
20
|
+
const colonIndex = raw.indexOf(":");
|
|
21
|
+
if (colonIndex === -1) {
|
|
22
|
+
throw new Error(`Invalid -e value: "${raw}". Expected format: key:value (e.g. -e "timeout:3000")`);
|
|
23
|
+
}
|
|
24
|
+
const key = raw.slice(0, colonIndex).trim();
|
|
25
|
+
const valueRaw = raw.slice(colonIndex + 1);
|
|
26
|
+
if (key.length === 0) {
|
|
27
|
+
throw new Error(`Invalid -e value: "${raw}". Key must not be empty.`);
|
|
28
|
+
}
|
|
29
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
30
|
+
throw new Error(`Invalid variable name: "${key}". Must be a valid identifier (letters, digits, underscores).`);
|
|
31
|
+
}
|
|
32
|
+
if (key in vars) {
|
|
33
|
+
throw new Error(`Duplicate variable: "${key}". Each variable can only be specified once.`);
|
|
34
|
+
}
|
|
35
|
+
vars[key] = inferValue(valueRaw);
|
|
36
|
+
}
|
|
37
|
+
return vars;
|
|
38
|
+
};
|
|
39
|
+
const inferValue = (raw) => {
|
|
40
|
+
const asNumber = Number(raw);
|
|
41
|
+
if (raw.length > 0 && Number.isFinite(asNumber)) {
|
|
42
|
+
return asNumber;
|
|
43
|
+
}
|
|
44
|
+
if (raw === "true") {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (raw === "false") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return raw;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Recursively resolve `{{varName}}` and `{{varName:default}}` placeholders in a JSON structure.
|
|
54
|
+
*
|
|
55
|
+
* - Only string values are scanned for placeholders.
|
|
56
|
+
* - `{{var:default}}` provides an inline default; the default is type-inferred the same way as `-e` values.
|
|
57
|
+
* - If the entire string is a single `{{var}}` or `{{var:default}}`, the value is replaced with the
|
|
58
|
+
* variable's typed value (number, boolean, or string).
|
|
59
|
+
* - If the string contains mixed text and placeholders (e.g. `"path/{{name}}"`),
|
|
60
|
+
* all placeholders are interpolated and the result remains a string.
|
|
61
|
+
* - Throws if any referenced variable has no `-e` value AND no inline default.
|
|
62
|
+
*
|
|
63
|
+
* Usage:
|
|
64
|
+
* resolveStrategyVars({ timeoutMs: "{{timeout:5000}}" }, {})
|
|
65
|
+
* // => { timeoutMs: 5000 }
|
|
66
|
+
*/
|
|
67
|
+
export const resolveStrategyVars = (json, vars) => {
|
|
68
|
+
const defaults = new Map();
|
|
69
|
+
const referenced = new Set();
|
|
70
|
+
const resolved = resolveNode(json, vars, defaults, referenced);
|
|
71
|
+
const missing = [...referenced].filter((name) => !(name in vars) && !defaults.has(name));
|
|
72
|
+
if (missing.length > 0) {
|
|
73
|
+
throw new Error(`Missing strategy variable(s): ${missing.join(", ")}. ` +
|
|
74
|
+
`Pass them with ${missing.map((name) => `-e "${name}:<value>"`).join(" ")}`);
|
|
75
|
+
}
|
|
76
|
+
return resolved;
|
|
77
|
+
};
|
|
78
|
+
const resolveNode = (node, vars, defaults, referenced) => {
|
|
79
|
+
if (typeof node === "string") {
|
|
80
|
+
return resolveString(node, vars, defaults, referenced);
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(node)) {
|
|
83
|
+
return node.map((item) => resolveNode(item, vars, defaults, referenced));
|
|
84
|
+
}
|
|
85
|
+
if (isRecord(node)) {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const [key, value] of Object.entries(node)) {
|
|
88
|
+
result[key] = resolveNode(value, vars, defaults, referenced);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
return node;
|
|
93
|
+
};
|
|
94
|
+
const resolveString = (value, vars, defaults, referenced) => {
|
|
95
|
+
const matches = [...value.matchAll(VAR_PATTERN)];
|
|
96
|
+
if (matches.length === 0) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
for (const match of matches) {
|
|
100
|
+
const varName = match[1];
|
|
101
|
+
const rawDefault = match[2];
|
|
102
|
+
referenced.add(varName);
|
|
103
|
+
if (rawDefault !== undefined && !defaults.has(varName)) {
|
|
104
|
+
defaults.set(varName, inferValue(rawDefault));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const isSingleWholeVar = matches.length === 1 && matches[0][0] === value;
|
|
108
|
+
if (isSingleWholeVar) {
|
|
109
|
+
const varName = matches[0][1];
|
|
110
|
+
return vars[varName] ?? defaults.get(varName);
|
|
111
|
+
}
|
|
112
|
+
return value.replace(VAR_PATTERN, (_match, varName) => {
|
|
113
|
+
const resolved = vars[varName] ?? defaults.get(varName);
|
|
114
|
+
return String(resolved ?? "");
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagepocket/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.5",
|
|
4
4
|
"description": "CLI for capturing offline snapshots of web pages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,33 +14,35 @@
|
|
|
14
14
|
"author": "",
|
|
15
15
|
"license": "ISC",
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@hono/node-server": "^1.14.1",
|
|
17
18
|
"@oclif/core": "^4.0.9",
|
|
18
19
|
"chalk": "^4.1.2",
|
|
19
20
|
"env-paths": "^2.2.1",
|
|
20
|
-
"@hono/node-server": "^1.14.1",
|
|
21
21
|
"hono": "^4.7.6",
|
|
22
22
|
"npm-package-arg": "^13.0.2",
|
|
23
23
|
"ora": "^9.0.0",
|
|
24
|
-
"@pagepocket/
|
|
25
|
-
"@pagepocket/
|
|
26
|
-
"@pagepocket/capture-http-lighterceptor-unit": "0.
|
|
27
|
-
"@pagepocket/
|
|
28
|
-
"@pagepocket/capture-http-puppeteer-unit": "0.
|
|
29
|
-
"@pagepocket/
|
|
30
|
-
"@pagepocket/
|
|
31
|
-
"@pagepocket/
|
|
32
|
-
"@pagepocket/
|
|
33
|
-
"@pagepocket/
|
|
34
|
-
"@pagepocket/
|
|
35
|
-
"@pagepocket/single-file-unit": "0.
|
|
36
|
-
"@pagepocket/write-down-unit": "0.
|
|
24
|
+
"@pagepocket/build-snapshot-unit": "0.14.5",
|
|
25
|
+
"@pagepocket/builtin-strategy": "0.14.5",
|
|
26
|
+
"@pagepocket/capture-http-lighterceptor-unit": "0.14.5",
|
|
27
|
+
"@pagepocket/capture-http-cdp-unit": "0.14.5",
|
|
28
|
+
"@pagepocket/capture-http-puppeteer-unit": "0.14.5",
|
|
29
|
+
"@pagepocket/lib": "0.14.5",
|
|
30
|
+
"@pagepocket/contracts": "0.14.5",
|
|
31
|
+
"@pagepocket/metadata-unit": "0.14.5",
|
|
32
|
+
"@pagepocket/plugin-yt-dlp": "0.14.5",
|
|
33
|
+
"@pagepocket/shared": "0.14.5",
|
|
34
|
+
"@pagepocket/main-content-unit": "0.14.5",
|
|
35
|
+
"@pagepocket/single-file-unit": "0.14.5",
|
|
36
|
+
"@pagepocket/write-down-unit": "0.14.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^20.11.30",
|
|
40
|
+
"cpx2": "^8.0.0",
|
|
41
|
+
"esbuild": "^0.25.12",
|
|
40
42
|
"rimraf": "^6.0.1",
|
|
41
43
|
"tsx": "^4.19.3",
|
|
42
44
|
"typescript": "^5.4.5",
|
|
43
|
-
"@pagepocket/content-reader": "0.
|
|
45
|
+
"@pagepocket/content-reader": "0.14.5"
|
|
44
46
|
},
|
|
45
47
|
"oclif": {
|
|
46
48
|
"bin": "pp",
|
|
@@ -52,8 +54,9 @@
|
|
|
52
54
|
},
|
|
53
55
|
"scripts": {
|
|
54
56
|
"build": "rimraf dist && tsc && pnpm build:vendor",
|
|
55
|
-
"build:vendor": "
|
|
57
|
+
"build:vendor": "cpx \"node_modules/@pagepocket/content-reader/dist/browser/*\" dist/vendor",
|
|
56
58
|
"start": "node dist/index.js",
|
|
57
|
-
"test": "pnpm build && tsx --test specs/*.spec.ts"
|
|
59
|
+
"test": "pnpm build && tsx --test specs/*.spec.ts",
|
|
60
|
+
"build:sea": "node --import tsx scripts/build-sea.ts"
|
|
58
61
|
}
|
|
59
62
|
}
|