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.
Files changed (152) hide show
  1. package/README.md +4 -0
  2. package/app/lib/active-wallet.ts +260 -0
  3. package/app/lib/agent-command.ts +85 -0
  4. package/app/lib/agent-readiness.ts +133 -0
  5. package/app/lib/apply-schema.ts +55 -0
  6. package/app/lib/bubble-text.ts +160 -0
  7. package/app/lib/cartoon-coach.ts +198 -0
  8. package/app/lib/cartoon-markdown.ts +83 -0
  9. package/app/lib/cartoon-prompt.ts +122 -0
  10. package/app/lib/cartoon-readiness.ts +813 -0
  11. package/app/lib/clean-image-sync.ts +245 -0
  12. package/app/lib/codex-images.ts +152 -0
  13. package/app/lib/cut-asset-diagnostics.ts +120 -0
  14. package/app/lib/cuts.ts +302 -0
  15. package/app/lib/fonts.ts +109 -0
  16. package/app/lib/generate-claude-md.ts +8 -1
  17. package/app/lib/generate-story-instructions.ts +731 -0
  18. package/app/lib/image-asset-validate.ts +123 -0
  19. package/app/lib/lettering-status.ts +133 -0
  20. package/app/lib/overlays.ts +637 -0
  21. package/app/lib/paths.ts +10 -0
  22. package/app/lib/public-title.ts +65 -0
  23. package/app/lib/publish.ts +16 -2
  24. package/app/lib/story-progress.ts +242 -0
  25. package/app/lib/terminal-protocol.ts +16 -0
  26. package/app/lib/terminal-redact.ts +50 -0
  27. package/app/prisma/schema.sql +25 -0
  28. package/app/routes/agent.ts +42 -0
  29. package/app/routes/codex-images.ts +67 -0
  30. package/app/routes/dashboard.ts +6 -4
  31. package/app/routes/publish.ts +259 -45
  32. package/app/routes/settings.ts +92 -37
  33. package/app/routes/stories.ts +961 -5
  34. package/app/routes/terminal.ts +383 -31
  35. package/app/routes/wallet.ts +58 -30
  36. package/app/server.ts +47 -12
  37. package/app/vite.config.ts +6 -0
  38. package/app/web/components/CartoonNextAction.tsx +145 -0
  39. package/app/web/components/CartoonPreview.tsx +267 -0
  40. package/app/web/components/CartoonPublishPage.tsx +407 -0
  41. package/app/web/components/CartoonPublishPreview.tsx +121 -0
  42. package/app/web/components/CartoonStepGuide.tsx +90 -0
  43. package/app/web/components/CartoonWorkflowNav.tsx +68 -0
  44. package/app/web/components/CodexImportPicker.tsx +230 -0
  45. package/app/web/components/CutListPanel.tsx +1337 -0
  46. package/app/web/components/Dashboard.tsx +15 -6
  47. package/app/web/components/EpisodesPage.tsx +80 -0
  48. package/app/web/components/FinishEpisodePanel.tsx +151 -0
  49. package/app/web/components/Layout.tsx +7 -4
  50. package/app/web/components/LetteringEditor.tsx +1182 -0
  51. package/app/web/components/PreviewPanel.tsx +952 -78
  52. package/app/web/components/Settings.tsx +63 -0
  53. package/app/web/components/StoriesPage.tsx +745 -33
  54. package/app/web/components/StoryBrowser.tsx +22 -14
  55. package/app/web/components/StoryInfoPage.tsx +266 -0
  56. package/app/web/components/StoryProgressPanel.tsx +446 -0
  57. package/app/web/components/TerminalPanel.tsx +233 -11
  58. package/app/web/components/WalletCard.tsx +110 -8
  59. package/app/web/components/WorkflowCoach.tsx +156 -0
  60. package/app/web/components/asset-image.tsx +114 -0
  61. package/app/web/components/asset-test-utils.ts +44 -0
  62. package/app/web/components/export-cut.ts +320 -0
  63. package/app/web/dist/assets/export-cut-che5mMWc.js +1 -0
  64. package/app/web/dist/assets/index-CcfChGEK.css +32 -0
  65. package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -0
  66. package/app/web/dist/index.html +2 -2
  67. package/app/web/lib/cartoon-publish-summary.ts +43 -0
  68. package/app/web/lib/codex-import.ts +94 -0
  69. package/app/web/lib/image-compress.ts +53 -0
  70. package/app/web/lib/import-image.ts +58 -0
  71. package/app/web/lib/publish-helpers.ts +385 -0
  72. package/app/web/lib/upload-retry.ts +130 -0
  73. package/app/web/lib/verify-public-title.ts +105 -0
  74. package/app/web/styles.css +9 -0
  75. package/bin/plotlink-ows.js +53 -16
  76. package/bin/startup-plan.cjs +58 -0
  77. package/lib/genres.ts +92 -0
  78. package/package.json +60 -20
  79. package/scripts/gen-schema-sql.mjs +49 -0
  80. package/scripts/package-hygiene.mjs +116 -0
  81. package/scripts/preflight.mjs +173 -0
  82. package/scripts/start-smoke.mjs +128 -0
  83. package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
  84. package/app/node_modules/.prisma/local-client/client.js +0 -5
  85. package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
  86. package/app/node_modules/.prisma/local-client/default.js +0 -5
  87. package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
  88. package/app/node_modules/.prisma/local-client/edge.js +0 -184
  89. package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
  90. package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
  91. package/app/node_modules/.prisma/local-client/index.js +0 -207
  92. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  93. package/app/node_modules/.prisma/local-client/package.json +0 -183
  94. package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
  95. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  96. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
  97. package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
  98. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
  99. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
  100. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
  101. package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
  102. package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
  103. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
  104. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
  105. package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
  106. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
  107. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
  108. package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
  109. package/app/node_modules/.prisma/local-client/wasm.js +0 -191
  110. package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
  111. package/app/web/dist/assets/index-DxATSk7X.js +0 -134
  112. package/packages/cli/node_modules/commander/LICENSE +0 -22
  113. package/packages/cli/node_modules/commander/Readme.md +0 -1149
  114. package/packages/cli/node_modules/commander/esm.mjs +0 -16
  115. package/packages/cli/node_modules/commander/index.js +0 -24
  116. package/packages/cli/node_modules/commander/lib/argument.js +0 -149
  117. package/packages/cli/node_modules/commander/lib/command.js +0 -2662
  118. package/packages/cli/node_modules/commander/lib/error.js +0 -39
  119. package/packages/cli/node_modules/commander/lib/help.js +0 -709
  120. package/packages/cli/node_modules/commander/lib/option.js +0 -367
  121. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
  122. package/packages/cli/node_modules/commander/package-support.json +0 -16
  123. package/packages/cli/node_modules/commander/package.json +0 -82
  124. package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
  125. package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
  126. package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
  127. package/packages/cli/node_modules/resolve-from/index.js +0 -47
  128. package/packages/cli/node_modules/resolve-from/license +0 -9
  129. package/packages/cli/node_modules/resolve-from/package.json +0 -36
  130. package/packages/cli/node_modules/resolve-from/readme.md +0 -72
  131. package/packages/cli/node_modules/tsup/LICENSE +0 -21
  132. package/packages/cli/node_modules/tsup/README.md +0 -75
  133. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
  134. package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
  135. package/packages/cli/node_modules/tsup/assets/package.json +0 -3
  136. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
  137. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
  138. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
  139. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
  140. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
  141. package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
  142. package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
  143. package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
  144. package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
  145. package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
  146. package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
  147. package/packages/cli/node_modules/tsup/package.json +0 -99
  148. package/packages/cli/node_modules/tsup/schema.json +0 -362
  149. package/public/screenshot-1.png +0 -0
  150. package/public/screenshot-2.png +0 -0
  151. package/public/screenshot-3.png +0 -0
  152. package/scripts/e2e-verify.ts +0 -1100
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Persistent right-panel workflow navigation for cartoon stories (#439, spec §2).
3
+ *
4
+ * A normal webtoon creator should not need the file tree: this compact tab bar
5
+ * sits above the right-panel content whenever a CARTOON story is selected and
6
+ * routes between the workflow pages — Progress, Story Info, Whitepaper, Genesis /
7
+ * Episode 1, Episodes, Publish. The left file tree stays for power users; opening
8
+ * a file directly just reflects the closest workflow tab here.
9
+ *
10
+ * Fiction renders no nav (the caller only mounts this for cartoon stories), so
11
+ * the fiction UX is unchanged.
12
+ */
13
+
14
+ export type CartoonWorkflowTab =
15
+ | "progress"
16
+ | "story-info"
17
+ | "whitepaper"
18
+ | "genesis"
19
+ | "episodes"
20
+ | "publish";
21
+
22
+ const TABS: { key: CartoonWorkflowTab; label: string }[] = [
23
+ { key: "progress", label: "Progress" },
24
+ { key: "story-info", label: "Story Info" },
25
+ { key: "whitepaper", label: "Whitepaper" },
26
+ { key: "genesis", label: "Genesis / Ep 1" },
27
+ { key: "episodes", label: "Episodes" },
28
+ { key: "publish", label: "Publish" },
29
+ ];
30
+
31
+ interface CartoonWorkflowNavProps {
32
+ storyTitle: string;
33
+ active: CartoonWorkflowTab;
34
+ onSelect: (tab: CartoonWorkflowTab) => void;
35
+ }
36
+
37
+ export function CartoonWorkflowNav({ storyTitle, active, onSelect }: CartoonWorkflowNavProps) {
38
+ return (
39
+ <div className="flex-shrink-0 border-b border-border bg-surface/40" data-testid="cartoon-workflow-nav">
40
+ <div className="flex items-center gap-2 px-3 pt-2">
41
+ <span className="text-[10px] font-medium uppercase tracking-[0.14em] text-accent">Cartoon</span>
42
+ <span className="text-xs font-serif text-foreground truncate">{storyTitle}</span>
43
+ </div>
44
+ <div className="flex items-center gap-1 px-2 py-1.5 overflow-x-auto" role="tablist">
45
+ {TABS.map((tab) => {
46
+ const isActive = tab.key === active;
47
+ return (
48
+ <button
49
+ key={tab.key}
50
+ role="tab"
51
+ aria-selected={isActive}
52
+ data-testid={`nav-tab-${tab.key}`}
53
+ data-active={isActive}
54
+ onClick={() => onSelect(tab.key)}
55
+ className={`flex-shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors ${
56
+ isActive
57
+ ? "bg-accent text-white"
58
+ : "text-muted hover:text-foreground hover:bg-surface"
59
+ }`}
60
+ >
61
+ {tab.label}
62
+ </button>
63
+ );
64
+ })}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,230 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { listCodexCacheImages, fetchCodexCacheFile, type CodexCacheImage } from "../lib/codex-import";
3
+
4
+ type AuthFetch = (url: string, opts?: RequestInit) => Promise<Response>;
5
+
6
+ /**
7
+ * Codex generated-image cache picker (#403, visual selection + filtering #409).
8
+ *
9
+ * Lists the recent images in Codex's generated-image cache (newest first) and
10
+ * lets the writer import one straight into the current cut — so a Codex-generated
11
+ * PNG no longer requires hunting through a hidden `~/.codex/generated_images`
12
+ * folder in an OS file dialog. Picking an image fetches its bytes as a File and
13
+ * hands it to `onImport`, which runs the SAME in-browser PNG→WebP conversion +
14
+ * upload-clean path as a manually-selected file, so the asset constraints and
15
+ * upload validation are unchanged.
16
+ *
17
+ * #409: the cache can hold a long run of near-identical `ig_<hash>.png` names, so
18
+ * the picker is built for *visual* selection — a large thumbnail leads each row,
19
+ * the noisy hash filename is demoted to a hover title, and the readable metadata
20
+ * (how recently it was generated + its size) is what the writer reads. A filter
21
+ * box narrows a long list by filename. The list stays read-only until the writer
22
+ * explicitly clicks Import.
23
+ *
24
+ * Read-only and best-effort: a missing/empty cache (e.g. Codex not installed)
25
+ * simply shows an empty state with no error, since this is an optional shortcut
26
+ * over the still-present manual "Upload clean image" button.
27
+ */
28
+
29
+ /** Load an auth-protected URL as a blob object URL for an <img> thumbnail. */
30
+ function useAuthedObjectUrl(url: string, authFetch: AuthFetch): string | null {
31
+ const [objectUrl, setObjectUrl] = useState<string | null>(null);
32
+ useEffect(() => {
33
+ let revoked: string | null = null;
34
+ let cancelled = false;
35
+ (async () => {
36
+ try {
37
+ const res = await authFetch(url);
38
+ if (!res.ok) return;
39
+ const blob = await res.blob();
40
+ if (cancelled) return;
41
+ revoked = URL.createObjectURL(blob);
42
+ setObjectUrl(revoked);
43
+ } catch {
44
+ /* best-effort thumbnail; the row still imports without it */
45
+ }
46
+ })();
47
+ return () => {
48
+ cancelled = true;
49
+ if (revoked) URL.revokeObjectURL(revoked);
50
+ };
51
+ }, [url, authFetch]);
52
+ return objectUrl;
53
+ }
54
+
55
+ export function formatSize(bytes: number): string {
56
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
57
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
58
+ return `${bytes} B`;
59
+ }
60
+
61
+ /**
62
+ * Human "how long ago" label for a cache image's mtime (#409). Pure and
63
+ * now-injectable so it's deterministic in tests. The cache lists newest-first, so
64
+ * this is the writer's main cue for "which one did I just generate".
65
+ */
66
+ export function formatRelativeTime(mtimeMs: number, nowMs: number): string {
67
+ const diff = nowMs - mtimeMs;
68
+ if (!Number.isFinite(diff) || diff < 45_000) return "just now";
69
+ const mins = Math.round(diff / 60_000);
70
+ if (mins < 60) return `${mins}m ago`;
71
+ const hours = Math.round(diff / 3_600_000);
72
+ if (hours < 24) return `${hours}h ago`;
73
+ const days = Math.round(diff / 86_400_000);
74
+ if (days < 7) return `${days}d ago`;
75
+ const weeks = Math.round(diff / (7 * 86_400_000));
76
+ return `${weeks}w ago`;
77
+ }
78
+
79
+ function CodexThumb({ image, authFetch }: { image: CodexCacheImage; authFetch: AuthFetch }) {
80
+ const url = useAuthedObjectUrl(`/api/codex/images/${encodeURIComponent(image.token)}`, authFetch);
81
+ if (!url) {
82
+ return <div className="w-16 h-16 flex-shrink-0 rounded border border-border bg-surface" />;
83
+ }
84
+ return (
85
+ <img
86
+ src={url}
87
+ alt={image.name}
88
+ className="w-16 h-16 flex-shrink-0 rounded border border-border object-cover bg-white"
89
+ />
90
+ );
91
+ }
92
+
93
+ export function CodexImportPicker({
94
+ authFetch,
95
+ cutId,
96
+ onImport,
97
+ onClose,
98
+ }: {
99
+ authFetch: AuthFetch;
100
+ cutId: number;
101
+ /** Receives the fetched cache file; runs the shared PNG→WebP import + upload. */
102
+ onImport: (file: File) => Promise<void>;
103
+ onClose: () => void;
104
+ }) {
105
+ const [images, setImages] = useState<CodexCacheImage[] | null>(null);
106
+ const [error, setError] = useState<string | null>(null);
107
+ const [importingToken, setImportingToken] = useState<string | null>(null);
108
+ const [query, setQuery] = useState("");
109
+
110
+ useEffect(() => {
111
+ let cancelled = false;
112
+ (async () => {
113
+ const list = await listCodexCacheImages(authFetch);
114
+ if (!cancelled) setImages(list);
115
+ })();
116
+ return () => {
117
+ cancelled = true;
118
+ };
119
+ }, [authFetch]);
120
+
121
+ const trimmedQuery = query.trim().toLowerCase();
122
+ const filtered = useMemo(() => {
123
+ if (!images) return [];
124
+ if (!trimmedQuery) return images;
125
+ return images.filter((img) => img.name.toLowerCase().includes(trimmedQuery));
126
+ }, [images, trimmedQuery]);
127
+
128
+ // One timestamp per render so all rows share the same "x ago" reference point.
129
+ const now = Date.now();
130
+
131
+ const handlePick = async (image: CodexCacheImage) => {
132
+ setError(null);
133
+ setImportingToken(image.token);
134
+ try {
135
+ const file = await fetchCodexCacheFile(authFetch, image);
136
+ await onImport(file);
137
+ } catch (err) {
138
+ setError(err instanceof Error ? err.message : "Could not import the generated image");
139
+ } finally {
140
+ setImportingToken(null);
141
+ }
142
+ };
143
+
144
+ const hasImages = images !== null && images.length > 0;
145
+
146
+ return (
147
+ <div
148
+ className="rounded border border-border bg-surface/60 p-2 space-y-2"
149
+ data-testid={`codex-picker-${cutId}`}
150
+ >
151
+ <div className="flex items-center justify-between">
152
+ <p className="text-[11px] font-medium text-foreground">Import a Codex-generated image</p>
153
+ <button
154
+ onClick={onClose}
155
+ data-testid={`codex-picker-close-${cutId}`}
156
+ className="text-[11px] text-muted hover:text-foreground"
157
+ >
158
+ Close
159
+ </button>
160
+ </div>
161
+
162
+ {hasImages && (
163
+ <div className="flex items-center gap-2">
164
+ <input
165
+ type="search"
166
+ value={query}
167
+ onChange={(e) => setQuery(e.target.value)}
168
+ placeholder="Filter by file name…"
169
+ data-testid={`codex-picker-search-${cutId}`}
170
+ className="min-w-0 flex-1 px-2 py-1 text-[11px] border border-border rounded bg-transparent focus:border-accent focus:outline-none"
171
+ />
172
+ <span className="text-[10px] text-muted whitespace-nowrap" data-testid={`codex-picker-count-${cutId}`}>
173
+ {trimmedQuery ? `${filtered.length} of ${images!.length}` : `${images!.length} image${images!.length === 1 ? "" : "s"}`}
174
+ </span>
175
+ </div>
176
+ )}
177
+
178
+ {images === null && (
179
+ <p className="text-[11px] text-muted" data-testid={`codex-picker-loading-${cutId}`}>
180
+ Looking for generated images…
181
+ </p>
182
+ )}
183
+
184
+ {images !== null && images.length === 0 && (
185
+ <p className="text-[11px] text-muted" data-testid={`codex-picker-empty-${cutId}`}>
186
+ No generated images found in the Codex cache yet. Generate art in Codex, then reopen this
187
+ list — or use &ldquo;Upload clean image&rdquo; to pick a file.
188
+ </p>
189
+ )}
190
+
191
+ {hasImages && filtered.length === 0 && (
192
+ <p className="text-[11px] text-muted" data-testid={`codex-picker-no-match-${cutId}`}>
193
+ No generated images match &ldquo;{query.trim()}&rdquo;.
194
+ </p>
195
+ )}
196
+
197
+ {hasImages && filtered.length > 0 && (
198
+ <ul className="space-y-1 max-h-72 overflow-y-auto">
199
+ {filtered.map((img) => (
200
+ <li
201
+ key={img.token}
202
+ data-testid={`codex-image-${img.token}`}
203
+ className="flex items-center gap-2 rounded border border-border bg-background/40 p-1.5"
204
+ >
205
+ <CodexThumb image={img} authFetch={authFetch} />
206
+ <div className="min-w-0 flex-1">
207
+ <p className="text-[11px] text-foreground">
208
+ {formatRelativeTime(img.mtimeMs, now)} · {formatSize(img.size)}
209
+ </p>
210
+ <p className="truncate text-[10px] font-mono text-muted" title={img.name}>
211
+ {img.name}
212
+ </p>
213
+ </div>
214
+ <button
215
+ onClick={() => handlePick(img)}
216
+ disabled={importingToken !== null}
217
+ data-testid={`codex-import-${img.token}`}
218
+ className="px-2 py-1 text-[11px] border border-accent/30 text-accent rounded hover:bg-accent/5 disabled:opacity-50"
219
+ >
220
+ {importingToken === img.token ? "Importing…" : "Import to this cut"}
221
+ </button>
222
+ </li>
223
+ ))}
224
+ </ul>
225
+ )}
226
+
227
+ {error && <p className="text-[11px] text-error">{error}</p>}
228
+ </div>
229
+ );
230
+ }