ossclip 0.1.24 → 0.1.25
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/editor-dist/assets/index-C9n6EfII.js +163 -0
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/analyze.ts +17 -1
- package/src/doctor.ts +7 -5
- package/src/edit.ts +495 -3
- package/src/interactive/ask-input.ts +11 -3
- package/src/interactive/pick-save-path.ts +214 -0
- package/src/interactive/produce-argv.ts +39 -2
- package/src/interactive/produce-wizard.ts +236 -35
- package/src/interactive/thumbnail-approve.ts +217 -0
- package/src/open.ts +26 -8
- package/src/paths.ts +88 -0
- package/src/portrait-override.ts +86 -0
- package/src/produce.ts +1607 -133
- package/src/program.ts +104 -6
- package/src/replay-argv.ts +75 -0
- package/src/setup/manifest.ts +93 -4
- package/src/setup/plan.ts +17 -6
- package/src/setup/setup.ts +28 -7
- package/src/thumbnail-panel.ts +167 -0
- package/src/ui/animation.ts +10 -0
- package/editor-dist/assets/index-MLGz89mM.js +0 -163
package/src/paths.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { copyFile, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Out-path safety helpers (2026-08-16 field incident): the wizard's output
|
|
8
|
+
* prompt is a plain text input, and NO shell expands a wizard text input —
|
|
9
|
+
* a typed `~/Downloads/x.mp4` resolved against cwd, so the end-of-run rename
|
|
10
|
+
* ENOENT'd after a 50-minute render because `<cwd>/~/Downloads` never
|
|
11
|
+
* existed. Two defenses live here: tilde expansion applied at every
|
|
12
|
+
* user-supplied path's resolution site, and failing (or healing) a bad out
|
|
13
|
+
* path in the first second instead of at the final rename.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expand a leading `~` to the home directory. `~user` forms are deliberately
|
|
18
|
+
* left untouched — resolving another user's home needs /etc/passwd semantics
|
|
19
|
+
* we don't have, and a wrong guess would be worse than the literal path.
|
|
20
|
+
* `home` is injectable so the matrix is testable without the real homedir.
|
|
21
|
+
*/
|
|
22
|
+
export function expandHome(path: string, home: string = homedir()): string {
|
|
23
|
+
if (path === "~") return home;
|
|
24
|
+
if (path.startsWith("~/")) return home + path.slice(1);
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* mkdir -p the parent of a would-be output file. mkdir chosen over refusal:
|
|
30
|
+
* the path is the user's explicit intent and creating a folder is what they'd
|
|
31
|
+
* do by hand; a genuinely un-creatable path (permissions) still fails loudly
|
|
32
|
+
* — just upfront now, not after the render. `mkdirFn` is injectable so the
|
|
33
|
+
* test asserts the directory asked for without touching a filesystem.
|
|
34
|
+
*/
|
|
35
|
+
export function ensureParentDir(
|
|
36
|
+
filePath: string,
|
|
37
|
+
mkdirFn: (dir: string) => void = (dir) => mkdirSync(dir, { recursive: true }),
|
|
38
|
+
): void {
|
|
39
|
+
mkdirFn(dirname(filePath));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `<out>.mp4` + `".cover.jpg"` → `<out>.cover.jpg`: the output's sibling
|
|
44
|
+
* artifact path, centralizing the replace idiom that lived (twice, and once
|
|
45
|
+
* stale — see the completion banner's call site) inline. The extension match
|
|
46
|
+
* excludes path separators on purpose: the bare-idiom regex `(\.[^.]+)?$`
|
|
47
|
+
* would treat a dotted DIRECTORY name as the extension of an extensionless
|
|
48
|
+
* output ("/out.v2/final" → "/out.cover.jpg", a file outside the folder the
|
|
49
|
+
* user chose). An input with no extension gains the suffix whole.
|
|
50
|
+
*
|
|
51
|
+
* Lives here rather than in produce.ts (its original home, 2026-08-17): the
|
|
52
|
+
* edit server derives `<out>.thumbnail.png` from command.json's recorded out
|
|
53
|
+
* and cannot import produce.ts — produce imports edit (recordRecentProject),
|
|
54
|
+
* and produce's import graph drags the renderer into a deliberately
|
|
55
|
+
* dependency-free server. produce.ts re-exports it for its existing callers.
|
|
56
|
+
*/
|
|
57
|
+
export function artifactPath(outPath: string, suffix: string): string {
|
|
58
|
+
return outPath.replace(/(\.[^./\\]+)?$/, suffix);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The rename/copy seam, injectable for the EXDEV test. */
|
|
62
|
+
export interface MoveDeps {
|
|
63
|
+
rename: (from: string, to: string) => Promise<void>;
|
|
64
|
+
copyFile: (from: string, to: string) => Promise<void>;
|
|
65
|
+
unlink: (path: string) => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const liveMoveDeps: MoveDeps = { rename, copyFile, unlink };
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `fs.rename` cannot cross volumes — an `--out` on an external drive throws
|
|
72
|
+
* EXDEV at the very end of the run (the sibling trap to the ENOENT above;
|
|
73
|
+
* ENOENT is prevented upfront by `ensureParentDir`). On EXDEV, fall back to
|
|
74
|
+
* copy+unlink; every other failure still throws, unchanged.
|
|
75
|
+
*/
|
|
76
|
+
export async function moveFile(
|
|
77
|
+
from: string,
|
|
78
|
+
to: string,
|
|
79
|
+
deps: MoveDeps = liveMoveDeps,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
await deps.rename(from, to);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if ((err as NodeJS.ErrnoException).code !== "EXDEV") throw err;
|
|
85
|
+
await deps.copyFile(from, to);
|
|
86
|
+
await deps.unlink(from);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { PORTRAIT_MIME_TYPES } from "@ossclip/core";
|
|
4
|
+
import { expandHome } from "./paths";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The per-project portrait override (editor face swap, 2026-08-17): a
|
|
8
|
+
* `portrait-override.<ext>` file in the workdir that outranks both the
|
|
9
|
+
* `--portrait` flag/pin and the config's `portrait`. One rule, spelled once,
|
|
10
|
+
* shared by produce.ts's resolution, thumbnail-panel.ts's pure matrix and
|
|
11
|
+
* edit.ts's endpoints.
|
|
12
|
+
*
|
|
13
|
+
* Lives in apps/cli rather than core because every consumer is here — core's
|
|
14
|
+
* thumbnailStep receives an already-resolved path — and in its own leaf
|
|
15
|
+
* module rather than produce.ts for the same reason `artifactPath` moved to
|
|
16
|
+
* paths.ts: edit.ts cannot import produce.ts (produce imports edit, and
|
|
17
|
+
* produce's import graph drags the renderer into a deliberately
|
|
18
|
+
* dependency-free server).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const PORTRAIT_OVERRIDE_BASENAME = "portrait-override";
|
|
22
|
+
|
|
23
|
+
/** Which portrait a resolution picked — the panel labels the swap state
|
|
24
|
+
* from this, so the vocabulary is part of the contract. */
|
|
25
|
+
export type PortraitSource = "override" | "flag" | "config";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The workdir's override file, or null when none exists. Extensions are
|
|
29
|
+
* probed in PORTRAIT_MIME_TYPES key order, first hit wins — the POST
|
|
30
|
+
* endpoint enforces at most one override, so two can only mean a hand-copied
|
|
31
|
+
* file, and a deterministic table-order pick beats a readdir-order coin
|
|
32
|
+
* flip. `exists` is injectable so the extension matrix needs no filesystem.
|
|
33
|
+
*/
|
|
34
|
+
export function portraitOverridePath(
|
|
35
|
+
work: string,
|
|
36
|
+
exists: (path: string) => boolean = existsSync,
|
|
37
|
+
): string | null {
|
|
38
|
+
for (const ext of Object.keys(PORTRAIT_MIME_TYPES)) {
|
|
39
|
+
const path = join(work, `${PORTRAIT_OVERRIDE_BASENAME}.${ext}`);
|
|
40
|
+
if (exists(path)) return path;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The override's filename extension for an uploaded mime type, or undefined
|
|
47
|
+
* when the type is outside the table the Gemini API accepts. Reverse lookup
|
|
48
|
+
* over PORTRAIT_MIME_TYPES so the two directions can never drift; for
|
|
49
|
+
* `image/jpeg`'s two spellings the first table key (`jpg`) wins.
|
|
50
|
+
*/
|
|
51
|
+
export function portraitExtensionForMime(mimeType: string): string | undefined {
|
|
52
|
+
return Object.keys(PORTRAIT_MIME_TYPES).find((ext) => PORTRAIT_MIME_TYPES[ext] === mimeType);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ResolvedPortrait {
|
|
56
|
+
path: string;
|
|
57
|
+
source: PortraitSource;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The one portrait-precedence rule: workdir override > flag/pin > config.
|
|
62
|
+
* A per-project expression chosen in the editor must survive CLI re-renders,
|
|
63
|
+
* so the override beats even an explicit `--portrait` — the flag/config
|
|
64
|
+
* portrait is the fallback headshot, and a replay silently reverting the
|
|
65
|
+
* swapped face would undo the one thing the swap exists for.
|
|
66
|
+
*
|
|
67
|
+
* expandHome covers the flag and config paths (the 2026-08-16 tilde
|
|
68
|
+
* incident, paths.ts) but not the override, which is server-built and
|
|
69
|
+
* already absolute. The config side is `typeof`, never truthiness — the
|
|
70
|
+
* `portrait` posture: config.json is hand-edited and unparsed.
|
|
71
|
+
*/
|
|
72
|
+
export function resolvePortrait(args: {
|
|
73
|
+
overridePath: string | null;
|
|
74
|
+
flagPortrait: string | undefined;
|
|
75
|
+
cfgPortrait: unknown;
|
|
76
|
+
home?: string;
|
|
77
|
+
}): ResolvedPortrait | undefined {
|
|
78
|
+
if (args.overridePath !== null) return { path: args.overridePath, source: "override" };
|
|
79
|
+
if (args.flagPortrait !== undefined) {
|
|
80
|
+
return { path: expandHome(args.flagPortrait, args.home), source: "flag" };
|
|
81
|
+
}
|
|
82
|
+
if (typeof args.cfgPortrait === "string") {
|
|
83
|
+
return { path: expandHome(args.cfgPortrait, args.home), source: "config" };
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|