@pagepocket/cli 0.9.2 → 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.
@@ -0,0 +1,59 @@
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.writeJsonAtomic = exports.readStrategyFile = exports.getStrategyPath = exports.getStrategiesDir = 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 isRecord = (value) => {
11
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ };
13
+ const isStrategyFile = (value) => {
14
+ if (!isRecord(value)) {
15
+ return false;
16
+ }
17
+ if (value.schemaVersion !== 1) {
18
+ return false;
19
+ }
20
+ if (typeof value.name !== "string" || value.name.trim().length === 0) {
21
+ return false;
22
+ }
23
+ const pipeline = value.pipeline;
24
+ if (!isRecord(pipeline)) {
25
+ return false;
26
+ }
27
+ if (!Array.isArray(pipeline.units)) {
28
+ return false;
29
+ }
30
+ return true;
31
+ };
32
+ const getStrategiesDir = (configService) => {
33
+ return node_path_1.default.join(configService.getConfigDir(), "strategies");
34
+ };
35
+ exports.getStrategiesDir = getStrategiesDir;
36
+ const getStrategyPath = (configService, name) => {
37
+ return node_path_1.default.join((0, exports.getStrategiesDir)(configService), `${name}.json`);
38
+ };
39
+ exports.getStrategyPath = getStrategyPath;
40
+ const readStrategyFile = (filePath) => {
41
+ const text = node_fs_1.default.readFileSync(filePath, "utf8");
42
+ const parsed = (0, parse_json_1.parseJson)(text);
43
+ if (!parsed.ok) {
44
+ throw parsed.error;
45
+ }
46
+ if (!isStrategyFile(parsed.value)) {
47
+ throw new Error(`Invalid strategy file: ${filePath}`);
48
+ }
49
+ return parsed.value;
50
+ };
51
+ exports.readStrategyFile = readStrategyFile;
52
+ const writeJsonAtomic = (filePath, value) => {
53
+ const dir = node_path_1.default.dirname(filePath);
54
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
55
+ const tmpPath = `${filePath}.tmp`;
56
+ node_fs_1.default.writeFileSync(tmpPath, `${JSON.stringify(value, undefined, 2)}\n`, "utf8");
57
+ node_fs_1.default.renameSync(tmpPath, filePath);
58
+ };
59
+ exports.writeJsonAtomic = writeJsonAtomic;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeStrategyUnits = void 0;
4
+ const parse_pinned_spec_1 = require("../user-packages/parse-pinned-spec");
5
+ const isRecord = (value) => {
6
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
+ };
8
+ const isJsonValue = (value) => {
9
+ if (value === null) {
10
+ return true;
11
+ }
12
+ const t = typeof value;
13
+ if (t === "string" || t === "number" || t === "boolean") {
14
+ return true;
15
+ }
16
+ if (Array.isArray(value)) {
17
+ return value.every((v) => isJsonValue(v));
18
+ }
19
+ if (isRecord(value)) {
20
+ return Object.values(value).every((v) => isJsonValue(v));
21
+ }
22
+ return false;
23
+ };
24
+ const normalizeUnitSpec = (spec, idx) => {
25
+ if (typeof spec === "string") {
26
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(spec);
27
+ return { ref: pinned.spec, args: [] };
28
+ }
29
+ if (!isRecord(spec)) {
30
+ throw new Error(`Invalid unit spec at index ${idx}`);
31
+ }
32
+ const ref = spec.ref;
33
+ if (typeof ref !== "string") {
34
+ throw new Error(`Invalid unit ref at index ${idx}`);
35
+ }
36
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(ref);
37
+ const argsRaw = spec.args;
38
+ const args = typeof argsRaw === "undefined" ? [] : argsRaw;
39
+ if (!Array.isArray(args)) {
40
+ throw new Error(`Invalid unit args at index ${idx} (must be an array)`);
41
+ }
42
+ if (!isJsonValue(args)) {
43
+ throw new Error(`Invalid unit args at index ${idx} (must be JSON-only)`);
44
+ }
45
+ return { ref: pinned.spec, args };
46
+ };
47
+ const normalizeStrategyUnits = (strategy) => {
48
+ const units = strategy.pipeline.units;
49
+ return units.map((spec, idx) => normalizeUnitSpec(spec, idx));
50
+ };
51
+ exports.normalizeStrategyUnits = normalizeStrategyUnits;
@@ -0,0 +1,54 @@
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.readStrategiesFromPackRoot = 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 isRecord = (value) => {
11
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ };
13
+ const isStrategyFile = (value) => {
14
+ if (!isRecord(value)) {
15
+ return false;
16
+ }
17
+ if (value.schemaVersion !== 1) {
18
+ return false;
19
+ }
20
+ if (typeof value.name !== "string" || value.name.trim().length === 0) {
21
+ return false;
22
+ }
23
+ const pipeline = value.pipeline;
24
+ if (!isRecord(pipeline)) {
25
+ return false;
26
+ }
27
+ if (!Array.isArray(pipeline.units)) {
28
+ return false;
29
+ }
30
+ return true;
31
+ };
32
+ const readStrategiesFromPackRoot = (packRoot) => {
33
+ const strategiesDir = node_path_1.default.join(packRoot, "strategies");
34
+ if (!node_fs_1.default.existsSync(strategiesDir)) {
35
+ return [];
36
+ }
37
+ const fileNames = node_fs_1.default
38
+ .readdirSync(strategiesDir)
39
+ .filter((f) => f.endsWith(".strategy.json"))
40
+ .sort((a, b) => a.localeCompare(b));
41
+ return fileNames.map((fileName) => {
42
+ const filePath = node_path_1.default.join(strategiesDir, fileName);
43
+ const text = node_fs_1.default.readFileSync(filePath, "utf8");
44
+ const parsed = (0, parse_json_1.parseJson)(text);
45
+ if (!parsed.ok) {
46
+ throw parsed.error;
47
+ }
48
+ if (!isStrategyFile(parsed.value)) {
49
+ throw new Error(`Invalid strategy file in pack: ${filePath}`);
50
+ }
51
+ return parsed.value;
52
+ });
53
+ };
54
+ exports.readStrategiesFromPackRoot = readStrategiesFromPackRoot;
@@ -0,0 +1,45 @@
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.StrategyPackStore = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const user_package_installer_1 = require("../user-packages/user-package-installer");
9
+ const user_package_installer_2 = require("../user-packages/user-package-installer");
10
+ const parse_pinned_spec_1 = require("../user-packages/parse-pinned-spec");
11
+ const user_package_store_1 = require("../user-packages/user-package-store");
12
+ const STRATEGY_PACKS_KIND = "strategy-packs";
13
+ const STRATEGY_PACKS_PACKAGE_JSON_NAME = "pagepocket-user-strategy-packs";
14
+ class StrategyPackStore {
15
+ constructor(configService) {
16
+ this.store = new user_package_store_1.UserPackageStore(configService, STRATEGY_PACKS_KIND);
17
+ }
18
+ getInstallDir() {
19
+ return this.store.getInstallDir();
20
+ }
21
+ readInstalledDependencyVersions() {
22
+ return this.store.readInstalledDependencyVersions(STRATEGY_PACKS_PACKAGE_JSON_NAME);
23
+ }
24
+ installPinned(spec) {
25
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(spec);
26
+ (0, user_package_installer_1.installPinnedPackage)(this.store, {
27
+ packageJsonName: STRATEGY_PACKS_PACKAGE_JSON_NAME,
28
+ packageSpec: pinned.spec
29
+ });
30
+ return pinned;
31
+ }
32
+ updateToLatest(packageName) {
33
+ (0, user_package_installer_2.updatePackageToLatest)(this.store, {
34
+ packageJsonName: STRATEGY_PACKS_PACKAGE_JSON_NAME,
35
+ packageName
36
+ });
37
+ }
38
+ resolvePackRoot(packageName) {
39
+ this.store.ensureInstallDirPackageJson(STRATEGY_PACKS_PACKAGE_JSON_NAME);
40
+ const req = this.store.createRequire(STRATEGY_PACKS_PACKAGE_JSON_NAME);
41
+ const pkgJsonPath = req.resolve(`${packageName}/package.json`);
42
+ return node_path_1.default.dirname(pkgJsonPath);
43
+ }
44
+ }
45
+ exports.StrategyPackStore = StrategyPackStore;
@@ -0,0 +1,300 @@
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.StrategyService = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const config_service_1 = require("../config-service");
9
+ const parse_pinned_spec_1 = require("../user-packages/parse-pinned-spec");
10
+ const unit_store_1 = require("../units/unit-store");
11
+ const strategy_analyze_1 = require("./strategy-analyze");
12
+ const strategy_config_1 = require("./strategy-config");
13
+ const strategy_fetch_1 = require("./strategy-fetch");
14
+ const strategy_io_1 = require("./strategy-io");
15
+ const strategy_normalize_1 = require("./strategy-normalize");
16
+ const strategy_pack_read_1 = require("./strategy-pack-read");
17
+ const strategy_pack_store_1 = require("./strategy-pack-store");
18
+ class StrategyService {
19
+ constructor(configService = new config_service_1.ConfigService()) {
20
+ this.configService = configService;
21
+ this.unitStore = new unit_store_1.UnitStore(configService);
22
+ this.packStore = new strategy_pack_store_1.StrategyPackStore(configService);
23
+ }
24
+ ensureConfigFileExists() {
25
+ return this.configService.ensureConfigFileExists();
26
+ }
27
+ getStrategiesDir() {
28
+ return (0, strategy_io_1.getStrategiesDir)(this.configService);
29
+ }
30
+ getStrategyPath(name) {
31
+ return (0, strategy_io_1.getStrategyPath)(this.configService, name);
32
+ }
33
+ readStrategy(name) {
34
+ return (0, strategy_io_1.readStrategyFile)(this.getStrategyPath(name));
35
+ }
36
+ listInstalledStrategyNames() {
37
+ const config = this.configService.readConfigOrDefault();
38
+ return (0, strategy_config_1.listStrategyNamesFromConfig)(config);
39
+ }
40
+ async addStrategy(input) {
41
+ this.configService.ensureConfigFileExists();
42
+ const config = this.configService.readConfigOrDefault();
43
+ const sourceTrimmed = input.source.trim();
44
+ const isPinnedNpm = (() => {
45
+ try {
46
+ (0, parse_pinned_spec_1.parsePinnedSpec)(sourceTrimmed);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ })();
53
+ if (isPinnedNpm) {
54
+ const pinned = this.packStore.installPinned(sourceTrimmed);
55
+ const packRoot = this.packStore.resolvePackRoot(pinned.name);
56
+ const strategies = (0, strategy_pack_read_1.readStrategiesFromPackRoot)(packRoot);
57
+ if (strategies.length === 0) {
58
+ throw new Error(`No *.strategy.json found in package: ${pinned.spec}`);
59
+ }
60
+ const allUnits = strategies.flatMap((s) => (0, strategy_normalize_1.normalizeStrategyUnits)(s));
61
+ const refs = (0, strategy_analyze_1.uniq)(allUnits.map((u) => u.ref));
62
+ (0, strategy_analyze_1.ensureNoInstalledVersionConflicts)(this.unitStore.readInstalledDependencyVersions(), refs);
63
+ refs.forEach((ref) => this.unitStore.installPinned(ref));
64
+ const installedStrategies = [];
65
+ strategies.forEach((strategy) => {
66
+ const name = strategy.name;
67
+ const strategyPath = this.getStrategyPath(name);
68
+ if (!input.force && node_fs_1.default.existsSync(strategyPath)) {
69
+ return;
70
+ }
71
+ const toWrite = {
72
+ ...strategy,
73
+ source: { type: "npm", value: pinned.spec }
74
+ };
75
+ (0, strategy_io_1.writeJsonAtomic)(strategyPath, toWrite);
76
+ installedStrategies.push(name);
77
+ });
78
+ const nextConfig = installedStrategies.reduce((acc, s) => (0, strategy_config_1.withStrategyInConfig)(acc, s), config);
79
+ const existing = nextConfig.strategyPacks ?? [];
80
+ const filtered = existing.filter((x) => {
81
+ if (typeof x === "string") {
82
+ return x.trim().length > 0;
83
+ }
84
+ return x.name !== pinned.name;
85
+ });
86
+ const strategyPacks = [
87
+ ...filtered,
88
+ {
89
+ name: pinned.name,
90
+ spec: pinned.spec
91
+ }
92
+ ];
93
+ this.configService.writeConfig({ ...nextConfig, strategyPacks });
94
+ return { installedRefs: refs, installedStrategies };
95
+ }
96
+ const { strategy, source } = await (0, strategy_fetch_1.fetchStrategyFile)(sourceTrimmed);
97
+ const normalizedUnits = (0, strategy_normalize_1.normalizeStrategyUnits)(strategy);
98
+ const refs = (0, strategy_analyze_1.uniq)(normalizedUnits.map((u) => u.ref));
99
+ const strategyPath = this.getStrategyPath(strategy.name);
100
+ if (!input.force && node_fs_1.default.existsSync(strategyPath)) {
101
+ throw new Error(`Strategy already exists: ${strategy.name}`);
102
+ }
103
+ (0, strategy_analyze_1.ensureNoInstalledVersionConflicts)(this.unitStore.readInstalledDependencyVersions(), refs);
104
+ refs.forEach((ref) => this.unitStore.installPinned(ref));
105
+ const toWrite = {
106
+ ...strategy,
107
+ source
108
+ };
109
+ (0, strategy_io_1.writeJsonAtomic)(strategyPath, toWrite);
110
+ this.configService.writeConfig((0, strategy_config_1.withStrategyInConfig)(config, strategy.name));
111
+ return { installedRefs: refs, installedStrategies: [strategy.name] };
112
+ }
113
+ removeStrategy(name) {
114
+ this.configService.ensureConfigFileExists();
115
+ const config = this.configService.readConfigOrDefault();
116
+ (0, strategy_config_1.requireStrategyInstalled)(config, name);
117
+ const strategyPath = this.getStrategyPath(name);
118
+ if (node_fs_1.default.existsSync(strategyPath)) {
119
+ node_fs_1.default.rmSync(strategyPath);
120
+ }
121
+ this.configService.writeConfig((0, strategy_config_1.withoutStrategyInConfig)(config, name));
122
+ }
123
+ async updateStrategy(name, opts) {
124
+ this.configService.ensureConfigFileExists();
125
+ const config = this.configService.readConfigOrDefault();
126
+ const names = name ? [name] : (0, strategy_config_1.listStrategyNamesFromConfig)(config);
127
+ if (names.length === 0) {
128
+ return;
129
+ }
130
+ const installed = this.unitStore.readInstalledDependencyVersions();
131
+ if (opts?.packageOnly) {
132
+ const packageNames = (0, strategy_analyze_1.uniq)(names
133
+ .flatMap((n) => (0, strategy_normalize_1.normalizeStrategyUnits)(this.readStrategy(n)).map((u) => (0, parse_pinned_spec_1.parsePinnedSpec)(u.ref).name))
134
+ .filter((x) => x.trim().length > 0));
135
+ packageNames.forEach((pkg) => {
136
+ this.unitStore.updateToLatest(pkg);
137
+ });
138
+ const afterInstalled = this.unitStore.readInstalledDependencyVersions();
139
+ const afterStrategies = (0, strategy_config_1.listStrategyNamesFromConfig)(config).map((n) => ({
140
+ name: n,
141
+ units: (0, strategy_normalize_1.normalizeStrategyUnits)(this.readStrategy(n))
142
+ }));
143
+ const wanted = (0, strategy_analyze_1.collectWantedVersions)(afterStrategies);
144
+ const conflicts = (0, strategy_analyze_1.computeConflicts)(wanted);
145
+ if (conflicts.length > 0) {
146
+ throw new Error(`Strategy version conflicts detected (${conflicts.length}). Run 'pp strategy doctor' for details.`);
147
+ }
148
+ const drift = afterStrategies
149
+ .map((s) => (0, strategy_analyze_1.computeDrift)({ strategyName: s.name, units: s.units, installed: afterInstalled }))
150
+ .filter((d) => d.items.length > 0);
151
+ if (drift.length > 0) {
152
+ throw new Error("Strategy drift detected after --package-only update. Run 'pp strategy doctor'.");
153
+ }
154
+ return;
155
+ }
156
+ const nextStrategies = [];
157
+ const npmPacksToUpdate = new Set();
158
+ for (const n of names) {
159
+ const current = this.readStrategy(n);
160
+ const src = current.source;
161
+ if (!src) {
162
+ throw new Error(`Strategy ${n} has no source. Re-add it with a URL/path source to enable update.`);
163
+ }
164
+ if (src.type === "npm") {
165
+ npmPacksToUpdate.add(src.value);
166
+ continue;
167
+ }
168
+ const fetched = await (0, strategy_fetch_1.fetchStrategyFile)(src.value);
169
+ if (fetched.strategy.name !== n) {
170
+ throw new Error(`Strategy name mismatch while updating ${n}: got ${fetched.strategy.name}`);
171
+ }
172
+ const file = { ...fetched.strategy, source: fetched.source };
173
+ const units = (0, strategy_normalize_1.normalizeStrategyUnits)(file);
174
+ nextStrategies.push({ name: n, file, units });
175
+ }
176
+ if (npmPacksToUpdate.size > 0) {
177
+ const specs = [...npmPacksToUpdate];
178
+ specs.forEach((spec) => {
179
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(spec);
180
+ this.packStore.updateToLatest(pinned.name);
181
+ });
182
+ const installedPackVersions = this.packStore.readInstalledDependencyVersions();
183
+ const updatedSpecs = specs.map((spec) => {
184
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(spec);
185
+ const v = installedPackVersions[pinned.name];
186
+ if (!v) {
187
+ throw new Error(`Strategy pack not installed after update: ${pinned.name}`);
188
+ }
189
+ return `${pinned.name}@${v}`;
190
+ });
191
+ const updatedFiles = [];
192
+ updatedSpecs.forEach((spec) => {
193
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(spec);
194
+ const root = this.packStore.resolvePackRoot(pinned.name);
195
+ const files = (0, strategy_pack_read_1.readStrategiesFromPackRoot)(root).map((f) => ({
196
+ ...f,
197
+ source: { type: "npm", value: spec }
198
+ }));
199
+ updatedFiles.push(...files);
200
+ });
201
+ updatedFiles.forEach((file) => {
202
+ const units = (0, strategy_normalize_1.normalizeStrategyUnits)(file);
203
+ nextStrategies.push({ name: file.name, file, units });
204
+ });
205
+ const nextConfig = this.configService.readConfigOrDefault();
206
+ const existing = nextConfig.strategyPacks ?? [];
207
+ const filtered = existing.filter((x) => {
208
+ if (typeof x === "string") {
209
+ return x.trim().length > 0;
210
+ }
211
+ return !updatedSpecs.some((spec) => (0, parse_pinned_spec_1.parsePinnedSpec)(spec).name === x.name);
212
+ });
213
+ const packsNext = [
214
+ ...filtered,
215
+ ...updatedSpecs.map((spec) => {
216
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(spec);
217
+ return { name: pinned.name, spec: pinned.spec };
218
+ })
219
+ ];
220
+ this.configService.writeConfig({ ...nextConfig, strategyPacks: packsNext });
221
+ }
222
+ const allOtherNames = (0, strategy_config_1.listStrategyNamesFromConfig)(config).filter((n) => !names.includes(n));
223
+ const otherStrategies = allOtherNames.map((n) => ({
224
+ name: n,
225
+ units: (0, strategy_normalize_1.normalizeStrategyUnits)(this.readStrategy(n))
226
+ }));
227
+ const wanted = (0, strategy_analyze_1.collectWantedVersions)([
228
+ ...otherStrategies,
229
+ ...nextStrategies.map((s) => ({ name: s.name, units: s.units }))
230
+ ]);
231
+ const conflicts = (0, strategy_analyze_1.computeConflicts)(wanted);
232
+ if (conflicts.length > 0) {
233
+ throw new Error(`Strategy version conflicts detected (${conflicts.length}). Run 'pp strategy doctor' for details.`);
234
+ }
235
+ const refs = (0, strategy_analyze_1.uniq)(nextStrategies.flatMap((s) => s.units.map((u) => u.ref)));
236
+ (0, strategy_analyze_1.ensureNoInstalledVersionConflicts)(installed, refs);
237
+ refs.forEach((ref) => {
238
+ this.unitStore.installPinned(ref);
239
+ });
240
+ nextStrategies.forEach((s) => {
241
+ (0, strategy_io_1.writeJsonAtomic)(this.getStrategyPath(s.name), s.file);
242
+ });
243
+ }
244
+ pinStrategy(name) {
245
+ this.configService.ensureConfigFileExists();
246
+ const config = this.configService.readConfigOrDefault();
247
+ (0, strategy_config_1.requireStrategyInstalled)(config, name);
248
+ const file = this.readStrategy(name);
249
+ const units = (0, strategy_normalize_1.normalizeStrategyUnits)(file);
250
+ const installed = this.unitStore.readInstalledDependencyVersions();
251
+ const pinnedUnits = units.map((u) => {
252
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(u.ref);
253
+ const v = installed[pinned.name];
254
+ if (!v) {
255
+ throw new Error(`Unit package is not installed: ${pinned.name}`);
256
+ }
257
+ const nextRef = `${pinned.name}@${v}`;
258
+ return u.args.length === 0 ? nextRef : { ref: nextRef, args: u.args };
259
+ });
260
+ const others = (0, strategy_config_1.listStrategyNamesFromConfig)(config)
261
+ .filter((n) => n !== name)
262
+ .map((n) => ({ name: n, units: (0, strategy_normalize_1.normalizeStrategyUnits)(this.readStrategy(n)) }));
263
+ const nextWanted = (0, strategy_analyze_1.collectWantedVersions)([
264
+ ...others,
265
+ {
266
+ name,
267
+ units: pinnedUnits.map((x) => typeof x === "string" ? { ref: x, args: [] } : { ref: x.ref, args: x.args ?? [] })
268
+ }
269
+ ]);
270
+ const conflicts = (0, strategy_analyze_1.computeConflicts)(nextWanted);
271
+ if (conflicts.length > 0) {
272
+ throw new Error(`Strategy version conflicts detected (${conflicts.length}). Run 'pp strategy doctor' for details.`);
273
+ }
274
+ const nextFile = {
275
+ ...file,
276
+ pipeline: {
277
+ ...file.pipeline,
278
+ units: pinnedUnits
279
+ }
280
+ };
281
+ (0, strategy_io_1.writeJsonAtomic)(this.getStrategyPath(name), nextFile);
282
+ }
283
+ doctor() {
284
+ this.configService.ensureConfigFileExists();
285
+ const config = this.configService.readConfigOrDefault();
286
+ const names = (0, strategy_config_1.listStrategyNamesFromConfig)(config);
287
+ const installed = this.unitStore.readInstalledDependencyVersions();
288
+ const strategies = names.map((n) => ({
289
+ name: n,
290
+ units: (0, strategy_normalize_1.normalizeStrategyUnits)(this.readStrategy(n))
291
+ }));
292
+ const wanted = (0, strategy_analyze_1.collectWantedVersions)(strategies);
293
+ const conflicts = (0, strategy_analyze_1.computeConflicts)(wanted);
294
+ const drift = strategies
295
+ .map((s) => (0, strategy_analyze_1.computeDrift)({ strategyName: s.name, units: s.units, installed }))
296
+ .filter((d) => d.items.length > 0);
297
+ return { conflicts, drift };
298
+ }
299
+ }
300
+ exports.StrategyService = StrategyService;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnitStore = void 0;
4
+ const user_package_installer_1 = require("../user-packages/user-package-installer");
5
+ const parse_pinned_spec_1 = require("../user-packages/parse-pinned-spec");
6
+ const user_package_store_1 = require("../user-packages/user-package-store");
7
+ const unit_validate_1 = require("./unit-validate");
8
+ const UNITS_KIND = "units";
9
+ const UNITS_PACKAGE_JSON_NAME = "pagepocket-user-units";
10
+ class UnitStore {
11
+ constructor(configService) {
12
+ this.store = new user_package_store_1.UserPackageStore(configService, UNITS_KIND);
13
+ }
14
+ getInstallDir() {
15
+ return this.store.getInstallDir();
16
+ }
17
+ readInstalledDependencyVersions() {
18
+ return this.store.readInstalledDependencyVersions(UNITS_PACKAGE_JSON_NAME);
19
+ }
20
+ readInstalledPackageMeta(packageName) {
21
+ return this.store.readInstalledPackageMeta(UNITS_PACKAGE_JSON_NAME, packageName);
22
+ }
23
+ installPinned(ref) {
24
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(ref);
25
+ (0, user_package_installer_1.installPinnedPackage)(this.store, {
26
+ packageJsonName: UNITS_PACKAGE_JSON_NAME,
27
+ packageSpec: pinned.spec
28
+ });
29
+ }
30
+ updateToLatest(packageName) {
31
+ (0, user_package_installer_1.updatePackageToLatest)(this.store, {
32
+ packageJsonName: UNITS_PACKAGE_JSON_NAME,
33
+ packageName
34
+ });
35
+ }
36
+ async importUnitModule(packageName) {
37
+ return this.store.importModule(UNITS_PACKAGE_JSON_NAME, packageName);
38
+ }
39
+ async instantiateFromRef(ref, args) {
40
+ const pinned = (0, parse_pinned_spec_1.parsePinnedSpec)(ref);
41
+ const mod = await this.importUnitModule(pinned.name);
42
+ const def = mod.default;
43
+ if (typeof def !== "function") {
44
+ throw new Error(`Unit ${pinned.name} must default export a constructor.`);
45
+ }
46
+ const ctor = def;
47
+ const instance = new ctor(...args);
48
+ if (!(0, unit_validate_1.isUnitLike)(instance)) {
49
+ throw new Error(`Unit ${pinned.name} default export did not construct a Unit.`);
50
+ }
51
+ return instance;
52
+ }
53
+ }
54
+ exports.UnitStore = UnitStore;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUnitLike = void 0;
4
+ const isRecord = (value) => {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ };
7
+ const isCallable = (value) => {
8
+ return typeof value === "function";
9
+ };
10
+ const isUnitLike = (value) => {
11
+ if (!isRecord(value)) {
12
+ return false;
13
+ }
14
+ if (typeof value.id !== "string" || value.id.trim().length === 0) {
15
+ return false;
16
+ }
17
+ if (typeof value.kind !== "string" || value.kind.trim().length === 0) {
18
+ return false;
19
+ }
20
+ if (!isCallable(value.run)) {
21
+ return false;
22
+ }
23
+ if (!isCallable(value.merge)) {
24
+ return false;
25
+ }
26
+ return true;
27
+ };
28
+ exports.isUnitLike = isUnitLike;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePinnedSpec = void 0;
4
+ const isRecord = (value) => {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ };
7
+ const isExactSemver = (value) => {
8
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
9
+ };
10
+ const parsePinnedSpec = (input) => {
11
+ const trimmed = input.trim();
12
+ if (!trimmed) {
13
+ throw new Error("package spec is empty");
14
+ }
15
+ const npa = require("npm-package-arg");
16
+ const parsed = npa(trimmed);
17
+ if (!isRecord(parsed)) {
18
+ throw new Error(`Invalid package spec: ${trimmed}`);
19
+ }
20
+ const name = parsed.name;
21
+ if (typeof name !== "string" || name.trim().length === 0) {
22
+ throw new Error(`Invalid package spec (missing name): ${trimmed}`);
23
+ }
24
+ const type = parsed.type;
25
+ if (type !== "version") {
26
+ throw new Error(`Package spec must pin an exact version (got ${String(type)}): ${trimmed}`);
27
+ }
28
+ const rawSpec = parsed.rawSpec;
29
+ if (typeof rawSpec !== "string" || rawSpec.trim().length === 0) {
30
+ throw new Error(`Package spec must pin an exact version: ${trimmed}`);
31
+ }
32
+ if (!isExactSemver(rawSpec)) {
33
+ throw new Error(`Package spec must pin an exact version (got ${rawSpec}): ${trimmed}`);
34
+ }
35
+ return { name, version: rawSpec, spec: `${name}@${rawSpec}` };
36
+ };
37
+ exports.parsePinnedSpec = parsePinnedSpec;