@hraness/slopcamera 3.4.0 → 3.5.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 +29 -17
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +1 -1
- package/apps/desktop/cli/commands.ts +8 -1
- package/apps/desktop/cli/help.ts +33 -9
- package/apps/desktop/cli/menubar.ts +91 -15
- package/apps/desktop/dist/cli/main.js +313 -281
- package/apps/desktop/html-overlay/catalog.ts +1 -0
- package/apps/desktop/html-overlay/spatial.ts +10 -6
- package/apps/desktop/workflows/index.ts +1 -0
- package/dist/cli.js +1 -1
- package/dist/{index-sy1n4zfy.js → index-24w75s6a.js} +1 -1
- package/dist/{index-qtn3s421.js → index-8bhp7cj2.js} +1 -1
- package/dist/{index-abnk4h9c.js → index-jab2mg0s.js} +2 -2
- package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/vectorize/worker.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +3 -3
- package/examples/studio/vgpu/README.md +1 -1
- package/package.json +4 -4
- package/skills/slopcamera/SKILL.md +4 -3
- package/skills/slopcamera/references/brand-illustrations.md +237 -0
- package/skills/slopcamera/references/gateway-media.md +1 -1
- package/skills/slopcamera/references/install.md +10 -1
- package/skills/slopcamera/references/patent-drawings.md +4 -4
- package/skills/slopcamera/references/support.md +1 -1
- package/skills/slopcamera/references/vectorization.md +3 -1
- package/src/code/public-operations.ts +9 -0
- package/src/mcp/tools.ts +4 -0
- package/src/operations.ts +4 -0
- package/src/support.ts +1 -1
- package/src/vectorize/command.ts +6 -1
- package/src/vectorize/supervisor.ts +23 -3
- package/src/vectorize/tool.ts +8 -4
- package/src/version.ts +1 -1
- package/src/visual-style.ts +4 -1
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Slopcamera
|
|
2
2
|
|
|
3
|
-
[](https://slopcamera.com)
|
|
4
4
|
|
|
5
|
-
**Slopcamera
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
**Slopcamera is a media studio for coding agents. Codex, Claude Code, and other
|
|
6
|
+
agents make images, diagrams, animation, 3D scenes, and edited video from source
|
|
7
|
+
files they can keep revising.**
|
|
8
8
|
|
|
9
|
-
Describe
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
Describe the result you want to Codex, Claude Code, or another coding agent.
|
|
10
|
+
Slopcamera gives the agent a Bun CLI, a TypeScript SDK, and an Agent Skill matched
|
|
11
|
+
to the CLI version, so it can inspect sources, set up cameras, edit a composition,
|
|
12
|
+
and render. `slopcamera mcp` runs an MCP server with a smaller, fixed set of diagram,
|
|
13
|
+
image, and scene tools. Projects stay in local files, and there is no Slopcamera
|
|
14
|
+
account. Generation uses your own Vercel AI Gateway account or, for prompt-only
|
|
15
|
+
images, prepaid Hraness Credits.
|
|
14
16
|
|
|
15
17
|
[Install](#install-slopcamera) · [Make a first diagram](#make-your-first-diagram) · [Capabilities](#what-slopcamera-does) · [Documentation](https://slopcamera.com/docs) · [slopcamera.com](https://slopcamera.com)
|
|
16
18
|
|
|
@@ -38,20 +40,21 @@ the rendered output with source, runtime requirements and reproduction steps.
|
|
|
38
40
|
timing, framing, captions, and effects as project decisions. Preview and final
|
|
39
41
|
renders use the same timeline and composition.
|
|
40
42
|
- **Choose where computation happens.** Local rendering and editing use your
|
|
41
|
-
machine. Optional model-backed work uses your Vercel AI Gateway access
|
|
42
|
-
|
|
43
|
+
machine. Optional model-backed work uses your Vercel AI Gateway access and
|
|
44
|
+
uploads named local media only after you acknowledge it. Prompt-only images can
|
|
45
|
+
instead run on the hosted API with prepaid Hraness Credits. Native Python
|
|
43
46
|
authoring requires separate trust because it runs as your current user.
|
|
44
47
|
|
|
45
48
|
## Install Slopcamera
|
|
46
49
|
|
|
47
|
-
Slopcamera v3.
|
|
50
|
+
Slopcamera v3.4.0 includes 17 reusable film and animation direction profiles, deterministic exposure helpers, and a sample renderer that defaults to native 4K. Use the [style direction guide](docs/how-to/direct-visual-styles.md) to author cel animation, pixel art, mathematical explanations, and historical film treatments from retained sources.
|
|
48
51
|
|
|
49
52
|
Install [Bun 1.3.14 or newer](https://bun.sh), then install the verified
|
|
50
|
-
[Slopcamera v3.
|
|
53
|
+
[Slopcamera v3.4.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.4.0)
|
|
51
54
|
from its canonical archive:
|
|
52
55
|
|
|
53
56
|
```sh
|
|
54
|
-
bun add --global https://github.com/hraness/slopcamera/releases/download/v3.
|
|
57
|
+
bun add --global https://github.com/hraness/slopcamera/releases/download/v3.4.0/hraness-slopcamera-3.4.0.tgz
|
|
55
58
|
slopcamera skill install --target agents
|
|
56
59
|
```
|
|
57
60
|
|
|
@@ -134,7 +137,8 @@ architecture from dimensions, [build a parametric design](https://slopcamera.com
|
|
|
134
137
|
|
|
135
138
|
## What Slopcamera does
|
|
136
139
|
|
|
137
|
-
Slopcamera
|
|
140
|
+
Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes,
|
|
141
|
+
and video from source files it can keep revising.
|
|
138
142
|
|
|
139
143
|
### Author scenes and direct cameras
|
|
140
144
|
|
|
@@ -153,7 +157,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
|
|
|
153
157
|
capture appearance; they do not establish collision geometry or editable native
|
|
154
158
|
meshes. See [Directed scenes](docs/spatial-scenes.md).
|
|
155
159
|
|
|
156
|
-
Slopcamera v3.3.1
|
|
160
|
+
Slopcamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
|
|
157
161
|
document describes beats, actions, camera coverage, and look intents, compiles
|
|
158
162
|
into proposed performance/camera/cinema/material-lighting/shot documents, and
|
|
159
163
|
fans out into bounded galleries per axis. Declared effects bind into renders,
|
|
@@ -308,7 +312,7 @@ declarative graphs, approvals, and resuming work.
|
|
|
308
312
|
workflows run with the current user's access. Hashes and receipts identify
|
|
309
313
|
observed inputs and outputs; they do not make arbitrary code hermetic.
|
|
310
314
|
- **MCP is a subset.** Its 17 fixed tools check and render diagrams, plan and audit
|
|
311
|
-
scenes, and run six bounded
|
|
315
|
+
scenes, and run six bounded operation codes. It does not
|
|
312
316
|
expose every local CLI operation and never mutates project state.
|
|
313
317
|
|
|
314
318
|
## Design and trust
|
|
@@ -333,6 +337,14 @@ source identity alone does not promise identical pixels on another machine.
|
|
|
333
337
|
See [Architecture](docs/architecture.md), [`SECURITY.md`](SECURITY.md),
|
|
334
338
|
[`PRIVACY.md`](PRIVACY.md), and [`NOTICE.md`](NOTICE.md) for the detailed boundaries.
|
|
335
339
|
|
|
340
|
+
Your agent renders images, diagrams, scenes, animation, and video from source
|
|
341
|
+
files you keep, and important operations record their inputs and outputs in
|
|
342
|
+
receipts, so a rendered result arrives with the record of how it was made: the
|
|
343
|
+
design every Hraness project shares. [The thread through
|
|
344
|
+
hraness](https://hraness.com/writing/the-thread-through-hraness) follows that
|
|
345
|
+
design across the projects, and the [ALGAL
|
|
346
|
+
vision](https://algal.computer/docs/vision/) states the bet behind it.
|
|
347
|
+
|
|
336
348
|
## Documentation
|
|
337
349
|
|
|
338
350
|
- **Learn:** [Your first diagram](docs/tutorials/first-diagram.md) · [Your first native film](docs/tutorials/first-native-film.md).
|
|
@@ -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.5.0" as const;
|
|
8
8
|
|
|
9
9
|
export const OPERATION_KINDS = [
|
|
10
10
|
"scene.audit",
|
package/apps/desktop/cli/args.ts
CHANGED
|
@@ -3659,7 +3659,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
|
|
|
3659
3659
|
case "menubar": {
|
|
3660
3660
|
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--foreground": "flag", "--background": "flag" });
|
|
3661
3661
|
const positionals = parsed.positionals;
|
|
3662
|
-
if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background]
|
|
3662
|
+
if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] or slopcamera menubar install|uninstall|status. See slopcamera help menubar.");
|
|
3663
3663
|
if (positionals[0] !== undefined && (optionFlag(parsed, "--foreground") || optionFlag(parsed, "--background"))) throw new CliError("usage", "Choose a menu-bar action or a run mode, not both.");
|
|
3664
3664
|
return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
|
|
3665
3665
|
}
|
|
@@ -362,7 +362,7 @@ import {
|
|
|
362
362
|
workflowRunStore,
|
|
363
363
|
} from "./workflow-runs";
|
|
364
364
|
|
|
365
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
365
|
+
export const SLOPCAMERA_VERSION = "3.5.0";
|
|
366
366
|
|
|
367
367
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
368
368
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -739,6 +739,13 @@ function humanTime(microseconds: number): string {
|
|
|
739
739
|
|
|
740
740
|
function requireCapability(capabilities: readonly Capability[], name: Capability["name"]): string {
|
|
741
741
|
const capability = capabilityByName(capabilities, name);
|
|
742
|
+
if ((!capability.available || capability.command === undefined) && name === "face-analyzer") {
|
|
743
|
+
throw new CliError(
|
|
744
|
+
"unavailable",
|
|
745
|
+
"Face analysis needs a small helper that isn't built on this Mac yet. In a Slopcamera checkout, run bun run build:desktop:analysis:macos; it uses Apple's command line tools and asks before installing them.",
|
|
746
|
+
{ capability: name, ...(capability.reason === undefined ? {} : { reason: capability.reason }) },
|
|
747
|
+
);
|
|
748
|
+
}
|
|
742
749
|
if (!capability.available || capability.command === undefined) {
|
|
743
750
|
throw new CliError(
|
|
744
751
|
"unavailable",
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { HTML_OVERLAY_SCAFFOLD_KINDS } from "../html-overlay";
|
|
|
3
3
|
const HTML_OVERLAY_SCAFFOLD_KIND_HELP =
|
|
4
4
|
HTML_OVERLAY_SCAFFOLD_KINDS.join("|");
|
|
5
5
|
|
|
6
|
-
const GLOBAL_HELP = `slopcamera
|
|
6
|
+
const GLOBAL_HELP = `slopcamera: a media studio for coding agents
|
|
7
7
|
|
|
8
8
|
Usage: slopcamera <command> [options]
|
|
9
9
|
|
|
10
10
|
Commands:
|
|
11
|
-
capabilities
|
|
12
|
-
operations list|show
|
|
11
|
+
capabilities List what this build can do and how well each part is tested
|
|
12
|
+
operations list|show List the operations agents can run and their limits
|
|
13
13
|
diagram init|check|render Create, validate, or render portable diagram sources
|
|
14
14
|
diagram sheets init|check|render
|
|
15
15
|
Create and render black-and-white drawing sheets
|
|
@@ -35,8 +35,9 @@ Commands:
|
|
|
35
35
|
credits status|topup|wait|forget
|
|
36
36
|
Buy and inspect hosted generation credits
|
|
37
37
|
media audio|color Apply local non-destructive audio and video effects
|
|
38
|
-
menubar [--background]
|
|
39
|
-
|
|
38
|
+
menubar [--foreground|--background]
|
|
39
|
+
Show your outputs in the menu bar
|
|
40
|
+
menubar install|uninstall|status Open it at login, stop that, or check it
|
|
40
41
|
support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
|
|
41
42
|
Optional support; no feature requires payment
|
|
42
43
|
outputs Print the agent outputs directory
|
|
@@ -55,13 +56,36 @@ Commands:
|
|
|
55
56
|
render plan|run <recording> Resolve or execute a render plan
|
|
56
57
|
assets emoji search|resolve Find checked local emoji overlays
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
Examples:
|
|
60
|
+
slopcamera doctor Check what this Mac can render
|
|
61
|
+
slopcamera html scaffold plain --output intro.html
|
|
62
|
+
Start an HTML scene
|
|
63
|
+
slopcamera render run REC --output out.mp4 Render a recording
|
|
61
64
|
|
|
62
|
-
Run slopcamera help <command> for command-specific help
|
|
65
|
+
Run slopcamera help <command> for command-specific help.
|
|
66
|
+
Optional support: slopcamera support · Turn off: HRANESS_SUPPORT_AUDIENCE=off`;
|
|
63
67
|
|
|
64
68
|
const HELP: Readonly<Record<string, string>> = {
|
|
69
|
+
menubar: `Usage:
|
|
70
|
+
slopcamera menubar [--foreground|--background] [--json]
|
|
71
|
+
slopcamera menubar install|uninstall|status [--json]
|
|
72
|
+
|
|
73
|
+
Shows 📷 in the menu bar with your recent outputs. It only lists and opens
|
|
74
|
+
files in the outputs folder (slopcamera outputs).
|
|
75
|
+
|
|
76
|
+
menubar Open it now; it closes when this command ends
|
|
77
|
+
menubar --background Open it now and return
|
|
78
|
+
menubar install Open it now and every time you log in
|
|
79
|
+
menubar uninstall Stop opening it at login and remove the installed copy
|
|
80
|
+
menubar status Check whether it opens at login and is running
|
|
81
|
+
|
|
82
|
+
If it's already in your menu bar, starting it again says so and succeeds.
|
|
83
|
+
macOS shows a notice that slopcamera-menubar can open at login. Turn it off any
|
|
84
|
+
time in System Settings › General › Login Items & Extensions.
|
|
85
|
+
|
|
86
|
+
The menu bar isn't in released packages yet. In a Slopcamera checkout, build it
|
|
87
|
+
with cargo build --release --manifest-path desktop/Cargo.toml and slopcamera
|
|
88
|
+
finds it; elsewhere, set SLOPCAMERA_MENUBAR to the built file.`,
|
|
65
89
|
capabilities: `Usage: slopcamera capabilities [--json]
|
|
66
90
|
|
|
67
91
|
Print the exact statically assembled capability manifest for this CLI build. Each module owns named operation, workflow, MCP-tool, command, and runtime-profile identities. The manifest includes trust classes, resource/effect policies, runtime requirements, qualification status, and a canonical SHA-256. It reads no workspace source, loads no plugin, probes no runtime, and grants no authority. Use doctor separately to inspect this machine's currently available executables.`,
|
|
@@ -208,33 +208,109 @@ export function uninstallLaunchAgent(_binary: string, environment: Environment =
|
|
|
208
208
|
if (infoAt(stable) !== null) rmSync(stable);
|
|
209
209
|
return "absent";
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
/** `slopcamera-menubar` exits with this code when another copy already holds the menu bar. */
|
|
212
|
+
export const ALREADY_RUNNING_EXIT = 3;
|
|
213
|
+
const NOT_BUILT = "The Slopcamera menu bar isn't in this release yet. Build it with cargo build --release --manifest-path desktop/Cargo.toml in a Slopcamera checkout; slopcamera finds it there, or set SLOPCAMERA_MENUBAR to the built file.";
|
|
214
|
+
const LOGIN_ITEMS = "System Settings › General › Login Items & Extensions";
|
|
215
|
+
|
|
216
|
+
export type Launched = "running" | "already-running" | "exited";
|
|
217
|
+
export type BinaryRunner = (binary: string, foreground: boolean) => Promise<number | null>;
|
|
218
|
+
|
|
219
|
+
/** Resolves the exit code, or null when a background copy is still running after startup. */
|
|
220
|
+
async function spawnBinary(binary: string, foreground: boolean): Promise<number | null> {
|
|
212
221
|
let child: Bun.Subprocess;
|
|
213
|
-
try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar
|
|
214
|
-
if (!foreground) { child.unref();
|
|
222
|
+
try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar couldn't start."); }
|
|
223
|
+
if (!foreground) { child.unref(); return await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); }
|
|
215
224
|
return await child.exited;
|
|
216
225
|
}
|
|
217
|
-
|
|
226
|
+
|
|
227
|
+
/** Interprets the companion's exit: another running copy is success, not a failure. */
|
|
228
|
+
export function launchOutcome(code: number | null, foreground: boolean): Launched {
|
|
229
|
+
if (code === ALREADY_RUNNING_EXIT) return "already-running";
|
|
230
|
+
if (code === null && !foreground) return "running";
|
|
231
|
+
if (code === 0 && foreground) return "exited";
|
|
232
|
+
throw new CliError("unavailable", "The Slopcamera menu bar stopped while starting. Run it in the foreground to see why: slopcamera menubar --foreground");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface Marks { readonly ok: string; readonly warn: string; readonly note: string; readonly next: string }
|
|
236
|
+
/** ✓ ⚠ 🔐 → with ASCII fallbacks for dumb terminals and non-UTF-8 locales. */
|
|
237
|
+
export function marks(env: Readonly<Record<string, string | undefined>>): Marks {
|
|
238
|
+
const utf8 = [env.LC_ALL, env.LC_CTYPE, env.LANG].some((value) => value !== undefined && /utf-?8/i.test(value));
|
|
239
|
+
const ascii = env.TERM === "dumb" || !utf8 || env.HRANESS_ASCII === "1";
|
|
240
|
+
return ascii ? { ok: "OK", warn: "WARN", note: "NOTE", next: "->" } : { ok: "✓", warn: "⚠", note: "🔐", next: "→" };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The LOGIN_ITEM notice (notifies, so there is no confirm line). The requester is the bare companion until Slopcamera ships as an app. */
|
|
244
|
+
export function loginItemNotice(env: Readonly<Record<string, string | undefined>>): string {
|
|
245
|
+
return `${marks(env).note} macOS will show a notice that slopcamera-menubar can open at login. That's Slopcamera's menu bar.\n It lists your outputs and opens when you log in. Nothing else runs in the background. Turn it off any time in ${LOGIN_ITEMS}.\n`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function launchMessage(outcome: Launched, env: Readonly<Record<string, string | undefined>>): string {
|
|
249
|
+
const mark = marks(env);
|
|
250
|
+
if (outcome === "already-running") return `${mark.ok} Slopcamera is already in your menu bar. Look for 📷.`;
|
|
251
|
+
if (outcome === "running") return `${mark.ok} Slopcamera is in your menu bar. Look for 📷.`;
|
|
252
|
+
return "The Slopcamera menu bar closed.";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function stateMessage(action: "install" | "uninstall" | "status", state: LaunchAgentState, running: boolean, env: Readonly<Record<string, string | undefined>>): string {
|
|
256
|
+
const mark = marks(env);
|
|
257
|
+
if (state === "conflict") return `${mark.warn} Another menu-bar setup that this command didn't create is in the way. It was left untouched.\n Remove ~/Library/LaunchAgents/${LABEL}.plist yourself if you don't need it.`;
|
|
258
|
+
if (state === "absent") return action === "uninstall" ? `${mark.ok} Slopcamera no longer opens in your menu bar at login.` : `Slopcamera doesn't open at login.\n${mark.next} slopcamera menubar install`;
|
|
259
|
+
if (action === "install") return `${mark.ok} Slopcamera opens in your menu bar now and at every login. Look for 📷.\n Remove it any time: slopcamera menubar uninstall`;
|
|
260
|
+
return running
|
|
261
|
+
? `${mark.ok} Slopcamera opens at login and is in your menu bar now.`
|
|
262
|
+
: `${mark.warn} Slopcamera is set to open at login but isn't running. It may be turned off in ${LOGIN_ITEMS}.\n${mark.next} slopcamera menubar install`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Whether launchd reports the loaded companion as running. Reads state only. */
|
|
266
|
+
export function serviceRunning(runLaunchctl: LaunchctlRunner): boolean {
|
|
267
|
+
const output = runLaunchctl(["print", `gui/{uid}/${LABEL}`], true);
|
|
268
|
+
return output !== null && output.split("\n").some((line) => line.trim() === "state = running");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type ProcessProbe = () => boolean;
|
|
272
|
+
/** Whether any copy of the companion runs for this user, including one started by hand. */
|
|
273
|
+
function companionProcessRunning(): boolean {
|
|
274
|
+
const uid = process.getuid?.();
|
|
275
|
+
if (uid === undefined) return false;
|
|
276
|
+
try {
|
|
277
|
+
return Bun.spawnSync(["/usr/bin/pgrep", "-x", "-U", String(uid), "slopcamera-menubar"], { stdin: "ignore", stdout: "ignore", stderr: "ignore", timeout: 5_000 }).exitCode === 0;
|
|
278
|
+
} catch { return false; }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The menu bar is showing when launchd runs the login item or another copy holds it. */
|
|
282
|
+
async function menubarShowing(runLaunchctl: LaunchctlRunner, probe: ProcessProbe, waitMs: number): Promise<boolean> {
|
|
283
|
+
const deadline = Date.now() + waitMs;
|
|
284
|
+
for (;;) {
|
|
285
|
+
if (serviceRunning(runLaunchctl) || probe()) return true;
|
|
286
|
+
if (Date.now() >= deadline) return false;
|
|
287
|
+
await Bun.sleep(200);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground", runBinary: BinaryRunner = spawnBinary): Promise<void> {
|
|
218
292
|
const binary = resolveMenubarBinary(repositoryRoot, io.env);
|
|
219
|
-
if (binary === null) throw new CliError("unavailable",
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
else writeLine(io, mode === "foreground" ? "Slopcamera menu bar exited." : "Slopcamera menu bar is running.");
|
|
293
|
+
if (binary === null) throw new CliError("unavailable", NOT_BUILT);
|
|
294
|
+
const outcome = launchOutcome(await runBinary(binary, mode === "foreground"), mode === "foreground");
|
|
295
|
+
if (asJson) writeJson(io, { running: outcome !== "exited", foreground: mode === "foreground", alreadyRunning: outcome === "already-running" });
|
|
296
|
+
else writeLine(io, launchMessage(outcome, io.env));
|
|
224
297
|
}
|
|
225
|
-
export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean): Promise<void> {
|
|
298
|
+
export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean, runLaunchctl: LaunchctlRunner = launchctl, probe: ProcessProbe = companionProcessRunning): Promise<void> {
|
|
226
299
|
assertMac(io.platform);
|
|
227
300
|
const stable = installedBinaryPath(io.env);
|
|
228
301
|
let state: LaunchAgentState;
|
|
229
302
|
if (action === "install") {
|
|
230
303
|
const binary = resolveMenubarBinary(repositoryRoot, io.env);
|
|
231
|
-
if (binary === null) throw new CliError("unavailable",
|
|
232
|
-
|
|
304
|
+
if (binary === null) throw new CliError("unavailable", NOT_BUILT);
|
|
305
|
+
if (!asJson) io.stderr(loginItemNotice(io.env));
|
|
306
|
+
state = installLaunchAgent(binary, io.env, io.platform, runLaunchctl);
|
|
233
307
|
} else {
|
|
234
|
-
state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform) : launchAgentState(stable, io.env);
|
|
308
|
+
state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform, runLaunchctl) : launchAgentState(stable, io.env);
|
|
235
309
|
}
|
|
236
|
-
|
|
237
|
-
|
|
310
|
+
// A fresh login item may take a moment to start, so install waits briefly.
|
|
311
|
+
const running = state === "installed" && await menubarShowing(runLaunchctl, probe, action === "install" ? 1_000 : 0);
|
|
312
|
+
if (asJson) writeJson(io, { launchAgent: state, running });
|
|
313
|
+
else writeLine(io, stateMessage(action, state, running, io.env));
|
|
238
314
|
}
|
|
239
315
|
export async function reportOutputsRoot(io: CliIo, stateRoot: string, asJson: boolean): Promise<void> {
|
|
240
316
|
const directory = outputsRoot(stateRoot);
|