ossclip 0.1.9 → 0.1.10
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/package.json +4 -4
- package/src/interactive/produce-wizard.ts +27 -20
- package/src/program.ts +78 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ossclip",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"tsx": "^4.19.0",
|
|
38
38
|
"zod": "^3.25.76",
|
|
39
|
-
"@ossclip/
|
|
40
|
-
"@ossclip/
|
|
41
|
-
"@ossclip/scenes": "0.1.
|
|
39
|
+
"@ossclip/core": "0.1.10",
|
|
40
|
+
"@ossclip/renderer": "0.1.10",
|
|
41
|
+
"@ossclip/scenes": "0.1.10"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/AhsanAyaz/ossclip#readme",
|
|
44
44
|
"bugs": {
|
|
@@ -50,29 +50,36 @@ export function extrasFor(graphics: boolean): (typeof EXTRAS)[number][] {
|
|
|
50
50
|
return graphics ? [...EXTRAS] : EXTRAS.filter((e) => e.value !== "graphicsClip");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export async function produceWizard(cfg: { speaker?: string } = {}): Promise<string[]> {
|
|
53
|
+
export async function produceWizard(cfg: { speaker?: string; input?: string } = {}): Promise<string[]> {
|
|
54
54
|
assertInteractive("produce wizard");
|
|
55
55
|
intro("ossclip produce");
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
57
|
+
// Pre-supplied by bare `ossclip <path>` (0.1.9 first-contact, 2026-08-05):
|
|
58
|
+
// the user already TYPED the input on the command line, and the old flow
|
|
59
|
+
// dropped it and asked again — the re-ask is where "./Anyhropic c Compiler"
|
|
60
|
+
// became "./" (all of ~/Downloads). The router checks existence before the
|
|
61
|
+
// wizard ever opens, so a prefilled path skips the prompt entirely.
|
|
62
|
+
const input =
|
|
63
|
+
cfg.input ??
|
|
64
|
+
(unwrap(
|
|
65
|
+
await text({
|
|
66
|
+
// Finding 1 (final-review fix wave): `ossclip produce <folder>` shipped
|
|
67
|
+
// (folder-input-brief.md) but this prompt still rejected a directory —
|
|
68
|
+
// the wizard was the only way in that couldn't do what the CLI could.
|
|
69
|
+
// A folder is concatenated by name (codepoint order, like `ls`); --sort
|
|
70
|
+
// mtime reorders it but stays a typed flag, not a wizard question (see
|
|
71
|
+
// the file-level comment above for why).
|
|
72
|
+
message: "Video file, or a folder of clips to concatenate (by name; --sort mtime is a typed flag)",
|
|
73
|
+
placeholder: "./raw/take1.mp4",
|
|
74
|
+
validate: (v) => {
|
|
75
|
+
if (!v) return "a path is required";
|
|
76
|
+
if (!existsSync(v)) return `no such path: ${v}`;
|
|
77
|
+
const st = statSync(v);
|
|
78
|
+
if (!st.isFile() && !st.isDirectory()) return `${v} is neither a video file nor a folder`;
|
|
79
|
+
return undefined;
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
) as string);
|
|
76
83
|
|
|
77
84
|
const aspect = unwrap(
|
|
78
85
|
await select({
|
package/src/program.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
import { z } from "zod/v4";
|
|
@@ -51,31 +51,83 @@ export function buildProgram(): Command {
|
|
|
51
51
|
// Bare `ossclip` at a TTY opens the menu. Piped or in CI it prints help,
|
|
52
52
|
// byte for byte what it printed before — a front door must not become a
|
|
53
53
|
// hang for a script.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
//
|
|
55
|
+
// Bare `ossclip <path>` ROUTES (0.1.9 first-contact, 2026-08-05): with no
|
|
56
|
+
// argument declared here, commander's default allow-excess-args silently
|
|
57
|
+
// DROPPED the positional — `ossclip "./Anyhropic c Compiler"` opened the
|
|
58
|
+
// menu as if nothing had been typed, and the user re-answered the wizard's
|
|
59
|
+
// input prompt by hand, wrongly, with all of ~/Downloads. Commander
|
|
60
|
+
// dispatches registered subcommand names before this action ever runs, so
|
|
61
|
+
// `produce`/`edit`/… always win over path interpretation (a folder that
|
|
62
|
+
// happens to be NAMED `doctor` goes to the subcommand — acceptable);
|
|
63
|
+
// anything that lands here is a path or a typo, and a typo must be a loud
|
|
64
|
+
// error naming what was tried, never the menu.
|
|
65
|
+
program
|
|
66
|
+
.argument(
|
|
67
|
+
"[path]",
|
|
68
|
+
"an existing video file or clips folder — shorthand for `ossclip produce <path>`, " +
|
|
69
|
+
"with the wizard asking the remaining questions",
|
|
70
|
+
)
|
|
71
|
+
// Commander 12 allows excess args by default, which is the SECOND half of
|
|
72
|
+
// the field failure (review, Important): the report's path was typed
|
|
73
|
+
// UNQUOTED — `ossclip ./Anyhropic c Compiler` — and with excess args
|
|
74
|
+
// allowed, `c` and `Compiler` would vanish wordlessly the moment
|
|
75
|
+
// `./Anyhropic` happened to exist, producing the wrong scope. An unquoted
|
|
76
|
+
// multi-word path must be a loud error, never a partial run.
|
|
77
|
+
.allowExcessArguments(false)
|
|
78
|
+
.action(async (path: string | undefined) => {
|
|
79
|
+
const { isInteractive } = await import("./interactive/tty");
|
|
80
|
+
if (path !== undefined) {
|
|
81
|
+
if (!existsSync(path)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`no such file or directory: ${path}\n` +
|
|
84
|
+
" bare `ossclip <path>` produces an existing video file or clips folder;\n" +
|
|
85
|
+
" run `ossclip --help` for the commands.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const { renderCommand } = await import("./interactive/render");
|
|
89
|
+
// Both branches hand argv back through `program.parseAsync` — the same
|
|
90
|
+
// re-entry shape as the wizard paths below, for the same reason: the
|
|
91
|
+
// zod checks in the produce action must run on this input exactly as
|
|
92
|
+
// they do on a typed command line.
|
|
93
|
+
if (!isInteractive()) {
|
|
94
|
+
// No TTY means no wizard for the remaining questions, but the input
|
|
95
|
+
// IS supplied — a piped `ossclip <path>` is `ossclip produce <path>`.
|
|
96
|
+
const direct = ["produce", path];
|
|
97
|
+
console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
|
|
98
|
+
await program.parseAsync(["node", "ossclip", ...direct]);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
102
|
+
const { loadConfig } = await import("@ossclip/core");
|
|
103
|
+
const argv = await produceWizard({ speaker: loadConfig().speaker, input: path });
|
|
104
|
+
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
105
|
+
await program.parseAsync(["node", "ossclip", ...argv]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isInteractive()) {
|
|
109
|
+
program.outputHelp();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { chooseFromMenu, menuArgv } = await import("./interactive/menu");
|
|
113
|
+
const choice = await chooseFromMenu();
|
|
114
|
+
const direct = menuArgv(choice);
|
|
115
|
+
const { renderCommand } = await import("./interactive/render");
|
|
116
|
+
if (direct !== null) {
|
|
117
|
+
// Echoed for the same reason the wizard echoes: the README promises
|
|
118
|
+
// "every choice prints the equivalent command before it runs, so the
|
|
119
|
+
// menu is also how you learn the flags", and three of the four entries
|
|
120
|
+
// printed nothing. The menu's whole pedagogical point is this line.
|
|
121
|
+
console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
|
|
122
|
+
await program.parseAsync(["node", "ossclip", ...direct]);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
126
|
+
const { loadConfig } = await import("@ossclip/core");
|
|
127
|
+
const argv = await produceWizard({ speaker: loadConfig().speaker });
|
|
128
|
+
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
129
|
+
await program.parseAsync(["node", "ossclip", ...argv]);
|
|
130
|
+
});
|
|
79
131
|
|
|
80
132
|
program
|
|
81
133
|
.command("produce")
|