plotlink-ows 1.0.32 → 1.2.94

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 (145) hide show
  1. package/README.md +4 -0
  2. package/app/lib/agent-command.ts +85 -0
  3. package/app/lib/agent-readiness.ts +133 -0
  4. package/app/lib/apply-schema.ts +55 -0
  5. package/app/lib/bubble-text.ts +160 -0
  6. package/app/lib/cartoon-coach.ts +198 -0
  7. package/app/lib/cartoon-markdown.ts +83 -0
  8. package/app/lib/cartoon-prompt.ts +122 -0
  9. package/app/lib/cartoon-readiness.ts +811 -0
  10. package/app/lib/clean-image-sync.ts +245 -0
  11. package/app/lib/codex-images.ts +152 -0
  12. package/app/lib/cut-asset-diagnostics.ts +120 -0
  13. package/app/lib/cuts.ts +302 -0
  14. package/app/lib/fonts.ts +109 -0
  15. package/app/lib/generate-claude-md.ts +10 -3
  16. package/app/lib/generate-story-instructions.ts +731 -0
  17. package/app/lib/image-asset-validate.ts +123 -0
  18. package/app/lib/lettering-status.ts +133 -0
  19. package/app/lib/overlays.ts +637 -0
  20. package/app/lib/paths.ts +10 -0
  21. package/app/lib/public-title.ts +65 -0
  22. package/app/lib/publish.ts +16 -2
  23. package/app/lib/story-progress.ts +243 -0
  24. package/app/lib/terminal-protocol.ts +16 -0
  25. package/app/lib/terminal-redact.ts +50 -0
  26. package/app/prisma/schema.sql +25 -0
  27. package/app/routes/agent.ts +42 -0
  28. package/app/routes/codex-images.ts +67 -0
  29. package/app/routes/publish.ts +209 -28
  30. package/app/routes/stories.ts +961 -5
  31. package/app/routes/terminal.ts +383 -31
  32. package/app/server.ts +47 -12
  33. package/app/vite.config.ts +6 -0
  34. package/app/web/components/CartoonPreview.tsx +267 -0
  35. package/app/web/components/CartoonPublishPage.tsx +407 -0
  36. package/app/web/components/CartoonPublishPreview.tsx +121 -0
  37. package/app/web/components/CartoonStepGuide.tsx +90 -0
  38. package/app/web/components/CartoonWorkflowNav.tsx +68 -0
  39. package/app/web/components/CodexImportPicker.tsx +230 -0
  40. package/app/web/components/CutListPanel.tsx +1299 -0
  41. package/app/web/components/EpisodesPage.tsx +80 -0
  42. package/app/web/components/FinishEpisodePanel.tsx +151 -0
  43. package/app/web/components/Layout.tsx +7 -4
  44. package/app/web/components/LetteringEditor.tsx +1141 -0
  45. package/app/web/components/PreviewPanel.tsx +1017 -144
  46. package/app/web/components/Settings.tsx +63 -0
  47. package/app/web/components/StoriesPage.tsx +710 -33
  48. package/app/web/components/StoryBrowser.tsx +22 -14
  49. package/app/web/components/StoryInfoPage.tsx +266 -0
  50. package/app/web/components/StoryProgressPanel.tsx +516 -0
  51. package/app/web/components/TerminalPanel.tsx +233 -11
  52. package/app/web/components/WorkflowCoach.tsx +128 -0
  53. package/app/web/components/asset-image.tsx +114 -0
  54. package/app/web/components/asset-test-utils.ts +44 -0
  55. package/app/web/components/export-cut.ts +320 -0
  56. package/app/web/dist/assets/export-cut-nKQ_n2-J.js +1 -0
  57. package/app/web/dist/assets/index-BAZGwVwj.js +143 -0
  58. package/app/web/dist/assets/index-DoXH2OlP.css +32 -0
  59. package/app/web/dist/index.html +2 -2
  60. package/app/web/lib/cartoon-publish-summary.ts +43 -0
  61. package/app/web/lib/codex-import.ts +94 -0
  62. package/app/web/lib/image-compress.ts +53 -0
  63. package/app/web/lib/import-image.ts +58 -0
  64. package/app/web/lib/publish-helpers.ts +385 -0
  65. package/app/web/lib/upload-retry.ts +130 -0
  66. package/app/web/lib/verify-public-title.ts +105 -0
  67. package/app/web/styles.css +9 -0
  68. package/bin/plotlink-ows.js +53 -16
  69. package/bin/startup-plan.cjs +58 -0
  70. package/lib/genres.ts +92 -0
  71. package/package.json +60 -20
  72. package/scripts/gen-schema-sql.mjs +49 -0
  73. package/scripts/package-hygiene.mjs +116 -0
  74. package/scripts/preflight.mjs +173 -0
  75. package/scripts/start-smoke.mjs +128 -0
  76. package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
  77. package/app/node_modules/.prisma/local-client/client.js +0 -5
  78. package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
  79. package/app/node_modules/.prisma/local-client/default.js +0 -5
  80. package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
  81. package/app/node_modules/.prisma/local-client/edge.js +0 -184
  82. package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
  83. package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
  84. package/app/node_modules/.prisma/local-client/index.js +0 -207
  85. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  86. package/app/node_modules/.prisma/local-client/package.json +0 -183
  87. package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
  88. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  89. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
  90. package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
  91. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
  92. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
  93. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
  94. package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
  95. package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
  96. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
  97. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
  98. package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
  99. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
  100. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
  101. package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
  102. package/app/node_modules/.prisma/local-client/wasm.js +0 -191
  103. package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
  104. package/app/web/dist/assets/index-BFw-v-OZ.js +0 -134
  105. package/packages/cli/node_modules/commander/LICENSE +0 -22
  106. package/packages/cli/node_modules/commander/Readme.md +0 -1149
  107. package/packages/cli/node_modules/commander/esm.mjs +0 -16
  108. package/packages/cli/node_modules/commander/index.js +0 -24
  109. package/packages/cli/node_modules/commander/lib/argument.js +0 -149
  110. package/packages/cli/node_modules/commander/lib/command.js +0 -2662
  111. package/packages/cli/node_modules/commander/lib/error.js +0 -39
  112. package/packages/cli/node_modules/commander/lib/help.js +0 -709
  113. package/packages/cli/node_modules/commander/lib/option.js +0 -367
  114. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
  115. package/packages/cli/node_modules/commander/package-support.json +0 -16
  116. package/packages/cli/node_modules/commander/package.json +0 -82
  117. package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
  118. package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
  119. package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
  120. package/packages/cli/node_modules/resolve-from/index.js +0 -47
  121. package/packages/cli/node_modules/resolve-from/license +0 -9
  122. package/packages/cli/node_modules/resolve-from/package.json +0 -36
  123. package/packages/cli/node_modules/resolve-from/readme.md +0 -72
  124. package/packages/cli/node_modules/tsup/LICENSE +0 -21
  125. package/packages/cli/node_modules/tsup/README.md +0 -75
  126. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
  127. package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
  128. package/packages/cli/node_modules/tsup/assets/package.json +0 -3
  129. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
  130. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
  131. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
  132. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
  133. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
  134. package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
  135. package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
  136. package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
  137. package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
  138. package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
  139. package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
  140. package/packages/cli/node_modules/tsup/package.json +0 -99
  141. package/packages/cli/node_modules/tsup/schema.json +0 -362
  142. package/public/screenshot-1.png +0 -0
  143. package/public/screenshot-2.png +0 -0
  144. package/public/screenshot-3.png +0 -0
  145. package/scripts/e2e-verify.ts +0 -1100
@@ -0,0 +1,123 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { sniffImageType, type SniffedType } from "./clean-image-sync";
4
+
5
+ // Shared filesystem validation for cartoon clean/final image assets. A single
6
+ // source of truth for "is this recorded/candidate relative path a real, valid
7
+ // WebP/JPEG file" — used by the sync/detect endpoints and by the stale-path
8
+ // detection that gates publish readiness (#302).
9
+
10
+ export const CLEAN_IMAGE_MAX_BYTES = 1024 * 1024;
11
+ export const CLEAN_IMAGE_VALID_EXT = new Set(["webp", "jpg", "jpeg"]);
12
+
13
+ /** Map an allowed file extension to the image type its content must match. */
14
+ export const CLEAN_IMAGE_EXT_TO_TYPE: Record<string, Exclude<SniffedType, "unknown">> = {
15
+ webp: "webp",
16
+ jpg: "jpeg",
17
+ jpeg: "jpeg",
18
+ };
19
+
20
+ /**
21
+ * Validate a relative asset path against the real filesystem. Returns `null`
22
+ * when the file exists and is a valid WebP/JPEG (regular file, allowed
23
+ * extension, <=1MB, magic-byte content matches the extension); otherwise a short
24
+ * reason string. Filesystem read only — never mutates anything.
25
+ *
26
+ * `"missing"` covers a non-existent path, a non-regular file, an unreadable
27
+ * file, or a path that escapes the story's `assets/` tree; the other reasons
28
+ * describe a present-but-invalid asset.
29
+ */
30
+ export function imageAssetIssue(storyDir: string, relPath: string): string | null {
31
+ // Recorded cut asset paths come from cuts.json and must be a canonical
32
+ // RELATIVE path inside the story's assets/ tree. Reject non-canonical forms
33
+ // before any filesystem read so a recorded path cannot be trusted as a local
34
+ // asset on a technicality:
35
+ // - absolute paths (even ones that point inside assets/);
36
+ // - any `..` path segment (even when it resolves back inside assets/, e.g.
37
+ // "assets/plot-01/../evil.webp" → "assets/evil.webp").
38
+ // Then a resolved-boundary check rejects anything that still escapes assets/.
39
+ if (path.isAbsolute(relPath)) return "missing";
40
+ if (relPath.split(/[/\\]/).includes("..")) return "missing";
41
+
42
+ const assetsRoot = path.resolve(storyDir, "assets");
43
+ const abs = path.resolve(storyDir, relPath);
44
+ if (abs !== assetsRoot && !abs.startsWith(assetsRoot + path.sep)) return "missing";
45
+
46
+ if (!fs.existsSync(abs)) return "missing";
47
+
48
+ let stat: fs.Stats;
49
+ try {
50
+ stat = fs.statSync(abs);
51
+ } catch {
52
+ return "missing";
53
+ }
54
+ if (!stat.isFile()) return "missing";
55
+
56
+ const ext = path.extname(relPath).slice(1).toLowerCase();
57
+ if (!CLEAN_IMAGE_VALID_EXT.has(ext)) return `Unsupported extension .${ext}`;
58
+ if (stat.size > CLEAN_IMAGE_MAX_BYTES) return "File must be under 1MB";
59
+
60
+ // Sniff the real content so a text file (or a renamed/mismatched image) named
61
+ // `.webp`/`.jpg` cannot pass on extension alone.
62
+ let sniffed: SniffedType;
63
+ try {
64
+ const fd = fs.openSync(abs, "r");
65
+ try {
66
+ const head = Buffer.alloc(16);
67
+ const read = fs.readSync(fd, head, 0, 16, 0);
68
+ sniffed = sniffImageType(head.subarray(0, read));
69
+ } finally {
70
+ fs.closeSync(fd);
71
+ }
72
+ } catch {
73
+ return "missing";
74
+ }
75
+
76
+ if (sniffed === "unknown") return "not a valid image (content does not match WebP/JPEG/PNG)";
77
+ if (sniffed !== CLEAN_IMAGE_EXT_TO_TYPE[ext]) return `content does not match .${ext} extension`;
78
+ return null;
79
+ }
80
+
81
+ /** True when a relative asset path is a real, valid WebP/JPEG file on disk. */
82
+ export function isValidImageAsset(storyDir: string, relPath: string): boolean {
83
+ return imageAssetIssue(storyDir, relPath) === null;
84
+ }
85
+
86
+ /**
87
+ * True when a relative asset path points to a real PNG image on disk (#441).
88
+ * PNG is NOT a publishable clean format — but it is a normal intermediate that
89
+ * the writer converts to WebP/JPEG, so detection treats it as a conversion step
90
+ * rather than a hard "unsupported extension" error. Same traversal guards as
91
+ * `imageAssetIssue`; deliberately does NOT gate on the 1MB size limit (the
92
+ * browser conversion compresses, so an oversize PNG is still convertible).
93
+ */
94
+ export function pngAssetExists(storyDir: string, relPath: string): boolean {
95
+ if (path.isAbsolute(relPath)) return false;
96
+ if (relPath.split(/[/\\]/).includes("..")) return false;
97
+
98
+ const assetsRoot = path.resolve(storyDir, "assets");
99
+ const abs = path.resolve(storyDir, relPath);
100
+ if (abs !== assetsRoot && !abs.startsWith(assetsRoot + path.sep)) return false;
101
+
102
+ if (!fs.existsSync(abs)) return false;
103
+ let stat: fs.Stats;
104
+ try {
105
+ stat = fs.statSync(abs);
106
+ } catch {
107
+ return false;
108
+ }
109
+ if (!stat.isFile()) return false;
110
+
111
+ try {
112
+ const fd = fs.openSync(abs, "r");
113
+ try {
114
+ const head = Buffer.alloc(16);
115
+ const read = fs.readSync(fd, head, 0, 16, 0);
116
+ return sniffImageType(head.subarray(0, read)) === "png";
117
+ } finally {
118
+ fs.closeSync(fd);
119
+ }
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
@@ -0,0 +1,133 @@
1
+ // Per-cut lettering status + "insert from script" helpers for the cartoon
2
+ // lettering editor guidance (#336). Pure and UI-agnostic so they can be unit
3
+ // tested and shared between the editor checklist and the insert-from-script
4
+ // panel. None of this changes the export model or publish readiness rules.
5
+
6
+ import type { Overlay } from "./overlays";
7
+
8
+ /** The cut fields the lettering guidance reads (a structural subset of Cut). */
9
+ export interface LetteringCut {
10
+ cleanImagePath?: string | null;
11
+ finalImagePath?: string | null;
12
+ exportedAt?: string | null;
13
+ uploadedUrl?: string | null;
14
+ uploadedCid?: string | null;
15
+ narration?: string;
16
+ sfx?: string;
17
+ dialogue?: { speaker: string; text: string }[];
18
+ overlays?: Overlay[];
19
+ }
20
+
21
+ export interface LetteringChecklist {
22
+ /** A clean (text-free) image has been recorded for the cut. */
23
+ hasCleanImage: boolean;
24
+ /** The cut plan carries script text (dialogue, narration, or SFX) to letter. */
25
+ hasScriptText: boolean;
26
+ /** How many overlays (bubbles/captions/SFX) have been placed. */
27
+ bubblesPlaced: number;
28
+ /** A final lettered image has been exported. */
29
+ exported: boolean;
30
+ /** An uploaded URL/CID is recorded for the cut. */
31
+ uploaded: boolean;
32
+ }
33
+
34
+ /**
35
+ * Summarize a single cut's lettering progress for the editor's status strip
36
+ * (#336): clean image present → script text available → bubbles placed →
37
+ * exported → uploaded. Read-only; derived straight from the cut record.
38
+ *
39
+ * `opts.staleExport` (#336, re1): when the writer has edited the overlays since
40
+ * the recorded export, the existing final image / uploaded URL no longer match
41
+ * what's on screen, so export & upload are reported as NOT done — the writer
42
+ * must re-export before those steps count again.
43
+ */
44
+ export function cutLetteringChecklist(
45
+ cut: LetteringCut,
46
+ opts: { staleExport?: boolean } = {},
47
+ ): LetteringChecklist {
48
+ const exported = !opts.staleExport && (!!cut.finalImagePath || !!cut.exportedAt);
49
+ const uploaded = !opts.staleExport && (!!cut.uploadedUrl || !!cut.uploadedCid);
50
+ return {
51
+ hasCleanImage: !!cut.cleanImagePath,
52
+ hasScriptText:
53
+ (cut.dialogue?.length ?? 0) > 0 || !!cut.narration?.trim() || !!cut.sfx?.trim(),
54
+ bubblesPlaced: cut.overlays?.length ?? 0,
55
+ exported,
56
+ uploaded,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Stable signature of an overlay set for change detection (#336). Captures only
62
+ * the fields that affect the rendered/exported image (type, geometry, text,
63
+ * speaker, tail), so reordering of unrelated metadata doesn't matter and any
64
+ * real edit changes the signature.
65
+ */
66
+ export function overlaysSignature(overlays: Overlay[] | undefined): string {
67
+ return JSON.stringify(
68
+ (overlays ?? []).map((o) => [
69
+ o.type,
70
+ o.x,
71
+ o.y,
72
+ o.width,
73
+ o.height,
74
+ o.text,
75
+ o.speaker ?? "",
76
+ o.tailAnchor ?? null,
77
+ o.textStyle ?? null,
78
+ o.bubbleStyle ?? null,
79
+ ]),
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Whether a cut's recorded export/upload is stale because the overlays were
85
+ * edited since (#336, re1). Only meaningful once the cut has actually been
86
+ * exported or uploaded; compares the current overlays against the baseline that
87
+ * was on screen when the editor opened (already normalized the same way), so a
88
+ * load-time normalization is not mistaken for a user edit.
89
+ */
90
+ export function isExportStale(opts: {
91
+ exported: boolean;
92
+ uploaded: boolean;
93
+ /** Signature of the overlays that match the recorded export (see overlaysSignature). */
94
+ baselineSig: string;
95
+ current: Overlay[] | undefined;
96
+ }): boolean {
97
+ if (!opts.exported && !opts.uploaded) return false;
98
+ return opts.baselineSig !== overlaysSignature(opts.current);
99
+ }
100
+
101
+ export type ScriptLineType = "speech" | "narration" | "sfx";
102
+
103
+ /** A piece of the cut's script the writer can drop straight into an overlay. */
104
+ export interface ScriptLine {
105
+ type: ScriptLineType;
106
+ /** Speaker for a dialogue line; undefined for narration/SFX. */
107
+ speaker?: string;
108
+ text: string;
109
+ /** Stable key for list rendering / dedupe (type + index within its kind). */
110
+ key: string;
111
+ }
112
+
113
+ /**
114
+ * Flatten a cut's `cuts.json` script (dialogue lines, narration, SFX) into the
115
+ * ordered list the editor offers as one-click "insert into a bubble" actions
116
+ * (#336) — so a writer never has to hand-copy text out of the JSON. Empty
117
+ * pieces are skipped.
118
+ */
119
+ export function cutScriptLines(cut: LetteringCut): ScriptLine[] {
120
+ const lines: ScriptLine[] = [];
121
+ (cut.dialogue ?? []).forEach((d, i) => {
122
+ if (d?.text?.trim()) {
123
+ lines.push({ type: "speech", speaker: d.speaker, text: d.text.trim(), key: `speech-${i}` });
124
+ }
125
+ });
126
+ if (cut.narration?.trim()) {
127
+ lines.push({ type: "narration", text: cut.narration.trim(), key: "narration" });
128
+ }
129
+ if (cut.sfx?.trim()) {
130
+ lines.push({ type: "sfx", text: cut.sfx.trim(), key: "sfx" });
131
+ }
132
+ return lines;
133
+ }