@pagepocket/cli 0.11.1 → 0.13.0
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/dist/commands/archive.js +46 -161
- package/dist/commands/plugin/set.js +1 -3
- package/dist/commands/strategy/doctor.js +6 -6
- package/dist/commands/strategy/ls.js +32 -8
- package/dist/commands/view.js +1 -1
- package/dist/services/config-service.js +8 -8
- package/dist/services/plugin-installer.js +10 -10
- package/dist/services/plugin-store.js +11 -13
- package/dist/services/strategy/builtin-strategy-registry.js +12 -21
- package/dist/services/strategy/strategy-analyze.js +6 -6
- package/dist/services/strategy/strategy-config.js +4 -4
- package/dist/services/strategy/strategy-fetch.js +2 -4
- package/dist/services/strategy/strategy-io.js +3 -9
- package/dist/services/strategy/strategy-normalize.js +43 -7
- package/dist/services/strategy/strategy-pack-read.js +2 -4
- package/dist/services/strategy/strategy-pack-store.js +1 -2
- package/dist/services/strategy/strategy-service.js +63 -54
- package/dist/services/units/unit-store.js +1 -1
- package/dist/services/units/unit-validate.js +4 -10
- package/dist/services/user-packages/parse-pinned-spec.js +3 -7
- package/dist/services/user-packages/user-package-installer.js +5 -5
- package/dist/services/user-packages/user-package-store.js +8 -8
- package/dist/units/network-observer-unit.js +1 -1
- package/dist/utils/archive-strategy.js +138 -0
- package/dist/utils/array.js +1 -3
- package/dist/utils/parse-plugin-spec.js +1 -1
- package/dist/vendor/content-reader.css +1 -0
- package/dist/vendor/content-reader.iife.js +60 -0
- package/dist/view-main-content.js +40 -0
- package/dist/view.js +71 -33
- package/package.json +21 -19
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
|
|
2
|
-
const isRecord = (value) =>
|
|
3
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4
|
-
};
|
|
2
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5
3
|
const isJsonValue = (value) => {
|
|
6
4
|
if (value === null) {
|
|
7
5
|
return true;
|
|
8
6
|
}
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
7
|
+
const valueType = typeof value;
|
|
8
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean") {
|
|
11
9
|
return true;
|
|
12
10
|
}
|
|
13
11
|
if (Array.isArray(value)) {
|
|
14
|
-
return value.every((
|
|
12
|
+
return value.every((item) => isJsonValue(item));
|
|
15
13
|
}
|
|
16
14
|
if (isRecord(value)) {
|
|
17
|
-
return Object.values(value).every((
|
|
15
|
+
return Object.values(value).every((entryValue) => isJsonValue(entryValue));
|
|
18
16
|
}
|
|
19
17
|
return false;
|
|
20
18
|
};
|
|
@@ -45,3 +43,41 @@ export const normalizeStrategyUnits = (strategy) => {
|
|
|
45
43
|
const units = strategy.pipeline.units;
|
|
46
44
|
return units.map((spec, idx) => normalizeUnitSpec(spec, idx));
|
|
47
45
|
};
|
|
46
|
+
const parsePackageName = (spec) => {
|
|
47
|
+
const trimmed = spec.trim();
|
|
48
|
+
if (trimmed.startsWith("@")) {
|
|
49
|
+
const afterScope = trimmed.indexOf("/");
|
|
50
|
+
if (afterScope === -1) {
|
|
51
|
+
throw new Error(`Invalid scoped package spec: ${trimmed}`);
|
|
52
|
+
}
|
|
53
|
+
const versionSep = trimmed.indexOf("@", afterScope + 1);
|
|
54
|
+
return versionSep === -1 ? trimmed : trimmed.slice(0, versionSep);
|
|
55
|
+
}
|
|
56
|
+
const versionSep = trimmed.indexOf("@");
|
|
57
|
+
return versionSep === -1 ? trimmed : trimmed.slice(0, versionSep);
|
|
58
|
+
};
|
|
59
|
+
const normalizeBuiltinUnitSpec = (spec, idx) => {
|
|
60
|
+
if (typeof spec === "string") {
|
|
61
|
+
return { name: parsePackageName(spec), args: [] };
|
|
62
|
+
}
|
|
63
|
+
if (!isRecord(spec)) {
|
|
64
|
+
throw new Error(`Invalid unit spec at index ${idx}`);
|
|
65
|
+
}
|
|
66
|
+
const ref = spec.ref;
|
|
67
|
+
if (typeof ref !== "string") {
|
|
68
|
+
throw new Error(`Invalid unit ref at index ${idx}`);
|
|
69
|
+
}
|
|
70
|
+
const argsRaw = spec.args;
|
|
71
|
+
const args = typeof argsRaw === "undefined" ? [] : argsRaw;
|
|
72
|
+
if (!Array.isArray(args)) {
|
|
73
|
+
throw new Error(`Invalid unit args at index ${idx} (must be an array)`);
|
|
74
|
+
}
|
|
75
|
+
if (!isJsonValue(args)) {
|
|
76
|
+
throw new Error(`Invalid unit args at index ${idx} (must be JSON-only)`);
|
|
77
|
+
}
|
|
78
|
+
return { name: parsePackageName(ref), args };
|
|
79
|
+
};
|
|
80
|
+
export const normalizeBuiltinStrategyUnits = (strategy) => {
|
|
81
|
+
const units = strategy.pipeline.units;
|
|
82
|
+
return units.map((spec, idx) => normalizeBuiltinUnitSpec(spec, idx));
|
|
83
|
+
};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseJson } from "../../utils/parse-json.js";
|
|
4
|
-
const isRecord = (value) =>
|
|
5
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
-
};
|
|
4
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
5
|
const isStrategyFile = (value) => {
|
|
8
6
|
if (!isRecord(value)) {
|
|
9
7
|
return false;
|
|
@@ -30,7 +28,7 @@ export const readStrategiesFromPackRoot = (packRoot) => {
|
|
|
30
28
|
}
|
|
31
29
|
const fileNames = fs
|
|
32
30
|
.readdirSync(strategiesDir)
|
|
33
|
-
.filter((
|
|
31
|
+
.filter((fileName) => fileName.endsWith(".strategy.json"))
|
|
34
32
|
.sort((a, b) => a.localeCompare(b));
|
|
35
33
|
return fileNames.map((fileName) => {
|
|
36
34
|
const filePath = path.join(strategiesDir, fileName);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { installPinnedPackage } from "../user-packages/user-package-installer.js";
|
|
3
|
-
import { updatePackageToLatest } from "../user-packages/user-package-installer.js";
|
|
4
2
|
import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
|
|
3
|
+
import { installPinnedPackage, updatePackageToLatest } from "../user-packages/user-package-installer.js";
|
|
5
4
|
import { UserPackageStore } from "../user-packages/user-package-store.js";
|
|
6
5
|
const STRATEGY_PACKS_KIND = "strategy-packs";
|
|
7
6
|
const STRATEGY_PACKS_PACKAGE_JSON_NAME = "pagepocket-user-strategy-packs";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { uniq } from "../../utils/array.js";
|
|
2
3
|
import { ConfigService } from "../config-service.js";
|
|
3
|
-
import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
|
|
4
4
|
import { UnitStore } from "../units/unit-store.js";
|
|
5
|
-
import {
|
|
5
|
+
import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
|
|
6
6
|
import { collectWantedVersions, computeConflicts, computeDrift, ensureNoInstalledVersionConflicts } from "./strategy-analyze.js";
|
|
7
7
|
import { listStrategyNamesFromConfig, requireStrategyInstalled, withStrategyInConfig, withoutStrategyInConfig } from "./strategy-config.js";
|
|
8
8
|
import { fetchStrategyFile } from "./strategy-fetch.js";
|
|
@@ -55,8 +55,8 @@ export class StrategyService {
|
|
|
55
55
|
if (strategies.length === 0) {
|
|
56
56
|
throw new Error(`No *.strategy.json found in package: ${pinned.spec}`);
|
|
57
57
|
}
|
|
58
|
-
const allUnits = strategies.flatMap((
|
|
59
|
-
const refs = uniq(allUnits.map((
|
|
58
|
+
const allUnits = strategies.flatMap((strategy) => normalizeStrategyUnits(strategy));
|
|
59
|
+
const refs = uniq(allUnits.map((unit) => unit.ref));
|
|
60
60
|
ensureNoInstalledVersionConflicts(this.unitStore.readInstalledDependencyVersions(), refs);
|
|
61
61
|
refs.forEach((ref) => this.unitStore.installPinned(ref));
|
|
62
62
|
const installedStrategies = [];
|
|
@@ -73,13 +73,13 @@ export class StrategyService {
|
|
|
73
73
|
writeJsonAtomic(strategyPath, toWrite);
|
|
74
74
|
installedStrategies.push(name);
|
|
75
75
|
});
|
|
76
|
-
const nextConfig = installedStrategies.reduce((
|
|
76
|
+
const nextConfig = installedStrategies.reduce((configAccumulator, strategyName) => withStrategyInConfig(configAccumulator, strategyName), config);
|
|
77
77
|
const existing = nextConfig.strategyPacks ?? [];
|
|
78
|
-
const filtered = existing.filter((
|
|
79
|
-
if (typeof
|
|
80
|
-
return
|
|
78
|
+
const filtered = existing.filter((packEntry) => {
|
|
79
|
+
if (typeof packEntry === "string") {
|
|
80
|
+
return packEntry.trim().length > 0;
|
|
81
81
|
}
|
|
82
|
-
return
|
|
82
|
+
return packEntry.name !== pinned.name;
|
|
83
83
|
});
|
|
84
84
|
const strategyPacks = [
|
|
85
85
|
...filtered,
|
|
@@ -93,7 +93,7 @@ export class StrategyService {
|
|
|
93
93
|
}
|
|
94
94
|
const { strategy, source } = await fetchStrategyFile(sourceTrimmed);
|
|
95
95
|
const normalizedUnits = normalizeStrategyUnits(strategy);
|
|
96
|
-
const refs = uniq(normalizedUnits.map((
|
|
96
|
+
const refs = uniq(normalizedUnits.map((unit) => unit.ref));
|
|
97
97
|
const strategyPath = this.getStrategyPath(strategy.name);
|
|
98
98
|
if (!input.force && fs.existsSync(strategyPath)) {
|
|
99
99
|
throw new Error(`Strategy already exists: ${strategy.name}`);
|
|
@@ -128,15 +128,15 @@ export class StrategyService {
|
|
|
128
128
|
const installed = this.unitStore.readInstalledDependencyVersions();
|
|
129
129
|
if (opts?.packageOnly) {
|
|
130
130
|
const packageNames = uniq(names
|
|
131
|
-
.flatMap((
|
|
132
|
-
.filter((
|
|
131
|
+
.flatMap((strategyName) => normalizeStrategyUnits(this.readStrategy(strategyName)).map((unit) => parsePinnedSpec(unit.ref).name))
|
|
132
|
+
.filter((packageName) => packageName.trim().length > 0));
|
|
133
133
|
packageNames.forEach((pkg) => {
|
|
134
134
|
this.unitStore.updateToLatest(pkg);
|
|
135
135
|
});
|
|
136
136
|
const afterInstalled = this.unitStore.readInstalledDependencyVersions();
|
|
137
|
-
const afterStrategies = listStrategyNamesFromConfig(config).map((
|
|
138
|
-
name:
|
|
139
|
-
units: normalizeStrategyUnits(this.readStrategy(
|
|
137
|
+
const afterStrategies = listStrategyNamesFromConfig(config).map((strategyName) => ({
|
|
138
|
+
name: strategyName,
|
|
139
|
+
units: normalizeStrategyUnits(this.readStrategy(strategyName))
|
|
140
140
|
}));
|
|
141
141
|
const wanted = collectWantedVersions(afterStrategies);
|
|
142
142
|
const conflicts = computeConflicts(wanted);
|
|
@@ -144,8 +144,12 @@ export class StrategyService {
|
|
|
144
144
|
throw new Error(`Strategy version conflicts detected (${conflicts.length}). Run 'pp strategy doctor' for details.`);
|
|
145
145
|
}
|
|
146
146
|
const drift = afterStrategies
|
|
147
|
-
.map((
|
|
148
|
-
|
|
147
|
+
.map((strategy) => computeDrift({
|
|
148
|
+
strategyName: strategy.name,
|
|
149
|
+
units: strategy.units,
|
|
150
|
+
installed: afterInstalled
|
|
151
|
+
}))
|
|
152
|
+
.filter((drift) => drift.items.length > 0);
|
|
149
153
|
if (drift.length > 0) {
|
|
150
154
|
throw new Error("Strategy drift detected after --package-only update. Run 'pp strategy doctor'.");
|
|
151
155
|
}
|
|
@@ -153,23 +157,23 @@ export class StrategyService {
|
|
|
153
157
|
}
|
|
154
158
|
const nextStrategies = [];
|
|
155
159
|
const npmPacksToUpdate = new Set();
|
|
156
|
-
for (const
|
|
157
|
-
const current = this.readStrategy(
|
|
160
|
+
for (const strategyName of names) {
|
|
161
|
+
const current = this.readStrategy(strategyName);
|
|
158
162
|
const src = current.source;
|
|
159
163
|
if (!src) {
|
|
160
|
-
throw new Error(`Strategy ${
|
|
164
|
+
throw new Error(`Strategy ${strategyName} has no source. Re-add it with a URL/path source to enable update.`);
|
|
161
165
|
}
|
|
162
166
|
if (src.type === "npm") {
|
|
163
167
|
npmPacksToUpdate.add(src.value);
|
|
164
168
|
continue;
|
|
165
169
|
}
|
|
166
170
|
const fetched = await fetchStrategyFile(src.value);
|
|
167
|
-
if (fetched.strategy.name !==
|
|
168
|
-
throw new Error(`Strategy name mismatch while updating ${
|
|
171
|
+
if (fetched.strategy.name !== strategyName) {
|
|
172
|
+
throw new Error(`Strategy name mismatch while updating ${strategyName}: got ${fetched.strategy.name}`);
|
|
169
173
|
}
|
|
170
174
|
const file = { ...fetched.strategy, source: fetched.source };
|
|
171
175
|
const units = normalizeStrategyUnits(file);
|
|
172
|
-
nextStrategies.push({ name:
|
|
176
|
+
nextStrategies.push({ name: strategyName, file, units });
|
|
173
177
|
}
|
|
174
178
|
if (npmPacksToUpdate.size > 0) {
|
|
175
179
|
const specs = [...npmPacksToUpdate];
|
|
@@ -180,18 +184,18 @@ export class StrategyService {
|
|
|
180
184
|
const installedPackVersions = this.packStore.readInstalledDependencyVersions();
|
|
181
185
|
const updatedSpecs = specs.map((spec) => {
|
|
182
186
|
const pinned = parsePinnedSpec(spec);
|
|
183
|
-
const
|
|
184
|
-
if (!
|
|
187
|
+
const installedVersion = installedPackVersions[pinned.name];
|
|
188
|
+
if (!installedVersion) {
|
|
185
189
|
throw new Error(`Strategy pack not installed after update: ${pinned.name}`);
|
|
186
190
|
}
|
|
187
|
-
return `${pinned.name}@${
|
|
191
|
+
return `${pinned.name}@${installedVersion}`;
|
|
188
192
|
});
|
|
189
193
|
const updatedFiles = [];
|
|
190
194
|
updatedSpecs.forEach((spec) => {
|
|
191
195
|
const pinned = parsePinnedSpec(spec);
|
|
192
196
|
const root = this.packStore.resolvePackRoot(pinned.name);
|
|
193
|
-
const files = readStrategiesFromPackRoot(root).map((
|
|
194
|
-
...
|
|
197
|
+
const files = readStrategiesFromPackRoot(root).map((strategyFile) => ({
|
|
198
|
+
...strategyFile,
|
|
195
199
|
source: { type: "npm", value: spec }
|
|
196
200
|
}));
|
|
197
201
|
updatedFiles.push(...files);
|
|
@@ -202,11 +206,11 @@ export class StrategyService {
|
|
|
202
206
|
});
|
|
203
207
|
const nextConfig = this.configService.readConfigOrDefault();
|
|
204
208
|
const existing = nextConfig.strategyPacks ?? [];
|
|
205
|
-
const filtered = existing.filter((
|
|
206
|
-
if (typeof
|
|
207
|
-
return
|
|
209
|
+
const filtered = existing.filter((packEntry) => {
|
|
210
|
+
if (typeof packEntry === "string") {
|
|
211
|
+
return packEntry.trim().length > 0;
|
|
208
212
|
}
|
|
209
|
-
return !updatedSpecs.some((spec) => parsePinnedSpec(spec).name ===
|
|
213
|
+
return !updatedSpecs.some((spec) => parsePinnedSpec(spec).name === packEntry.name);
|
|
210
214
|
});
|
|
211
215
|
const packsNext = [
|
|
212
216
|
...filtered,
|
|
@@ -217,26 +221,26 @@ export class StrategyService {
|
|
|
217
221
|
];
|
|
218
222
|
this.configService.writeConfig({ ...nextConfig, strategyPacks: packsNext });
|
|
219
223
|
}
|
|
220
|
-
const allOtherNames = listStrategyNamesFromConfig(config).filter((
|
|
221
|
-
const otherStrategies = allOtherNames.map((
|
|
222
|
-
name:
|
|
223
|
-
units: normalizeStrategyUnits(this.readStrategy(
|
|
224
|
+
const allOtherNames = listStrategyNamesFromConfig(config).filter((strategyName) => !names.includes(strategyName));
|
|
225
|
+
const otherStrategies = allOtherNames.map((strategyName) => ({
|
|
226
|
+
name: strategyName,
|
|
227
|
+
units: normalizeStrategyUnits(this.readStrategy(strategyName))
|
|
224
228
|
}));
|
|
225
229
|
const wanted = collectWantedVersions([
|
|
226
230
|
...otherStrategies,
|
|
227
|
-
...nextStrategies.map((
|
|
231
|
+
...nextStrategies.map((strategy) => ({ name: strategy.name, units: strategy.units }))
|
|
228
232
|
]);
|
|
229
233
|
const conflicts = computeConflicts(wanted);
|
|
230
234
|
if (conflicts.length > 0) {
|
|
231
235
|
throw new Error(`Strategy version conflicts detected (${conflicts.length}). Run 'pp strategy doctor' for details.`);
|
|
232
236
|
}
|
|
233
|
-
const refs = uniq(nextStrategies.flatMap((
|
|
237
|
+
const refs = uniq(nextStrategies.flatMap((strategy) => strategy.units.map((unit) => unit.ref)));
|
|
234
238
|
ensureNoInstalledVersionConflicts(installed, refs);
|
|
235
239
|
refs.forEach((ref) => {
|
|
236
240
|
this.unitStore.installPinned(ref);
|
|
237
241
|
});
|
|
238
|
-
nextStrategies.forEach((
|
|
239
|
-
writeJsonAtomic(this.getStrategyPath(
|
|
242
|
+
nextStrategies.forEach((strategy) => {
|
|
243
|
+
writeJsonAtomic(this.getStrategyPath(strategy.name), strategy.file);
|
|
240
244
|
});
|
|
241
245
|
}
|
|
242
246
|
pinStrategy(name) {
|
|
@@ -246,23 +250,28 @@ export class StrategyService {
|
|
|
246
250
|
const file = this.readStrategy(name);
|
|
247
251
|
const units = normalizeStrategyUnits(file);
|
|
248
252
|
const installed = this.unitStore.readInstalledDependencyVersions();
|
|
249
|
-
const pinnedUnits = units.map((
|
|
250
|
-
const pinned = parsePinnedSpec(
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
253
|
+
const pinnedUnits = units.map((unit) => {
|
|
254
|
+
const pinned = parsePinnedSpec(unit.ref);
|
|
255
|
+
const installedVersion = installed[pinned.name];
|
|
256
|
+
if (!installedVersion) {
|
|
253
257
|
throw new Error(`Unit package is not installed: ${pinned.name}`);
|
|
254
258
|
}
|
|
255
|
-
const nextRef = `${pinned.name}@${
|
|
256
|
-
return
|
|
259
|
+
const nextRef = `${pinned.name}@${installedVersion}`;
|
|
260
|
+
return unit.args.length === 0 ? nextRef : { ref: nextRef, args: unit.args };
|
|
257
261
|
});
|
|
258
262
|
const others = listStrategyNamesFromConfig(config)
|
|
259
|
-
.filter((
|
|
260
|
-
.map((
|
|
263
|
+
.filter((strategyName) => strategyName !== name)
|
|
264
|
+
.map((strategyName) => ({
|
|
265
|
+
name: strategyName,
|
|
266
|
+
units: normalizeStrategyUnits(this.readStrategy(strategyName))
|
|
267
|
+
}));
|
|
261
268
|
const nextWanted = collectWantedVersions([
|
|
262
269
|
...others,
|
|
263
270
|
{
|
|
264
271
|
name,
|
|
265
|
-
units: pinnedUnits.map((
|
|
272
|
+
units: pinnedUnits.map((pinnedUnit) => typeof pinnedUnit === "string"
|
|
273
|
+
? { ref: pinnedUnit, args: [] }
|
|
274
|
+
: { ref: pinnedUnit.ref, args: pinnedUnit.args ?? [] })
|
|
266
275
|
}
|
|
267
276
|
]);
|
|
268
277
|
const conflicts = computeConflicts(nextWanted);
|
|
@@ -283,15 +292,15 @@ export class StrategyService {
|
|
|
283
292
|
const config = this.configService.readConfigOrDefault();
|
|
284
293
|
const names = listStrategyNamesFromConfig(config);
|
|
285
294
|
const installed = this.unitStore.readInstalledDependencyVersions();
|
|
286
|
-
const strategies = names.map((
|
|
287
|
-
name:
|
|
288
|
-
units: normalizeStrategyUnits(this.readStrategy(
|
|
295
|
+
const strategies = names.map((strategyName) => ({
|
|
296
|
+
name: strategyName,
|
|
297
|
+
units: normalizeStrategyUnits(this.readStrategy(strategyName))
|
|
289
298
|
}));
|
|
290
299
|
const wanted = collectWantedVersions(strategies);
|
|
291
300
|
const conflicts = computeConflicts(wanted);
|
|
292
301
|
const drift = strategies
|
|
293
|
-
.map((
|
|
294
|
-
.filter((
|
|
302
|
+
.map((strategy) => computeDrift({ strategyName: strategy.name, units: strategy.units, installed }))
|
|
303
|
+
.filter((drift) => drift.items.length > 0);
|
|
295
304
|
return { conflicts, drift };
|
|
296
305
|
}
|
|
297
306
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { installPinnedPackage, updatePackageToLatest } from "../user-packages/user-package-installer.js";
|
|
2
1
|
import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
|
|
2
|
+
import { installPinnedPackage, updatePackageToLatest } from "../user-packages/user-package-installer.js";
|
|
3
3
|
import { UserPackageStore } from "../user-packages/user-package-store.js";
|
|
4
4
|
import { isUnitLike, resolveUnitConstructor } from "./unit-validate.js";
|
|
5
5
|
const UNITS_KIND = "units";
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
const isRecord = (value) =>
|
|
2
|
-
|
|
3
|
-
};
|
|
4
|
-
const isCallable = (value) => {
|
|
5
|
-
return typeof value === "function";
|
|
6
|
-
};
|
|
1
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2
|
+
const isCallable = (value) => typeof value === "function";
|
|
7
3
|
export const isUnitLike = (value) => {
|
|
8
4
|
if (!isRecord(value)) {
|
|
9
5
|
return false;
|
|
@@ -11,9 +7,6 @@ export const isUnitLike = (value) => {
|
|
|
11
7
|
if (typeof value.id !== "string" || value.id.trim().length === 0) {
|
|
12
8
|
return false;
|
|
13
9
|
}
|
|
14
|
-
if (typeof value.kind !== "string" || value.kind.trim().length === 0) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
10
|
if (!isCallable(value.run)) {
|
|
18
11
|
return false;
|
|
19
12
|
}
|
|
@@ -35,7 +28,8 @@ export const resolveUnitConstructor = (mod) => {
|
|
|
35
28
|
if (typeof mod.default === "function") {
|
|
36
29
|
return mod.default;
|
|
37
30
|
}
|
|
38
|
-
const fallback = Object.values(mod).find((
|
|
31
|
+
const fallback = Object.values(mod).find((exportedValue) => typeof exportedValue === "function" &&
|
|
32
|
+
exportedValue.name?.endsWith("Unit"));
|
|
39
33
|
if (typeof fallback === "function") {
|
|
40
34
|
return fallback;
|
|
41
35
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
const isRecord = (value) =>
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
const isExactSemver = (value) => {
|
|
8
|
-
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
|
|
9
|
-
};
|
|
4
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
const isExactSemver = (value) => /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
|
|
10
6
|
export const parsePinnedSpec = (input) => {
|
|
11
7
|
const trimmed = input.trim();
|
|
12
8
|
if (!trimmed) {
|
|
@@ -6,16 +6,16 @@ const toError = (value) => {
|
|
|
6
6
|
return new Error(String(value));
|
|
7
7
|
};
|
|
8
8
|
const tryRun = (cwd, cmd, args) => {
|
|
9
|
-
const
|
|
9
|
+
const result = spawnSync(cmd, args, {
|
|
10
10
|
cwd,
|
|
11
11
|
stdio: "inherit",
|
|
12
12
|
shell: false
|
|
13
13
|
});
|
|
14
|
-
if (
|
|
15
|
-
return { ok: false, error: toError(
|
|
14
|
+
if (result.error) {
|
|
15
|
+
return { ok: false, error: toError(result.error) };
|
|
16
16
|
}
|
|
17
|
-
if (typeof
|
|
18
|
-
return { ok: false, error: new Error(`${cmd} exited with code ${
|
|
17
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
18
|
+
return { ok: false, error: new Error(`${cmd} exited with code ${result.status}`) };
|
|
19
19
|
}
|
|
20
20
|
return { ok: true };
|
|
21
21
|
};
|
|
@@ -3,14 +3,12 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { parseJson } from "../../utils/parse-json.js";
|
|
6
|
-
const isRecord = (value) =>
|
|
7
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8
|
-
};
|
|
6
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
7
|
const isStringRecord = (value) => {
|
|
10
8
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11
9
|
return false;
|
|
12
10
|
}
|
|
13
|
-
return Object.values(value).every((
|
|
11
|
+
return Object.values(value).every((entryValue) => typeof entryValue === "string");
|
|
14
12
|
};
|
|
15
13
|
export class UserPackageStore {
|
|
16
14
|
configService;
|
|
@@ -55,11 +53,13 @@ export class UserPackageStore {
|
|
|
55
53
|
if (!json.ok || !isRecord(json.value)) {
|
|
56
54
|
return { name: packageName };
|
|
57
55
|
}
|
|
58
|
-
const
|
|
56
|
+
const packageJsonRecord = json.value;
|
|
59
57
|
return {
|
|
60
|
-
name: typeof
|
|
61
|
-
description: typeof
|
|
62
|
-
|
|
58
|
+
name: typeof packageJsonRecord.name === "string" ? packageJsonRecord.name : packageName,
|
|
59
|
+
description: typeof packageJsonRecord.description === "string"
|
|
60
|
+
? packageJsonRecord.description
|
|
61
|
+
: undefined,
|
|
62
|
+
version: typeof packageJsonRecord.version === "string" ? packageJsonRecord.version : undefined
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
catch {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { readBuiltinStrategy, listBuiltinStrategyNames } from "../services/strategy/builtin-strategy-registry.js";
|
|
4
|
+
import { normalizeBuiltinStrategyUnits, normalizeStrategyUnits } from "../services/strategy/strategy-normalize.js";
|
|
5
|
+
import { StrategyService } from "../services/strategy/strategy-service.js";
|
|
6
|
+
import { UnitStore } from "../services/units/unit-store.js";
|
|
7
|
+
import { isUnitLike, resolveUnitConstructor } from "../services/units/unit-validate.js";
|
|
8
|
+
import { parsePinnedSpec } from "../services/user-packages/parse-pinned-spec.js";
|
|
9
|
+
import { uniq } from "./array.js";
|
|
10
|
+
const buildMissingStrategyError = (input) => {
|
|
11
|
+
const available = uniq([...input.installedNames, ...listBuiltinStrategyNames()])
|
|
12
|
+
.filter((name) => name.trim().length > 0)
|
|
13
|
+
.sort((left, right) => left.localeCompare(right));
|
|
14
|
+
const suffix = available.length > 0
|
|
15
|
+
? ` Available strategies: ${available.join(", ")}`
|
|
16
|
+
: " No strategies found.";
|
|
17
|
+
return new Error(`Strategy not found: ${input.strategyName}.${suffix}`);
|
|
18
|
+
};
|
|
19
|
+
const assertNoDuplicateUnitIds = (input) => {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const duplicateUnit = input.units.find((unit) => {
|
|
22
|
+
if (seen.has(unit.id)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
seen.add(unit.id);
|
|
26
|
+
return false;
|
|
27
|
+
});
|
|
28
|
+
if (duplicateUnit) {
|
|
29
|
+
throw new Error(`Duplicate unit id detected in strategy ${input.strategyName}: ${duplicateUnit.id}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const loadInstalledStrategyUnits = async (input) => {
|
|
33
|
+
const strategyService = new StrategyService(input.configService);
|
|
34
|
+
const unitStore = new UnitStore(input.configService);
|
|
35
|
+
strategyService.ensureConfigFileExists();
|
|
36
|
+
const strategyFile = strategyService.readStrategy(input.strategyName);
|
|
37
|
+
const normalized = normalizeStrategyUnits(strategyFile);
|
|
38
|
+
const installed = unitStore.readInstalledDependencyVersions();
|
|
39
|
+
const drift = normalized.flatMap((unit) => {
|
|
40
|
+
const pinned = parsePinnedSpec(unit.ref);
|
|
41
|
+
const installedVersion = installed[pinned.name];
|
|
42
|
+
if (!installedVersion || installedVersion !== pinned.version) {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
name: pinned.name,
|
|
46
|
+
pinned: pinned.version,
|
|
47
|
+
installed: installedVersion
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
});
|
|
53
|
+
if (drift.length > 0) {
|
|
54
|
+
const details = drift
|
|
55
|
+
.map((driftItem) => {
|
|
56
|
+
const installedText = typeof driftItem.installed === "string" ? driftItem.installed : "(not installed)";
|
|
57
|
+
return `${driftItem.name}: pinned ${driftItem.pinned}, installed ${installedText}`;
|
|
58
|
+
})
|
|
59
|
+
.join("; ");
|
|
60
|
+
throw new Error(`Strategy drift detected (${input.strategyName}). ${details}. ` +
|
|
61
|
+
`Fix: pp strategy update ${input.strategyName} OR pp strategy pin ${input.strategyName}`);
|
|
62
|
+
}
|
|
63
|
+
const units = await Promise.all(normalized.map(async (unit) => unitStore.instantiateFromRef(unit.ref, unit.args)));
|
|
64
|
+
assertNoDuplicateUnitIds({
|
|
65
|
+
strategyName: input.strategyName,
|
|
66
|
+
units
|
|
67
|
+
});
|
|
68
|
+
return units;
|
|
69
|
+
};
|
|
70
|
+
const instantiateBuiltinUnit = async (input) => {
|
|
71
|
+
const req = createRequire(import.meta.url);
|
|
72
|
+
const resolved = req.resolve(input.name);
|
|
73
|
+
const mod = (await import(pathToFileURL(resolved).href));
|
|
74
|
+
const ctor = resolveUnitConstructor(mod);
|
|
75
|
+
if (!ctor) {
|
|
76
|
+
throw new Error(`Unit ${input.name} does not export a Unit constructor.`);
|
|
77
|
+
}
|
|
78
|
+
const instance = new ctor(...input.args);
|
|
79
|
+
if (!isUnitLike(instance)) {
|
|
80
|
+
throw new Error(`Unit ${input.name} exported constructor did not produce a Unit.`);
|
|
81
|
+
}
|
|
82
|
+
return instance;
|
|
83
|
+
};
|
|
84
|
+
const loadBuiltinStrategyUnits = async (input) => {
|
|
85
|
+
const normalized = normalizeBuiltinStrategyUnits(input.strategyFile);
|
|
86
|
+
const units = await Promise.all(normalized.map(async (unit) => instantiateBuiltinUnit(unit)));
|
|
87
|
+
assertNoDuplicateUnitIds({
|
|
88
|
+
strategyName: input.strategyName,
|
|
89
|
+
units
|
|
90
|
+
});
|
|
91
|
+
return units;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Resolve strategy by name from installed strategies first, then built-in strategies.
|
|
95
|
+
*
|
|
96
|
+
* Usage:
|
|
97
|
+
* const strategy = resolveStrategy({ strategyName: "default", strategyService });
|
|
98
|
+
*/
|
|
99
|
+
export const resolveStrategy = (input) => {
|
|
100
|
+
const installedNames = input.strategyService.listInstalledStrategyNames();
|
|
101
|
+
if (installedNames.includes(input.strategyName)) {
|
|
102
|
+
return {
|
|
103
|
+
name: input.strategyName,
|
|
104
|
+
strategyFile: input.strategyService.readStrategy(input.strategyName),
|
|
105
|
+
source: "installed"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const builtin = readBuiltinStrategy(input.strategyName);
|
|
109
|
+
if (builtin) {
|
|
110
|
+
return {
|
|
111
|
+
name: input.strategyName,
|
|
112
|
+
strategyFile: builtin,
|
|
113
|
+
source: "builtin"
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw buildMissingStrategyError({
|
|
117
|
+
strategyName: input.strategyName,
|
|
118
|
+
installedNames
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Load executable unit instances for a resolved strategy.
|
|
123
|
+
*
|
|
124
|
+
* Usage:
|
|
125
|
+
* const units = await loadStrategyUnits({ strategy, configService });
|
|
126
|
+
*/
|
|
127
|
+
export const loadStrategyUnits = async (input) => {
|
|
128
|
+
if (input.strategy.source === "installed") {
|
|
129
|
+
return loadInstalledStrategyUnits({
|
|
130
|
+
strategyName: input.strategy.name,
|
|
131
|
+
configService: input.configService
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return loadBuiltinStrategyUnits({
|
|
135
|
+
strategyName: input.strategy.name,
|
|
136
|
+
strategyFile: input.strategy.strategyFile
|
|
137
|
+
});
|
|
138
|
+
};
|
package/dist/utils/array.js
CHANGED