@pagepocket/cli 0.8.0 → 0.8.2

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/README.md CHANGED
@@ -61,6 +61,12 @@ Notes:
61
61
  - During `pp archive ...`, the CLI reads `config.json` and tries to load all configured plugins.
62
62
  If a plugin fails to load, it will be skipped and the archive will continue.
63
63
 
64
+ Plugin packages are installed to your user data directory (not the config directory):
65
+
66
+ - Linux: `~/.local/share/pagepocket/plugins/`
67
+ - macOS: `~/Library/Application Support/pagepocket/plugins/`
68
+ - Windows: `%APPDATA%\\pagepocket\\plugins\\`
69
+
64
70
  ## Output
65
71
 
66
72
  Snapshots are written to a folder named after the page title inside the output
@@ -12,9 +12,9 @@ const lib_1 = require("@pagepocket/lib");
12
12
  const single_file_unit_1 = require("@pagepocket/single-file-unit");
13
13
  const write_down_unit_1 = require("@pagepocket/write-down-unit");
14
14
  const chalk_1 = __importDefault(require("chalk"));
15
+ const load_configured_plugins_1 = require("../services/load-configured-plugins");
15
16
  const network_observer_unit_1 = require("../units/network-observer-unit");
16
17
  const with_spinner_1 = require("../utils/with-spinner");
17
- const load_configured_plugins_1 = require("../services/load-configured-plugins");
18
18
  const MAX_STATUS_URL_LENGTH = 80;
19
19
  const formatStatusUrl = (url) => {
20
20
  if (url.length <= MAX_STATUS_URL_LENGTH) {
@@ -8,6 +8,8 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const config_service_1 = require("../../services/config-service");
9
9
  const plugin_installer_1 = require("../../services/plugin-installer");
10
10
  const plugin_store_1 = require("../../services/plugin-store");
11
+ const parse_plugin_spec_1 = require("../../utils/parse-plugin-spec");
12
+ const validate_plugin_default_export_1 = require("../../utils/validate-plugin-default-export");
11
13
  const parseDynamicOptions = (argv) => {
12
14
  const tokens = argv.filter(Boolean);
13
15
  const pairs = tokens
@@ -56,29 +58,35 @@ class PluginAddCommand extends core_1.Command {
56
58
  this.log("Usage: pp plugin add <plugin-name> [--option-* <value> ...] [--option <value> ...]");
57
59
  return;
58
60
  }
61
+ const parsedSpec = (0, parse_plugin_spec_1.parsePluginSpec)(name);
59
62
  const configService = new config_service_1.ConfigService();
60
63
  const store = new plugin_store_1.PluginStore(configService);
61
64
  configService.ensureConfigFileExists();
62
65
  const config = configService.readConfigOrDefault();
63
- if (store.hasPlugin(config, name)) {
64
- this.log(chalk_1.default.gray(`Plugin already exists in config: ${name}`));
66
+ if (store.hasPlugin(config, parsedSpec.name)) {
67
+ this.log(chalk_1.default.gray(`Plugin already exists in config: ${parsedSpec.name}`));
65
68
  return;
66
69
  }
67
70
  const parsed = parseDynamicOptions(rest);
68
71
  const entry = parsed.kind === "none"
69
- ? name
72
+ ? parsedSpec.name
70
73
  : parsed.kind === "array"
71
- ? { name, options: parsed.value }
72
- : { name, options: parsed.value };
73
- (0, plugin_installer_1.installPluginPackage)(store, { packageName: name });
74
- const mod = await store.importPluginModule(name);
75
- if (typeof mod.default !== "function") {
76
- throw new Error(`Plugin ${name} must have a default export (class).`);
74
+ ? { name: parsedSpec.name, spec: parsedSpec.spec, options: parsed.value }
75
+ : { name: parsedSpec.name, spec: parsedSpec.spec, options: parsed.value };
76
+ (0, plugin_installer_1.installPluginPackage)(store, { packageName: parsedSpec.spec });
77
+ const mod = await store.importPluginModule(parsedSpec.name);
78
+ const validation = (0, validate_plugin_default_export_1.validatePluginDefaultExport)({
79
+ pluginName: parsedSpec.name,
80
+ moduleExports: mod,
81
+ options: parsed.kind === "none" ? undefined : parsed.value
82
+ });
83
+ if (!validation.ok) {
84
+ throw validation.error;
77
85
  }
78
86
  const nextConfig = store.addPluginToConfig(config, entry);
79
87
  configService.writeConfig(nextConfig);
80
- await store.runOptionalSetup(name);
81
- this.log(chalk_1.default.green(`Added plugin: ${name}`));
88
+ await store.runOptionalSetup(parsedSpec.name);
89
+ this.log(chalk_1.default.green(`Added plugin: ${parsedSpec.name}`));
82
90
  }
83
91
  }
84
92
  PluginAddCommand.description = "Install a plugin package, add it to config, and run its optional setup().";
@@ -21,15 +21,22 @@ class PluginLsCommand extends core_1.Command {
21
21
  entries
22
22
  .map((entry) => {
23
23
  const name = typeof entry === "string" ? entry : entry.name;
24
+ const configuredSpec = typeof entry === "string"
25
+ ? undefined
26
+ : typeof entry.spec === "string" && entry.spec.trim().length > 0
27
+ ? entry.spec
28
+ : undefined;
24
29
  const meta = store.readInstalledPackageMeta(name);
25
30
  if (!meta) {
26
- return `${chalk_1.default.yellow(name)} ${chalk_1.default.gray("(not installed)")}`;
31
+ const specText = configuredSpec ? chalk_1.default.gray(`(configured: ${configuredSpec})`) : "";
32
+ return `${chalk_1.default.yellow(name)} ${chalk_1.default.gray("(not installed)")} ${specText}`.trim();
27
33
  }
34
+ const specText = configuredSpec ? chalk_1.default.gray(`(configured: ${configuredSpec})`) : "";
28
35
  const desc = meta.description
29
36
  ? chalk_1.default.gray(meta.description)
30
37
  : chalk_1.default.gray("(no description)");
31
38
  const version = meta.version ? chalk_1.default.cyan(`v${meta.version}`) : chalk_1.default.gray("(no version)");
32
- return `${chalk_1.default.green(meta.name)} ${version} ${desc}`;
39
+ return `${chalk_1.default.green(meta.name)} ${version} ${specText} ${desc}`.replace(/\s{2,}/g, " ");
33
40
  })
34
41
  .forEach((line) => this.log(line));
35
42
  }
@@ -25,11 +25,15 @@ class ConfigService {
25
25
  const base = xdgConfigHome && xdgConfigHome.trim() ? xdgConfigHome : node_path_1.default.join(homeDir, ".config");
26
26
  return node_path_1.default.join(base, this.appName);
27
27
  }
28
+ getDataDir() {
29
+ const p = (0, env_paths_1.default)(this.appName, { suffix: "" });
30
+ return p.data;
31
+ }
28
32
  getConfigPath() {
29
33
  return node_path_1.default.join(this.getConfigDir(), "config.json");
30
34
  }
31
35
  getPluginsInstallDir() {
32
- return node_path_1.default.join(this.getConfigDir(), "plugins");
36
+ return node_path_1.default.join(this.getDataDir(), "plugins");
33
37
  }
34
38
  ensureConfigDirExists() {
35
39
  this.ensureDirExists(this.getConfigDir());
@@ -61,9 +65,11 @@ class ConfigService {
61
65
  return entry;
62
66
  }
63
67
  if (this.isObject(entry) && typeof entry.name === "string") {
68
+ const spec = "spec" in entry && typeof entry.spec === "string" ? entry.spec : undefined;
64
69
  const options = "options" in entry ? entry.options : undefined;
65
70
  return {
66
71
  name: entry.name,
72
+ ...(typeof spec === "undefined" ? {} : { spec }),
67
73
  ...(typeof options === "undefined" ? {} : { options })
68
74
  };
69
75
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadConfiguredPlugins = void 0;
4
+ const validate_plugin_default_export_1 = require("../utils/validate-plugin-default-export");
4
5
  const config_service_1 = require("./config-service");
5
6
  const plugin_store_1 = require("./plugin-store");
6
7
  const loadConfiguredPlugins = async () => {
@@ -11,12 +12,17 @@ const loadConfiguredPlugins = async () => {
11
12
  const specs = config.plugins.map(plugin_store_1.normalizePluginConfigEntry);
12
13
  const loaded = await Promise.all(specs.map(async (spec) => {
13
14
  try {
14
- const p = await store.instantiatePluginFromSpec(spec);
15
- if (!p || typeof p.setup !== "function") {
16
- console.error(`Skipping plugin ${spec.name}: default export is not a PagePocket plugin instance.`);
15
+ const mod = await store.importPluginModule(spec.name);
16
+ const validation = (0, validate_plugin_default_export_1.validatePluginDefaultExport)({
17
+ pluginName: spec.name,
18
+ moduleExports: mod,
19
+ options: spec.options
20
+ });
21
+ if (!validation.ok) {
22
+ console.error(`Skipping plugin ${spec.name}: ${validation.error.message}`);
17
23
  return null;
18
24
  }
19
- return p;
25
+ return validation.plugin;
20
26
  }
21
27
  catch (e) {
22
28
  const msg = e && typeof e.message === "string"
@@ -5,8 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PluginStore = exports.getPluginNameFromSpec = exports.normalizePluginConfigEntry = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
- const node_path_1 = __importDefault(require("node:path"));
9
8
  const node_module_1 = require("node:module");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_url_1 = require("node:url");
11
11
  const normalizePluginConfigEntry = (entry) => {
12
12
  if (typeof entry === "string") {
@@ -14,6 +14,7 @@ const normalizePluginConfigEntry = (entry) => {
14
14
  }
15
15
  return {
16
16
  name: entry.name,
17
+ ...(typeof entry.spec === "string" ? { spec: entry.spec } : {}),
17
18
  ...(typeof entry.options === "undefined" ? {} : { options: entry.options })
18
19
  };
19
20
  };
@@ -44,10 +45,22 @@ class PluginStore {
44
45
  node_fs_1.default.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`, "utf8");
45
46
  return pkgJsonPath;
46
47
  }
47
- createPluginsRequire() {
48
- const pkgJsonPath = this.ensurePluginPackageJson();
48
+ createPluginsRequireFromDir(dirPath) {
49
+ const pkgJsonPath = node_path_1.default.join(dirPath, "package.json");
50
+ if (!node_fs_1.default.existsSync(pkgJsonPath)) {
51
+ const pkgJson = {
52
+ name: "pagepocket-user-plugins",
53
+ private: true,
54
+ version: "0.0.0"
55
+ };
56
+ node_fs_1.default.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`, "utf8");
57
+ }
49
58
  return (0, node_module_1.createRequire)(pkgJsonPath);
50
59
  }
60
+ createPluginsRequire() {
61
+ this.ensurePluginPackageJson();
62
+ return this.createPluginsRequireFromDir(this.getInstallDir());
63
+ }
51
64
  resolveInstalledPackageJsonPath(pluginName) {
52
65
  const req = this.createPluginsRequire();
53
66
  return req.resolve(`${pluginName}/package.json`);
@@ -74,6 +87,9 @@ class PluginStore {
74
87
  async importPluginModule(pluginName) {
75
88
  const req = this.createPluginsRequire();
76
89
  const resolved = req.resolve(pluginName);
90
+ return this.importResolvedPath(resolved);
91
+ }
92
+ async importResolvedPath(resolved) {
77
93
  const dynamicImport = new Function("specifier", "return import(specifier)");
78
94
  const mod = await dynamicImport((0, node_url_1.pathToFileURL)(resolved).href);
79
95
  return (mod ?? {});
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePluginSpec = void 0;
4
+ const parsePluginSpec = (spec) => {
5
+ const trimmed = spec.trim();
6
+ if (!trimmed) {
7
+ throw new Error("plugin spec is empty");
8
+ }
9
+ const npa = require("npm-package-arg");
10
+ const parsed = npa(trimmed);
11
+ if (!parsed || typeof parsed.name !== "string" || !parsed.name) {
12
+ throw new Error(`Invalid plugin spec: ${trimmed}`);
13
+ }
14
+ return { name: parsed.name, spec: trimmed };
15
+ };
16
+ exports.parsePluginSpec = parsePluginSpec;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validatePluginDefaultExport = void 0;
4
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
5
+ const toError = (value) => {
6
+ if (value instanceof Error) {
7
+ return value;
8
+ }
9
+ return new Error(typeof value === "string" ? value : JSON.stringify(value));
10
+ };
11
+ const validatePluginDefaultExport = (input) => {
12
+ const { pluginName, moduleExports, options } = input;
13
+ const defaultExport = moduleExports.default;
14
+ if (typeof defaultExport !== "function") {
15
+ return {
16
+ ok: false,
17
+ error: new Error(`Plugin ${pluginName} must default export a class/constructor.`)
18
+ };
19
+ }
20
+ const ctor = defaultExport;
21
+ try {
22
+ const instance = typeof options === "undefined"
23
+ ? new ctor()
24
+ : Array.isArray(options)
25
+ ? new ctor(...options)
26
+ : new ctor(options);
27
+ if (!isRecord(instance)) {
28
+ return {
29
+ ok: false,
30
+ error: new Error(`Plugin ${pluginName} default export did not construct an object.`)
31
+ };
32
+ }
33
+ const name = instance.name;
34
+ if (typeof name !== "string" || name.trim().length === 0) {
35
+ return {
36
+ ok: false,
37
+ error: new Error(`Plugin ${pluginName} instance must have a non-empty string 'name'.`)
38
+ };
39
+ }
40
+ const setup = instance.setup;
41
+ if (typeof setup !== "function") {
42
+ return {
43
+ ok: false,
44
+ error: new Error(`Plugin ${pluginName} instance must implement setup(host).`)
45
+ };
46
+ }
47
+ const contribute = instance.contribute;
48
+ if (typeof contribute !== "undefined" && typeof contribute !== "function") {
49
+ return {
50
+ ok: false,
51
+ error: new Error(`Plugin ${pluginName} contribute must be a function if provided.`)
52
+ };
53
+ }
54
+ return { ok: true, plugin: instance };
55
+ }
56
+ catch (e) {
57
+ return { ok: false, error: toError(e) };
58
+ }
59
+ };
60
+ exports.validatePluginDefaultExport = validatePluginDefaultExport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagepocket/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "CLI for capturing offline snapshots of web pages.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,26 +15,27 @@
15
15
  "dependencies": {
16
16
  "@oclif/core": "^4.0.9",
17
17
  "chalk": "^4.1.2",
18
+ "env-paths": "^2.2.1",
18
19
  "koa": "^2.16.2",
19
20
  "koa-send": "^5.0.1",
20
21
  "koa-static": "^5.0.0",
22
+ "npm-package-arg": "^13.0.2",
21
23
  "ora": "^9.0.0",
22
- "env-paths": "^2.2.1",
23
- "@pagepocket/lib": "0.8.0",
24
- "@pagepocket/capture-http-cdp-unit": "0.8.0",
25
- "@pagepocket/capture-http-puppeteer-unit": "0.8.0",
26
- "@pagepocket/build-snapshot-unit": "0.8.0",
27
- "@pagepocket/capture-http-lighterceptor-unit": "0.8.0",
28
- "@pagepocket/plugin-yt-dlp": "0.8.0",
29
- "@pagepocket/single-file-unit": "0.8.0",
30
- "@pagepocket/write-down-unit": "0.8.0",
31
- "@pagepocket/contracts": "0.8.0"
24
+ "@pagepocket/build-snapshot-unit": "0.8.2",
25
+ "@pagepocket/capture-http-puppeteer-unit": "0.8.2",
26
+ "@pagepocket/capture-http-cdp-unit": "0.8.2",
27
+ "@pagepocket/contracts": "0.8.2",
28
+ "@pagepocket/lib": "0.8.2",
29
+ "@pagepocket/capture-http-lighterceptor-unit": "0.8.2",
30
+ "@pagepocket/plugin-yt-dlp": "0.8.2",
31
+ "@pagepocket/single-file-unit": "0.8.2",
32
+ "@pagepocket/write-down-unit": "0.8.2"
32
33
  },
33
34
  "devDependencies": {
34
- "@types/node": "^20.11.30",
35
35
  "@types/koa": "^2.15.0",
36
36
  "@types/koa-send": "^4.1.5",
37
37
  "@types/koa-static": "^4.0.4",
38
+ "@types/node": "^20.11.30",
38
39
  "tsx": "^4.19.3",
39
40
  "typescript": "^5.4.5"
40
41
  },