@pagepocket/cli 0.8.6 → 0.9.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.
package/README.md CHANGED
@@ -39,7 +39,12 @@ Manage configured plugins:
39
39
  ```bash
40
40
  pp plugin ls
41
41
  pp plugin add <plugin-name>
42
+ pp plugin set <plugin-name> [...options]
43
+ pp plugin update [plugin-name]
44
+ pp plugin doctor [plugin-name]
42
45
  pp plugin remove <plugin-name>
46
+ pp plugin uninstall <plugin-name> [--from-config]
47
+ pp plugin prune
43
48
  ```
44
49
 
45
50
  Options passing:
@@ -58,6 +63,12 @@ pp plugin add @scope/plugin --option 1 --option 2
58
63
  Notes:
59
64
 
60
65
  - `pp plugin remove` only removes the entry from `config.json` (it does not uninstall the package).
66
+ - `pp plugin set` updates options for an already configured plugin.
67
+ - `pp plugin update <plugin-name>` reinstalls that plugin to `latest`.
68
+ - `pp plugin update` reinstalls all configured plugins to `latest`.
69
+ - `pp plugin doctor` checks plugin install/load health and exits non-zero on failures.
70
+ - `pp plugin uninstall <plugin-name>` removes the installed package; add `--from-config` to also remove config.
71
+ - `pp plugin prune` removes installed packages that are no longer referenced in `config.json`.
61
72
  - During `pp archive ...`, the CLI reads `config.json` and tries to load all configured plugins.
62
73
  If a plugin fails to load, it will be skipped and the archive will continue.
63
74
 
@@ -8,46 +8,9 @@ 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_options_1 = require("../../utils/parse-plugin-options");
11
12
  const parse_plugin_spec_1 = require("../../utils/parse-plugin-spec");
12
13
  const validate_plugin_default_export_1 = require("../../utils/validate-plugin-default-export");
13
- const parseDynamicOptions = (argv) => {
14
- const tokens = argv.filter(Boolean);
15
- const pairs = tokens
16
- .map((t, i) => ({ t, next: tokens[i + 1] }))
17
- .filter(({ t }) => t === "--option" || t.startsWith("--option-"));
18
- const object = pairs
19
- .filter(({ t }) => t.startsWith("--option-"))
20
- .reduce((acc, { t, next }) => {
21
- const key = t.slice("--option-".length);
22
- if (!key) {
23
- throw new Error("--option-* key is empty");
24
- }
25
- if (typeof next === "undefined" || next.startsWith("-")) {
26
- throw new Error(`${t} expects a value`);
27
- }
28
- return { ...acc, [key]: next };
29
- }, {});
30
- const arrayValues = pairs
31
- .filter(({ t }) => t === "--option")
32
- .map(({ next }) => {
33
- if (typeof next === "undefined" || next.startsWith("-")) {
34
- throw new Error("--option expects a value");
35
- }
36
- return next;
37
- });
38
- const hasObject = Object.keys(object).length > 0;
39
- const hasArray = arrayValues.length > 0;
40
- if (hasObject && hasArray) {
41
- throw new Error("Cannot mix --option with --option-* flags");
42
- }
43
- if (hasObject) {
44
- return { kind: "object", value: object };
45
- }
46
- if (hasArray) {
47
- return { kind: "array", value: arrayValues };
48
- }
49
- return { kind: "none" };
50
- };
51
14
  class PluginAddCommand extends core_1.Command {
52
15
  async run() {
53
16
  const [name, ...rest] = this.argv;
@@ -67,7 +30,7 @@ class PluginAddCommand extends core_1.Command {
67
30
  this.log(chalk_1.default.gray(`Plugin already exists in config: ${parsedSpec.name}`));
68
31
  return;
69
32
  }
70
- const parsed = parseDynamicOptions(rest);
33
+ const parsed = (0, parse_plugin_options_1.parsePluginOptions)(rest);
71
34
  const entry = parsed.kind === "none"
72
35
  ? parsedSpec.name
73
36
  : parsed.kind === "array"
@@ -90,4 +53,5 @@ class PluginAddCommand extends core_1.Command {
90
53
  }
91
54
  }
92
55
  PluginAddCommand.description = "Install a plugin package, add it to config, and run its optional setup().";
56
+ PluginAddCommand.strict = false;
93
57
  exports.default = PluginAddCommand;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_service_1 = require("../../services/config-service");
9
+ const plugin_store_1 = require("../../services/plugin-store");
10
+ const parse_plugin_spec_1 = require("../../utils/parse-plugin-spec");
11
+ /**
12
+ * Validate one configured plugin can be loaded and instantiated.
13
+ *
14
+ * Usage:
15
+ * const result = await runDoctorCheck(store, spec);
16
+ */
17
+ const runDoctorCheck = async (store, spec) => {
18
+ const installed = Boolean(store.readInstalledPackageMeta(spec.name));
19
+ if (!installed) {
20
+ return {
21
+ plugin: spec.name,
22
+ installed: false,
23
+ loadable: false,
24
+ error: "not installed"
25
+ };
26
+ }
27
+ try {
28
+ await store.instantiatePluginFromSpec(spec);
29
+ return {
30
+ plugin: spec.name,
31
+ installed: true,
32
+ loadable: true
33
+ };
34
+ }
35
+ catch (error) {
36
+ return {
37
+ plugin: spec.name,
38
+ installed: true,
39
+ loadable: false,
40
+ error: error instanceof Error ? error.message : String(error)
41
+ };
42
+ }
43
+ };
44
+ class PluginDoctorCommand extends core_1.Command {
45
+ async run() {
46
+ const { args } = await this.parse(PluginDoctorCommand);
47
+ const configService = new config_service_1.ConfigService();
48
+ const store = new plugin_store_1.PluginStore(configService);
49
+ configService.ensureConfigFileExists();
50
+ const config = store.readConfig();
51
+ const targetName = args.name ? (0, parse_plugin_spec_1.parsePluginSpec)(args.name).name : undefined;
52
+ const configured = config.plugins
53
+ .map((entry) => (0, plugin_store_1.normalizePluginConfigEntry)(entry))
54
+ .filter((entry) => {
55
+ if (!targetName) {
56
+ return true;
57
+ }
58
+ return entry.name === targetName;
59
+ });
60
+ if (configured.length === 0) {
61
+ if (targetName) {
62
+ this.log(chalk_1.default.gray(`Plugin not found in config: ${targetName}`));
63
+ return;
64
+ }
65
+ this.log(chalk_1.default.gray("No plugins configured."));
66
+ return;
67
+ }
68
+ const checks = await Promise.all(configured.map((entry) => runDoctorCheck(store, entry)));
69
+ const failed = checks.filter((item) => !item.installed || !item.loadable);
70
+ checks.forEach((item) => {
71
+ if (item.installed && item.loadable) {
72
+ this.log(chalk_1.default.green(`[OK] ${item.plugin}`));
73
+ return;
74
+ }
75
+ this.log(chalk_1.default.red(`[FAIL] ${item.plugin}: ${item.error ?? "unknown error"}`));
76
+ });
77
+ if (failed.length === 0) {
78
+ this.log(chalk_1.default.green("Plugin doctor checks passed."));
79
+ return;
80
+ }
81
+ throw new Error(`${failed.length} plugin(s) failed doctor checks.`);
82
+ }
83
+ }
84
+ PluginDoctorCommand.description = "Validate configured plugins are installed and loadable.";
85
+ PluginDoctorCommand.args = {
86
+ name: core_1.Args.string({
87
+ description: "npm package name (optional; if omitted checks all configured plugins)",
88
+ required: false
89
+ })
90
+ };
91
+ exports.default = PluginDoctorCommand;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_service_1 = require("../../services/config-service");
9
+ const plugin_installer_1 = require("../../services/plugin-installer");
10
+ const plugin_store_1 = require("../../services/plugin-store");
11
+ /**
12
+ * Get configured plugin names from config.
13
+ *
14
+ * Usage:
15
+ * const names = getConfiguredPluginNames(store);
16
+ */
17
+ const getConfiguredPluginNames = (store) => {
18
+ const config = store.readConfig();
19
+ const names = config.plugins
20
+ .map((entry) => (0, plugin_store_1.normalizePluginConfigEntry)(entry).name)
21
+ .filter((name) => name.trim().length > 0);
22
+ return new Set(names);
23
+ };
24
+ class PluginPruneCommand extends core_1.Command {
25
+ async run() {
26
+ const configService = new config_service_1.ConfigService();
27
+ const store = new plugin_store_1.PluginStore(configService);
28
+ configService.ensureConfigFileExists();
29
+ const configured = getConfiguredPluginNames(store);
30
+ const installed = store.readInstalledDependencyNames();
31
+ const orphans = installed.filter((name) => !configured.has(name));
32
+ if (orphans.length === 0) {
33
+ this.log(chalk_1.default.gray("No orphan plugin packages found."));
34
+ return;
35
+ }
36
+ orphans.forEach((name) => {
37
+ (0, plugin_installer_1.uninstallPluginPackage)(store, { packageName: name });
38
+ this.log(chalk_1.default.green(`Pruned plugin package: ${name}`));
39
+ });
40
+ }
41
+ }
42
+ PluginPruneCommand.description = "Uninstall plugin packages that are not referenced by config.";
43
+ exports.default = PluginPruneCommand;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_service_1 = require("../../services/config-service");
9
+ const parse_plugin_options_1 = require("../../utils/parse-plugin-options");
10
+ const parse_plugin_spec_1 = require("../../utils/parse-plugin-spec");
11
+ /**
12
+ * Replace options for a configured plugin and keep its current name/spec.
13
+ *
14
+ * Usage:
15
+ * const next = buildUpdatedPluginEntry(existingEntry, parsedOptions);
16
+ */
17
+ const buildUpdatedPluginEntry = (entry, options) => {
18
+ if (typeof entry === "string") {
19
+ return {
20
+ name: entry,
21
+ options
22
+ };
23
+ }
24
+ return {
25
+ name: entry.name,
26
+ ...(typeof entry.spec === "string" ? { spec: entry.spec } : {}),
27
+ options
28
+ };
29
+ };
30
+ class PluginSetCommand extends core_1.Command {
31
+ async run() {
32
+ const [rawName, ...rest] = this.argv;
33
+ if (!rawName) {
34
+ throw new Error("plugin set: missing <plugin-name>");
35
+ }
36
+ const parsedOptions = (0, parse_plugin_options_1.parsePluginOptions)(rest);
37
+ if (parsedOptions.kind === "none") {
38
+ throw new Error("plugin set: provide options with --option or --option-*");
39
+ }
40
+ const targetName = (0, parse_plugin_spec_1.parsePluginSpec)(rawName).name;
41
+ const configService = new config_service_1.ConfigService();
42
+ configService.ensureConfigFileExists();
43
+ const config = configService.readConfigOrDefault();
44
+ const targetIndex = config.plugins.findIndex((entry) => {
45
+ return (typeof entry === "string" ? entry : entry.name) === targetName;
46
+ });
47
+ if (targetIndex < 0) {
48
+ this.log(chalk_1.default.gray(`Plugin not found in config: ${targetName}`));
49
+ return;
50
+ }
51
+ const targetEntry = config.plugins[targetIndex];
52
+ const nextEntry = buildUpdatedPluginEntry(targetEntry, parsedOptions.value);
53
+ const nextPlugins = config.plugins.map((entry, idx) => {
54
+ if (idx === targetIndex) {
55
+ return nextEntry;
56
+ }
57
+ return entry;
58
+ });
59
+ configService.writeConfig({
60
+ ...config,
61
+ plugins: nextPlugins
62
+ });
63
+ this.log(chalk_1.default.green(`Updated plugin options: ${targetName}`));
64
+ }
65
+ }
66
+ PluginSetCommand.description = "Update options for an already configured plugin.";
67
+ PluginSetCommand.strict = false;
68
+ exports.default = PluginSetCommand;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_service_1 = require("../../services/config-service");
9
+ const plugin_installer_1 = require("../../services/plugin-installer");
10
+ const plugin_store_1 = require("../../services/plugin-store");
11
+ const parse_plugin_spec_1 = require("../../utils/parse-plugin-spec");
12
+ class PluginUninstallCommand extends core_1.Command {
13
+ async run() {
14
+ const { args, flags } = await this.parse(PluginUninstallCommand);
15
+ const parsed = (0, parse_plugin_spec_1.parsePluginSpec)(args.name);
16
+ const targetName = parsed.name;
17
+ const configService = new config_service_1.ConfigService();
18
+ const store = new plugin_store_1.PluginStore(configService);
19
+ configService.ensureConfigFileExists();
20
+ const installedMeta = store.readInstalledPackageMeta(targetName);
21
+ if (!installedMeta) {
22
+ this.log(chalk_1.default.gray(`Plugin package is not installed: ${targetName}`));
23
+ }
24
+ else {
25
+ (0, plugin_installer_1.uninstallPluginPackage)(store, { packageName: targetName });
26
+ this.log(chalk_1.default.green(`Uninstalled plugin package: ${targetName}`));
27
+ }
28
+ if (!flags.fromConfig) {
29
+ return;
30
+ }
31
+ const config = configService.readConfigOrDefault();
32
+ const out = store.removePluginFromConfig(config, targetName);
33
+ if (!out.removed) {
34
+ this.log(chalk_1.default.gray(`Plugin not found in config: ${targetName}`));
35
+ return;
36
+ }
37
+ configService.writeConfig(out.config);
38
+ this.log(chalk_1.default.green(`Removed from config: ${targetName}`));
39
+ }
40
+ }
41
+ PluginUninstallCommand.description = "Uninstall a plugin package from the plugin install directory (optional config removal).";
42
+ PluginUninstallCommand.args = {
43
+ name: core_1.Args.string({
44
+ description: "npm package name",
45
+ required: true
46
+ })
47
+ };
48
+ PluginUninstallCommand.flags = {
49
+ fromConfig: core_1.Flags.boolean({
50
+ description: "also remove the plugin from config",
51
+ aliases: ["from-config"],
52
+ default: false
53
+ })
54
+ };
55
+ exports.default = PluginUninstallCommand;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const config_service_1 = require("../../services/config-service");
9
+ const plugin_installer_1 = require("../../services/plugin-installer");
10
+ const plugin_store_1 = require("../../services/plugin-store");
11
+ const parse_plugin_spec_1 = require("../../utils/parse-plugin-spec");
12
+ /**
13
+ * Read configured plugins and return unique package names.
14
+ *
15
+ * Usage:
16
+ * const names = getUniqueConfiguredPluginNames(store);
17
+ */
18
+ const getUniqueConfiguredPluginNames = (store) => {
19
+ const config = store.readConfig();
20
+ return [
21
+ ...new Set(config.plugins
22
+ .map((entry) => (0, plugin_store_1.normalizePluginConfigEntry)(entry).name)
23
+ .filter((name) => name.trim().length > 0))
24
+ ];
25
+ };
26
+ class PluginUpdateCommand extends core_1.Command {
27
+ async run() {
28
+ const { args } = await this.parse(PluginUpdateCommand);
29
+ const configService = new config_service_1.ConfigService();
30
+ const store = new plugin_store_1.PluginStore(configService);
31
+ configService.ensureConfigFileExists();
32
+ const targetName = args.name ? (0, parse_plugin_spec_1.parsePluginSpec)(args.name).name : undefined;
33
+ const configuredNames = getUniqueConfiguredPluginNames(store);
34
+ if (configuredNames.length === 0) {
35
+ this.log(chalk_1.default.gray("No plugins configured."));
36
+ return;
37
+ }
38
+ if (targetName && !configuredNames.includes(targetName)) {
39
+ this.log(chalk_1.default.gray(`Plugin not found in config: ${targetName}`));
40
+ return;
41
+ }
42
+ const namesToUpdate = targetName ? [targetName] : configuredNames;
43
+ namesToUpdate.forEach((name) => {
44
+ (0, plugin_installer_1.updatePluginPackageToLatest)(store, { packageName: name });
45
+ this.log(chalk_1.default.green(`Updated plugin: ${name}`));
46
+ });
47
+ }
48
+ }
49
+ PluginUpdateCommand.description = "Update one plugin (or all configured plugins) to latest.";
50
+ PluginUpdateCommand.args = {
51
+ name: core_1.Args.string({
52
+ description: "npm package name (optional; if omitted updates all configured plugins)",
53
+ required: false
54
+ })
55
+ };
56
+ exports.default = PluginUpdateCommand;
@@ -14,11 +14,11 @@ class ViewCommand extends core_1.Command {
14
14
  const { args, flags } = await this.parse(ViewCommand);
15
15
  const rootDir = node_path_1.default.resolve(node_process_1.default.cwd(), args.directory);
16
16
  const indexPath = node_path_1.default.join(rootDir, "index.html");
17
- const stat = await node_fs_1.default.promises.stat(rootDir).catch(() => null);
17
+ const stat = await node_fs_1.default.promises.stat(rootDir).catch(() => undefined);
18
18
  if (!stat || !stat.isDirectory()) {
19
19
  throw new Error(`view: directory not found: ${rootDir}`);
20
20
  }
21
- const indexStat = await node_fs_1.default.promises.stat(indexPath).catch(() => null);
21
+ const indexStat = await node_fs_1.default.promises.stat(indexPath).catch(() => undefined);
22
22
  if (!indexStat || !indexStat.isFile()) {
23
23
  throw new Error(`view: index.html not found under: ${rootDir}`);
24
24
  }
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const env_paths_1 = __importDefault(require("env-paths"));
11
+ const parse_json_1 = require("../utils/parse-json");
11
12
  const defaultConfig = {
12
13
  plugins: []
13
14
  };
@@ -45,7 +46,7 @@ class ConfigService {
45
46
  const dir = node_path_1.default.dirname(filePath);
46
47
  this.ensureDirExists(dir);
47
48
  const tmpPath = `${filePath}.tmp`;
48
- node_fs_1.default.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
49
+ node_fs_1.default.writeFileSync(tmpPath, `${JSON.stringify(value, undefined, 2)}\n`, "utf8");
49
50
  node_fs_1.default.renameSync(tmpPath, filePath);
50
51
  }
51
52
  isObject(value) {
@@ -73,33 +74,28 @@ class ConfigService {
73
74
  ...(typeof options === "undefined" ? {} : { options })
74
75
  };
75
76
  }
76
- return null;
77
+ return undefined;
77
78
  })
78
- .filter((x) => x !== null);
79
+ .filter((x) => typeof x !== "undefined");
79
80
  return { plugins };
80
81
  }
81
82
  readConfig() {
82
83
  const configPath = this.getConfigPath();
83
84
  const rawText = node_fs_1.default.readFileSync(configPath, "utf8");
84
- const parsed = JSON.parse(rawText);
85
- return this.normalizeConfig(parsed);
85
+ const parsed = (0, parse_json_1.parseJson)(rawText);
86
+ return this.normalizeConfig(parsed.ok ? parsed.value : undefined);
86
87
  }
87
88
  isConfigFilePresent() {
88
89
  return node_fs_1.default.existsSync(this.getConfigPath());
89
90
  }
90
91
  readConfigOrDefault() {
91
92
  const configPath = this.getConfigPath();
92
- const text = node_fs_1.default.existsSync(configPath) ? node_fs_1.default.readFileSync(configPath, "utf8") : null;
93
+ const text = node_fs_1.default.existsSync(configPath) ? node_fs_1.default.readFileSync(configPath, "utf8") : undefined;
93
94
  if (!text) {
94
95
  return defaultConfig;
95
96
  }
96
- try {
97
- const parsed = JSON.parse(text);
98
- return this.normalizeConfig(parsed);
99
- }
100
- catch {
101
- return defaultConfig;
102
- }
97
+ const parsed = (0, parse_json_1.parseJson)(text);
98
+ return parsed.ok ? this.normalizeConfig(parsed.value) : defaultConfig;
103
99
  }
104
100
  writeConfig(config) {
105
101
  this.writeJsonAtomic(this.getConfigPath(), this.normalizeConfig(config));
@@ -5,6 +5,18 @@ const validate_plugin_default_export_1 = require("../utils/validate-plugin-defau
5
5
  const config_service_1 = require("./config-service");
6
6
  const plugin_store_1 = require("./plugin-store");
7
7
  const loadConfiguredPlugins = async () => {
8
+ const getErrorMessage = (error) => {
9
+ if (error instanceof Error) {
10
+ return error.message;
11
+ }
12
+ if (error && typeof error === "object" && "message" in error) {
13
+ const msg = error.message;
14
+ if (typeof msg === "string") {
15
+ return msg;
16
+ }
17
+ }
18
+ return String(error);
19
+ };
8
20
  const configService = new config_service_1.ConfigService();
9
21
  const store = new plugin_store_1.PluginStore(configService);
10
22
  configService.ensureConfigFileExists();
@@ -20,18 +32,16 @@ const loadConfiguredPlugins = async () => {
20
32
  });
21
33
  if (!validation.ok) {
22
34
  console.error(`Skipping plugin ${spec.name}: ${validation.error.message}`);
23
- return null;
35
+ return undefined;
24
36
  }
25
37
  return validation.plugin;
26
38
  }
27
39
  catch (e) {
28
- const msg = e && typeof e.message === "string"
29
- ? e.message
30
- : String(e);
40
+ const msg = getErrorMessage(e);
31
41
  console.error(`Failed to load plugin ${spec.name}: ${msg}`);
32
- return null;
42
+ return undefined;
33
43
  }
34
44
  }));
35
- return loaded.filter((x) => x !== null);
45
+ return loaded.filter((x) => typeof x !== "undefined");
36
46
  };
37
47
  exports.loadConfiguredPlugins = loadConfiguredPlugins;
@@ -1,8 +1,58 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.installPluginPackage = void 0;
3
+ exports.uninstallPluginPackage = exports.updatePluginPackageToLatest = exports.installPluginPackage = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
+ /**
6
+ * Install a package spec with pnpm first, then fallback to npm.
7
+ *
8
+ * Usage:
9
+ * tryInstallWithFallback("/path/to/plugins", "@scope/plugin@latest");
10
+ */
11
+ const tryInstallWithFallback = (installDir, packageSpec) => {
12
+ const tryRun = (cmd, args) => {
13
+ const r = (0, node_child_process_1.spawnSync)(cmd, args, {
14
+ cwd: installDir,
15
+ stdio: "inherit",
16
+ shell: false
17
+ });
18
+ if (r.error) {
19
+ return { ok: false, error: r.error };
20
+ }
21
+ if (typeof r.status === "number" && r.status !== 0) {
22
+ return { ok: false, error: new Error(`${cmd} exited with code ${r.status}`) };
23
+ }
24
+ return { ok: true };
25
+ };
26
+ const pnpm = tryRun("pnpm", ["add", "--silent", packageSpec]);
27
+ if (pnpm.ok) {
28
+ return;
29
+ }
30
+ const npm = tryRun("npm", ["install", "--silent", packageSpec]);
31
+ if (npm.ok) {
32
+ return;
33
+ }
34
+ throw new Error(`Failed to install ${packageSpec}. Ensure pnpm or npm is available.`);
35
+ };
5
36
  const installPluginPackage = (store, input) => {
37
+ store.ensurePluginPackageJson();
38
+ const installDir = store.getInstallDir();
39
+ tryInstallWithFallback(installDir, input.packageName);
40
+ };
41
+ exports.installPluginPackage = installPluginPackage;
42
+ const updatePluginPackageToLatest = (store, input) => {
43
+ store.ensurePluginPackageJson();
44
+ const installDir = store.getInstallDir();
45
+ const packageSpec = `${input.packageName}@latest`;
46
+ tryInstallWithFallback(installDir, packageSpec);
47
+ };
48
+ exports.updatePluginPackageToLatest = updatePluginPackageToLatest;
49
+ /**
50
+ * Uninstall a package name with pnpm first, then fallback to npm.
51
+ *
52
+ * Usage:
53
+ * uninstallPluginPackage(store, { packageName: "@scope/plugin" });
54
+ */
55
+ const uninstallPluginPackage = (store, input) => {
6
56
  store.ensurePluginPackageJson();
7
57
  const installDir = store.getInstallDir();
8
58
  const tryRun = (cmd, args) => {
@@ -19,14 +69,14 @@ const installPluginPackage = (store, input) => {
19
69
  }
20
70
  return { ok: true };
21
71
  };
22
- const pnpm = tryRun("pnpm", ["add", "--silent", input.packageName]);
72
+ const pnpm = tryRun("pnpm", ["remove", "--silent", input.packageName]);
23
73
  if (pnpm.ok) {
24
74
  return;
25
75
  }
26
- const npm = tryRun("npm", ["install", "--silent", input.packageName]);
76
+ const npm = tryRun("npm", ["uninstall", "--silent", input.packageName]);
27
77
  if (npm.ok) {
28
78
  return;
29
79
  }
30
- throw new Error(`Failed to install ${input.packageName}. Ensure pnpm or npm is available.`);
80
+ throw new Error(`Failed to uninstall ${input.packageName}. Ensure pnpm or npm is available.`);
31
81
  };
32
- exports.installPluginPackage = installPluginPackage;
82
+ exports.uninstallPluginPackage = uninstallPluginPackage;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -8,6 +41,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
41
  const node_module_1 = require("node:module");
9
42
  const node_path_1 = __importDefault(require("node:path"));
10
43
  const node_url_1 = require("node:url");
44
+ const parse_json_1 = require("../utils/parse-json");
45
+ const validate_plugin_default_export_1 = require("../utils/validate-plugin-default-export");
11
46
  const normalizePluginConfigEntry = (entry) => {
12
47
  if (typeof entry === "string") {
13
48
  return { name: entry };
@@ -21,6 +56,18 @@ const normalizePluginConfigEntry = (entry) => {
21
56
  exports.normalizePluginConfigEntry = normalizePluginConfigEntry;
22
57
  const getPluginNameFromSpec = (spec) => spec.name;
23
58
  exports.getPluginNameFromSpec = getPluginNameFromSpec;
59
+ const isRecord = (value) => {
60
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
61
+ };
62
+ const isCallable = (value) => {
63
+ return typeof value === "function";
64
+ };
65
+ const isStringRecord = (value) => {
66
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
67
+ return false;
68
+ }
69
+ return Object.values(value).every((v) => typeof v === "string");
70
+ };
24
71
  class PluginStore {
25
72
  constructor(configService) {
26
73
  this.configService = configService;
@@ -42,7 +89,7 @@ class PluginStore {
42
89
  private: true,
43
90
  version: "0.0.0"
44
91
  };
45
- node_fs_1.default.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`, "utf8");
92
+ node_fs_1.default.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, undefined, 2)}\n`, "utf8");
46
93
  return pkgJsonPath;
47
94
  }
48
95
  createPluginsRequireFromDir(dirPath) {
@@ -53,7 +100,7 @@ class PluginStore {
53
100
  private: true,
54
101
  version: "0.0.0"
55
102
  };
56
- node_fs_1.default.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`, "utf8");
103
+ node_fs_1.default.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, undefined, 2)}\n`, "utf8");
57
104
  }
58
105
  return (0, node_module_1.createRequire)(pkgJsonPath);
59
106
  }
@@ -69,11 +116,11 @@ class PluginStore {
69
116
  try {
70
117
  const pkgJsonPath = this.resolveInstalledPackageJsonPath(pluginName);
71
118
  const text = node_fs_1.default.readFileSync(pkgJsonPath, "utf8");
72
- const json = JSON.parse(text);
73
- if (!json || typeof json !== "object") {
119
+ const json = (0, parse_json_1.parseJson)(text);
120
+ if (!json.ok || !json.value || typeof json.value !== "object") {
74
121
  return { name: pluginName };
75
122
  }
76
- const o = json;
123
+ const o = json.value;
77
124
  return {
78
125
  name: typeof o.name === "string" ? o.name : pluginName,
79
126
  description: typeof o.description === "string" ? o.description : undefined,
@@ -81,8 +128,28 @@ class PluginStore {
81
128
  };
82
129
  }
83
130
  catch {
84
- return null;
131
+ return undefined;
132
+ }
133
+ }
134
+ /**
135
+ * Read top-level dependencies from plugins install `package.json`.
136
+ *
137
+ * Usage:
138
+ * const names = store.readInstalledDependencyNames();
139
+ */
140
+ readInstalledDependencyNames() {
141
+ const pkgJsonPath = this.ensurePluginPackageJson();
142
+ const text = node_fs_1.default.readFileSync(pkgJsonPath, "utf8");
143
+ const parsed = (0, parse_json_1.parseJson)(text);
144
+ if (!parsed.ok || !parsed.value || typeof parsed.value !== "object") {
145
+ return [];
85
146
  }
147
+ const record = parsed.value;
148
+ const dependencies = record.dependencies;
149
+ if (!isStringRecord(dependencies)) {
150
+ return [];
151
+ }
152
+ return Object.keys(dependencies);
86
153
  }
87
154
  async importPluginModule(pluginName) {
88
155
  const req = this.createPluginsRequire();
@@ -90,30 +157,26 @@ class PluginStore {
90
157
  return this.importResolvedPath(resolved);
91
158
  }
92
159
  async importResolvedPath(resolved) {
93
- const dynamicImport = new Function("specifier", "return import(specifier)");
94
- const mod = await dynamicImport((0, node_url_1.pathToFileURL)(resolved).href);
95
- return (mod ?? {});
160
+ const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(resolved).href}`).then(s => __importStar(require(s)));
161
+ return isRecord(mod) ? mod : {};
96
162
  }
97
163
  async instantiatePluginFromSpec(spec) {
98
164
  const mod = await this.importPluginModule(spec.name);
99
- const defaultExport = mod.default;
100
- if (typeof defaultExport !== "function") {
101
- throw new Error(`Plugin ${spec.name} has no default export (class/function).`);
102
- }
103
- const ctor = defaultExport;
104
- if (typeof spec.options === "undefined") {
105
- return new ctor();
106
- }
107
- if (Array.isArray(spec.options)) {
108
- return new ctor(...spec.options);
165
+ const result = (0, validate_plugin_default_export_1.validatePluginDefaultExport)({
166
+ pluginName: spec.name,
167
+ moduleExports: mod,
168
+ options: spec.options
169
+ });
170
+ if (!result.ok) {
171
+ throw result.error;
109
172
  }
110
- return new ctor(spec.options);
173
+ return result.plugin;
111
174
  }
112
175
  async runOptionalSetup(pluginName) {
113
176
  const mod = await this.importPluginModule(pluginName);
114
177
  const setup = mod.setup;
115
- if (typeof setup === "function") {
116
- await setup();
178
+ if (isCallable(setup)) {
179
+ await Promise.resolve(setup());
117
180
  }
118
181
  }
119
182
  readConfig() {
@@ -21,12 +21,6 @@ const normalizeArgv = (argv) => {
21
21
  return argv;
22
22
  }
23
23
  const first = argv[0];
24
- if (first === "plugin") {
25
- const sub = argv[1];
26
- if (sub === "ls" || sub === "add" || sub === "remove") {
27
- return [`plugin:${sub}`, ...argv.slice(2)];
28
- }
29
- }
30
24
  // If the first token is a URL, treat it as implicit `archive`.
31
25
  if (first && (0, exports.isUrlLike)(first)) {
32
26
  return ["archive", ...argv];
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseJson = void 0;
4
+ const toErrorMessage = (value) => {
5
+ if (value instanceof Error) {
6
+ return value.message;
7
+ }
8
+ if (typeof value === "string") {
9
+ return value;
10
+ }
11
+ if (value && typeof value === "object" && "message" in value) {
12
+ return String(value.message);
13
+ }
14
+ return JSON.stringify(value);
15
+ };
16
+ const parseJson = (text) => {
17
+ try {
18
+ return { ok: true, value: JSON.parse(text) };
19
+ }
20
+ catch (e) {
21
+ return { ok: false, error: new Error(toErrorMessage(e)) };
22
+ }
23
+ };
24
+ exports.parseJson = parseJson;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePluginOptions = void 0;
4
+ /**
5
+ * Parse plugin options from argv tokens.
6
+ *
7
+ * Usage:
8
+ * const parsed = parsePluginOptions(["--option-mode", "strict"]);
9
+ */
10
+ const parsePluginOptions = (argv) => {
11
+ const tokens = argv.filter(Boolean);
12
+ const pairs = tokens
13
+ .map((t, i) => ({ t, next: tokens[i + 1] }))
14
+ .filter(({ t }) => t === "--option" || t.startsWith("--option-"));
15
+ const object = pairs
16
+ .filter(({ t }) => t.startsWith("--option-"))
17
+ .reduce((acc, { t, next }) => {
18
+ const key = t.slice("--option-".length);
19
+ if (!key) {
20
+ throw new Error("--option-* key is empty");
21
+ }
22
+ if (typeof next === "undefined" || next.startsWith("-")) {
23
+ throw new Error(`${t} expects a value`);
24
+ }
25
+ return { ...acc, [key]: next };
26
+ }, {});
27
+ const arrayValues = pairs
28
+ .filter(({ t }) => t === "--option")
29
+ .map(({ next }) => {
30
+ if (typeof next === "undefined" || next.startsWith("-")) {
31
+ throw new Error("--option expects a value");
32
+ }
33
+ return next;
34
+ });
35
+ const hasObject = Object.keys(object).length > 0;
36
+ const hasArray = arrayValues.length > 0;
37
+ if (hasObject && hasArray) {
38
+ throw new Error("Cannot mix --option with --option-* flags");
39
+ }
40
+ if (hasObject) {
41
+ return { kind: "object", value: object };
42
+ }
43
+ if (hasArray) {
44
+ return { kind: "array", value: arrayValues };
45
+ }
46
+ return { kind: "none" };
47
+ };
48
+ exports.parsePluginOptions = parsePluginOptions;
@@ -2,6 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validatePluginDefaultExport = void 0;
4
4
  const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
5
+ const isPagePocketPlugin = (value) => {
6
+ if (!isRecord(value)) {
7
+ return false;
8
+ }
9
+ if (typeof value.name !== "string" || value.name.trim().length === 0) {
10
+ return false;
11
+ }
12
+ if (typeof value.setup !== "function") {
13
+ return false;
14
+ }
15
+ if (typeof value.contribute !== "undefined" && typeof value.contribute !== "function") {
16
+ return false;
17
+ }
18
+ return true;
19
+ };
5
20
  const toError = (value) => {
6
21
  if (value instanceof Error) {
7
22
  return value;
@@ -24,33 +39,12 @@ const validatePluginDefaultExport = (input) => {
24
39
  : Array.isArray(options)
25
40
  ? new ctor(...options)
26
41
  : new ctor(options);
27
- if (!isRecord(instance)) {
42
+ if (!isPagePocketPlugin(instance)) {
28
43
  return {
29
44
  ok: false,
30
45
  error: new Error(`Plugin ${pluginName} default export did not construct an object.`)
31
46
  };
32
47
  }
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
48
  return { ok: true, plugin: instance };
55
49
  }
56
50
  catch (e) {
package/dist/view.js CHANGED
@@ -11,11 +11,11 @@ const koa_send_1 = __importDefault(require("koa-send"));
11
11
  const koa_static_1 = __importDefault(require("koa-static"));
12
12
  const createViewServer = async (args) => {
13
13
  const indexPath = node_path_1.default.join(args.rootDir, "index.html");
14
- const stat = await node_fs_1.default.promises.stat(args.rootDir).catch(() => null);
14
+ const stat = await node_fs_1.default.promises.stat(args.rootDir).catch(() => undefined);
15
15
  if (!stat || !stat.isDirectory()) {
16
16
  throw new Error(`view: directory not found: ${args.rootDir}`);
17
17
  }
18
- const indexStat = await node_fs_1.default.promises.stat(indexPath).catch(() => null);
18
+ const indexStat = await node_fs_1.default.promises.stat(indexPath).catch(() => undefined);
19
19
  if (!indexStat || !indexStat.isFile()) {
20
20
  throw new Error(`view: index.html not found under: ${args.rootDir}`);
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagepocket/cli",
3
- "version": "0.8.6",
3
+ "version": "0.9.1",
4
4
  "description": "CLI for capturing offline snapshots of web pages.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,15 +21,15 @@
21
21
  "koa-static": "^5.0.0",
22
22
  "npm-package-arg": "^13.0.2",
23
23
  "ora": "^9.0.0",
24
- "@pagepocket/build-snapshot-unit": "0.8.6",
25
- "@pagepocket/capture-http-cdp-unit": "0.8.6",
26
- "@pagepocket/capture-http-lighterceptor-unit": "0.8.6",
27
- "@pagepocket/capture-http-puppeteer-unit": "0.8.6",
28
- "@pagepocket/contracts": "0.8.6",
29
- "@pagepocket/lib": "0.8.6",
30
- "@pagepocket/plugin-yt-dlp": "0.8.6",
31
- "@pagepocket/single-file-unit": "0.8.6",
32
- "@pagepocket/write-down-unit": "0.8.6"
24
+ "@pagepocket/build-snapshot-unit": "0.9.1",
25
+ "@pagepocket/capture-http-puppeteer-unit": "0.9.1",
26
+ "@pagepocket/capture-http-lighterceptor-unit": "0.9.1",
27
+ "@pagepocket/capture-http-cdp-unit": "0.9.1",
28
+ "@pagepocket/lib": "0.9.1",
29
+ "@pagepocket/contracts": "0.9.1",
30
+ "@pagepocket/plugin-yt-dlp": "0.9.1",
31
+ "@pagepocket/single-file-unit": "0.9.1",
32
+ "@pagepocket/write-down-unit": "0.9.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/koa": "^2.15.0",
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "oclif": {
43
43
  "bin": "pp",
44
+ "topicSeparator": " ",
44
45
  "commands": {
45
46
  "strategy": "pattern",
46
47
  "target": "dist/commands"