ossclip 0.1.33 → 0.1.35

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.
@@ -11,7 +11,17 @@ import { renderCommand } from "./render";
11
11
  */
12
12
  export async function offerEditor(
13
13
  result: ProduceResult,
14
- opts: { flag: boolean | undefined; port: number },
14
+ opts: {
15
+ flag: boolean | undefined;
16
+ port: number;
17
+ /**
18
+ * Whether the user TYPED `--editor-port`. Commander's 5174 default is not
19
+ * a choice anybody made, so the common case must be free to attach or step
20
+ * around a busy port; only a typed port is defended (edit-port.ts's
21
+ * `pinned`, and the `--port` half of the same rule in program.ts).
22
+ */
23
+ portPinned: boolean;
24
+ },
15
25
  ): Promise<void> {
16
26
  const pref: OpenEditorPref = loadConfig().openEditorAfterProduce ?? "ask";
17
27
  const decision = decideOpenEditor({
@@ -64,8 +74,23 @@ export async function offerEditor(
64
74
  );
65
75
  return;
66
76
  }
67
- const server = await startEditServer(result.workdir, { port: opts.port, pageDir });
68
- console.log(`▸ editor at ${server.url}`);
77
+ // The same busy-port ladder `ossclip edit` runs (edit-port.ts): a produce
78
+ // finishing into an already-open editor on THIS project attaches to it
79
+ // instead of dying on EADDRINUSE — which here would throw away the run's
80
+ // whole summary behind a stack trace. `liveEditPortDeps` reads the real
81
+ // `isInteractive()`, so the three-way prompt is available exactly where this
82
+ // offer already asks questions, and absent in a piped run.
83
+ const { openEditServer, liveEditPortDeps } = await import("../edit-port");
84
+ const opened = await openEditServer(
85
+ result.workdir,
86
+ { port: opts.port, pinned: opts.portPinned },
87
+ liveEditPortDeps((port) => startEditServer(result.workdir, { port, pageDir })),
88
+ );
89
+ if (opened.kind === "cancelled") return;
90
+ const url = opened.kind === "attached" ? opened.url : opened.server.url;
91
+ // The attach path already printed "▸ already open at …"; a second line
92
+ // underneath it would read as a second server.
93
+ if (opened.kind === "started") console.log(`▸ editor at ${url}`);
69
94
  const { openInBrowser } = await import("../open");
70
- openInBrowser(server.url);
95
+ openInBrowser(url);
71
96
  }
@@ -1,4 +1,4 @@
1
- import type { ProviderName } from "@ossclip/core";
1
+ import type { ProviderName, SfxLevel } from "@ossclip/core";
2
2
 
3
3
  /**
4
4
  * Wizard answers → the argv a user could have typed.
@@ -35,6 +35,16 @@ export interface ProduceExtras {
35
35
  * it exists to beat a future config-off, and emitting it here would
36
36
  * restate the default. */
37
37
  jumpCuts?: boolean;
38
+ /** Sound effects, the watermark's polarity with a level attached: PRESENT
39
+ * means on (the wizard only ever turns it ON — off is the default, there is
40
+ * no `--no-sfx` spelling to mirror, and a config-on user who wants silence
41
+ * for one run edits the config's `sfx` key), and the value is whatever the
42
+ * level follow-up answered. Typed as core's `SfxLevel`, not an inline
43
+ * union, for `llm`'s reason: a level added to `SfxLevelSchema` must not be
44
+ * silently unofferable here. Only reachable under graphics — sound effects
45
+ * are placed against the producer's beat sheet, so `extrasFor` gates the
46
+ * entry the way it gates `--clip`. */
47
+ sfx?: SfxLevel;
38
48
  /** The YouTube pack (Y2): the wizard only ever turns it ON — off is the
39
49
  * default, and a config-on user who wants it off for one run types
40
50
  * `--no-youtube`, flags-only like `--no-watermark`. */
@@ -116,6 +126,13 @@ export function produceArgv(a: ProduceAnswers): string[] {
116
126
  // negative spelling — auto must stay an ABSENT flag, or the taught
117
127
  // command line restates a default.
118
128
  if (e.jumpCuts === false) argv.push("--no-jump-cuts");
129
+ // One flag, never both: `--sfx-level` already implies `--sfx` (program.ts's
130
+ // `sfxFlag`), which is why replay-argv pins a level WITHOUT the switch too —
131
+ // emitting the pair would teach a flag that changes nothing. And `normal` is
132
+ // the CLI's own default, so naming it would restate a default per the
133
+ // elision rule above: a normal-level run's whole sound design is `--sfx`.
134
+ if (e.sfx === "normal") argv.push("--sfx");
135
+ else if (e.sfx !== undefined) argv.push("--sfx-level", e.sfx);
119
136
  // Watermark's shape: only the ON tick emits (off is the default, elided),
120
137
  // and the portrait only rides along with a value — the wizard already
121
138
  // dropped empty answers.
@@ -12,8 +12,9 @@ import { assertInteractive, confirm, intro, multiselect, select, text, unwrap }
12
12
  /**
13
13
  * The produce wizard. Forty-one flags (plus the positional input path)
14
14
  * sorted into three tiers: six prompts asked directly — the input path, plus
15
- * five flags (--out, --cleanup, --aspect, --produce, --intent) — eleven
16
- * behind one "anything else?" multiselect, and the remaining stay flags-only:
15
+ * five flags (--out, --cleanup, --aspect, --produce, --intent) — twelve
16
+ * behind one "anything else?" multiselect (--sfx being the twelfth, with
17
+ * --sfx-level as its follow-up prompt rather than an entry of its own), and the remaining stay flags-only:
17
18
  * debug/internal surfaces, replay-only fields, --no-watermark (the
18
19
  * multiselect only turns the credit ON; off is already the default),
19
20
  * --no-youtube (the same shape: the pack entry only turns it ON),
@@ -48,6 +49,11 @@ const EXTRAS = [
48
49
  { value: "sourceIsEdited", label: "Source already has burned-in text", hint: "--source-is-edited" },
49
50
  { value: "captionsOff", label: "Turn the burned-in captions off", hint: "--no-captions" },
50
51
  { value: "jumpCutsOff", label: "No punch-in zooms at cuts", hint: "--no-jump-cuts" },
52
+ // Watermark/youtube polarity: OFF is the default and the entry only ever
53
+ // turns it ON — `--no-sfx` does not exist (program.ts declares `--sfx`
54
+ // alone so the config's `sfx` key can still supply it), so there is no OFF
55
+ // spelling to mirror in a second entry.
56
+ { value: "sfx", label: "Add sound effects (whoosh, ding…)", hint: "--sfx" },
51
57
  { value: "watermark", label: 'Credit the tool with a small "made with ossclip"', hint: "--watermark" },
52
58
  // The hint says the approval part out loud (thumbnail UX, 2026-08-16):
53
59
  // ticking this adds an interactive stop before the render, and a surprise
@@ -69,6 +75,15 @@ const EXTRAS = [
69
75
  * graphics is already on. Exported and kept pure so this can be asserted
70
76
  * without a TTY.
71
77
  *
78
+ * The sfx entry is gated on the same precedent (2026-08-29). Sound effects
79
+ * are placed against the producer's beat sheet, so a `--sfx` run without
80
+ * `--produce` reaches produce.ts's own "sound effects are placed against the
81
+ * producer's beat sheet — add --produce" warning and renders silent (a
82
+ * previous run's plan in the workdir is the only thing that saves it, which
83
+ * is not something a menu can promise). A guaranteed-inert menu item is the
84
+ * same broken offer as the clip one, softer only in that it warns instead of
85
+ * throwing — so it is only ever listed once graphics is already on.
86
+ *
72
87
  * `watermarkFromConfig` (review, minor a): on a config-on machine the
73
88
  * watermark entry sits UNCHECKED while the credit will render anyway —
74
89
  * unchecked is "don't emit the flag", not "off", and the multiselect has no
@@ -81,7 +96,9 @@ export function extrasFor(
81
96
  graphics: boolean,
82
97
  opts: { watermarkFromConfig?: boolean } = {},
83
98
  ): { value: (typeof EXTRAS)[number]["value"]; label: string; hint: string }[] {
84
- const list = graphics ? [...EXTRAS] : EXTRAS.filter((e) => e.value !== "graphicsClip");
99
+ const list = graphics
100
+ ? [...EXTRAS]
101
+ : EXTRAS.filter((e) => e.value !== "graphicsClip" && e.value !== "sfx");
85
102
  if (opts.watermarkFromConfig !== true) return [...list];
86
103
  return list.map((e) =>
87
104
  e.value === "watermark"
@@ -346,6 +363,26 @@ export async function produceWizard(
346
363
  // Same OFF-switch shape (the punch defaults ON, face-only): a tick maps
347
364
  // to `jumpCuts: false` and produceArgv emits `--no-jump-cuts`.
348
365
  if (chosen.includes("jumpCutsOff")) extras.jumpCuts = false;
366
+ if (chosen.includes("sfx")) {
367
+ // Follow-up under the same extra, like --clip's seconds prompt: the level
368
+ // only means anything once effects are on. `normal` is preselected
369
+ // because it is the CLI's own default — and picking it still emits the
370
+ // bare `--sfx` rather than `--sfx-level normal`, produceArgv's
371
+ // default-elision rule. The hints say what the level actually CHANGES
372
+ // (density, and whether the meme-tagged sounds are eligible at all),
373
+ // because "subtle/normal/meme" alone reads as a volume knob.
374
+ extras.sfx = unwrap(
375
+ await select({
376
+ message: "How much sound design?",
377
+ initialValue: "normal",
378
+ options: [
379
+ { value: "subtle", label: "subtle", hint: "a couple of accents a minute" },
380
+ { value: "normal", label: "normal", hint: "recommended" },
381
+ { value: "meme", label: "meme", hint: "denser, and meme-tagged sounds become eligible" },
382
+ ],
383
+ }),
384
+ ) as ProduceExtras["sfx"];
385
+ }
349
386
  if (chosen.includes("watermark")) extras.watermark = true;
350
387
  if (chosen.includes("youtube")) {
351
388
  extras.youtube = true;