@pagepocket/cli 0.12.0 → 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.
@@ -1,139 +1,11 @@
1
- import { createRequire } from "node:module";
2
- import { pathToFileURL } from "node:url";
3
1
  import { Args, Command, Flags } from "@oclif/core";
4
2
  import { ga, ns, PagePocket } from "@pagepocket/lib";
5
3
  import chalk from "chalk";
4
+ import ora from "ora";
6
5
  import { ConfigService } from "../services/config-service.js";
7
6
  import { loadConfiguredPlugins } from "../services/load-configured-plugins.js";
8
- import { readBuiltinStrategy, listBuiltinStrategyNames } from "../services/strategy/builtin-strategy-registry.js";
9
- import { normalizeStrategyUnits } from "../services/strategy/strategy-normalize.js";
10
7
  import { StrategyService } from "../services/strategy/strategy-service.js";
11
- import { UnitStore } from "../services/units/unit-store.js";
12
- import { isUnitLike, resolveUnitConstructor } from "../services/units/unit-validate.js";
13
- import { parsePinnedSpec } from "../services/user-packages/parse-pinned-spec.js";
14
- import { uniq } from "../utils/array.js";
15
- import { withSpinner } from "../utils/with-spinner.js";
16
- const buildMissingStrategyError = (input) => {
17
- const available = uniq([...input.installedNames, ...listBuiltinStrategyNames()])
18
- .filter((name) => name.trim().length > 0)
19
- .sort((left, right) => left.localeCompare(right));
20
- const suffix = available.length > 0
21
- ? ` Available strategies: ${available.join(", ")}`
22
- : " No strategies found.";
23
- return new Error(`Strategy not found: ${input.strategyName}.${suffix}`);
24
- };
25
- const resolveStrategy = (input) => {
26
- const installedNames = input.strategyService.listInstalledStrategyNames();
27
- if (installedNames.includes(input.strategyName)) {
28
- return {
29
- name: input.strategyName,
30
- strategyFile: input.strategyService.readStrategy(input.strategyName),
31
- source: "installed"
32
- };
33
- }
34
- const builtin = readBuiltinStrategy(input.strategyName);
35
- if (builtin) {
36
- return {
37
- name: input.strategyName,
38
- strategyFile: builtin,
39
- source: "builtin"
40
- };
41
- }
42
- throw buildMissingStrategyError({
43
- strategyName: input.strategyName,
44
- installedNames
45
- });
46
- };
47
- const assertNoDuplicateUnitIds = (input) => {
48
- const seen = new Set();
49
- const duplicateUnit = input.units.find((unit) => {
50
- if (seen.has(unit.id)) {
51
- return true;
52
- }
53
- seen.add(unit.id);
54
- return false;
55
- });
56
- if (duplicateUnit) {
57
- throw new Error(`Duplicate unit id detected in strategy ${input.strategyName}: ${duplicateUnit.id}`);
58
- }
59
- };
60
- const loadInstalledStrategyUnits = async (input) => {
61
- const strategyService = new StrategyService(input.configService);
62
- const unitStore = new UnitStore(input.configService);
63
- strategyService.ensureConfigFileExists();
64
- const strategyFile = strategyService.readStrategy(input.strategyName);
65
- const normalized = normalizeStrategyUnits(strategyFile);
66
- const installed = unitStore.readInstalledDependencyVersions();
67
- const drift = normalized.flatMap((unit) => {
68
- const pinned = parsePinnedSpec(unit.ref);
69
- const installedVersion = installed[pinned.name];
70
- if (!installedVersion || installedVersion !== pinned.version) {
71
- return [
72
- {
73
- name: pinned.name,
74
- pinned: pinned.version,
75
- installed: installedVersion
76
- }
77
- ];
78
- }
79
- return [];
80
- });
81
- if (drift.length > 0) {
82
- const details = drift
83
- .map((driftItem) => {
84
- const installedText = typeof driftItem.installed === "string" ? driftItem.installed : "(not installed)";
85
- return `${driftItem.name}: pinned ${driftItem.pinned}, installed ${installedText}`;
86
- })
87
- .join("; ");
88
- throw new Error(`Strategy drift detected (${input.strategyName}). ${details}. ` +
89
- `Fix: pp strategy update ${input.strategyName} OR pp strategy pin ${input.strategyName}`);
90
- }
91
- const units = await Promise.all(normalized.map(async (unit) => unitStore.instantiateFromRef(unit.ref, unit.args)));
92
- assertNoDuplicateUnitIds({
93
- strategyName: input.strategyName,
94
- units
95
- });
96
- return units;
97
- };
98
- const instantiateBuiltinUnit = async (input) => {
99
- const req = createRequire(import.meta.url);
100
- const pinned = parsePinnedSpec(input.ref);
101
- const resolved = req.resolve(pinned.name);
102
- const mod = (await import(pathToFileURL(resolved).href));
103
- const ctor = resolveUnitConstructor(mod);
104
- if (!ctor) {
105
- throw new Error(`Unit ${pinned.name} does not export a Unit constructor.`);
106
- }
107
- const instance = new ctor(...input.args);
108
- if (!isUnitLike(instance)) {
109
- throw new Error(`Unit ${pinned.name} exported constructor did not produce a Unit.`);
110
- }
111
- return instance;
112
- };
113
- const loadBuiltinStrategyUnits = async (input) => {
114
- const normalized = normalizeStrategyUnits(input.strategyFile);
115
- const units = await Promise.all(normalized.map(async (unit) => instantiateBuiltinUnit({
116
- ref: unit.ref,
117
- args: unit.args
118
- })));
119
- assertNoDuplicateUnitIds({
120
- strategyName: input.strategyName,
121
- units
122
- });
123
- return units;
124
- };
125
- const loadStrategyUnits = async (input) => {
126
- if (input.strategy.source === "installed") {
127
- return loadInstalledStrategyUnits({
128
- strategyName: input.strategy.name,
129
- configService: input.configService
130
- });
131
- }
132
- return loadBuiltinStrategyUnits({
133
- strategyName: input.strategy.name,
134
- strategyFile: input.strategy.strategyFile
135
- });
136
- };
8
+ import { loadStrategyUnits, resolveStrategy } from "../utils/archive-strategy.js";
137
9
  export default class ArchiveCommand extends Command {
138
10
  static description = "Archive a web page as an offline snapshot.";
139
11
  static args = {
@@ -165,38 +37,51 @@ export default class ArchiveCommand extends Command {
165
37
  const timeoutMs = typeof flags.timeout === "number" ? flags.timeout : undefined;
166
38
  const maxDurationMs = typeof flags.maxDuration === "number" ? flags.maxDuration : undefined;
167
39
  const strategyName = typeof flags.strategy === "string" ? flags.strategy.trim() : undefined;
40
+ const configService = new ConfigService();
41
+ const name = strategyName && strategyName.length > 0 ? strategyName : "default";
42
+ const strategyService = new StrategyService(configService);
43
+ strategyService.ensureConfigFileExists();
44
+ const strategy = resolveStrategy({ strategyName: name, strategyService });
45
+ const units = await loadStrategyUnits({ strategy, configService });
46
+ const strategyFile = strategy.strategyFile;
47
+ const captureOptions = strategyFile.pipeline.captureOptions;
48
+ const effectiveTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : captureOptions?.timeoutMs;
49
+ const effectiveMaxDurationMs = typeof maxDurationMs === "number" ? maxDurationMs : captureOptions?.maxDurationMs;
168
50
  const pagepocket = PagePocket.fromURL(targetUrl);
169
- const result = await withSpinner(async (spinner) => {
170
- {
171
- const configService = new ConfigService();
172
- const name = strategyName && strategyName.length > 0 ? strategyName : "default";
173
- const strategyService = new StrategyService(configService);
174
- strategyService.ensureConfigFileExists();
175
- const strategy = resolveStrategy({
176
- strategyName: name,
177
- strategyService
178
- });
179
- const units = await loadStrategyUnits({
180
- strategy,
181
- configService
182
- });
183
- const entryTarget = pagepocket;
184
- const strategyFile = strategy.strategyFile;
185
- const captureOptions = strategyFile.pipeline.captureOptions;
186
- const effectiveTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : captureOptions?.timeoutMs;
187
- const effectiveMaxDurationMs = typeof maxDurationMs === "number" ? maxDurationMs : captureOptions?.maxDurationMs;
188
- const result = await entryTarget.capture({
189
- ...(typeof effectiveTimeoutMs === "number" ? { timeoutMs: effectiveTimeoutMs } : {}),
190
- ...(typeof effectiveMaxDurationMs === "number"
191
- ? { maxDurationMs: effectiveMaxDurationMs }
192
- : {}),
193
- blacklist: [...ga, ...ns],
194
- units,
195
- plugins: await loadConfiguredPlugins().catch(() => [])
196
- });
197
- return result;
51
+ let spinner = ora();
52
+ let activeUnitId = "";
53
+ let activeUnitLabel = "";
54
+ const formatUnitLabel = (index, total, unitDescription) => `[${index + 1}/${total}] ${unitDescription}`;
55
+ pagepocket.on("unit:start", (e) => {
56
+ activeUnitId = e.unitId;
57
+ activeUnitLabel = formatUnitLabel(e.index, e.total, e.unitDescription);
58
+ spinner = ora(activeUnitLabel).start();
59
+ });
60
+ pagepocket.on("unit:log", (e) => {
61
+ if (e.unitId !== activeUnitId) {
62
+ return;
198
63
  }
199
- }, "Freezing page");
64
+ spinner.text = `${activeUnitLabel} ${chalk.gray(e.message)}`;
65
+ });
66
+ pagepocket.on("unit:end", (e) => {
67
+ spinner.succeed(formatUnitLabel(e.index, e.total, e.unitDescription));
68
+ });
69
+ let result;
70
+ try {
71
+ result = await pagepocket.capture({
72
+ ...(typeof effectiveTimeoutMs === "number" ? { timeoutMs: effectiveTimeoutMs } : {}),
73
+ ...(typeof effectiveMaxDurationMs === "number"
74
+ ? { maxDurationMs: effectiveMaxDurationMs }
75
+ : {}),
76
+ blacklist: [...ga, ...ns],
77
+ units,
78
+ plugins: await loadConfiguredPlugins().catch(() => [])
79
+ });
80
+ }
81
+ catch (error) {
82
+ spinner.fail();
83
+ throw error;
84
+ }
200
85
  this.log(chalk.green("All done! Snapshot created."));
201
86
  if (result.kind === "zip") {
202
87
  this.log(`Snapshot saved to ${chalk.cyan(result.zip.outputPath)}`);
@@ -1,14 +1,19 @@
1
1
  import { Command } from "@oclif/core";
2
2
  import chalk from "chalk";
3
3
  import { listBuiltinStrategyNames, readBuiltinStrategy } from "../../services/strategy/builtin-strategy-registry.js";
4
- import { normalizeStrategyUnits } from "../../services/strategy/strategy-normalize.js";
4
+ import { normalizeBuiltinStrategyUnits, normalizeStrategyUnits } from "../../services/strategy/strategy-normalize.js";
5
5
  import { StrategyService } from "../../services/strategy/strategy-service.js";
6
6
  import { uniq } from "../../utils/array.js";
7
- const formatStrategy = (name, strategyFile) => {
7
+ const formatInstalledStrategy = (name, strategyFile) => {
8
8
  const units = normalizeStrategyUnits(strategyFile);
9
9
  const pipeline = units.map((unit) => unit.ref).join(" -> ");
10
10
  return pipeline.length > 0 ? `${name}${chalk.dim(`(${pipeline})`)}` : name;
11
11
  };
12
+ const formatBuiltinStrategy = (name, strategyFile) => {
13
+ const units = normalizeBuiltinStrategyUnits(strategyFile);
14
+ const pipeline = units.map((unit) => unit.name).join(" -> ");
15
+ return pipeline.length > 0 ? `${name}${chalk.dim(`(${pipeline})`)}` : name;
16
+ };
12
17
  export default class StrategyLsCommand extends Command {
13
18
  static description = "List available strategies.";
14
19
  async run() {
@@ -25,11 +30,13 @@ export default class StrategyLsCommand extends Command {
25
30
  return;
26
31
  }
27
32
  for (const name of names) {
28
- const strategyFile = installedNames.includes(name)
29
- ? service.readStrategy(name)
30
- : readBuiltinStrategy(name);
33
+ const isInstalled = installedNames.includes(name);
34
+ const strategyFile = isInstalled ? service.readStrategy(name) : readBuiltinStrategy(name);
31
35
  if (strategyFile) {
32
- this.log(formatStrategy(name, strategyFile));
36
+ const formatted = isInstalled
37
+ ? formatInstalledStrategy(name, strategyFile)
38
+ : formatBuiltinStrategy(name, strategyFile);
39
+ this.log(formatted);
33
40
  }
34
41
  else {
35
42
  this.log(name);
@@ -51,7 +51,7 @@ export default class ViewCommand extends Command {
51
51
  host: flags.host,
52
52
  port
53
53
  });
54
- this.log(`Serving ${chalk.cyan(rootDir)}`);
54
+ this.log(`Serving ${chalk.cyan(rootDir)} ${chalk.dim(`(${server.snapshotType})`)}`);
55
55
  this.log(chalk.green(server.url));
56
56
  }
57
57
  }
@@ -43,3 +43,41 @@ export const normalizeStrategyUnits = (strategy) => {
43
43
  const units = strategy.pipeline.units;
44
44
  return units.map((spec, idx) => normalizeUnitSpec(spec, idx));
45
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,7 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { parsePinnedSpec } from "../user-packages/parse-pinned-spec.js";
3
- import { installPinnedPackage } from "../user-packages/user-package-installer.js";
4
- import { updatePackageToLatest } from "../user-packages/user-package-installer.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";
@@ -7,9 +7,6 @@ export const isUnitLike = (value) => {
7
7
  if (typeof value.id !== "string" || value.id.trim().length === 0) {
8
8
  return false;
9
9
  }
10
- if (typeof value.kind !== "string" || value.kind.trim().length === 0) {
11
- return false;
12
- }
13
10
  if (!isCallable(value.run)) {
14
11
  return false;
15
12
  }
@@ -7,7 +7,7 @@ import { Unit } from "@pagepocket/lib";
7
7
  */
8
8
  export class NetworkObserverUnit extends Unit {
9
9
  id = "networkObserver";
10
- kind = "observe.network";
10
+ description = "Observing network events";
11
11
  config;
12
12
  constructor(config = {}) {
13
13
  super();
@@ -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
+ };
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;--font-mono:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wider:.05em;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-notion-bg-light:#fff;--color-notion-text-light:#37352f;--color-notion-bg-dark:#191919;--color-notion-text-dark:#d4d4d4;--color-notion-border-light:#e9e9e8;--color-notion-border-dark:#373737;--color-notion-code-bg-light:#f7f6f3;--color-notion-code-bg-dark:#1e1e1e;--color-notion-quote-border-light:#37352f;--color-notion-quote-border-dark:#d4d4d4}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-2{top:calc(var(--spacing) * 2)}.right-0{right:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.bottom-0{bottom:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing) * 0)!important}.mx-auto{margin-inline:auto}.my-6{margin-block:calc(var(--spacing) * 6)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.table{display:table}.h-5{height:calc(var(--spacing) * 5)}.h-screen{height:100vh}.max-h-\[calc\(100vh-4rem\)\]{max-height:calc(100vh - 4rem)}.min-h-screen{min-height:100vh}.w-5{width:calc(var(--spacing) * 5)}.w-\[280px\]{width:280px}.max-w-\[800px\]{max-width:800px}.max-w-\[1200px\]{max-width:1200px}.max-w-full{max-width:100%}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-zoom-in{cursor:zoom-in}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-2{gap:calc(var(--spacing) * 2)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-\[var\(--border-primary\)\]{border-color:var(--border-primary)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-transparent{border-color:#0000}.bg-\[var\(--bg-primary\)\],.bg-\[var\(--bg-primary\)\]\/90{background-color:var(--bg-primary)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--bg-primary\)\]\/90{background-color:color-mix(in oklab,var(--bg-primary) 90%,transparent)}}.bg-\[var\(--color-notion-code-bg-light\)\]{background-color:var(--color-notion-code-bg-light)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-white{background-color:var(--color-white)}.\!p-4{padding:calc(var(--spacing) * 4)!important}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-\[var\(--text-primary\)\]{color:var(--text-primary)}.text-blue-600{color:var(--color-blue-600)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.uppercase{text-transform:uppercase}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media(min-width:64rem){.lg\:mx-auto{margin-inline:auto}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:px-12{padding-inline:calc(var(--spacing) * 12)}.lg\:py-16{padding-block:calc(var(--spacing) * 16)}}.dark\:border-gray-800:is(.dark *){border-color:var(--color-gray-800)}.dark\:bg-\[var\(--color-notion-code-bg-dark\)\]:is(.dark *){background-color:var(--color-notion-code-bg-dark)}.dark\:bg-gray-700:is(.dark *){background-color:var(--color-gray-700)}.dark\:text-blue-400:is(.dark *){color:var(--color-blue-400)}.dark\:text-gray-400:is(.dark *){color:var(--color-gray-400)}@media(hover:hover){.dark\:hover\:bg-gray-600:is(.dark *):hover{background-color:var(--color-gray-600)}.dark\:hover\:bg-gray-800:is(.dark *):hover{background-color:var(--color-gray-800)}.dark\:hover\:text-gray-200:is(.dark *):hover{color:var(--color-gray-200)}}}pre code.hljs{padding:1em;display:block;overflow-x:auto}code.hljs{padding:3px 5px}.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}[data-rmiz-ghost]{pointer-events:none;position:absolute}[data-rmiz-btn-zoom],[data-rmiz-btn-unzoom]{color:#fff;outline-offset:2px;touch-action:manipulation;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#000000b3;border:none;border-radius:50%;width:40px;height:40px;margin:0;padding:9px;box-shadow:0 0 1px #ffffff80}[data-rmiz-btn-zoom]:not(:focus):not(:active){clip:rect(0 0 0 0);clip-path:inset(50%);pointer-events:none;white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}[data-rmiz-btn-zoom]{cursor:zoom-in;position:absolute;inset:10px 10px auto auto}[data-rmiz-btn-unzoom]{cursor:zoom-out;z-index:1;position:absolute;inset:20px 20px auto auto}[data-rmiz-content=found] img,[data-rmiz-content=found] svg,[data-rmiz-content=found] [role=img],[data-rmiz-content=found] [data-zoom]{cursor:zoom-in}[data-rmiz-modal]::backdrop{display:none}[data-rmiz-modal][open]{pointer-events:all;background:0 0;border:0;width:100dvw;max-width:none;height:100dvh;max-height:none;margin:0;padding:0;position:fixed;overflow:hidden}[data-rmiz-modal-overlay]{transition:background-color .3s;position:absolute;top:0;right:0;bottom:0;left:0}[data-rmiz-modal-overlay=hidden]{background-color:#fff0}[data-rmiz-modal-overlay=visible]{background-color:#fff}[data-rmiz-modal-content]{width:100%;height:100%;position:relative}[data-rmiz-modal-img]{cursor:zoom-out;image-rendering:high-quality;transform-origin:0 0;transition:transform .3s;position:absolute}@media(prefers-reduced-motion:reduce){[data-rmiz-modal-overlay],[data-rmiz-modal-img]{transition-duration:.01ms!important}}::view-transition-group(root){animation-duration:1s}::view-transition-new(root){mix-blend-mode:normal}::view-transition-old(root){mix-blend-mode:normal}::view-transition-new(root){animation-name:reveal-light}::view-transition-old(root){animation:none}.dark::view-transition-old(root){animation:none}.dark::view-transition-new(root){animation-name:reveal-dark}li{margin-bottom:5px}@keyframes reveal-dark{0%{clip-path:polygon(-30% 0,-30% 0,-15% 100%,-10% 115%)}to{clip-path:polygon(-30% 0,130% 0,115% 100%,-10% 115%)}}@keyframes reveal-light{0%{clip-path:polygon(130% 0,130% 0,115% 100%,110% 115%)}to{clip-path:polygon(130% 0,-30% 0,-15% 100%,110% 115%)}}.prose-notion{max-width:720px;font-family:var(--font-sans);margin-left:auto;margin-right:auto;font-size:16px;line-height:1.7}:root{--bg-primary:var(--color-notion-bg-light);--text-primary:var(--color-notion-text-light);--border-primary:var(--color-notion-border-light);--code-bg:var(--color-notion-code-bg-light);--quote-border:var(--color-notion-quote-border-light)}.dark{--bg-primary:var(--color-notion-bg-dark);--text-primary:var(--color-notion-text-dark);--border-primary:var(--color-notion-border-dark);--code-bg:var(--color-notion-code-bg-dark);--quote-border:var(--color-notion-quote-border-dark)}.prose-notion{color:var(--text-primary);background-color:var(--bg-primary)}.prose-notion h1{margin-top:2em;margin-bottom:.5em;font-size:2em;font-weight:700;line-height:1.3}.prose-notion h2{margin-top:1.4em;margin-bottom:.4em;font-size:1.5em;font-weight:600;line-height:1.3}.prose-notion h3{margin-top:1.2em;margin-bottom:.3em;font-size:1.25em;font-weight:600;line-height:1.3}.prose-notion p{margin-top:.5em;margin-bottom:.5em}.prose-notion blockquote{border-left:3px solid var(--quote-border);opacity:.8;margin-top:1em;margin-bottom:1em;padding-left:1rem;font-style:italic}.prose-notion ul,.prose-notion ol{margin-top:.5em;margin-bottom:.5em;padding-left:1.5em}.prose-notion li{margin-bottom:.25em}.prose-notion hr{border:0;border-top:1px solid var(--border-primary);margin:2em 0}.prose-notion a{text-underline-offset:4px;color:inherit;-webkit-text-decoration:underline #78777466;text-decoration:underline #78777466;transition:text-decoration-color .1s}.prose-notion a:hover{-webkit-text-decoration-color:var(--text-primary);text-decoration-color:var(--text-primary)}.prose-notion img{border-radius:4px;max-width:100%;margin:1.5em auto;display:block}.prose-notion table{border-collapse:collapse;width:100%;margin:1.5em 0;font-size:.9em}.prose-notion th,.prose-notion td{border:1px solid var(--border-primary);text-align:left;padding:8px 12px}.prose-notion th{background-color:#7877740d;font-weight:600}.prose-notion :not(pre)>code{background:var(--code-bg);font-family:var(--font-mono);border-radius:3px;padding:2px 6px;font-size:.9em}.prose-notion pre{background:0 0;margin:0;padding:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}