@hraness/slopcamera 3.9.1 → 3.10.0
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/README.md +64 -112
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/commands.ts +1 -1
- package/apps/desktop/cli/entry.ts +31 -0
- package/apps/desktop/cli/help.ts +10 -1
- package/apps/desktop/cli/update-policy.ts +18 -0
- package/apps/desktop/cli/update-verification.ts +76 -0
- package/apps/desktop/cli/verbs.ts +5 -0
- package/apps/desktop/dist/cli/main.js +311 -304
- package/dist/cli.js +1 -1
- package/dist/{index-jmyg6t9c.js → index-0cby5jc7.js} +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +4 -2
- package/package.json +4 -3
- package/skills/slopcamera/SKILL.md +3 -0
- package/skills/slopcamera/references/creative-direction.md +38 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,44 @@
|
|
|
1
1
|
# SlopCamera
|
|
2
2
|
|
|
3
|
-
[](https://slopcamera.com/docs/how-to/remix-the-showcase#rain-bottled)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**A multimedia studio for your coding agent.** SlopCamera lets your coding
|
|
6
6
|
agent make images, diagrams, animation, 3D scenes, and video from source files
|
|
7
|
-
it can keep revising.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
scene audits, render plans, and `doctor` catch problems the agent can
|
|
34
|
-
fix before you look. They check structure, not taste; you still review the
|
|
35
|
-
render.
|
|
36
|
-
- **The techniques match the installed CLI.** The Bun CLI, a TypeScript SDK,
|
|
37
|
-
and an Agent Skill ship at the same version. The skill sends the agent to
|
|
38
|
-
the guide for its current job instead of loading every guide at once.
|
|
39
|
-
- **Work stays on your machine.** Projects are local files and there is no
|
|
40
|
-
SlopCamera account. Generation is optional: it uses your own Vercel AI
|
|
41
|
-
Gateway access or, for prompt-only images, prepaid Hraness Credits.
|
|
7
|
+
it can keep revising. Use it with Codex, Claude Code, or another coding agent.
|
|
8
|
+
Direct the style, timing, camera, and sound, then refine the result.
|
|
9
|
+
|
|
10
|
+
Your agent writes the scene or edit as source. SlopCamera renders it with the
|
|
11
|
+
appropriate engine and keeps the inputs so the agent can change a shot,
|
|
12
|
+
rework a composition, or deliver another format. Combine authored scenes,
|
|
13
|
+
generated media, and your own footage in the same project.
|
|
14
|
+
|
|
15
|
+
[Watch the films](https://slopcamera.com/#examples) · [Install](#install-slopcamera) · [Direct a film](https://slopcamera.com/docs/how-to/direct-a-film) · [Docs](https://slopcamera.com/docs) · [GitHub examples](examples/showcase)
|
|
16
|
+
|
|
17
|
+
## What you can make
|
|
18
|
+
|
|
19
|
+
- **Films and animation.** Author motion graphics, illustrated scenes, and
|
|
20
|
+
native Blender shots. Control camera, materials, lighting, and timing.
|
|
21
|
+
- **Visual explanations.** Make diagrams and educational films with editable
|
|
22
|
+
labels, mathematics, narration, and graphics.
|
|
23
|
+
- **Images and designs.** Create illustrations, trace raster artwork to SVG,
|
|
24
|
+
or revise a parametric architectural model.
|
|
25
|
+
- **Edits of your footage.** Compose shots, sound, overlays, and captions,
|
|
26
|
+
then export horizontal, vertical, square, or portrait versions.
|
|
27
|
+
|
|
28
|
+
The [techniques catalog](https://slopcamera.com/docs/reference/techniques)
|
|
29
|
+
connects each job to its supported commands and requirements. SlopCamera is
|
|
30
|
+
free and open source. Local editing and rendering need no SlopCamera account.
|
|
31
|
+
Generation uses your own Vercel AI Gateway account, or prepaid Hraness Credits
|
|
32
|
+
for prompt-only hosted images; model usage is billed separately.
|
|
42
33
|
|
|
43
34
|
## Install SlopCamera
|
|
44
35
|
|
|
45
36
|
Install [Bun 1.3.14 or newer](https://bun.sh), then install the
|
|
46
|
-
[SlopCamera v3.9.
|
|
37
|
+
[SlopCamera v3.9.2 release](https://github.com/hraness/slopcamera/releases/tag/v3.9.2)
|
|
47
38
|
and its matching Agent Skill:
|
|
48
39
|
|
|
49
40
|
```sh
|
|
50
|
-
bun add --global https://github.com/hraness/slopcamera/releases/download/v3.9.
|
|
41
|
+
bun add --global https://github.com/hraness/slopcamera/releases/download/v3.9.2/hraness-slopcamera-3.9.2.tgz
|
|
51
42
|
slopcamera skill install --target agents
|
|
52
43
|
```
|
|
53
44
|
|
|
@@ -56,8 +47,10 @@ The `agents` target works for agents that read `~/.agents/skills`. Use
|
|
|
56
47
|
inside a repository for a project-only install. Start a new
|
|
57
48
|
agent session so it loads the skill, then ask for a result:
|
|
58
49
|
|
|
59
|
-
>
|
|
60
|
-
>
|
|
50
|
+
> Make a short illustrated film of a late-night tram whose destination
|
|
51
|
+
> changes to the moon. Give it warm windows, a rainy city, and a quiet ending.
|
|
52
|
+
> Start with a frame and a motion test so we can direct it before the final
|
|
53
|
+
> render. Keep the source and sound editable.
|
|
61
54
|
|
|
62
55
|
Setup guides: [Claude Code](https://slopcamera.com/docs/tutorials/claude-code) ·
|
|
63
56
|
[Codex](https://slopcamera.com/docs/tutorials/codex) ·
|
|
@@ -67,16 +60,13 @@ Blender install separately; the
|
|
|
67
60
|
[capability reference](docs/reference/capabilities.md) lists what each
|
|
68
61
|
technique needs.
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
every command with what it may change, and `slopcamera legacy retire` stops
|
|
78
|
-
the retired menu-bar companion opening at login by moving its LaunchAgent aside; it
|
|
79
|
-
deletes nothing, and `slopcamera doctor` prints the command that restores it.
|
|
63
|
+
On macOS, start with [your first animation](https://slopcamera.com/docs/tutorials/first-animation)
|
|
64
|
+
for a supplied scene you can render and revise without a model account. It
|
|
65
|
+
needs the browser and FFmpeg runtimes reported by `slopcamera doctor --json`.
|
|
66
|
+
For a first result on macOS, Linux, or Windows with no browser or paid model,
|
|
67
|
+
follow [your first diagram](https://slopcamera.com/docs/tutorials/first-diagram).
|
|
68
|
+
The [directing guide](docs/how-to/direct-a-film.md) explains how to ask for useful
|
|
69
|
+
changes to composition, timing, camera, and sound.
|
|
80
70
|
|
|
81
71
|
<details>
|
|
82
72
|
<summary>Build from source</summary>
|
|
@@ -110,70 +100,32 @@ placement and resuming long runs.
|
|
|
110
100
|
|
|
111
101
|
</details>
|
|
112
102
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
included diagram, check it, and render it:
|
|
103
|
+
CLI updates in SlopCamera 3.10.0 and newer are enabled by default for supported
|
|
104
|
+
Bun and npm global installations. See [update controls](docs/reference/capabilities.md#cli-updates)
|
|
105
|
+
to keep a version or check for a release.
|
|
117
106
|
|
|
118
|
-
|
|
119
|
-
mkdir slopcamera-first
|
|
120
|
-
cd slopcamera-first
|
|
121
|
-
slopcamera diagram init first.diagram.json
|
|
122
|
-
slopcamera diagram check first.diagram.json --strict
|
|
123
|
-
slopcamera diagram render first.diagram.json
|
|
124
|
-
```
|
|
107
|
+
## Make a film. Then direct it again.
|
|
125
108
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<picture>
|
|
132
|
-
<source media="(prefers-color-scheme: dark)" srcset="apps/web/media/source-to-film-dark-png-b8db55ec6e42.png">
|
|
133
|
-
<img alt="Diagram export, rendered scene, and footage point into Project, which points to Preview and Delivery." src="apps/web/media/source-to-film-light-png-c3c24c460490.png">
|
|
134
|
-
</picture>
|
|
135
|
-
|
|
136
|
-
The diagram above was rendered from one
|
|
137
|
-
[JSON file](examples/showcase/diagram/source-to-film.diagram.json). Each box in
|
|
138
|
-
it is a short object the agent writes:
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{
|
|
142
|
-
"id": "diagram",
|
|
143
|
-
"type": "rect",
|
|
144
|
-
"x": 100,
|
|
145
|
-
"y": 140,
|
|
146
|
-
"width": 320,
|
|
147
|
-
"height": 140,
|
|
148
|
-
"radius": 24,
|
|
149
|
-
"label": "Diagram export",
|
|
150
|
-
"labelFontSize": 36,
|
|
151
|
-
"labelWeight": 500,
|
|
152
|
-
"tone": "blue"
|
|
153
|
-
}
|
|
154
|
-
```
|
|
109
|
+
[Last tram to the moon](https://slopcamera.com/docs/tutorials/first-animation)
|
|
110
|
+
is a complete first project: original illustrated artwork, a twelve-second
|
|
111
|
+
journey, and a visible revision. The tutorial renders locally without a model
|
|
112
|
+
account or downloaded artwork.
|
|
155
113
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
114
|
+
The two supplied render requests use the same scene. The first sets the city
|
|
115
|
+
and departure; the second enlarges the moon while keeping the journey, palette,
|
|
116
|
+
and timing. Ask your agent:
|
|
159
117
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
+ "name": "source-to-film-revised",
|
|
163
|
-
...
|
|
164
|
-
- "label": "Delivery",
|
|
165
|
-
+ "label": "Social delivery",
|
|
166
|
-
```
|
|
118
|
+
> Make the moon dominate the destination. Preserve the copper tram and rainy
|
|
119
|
+
> city. Keep both versions so I can compare their endings.
|
|
167
120
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
121
|
+
[Follow the animation tutorial](https://slopcamera.com/docs/tutorials/first-animation),
|
|
122
|
+
then [remix the showcase](https://slopcamera.com/docs/how-to/remix-the-showcase):
|
|
123
|
+
a bottled storm, a paper ocean, a dancing laundromat, a band of harmonics,
|
|
124
|
+
and three edits of the same eclipse footage. Each piece has source,
|
|
125
|
+
production instructions, and a suggested creative revision.
|
|
171
126
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
[revised light PNG](apps/web/media/source-to-film-revised-light-png-9ad9767c2bdf.png).
|
|
175
|
-
[Your first diagram](https://slopcamera.com/docs/tutorials/first-diagram) walks
|
|
176
|
-
through the same edit.
|
|
127
|
+
For a smaller first task, [make a diagram](https://slopcamera.com/docs/tutorials/first-diagram)
|
|
128
|
+
and export its editable tldraw document, light and dark SVGs, and PNGs.
|
|
177
129
|
|
|
178
130
|
## What SlopCamera does
|
|
179
131
|
|
|
@@ -210,10 +162,10 @@ characters, cloth, and liquid caches; CadQuery for parametric solids and STEP;
|
|
|
210
162
|
and Manim Community for mathematical animation. Seven editable starters
|
|
211
163
|
include a product, character, shaded street, cloth, liquid, CAD bracket, and
|
|
212
164
|
educational presenter. Watch the
|
|
213
|
-
[
|
|
214
|
-
then change its [
|
|
165
|
+
[Rain, bottled](https://slopcamera.com/docs/how-to/remix-the-showcase#rain-bottled),
|
|
166
|
+
then change its [weather, materials, and camera](examples/showcase/studio-relaunch/rain-bottled).
|
|
215
167
|
For 2D motion, try the
|
|
216
|
-
[
|
|
168
|
+
[illustrated tram film](https://slopcamera.com/docs/tutorials/first-animation).
|
|
217
169
|
The [portrait geometry lesson](https://slopcamera.com/docs/how-to/educational-video)
|
|
218
170
|
adds mathematical typesetting, a presenter, and captions.
|
|
219
171
|
|
|
@@ -4,7 +4,7 @@ import type { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import type { ApplicationContext } from "./context";
|
|
6
6
|
|
|
7
|
-
export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.
|
|
7
|
+
export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.10.0" as const;
|
|
8
8
|
|
|
9
9
|
export const OPERATION_KINDS = [
|
|
10
10
|
"scene.audit",
|
|
@@ -365,7 +365,7 @@ import {
|
|
|
365
365
|
workflowRunStore,
|
|
366
366
|
} from "./workflow-runs";
|
|
367
367
|
|
|
368
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
368
|
+
export const SLOPCAMERA_VERSION = "3.10.0";
|
|
369
369
|
|
|
370
370
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
371
371
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { CliUpdateOptions, StartupResult } from "@hraness/cli-update";
|
|
4
|
+
import { SLOPCAMERA_VERSION } from "../../../src/version";
|
|
5
|
+
import { slopcameraUpdatePolicy } from "./update-policy";
|
|
6
|
+
|
|
7
|
+
type EntryPorts = {
|
|
8
|
+
update(options: CliUpdateOptions): Promise<StartupResult>;
|
|
9
|
+
main(): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function runSlopcameraEntrypoint(argv = process.argv.slice(2), ports: Partial<EntryPorts> = {}): Promise<void> {
|
|
13
|
+
const main = ports.main ?? (async () => (await import("./main")).runMainEntrypoint({}, true));
|
|
14
|
+
// Compiled private binaries and their embedded workers keep the native installer.
|
|
15
|
+
if (import.meta.path.includes("$bunfs")) { await main(); return; }
|
|
16
|
+
// The product renders its own command help; the updater sees an inert help request.
|
|
17
|
+
const gateArgv = argv[0] === "update" && argv.slice(1).some(arg => arg === "--help" || arg === "-h")
|
|
18
|
+
? ["help", "update"] : argv;
|
|
19
|
+
const update = await (ports.update ?? (async options => (await import("@hraness/cli-update")).runCliUpdate(options)))({
|
|
20
|
+
packageName: "@hraness/slopcamera", version: SLOPCAMERA_VERSION, binName: "slopcamera",
|
|
21
|
+
entrypoint: fileURLToPath(import.meta.url), argv: gateArgv,
|
|
22
|
+
provider: { kind: "github", repository: "hraness/slopcamera", assetName: "hraness-slopcamera-{version}.tgz" },
|
|
23
|
+
...slopcameraUpdatePolicy(gateArgv),
|
|
24
|
+
verifyArtifact: async artifact => (await import("./update-verification")).verifySlopcameraUpdate(artifact),
|
|
25
|
+
});
|
|
26
|
+
if (update.handled) { process.exitCode = update.exitCode; return; }
|
|
27
|
+
try { await main(); }
|
|
28
|
+
finally { await update.release(); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (import.meta.main) await runSlopcameraEntrypoint();
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -33,6 +33,7 @@ Commands:
|
|
|
33
33
|
runs list|show|resume|approve|cancel
|
|
34
34
|
Inspect and control durable workflow runs
|
|
35
35
|
doctor Check local render and asset capabilities
|
|
36
|
+
update Update the CLI or manage automatic updates
|
|
36
37
|
ai models|image|video|speech|transcribe
|
|
37
38
|
Discover and run Vercel AI Gateway media models
|
|
38
39
|
credits status|topup|wait|forget
|
|
@@ -719,6 +720,13 @@ compiles a bounded set of single-axis candidate plans under cinema/galleries/, e
|
|
|
719
720
|
re-hashed through the canonical composition digest; it never selects a candidate.
|
|
720
721
|
|
|
721
722
|
Imported media starts unverified. Align its audio before relying on synchronization. Structural edits are project-time operations and affect every placement. Camera moves address any placed video stream; metadata-driven screen zooms still require a recording-backed source placement. Face analysis and geometry tracks remain local. --select largest follows the largest currently visible prepared-layer face per frame. --require-all-selected makes a missing explicit/all-selected face invoke the chosen gap policy.`,
|
|
723
|
+
update: `Usage: slopcamera update [check|status|enable|disable] [--json]
|
|
724
|
+
|
|
725
|
+
Supported Bun and npm global installations on macOS and Linux check the immutable
|
|
726
|
+
GitHub release before work starts, at most once a day. Verification needs GitHub CLI.
|
|
727
|
+
Use slopcamera update disable to keep this version, or HRANESS_NO_UPDATE=1 for one
|
|
728
|
+
invocation. Exact Bun version pins require slopcamera update enable. Local image
|
|
729
|
+
vectorization, SDK imports, and the private compiled binary do not update automatically.`,
|
|
722
730
|
render: `Usage: slopcamera render <plan|run> <recording> [--display <id|primary>] [--output <path>] [--dry-run] [--keep-inactivity] [--json]
|
|
723
731
|
|
|
724
732
|
Long inactivity is analyzed and removed by default; use --keep-inactivity to opt out.`,
|
|
@@ -731,10 +739,11 @@ export function commandHelp(topic: readonly string[]): string {
|
|
|
731
739
|
|
|
732
740
|
export function completions(words: readonly string[]): readonly string[] {
|
|
733
741
|
const topLevel = [
|
|
734
|
-
"capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "support", "outputs", "status", "tui", "commands", "legacy", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
|
|
742
|
+
"update", "capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "support", "outputs", "status", "tui", "commands", "legacy", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
|
|
735
743
|
];
|
|
736
744
|
if (words.length <= 1) return topLevel;
|
|
737
745
|
const command = words[0];
|
|
746
|
+
if (command === "update") return ["check", "status", "enable", "disable"];
|
|
738
747
|
if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
|
|
739
748
|
if (command === "html") return ["catalog", "scaffold", "init", "render", "still", "preview", "deliver"];
|
|
740
749
|
if (command === "style") return ["list", "show"];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function slopcameraUpdatePolicy(argv: readonly string[]): { effectFree: boolean; offline: boolean } {
|
|
2
|
+
const [command, action] = argv;
|
|
3
|
+
const portable = ["diagram", "mcp", "canvas", "skill"].includes(command ?? "")
|
|
4
|
+
|| (command === "style" && !argv.includes("--help") && !argv.includes("-h"))
|
|
5
|
+
|| (command === "code" && ["search", "execute"].includes(action ?? ""))
|
|
6
|
+
|| (command === "media" && action === "soundtrack")
|
|
7
|
+
|| (command === "image" && ["vectorize", "icon", "gallery"].includes(action ?? ""))
|
|
8
|
+
|| (command === "image" && action === "generate" && argv.includes("--output"));
|
|
9
|
+
const registry = ["status", "tui", "commands", "outputs", "legacy", "support"].includes(command ?? "");
|
|
10
|
+
return {
|
|
11
|
+
effectFree: command === undefined || ["help", "--help", "-h", "version", "--version", "__complete", "capabilities"].includes(command)
|
|
12
|
+
|| (command === "diagram" && ["help", "--help", "-h"].includes(action ?? ""))
|
|
13
|
+
|| (!portable && !registry && (argv.includes("--help") || argv.includes("-h"))),
|
|
14
|
+
offline: command === "diagram"
|
|
15
|
+
|| (command === "image" && ["vectorize", "icon"].includes(action ?? ""))
|
|
16
|
+
|| (command === "media" && action === "soundtrack"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const repository = "hraness/slopcamera";
|
|
8
|
+
const maximumMetadataBytes = 2 * 1024 * 1024;
|
|
9
|
+
type Gh = (args: readonly string[]) => Promise<Buffer>;
|
|
10
|
+
|
|
11
|
+
const runGh: Gh = args => new Promise((resolve, reject) => {
|
|
12
|
+
execFile("gh", [...args], {
|
|
13
|
+
encoding: "buffer", timeout: 30_000, maxBuffer: maximumMetadataBytes,
|
|
14
|
+
env: { ...process.env, GH_HOST: "github.com", GH_PROMPT_DISABLED: "1", GH_PAGER: "cat" },
|
|
15
|
+
}, (error, stdout) => {
|
|
16
|
+
// gh's error can include authenticated response bodies. Keep diagnostics fixed.
|
|
17
|
+
if (error !== null) reject(new Error("SlopCamera release verification failed. Check GitHub CLI access and retry slopcamera update."));
|
|
18
|
+
else resolve(stdout);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function record(value: unknown): Record<string, unknown> {
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Invalid SlopCamera release metadata.");
|
|
24
|
+
return value as Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Keep the install guide's immutable-release and source-bound attestation checks. */
|
|
28
|
+
export async function verifySlopcameraUpdate(
|
|
29
|
+
artifact: { path: string; version: string; sha256: string },
|
|
30
|
+
gh: Gh = runGh,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u.test(artifact.version)
|
|
33
|
+
|| artifact.version.length > 50 || !/^[a-f0-9]{64}$/u.test(artifact.sha256)) throw new Error("Invalid SlopCamera update identity.");
|
|
34
|
+
const tag = `v${artifact.version}`;
|
|
35
|
+
const json = async (path: string) => {
|
|
36
|
+
const bytes = await gh(["api", path]);
|
|
37
|
+
if (bytes.length > maximumMetadataBytes) throw new Error("SlopCamera release metadata is too large.");
|
|
38
|
+
return record(JSON.parse(bytes.toString("utf8")) as unknown);
|
|
39
|
+
};
|
|
40
|
+
await gh(["release", "verify", tag, "--repo", repository]);
|
|
41
|
+
await gh(["release", "verify-asset", tag, artifact.path, "--repo", repository]);
|
|
42
|
+
const reference = record((await json(`repos/${repository}/git/ref/tags/${tag}`)).object);
|
|
43
|
+
if (reference.type !== "tag" || typeof reference.sha !== "string" || !/^[a-f0-9]{40}$/u.test(reference.sha)) {
|
|
44
|
+
throw new Error("SlopCamera updates require an annotated release tag.");
|
|
45
|
+
}
|
|
46
|
+
const annotated = await json(`repos/${repository}/git/tags/${reference.sha}`);
|
|
47
|
+
const source = record(annotated.object);
|
|
48
|
+
if (annotated.tag !== tag || source.type !== "commit" || typeof source.sha !== "string" || !/^[a-f0-9]{40}$/u.test(source.sha)) {
|
|
49
|
+
throw new Error("SlopCamera release tag does not identify its source commit.");
|
|
50
|
+
}
|
|
51
|
+
const release = await json(`repos/${repository}/releases/tags/${tag}`);
|
|
52
|
+
if (release.tag_name !== tag || release.immutable !== true || !Array.isArray(release.assets)) {
|
|
53
|
+
throw new Error("SlopCamera release is not immutable.");
|
|
54
|
+
}
|
|
55
|
+
const bundles = release.assets.map(record).filter(asset => asset.name === "provenance.jsonl");
|
|
56
|
+
const bundle = bundles[0];
|
|
57
|
+
if (bundles.length !== 1 || bundle === undefined || bundle.state !== "uploaded"
|
|
58
|
+
|| typeof bundle.id !== "number" || !Number.isSafeInteger(bundle.id) || bundle.id <= 0
|
|
59
|
+
|| typeof bundle.size !== "number" || !Number.isSafeInteger(bundle.size) || bundle.size < 1 || bundle.size > maximumMetadataBytes
|
|
60
|
+
|| typeof bundle.digest !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(bundle.digest)) {
|
|
61
|
+
throw new Error("SlopCamera release lacks a checked provenance bundle.");
|
|
62
|
+
}
|
|
63
|
+
const bytes = await gh(["api", `repos/${repository}/releases/assets/${bundle.id}`, "--header", "Accept: application/octet-stream"]);
|
|
64
|
+
if (bytes.length !== bundle.size || `sha256:${createHash("sha256").update(bytes).digest("hex")}` !== bundle.digest) {
|
|
65
|
+
throw new Error("SlopCamera provenance bundle differs from the immutable release.");
|
|
66
|
+
}
|
|
67
|
+
const directory = await mkdtemp(join(tmpdir(), "slopcamera-update-verification-"));
|
|
68
|
+
try {
|
|
69
|
+
const path = join(directory, "provenance.jsonl");
|
|
70
|
+
await writeFile(path, bytes, { mode: 0o600, flag: "wx" });
|
|
71
|
+
await gh(["attestation", "verify", artifact.path, "--repo", repository,
|
|
72
|
+
"--signer-workflow", `${repository}/.github/workflows/release.yml`,
|
|
73
|
+
"--signer-digest", source.sha, "--source-digest", source.sha,
|
|
74
|
+
"--source-ref", `refs/tags/${tag}`, "--deny-self-hosted-runners", "--bundle", path]);
|
|
75
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
76
|
+
}
|
|
@@ -21,6 +21,11 @@ export interface VerbRow {
|
|
|
21
21
|
const row = (words: string, opClass: OpClass, summary: string): VerbRow => ({ path: words.split(" "), opClass, summary });
|
|
22
22
|
|
|
23
23
|
export const CLI_VERBS: readonly VerbRow[] = [
|
|
24
|
+
row("update", "operate", "Install a verified newer CLI release"),
|
|
25
|
+
row("update check", "operate", "Check release availability and save the check time"),
|
|
26
|
+
row("update status", "read", "Show installation support and update preferences"),
|
|
27
|
+
row("update enable", "operate", "Enable automatic CLI updates"),
|
|
28
|
+
row("update disable", "operate", "Keep the installed CLI version"),
|
|
24
29
|
row("capabilities", "read", "List what this build can do and how well each part is tested"),
|
|
25
30
|
row("doctor", "read", "Check local render tools, asset caches and old login items"),
|
|
26
31
|
row("operations list", "read", "List the operations agents can run and their limits"),
|