plotlink-ows 1.0.33 → 1.2.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/app/lib/active-wallet.ts +260 -0
- package/app/lib/agent-command.ts +85 -0
- package/app/lib/agent-readiness.ts +133 -0
- package/app/lib/apply-schema.ts +55 -0
- package/app/lib/bubble-text.ts +160 -0
- package/app/lib/cartoon-coach.ts +198 -0
- package/app/lib/cartoon-markdown.ts +83 -0
- package/app/lib/cartoon-prompt.ts +122 -0
- package/app/lib/cartoon-readiness.ts +813 -0
- package/app/lib/clean-image-sync.ts +245 -0
- package/app/lib/codex-images.ts +152 -0
- package/app/lib/cut-asset-diagnostics.ts +120 -0
- package/app/lib/cuts.ts +302 -0
- package/app/lib/fonts.ts +109 -0
- package/app/lib/generate-claude-md.ts +8 -1
- package/app/lib/generate-story-instructions.ts +731 -0
- package/app/lib/image-asset-validate.ts +123 -0
- package/app/lib/lettering-status.ts +133 -0
- package/app/lib/overlays.ts +637 -0
- package/app/lib/paths.ts +10 -0
- package/app/lib/public-title.ts +65 -0
- package/app/lib/publish.ts +16 -2
- package/app/lib/story-progress.ts +242 -0
- package/app/lib/terminal-protocol.ts +16 -0
- package/app/lib/terminal-redact.ts +50 -0
- package/app/prisma/schema.sql +25 -0
- package/app/routes/agent.ts +42 -0
- package/app/routes/codex-images.ts +67 -0
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +259 -45
- package/app/routes/settings.ts +92 -37
- package/app/routes/stories.ts +961 -5
- package/app/routes/terminal.ts +383 -31
- package/app/routes/wallet.ts +58 -30
- package/app/server.ts +47 -12
- package/app/vite.config.ts +6 -0
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPreview.tsx +267 -0
- package/app/web/components/CartoonPublishPage.tsx +407 -0
- package/app/web/components/CartoonPublishPreview.tsx +121 -0
- package/app/web/components/CartoonStepGuide.tsx +90 -0
- package/app/web/components/CartoonWorkflowNav.tsx +68 -0
- package/app/web/components/CodexImportPicker.tsx +230 -0
- package/app/web/components/CutListPanel.tsx +1337 -0
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/EpisodesPage.tsx +80 -0
- package/app/web/components/FinishEpisodePanel.tsx +151 -0
- package/app/web/components/Layout.tsx +7 -4
- package/app/web/components/LetteringEditor.tsx +1182 -0
- package/app/web/components/PreviewPanel.tsx +952 -78
- package/app/web/components/Settings.tsx +63 -0
- package/app/web/components/StoriesPage.tsx +745 -33
- package/app/web/components/StoryBrowser.tsx +22 -14
- package/app/web/components/StoryInfoPage.tsx +266 -0
- package/app/web/components/StoryProgressPanel.tsx +446 -0
- package/app/web/components/TerminalPanel.tsx +233 -11
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +156 -0
- package/app/web/components/asset-image.tsx +114 -0
- package/app/web/components/asset-test-utils.ts +44 -0
- package/app/web/components/export-cut.ts +320 -0
- package/app/web/dist/assets/export-cut-che5mMWc.js +1 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/lib/cartoon-publish-summary.ts +43 -0
- package/app/web/lib/codex-import.ts +94 -0
- package/app/web/lib/image-compress.ts +53 -0
- package/app/web/lib/import-image.ts +58 -0
- package/app/web/lib/publish-helpers.ts +385 -0
- package/app/web/lib/upload-retry.ts +130 -0
- package/app/web/lib/verify-public-title.ts +105 -0
- package/app/web/styles.css +9 -0
- package/bin/plotlink-ows.js +53 -16
- package/bin/startup-plan.cjs +58 -0
- package/lib/genres.ts +92 -0
- package/package.json +60 -20
- package/scripts/gen-schema-sql.mjs +49 -0
- package/scripts/package-hygiene.mjs +116 -0
- package/scripts/preflight.mjs +173 -0
- package/scripts/start-smoke.mjs +128 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/client.js +0 -5
- package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/default.js +0 -5
- package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/edge.js +0 -184
- package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
- package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
- package/app/node_modules/.prisma/local-client/index.js +0 -207
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +0 -183
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
- package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
- package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/wasm.js +0 -191
- package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
- package/app/web/dist/assets/index-DxATSk7X.js +0 -134
- package/packages/cli/node_modules/commander/LICENSE +0 -22
- package/packages/cli/node_modules/commander/Readme.md +0 -1149
- package/packages/cli/node_modules/commander/esm.mjs +0 -16
- package/packages/cli/node_modules/commander/index.js +0 -24
- package/packages/cli/node_modules/commander/lib/argument.js +0 -149
- package/packages/cli/node_modules/commander/lib/command.js +0 -2662
- package/packages/cli/node_modules/commander/lib/error.js +0 -39
- package/packages/cli/node_modules/commander/lib/help.js +0 -709
- package/packages/cli/node_modules/commander/lib/option.js +0 -367
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/packages/cli/node_modules/commander/package-support.json +0 -16
- package/packages/cli/node_modules/commander/package.json +0 -82
- package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
- package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
- package/packages/cli/node_modules/resolve-from/index.js +0 -47
- package/packages/cli/node_modules/resolve-from/license +0 -9
- package/packages/cli/node_modules/resolve-from/package.json +0 -36
- package/packages/cli/node_modules/resolve-from/readme.md +0 -72
- package/packages/cli/node_modules/tsup/LICENSE +0 -21
- package/packages/cli/node_modules/tsup/README.md +0 -75
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
- package/packages/cli/node_modules/tsup/assets/package.json +0 -3
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
- package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
- package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
- package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
- package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
- package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
- package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
- package/packages/cli/node_modules/tsup/package.json +0 -99
- package/packages/cli/node_modules/tsup/schema.json +0 -362
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/scripts/e2e-verify.ts +0 -1100
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Retry/backoff for rate-limited cartoon cut image uploads (#288).
|
|
2
|
+
//
|
|
3
|
+
// The PlotLink upload endpoint rate-limits to 5 uploads/minute. A normal webtoon
|
|
4
|
+
// episode commonly has more than five cuts, so the batch "Upload & Generate" flow
|
|
5
|
+
// would otherwise fail mid-batch on the 6th+ cut with a "Rate limit exceeded"
|
|
6
|
+
// response. These helpers retry a single upload with backoff while it is
|
|
7
|
+
// rate-limited, while leaving genuine (non-rate-limit) failures to fail fast.
|
|
8
|
+
|
|
9
|
+
// PlotLink allows 5 uploads/minute, so ~12s spacing clears the window for the
|
|
10
|
+
// next cut. Backoff grows from here and is capped so a stuck batch still ends.
|
|
11
|
+
export const RATE_LIMIT_BASE_DELAY_MS = 12_000;
|
|
12
|
+
export const RATE_LIMIT_MAX_RETRIES = 5;
|
|
13
|
+
const MAX_BACKOFF_MS = 60_000;
|
|
14
|
+
|
|
15
|
+
// PlotLink's documented limit: 5 uploads per rolling 60s window. The proactive
|
|
16
|
+
// throttle below paces the batch to stay under this, so a 7–10 cut episode never
|
|
17
|
+
// blows the budget in a tight loop and then thrashes on reactive backoff (#413).
|
|
18
|
+
export const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
19
|
+
export const RATE_LIMIT_BURST = 5;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A rate-limit response. The OWS route currently forwards PlotLink's rate-limit
|
|
23
|
+
* as a 500 carrying the upstream message ("Rate limit exceeded. Max 5 uploads
|
|
24
|
+
* per minute."), so we detect by status 429 OR a rate-limit message — either is
|
|
25
|
+
* treated as retryable.
|
|
26
|
+
*/
|
|
27
|
+
export function isRateLimitError(status: number, errorMessage?: string | null): boolean {
|
|
28
|
+
if (status === 429) return true;
|
|
29
|
+
return !!errorMessage && /rate[\s-]?limit/i.test(errorMessage);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Backoff for the Nth retry (0-based): base, 2×base, 4×base, … capped. */
|
|
33
|
+
export function backoffMs(retry: number, baseDelayMs = RATE_LIMIT_BASE_DELAY_MS): number {
|
|
34
|
+
return Math.min(baseDelayMs * 2 ** retry, MAX_BACKOFF_MS);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AttemptResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
status: number;
|
|
40
|
+
errorMessage?: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RetryDeps {
|
|
44
|
+
/** Injectable for tests; defaults to a real setTimeout-based sleep. */
|
|
45
|
+
sleep?: (ms: number) => Promise<void>;
|
|
46
|
+
maxRetries?: number;
|
|
47
|
+
baseDelayMs?: number;
|
|
48
|
+
/** Called once before each backoff wait so the UI can show a waiting state. */
|
|
49
|
+
onWaiting?: (info: { attempt: number; maxRetries: number; waitMs: number }) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run `attempt` and, while its result is rate-limited, wait with backoff and
|
|
56
|
+
* retry up to `maxRetries` times. Returns the first non-rate-limited result, or
|
|
57
|
+
* the last rate-limited result once retries are exhausted (so the caller still
|
|
58
|
+
* gets the affected status/message to report). Never retries a non-rate-limit
|
|
59
|
+
* failure or a success.
|
|
60
|
+
*/
|
|
61
|
+
export async function withRateLimitRetry<T extends AttemptResult>(
|
|
62
|
+
attempt: () => Promise<T>,
|
|
63
|
+
deps: RetryDeps = {},
|
|
64
|
+
): Promise<T> {
|
|
65
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
66
|
+
const maxRetries = deps.maxRetries ?? RATE_LIMIT_MAX_RETRIES;
|
|
67
|
+
const baseDelayMs = deps.baseDelayMs ?? RATE_LIMIT_BASE_DELAY_MS;
|
|
68
|
+
|
|
69
|
+
let retries = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const result = await attempt();
|
|
72
|
+
if (result.ok || !isRateLimitError(result.status, result.errorMessage)) return result;
|
|
73
|
+
if (retries >= maxRetries) return result;
|
|
74
|
+
const waitMs = backoffMs(retries, baseDelayMs);
|
|
75
|
+
retries += 1;
|
|
76
|
+
deps.onWaiting?.({ attempt: retries, maxRetries, waitMs });
|
|
77
|
+
await sleep(waitMs);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ThrottleDeps {
|
|
82
|
+
/** Max uploads allowed per window (default 5 = PlotLink's per-minute limit). */
|
|
83
|
+
limit?: number;
|
|
84
|
+
/** Rolling window length in ms (default 60_000). */
|
|
85
|
+
windowMs?: number;
|
|
86
|
+
/** Injectable for tests; defaults to a real setTimeout-based sleep. */
|
|
87
|
+
sleep?: (ms: number) => Promise<void>;
|
|
88
|
+
/** Injectable clock for tests; defaults to Date.now. */
|
|
89
|
+
now?: () => number;
|
|
90
|
+
/** Called once before each proactive wait so the UI can show a waiting state. */
|
|
91
|
+
onWaiting?: (info: { waitMs: number }) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a proactive sliding-window throttle for a batch of uploads (#413).
|
|
96
|
+
*
|
|
97
|
+
* Returns an async `throttle()` to call immediately BEFORE each upload. It records
|
|
98
|
+
* each call's timestamp and, once `limit` uploads have happened inside the last
|
|
99
|
+
* `windowMs`, sleeps until the oldest of those falls out of the window before
|
|
100
|
+
* letting the next through — so a 7–10 cut batch paces itself under PlotLink's
|
|
101
|
+
* 5/min limit instead of firing all uploads at once and then thrashing on reactive
|
|
102
|
+
* 429 backoff. `withRateLimitRetry` stays as the safety net for any 429 that still
|
|
103
|
+
* slips through (e.g. budget consumed by another client). Pure aside from the
|
|
104
|
+
* injected clock/sleep, so it's deterministic in tests.
|
|
105
|
+
*/
|
|
106
|
+
export function createUploadThrottle(deps: ThrottleDeps = {}) {
|
|
107
|
+
const limit = deps.limit ?? RATE_LIMIT_BURST;
|
|
108
|
+
const windowMs = deps.windowMs ?? RATE_LIMIT_WINDOW_MS;
|
|
109
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
110
|
+
const now = deps.now ?? (() => Date.now());
|
|
111
|
+
const stamps: number[] = [];
|
|
112
|
+
|
|
113
|
+
const dropExpired = () => {
|
|
114
|
+
const cutoff = now() - windowMs;
|
|
115
|
+
while (stamps.length && stamps[0] <= cutoff) stamps.shift();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return async function throttle(): Promise<void> {
|
|
119
|
+
dropExpired();
|
|
120
|
+
if (stamps.length >= limit) {
|
|
121
|
+
const waitMs = stamps[0] + windowMs - now();
|
|
122
|
+
if (waitMs > 0) {
|
|
123
|
+
deps.onWaiting?.({ waitMs });
|
|
124
|
+
await sleep(waitMs);
|
|
125
|
+
}
|
|
126
|
+
dropExpired();
|
|
127
|
+
}
|
|
128
|
+
stamps.push(now());
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isRawFilenameTitle, isGenericEpisodeTitle } from "./publish-helpers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* End-to-end public-title verification for cartoon publishes (#379).
|
|
5
|
+
*
|
|
6
|
+
* Local guards (#347/#358/#365/#368) ensure OWS *sends* a reader-facing title,
|
|
7
|
+
* but the real pilot (`plotlink.xyz/story/59/1` rendered `genesis` / `plot-01`)
|
|
8
|
+
* showed we must also prove the PUBLIC, indexed metadata is reader-facing — not
|
|
9
|
+
* just that local preview computed a good label. After indexing, OWS reads the
|
|
10
|
+
* indexed storyline detail and verifies the title here. Already-published bad
|
|
11
|
+
* titles are immutable, so this can only warn + keep the next publish honest.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Subset of PlotLink's `GET /api/storyline/<id>` response we read for #379. */
|
|
15
|
+
export interface PublicStorylineDetail {
|
|
16
|
+
title?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
// PlotLink may expose per-episode entries (plots/chapters) with their own
|
|
19
|
+
// title + index; we read whichever is present, matching by plot index.
|
|
20
|
+
plots?: Array<{ title?: string; name?: string; index?: number; plotIndex?: number }>;
|
|
21
|
+
chapters?: Array<{ title?: string; name?: string; index?: number; plotIndex?: number }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PublicTitleVerdict {
|
|
25
|
+
/** false → the indexed public title is raw/generic (verification failed). */
|
|
26
|
+
ok: boolean;
|
|
27
|
+
/** false → the relevant public title field was absent (read inconclusive). */
|
|
28
|
+
checked: boolean;
|
|
29
|
+
/** the public title actually evaluated, when present. */
|
|
30
|
+
publicTitle?: string;
|
|
31
|
+
/** human-facing failure reason, when ok === false. */
|
|
32
|
+
reason?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Find the public title for a plot index across whichever list PlotLink returns. */
|
|
36
|
+
function pickPlotTitle(detail: PublicStorylineDetail, plotIndex: number | undefined): string | undefined {
|
|
37
|
+
const lists = [detail.plots, detail.chapters].filter(Boolean) as NonNullable<PublicStorylineDetail["plots"]>[];
|
|
38
|
+
for (const list of lists) {
|
|
39
|
+
const byIndex =
|
|
40
|
+
plotIndex != null ? list.find((p) => p.plotIndex === plotIndex || p.index === plotIndex) : undefined;
|
|
41
|
+
let entry = byIndex;
|
|
42
|
+
if (!entry && list.length === 1) {
|
|
43
|
+
// Fall back to the lone entry ONLY when it carries no index to match on
|
|
44
|
+
// (or the caller gave no index) — never when a known index simply differs,
|
|
45
|
+
// which would verify the wrong episode.
|
|
46
|
+
const only = list[0];
|
|
47
|
+
const hasIndex = only.plotIndex != null || only.index != null;
|
|
48
|
+
if (!hasIndex || plotIndex == null) entry = only;
|
|
49
|
+
}
|
|
50
|
+
const t = (entry?.title ?? entry?.name)?.trim();
|
|
51
|
+
if (t) return t;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verify the indexed PlotLink title for a cartoon publish is reader-facing.
|
|
58
|
+
* Genesis (storyline) titles must not be the raw `genesis` fallback; plot titles
|
|
59
|
+
* must not be `plot-NN` or a generic `Episode NN` placeholder. Returns
|
|
60
|
+
* `checked: false` when the relevant public title is absent, so the caller never
|
|
61
|
+
* false-fails an inconclusive read (e.g. a transient indexer response).
|
|
62
|
+
*/
|
|
63
|
+
export function verifyPublicCartoonTitle(opts: {
|
|
64
|
+
fileName: string;
|
|
65
|
+
detail: PublicStorylineDetail | null | undefined;
|
|
66
|
+
plotIndex?: number;
|
|
67
|
+
}): PublicTitleVerdict {
|
|
68
|
+
const { fileName, detail, plotIndex } = opts;
|
|
69
|
+
if (!detail) return { ok: true, checked: false };
|
|
70
|
+
|
|
71
|
+
if (fileName === "genesis.md") {
|
|
72
|
+
const publicTitle = (detail.title ?? detail.name)?.trim();
|
|
73
|
+
if (!publicTitle) return { ok: true, checked: false };
|
|
74
|
+
if (isRawFilenameTitle(publicTitle, "genesis.md")) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
checked: true,
|
|
78
|
+
publicTitle,
|
|
79
|
+
reason: `PlotLink indexed the storyline title as “${publicTitle}”, a raw filename rather than the reader-facing title.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, checked: true, publicTitle };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const publicTitle = pickPlotTitle(detail, plotIndex);
|
|
86
|
+
if (!publicTitle) return { ok: true, checked: false };
|
|
87
|
+
if (isRawFilenameTitle(publicTitle, fileName) || isGenericEpisodeTitle(publicTitle)) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
checked: true,
|
|
91
|
+
publicTitle,
|
|
92
|
+
reason: `PlotLink indexed the episode title as “${publicTitle}”, a generic placeholder rather than a reader-facing episode title.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, checked: true, publicTitle };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Durable, writer-facing warning shown when public-title verification fails (#379). */
|
|
99
|
+
export function publicTitleWarning(verdict: PublicTitleVerdict): string {
|
|
100
|
+
return (
|
|
101
|
+
`${verdict.reason ?? "PlotLink indexed a raw/generic public title for this publish."} ` +
|
|
102
|
+
`Published metadata is immutable on-chain and cannot be edited — the next publish must use corrected, reader-facing metadata. ` +
|
|
103
|
+
`(The webtoon pilot stays blocked until a publish indexes a real public title.)`
|
|
104
|
+
);
|
|
105
|
+
}
|
package/app/web/styles.css
CHANGED
|
@@ -62,6 +62,15 @@ code, pre {
|
|
|
62
62
|
font-family: "Geist Mono", ui-monospace, monospace;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/* Cartoon awaiting-upload pending panel — calm, info-toned (NOT red). Shown
|
|
66
|
+
after Generate MD lays down the skeleton but before final images are
|
|
67
|
+
uploaded. Mirrors the planning-callout treatment so the two pending states
|
|
68
|
+
feel related rather than alarming. */
|
|
69
|
+
.cartoon-awaiting-upload {
|
|
70
|
+
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
|
71
|
+
background: color-mix(in srgb, var(--accent) 5%, transparent);
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
/* Ensure dim/faint terminal text (SGR 2) stays readable on cream bg */
|
|
66
75
|
.xterm .xterm-dim {
|
|
67
76
|
opacity: 1 !important;
|
package/bin/plotlink-ows.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// PlotLink OWS — CLI Wizard
|
|
3
|
-
// Zero external dependencies — Node builtins only
|
|
3
|
+
// Zero external dependencies — Node builtins + one in-package helper only.
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const readline = require("readline");
|
|
8
8
|
const { execSync, spawn } = require("child_process");
|
|
9
9
|
const crypto = require("crypto");
|
|
10
|
+
const { planStartup, shouldAutoOpen } = require("./startup-plan.cjs");
|
|
10
11
|
|
|
11
12
|
const CONFIG_DIR = path.join(require("os").homedir(), ".plotlink-ows");
|
|
12
13
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
@@ -203,6 +204,19 @@ async function cmdInit() {
|
|
|
203
204
|
process.exit(0);
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
// Are the runtime dependencies resolvable? Resolve a known runtime dep rather
|
|
208
|
+
// than probing `PROJECT_DIR/node_modules` directly — under a global (`-g`)
|
|
209
|
+
// install the package's deps are hoisted to a sibling `node_modules`, so that
|
|
210
|
+
// directory may not exist even though the deps resolve fine up the tree.
|
|
211
|
+
function runtimeDepsInstalled() {
|
|
212
|
+
try {
|
|
213
|
+
require.resolve("tsx", { paths: [PROJECT_DIR] });
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
206
220
|
function cmdStart() {
|
|
207
221
|
const config = readConfig();
|
|
208
222
|
if (!config) {
|
|
@@ -211,28 +225,51 @@ function cmdStart() {
|
|
|
211
225
|
process.exit(1);
|
|
212
226
|
}
|
|
213
227
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
// Runtime/build-time boundary (#470, EPIC #465): an installed package ships
|
|
229
|
+
// prebuilt assets and only runtime deps, so the start path must NOT run a web
|
|
230
|
+
// build or `npm install` here — that would fetch the build toolchain from the
|
|
231
|
+
// network (an unexpected rebuild on a user's machine). Only a source checkout
|
|
232
|
+
// (detected by `src/`, which is never in the published tarball) may build.
|
|
233
|
+
// Missing assets in an installed package mean a corrupted install.
|
|
234
|
+
const plan = planStartup({
|
|
235
|
+
isSourceCheckout: fs.existsSync(path.join(PROJECT_DIR, "src")),
|
|
236
|
+
depsInstalled: runtimeDepsInstalled(),
|
|
237
|
+
distBuilt: fs.existsSync(path.join(PROJECT_DIR, "app", "web", "dist", "index.html")),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (plan.error) {
|
|
241
|
+
const what = plan.error === "deps"
|
|
242
|
+
? "Runtime dependencies are missing (node_modules)"
|
|
243
|
+
: "Prebuilt web assets are missing (app/web/dist)";
|
|
244
|
+
error(`${what} — this looks like a corrupted install.`);
|
|
245
|
+
log("Reinstall and try again:");
|
|
246
|
+
log(" \x1b[1mnpx plotlink-ows@latest\x1b[0m (or)");
|
|
247
|
+
log(" \x1b[1mnpm install -g plotlink-ows@latest\x1b[0m");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
if (plan.install) {
|
|
251
|
+
log("Installing dependencies (source checkout)...");
|
|
217
252
|
execSync("npm install", { cwd: PROJECT_DIR, stdio: "inherit" });
|
|
218
253
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const distDir = path.join(PROJECT_DIR, "app", "web", "dist");
|
|
222
|
-
if (!fs.existsSync(distDir)) {
|
|
223
|
-
log("Building frontend...");
|
|
254
|
+
if (plan.build) {
|
|
255
|
+
log("Building frontend (source checkout)...");
|
|
224
256
|
execSync("npx vite build --config app/vite.config.ts", { cwd: PROJECT_DIR, stdio: "inherit" });
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
const port = config.port || 7777;
|
|
228
260
|
|
|
229
|
-
// Auto-open browser after a short delay
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
// Auto-open browser after a short delay. Skipped for non-interactive runs
|
|
262
|
+
// (the release start smoke / preflight set PLOTLINK_OWS_NO_OPEN=1) so a publish
|
|
263
|
+
// check never pops a browser tab on the operator machine (#481). Normal
|
|
264
|
+
// `npx plotlink-ows` startup is unaffected.
|
|
265
|
+
if (shouldAutoOpen(process.env)) {
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
try {
|
|
268
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
269
|
+
execSync(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
|
|
270
|
+
} catch { /* ignore */ }
|
|
271
|
+
}, 2000);
|
|
272
|
+
}
|
|
236
273
|
|
|
237
274
|
// Run server in foreground with visible logs
|
|
238
275
|
const server = spawn("npx", ["tsx", "app/server.ts"], {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Runtime/build-time boundary planner for the PlotLink OWS CLI (#470, EPIC #465).
|
|
2
|
+
//
|
|
3
|
+
// The published `plotlink-ows` package ships PREBUILT runtime assets
|
|
4
|
+
// (`app/web/dist`) and installs only runtime `dependencies`. All build tooling
|
|
5
|
+
// (vite, tailwind, react, …) lives in `devDependencies` (see #469) and is
|
|
6
|
+
// therefore ABSENT from an installed package. Consequence: the `start` path must
|
|
7
|
+
// NEVER run a web build or `npm install` on a user's machine — doing so would
|
|
8
|
+
// fetch the build toolchain from the network (an unexpected rebuild). Missing
|
|
9
|
+
// prebuilt assets in an installed package mean a CORRUPTED install, which we
|
|
10
|
+
// surface loudly instead of silently trying to rebuild.
|
|
11
|
+
//
|
|
12
|
+
// A *source checkout* (the repo) is the only place a (re)build is allowed. It is
|
|
13
|
+
// detected by the presence of `src/` (the Next.js web app), which is NOT part of
|
|
14
|
+
// the published `files` allowlist and so never exists in an installed package.
|
|
15
|
+
//
|
|
16
|
+
// This module is intentionally PURE (no fs/process access) so the boundary
|
|
17
|
+
// policy is unit-tested in isolation; `bin/plotlink-ows.js` probes the
|
|
18
|
+
// environment and feeds the facts in.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decide what the `start` command must do before launching the server.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} env
|
|
24
|
+
* @param {boolean} env.isSourceCheckout running from the repo (has `src/`)
|
|
25
|
+
* @param {boolean} env.depsInstalled runtime deps resolvable
|
|
26
|
+
* @param {boolean} env.distBuilt prebuilt web UI present (app/web/dist/index.html)
|
|
27
|
+
* @returns {{ install: boolean, build: boolean, error: null | "deps" | "dist" }}
|
|
28
|
+
* install/build are only ever `true` in a source checkout (dev convenience).
|
|
29
|
+
* `error` is a fatal broken-install condition for an installed package.
|
|
30
|
+
*/
|
|
31
|
+
function planStartup({ isSourceCheckout, depsInstalled, distBuilt }) {
|
|
32
|
+
if (isSourceCheckout) {
|
|
33
|
+
// Dev convenience: bring a fresh checkout up without manual build steps.
|
|
34
|
+
return { install: !depsInstalled, build: !distBuilt, error: null };
|
|
35
|
+
}
|
|
36
|
+
// Installed package: never pull build tooling. Missing assets ⇒ broken install.
|
|
37
|
+
if (!depsInstalled) return { install: false, build: false, error: "deps" };
|
|
38
|
+
if (!distBuilt) return { install: false, build: false, error: "dist" };
|
|
39
|
+
return { install: false, build: false, error: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether `start` should auto-open the local app in a browser (#481).
|
|
44
|
+
*
|
|
45
|
+
* Normal `npx plotlink-ows` startup auto-opens for convenience. Non-interactive
|
|
46
|
+
* release checks (the packed start smoke / `npm run preflight`) set
|
|
47
|
+
* `PLOTLINK_OWS_NO_OPEN=1` so they never pop a browser tab on the operator
|
|
48
|
+
* machine. Only the exact string "1" disables it; any other value keeps the
|
|
49
|
+
* default open behavior.
|
|
50
|
+
*
|
|
51
|
+
* @param {Record<string, string | undefined>} env a process.env-shaped object
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function shouldAutoOpen(env) {
|
|
55
|
+
return env.PLOTLINK_OWS_NO_OPEN !== "1";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { planStartup, shouldAutoOpen };
|
package/lib/genres.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const GENRES = [
|
|
2
|
+
"Romance",
|
|
3
|
+
"Fantasy",
|
|
4
|
+
"Science Fiction",
|
|
5
|
+
"Mystery",
|
|
6
|
+
"Thriller",
|
|
7
|
+
"Horror",
|
|
8
|
+
"Adventure",
|
|
9
|
+
"Historical Fiction",
|
|
10
|
+
"Contemporary Lit",
|
|
11
|
+
"Humor",
|
|
12
|
+
"Poetry",
|
|
13
|
+
"Non-Fiction",
|
|
14
|
+
"Fanfiction",
|
|
15
|
+
"Short Story",
|
|
16
|
+
"Paranormal",
|
|
17
|
+
"Werewolf",
|
|
18
|
+
"LGBTQ+",
|
|
19
|
+
"New Adult",
|
|
20
|
+
"Teen Fiction",
|
|
21
|
+
"Diverse Lit",
|
|
22
|
+
"Others",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export const LANGUAGES = [
|
|
26
|
+
"English",
|
|
27
|
+
"Chinese",
|
|
28
|
+
"Korean",
|
|
29
|
+
"Japanese",
|
|
30
|
+
"Spanish",
|
|
31
|
+
"French",
|
|
32
|
+
"Hindi",
|
|
33
|
+
"Arabic",
|
|
34
|
+
"Portuguese",
|
|
35
|
+
"Russian",
|
|
36
|
+
"Others",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export const CONTENT_TYPES = ["fiction", "cartoon"] as const;
|
|
40
|
+
|
|
41
|
+
export type Genre = (typeof GENRES)[number];
|
|
42
|
+
export type Language = (typeof LANGUAGES)[number];
|
|
43
|
+
export type ContentType = (typeof CONTENT_TYPES)[number];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Punctuation/spacing/case-insensitive key for matching a free-form genre label
|
|
47
|
+
* to a canonical value. Strips everything but letters, digits and `+` (so
|
|
48
|
+
* `LGBTQ+` survives), which collapses `Sci-Fi`, `sci fi`, `SciFi` → `scifi` and
|
|
49
|
+
* `Science Fiction` / `science-fiction` → `sciencefiction`.
|
|
50
|
+
*/
|
|
51
|
+
function genreKey(input: string): string {
|
|
52
|
+
return input.toLowerCase().replace(/[^a-z0-9+]/g, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CANONICAL_GENRE_BY_KEY: Record<string, Genre> = Object.fromEntries(
|
|
56
|
+
GENRES.map((g) => [genreKey(g), g]),
|
|
57
|
+
) as Record<string, Genre>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Common natural-language genre aliases → canonical PlotLink value (#412). Keyed
|
|
61
|
+
* by `genreKey`, so each entry already covers punctuation/spacing/case variants
|
|
62
|
+
* (e.g. the `scifi` key matches `Sci-Fi`, `Sci Fi`, `SciFi`). Canonical labels and
|
|
63
|
+
* their punctuation variants (e.g. `non fiction` → `Non-Fiction`) are handled by
|
|
64
|
+
* `CANONICAL_GENRE_BY_KEY` and don't need an alias here.
|
|
65
|
+
*/
|
|
66
|
+
const GENRE_ALIAS_BY_KEY: Record<string, Genre> = {
|
|
67
|
+
scifi: "Science Fiction",
|
|
68
|
+
sf: "Science Fiction",
|
|
69
|
+
comedy: "Humor",
|
|
70
|
+
humour: "Humor",
|
|
71
|
+
ya: "Teen Fiction",
|
|
72
|
+
youngadult: "Teen Fiction",
|
|
73
|
+
lgbt: "LGBTQ+",
|
|
74
|
+
lgbtq: "LGBTQ+",
|
|
75
|
+
"lgbtqia+": "LGBTQ+",
|
|
76
|
+
historical: "Historical Fiction",
|
|
77
|
+
scary: "Horror",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Map a free-form genre label to its canonical PlotLink value, or `null` if it
|
|
82
|
+
* can't be resolved (#412). PlotLink's metadata update rejects non-canonical
|
|
83
|
+
* genres (e.g. `Sci-Fi` → `Invalid genre`), which once left a published pilot
|
|
84
|
+
* `UNCATEGORIZED`; callers normalize through this before sending metadata and
|
|
85
|
+
* surface a clear local error when it returns `null`. Empty/blank input → `null`.
|
|
86
|
+
*/
|
|
87
|
+
export function canonicalizeGenre(input: string | null | undefined): Genre | null {
|
|
88
|
+
if (!input) return null;
|
|
89
|
+
const key = genreKey(input.trim());
|
|
90
|
+
if (!key) return null;
|
|
91
|
+
return CANONICAL_GENRE_BY_KEY[key] ?? GENRE_ALIAS_BY_KEY[key] ?? null;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plotlink-ows",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.95",
|
|
4
|
+
"packageManager": "npm@10.9.8",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "20.x",
|
|
7
|
+
"npm": "10.x"
|
|
8
|
+
},
|
|
4
9
|
"bin": {
|
|
5
|
-
"plotlink-ows": "
|
|
10
|
+
"plotlink-ows": "bin/plotlink-ows.js"
|
|
6
11
|
},
|
|
7
12
|
"files": [
|
|
8
13
|
"bin/",
|
|
9
14
|
"app/",
|
|
10
15
|
"lib/ows/",
|
|
16
|
+
"lib/genres.ts",
|
|
11
17
|
"packages/",
|
|
12
18
|
"public/",
|
|
13
|
-
"scripts/"
|
|
19
|
+
"scripts/",
|
|
20
|
+
"!**/*.test.ts",
|
|
21
|
+
"!**/*.test.tsx",
|
|
22
|
+
"!**/*.test.js",
|
|
23
|
+
"!**/*.test.jsx",
|
|
24
|
+
"!**/*.test.mjs",
|
|
25
|
+
"!**/*.spec.ts",
|
|
26
|
+
"!**/*.spec.tsx",
|
|
27
|
+
"!**/*.spec.js",
|
|
28
|
+
"!**/__tests__/**",
|
|
29
|
+
"!**/node_modules/**",
|
|
30
|
+
"!**/*.tgz",
|
|
31
|
+
"!**/.next/cache/**",
|
|
32
|
+
"!**/.turbo/**",
|
|
33
|
+
"!**/.vite/**",
|
|
34
|
+
"!**/.cache/**",
|
|
35
|
+
"!**/*.tsbuildinfo",
|
|
36
|
+
"!**/coverage/**",
|
|
37
|
+
"!**/.nyc_output/**",
|
|
38
|
+
"!**/__fixtures__/**",
|
|
39
|
+
"!**/fixtures/**",
|
|
40
|
+
"!**/*.fixture.*",
|
|
41
|
+
"!**/*.snap",
|
|
42
|
+
"!**/screenshot-*",
|
|
43
|
+
"!**/screenshots/**",
|
|
44
|
+
"!**/e2e-verify.*",
|
|
45
|
+
"!**/tmp/**",
|
|
46
|
+
"!**/temp/**",
|
|
47
|
+
"!**/*.log",
|
|
48
|
+
"!**/*.tmp",
|
|
49
|
+
"!**/*.bak",
|
|
50
|
+
"!**/*.swp"
|
|
14
51
|
],
|
|
15
52
|
"workspaces": [
|
|
16
53
|
"packages/*"
|
|
@@ -26,39 +63,31 @@
|
|
|
26
63
|
"app:dev": "concurrently \"tsx watch app/server.ts\" \"vite --config app/vite.config.ts\"",
|
|
27
64
|
"app:build": "vite build --config app/vite.config.ts",
|
|
28
65
|
"app:start": "tsx app/server.ts",
|
|
66
|
+
"preflight": "node scripts/preflight.mjs",
|
|
67
|
+
"smoke:start": "node scripts/start-smoke.mjs",
|
|
29
68
|
"prepublishOnly": "npm run app:build",
|
|
30
69
|
"postinstall": "prisma generate --schema app/prisma/schema.prisma",
|
|
31
70
|
"prisma:local": "prisma generate --schema app/prisma/schema.prisma && prisma db push --schema app/prisma/schema.prisma",
|
|
71
|
+
"prisma:sql": "node scripts/gen-schema-sql.mjs",
|
|
32
72
|
"release:patch": "npm version patch && git push origin main --follow-tags && VERSION=$(node -p 'require(\"./package.json\").version') && gh release create \"v$VERSION\" --generate-notes --latest && npm publish",
|
|
33
73
|
"release:minor": "npm version minor && git push origin main --follow-tags && VERSION=$(node -p 'require(\"./package.json\").version') && gh release create \"v$VERSION\" --generate-notes --latest && npm publish",
|
|
34
74
|
"release:major": "npm version major && git push origin main --follow-tags && VERSION=$(node -p 'require(\"./package.json\").version') && gh release create \"v$VERSION\" --generate-notes --latest && npm publish"
|
|
35
75
|
},
|
|
36
76
|
"dependencies": {
|
|
37
|
-
"@
|
|
38
|
-
"@hono/node-server": "^1.19.12",
|
|
77
|
+
"@hono/node-server": "^1.19.14",
|
|
39
78
|
"@open-wallet-standard/core": "^1.2.4",
|
|
40
79
|
"@prisma/client": "^6.19.3",
|
|
41
80
|
"@supabase/supabase-js": "^2.99.1",
|
|
42
|
-
"@xterm/addon-fit": "^0.11.0",
|
|
43
|
-
"@xterm/addon-serialize": "^0.14.0",
|
|
44
|
-
"@xterm/xterm": "^6.0.0",
|
|
45
81
|
"dotenv": "^17.4.0",
|
|
46
|
-
"hono": "^4.12.
|
|
82
|
+
"hono": "^4.12.23",
|
|
47
83
|
"node-pty": "^1.2.0-beta.12",
|
|
48
84
|
"prisma": "^6.19.3",
|
|
49
|
-
"react": "19.2.3",
|
|
50
|
-
"react-dom": "19.2.3",
|
|
51
|
-
"react-markdown": "^10.1.0",
|
|
52
|
-
"rehype-sanitize": "^6.0.0",
|
|
53
|
-
"remark-breaks": "^4.0.0",
|
|
54
|
-
"remark-gfm": "^4.0.1",
|
|
55
|
-
"tailwindcss": "^4",
|
|
56
85
|
"tsx": "^4.21.0",
|
|
57
86
|
"viem": "^2.47.2",
|
|
58
|
-
"
|
|
59
|
-
"ws": "^8.20.0"
|
|
87
|
+
"ws": "^8.20.1"
|
|
60
88
|
},
|
|
61
89
|
"devDependencies": {
|
|
90
|
+
"@aws-sdk/client-s3": "^3.1009.0",
|
|
62
91
|
"@farcaster/miniapp-node": "^0.1.13",
|
|
63
92
|
"@farcaster/miniapp-sdk": "^0.3.0",
|
|
64
93
|
"@farcaster/miniapp-wagmi-connector": "^2.0.0",
|
|
@@ -77,13 +106,24 @@
|
|
|
77
106
|
"@types/ws": "^8.18.1",
|
|
78
107
|
"@vercel/analytics": "^2.0.1",
|
|
79
108
|
"@vitejs/plugin-react": "^4.7.0",
|
|
109
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
110
|
+
"@xterm/addon-serialize": "^0.14.0",
|
|
111
|
+
"@xterm/xterm": "^6.0.0",
|
|
80
112
|
"concurrently": "^9.2.1",
|
|
81
113
|
"eslint": "^9",
|
|
82
|
-
"eslint-config-next": "16.
|
|
114
|
+
"eslint-config-next": "16.2.6",
|
|
83
115
|
"jsdom": "^27.0.1",
|
|
84
|
-
"next": "16.
|
|
116
|
+
"next": "16.2.6",
|
|
85
117
|
"ox": "^0.14.8",
|
|
118
|
+
"react": "19.2.3",
|
|
119
|
+
"react-dom": "19.2.3",
|
|
120
|
+
"react-markdown": "^10.1.0",
|
|
121
|
+
"rehype-sanitize": "^6.0.0",
|
|
122
|
+
"remark-breaks": "^4.0.0",
|
|
123
|
+
"remark-gfm": "^4.0.1",
|
|
124
|
+
"tailwindcss": "^4",
|
|
86
125
|
"typescript": "^5",
|
|
126
|
+
"vite": "^6.4.3",
|
|
87
127
|
"vitest": "^3.2.4",
|
|
88
128
|
"wagmi": "^2.19.5"
|
|
89
129
|
}
|