@pagepocket/cli 0.9.1 → 0.10.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.
@@ -4,83 +4,97 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const core_1 = require("@oclif/core");
7
- const build_snapshot_unit_1 = require("@pagepocket/build-snapshot-unit");
8
- const capture_http_cdp_unit_1 = require("@pagepocket/capture-http-cdp-unit");
9
- const capture_http_lighterceptor_unit_1 = require("@pagepocket/capture-http-lighterceptor-unit");
10
- const capture_http_puppeteer_unit_1 = require("@pagepocket/capture-http-puppeteer-unit");
11
7
  const lib_1 = require("@pagepocket/lib");
12
- const single_file_unit_1 = require("@pagepocket/single-file-unit");
13
- const write_down_unit_1 = require("@pagepocket/write-down-unit");
14
8
  const chalk_1 = __importDefault(require("chalk"));
15
9
  const load_configured_plugins_1 = require("../services/load-configured-plugins");
16
- const network_observer_unit_1 = require("../units/network-observer-unit");
10
+ const config_service_1 = require("../services/config-service");
11
+ const read_installed_package_version_1 = require("../services/strategy/read-installed-package-version");
12
+ const strategy_service_1 = require("../services/strategy/strategy-service");
13
+ const strategy_normalize_1 = require("../services/strategy/strategy-normalize");
14
+ const parse_pinned_spec_1 = require("../services/user-packages/parse-pinned-spec");
15
+ const unit_store_1 = require("../services/units/unit-store");
17
16
  const with_spinner_1 = require("../utils/with-spinner");
18
- const MAX_STATUS_URL_LENGTH = 80;
19
- const formatStatusUrl = (url) => {
20
- if (url.length <= MAX_STATUS_URL_LENGTH) {
21
- return url;
17
+ const loadStrategyUnits = async (input) => {
18
+ const strategyService = new strategy_service_1.StrategyService(input.configService);
19
+ const unitStore = new unit_store_1.UnitStore(input.configService);
20
+ strategyService.ensureConfigFileExists();
21
+ const strategyFile = strategyService.readStrategy(input.strategyName);
22
+ const normalized = (0, strategy_normalize_1.normalizeStrategyUnits)(strategyFile);
23
+ const installed = unitStore.readInstalledDependencyVersions();
24
+ const drift = normalized.flatMap((u) => {
25
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(u.ref);
26
+ const installedVersion = installed[pinned.name];
27
+ if (!installedVersion || installedVersion !== pinned.version) {
28
+ return [
29
+ {
30
+ name: pinned.name,
31
+ pinned: pinned.version,
32
+ installed: installedVersion
33
+ }
34
+ ];
35
+ }
36
+ return [];
37
+ });
38
+ if (drift.length > 0) {
39
+ const details = drift
40
+ .map((d) => {
41
+ const installedText = typeof d.installed === "string" ? d.installed : "(not installed)";
42
+ return `${d.name}: pinned ${d.pinned}, installed ${installedText}`;
43
+ })
44
+ .join("; ");
45
+ throw new Error(`Strategy drift detected (${input.strategyName}). ${details}. ` +
46
+ `Fix: pp strategy update ${input.strategyName} OR pp strategy pin ${input.strategyName}`);
47
+ }
48
+ const loadedUnits = await Promise.all(normalized.map(async (u) => unitStore.instantiateFromRef(u.ref, u.args)));
49
+ const seen = new Set();
50
+ const dup = loadedUnits.find((u) => {
51
+ if (seen.has(u.id)) {
52
+ return true;
53
+ }
54
+ seen.add(u.id);
55
+ return false;
56
+ });
57
+ if (dup) {
58
+ throw new Error(`Duplicate unit id detected in strategy ${input.strategyName}: ${dup.id}`);
22
59
  }
23
- return `${url.slice(0, MAX_STATUS_URL_LENGTH - 3)}...`;
60
+ return loadedUnits;
24
61
  };
25
62
  class ArchiveCommand extends core_1.Command {
26
63
  async run() {
27
64
  const { args, flags } = await this.parse(ArchiveCommand);
28
65
  const targetUrl = args.url;
29
- const outputFlag = flags.output ? flags.output.trim() : undefined;
30
- const emit = flags.emit;
31
- const type = flags.type;
32
- const overwrite = flags.overwrite === true;
33
- const triggerActions = (flags.action ?? []);
34
- const runtime = flags.runtime;
35
66
  const timeoutMs = typeof flags.timeout === "number" ? flags.timeout : undefined;
36
67
  const maxDurationMs = typeof flags.maxDuration === "number" ? flags.maxDuration : undefined;
37
- const headers = {
38
- "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
39
- accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
40
- "accept-language": "en-US,en;q=0.9",
41
- referer: targetUrl
42
- };
68
+ const strategyName = typeof flags.strategy === "string" ? flags.strategy.trim() : undefined;
43
69
  const pagepocket = lib_1.PagePocket.fromURL(targetUrl);
44
- if (type === "single-file" && emit === "zip") {
45
- throw new Error("--type single-file is not compatible with --emit zip (single-file is a directory output)");
46
- }
47
70
  const result = await (0, with_spinner_1.withSpinner)(async (spinner) => {
48
71
  {
49
- const writeDownOptions = {
50
- type: emit,
51
- outputPath: outputFlag ?? process.cwd(),
52
- overwrite
53
- };
54
- const captureUnit = runtime === "puppeteer"
55
- ? new capture_http_puppeteer_unit_1.CaptureHttpPuppeteerUnit({ triggerActions })
56
- : runtime === "lighterceptor"
57
- ? new capture_http_lighterceptor_unit_1.CaptureHttpLighterceptorUnit({ headers, triggerActions })
58
- : new capture_http_cdp_unit_1.CaptureHttpCdpUnit({ triggerActions });
59
- const units = [
60
- new network_observer_unit_1.NetworkObserverUnit({
61
- onRequest: (event) => {
62
- spinner.text = `Freezing page (${formatStatusUrl(event.url)})`;
63
- }
64
- }),
65
- captureUnit,
66
- new build_snapshot_unit_1.BuildSnapshotUnit()
67
- ];
68
- if (type === "single-file") {
69
- units.push(new single_file_unit_1.SingleFileUnit());
72
+ const configService = new config_service_1.ConfigService();
73
+ const name = strategyName && strategyName.length > 0 ? strategyName : "default";
74
+ const strategyService = new strategy_service_1.StrategyService(configService);
75
+ strategyService.ensureConfigFileExists();
76
+ const installedNames = strategyService.listInstalledStrategyNames();
77
+ if (installedNames.length === 0) {
78
+ const v = (0, read_installed_package_version_1.readInstalledPackageVersion)("@pagepocket/builtin-strategy");
79
+ await strategyService.addStrategy({
80
+ source: `@pagepocket/builtin-strategy@${v}`,
81
+ force: false
82
+ });
70
83
  }
71
- units.push(new write_down_unit_1.WriteDownUnit(writeDownOptions));
72
- const entryTarget = runtime === "cdp"
73
- ? (() => {
74
- const tabId = flags.tabId;
75
- if (typeof tabId !== "number") {
76
- throw new Error("--tabId is required when --runtime cdp");
77
- }
78
- return lib_1.PagePocket.fromCDPTab(tabId);
79
- })()
80
- : pagepocket;
84
+ const units = await loadStrategyUnits({
85
+ strategyName: name,
86
+ configService
87
+ });
88
+ const entryTarget = pagepocket;
89
+ const strategyFile = strategyService.readStrategy(name);
90
+ const captureOptions = strategyFile.pipeline.captureOptions;
91
+ const effectiveTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : captureOptions?.timeoutMs;
92
+ const effectiveMaxDurationMs = typeof maxDurationMs === "number" ? maxDurationMs : captureOptions?.maxDurationMs;
81
93
  const result = await entryTarget.capture({
82
- timeoutMs,
83
- maxDurationMs,
94
+ ...(typeof effectiveTimeoutMs === "number" ? { timeoutMs: effectiveTimeoutMs } : {}),
95
+ ...(typeof effectiveMaxDurationMs === "number"
96
+ ? { maxDurationMs: effectiveMaxDurationMs }
97
+ : {}),
84
98
  blacklist: [...lib_1.ga, ...lib_1.ns],
85
99
  units,
86
100
  plugins: await (0, load_configured_plugins_1.loadConfiguredPlugins)().catch(() => [])
@@ -109,47 +123,17 @@ ArchiveCommand.flags = {
109
123
  help: core_1.Flags.help({
110
124
  char: "h"
111
125
  }),
112
- output: core_1.Flags.string({
113
- char: "o",
114
- description: "Output directory"
115
- }),
116
- emit: core_1.Flags.string({
117
- description: "Output format",
118
- options: ["raw", "zip"],
119
- default: "raw"
120
- }),
121
- type: core_1.Flags.string({
122
- description: "Snapshot type",
123
- options: ["directory", "single-file"],
124
- default: "directory"
125
- }),
126
- overwrite: core_1.Flags.boolean({
127
- description: "Overwrite existing output directory instead of suffixing",
128
- default: false
129
- }),
130
- action: core_1.Flags.string({
131
- char: "a",
132
- description: "Trigger action to run during/after capture (repeatable). Use -a multiple times to run multiple actions.",
133
- options: Object.values({ HOVER: "HOVER", SCROLL_TO_END: "SCROLL_TO_END" }),
134
- multiple: true
135
- }),
136
- runtime: core_1.Flags.string({
137
- char: "r",
138
- description: "Interceptor runtime to use",
139
- options: ["lighterceptor", "puppeteer", "cdp"],
140
- default: "puppeteer"
141
- }),
142
- tabId: core_1.Flags.integer({
143
- description: "CDP tabId (required when --runtime cdp)"
144
- }),
145
126
  timeout: core_1.Flags.integer({
146
127
  char: "t",
147
128
  description: "Network idle duration in milliseconds before capture stops",
148
- default: 5000
129
+ required: false
149
130
  }),
150
131
  maxDuration: core_1.Flags.integer({
151
132
  description: "Hard max capture duration in milliseconds",
152
- default: 60000
133
+ required: false
134
+ }),
135
+ strategy: core_1.Flags.string({
136
+ description: "Run an installed strategy (unit pipeline) by name"
153
137
  })
154
138
  };
155
139
  exports.default = ArchiveCommand;
@@ -0,0 +1,34 @@
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 strategy_service_1 = require("../../services/strategy/strategy-service");
9
+ class StrategyAddCommand extends core_1.Command {
10
+ async run() {
11
+ const { args, flags } = await this.parse(StrategyAddCommand);
12
+ const service = new strategy_service_1.StrategyService();
13
+ service.ensureConfigFileExists();
14
+ const out = await service.addStrategy({
15
+ source: args.source,
16
+ force: flags.force === true
17
+ });
18
+ out.installedStrategies.forEach((name) => {
19
+ this.log(chalk_1.default.green(`Added strategy: ${name}`));
20
+ });
21
+ out.installedRefs.forEach((ref) => {
22
+ this.log(chalk_1.default.green(`Installed unit: ${ref}`));
23
+ });
24
+ void flags;
25
+ }
26
+ }
27
+ StrategyAddCommand.description = "Install strategies from a file/url OR install a strategy pack from a pinned npm package (applies all *.strategy.json).";
28
+ StrategyAddCommand.args = {
29
+ source: core_1.Args.string({ description: "strategy file/url OR pinned npm package", required: true })
30
+ };
31
+ StrategyAddCommand.flags = {
32
+ force: core_1.Flags.boolean({ description: "overwrite existing strategy", default: false })
33
+ };
34
+ exports.default = StrategyAddCommand;
@@ -0,0 +1,34 @@
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 strategy_service_1 = require("../../services/strategy/strategy-service");
9
+ class StrategyDoctorCommand extends core_1.Command {
10
+ async run() {
11
+ const service = new strategy_service_1.StrategyService();
12
+ const report = service.doctor();
13
+ report.conflicts.forEach((c) => {
14
+ this.log(chalk_1.default.red(`[CONFLICT] ${c.name}`));
15
+ Object.entries(c.versions).forEach(([version, strategies]) => {
16
+ this.log(chalk_1.default.red(` ${version}: ${strategies.join(", ")}`));
17
+ });
18
+ });
19
+ report.drift.forEach((d) => {
20
+ this.log(chalk_1.default.yellow(`[DRIFT] ${d.strategy}`));
21
+ d.items.forEach((item) => {
22
+ const installed = typeof item.installed === "string" ? item.installed : "(not installed)";
23
+ this.log(chalk_1.default.yellow(` ${item.name}: pinned ${item.pinned}, installed ${installed}`));
24
+ });
25
+ });
26
+ if (report.conflicts.length === 0 && report.drift.length === 0) {
27
+ this.log(chalk_1.default.green("Strategy doctor checks passed."));
28
+ return;
29
+ }
30
+ throw new Error("Strategy doctor checks failed.");
31
+ }
32
+ }
33
+ StrategyDoctorCommand.description = "Diagnose strategy conflicts and unit package drift.";
34
+ exports.default = StrategyDoctorCommand;
@@ -0,0 +1,21 @@
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 strategy_service_1 = require("../../services/strategy/strategy-service");
9
+ class StrategyPinCommand extends core_1.Command {
10
+ async run() {
11
+ const { args } = await this.parse(StrategyPinCommand);
12
+ const service = new strategy_service_1.StrategyService();
13
+ service.pinStrategy(args.name);
14
+ this.log(chalk_1.default.green(`Pinned strategy: ${args.name}`));
15
+ }
16
+ }
17
+ StrategyPinCommand.description = "Pin a strategy file to currently installed unit package versions.";
18
+ StrategyPinCommand.args = {
19
+ name: core_1.Args.string({ description: "strategy name", required: true })
20
+ };
21
+ exports.default = StrategyPinCommand;
@@ -0,0 +1,21 @@
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 strategy_service_1 = require("../../services/strategy/strategy-service");
9
+ class StrategyRemoveCommand extends core_1.Command {
10
+ async run() {
11
+ const { args } = await this.parse(StrategyRemoveCommand);
12
+ const service = new strategy_service_1.StrategyService();
13
+ service.removeStrategy(args.name);
14
+ this.log(chalk_1.default.green(`Removed strategy: ${args.name}`));
15
+ }
16
+ }
17
+ StrategyRemoveCommand.description = "Remove an installed strategy.";
18
+ StrategyRemoveCommand.args = {
19
+ name: core_1.Args.string({ description: "strategy name", required: true })
20
+ };
21
+ exports.default = StrategyRemoveCommand;
@@ -0,0 +1,34 @@
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 strategy_service_1 = require("../../services/strategy/strategy-service");
9
+ class StrategyUpdateCommand extends core_1.Command {
10
+ async run() {
11
+ const { args, flags } = await this.parse(StrategyUpdateCommand);
12
+ const service = new strategy_service_1.StrategyService();
13
+ await service.updateStrategy(args.name, { packageOnly: flags.packageOnly === true });
14
+ if (flags.packageOnly) {
15
+ this.log(chalk_1.default.yellow("WARNING: Updated unit packages without updating strategy files. This may cause version drift."));
16
+ this.log(chalk_1.default.gray("If drift occurs, fix by updating packages to pinned versions:"));
17
+ this.log(chalk_1.default.gray(` pp strategy update${args.name ? ` ${args.name}` : ""}`));
18
+ this.log(chalk_1.default.gray("Or pin a strategy to installed versions:"));
19
+ this.log(chalk_1.default.gray(` pp strategy pin ${args.name ?? "<strategy-name>"}`));
20
+ }
21
+ this.log(chalk_1.default.green("Strategy update completed."));
22
+ }
23
+ }
24
+ StrategyUpdateCommand.description = "Update a strategy (or all strategies).";
25
+ StrategyUpdateCommand.args = {
26
+ name: core_1.Args.string({ description: "strategy name (optional)", required: false })
27
+ };
28
+ StrategyUpdateCommand.flags = {
29
+ packageOnly: core_1.Flags.boolean({
30
+ description: "update unit packages to latest only",
31
+ default: false
32
+ })
33
+ };
34
+ exports.default = StrategyUpdateCommand;
@@ -10,7 +10,9 @@ const node_path_1 = __importDefault(require("node:path"));
10
10
  const env_paths_1 = __importDefault(require("env-paths"));
11
11
  const parse_json_1 = require("../utils/parse-json");
12
12
  const defaultConfig = {
13
- plugins: []
13
+ plugins: [],
14
+ strategies: [],
15
+ strategyPacks: []
14
16
  };
15
17
  class ConfigService {
16
18
  constructor(appName = "pagepocket") {
@@ -57,27 +59,57 @@ class ConfigService {
57
59
  return defaultConfig;
58
60
  }
59
61
  const pluginsRaw = value.plugins;
60
- if (!Array.isArray(pluginsRaw)) {
61
- return defaultConfig;
62
- }
63
- const plugins = pluginsRaw
64
- .map((entry) => {
65
- if (typeof entry === "string") {
66
- return entry;
67
- }
68
- if (this.isObject(entry) && typeof entry.name === "string") {
69
- const spec = "spec" in entry && typeof entry.spec === "string" ? entry.spec : undefined;
70
- const options = "options" in entry ? entry.options : undefined;
71
- return {
72
- name: entry.name,
73
- ...(typeof spec === "undefined" ? {} : { spec }),
74
- ...(typeof options === "undefined" ? {} : { options })
75
- };
76
- }
77
- return undefined;
78
- })
79
- .filter((x) => typeof x !== "undefined");
80
- return { plugins };
62
+ const plugins = Array.isArray(pluginsRaw)
63
+ ? pluginsRaw
64
+ .map((entry) => {
65
+ if (typeof entry === "string") {
66
+ return entry;
67
+ }
68
+ if (this.isObject(entry) && typeof entry.name === "string") {
69
+ const spec = "spec" in entry && typeof entry.spec === "string"
70
+ ? entry.spec
71
+ : undefined;
72
+ const options = "options" in entry ? entry.options : undefined;
73
+ return {
74
+ name: entry.name,
75
+ ...(typeof spec === "undefined" ? {} : { spec }),
76
+ ...(typeof options === "undefined" ? {} : { options })
77
+ };
78
+ }
79
+ return undefined;
80
+ })
81
+ .filter((x) => typeof x !== "undefined")
82
+ : [];
83
+ const strategiesRaw = value.strategies;
84
+ const strategies = Array.isArray(strategiesRaw)
85
+ ? strategiesRaw
86
+ .map((x) => (typeof x === "string" ? x.trim() : ""))
87
+ .filter((x) => x.length > 0)
88
+ : [];
89
+ const packsRaw = value.strategyPacks;
90
+ const strategyPacks = Array.isArray(packsRaw)
91
+ ? packsRaw
92
+ .map((entry) => {
93
+ if (typeof entry === "string") {
94
+ return entry.trim();
95
+ }
96
+ if (this.isObject(entry) &&
97
+ typeof entry.name === "string" &&
98
+ typeof entry.spec === "string") {
99
+ return {
100
+ name: entry.name.trim(),
101
+ spec: entry.spec.trim()
102
+ };
103
+ }
104
+ return undefined;
105
+ })
106
+ .filter((x) => Boolean(x))
107
+ : [];
108
+ return {
109
+ plugins,
110
+ strategies,
111
+ strategyPacks
112
+ };
81
113
  }
82
114
  readConfig() {
83
115
  const configPath = this.getConfigPath();
@@ -0,0 +1,28 @@
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
+ exports.readInstalledPackageVersion = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const parse_json_1 = require("../../utils/parse-json");
9
+ const isRecord = (value) => {
10
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11
+ };
12
+ const readInstalledPackageVersion = (packageName) => {
13
+ const req = globalThis.require;
14
+ if (!req) {
15
+ throw new Error("require is not available in this runtime");
16
+ }
17
+ const pkgJsonPath = req.resolve(`${packageName}/package.json`);
18
+ const text = node_fs_1.default.readFileSync(pkgJsonPath, "utf8");
19
+ const parsed = (0, parse_json_1.parseJson)(text);
20
+ if (!parsed.ok) {
21
+ throw parsed.error;
22
+ }
23
+ if (!isRecord(parsed.value) || typeof parsed.value.version !== "string") {
24
+ throw new Error(`Failed to read package version: ${packageName}`);
25
+ }
26
+ return parsed.value.version;
27
+ };
28
+ exports.readInstalledPackageVersion = readInstalledPackageVersion;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureNoInstalledVersionConflicts = exports.computeDrift = exports.computeConflicts = exports.collectWantedVersions = exports.uniq = void 0;
4
+ const parse_pinned_spec_1 = require("../user-packages/parse-pinned-spec");
5
+ const uniq = (xs) => [...new Set(xs)];
6
+ exports.uniq = uniq;
7
+ const collectWantedVersions = (strategies) => {
8
+ var _a, _b, _c;
9
+ const wanted = {};
10
+ for (const s of strategies) {
11
+ for (const u of s.units) {
12
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(u.ref);
13
+ wanted[_a = pinned.name] ?? (wanted[_a] = {});
14
+ (_b = wanted[pinned.name])[_c = pinned.version] ?? (_b[_c] = []);
15
+ wanted[pinned.name][pinned.version].push(s.name);
16
+ }
17
+ }
18
+ return wanted;
19
+ };
20
+ exports.collectWantedVersions = collectWantedVersions;
21
+ const computeConflicts = (wanted) => {
22
+ const conflicts = [];
23
+ for (const [name, versions] of Object.entries(wanted)) {
24
+ const uniqueVersions = Object.keys(versions);
25
+ if (uniqueVersions.length <= 1) {
26
+ continue;
27
+ }
28
+ conflicts.push({ name, versions });
29
+ }
30
+ return conflicts;
31
+ };
32
+ exports.computeConflicts = computeConflicts;
33
+ const computeDrift = (input) => {
34
+ const items = input.units.flatMap((u) => {
35
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(u.ref);
36
+ const installedVersion = input.installed[pinned.name];
37
+ if (!installedVersion || installedVersion !== pinned.version) {
38
+ return [
39
+ {
40
+ name: pinned.name,
41
+ pinned: pinned.version,
42
+ installed: installedVersion
43
+ }
44
+ ];
45
+ }
46
+ return [];
47
+ });
48
+ return { strategy: input.strategyName, items };
49
+ };
50
+ exports.computeDrift = computeDrift;
51
+ const ensureNoInstalledVersionConflicts = (installed, refs) => {
52
+ const byName = {};
53
+ for (const ref of refs) {
54
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(ref);
55
+ const already = byName[pinned.name];
56
+ if (already && already !== pinned.version) {
57
+ throw new Error(`Strategy contains conflicting versions for ${pinned.name}: ${already} vs ${pinned.version}`);
58
+ }
59
+ byName[pinned.name] = pinned.version;
60
+ }
61
+ for (const [name, version] of Object.entries(byName)) {
62
+ const installedVersion = installed[name];
63
+ if (installedVersion && installedVersion !== version) {
64
+ throw new Error(`Version conflict for ${name}: installed ${installedVersion}, strategy requires ${version}. ` +
65
+ "This CLI stores only one version per package name.");
66
+ }
67
+ }
68
+ };
69
+ exports.ensureNoInstalledVersionConflicts = ensureNoInstalledVersionConflicts;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withoutStrategyInConfig = exports.withStrategyInConfig = exports.requireStrategyInstalled = exports.listStrategyNamesFromConfig = void 0;
4
+ const uniq = (xs) => [...new Set(xs)];
5
+ const listStrategyNamesFromConfig = (config) => {
6
+ return uniq((config.strategies ?? []).map((s) => s.trim()).filter((s) => s.length > 0));
7
+ };
8
+ exports.listStrategyNamesFromConfig = listStrategyNamesFromConfig;
9
+ const requireStrategyInstalled = (config, name) => {
10
+ const installed = (0, exports.listStrategyNamesFromConfig)(config);
11
+ if (!installed.includes(name)) {
12
+ throw new Error(`Strategy not installed: ${name}`);
13
+ }
14
+ };
15
+ exports.requireStrategyInstalled = requireStrategyInstalled;
16
+ const withStrategyInConfig = (config, name) => {
17
+ const strategies = uniq([...(config.strategies ?? []), name]);
18
+ return { ...config, strategies };
19
+ };
20
+ exports.withStrategyInConfig = withStrategyInConfig;
21
+ const withoutStrategyInConfig = (config, name) => {
22
+ const strategies = (config.strategies ?? []).filter((s) => s !== name);
23
+ return { ...config, strategies };
24
+ };
25
+ exports.withoutStrategyInConfig = withoutStrategyInConfig;
@@ -0,0 +1,71 @@
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
+ exports.fetchStrategyFile = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const parse_json_1 = require("../../utils/parse-json");
10
+ const normalize_argv_1 = require("../../utils/normalize-argv");
11
+ const isRecord = (value) => {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ };
14
+ const isStrategyFile = (value) => {
15
+ if (!isRecord(value)) {
16
+ return false;
17
+ }
18
+ if (value.schemaVersion !== 1) {
19
+ return false;
20
+ }
21
+ if (typeof value.name !== "string" || value.name.trim().length === 0) {
22
+ return false;
23
+ }
24
+ const pipeline = value.pipeline;
25
+ if (!isRecord(pipeline)) {
26
+ return false;
27
+ }
28
+ if (!Array.isArray(pipeline.units)) {
29
+ return false;
30
+ }
31
+ return true;
32
+ };
33
+ const readTextFromPath = (p) => {
34
+ const full = node_path_1.default.resolve(p);
35
+ return node_fs_1.default.readFileSync(full, "utf8");
36
+ };
37
+ const fetchStrategyFile = async (input) => {
38
+ const raw = input.trim();
39
+ if (!raw) {
40
+ throw new Error("strategy source is empty");
41
+ }
42
+ if ((0, normalize_argv_1.isUrlLike)(raw)) {
43
+ const url = new URL(raw);
44
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
45
+ throw new Error(`Unsupported strategy URL protocol: ${url.protocol}`);
46
+ }
47
+ const res = await fetch(url.toString());
48
+ if (!res.ok) {
49
+ throw new Error(`Failed to fetch strategy: ${res.status} ${res.statusText}`);
50
+ }
51
+ const text = await res.text();
52
+ const parsed = (0, parse_json_1.parseJson)(text);
53
+ if (!parsed.ok) {
54
+ throw parsed.error;
55
+ }
56
+ if (!isStrategyFile(parsed.value)) {
57
+ throw new Error(`Invalid strategy JSON from URL: ${url.toString()}`);
58
+ }
59
+ return { strategy: parsed.value, source: { type: "url", value: url.toString() } };
60
+ }
61
+ const text = readTextFromPath(raw);
62
+ const parsed = (0, parse_json_1.parseJson)(text);
63
+ if (!parsed.ok) {
64
+ throw parsed.error;
65
+ }
66
+ if (!isStrategyFile(parsed.value)) {
67
+ throw new Error(`Invalid strategy JSON from file: ${raw}`);
68
+ }
69
+ return { strategy: parsed.value, source: { type: "file", value: node_path_1.default.resolve(raw) } };
70
+ };
71
+ exports.fetchStrategyFile = fetchStrategyFile;