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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ossclip",
3
- "version": "0.1.9",
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/renderer": "0.1.9",
40
- "@ossclip/core": "0.1.9",
41
- "@ossclip/scenes": "0.1.9"
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
- const input = unwrap(
58
- await text({
59
- // Finding 1 (final-review fix wave): `ossclip produce <folder>` shipped
60
- // (folder-input-brief.md) but this prompt still rejected a directory —
61
- // the wizard was the only way in that couldn't do what the CLI could.
62
- // A folder is concatenated by name (codepoint order, like `ls`); --sort
63
- // mtime reorders it but stays a typed flag, not a wizard question (see
64
- // the file-level comment above for why).
65
- message: "Video file, or a folder of clips to concatenate (by name; --sort mtime is a typed flag)",
66
- placeholder: "./raw/take1.mp4",
67
- validate: (v) => {
68
- if (!v) return "a path is required";
69
- if (!existsSync(v)) return `no such path: ${v}`;
70
- const st = statSync(v);
71
- if (!st.isFile() && !st.isDirectory()) return `${v} is neither a video file nor a folder`;
72
- return undefined;
73
- },
74
- }),
75
- ) as string;
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
- program.action(async () => {
55
- const { isInteractive } = await import("./interactive/tty");
56
- if (!isInteractive()) {
57
- program.outputHelp();
58
- return;
59
- }
60
- const { chooseFromMenu, menuArgv } = await import("./interactive/menu");
61
- const choice = await chooseFromMenu();
62
- const direct = menuArgv(choice);
63
- const { renderCommand } = await import("./interactive/render");
64
- if (direct !== null) {
65
- // Echoed for the same reason the wizard echoes: the README promises
66
- // "every choice prints the equivalent command before it runs, so the
67
- // menu is also how you learn the flags", and three of the four entries
68
- // printed nothing. The menu's whole pedagogical point is this line.
69
- console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
70
- await program.parseAsync(["node", "ossclip", ...direct]);
71
- return;
72
- }
73
- const { produceWizard } = await import("./interactive/produce-wizard");
74
- const { loadConfig } = await import("@ossclip/core");
75
- const argv = await produceWizard({ speaker: loadConfig().speaker });
76
- console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
77
- await program.parseAsync(["node", "ossclip", ...argv]);
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")