@pagepocket/cli 0.9.0 → 0.9.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
@@ -39,8 +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]
42
43
  pp plugin update [plugin-name]
44
+ pp plugin doctor [plugin-name]
43
45
  pp plugin remove <plugin-name>
46
+ pp plugin uninstall <plugin-name> [--from-config]
47
+ pp plugin prune
44
48
  ```
45
49
 
46
50
  Options passing:
@@ -59,8 +63,12 @@ pp plugin add @scope/plugin --option 1 --option 2
59
63
  Notes:
60
64
 
61
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.
62
67
  - `pp plugin update <plugin-name>` reinstalls that plugin to `latest`.
63
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`.
64
72
  - During `pp archive ...`, the CLI reads `config.json` and tries to load all configured plugins.
65
73
  If a plugin fails to load, it will be skipped and the archive will continue.
66
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;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.updatePluginPackageToLatest = 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
5
  /**
6
6
  * Install a package spec with pnpm first, then fallback to npm.
@@ -46,3 +46,37 @@ const updatePluginPackageToLatest = (store, input) => {
46
46
  tryInstallWithFallback(installDir, packageSpec);
47
47
  };
48
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) => {
56
+ store.ensurePluginPackageJson();
57
+ const installDir = store.getInstallDir();
58
+ const tryRun = (cmd, args) => {
59
+ const r = (0, node_child_process_1.spawnSync)(cmd, args, {
60
+ cwd: installDir,
61
+ stdio: "inherit",
62
+ shell: false
63
+ });
64
+ if (r.error) {
65
+ return { ok: false, error: r.error };
66
+ }
67
+ if (typeof r.status === "number" && r.status !== 0) {
68
+ return { ok: false, error: new Error(`${cmd} exited with code ${r.status}`) };
69
+ }
70
+ return { ok: true };
71
+ };
72
+ const pnpm = tryRun("pnpm", ["remove", "--silent", input.packageName]);
73
+ if (pnpm.ok) {
74
+ return;
75
+ }
76
+ const npm = tryRun("npm", ["uninstall", "--silent", input.packageName]);
77
+ if (npm.ok) {
78
+ return;
79
+ }
80
+ throw new Error(`Failed to uninstall ${input.packageName}. Ensure pnpm or npm is available.`);
81
+ };
82
+ exports.uninstallPluginPackage = uninstallPluginPackage;
@@ -62,6 +62,12 @@ const isRecord = (value) => {
62
62
  const isCallable = (value) => {
63
63
  return typeof value === "function";
64
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
+ };
65
71
  class PluginStore {
66
72
  constructor(configService) {
67
73
  this.configService = configService;
@@ -125,6 +131,26 @@ class PluginStore {
125
131
  return undefined;
126
132
  }
127
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 [];
146
+ }
147
+ const record = parsed.value;
148
+ const dependencies = record.dependencies;
149
+ if (!isStringRecord(dependencies)) {
150
+ return [];
151
+ }
152
+ return Object.keys(dependencies);
153
+ }
128
154
  async importPluginModule(pluginName) {
129
155
  const req = this.createPluginsRequire();
130
156
  const resolved = req.resolve(pluginName);
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagepocket/cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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.9.0",
25
- "@pagepocket/contracts": "0.9.0",
26
- "@pagepocket/capture-http-lighterceptor-unit": "0.9.0",
27
- "@pagepocket/capture-http-puppeteer-unit": "0.9.0",
28
- "@pagepocket/lib": "0.9.0",
29
- "@pagepocket/capture-http-cdp-unit": "0.9.0",
30
- "@pagepocket/single-file-unit": "0.9.0",
31
- "@pagepocket/plugin-yt-dlp": "0.9.0",
32
- "@pagepocket/write-down-unit": "0.9.0"
24
+ "@pagepocket/build-snapshot-unit": "0.9.2",
25
+ "@pagepocket/capture-http-lighterceptor-unit": "0.9.2",
26
+ "@pagepocket/contracts": "0.9.2",
27
+ "@pagepocket/capture-http-puppeteer-unit": "0.9.2",
28
+ "@pagepocket/lib": "0.9.2",
29
+ "@pagepocket/capture-http-cdp-unit": "0.9.2",
30
+ "@pagepocket/single-file-unit": "0.9.2",
31
+ "@pagepocket/plugin-yt-dlp": "0.9.2",
32
+ "@pagepocket/write-down-unit": "0.9.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/koa": "^2.15.0",