ossclip 0.1.27 → 0.1.28
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-BBJKC2Z2.js +164 -0
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/edit.ts +42 -0
- package/src/llm-detect.ts +11 -0
- package/src/produce.ts +597 -166
- package/src/program.ts +48 -4
- package/src/replay-argv.ts +22 -1
- package/src/setup/download.ts +24 -0
- package/src/setup/manifest.ts +30 -12
- package/src/setup/setup.ts +47 -4
- package/editor-dist/assets/index-CWIUcpJC.js +0 -163
package/src/program.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { loadEnvFiles } from "./env";
|
|
|
9
9
|
import { ExportFormatSchema, runAnalyze } from "./analyze";
|
|
10
10
|
import { expandHome } from "./paths";
|
|
11
11
|
import { phaseBucketProps } from "./phase-timing";
|
|
12
|
-
import { dictionaryFlag, jumpCutsFlag, produce } from "./produce";
|
|
12
|
+
import { dictionaryFlag, jumpCutsFlag, produce, reviewFlag } from "./produce";
|
|
13
13
|
// The one interactive import that is STATIC rather than `await import()`: the
|
|
14
14
|
// `resetInputSource()` run boundary in `buildProgram` has to run synchronously
|
|
15
15
|
// while the program is being built, and `buildProgram` cannot await. The graph
|
|
@@ -334,6 +334,10 @@ export function buildProgram(): Command {
|
|
|
334
334
|
"if ANTHROPIC_API_KEY is set, else claude-cli",
|
|
335
335
|
)
|
|
336
336
|
.option("--llm-model <id>", "override the provider's default model")
|
|
337
|
+
.option(
|
|
338
|
+
"--llm-effort <level>",
|
|
339
|
+
"reasoning effort for the antigravity provider (low|medium|high)",
|
|
340
|
+
)
|
|
337
341
|
.option(
|
|
338
342
|
"--llm-fast-model <id>",
|
|
339
343
|
"model for mechanical calls (repair, scene props); 'same' disables tiering",
|
|
@@ -478,6 +482,13 @@ export function buildProgram(): Command {
|
|
|
478
482
|
"--no-open-editor",
|
|
479
483
|
"don't open the editor, and don't ask (overrides openEditorAfterProduce)",
|
|
480
484
|
)
|
|
485
|
+
// Implies --no-render and forces the editor open — resolved by reviewFlag
|
|
486
|
+
// in the action, where typing --no-render alongside it is agreement and
|
|
487
|
+
// --no-open-editor is the loud contradiction.
|
|
488
|
+
.option(
|
|
489
|
+
"--review",
|
|
490
|
+
"produce without rendering, then open the editor to review the cut before rendering once",
|
|
491
|
+
)
|
|
481
492
|
.option("--editor-port <n>", "port for the editor started by --open-editor",
|
|
482
493
|
(v) => Number.parseInt(v, 10), 5174)
|
|
483
494
|
// No default: undefined = "not typed" is what lets the config's
|
|
@@ -539,6 +550,13 @@ export function buildProgram(): Command {
|
|
|
539
550
|
const provider = opts.llm
|
|
540
551
|
? z.enum(["antigravity", "claude", "claude-cli", "gemini", "mock"]).parse(opts.llm)
|
|
541
552
|
: undefined;
|
|
553
|
+
// Parsed like --llm above: a typo'd `--llm-effort hgh` must die here
|
|
554
|
+
// naming the allowed values, not silently run at agy's default — the
|
|
555
|
+
// knob exists because of a hang (§143), and a user reaching for it is
|
|
556
|
+
// mid-investigation.
|
|
557
|
+
const llmEffort = opts.llmEffort
|
|
558
|
+
? z.enum(["low", "medium", "high"]).parse(opts.llmEffort)
|
|
559
|
+
: undefined;
|
|
542
560
|
const forceComponent = opts.forceComponent
|
|
543
561
|
? SceneComponentIdSchema.parse(opts.forceComponent)
|
|
544
562
|
: undefined;
|
|
@@ -569,6 +587,15 @@ export function buildProgram(): Command {
|
|
|
569
587
|
opts.addJumpCuts,
|
|
570
588
|
command.getOptionValueSource("jumpCuts") === "cli",
|
|
571
589
|
);
|
|
590
|
+
// --review resolved BEFORE produce runs: it implies --no-render (typed
|
|
591
|
+
// alongside is agreement) and forces the end-of-run editor open, so the
|
|
592
|
+
// one render happens from the editor's Render button. reviewFlag throws
|
|
593
|
+
// on --no-open-editor, the jumpCutsFlag contradiction posture.
|
|
594
|
+
const { render, openEditor } = reviewFlag(
|
|
595
|
+
opts.review === true,
|
|
596
|
+
opts.render,
|
|
597
|
+
opts.openEditor,
|
|
598
|
+
);
|
|
572
599
|
// Wall clock around produce() only — the editor offer below can sit at
|
|
573
600
|
// an interactive prompt for as long as the user thinks, and think-time
|
|
574
601
|
// would poison the duration metric (FINDINGS §134).
|
|
@@ -578,7 +605,12 @@ export function buildProgram(): Command {
|
|
|
578
605
|
out: opts.out,
|
|
579
606
|
cleanup,
|
|
580
607
|
transcript: opts.transcript,
|
|
581
|
-
render:
|
|
608
|
+
// The reviewFlag resolution, not opts.render: --review implies off.
|
|
609
|
+
render,
|
|
610
|
+
// Only phrases the no-render exit ("the editor is opening" instead
|
|
611
|
+
// of the --no-render skip + edit hint) — the render/openEditor
|
|
612
|
+
// consequences are already resolved above.
|
|
613
|
+
review: opts.review === true,
|
|
582
614
|
mezzanine: opts.mezzanine,
|
|
583
615
|
workdir: opts.workdir,
|
|
584
616
|
sort,
|
|
@@ -590,6 +622,9 @@ export function buildProgram(): Command {
|
|
|
590
622
|
provider,
|
|
591
623
|
llmModel: opts.llmModel,
|
|
592
624
|
llmFastModel: opts.llmFastModel,
|
|
625
|
+
// The zod-parsed union above; untyped = undefined lets the config's
|
|
626
|
+
// `llmEffort` supply it (resolveLlmEffort at the use site).
|
|
627
|
+
llmEffort,
|
|
593
628
|
speaker: opts.speaker,
|
|
594
629
|
scenes: opts.scenes,
|
|
595
630
|
repair: opts.repair,
|
|
@@ -645,7 +680,10 @@ export function buildProgram(): Command {
|
|
|
645
680
|
produced: opts.produce === true,
|
|
646
681
|
aspect: opts.aspect === "16:9" ? "16:9" : "9:16",
|
|
647
682
|
clip: opts.clip !== undefined,
|
|
648
|
-
|
|
683
|
+
// The reviewFlag resolution, spelled `key:` and not shorthand — a
|
|
684
|
+
// --review run must report render: false, and telemetry.test.ts's
|
|
685
|
+
// source-drift scan (§136) only sees `key:`-form props.
|
|
686
|
+
render: render,
|
|
649
687
|
source_duration_bucket: durationBucket(result.sourceDurationSec),
|
|
650
688
|
scenes: result.sceneCount,
|
|
651
689
|
// Per-phase buckets (§140), one `<phase>_bucket` per phase that
|
|
@@ -668,7 +706,13 @@ export function buildProgram(): Command {
|
|
|
668
706
|
}
|
|
669
707
|
}
|
|
670
708
|
const { offerEditor } = await import("./interactive/offer-editor");
|
|
671
|
-
|
|
709
|
+
// The reviewFlag resolution: --review forces flag=true, and
|
|
710
|
+
// decideOpenEditor already documents that an explicit flag beats
|
|
711
|
+
// `rendered` — the editor reads render-props.json, which a no-render
|
|
712
|
+
// run does write. Reusing this path is what gives --review the edit
|
|
713
|
+
// command's whole posture (resolveEditorPageDir's loud "run pnpm
|
|
714
|
+
// build" degrade, startEditServer, openInBrowser) for free.
|
|
715
|
+
await offerEditor(result, { flag: openEditor, port: opts.editorPort });
|
|
672
716
|
// Deliberately LAST — after the render summary and the editor offer —
|
|
673
717
|
// so the one question ossclip ever asks is the last thing on screen.
|
|
674
718
|
await maybeAskRating(telemetry);
|
package/src/replay-argv.ts
CHANGED
|
@@ -54,6 +54,8 @@ export function consumeReplayArgv(): string[] | null {
|
|
|
54
54
|
*/
|
|
55
55
|
export function recordedProduceArgs(pins: {
|
|
56
56
|
llm?: string;
|
|
57
|
+
/** The RESOLVED §143 effort — flag or valid config, never a raw config string. */
|
|
58
|
+
llmEffort?: "low" | "medium" | "high";
|
|
57
59
|
clipWindow?: string;
|
|
58
60
|
watermark?: boolean;
|
|
59
61
|
captions?: boolean;
|
|
@@ -68,10 +70,29 @@ export function recordedProduceArgs(pins: {
|
|
|
68
70
|
/** The RESOLVED thumbnail brief — pinned only when non-empty. */
|
|
69
71
|
thumbnailBrief?: string;
|
|
70
72
|
}): string[] {
|
|
71
|
-
|
|
73
|
+
// --review and --no-render are stripped at record (cut-review step 1):
|
|
74
|
+
// command.json exists for exactly one consumer — the editor's Render
|
|
75
|
+
// button, which replays this argv to produce the video — and a record
|
|
76
|
+
// carrying --no-render would replay as a run that skips the render again,
|
|
77
|
+
// while a recorded --review would ALSO spawn a second editor from inside
|
|
78
|
+
// the replay child. Record the invocation the user wants Render to run.
|
|
79
|
+
// Both are bare boolean flags, so a value-free filter cannot orphan an
|
|
80
|
+
// option's argument.
|
|
81
|
+
const args = (consumeReplayArgv() ?? process.argv.slice(2)).filter(
|
|
82
|
+
(a) => a !== "--review" && a !== "--no-render",
|
|
83
|
+
);
|
|
72
84
|
if (pins.llm !== undefined && !args.includes("--llm")) {
|
|
73
85
|
args.push("--llm", pins.llm);
|
|
74
86
|
}
|
|
87
|
+
// The effort pin (§143), the dictionary's rationale: the resolved level may
|
|
88
|
+
// have come from ~/.ossclip/config.json's `llmEffort`, and it steers the
|
|
89
|
+
// editorial call — an unpinned record would replay a DIFFERENT plan the
|
|
90
|
+
// moment that config is edited. Unset stays unpinned: there is no flag
|
|
91
|
+
// spelling for "agy's own default", and an argv without the flag replays as
|
|
92
|
+
// "config decides" — the same accepted cost as the dictionary's empty case.
|
|
93
|
+
if (pins.llmEffort !== undefined && !args.includes("--llm-effort")) {
|
|
94
|
+
args.push("--llm-effort", pins.llmEffort);
|
|
95
|
+
}
|
|
75
96
|
if (pins.clipWindow !== undefined && !args.includes("--clip-window")) {
|
|
76
97
|
args.push("--clip-window", pins.clipWindow);
|
|
77
98
|
}
|
package/src/setup/download.ts
CHANGED
|
@@ -12,6 +12,28 @@ import { dirname } from "node:path";
|
|
|
12
12
|
* before renaming into place — a `.part` never becomes a `dest` unverified.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A pinned asset is gone from the host — distinguished from every other HTTP
|
|
17
|
+
* failure because it is OURS, not the user's: the manifest names an exact
|
|
18
|
+
* upstream release, and hosts rotate releases out from under a pin (BtbN
|
|
19
|
+
* prunes daily autobuilds after ~2 weeks — #6, §145). A stale pin reported as
|
|
20
|
+
* a bare "download failed: HTTP 404" reads as a broken network and leaves the
|
|
21
|
+
* user nowhere; the caller catches this type and adds the manual install for
|
|
22
|
+
* their platform, which turns a dead end into a two-minute detour.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately generic: this module downloads models and whisper builds too,
|
|
25
|
+
* so it names the problem and lets the caller name the remedy.
|
|
26
|
+
*/
|
|
27
|
+
export class PinnedAssetGoneError extends Error {
|
|
28
|
+
constructor(readonly url: string) {
|
|
29
|
+
super(
|
|
30
|
+
`the pinned download is gone upstream (HTTP 404)\n ${url}\n` +
|
|
31
|
+
" This is a stale pin in ossclip's manifest, not a problem on your machine.",
|
|
32
|
+
);
|
|
33
|
+
this.name = "PinnedAssetGoneError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
export interface DownloadOptions {
|
|
16
38
|
/** hex digest; algorithm chosen by which field is set */
|
|
17
39
|
sha256?: string;
|
|
@@ -39,6 +61,8 @@ export async function download(url: string, dest: string, opts: DownloadOptions
|
|
|
39
61
|
const res = await fetchImpl(url, { headers, redirect: "follow" });
|
|
40
62
|
if (res.status === 200) {
|
|
41
63
|
offset = 0; // server ignored the range — start over
|
|
64
|
+
} else if (res.status === 404) {
|
|
65
|
+
throw new PinnedAssetGoneError(url);
|
|
42
66
|
} else if (res.status !== 206) {
|
|
43
67
|
throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
44
68
|
}
|
package/src/setup/manifest.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* `checksums.sha256` per release; ggml-org assets are hashed by hand), and
|
|
12
12
|
* re-run the setup-e2e workflow. The manifest test asserts every supported
|
|
13
13
|
* platform×arch resolves to either an asset or an explicit manual hint.
|
|
14
|
+
*
|
|
15
|
+
* A pinned URL can also rot without anything here changing, so the `manifest
|
|
16
|
+
* urls` CI job HEADs every one of them on every setup PR and weekly (§145,
|
|
17
|
+
* §146) — a shape assertion in the unit suite cannot see a 404.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { basename, isAbsolute, join } from "node:path";
|
|
@@ -29,9 +33,23 @@ export interface BinaryAsset {
|
|
|
29
33
|
version: string;
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* PIN A MONTH-END TAG ONLY. BtbN's `autobuild-*` releases are not all equal:
|
|
38
|
+
* the dailies are pruned after roughly two weeks, but the LAST autobuild of
|
|
39
|
+
* each month is retained indefinitely — verified 2026-08-22 against tags going
|
|
40
|
+
* back to `autobuild-2024-09-30-15-36`, still serving all 48 assets.
|
|
41
|
+
*
|
|
42
|
+
* The previous pin here was a daily (`autobuild-2026-07-29-13-36`) and took
|
|
43
|
+
* the whole tag with it when it rotated, so `ossclip setup` could not install
|
|
44
|
+
* ffmpeg on ANY platform it provisions for — every non-darwin branch below
|
|
45
|
+
* interpolates this one base (#6, §145). Picking the month-end tag costs
|
|
46
|
+
* nothing and cannot rot the same way; picking `latest` would fix the 404 but
|
|
47
|
+
* forfeit the checksum guarantee this whole file exists for, since the asset
|
|
48
|
+
* behind it changes daily.
|
|
49
|
+
*/
|
|
32
50
|
const BTBN =
|
|
33
|
-
"https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-07-
|
|
34
|
-
const FFMPEG_VER = "n8.1.2-
|
|
51
|
+
"https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-07-31-14-10";
|
|
52
|
+
const FFMPEG_VER = "n8.1.2-34-g9b6c8969e0";
|
|
35
53
|
|
|
36
54
|
const WHISPER =
|
|
37
55
|
"https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.1";
|
|
@@ -50,7 +68,7 @@ export function ffmpegAsset(platform: NodeJS.Platform, arch: string): BinaryAsse
|
|
|
50
68
|
return {
|
|
51
69
|
...common,
|
|
52
70
|
url: `${BTBN}/ffmpeg-${FFMPEG_VER}-win64-gpl-8.1.zip`,
|
|
53
|
-
sha256: "
|
|
71
|
+
sha256: "cc4156d51387566ea8ba653fc3a04897bdf812fddf652428d9030bbf7ae24835",
|
|
54
72
|
archive: "zip",
|
|
55
73
|
sizeMB: 160,
|
|
56
74
|
};
|
|
@@ -59,7 +77,7 @@ export function ffmpegAsset(platform: NodeJS.Platform, arch: string): BinaryAsse
|
|
|
59
77
|
return {
|
|
60
78
|
...common,
|
|
61
79
|
url: `${BTBN}/ffmpeg-${FFMPEG_VER}-winarm64-gpl-8.1.zip`,
|
|
62
|
-
sha256: "
|
|
80
|
+
sha256: "abf3b41c200ce5346b9bb5be6fe634c4720d891778d8921f7b36b76d002b3c96",
|
|
63
81
|
archive: "zip",
|
|
64
82
|
sizeMB: 107,
|
|
65
83
|
};
|
|
@@ -68,16 +86,16 @@ export function ffmpegAsset(platform: NodeJS.Platform, arch: string): BinaryAsse
|
|
|
68
86
|
return {
|
|
69
87
|
...common,
|
|
70
88
|
url: `${BTBN}/ffmpeg-${FFMPEG_VER}-linux64-gpl-8.1.tar.xz`,
|
|
71
|
-
sha256: "
|
|
89
|
+
sha256: "09fc77be269c7053e438b7e96548e4af97604faf96a42c4a3c56a1ad74c22c0a",
|
|
72
90
|
archive: "tar.xz",
|
|
73
|
-
sizeMB:
|
|
91
|
+
sizeMB: 119,
|
|
74
92
|
};
|
|
75
93
|
}
|
|
76
94
|
if (platform === "linux" && arch === "arm64") {
|
|
77
95
|
return {
|
|
78
96
|
...common,
|
|
79
97
|
url: `${BTBN}/ffmpeg-${FFMPEG_VER}-linuxarm64-gpl-8.1.tar.xz`,
|
|
80
|
-
sha256: "
|
|
98
|
+
sha256: "177e40c91564dec3840096f3bf1ffe696b94330585972462cfc739fa29fe0e1a",
|
|
81
99
|
archive: "tar.xz",
|
|
82
100
|
sizeMB: 102,
|
|
83
101
|
};
|
|
@@ -163,13 +181,13 @@ export const MODELS: Record<string, ModelInfo> = {
|
|
|
163
181
|
"base.en": { sizeMB: 142, sha1: "137c40403d78fd54d454da0f9bd998f78703390c" },
|
|
164
182
|
"small.en": { sizeMB: 466, sha1: "db8a495a91d927739e50b3fc1cc4c6b8f6c2d022" },
|
|
165
183
|
"medium.en": { sizeMB: 1536, sha1: "8c30f0e44ce9560643ebd10bbe50cd20eafd3723" },
|
|
166
|
-
// URL pending the author's upload (2026-08-17) — sha1 added when the file
|
|
167
|
-
// is published; setup already warns-and-continues without a checksum.
|
|
168
184
|
"medium-urdu": {
|
|
169
185
|
sizeMB: 1463,
|
|
170
|
-
// sha1 computed 2026-08-17 from the author's converted file
|
|
171
|
-
// upload
|
|
172
|
-
//
|
|
186
|
+
// sha1 was computed 2026-08-17 from the author's converted file before the
|
|
187
|
+
// HF upload, then the published file was checked byte-identical to it
|
|
188
|
+
// (exact size + head/mid/tail ranges) rather than trusted — so a corrupted
|
|
189
|
+
// or tampered mirror download fails the checksum loudly. Live since that
|
|
190
|
+
// date; `pnpm check:urls` re-probes this URL with the rest of the table.
|
|
173
191
|
sha1: "59769d590f62eeeb3bc3f5b82ce8c03b6e96831e",
|
|
174
192
|
language: "ur",
|
|
175
193
|
note: "community Urdu fine-tune (Abdul145/whisper-medium-urdu-custom, Apache-2.0), converted to GGML",
|
package/src/setup/setup.ts
CHANGED
|
@@ -11,8 +11,15 @@ import {
|
|
|
11
11
|
whisperModelPath,
|
|
12
12
|
type BinaryAsset,
|
|
13
13
|
} from "./manifest";
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
formatPlan,
|
|
16
|
+
managedBinDir,
|
|
17
|
+
planSetup,
|
|
18
|
+
type SetupProbes,
|
|
19
|
+
type SetupStep,
|
|
20
|
+
type StepKind,
|
|
21
|
+
} from "./plan";
|
|
22
|
+
import { PinnedAssetGoneError, download, progressLine } from "./download";
|
|
16
23
|
import { extractArchive, findFile, markExecutable } from "./extract";
|
|
17
24
|
import { promptForProvider } from "./provider";
|
|
18
25
|
|
|
@@ -37,6 +44,35 @@ export interface SetupCliOptions {
|
|
|
37
44
|
yes: boolean;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The hand-install to fall back to when setup's own pinned download is gone
|
|
49
|
+
* (§145). Pure, and exported so the platform matrix is assertable — the whole
|
|
50
|
+
* point is that a Windows or Linux user is never the one who finds out the
|
|
51
|
+
* command was wrong (§136).
|
|
52
|
+
*
|
|
53
|
+
* Deliberately NOT doctor's `installHint`: doctor imports from this directory,
|
|
54
|
+
* and pointing setup back at doctor would invert that. These are the same
|
|
55
|
+
* commands doctor prints, and doctor.test.ts pins those independently.
|
|
56
|
+
*/
|
|
57
|
+
export function manualInstall(kind: StepKind, platform: NodeJS.Platform): string {
|
|
58
|
+
if (kind === "model") return "download it by hand — `ossclip doctor` prints the exact curl";
|
|
59
|
+
if (kind === "provider") return "set an API key, or install the agy or claude CLI";
|
|
60
|
+
const pkg = kind === "ffmpeg" ? "ffmpeg" : "whisper-cpp";
|
|
61
|
+
if (platform === "darwin") return `brew install ${pkg}`;
|
|
62
|
+
if (platform === "linux") {
|
|
63
|
+
// apt has no whisper.cpp package; building it is the documented path.
|
|
64
|
+
return kind === "ffmpeg" ? "sudo apt install ffmpeg" : WHISPER_BUILD_HINT;
|
|
65
|
+
}
|
|
66
|
+
if (platform === "win32") {
|
|
67
|
+
return kind === "ffmpeg"
|
|
68
|
+
? "winget install ffmpeg — then point OSSCLIP_FFMPEG/OSSCLIP_FFPROBE (or config.json) at the binaries"
|
|
69
|
+
: WHISPER_BUILD_HINT;
|
|
70
|
+
}
|
|
71
|
+
return kind === "ffmpeg"
|
|
72
|
+
? "install ffmpeg from https://ffmpeg.org and set OSSCLIP_FFMPEG"
|
|
73
|
+
: WHISPER_BUILD_HINT;
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
const probeBin = (bin: string, arg: string): Promise<boolean> =>
|
|
41
77
|
new Promise((resolve) => {
|
|
42
78
|
// Existence is the question, not exit code (same contract as doctor).
|
|
@@ -92,8 +128,15 @@ export async function setup(
|
|
|
92
128
|
await runStep(step);
|
|
93
129
|
} catch (err) {
|
|
94
130
|
const msg = err instanceof Error ? err.message : String(err);
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
// A rotated-away pin is ossclip's bug, and the user still needs a
|
|
132
|
+
// working toolchain today — so name the hand-install for their
|
|
133
|
+
// platform instead of leaving them on a bare 404 (#6, §145).
|
|
134
|
+
const detour =
|
|
135
|
+
err instanceof PinnedAssetGoneError
|
|
136
|
+
? `\n Until it is re-pinned, install it yourself: ${manualInstall(step.kind, probes.platform)}`
|
|
137
|
+
: "";
|
|
138
|
+
failures.push(`${step.kind}: ${msg}${detour}`);
|
|
139
|
+
console.error(`✗ ${step.kind} failed: ${msg}${detour}`);
|
|
97
140
|
}
|
|
98
141
|
}
|
|
99
142
|
|