@hraness/slopcamera 3.2.7 → 3.3.1
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/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { Stats } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { CliError } from "./errors";
|
|
5
|
+
import type { CliIo } from "./io";
|
|
6
|
+
import { writeJson, writeLine } from "./io";
|
|
7
|
+
import { ensurePrivateDirectory } from "./paths";
|
|
8
|
+
|
|
9
|
+
const SETTLE_MS = 400;
|
|
10
|
+
const LABEL = "com.hraness.slopcamera.menubar";
|
|
11
|
+
|
|
12
|
+
function xml(value: string): string { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); }
|
|
13
|
+
function exists(path: string): boolean { try { const info = lstatSync(path); return isAbsolute(path) && realpathSync(path) === resolve(path) && (info.uid === process.getuid?.() || info.uid === 0) && info.isFile() && (info.mode & 0o111) !== 0 && (info.mode & 0o022) === 0; } catch { return false; } }
|
|
14
|
+
|
|
15
|
+
/** The directory agents write user-facing outputs into. */
|
|
16
|
+
export function outputsRoot(stateRoot: string): string {
|
|
17
|
+
const productRoot = stateRoot.endsWith(`${sep}cli`) ? dirname(stateRoot) : stateRoot;
|
|
18
|
+
return join(productRoot, "outputs");
|
|
19
|
+
}
|
|
20
|
+
export function resolveMenubarBinary(repositoryRoot: string, environment: Readonly<Record<string, string | undefined>> = process.env): string | null {
|
|
21
|
+
const explicit = environment.SLOPCAMERA_MENUBAR;
|
|
22
|
+
if (explicit !== undefined) return explicit !== "" && exists(explicit) ? explicit : null;
|
|
23
|
+
const candidates = [ resolve(dirname(process.execPath), "slopcamera-menubar"), ...(environment.HOME === undefined || environment.HOME === "" ? [] : [installedBinaryPath(environment)]), resolve(repositoryRoot, "desktop", "target", "release", "slopcamera-menubar")];
|
|
24
|
+
for (const candidate of candidates) if (candidate !== undefined && candidate !== "" && exists(candidate)) return candidate;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function launchAgentPath(environment: Readonly<Record<string, string | undefined>> = process.env): string {
|
|
28
|
+
const home = environment.HOME; if (home === undefined || home === "" || !isAbsolute(home)) throw new CliError("unavailable", "HOME is required for a per-user LaunchAgent.");
|
|
29
|
+
return join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
30
|
+
}
|
|
31
|
+
export function installedBinaryPath(environment: Readonly<Record<string, string | undefined>> = process.env): string {
|
|
32
|
+
const home = environment.HOME; if (home === undefined || home === "" || !isAbsolute(home)) throw new CliError("unavailable", "HOME is required for a per-user menu-bar install.");
|
|
33
|
+
return join(home, "Library", "Application Support", "Slopcamera", "menubar", "slopcamera-menubar");
|
|
34
|
+
}
|
|
35
|
+
export function launchAgentPlist(binary: string): string {
|
|
36
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${xml(LABEL)}</string><key>ProgramArguments</key><array><string>${xml(binary)}</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><false/><key>ProcessType</key><string>Interactive</string><key>LimitLoadToSessionType</key><string>Aqua</string></dict></plist>\n`;
|
|
37
|
+
}
|
|
38
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
39
|
+
const MAX_BINARY_BYTES = 128 * 1024 * 1024;
|
|
40
|
+
|
|
41
|
+
function infoAt(path: string): Stats | null {
|
|
42
|
+
try { return lstatSync(path); } catch (error) {
|
|
43
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Existing shared directories retain their permissions; links are never followed. */
|
|
49
|
+
function checkParents(path: string, environment: Environment, create = false): void {
|
|
50
|
+
const home = environment.HOME;
|
|
51
|
+
if (!home || !isAbsolute(home) || realpathSync(home) !== resolve(home)) {
|
|
52
|
+
throw new CliError("unsafe-path", "HOME must be an absolute physical directory.");
|
|
53
|
+
}
|
|
54
|
+
const suffix = relative(home, dirname(path));
|
|
55
|
+
if (suffix.startsWith(`..${sep}`) || suffix === ".." || isAbsolute(suffix)) {
|
|
56
|
+
throw new CliError("unsafe-path", "The menu-bar install must remain under HOME.");
|
|
57
|
+
}
|
|
58
|
+
let cursor = home;
|
|
59
|
+
for (const part of ["", ...suffix.split(sep)]) {
|
|
60
|
+
if (part) cursor = join(cursor, part);
|
|
61
|
+
let info = infoAt(cursor);
|
|
62
|
+
if (info === null) {
|
|
63
|
+
if (!create) return;
|
|
64
|
+
mkdirSync(cursor, { mode: 0o700 });
|
|
65
|
+
info = lstatSync(cursor);
|
|
66
|
+
}
|
|
67
|
+
if (!info.isDirectory() || info.uid !== process.getuid?.() || (info.mode & 0o022) !== 0) {
|
|
68
|
+
throw new CliError("unsafe-path", "Menu-bar install directories must be owned physical directories without shared write access.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertOwnedFile(path: string): void {
|
|
74
|
+
const info = infoAt(path);
|
|
75
|
+
if (info !== null && (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || (info.mode & 0o022) !== 0)) {
|
|
76
|
+
throw new CliError("unsafe-path", "The menu-bar install contains an unsafe file.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readRegular(path: string, maximum: number, executable = false): Buffer {
|
|
81
|
+
if (!isAbsolute(path) || realpathSync(path) !== resolve(path) || Buffer.byteLength(path) > 4096 || /[\u0000-\u001f\u007f{}"\\]/u.test(path)) {
|
|
82
|
+
throw new CliError("unsafe-path", "The menu-bar file must have a bounded physical absolute path.");
|
|
83
|
+
}
|
|
84
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
85
|
+
try {
|
|
86
|
+
const info = fstatSync(fd);
|
|
87
|
+
if (!info.isFile() || (info.uid !== process.getuid?.() && info.uid !== 0) || info.size > maximum || info.size === 0 || (info.mode & 0o022) !== 0 || (executable && (info.mode & 0o111) === 0)) {
|
|
88
|
+
throw new CliError("unsafe-path", "The menu-bar file must be bounded, regular, and protected from shared writes.");
|
|
89
|
+
}
|
|
90
|
+
const bytes = Buffer.alloc(info.size + 1);
|
|
91
|
+
let count = 0;
|
|
92
|
+
while (count < bytes.length) {
|
|
93
|
+
const read = readSync(fd, bytes, count, bytes.length - count, count);
|
|
94
|
+
if (read === 0) break;
|
|
95
|
+
count += read;
|
|
96
|
+
}
|
|
97
|
+
const after = fstatSync(fd);
|
|
98
|
+
if (count !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs || after.ctimeMs !== info.ctimeMs) {
|
|
99
|
+
throw new CliError("conflict", "The menu-bar file changed while it was being read.");
|
|
100
|
+
}
|
|
101
|
+
return bytes.subarray(0, count);
|
|
102
|
+
} finally { closeSync(fd); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readAgent(path: string, environment: Environment): string | null {
|
|
106
|
+
checkParents(path, environment);
|
|
107
|
+
if (infoAt(path) === null) return null;
|
|
108
|
+
assertOwnedFile(path);
|
|
109
|
+
return readRegular(path, 16 * 1024).toString("utf8");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Configuration state only; it does not claim the launchd service is running. */
|
|
113
|
+
export type LaunchAgentState = "absent" | "installed" | "conflict";
|
|
114
|
+
export type LaunchctlRunner = (args: readonly string[], allowMissing?: boolean) => string | null;
|
|
115
|
+
function launchctl(args: readonly string[], allowMissing = false): string | null {
|
|
116
|
+
const uid = process.getuid?.();
|
|
117
|
+
if (uid === undefined) throw new CliError("unavailable", "The current user has no launchd GUI domain.");
|
|
118
|
+
const result = Bun.spawnSync(["/bin/launchctl", ...args.map((arg) => arg.replaceAll("{uid}", String(uid)))], { stdin: "ignore", stdout: "pipe", stderr: "pipe", timeout: 10_000, maxBuffer: 64 * 1024 });
|
|
119
|
+
return interpretLaunchctlResult(result, uid, allowMissing);
|
|
120
|
+
}
|
|
121
|
+
export function interpretLaunchctlResult(result: { success: boolean; exitCode: number; stdout: Uint8Array; stderr: Uint8Array }, uid: number, allowMissing: boolean): string | null {
|
|
122
|
+
if (result.stdout.byteLength + result.stderr.byteLength > 64 * 1024) throw new CliError("unavailable", "launchctl exceeded the bounded response size.");
|
|
123
|
+
if (result.success && result.exitCode === 0) return Buffer.from(result.stdout).toString();
|
|
124
|
+
if (allowMissing && result.exitCode === 113 && result.stdout.length === 0 && Buffer.from(result.stderr).toString() === `Bad request.\nCould not find service "${LABEL}" in domain for user gui: ${uid}\n`) return null;
|
|
125
|
+
throw new CliError("unavailable", "launchctl could not reconcile the Slopcamera menu-bar LaunchAgent. Run menubar install again to retry.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function loadedOwned(path: string, binary: string, current: string | null, run: LaunchctlRunner): boolean {
|
|
129
|
+
const output = run(["print", `gui/{uid}/${LABEL}`], true);
|
|
130
|
+
if (output === null) return false;
|
|
131
|
+
const uid = process.getuid?.();
|
|
132
|
+
if (current === null || Buffer.byteLength(output) > 64 * 1024 || !output.startsWith(`gui/${uid}/${LABEL} = {\n`)) {
|
|
133
|
+
throw new CliError("conflict", "The loaded menu-bar service has an unverified identity.");
|
|
134
|
+
}
|
|
135
|
+
const lines = output.split("\n").map(line => line.trim());
|
|
136
|
+
const field = (key: string): string | null => {
|
|
137
|
+
const values = lines.filter(line => line.startsWith(`${key} = `));
|
|
138
|
+
return values.length === 1 ? values[0]!.slice(key.length + 3) : null;
|
|
139
|
+
};
|
|
140
|
+
const argumentsStart = lines.indexOf("arguments = {");
|
|
141
|
+
const argumentsEnd = lines.indexOf("}", argumentsStart + 1);
|
|
142
|
+
if (field("path") !== path || field("program") !== binary || argumentsStart < 0 || argumentsEnd !== argumentsStart + 2 || lines[argumentsStart + 1] !== binary) {
|
|
143
|
+
throw new CliError("conflict", "The loaded menu-bar service does not match this install; it was left untouched.");
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
export function launchAgentState(binary: string, environment: Environment = process.env): LaunchAgentState {
|
|
148
|
+
const content = readAgent(launchAgentPath(environment), environment);
|
|
149
|
+
if (content === null) return "absent";
|
|
150
|
+
checkParents(binary, environment);
|
|
151
|
+
assertOwnedFile(binary);
|
|
152
|
+
return content === launchAgentPlist(binary) && exists(binary) ? "installed" : "conflict";
|
|
153
|
+
}
|
|
154
|
+
function assertMac(platform: NodeJS.Platform): void {
|
|
155
|
+
if (platform !== "darwin") throw new CliError("unsupported-plan", "Slopcamera menu-bar LaunchAgents are supported only on macOS.");
|
|
156
|
+
}
|
|
157
|
+
export function installLaunchAgent(binary: string, environment: Environment = process.env, platform: NodeJS.Platform = process.platform, runLaunchctl: LaunchctlRunner = launchctl): LaunchAgentState {
|
|
158
|
+
assertMac(platform);
|
|
159
|
+
if (!isAbsolute(binary)) throw new CliError("unsafe-path", "LaunchAgent binary must be an absolute path.");
|
|
160
|
+
const stable = installedBinaryPath(environment);
|
|
161
|
+
const path = launchAgentPath(environment);
|
|
162
|
+
const current = readAgent(path, environment);
|
|
163
|
+
const expected = launchAgentPlist(stable);
|
|
164
|
+
checkParents(stable, environment);
|
|
165
|
+
assertOwnedFile(stable);
|
|
166
|
+
if (current !== null && current !== expected || current === null && infoAt(stable) !== null) {
|
|
167
|
+
throw new CliError("conflict", "The existing Slopcamera menu-bar install is not owned by this command.");
|
|
168
|
+
}
|
|
169
|
+
const bytes = readRegular(binary, MAX_BINARY_BYTES, true);
|
|
170
|
+
loadedOwned(path, stable, current, runLaunchctl);
|
|
171
|
+
checkParents(path, environment, true);
|
|
172
|
+
checkParents(stable, environment, true);
|
|
173
|
+
// Unique private staging directories prevent predictable temporary-file clobbering.
|
|
174
|
+
const binaryStage = mkdtempSync(join(dirname(stable), ".install-"));
|
|
175
|
+
let agentStage: string | undefined;
|
|
176
|
+
try {
|
|
177
|
+
agentStage = mkdtempSync(join(dirname(path), ".slopcamera-install-"));
|
|
178
|
+
const stagedBinary = join(binaryStage, "binary");
|
|
179
|
+
const stagedAgent = join(agentStage, "agent.plist");
|
|
180
|
+
writeFileSync(stagedBinary, bytes, { mode: 0o700, flag: "wx" });
|
|
181
|
+
writeFileSync(stagedAgent, expected, { mode: 0o600, flag: "wx" });
|
|
182
|
+
if (readAgent(path, environment) !== current) throw new CliError("conflict", "The menu-bar install changed concurrently.");
|
|
183
|
+
assertOwnedFile(stable);
|
|
184
|
+
// Retry bootstrap even when the plist matches: a previous bootstrap may have failed.
|
|
185
|
+
if (loadedOwned(path, stable, current, runLaunchctl)) runLaunchctl(["bootout", `gui/{uid}/${LABEL}`], true);
|
|
186
|
+
renameSync(stagedBinary, stable);
|
|
187
|
+
renameSync(stagedAgent, path);
|
|
188
|
+
runLaunchctl(["bootstrap", "gui/{uid}", path]);
|
|
189
|
+
return "installed";
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(binaryStage, { recursive: true });
|
|
192
|
+
if (agentStage !== undefined) rmSync(agentStage, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export function uninstallLaunchAgent(_binary: string, environment: Environment = process.env, platform: NodeJS.Platform = process.platform, runLaunchctl: LaunchctlRunner = launchctl): LaunchAgentState {
|
|
196
|
+
assertMac(platform);
|
|
197
|
+
const stable = installedBinaryPath(environment);
|
|
198
|
+
const path = launchAgentPath(environment);
|
|
199
|
+
const current = readAgent(path, environment);
|
|
200
|
+
if (current === null) return "absent";
|
|
201
|
+
if (current !== launchAgentPlist(stable)) throw new CliError("conflict", "The existing Slopcamera menu-bar LaunchAgent is not owned by this command.");
|
|
202
|
+
checkParents(stable, environment);
|
|
203
|
+
assertOwnedFile(stable);
|
|
204
|
+
if (loadedOwned(path, stable, current, runLaunchctl)) runLaunchctl(["bootout", `gui/{uid}/${LABEL}`], true);
|
|
205
|
+
if (readAgent(path, environment) !== current) throw new CliError("conflict", "The menu-bar install changed concurrently.");
|
|
206
|
+
assertOwnedFile(stable);
|
|
207
|
+
rmSync(path);
|
|
208
|
+
if (infoAt(stable) !== null) rmSync(stable);
|
|
209
|
+
return "absent";
|
|
210
|
+
}
|
|
211
|
+
async function runBinary(binary: string, foreground: boolean): Promise<number> {
|
|
212
|
+
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 could not start."); }
|
|
214
|
+
if (!foreground) { child.unref(); const settled = await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); if (settled !== null) throw new CliError("unavailable", "The Slopcamera menu bar exited during startup."); return 0; }
|
|
215
|
+
return await child.exited;
|
|
216
|
+
}
|
|
217
|
+
export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground"): Promise<void> {
|
|
218
|
+
const binary = resolveMenubarBinary(repositoryRoot, io.env);
|
|
219
|
+
if (binary === null) throw new CliError("unavailable", "The Slopcamera menu bar is not installed. Build it separately or set SLOPCAMERA_MENUBAR.");
|
|
220
|
+
const code = await runBinary(binary, mode === "foreground");
|
|
221
|
+
if (code !== 0) throw new CliError("unavailable", "The Slopcamera menu bar exited during startup.");
|
|
222
|
+
if (asJson) writeJson(io, { running: mode === "background", foreground: mode === "foreground" });
|
|
223
|
+
else writeLine(io, mode === "foreground" ? "Slopcamera menu bar exited." : "Slopcamera menu bar is running.");
|
|
224
|
+
}
|
|
225
|
+
export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean): Promise<void> {
|
|
226
|
+
assertMac(io.platform);
|
|
227
|
+
const stable = installedBinaryPath(io.env);
|
|
228
|
+
let state: LaunchAgentState;
|
|
229
|
+
if (action === "install") {
|
|
230
|
+
const binary = resolveMenubarBinary(repositoryRoot, io.env);
|
|
231
|
+
if (binary === null) throw new CliError("unavailable", "The Slopcamera menu bar is not installed. Build it separately or set SLOPCAMERA_MENUBAR.");
|
|
232
|
+
state = installLaunchAgent(binary, io.env, io.platform);
|
|
233
|
+
} else {
|
|
234
|
+
state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform) : launchAgentState(stable, io.env);
|
|
235
|
+
}
|
|
236
|
+
if (asJson) writeJson(io, { launchAgent: state });
|
|
237
|
+
else writeLine(io, `Slopcamera menu-bar LaunchAgent: ${state}.`);
|
|
238
|
+
}
|
|
239
|
+
export async function reportOutputsRoot(io: CliIo, stateRoot: string, asJson: boolean): Promise<void> {
|
|
240
|
+
const directory = outputsRoot(stateRoot);
|
|
241
|
+
await ensurePrivateDirectory(directory);
|
|
242
|
+
if (asJson) writeJson(io, { outputs: directory });
|
|
243
|
+
else writeLine(io, directory);
|
|
244
|
+
}
|
|
@@ -15,18 +15,25 @@ import {
|
|
|
15
15
|
} from "../contracts";
|
|
16
16
|
import {
|
|
17
17
|
analyzeMusic,
|
|
18
|
+
analyzeAudioReactivity,
|
|
19
|
+
AUDIO_REACTIVITY_MAX_DURATION_US,
|
|
20
|
+
AUDIO_REACTIVITY_SAMPLE_RATE_HZ,
|
|
18
21
|
canonicalSlopcameraPersistenceDocument,
|
|
19
22
|
canonicalJson,
|
|
20
23
|
canonicalJsonSha256,
|
|
24
|
+
createNodeBundleFileSystem,
|
|
21
25
|
saveAnalysisArtifact,
|
|
22
26
|
saveVideoProject,
|
|
23
27
|
sha256Hex,
|
|
24
28
|
type MonoPcm,
|
|
25
29
|
} from "../core";
|
|
30
|
+
import { HtmlOverlayAudioReactivitySchema } from "../html-overlay/audio-reactivity";
|
|
26
31
|
import { decodeAlignmentPcm, resolveAudioAnalysisSubject } from "./audio-analysis";
|
|
27
32
|
import { CliError } from "./errors";
|
|
28
33
|
import type { ProcessRunner } from "./io";
|
|
34
|
+
import { SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS } from "./media-ingest-model";
|
|
29
35
|
import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
|
|
36
|
+
import { resolveVerifiedProjectMedia, type PhysicalProjectMediaFingerprint } from "./project-media-integrity";
|
|
30
37
|
import { projectAnalysisPath, type OpenProject } from "./project-service";
|
|
31
38
|
|
|
32
39
|
export const MUSIC_ANALYSIS_SAMPLE_RATE_HZ = 8_000;
|
|
@@ -174,6 +181,60 @@ export async function readMonoFloat32PcmBounded(
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
const MAX_AUDIO_REACTIVITY_PCM_BYTES = 600 * AUDIO_REACTIVITY_SAMPLE_RATE_HZ * 4;
|
|
185
|
+
|
|
186
|
+
/** Decode one verified retained soundtrack prefix for the optional bands-v1 sidecar. */
|
|
187
|
+
export async function analyzeRetainedAudioReactivity(options: {
|
|
188
|
+
readonly repositoryRoot: string;
|
|
189
|
+
readonly jobDirectory: string;
|
|
190
|
+
readonly audio: PhysicalProjectMediaFingerprint;
|
|
191
|
+
readonly durationUs: number;
|
|
192
|
+
readonly streamIndex: number;
|
|
193
|
+
readonly ffmpeg: string;
|
|
194
|
+
readonly run: (argv: readonly [string, ...string[]]) => Promise<unknown>;
|
|
195
|
+
readonly fence: () => Promise<void>;
|
|
196
|
+
}) {
|
|
197
|
+
if (!Number.isSafeInteger(options.durationUs) || options.durationUs < 1
|
|
198
|
+
|| options.durationUs > AUDIO_REACTIVITY_MAX_DURATION_US
|
|
199
|
+
|| !Number.isSafeInteger(options.streamIndex) || options.streamIndex < 0) {
|
|
200
|
+
throw new CliError("invalid-data", "Audio reactivity duration or stream index is outside its bound.");
|
|
201
|
+
}
|
|
202
|
+
const verify = async () => {
|
|
203
|
+
await options.fence();
|
|
204
|
+
return await resolveVerifiedProjectMedia({ repositoryRoot: options.repositoryRoot,
|
|
205
|
+
path: options.audio.path, expected: options.audio, label: "Audio reactivity soundtrack" });
|
|
206
|
+
};
|
|
207
|
+
const source = await verify();
|
|
208
|
+
const pcmPath = join(options.jobDirectory, "audio-reactivity.f32le");
|
|
209
|
+
const fs = createNodeBundleFileSystem(options.jobDirectory);
|
|
210
|
+
try {
|
|
211
|
+
await options.run([options.ffmpeg, "-nostdin", "-v", "error", "-n", "-threads", "2",
|
|
212
|
+
...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS, "-i", source, "-map", `0:${options.streamIndex}`,
|
|
213
|
+
"-vn", "-sn", "-dn", "-t", (options.durationUs / 1_000_000).toFixed(6),
|
|
214
|
+
"-ac", "1", "-ar", String(AUDIO_REACTIVITY_SAMPLE_RATE_HZ), "-c:a", "pcm_f32le", "-f", "f32le",
|
|
215
|
+
"-fs", String(MAX_AUDIO_REACTIVITY_PCM_BYTES + 4), pcmPath]);
|
|
216
|
+
await verify();
|
|
217
|
+
const before = await fs.inspectFile!("audio-reactivity.f32le", MAX_AUDIO_REACTIVITY_PCM_BYTES);
|
|
218
|
+
const pcm = await readMonoFloat32PcmBounded(pcmPath, { maxBytes: MAX_AUDIO_REACTIVITY_PCM_BYTES, sampleRateHz: AUDIO_REACTIVITY_SAMPLE_RATE_HZ });
|
|
219
|
+
const expectedSamples = Math.round(options.durationUs * AUDIO_REACTIVITY_SAMPLE_RATE_HZ / 1_000_000);
|
|
220
|
+
if (Math.abs(pcm.samples.length - expectedSamples) > 2) {
|
|
221
|
+
throw new CliError("invalid-data", "Audio reactivity decode did not cover the requested soundtrack prefix.");
|
|
222
|
+
}
|
|
223
|
+
const analysis = analyzeAudioReactivity(pcm);
|
|
224
|
+
await options.fence();
|
|
225
|
+
const after = await fs.inspectFile!("audio-reactivity.f32le", MAX_AUDIO_REACTIVITY_PCM_BYTES);
|
|
226
|
+
if (before.bytes !== after.bytes || before.sha256 !== after.sha256) {
|
|
227
|
+
throw new CliError("conflict", "Audio reactivity PCM changed during analysis.");
|
|
228
|
+
}
|
|
229
|
+
return HtmlOverlayAudioReactivitySchema.parse({ kind: "slopcamera.audio-reactivity", schemaVersion: 1,
|
|
230
|
+
profile: "bands-v1", rateHz: 60, ...analysis,
|
|
231
|
+
source: { bytes: options.audio.bytes, sha256: options.audio.sha256 },
|
|
232
|
+
pcm: { sampleCount: pcm.samples.length, sha256: before.sha256 } });
|
|
233
|
+
} finally {
|
|
234
|
+
await rm(pcmPath, { force: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
177
238
|
export interface WithDecodedMusicPcmOptions {
|
|
178
239
|
readonly ffmpeg: string;
|
|
179
240
|
readonly maxDecodedPcmBytes?: number;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
|
|
2
|
+
|
|
1
3
|
import { constants } from "node:fs";
|
|
2
4
|
import { mkdir, open } from "node:fs/promises";
|
|
3
5
|
import { dirname, resolve } from "node:path";
|
|
@@ -16,7 +18,10 @@ import { CliError } from "./errors";
|
|
|
16
18
|
|
|
17
19
|
const HEADLESS_SLOPCAMERA_CLI_MODULE = "@hraness/slopcamera/cli";
|
|
18
20
|
|
|
19
|
-
async function runHeadlessSlopcameraCli(
|
|
21
|
+
async function runHeadlessSlopcameraCli(
|
|
22
|
+
argv: readonly string[],
|
|
23
|
+
options?: Readonly<{ onUsefulResult?: UsefulResultObserver }>,
|
|
24
|
+
): Promise<void> {
|
|
20
25
|
// Keep this as a runtime package import so the headless CLI retains its own
|
|
21
26
|
// package-relative skill and asset resolution inside an installed bundle.
|
|
22
27
|
const module: unknown = await import(HEADLESS_SLOPCAMERA_CLI_MODULE);
|
|
@@ -28,13 +33,14 @@ async function runHeadlessSlopcameraCli(argv: readonly string[]): Promise<void>
|
|
|
28
33
|
) {
|
|
29
34
|
throw new CliError("unavailable", "The portable Slopcamera CLI is unavailable.");
|
|
30
35
|
}
|
|
31
|
-
await module.main(argv);
|
|
36
|
+
await module.main(argv, options);
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface PortableSurfaceDependencies {
|
|
40
|
+
readonly onUsefulResult?: UsefulResultObserver;
|
|
35
41
|
readonly cwd?: () => string;
|
|
36
42
|
readonly log?: (value: string) => void;
|
|
37
|
-
readonly runHeadless?: (argv: readonly string[]) => Promise<void>;
|
|
43
|
+
readonly runHeadless?: (argv: readonly string[], options?: Readonly<{ onUsefulResult?: UsefulResultObserver }>) => Promise<void>;
|
|
38
44
|
readonly writeScaffold?: (path: string, html: string) => Promise<void>;
|
|
39
45
|
}
|
|
40
46
|
function optionValue(argv: readonly string[], name: string): string | undefined {
|
|
@@ -187,6 +193,7 @@ async function runHtmlScaffold(
|
|
|
187
193
|
createHtmlOverlayScaffold(kind),
|
|
188
194
|
);
|
|
189
195
|
(dependencies.log ?? console.log)(`Created ${outputPath}`);
|
|
196
|
+
reportUsefulResult(dependencies.onUsefulResult);
|
|
190
197
|
return 0;
|
|
191
198
|
}
|
|
192
199
|
|
|
@@ -228,14 +235,20 @@ export async function runPortableSurface(
|
|
|
228
235
|
)
|
|
229
236
|
|| (
|
|
230
237
|
argv[0] === "image"
|
|
231
|
-
&& (
|
|
238
|
+
&& (
|
|
239
|
+
argv[1] === "vectorize" ||
|
|
240
|
+
argv[1] === "generate" ||
|
|
241
|
+
argv[1] === "icon" ||
|
|
242
|
+
argv[1] === "gallery"
|
|
243
|
+
)
|
|
232
244
|
);
|
|
233
245
|
if (!delegatesToHeadless) return undefined;
|
|
234
246
|
|
|
235
247
|
const previousExitCode = process.exitCode;
|
|
236
248
|
process.exitCode = undefined;
|
|
237
249
|
try {
|
|
238
|
-
await (dependencies.runHeadless ?? runHeadlessSlopcameraCli)(argv
|
|
250
|
+
await (dependencies.runHeadless ?? runHeadlessSlopcameraCli)(argv,
|
|
251
|
+
dependencies.onUsefulResult === undefined ? {} : { onUsefulResult: dependencies.onUsefulResult });
|
|
239
252
|
return process.exitCode ?? 0;
|
|
240
253
|
} finally {
|
|
241
254
|
process.exitCode = previousExitCode;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { ApplicationContext } from "../application/context";
|
|
5
|
+
import { admitSpatialAssetFile, SpatialAssetAdmissionError } from "../application/spatial-asset-admission";
|
|
6
|
+
import { SpatialAssetAdmissionV1Schema } from "../contracts/spatial-asset";
|
|
7
|
+
import type { SpatialAssetCommand } from "./args";
|
|
8
|
+
import { CliError } from "./errors";
|
|
9
|
+
import { displayPath, ensurePhysicalPrivateDirectoryWithin, resolveSafePath } from "./paths";
|
|
10
|
+
import { publishSpatialSource } from "./spatial-scene-service";
|
|
11
|
+
|
|
12
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
13
|
+
const pathRelative = relative(root, candidate);
|
|
14
|
+
return pathRelative === "" || (!pathRelative.startsWith("..") && !isAbsolute(pathRelative));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isMissing(error: unknown): boolean {
|
|
18
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A source root must exist and be physical before bounding one file below it. */
|
|
22
|
+
async function physicalSourceRoot(path: string): Promise<string> {
|
|
23
|
+
let details;
|
|
24
|
+
try { details = await lstat(path); }
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (isMissing(error)) throw new CliError("not-found", `--source-root is not a directory: ${path}`);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (details.isSymbolicLink() || !details.isDirectory()) {
|
|
30
|
+
throw new CliError("unsafe-path", `--source-root must be a physical directory without symlinks: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
return await realpath(path);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Admit one explicit local GLB below --source-root (default: the project root),
|
|
37
|
+
* publish its exact bytes and derived facts beside --output, and write the
|
|
38
|
+
* deterministic admission document without replacing any existing path.
|
|
39
|
+
*/
|
|
40
|
+
export async function executeSpatialAssetCommand(
|
|
41
|
+
application: ApplicationContext,
|
|
42
|
+
command: SpatialAssetCommand,
|
|
43
|
+
adapter: Readonly<{ signal?: AbortSignal }>,
|
|
44
|
+
): Promise<unknown> {
|
|
45
|
+
const repositoryRoot = application.paths.repositoryRoot;
|
|
46
|
+
const signal = adapter.signal ?? new AbortController().signal;
|
|
47
|
+
const fence = async () => {
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
await application.hostResourceLease?.assertOwned();
|
|
50
|
+
signal.throwIfAborted();
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
signal.throwIfAborted();
|
|
54
|
+
const sourceRoot = await physicalSourceRoot(resolve(repositoryRoot, command.sourceRoot ?? "."));
|
|
55
|
+
const source = resolve(sourceRoot, command.file);
|
|
56
|
+
let physicalSource: string;
|
|
57
|
+
try { physicalSource = await realpath(source); }
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (isMissing(error)) throw new CliError("not-found", `Admitted asset does not exist: ${command.file}`);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (!isWithin(sourceRoot, physicalSource)) {
|
|
63
|
+
throw new CliError("unsafe-path", `Admitted asset escapes --source-root: ${command.file}`);
|
|
64
|
+
}
|
|
65
|
+
if (physicalSource !== source) {
|
|
66
|
+
throw new CliError("unsafe-path", `Admitted asset must be a physical file without symlink components: ${command.file}`);
|
|
67
|
+
}
|
|
68
|
+
if (!(await lstat(physicalSource)).isFile()) {
|
|
69
|
+
throw new CliError("invalid-data", `Admitted asset is not a regular file: ${command.file}`);
|
|
70
|
+
}
|
|
71
|
+
const output = await resolveSafePath(repositoryRoot, command.output);
|
|
72
|
+
try {
|
|
73
|
+
await lstat(output);
|
|
74
|
+
throw new CliError("conflict", "Scene asset admission output already exists. Choose a new path to retain both revisions.");
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof CliError) throw error;
|
|
77
|
+
if (!isMissing(error)) throw error;
|
|
78
|
+
}
|
|
79
|
+
const outputParent = relative(repositoryRoot, dirname(output));
|
|
80
|
+
const assetRoot = outputParent === ""
|
|
81
|
+
? repositoryRoot
|
|
82
|
+
: await ensurePhysicalPrivateDirectoryWithin(repositoryRoot, outputParent);
|
|
83
|
+
const result = await admitSpatialAssetFile({
|
|
84
|
+
filePath: physicalSource, assetRoot,
|
|
85
|
+
...(command.assetId === undefined ? {} : { assetId: command.assetId }),
|
|
86
|
+
metersPerUnit: command.metersPerUnit, sourceUp: command.sourceUp,
|
|
87
|
+
signal, beforePublication: fence,
|
|
88
|
+
});
|
|
89
|
+
const document = SpatialAssetAdmissionV1Schema.parse({
|
|
90
|
+
kind: "slopcamera.spatial-asset-admission", schemaVersion: 1,
|
|
91
|
+
manifest: result.manifest, factsManifest: result.factsManifest, facts: result.facts,
|
|
92
|
+
bounds: result.bounds, entity: result.entity, artifacts: result.artifacts,
|
|
93
|
+
operations: result.operations,
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await publishSpatialSource(output, document, fence);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const failure = error instanceof CliError ? error : new CliError("conflict", "Scene asset admission output publication failed.");
|
|
99
|
+
throw new CliError(signal.aborted ? "cancelled" : failure.code, failure.message, {
|
|
100
|
+
...failure.details, published: [result.artifacts.payload, result.artifacts.facts], output: command.output,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
output: displayPath(repositoryRoot, output),
|
|
105
|
+
assetId: result.manifest.assetId,
|
|
106
|
+
factsAssetId: result.factsManifest.assetId,
|
|
107
|
+
entityId: result.entity.entityId,
|
|
108
|
+
bounds: result.bounds,
|
|
109
|
+
artifacts: result.artifacts,
|
|
110
|
+
document,
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof SpatialAssetAdmissionError) {
|
|
114
|
+
throw new CliError(signal.aborted ? "cancelled" : "conflict", error.message, {
|
|
115
|
+
published: error.published, attempted: error.attempted, output: command.output,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (signal.aborted && !(error instanceof CliError)) {
|
|
119
|
+
throw new CliError("cancelled", "Scene asset admission cancelled; retain its source and any publication evidence.");
|
|
120
|
+
}
|
|
121
|
+
if (error instanceof RangeError) throw new CliError("invalid-data", error.message);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|