@hypit/hypit 0.1.2 → 0.1.4
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 +1 -1
- package/dist/public/studio-adapter.d.ts +31 -2
- package/package.json +4 -1
- package/packages/audio-track-studio/src/index.ts +7 -4
- package/packages/build-result/README.md +58 -0
- package/packages/build-result/package.json +2 -1
- package/packages/build-result/src/decode.ts +10 -8
- package/packages/build-result/src/file-reference.ts +42 -0
- package/packages/build-result/src/index.ts +3 -0
- package/packages/build-result/src/replace-file-windows.ts +58 -0
- package/packages/build-result/src/replace-file.ts +11 -0
- package/packages/build-result/src/store.ts +44 -13
- package/packages/build-result/src/types.ts +38 -8
- package/packages/build-result/src/writer.ts +12 -1
- package/packages/build-result-fs/README.md +23 -1
- package/packages/build-result-s3/README.md +5 -0
- package/packages/build-result-s3/src/repository.ts +19 -5
- package/packages/caption-fine/README.md +17 -2
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +48 -36
- package/packages/caption-fine-studio/package.json +8 -3
- package/packages/caption-fine-studio/src/index.ts +10 -1
- package/packages/cli/README.md +25 -0
- package/packages/cli/src/arguments.ts +16 -11
- package/packages/cli/src/build-planning.ts +6 -1
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +42 -13
- package/packages/cli/src/machine-view.ts +12 -3
- package/packages/cli/src/main.ts +4 -0
- package/packages/cli/src/output.ts +28 -12
- package/packages/cli/src/result-export.ts +22 -13
- package/packages/cli/src/run-file.ts +19 -8
- package/packages/comment-sticker-studio/src/index.ts +2 -1
- package/packages/compiler-node/src/compiler.ts +1 -0
- package/packages/estimate/README.md +56 -3
- package/packages/estimate/src/program.ts +7 -8
- package/packages/fonts-open/README.md +5 -0
- package/packages/fonts-open/package.json +5 -1
- package/packages/fonts-open/src/studio.ts +21 -0
- package/packages/gpt-image-kits/README.md +17 -12
- package/packages/gpt-image-kits/kits/phone-ugc-v1.svs +8 -8
- package/packages/media-track-studio/src/index.ts +7 -2
- package/packages/provider-whisperx-local/README.md +57 -0
- package/packages/ranking/src/render.ts +11 -9
- package/packages/ranking-studio/README.md +11 -0
- package/packages/ranking-studio/package.json +8 -3
- package/packages/ranking-studio/src/index.ts +11 -4
- package/packages/runtime/src/catalog.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-local/README.md +19 -0
- package/packages/runtime-local/src/programs.ts +39 -24
- package/packages/runtime-local/src/runtime.ts +2 -0
- package/packages/script/README.md +22 -0
- package/packages/script/src/edit.ts +123 -73
- package/packages/script/src/lexical.ts +40 -6
- package/packages/script/src/parser.ts +36 -8
- package/packages/script/src/types.ts +4 -0
- package/packages/seedance-kits/README.md +3 -1
- package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
- package/packages/speech-alignment/src/align.ts +45 -21
- package/packages/studio/INSPECTOR.md +104 -0
- package/packages/studio/README.md +65 -4
- package/packages/studio/src/build-library.ts +99 -84
- package/packages/studio/src/library-media.ts +19 -0
- package/packages/studio/src/parameter-values.ts +41 -1
- package/packages/studio/src/parameters.ts +20 -21
- package/packages/studio/src/server.ts +58 -30
- package/packages/studio/src/shared.ts +20 -6
- package/packages/studio/src/studio-registry.ts +22 -0
- package/packages/studio/src/style.css +135 -144
- package/packages/studio/src/temporal-edit.ts +131 -0
- package/packages/studio/src/ui/artifact-name.ts +80 -0
- package/packages/studio/src/ui/artifact-preview.ts +99 -0
- package/packages/studio/src/ui/icons.ts +5 -1
- package/packages/studio/src/ui/library.ts +373 -192
- package/packages/studio/src/ui/main.ts +153 -31
- package/packages/studio/src/ui/material-preview.ts +1 -1
- package/packages/studio/src/ui/overlay.ts +21 -20
- package/packages/studio/src/ui/sidebar-panel.ts +31 -0
- package/packages/studio/src/ui/stage.ts +124 -8
- package/packages/studio/src/ui/timeline.ts +16 -179
- package/packages/studio/start.ts +15 -3
- package/packages/studio-adapter/README.md +21 -0
- package/packages/studio-adapter/src/index.ts +26 -1
- package/packages/temporal-markup/EDITING.md +183 -0
- package/packages/temporal-markup/README.md +4 -0
- package/packages/typography-track-studio/package.json +8 -3
- package/packages/typography-track-studio/src/index.ts +5 -1
- package/packages/video-cli/README.md +12 -3
- package/packages/video-cli/src/creation.ts +11 -5
- package/packages/whisperx/README.md +7 -0
- package/packages/workspace/src/index.ts +2 -0
- package/packages/workspace-fs-node/README.md +4 -1
- package/packages/workspace-fs-node/src/workspace.ts +3 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fileReferenceIdentity } from "@hypit/build-result";
|
|
1
2
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
3
|
|
|
3
4
|
import { buildIdCreatedAt } from "@hypit/protocol";
|
|
@@ -5,17 +6,16 @@ import type { BuildView, NodeRuntimeHost, RuntimeHostTransientExecution } from "
|
|
|
5
6
|
import type { StoredValue, TypeRef } from "@hypit/protocol";
|
|
6
7
|
import type {
|
|
7
8
|
BuildResultFileRange,
|
|
8
|
-
BuildResultFileRef,
|
|
9
9
|
BuildResultManifest,
|
|
10
10
|
BuildResultRepository,
|
|
11
11
|
FinishedBuildResultManifest,
|
|
12
|
-
RepositoryBuildResultOutput,
|
|
13
12
|
} from "@hypit/build-result";
|
|
14
13
|
|
|
15
14
|
import { resolveBuildResultValue } from "@hypit/cli";
|
|
16
15
|
import { videoCliDistribution } from "@hypit/video-cli";
|
|
17
16
|
|
|
18
|
-
import type { StudioArtifactView, StudioLibraryView, StudioTaskView } from "./shared.js";
|
|
17
|
+
import type { StudioArtifactView, StudioLibraryRequest, StudioLibraryView, StudioTaskView } from "./shared.js";
|
|
18
|
+
import { mergeStudioArtifacts } from "./library-media.js";
|
|
19
19
|
|
|
20
20
|
type RuntimeControl = Awaited<ReturnType<NodeRuntimeHost["openControl"]>>;
|
|
21
21
|
|
|
@@ -24,7 +24,8 @@ export type StudioBuildLibrary = {
|
|
|
24
24
|
readonly runtime?: Pick<RuntimeControl, "activity">;
|
|
25
25
|
/** Runtime-owned, disposable execution for the current authoring session. */
|
|
26
26
|
readonly transientExecution?: RuntimeHostTransientExecution;
|
|
27
|
-
readonly library: (
|
|
27
|
+
readonly library: (request: StudioLibraryRequest) => Promise<StudioLibraryView>;
|
|
28
|
+
readonly renameArtifact: (build: string, output: string, displayName: string | null) => Promise<string | undefined>;
|
|
28
29
|
readonly resolveHistoricalOutput: (
|
|
29
30
|
build: string,
|
|
30
31
|
output: string,
|
|
@@ -36,7 +37,6 @@ export type StudioBuildLibrary = {
|
|
|
36
37
|
readonly openArtifact: (
|
|
37
38
|
build: string,
|
|
38
39
|
output: string,
|
|
39
|
-
valuePath: string,
|
|
40
40
|
) => Promise<{
|
|
41
41
|
readonly mediaType: string;
|
|
42
42
|
readonly size: number;
|
|
@@ -51,7 +51,7 @@ function projectPath(root: string, path: string): string {
|
|
|
51
51
|
|
|
52
52
|
function isWithin(root: string, path: string): boolean {
|
|
53
53
|
const rel = relative(resolve(root), projectPath(root, path));
|
|
54
|
-
return rel === "" || (!rel.startsWith(
|
|
54
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -65,15 +65,9 @@ function presentedPath(root: string, path: string): string {
|
|
|
65
65
|
return (relative(resolve(root), absolute) || ".").split(sep).join("/");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function buildBelongsTo(root: string, view: BuildView): boolean {
|
|
69
|
-
return view.source !== undefined
|
|
70
|
-
&& (isWithin(root, view.run?.path ?? view.source.path) || isWithin(root, view.source.path));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
68
|
function taskStatus(view: BuildView): StudioTaskView["status"] {
|
|
74
|
-
if (view.issue !== undefined) return "
|
|
69
|
+
if (view.issue !== undefined) return "attention";
|
|
75
70
|
if (view.activity === "submitting" || view.activity === "ready") return "queued";
|
|
76
|
-
if (view.activity === "saving-result") return "active";
|
|
77
71
|
return view.activity;
|
|
78
72
|
}
|
|
79
73
|
|
|
@@ -83,18 +77,20 @@ function taskView(
|
|
|
83
77
|
result?: BuildResultManifest,
|
|
84
78
|
): StudioTaskView {
|
|
85
79
|
if (view.source === undefined) throw new Error(`Active Build ${view.id} has no source`);
|
|
80
|
+
const detail = view.issue?.message ?? view.stop?.reason;
|
|
86
81
|
return {
|
|
87
82
|
id: view.id,
|
|
88
83
|
...(result?.title === undefined ? {} : { title: result.title }),
|
|
89
84
|
...(result?.note === undefined ? {} : { note: result.note }),
|
|
90
85
|
...(result?.highlightedOutputs === undefined ? {} : { highlightedOutputs: result.highlightedOutputs }),
|
|
91
86
|
createdAt: view.createdAt,
|
|
87
|
+
ongoing: true,
|
|
92
88
|
status: taskStatus(view),
|
|
89
|
+
...(detail === undefined ? {} : { detail }),
|
|
90
|
+
...(view.requests === undefined ? {} : { requests: view.requests }),
|
|
93
91
|
source: presentedPath(root, view.source.path),
|
|
94
92
|
...(view.run === undefined ? {} : { run: presentedPath(root, view.run.path) }),
|
|
95
93
|
targets: result?.targets ?? view.targets,
|
|
96
|
-
acceptedRecords: view.acceptedRecords,
|
|
97
|
-
outstandingCommands: view.outstandingCommands,
|
|
98
94
|
operations: view.operations.map((operation) => ({
|
|
99
95
|
status: operation.status,
|
|
100
96
|
...(operation.progress?.phase === undefined ? {} : { phase: operation.progress.phase }),
|
|
@@ -112,107 +108,117 @@ function resultTaskView(root: string, result: FinishedBuildResultManifest): Stud
|
|
|
112
108
|
...(result.note === undefined ? {} : { note: result.note }),
|
|
113
109
|
...(result.highlightedOutputs === undefined ? {} : { highlightedOutputs: result.highlightedOutputs }),
|
|
114
110
|
createdAt: buildIdCreatedAt(result.id)!,
|
|
111
|
+
finishedAt: result.finishedAt,
|
|
112
|
+
ongoing: false,
|
|
115
113
|
status: result.outcome,
|
|
114
|
+
...(result.failure === undefined ? {} : { detail: result.failure }),
|
|
116
115
|
source: presentedPath(root, result.source.path),
|
|
117
116
|
...(result.run === undefined ? {} : { run: presentedPath(root, result.run.path) }),
|
|
118
117
|
targets: result.targets,
|
|
119
|
-
acceptedRecords: Object.keys(result.outputs).length,
|
|
120
|
-
outstandingCommands: 0,
|
|
121
118
|
operations: [],
|
|
122
119
|
};
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
function
|
|
126
|
-
return
|
|
127
|
-
? `${current}[${segment}]`
|
|
128
|
-
: /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(segment)
|
|
129
|
-
? `${current}.${segment}`
|
|
130
|
-
: `${current}[${JSON.stringify(segment)}]`, "$");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function filesInResolvedOutput(resolved: RepositoryBuildResultOutput): readonly {
|
|
134
|
-
readonly valuePath: string;
|
|
135
|
-
readonly file: BuildResultFileRef;
|
|
136
|
-
}[] {
|
|
137
|
-
if (resolved.value.kind === "build-file") return [{ valuePath: "$", file: resolved.value }];
|
|
138
|
-
if (resolved.value.kind === "value") {
|
|
139
|
-
return resolved.value.document.resources.map((binding) => ({
|
|
140
|
-
valuePath: presentedValuePath(binding.at),
|
|
141
|
-
file: binding.file,
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
return [];
|
|
122
|
+
function isMedia(mediaType: string): boolean {
|
|
123
|
+
return /^(image|video|audio)\//u.test(mediaType);
|
|
145
124
|
}
|
|
146
125
|
|
|
147
126
|
async function artifactsForResults(
|
|
148
127
|
root: string,
|
|
149
128
|
repository: BuildResultRepository,
|
|
150
129
|
manifests: readonly BuildResultManifest[],
|
|
130
|
+
media?: StudioLibraryRequest["media"],
|
|
151
131
|
): Promise<readonly StudioArtifactView[]> {
|
|
152
132
|
const nested = await Promise.all(manifests.flatMap((manifest) =>
|
|
153
|
-
Object.
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
133
|
+
Object.entries(manifest.outputs).map(async ([output, entry]): Promise<StudioArtifactView[]> => {
|
|
134
|
+
// Composite Outputs remain intact. Listing media never opens their Value Documents.
|
|
135
|
+
if (entry.value.kind !== "build-file" && entry.value.kind !== "external-file" && entry.value.kind !== "build-output") return [];
|
|
136
|
+
const description = entry.value.kind === "build-file" || entry.value.kind === "external-file"
|
|
137
|
+
? { kind: "resource", mediaType: entry.value.mediaType }
|
|
138
|
+
: await repository.describeOutput(manifest.id, output);
|
|
139
|
+
if (description?.kind !== "resource" || (description.mediaType === undefined || !isMedia(description.mediaType))) return [];
|
|
140
|
+
if (media !== undefined && !description.mediaType.startsWith(`${media}/`)) return [];
|
|
141
|
+
const resolved = entry.value.kind === "build-file"
|
|
142
|
+
? { build: manifest.id, output, value: entry.value }
|
|
143
|
+
: await repository.resolve(manifest.id, output);
|
|
144
|
+
if (resolved === undefined || (resolved.value.kind !== "build-file" && resolved.value.kind !== "external-file")) return [];
|
|
145
|
+
const file = resolved.value;
|
|
146
|
+
const source = presentedPath(root, manifest.source.path);
|
|
147
|
+
const run = manifest.run === undefined ? undefined : presentedPath(root, manifest.run.path);
|
|
148
|
+
return [{
|
|
149
|
+
id: fileReferenceIdentity(resolved.build, file),
|
|
158
150
|
build: manifest.id,
|
|
159
151
|
createdAt: buildIdCreatedAt(manifest.id)!,
|
|
160
152
|
output,
|
|
153
|
+
nameEditable: manifest.outcome !== undefined,
|
|
154
|
+
...(entry.displayName === undefined ? {} : { displayName: entry.displayName }),
|
|
161
155
|
highlighted: manifest.highlightedOutputs?.includes(output) === true,
|
|
162
156
|
...(manifest.title === undefined ? {} : { buildTitle: manifest.title }),
|
|
163
157
|
...(manifest.note === undefined ? {} : { buildNote: manifest.note }),
|
|
164
|
-
|
|
165
|
-
ownerBuild: resolved.build,
|
|
158
|
+
...(file.kind === "external-file" ? {} : { ownerBuild: file.build ?? resolved.build }),
|
|
166
159
|
ownerOutput: resolved.output,
|
|
167
|
-
filePath: file.path,
|
|
160
|
+
filePath: file.kind === "external-file" ? file.uri : file.path,
|
|
168
161
|
size: file.size,
|
|
169
162
|
mediaType: file.mediaType,
|
|
170
|
-
source
|
|
171
|
-
...(
|
|
172
|
-
|
|
163
|
+
source,
|
|
164
|
+
...(run === undefined ? {} : { run }),
|
|
165
|
+
origins: [{ build: manifest.id, output, source, ...(run === undefined ? {} : { run }) }],
|
|
166
|
+
}];
|
|
173
167
|
})));
|
|
174
|
-
return nested.flat()
|
|
175
|
-
Number(right.highlighted) - Number(left.highlighted)
|
|
176
|
-
|| right.createdAt - left.createdAt
|
|
177
|
-
|| left.output.localeCompare(right.output)
|
|
178
|
-
|| left.valuePath.localeCompare(right.valuePath));
|
|
168
|
+
return mergeStudioArtifacts(nested.flat());
|
|
179
169
|
}
|
|
180
170
|
|
|
181
|
-
/**
|
|
182
|
-
export async function readStudioLibrary(input: {
|
|
171
|
+
/** Query only the selected library view, through Runtime and Repository interfaces. */
|
|
172
|
+
export async function readStudioLibrary(input: StudioLibraryRequest & {
|
|
183
173
|
readonly profile?: string;
|
|
184
174
|
readonly workspaceRoot: string;
|
|
185
175
|
readonly runtime?: Pick<RuntimeControl, "activity">;
|
|
186
176
|
readonly results: BuildResultRepository;
|
|
187
|
-
readonly before?: string;
|
|
188
177
|
}): Promise<StudioLibraryView> {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|| isWithin(input.workspaceRoot, manifest.source.path));
|
|
196
|
-
const resultsByBuild = new Map(manifests.map((manifest) => [manifest.id, manifest]));
|
|
178
|
+
const matches = (item: { readonly id: string; readonly source: { readonly path: string }; readonly run?: { readonly path: string } }) =>
|
|
179
|
+
(isWithin(input.workspaceRoot, item.run?.path ?? item.source.path) || isWithin(input.workspaceRoot, item.source.path))
|
|
180
|
+
&& (input.build === undefined || item.id === input.build)
|
|
181
|
+
&& (input.run === undefined || (item.run !== undefined
|
|
182
|
+
&& projectPath(input.workspaceRoot, item.run.path) === projectPath(input.workspaceRoot, input.run)));
|
|
183
|
+
// Read activity before Results so a Build that finishes during this query is still represented.
|
|
197
184
|
const active = input.runtime === undefined || input.before !== undefined
|
|
198
|
-
? []
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
? [] : (await input.runtime.activity()).builds;
|
|
186
|
+
const views = active.filter((view) => view.source !== undefined && matches({ ...view, source: view.source }));
|
|
187
|
+
let page = input.build === undefined
|
|
188
|
+
? await input.results.browse({ limit: 25, ...(input.before === undefined ? {} : { before: input.before }) })
|
|
189
|
+
: { results: [await input.results.read(input.build)].filter((item): item is BuildResultManifest => item !== undefined) };
|
|
190
|
+
const manifests = page.results.filter(matches);
|
|
191
|
+
const resultsByBuild = new Map(manifests.map((manifest) => [manifest.id, manifest]));
|
|
192
|
+
await Promise.all(views.filter((view) => !resultsByBuild.has(view.id)).map(async (view) => {
|
|
193
|
+
const result = await input.results.read(view.id);
|
|
194
|
+
if (result !== undefined && matches(result)) resultsByBuild.set(view.id, result);
|
|
195
|
+
}));
|
|
196
|
+
let artifacts: readonly StudioArtifactView[] = [];
|
|
197
|
+
if (input.section === "artifacts") {
|
|
198
|
+
artifacts = await artifactsForResults(input.workspaceRoot, input.results, [...resultsByBuild.values()], input.media);
|
|
199
|
+
// Page through Result metadata until there is a useful media page. The repository still
|
|
200
|
+
// owns Build order and cursors; no media directory or secondary persistent index is needed.
|
|
201
|
+
while (artifacts.length < 25 && "next" in page && page.next !== undefined) {
|
|
202
|
+
const cursor = page.next;
|
|
203
|
+
page = await input.results.browse({ limit: 25, before: cursor });
|
|
204
|
+
if ("next" in page && page.next === cursor) throw new Error("Result pagination did not advance");
|
|
205
|
+
artifacts = mergeStudioArtifacts([...artifacts,
|
|
206
|
+
...await artifactsForResults(input.workspaceRoot, input.results, page.results.filter(matches), input.media)]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
201
209
|
return {
|
|
210
|
+
section: input.section,
|
|
202
211
|
environment: resolve(input.workspaceRoot),
|
|
203
212
|
...(input.profile === undefined ? {} : { runtime: resolve(input.profile) }),
|
|
204
|
-
...(page.next
|
|
205
|
-
tasks: [
|
|
206
|
-
...views.map((view) => taskView(
|
|
207
|
-
input.workspaceRoot,
|
|
208
|
-
view,
|
|
209
|
-
resultsByBuild.get(view.id),
|
|
210
|
-
)),
|
|
213
|
+
...("next" in page && page.next !== undefined ? { next: page.next } : {}),
|
|
214
|
+
tasks: input.section !== "tasks" ? [] : [
|
|
215
|
+
...views.map((view) => taskView(input.workspaceRoot, view, resultsByBuild.get(view.id))),
|
|
211
216
|
...manifests
|
|
212
|
-
.filter((manifest)
|
|
217
|
+
.filter((manifest): manifest is FinishedBuildResultManifest => manifest.outcome !== undefined
|
|
218
|
+
&& !views.some((view) => view.id === manifest.id))
|
|
213
219
|
.map((manifest) => resultTaskView(input.workspaceRoot, manifest)),
|
|
214
220
|
].sort((left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id)),
|
|
215
|
-
artifacts
|
|
221
|
+
artifacts,
|
|
216
222
|
};
|
|
217
223
|
}
|
|
218
224
|
|
|
@@ -245,31 +251,40 @@ export async function openStudioBuildLibrary(
|
|
|
245
251
|
throw error;
|
|
246
252
|
}
|
|
247
253
|
const results = openedResults.repository;
|
|
254
|
+
let presentationWrite: Promise<unknown> = Promise.resolve();
|
|
248
255
|
return {
|
|
249
256
|
...(resolvedProfile === undefined ? {} : { profile: resolvedProfile }),
|
|
250
257
|
...(runtime === undefined ? {} : { runtime }),
|
|
251
258
|
...(transientExecution === undefined ? {} : { transientExecution }),
|
|
252
|
-
async library(
|
|
259
|
+
async library(request) {
|
|
253
260
|
return await readStudioLibrary({
|
|
254
261
|
...(resolvedProfile === undefined ? {} : { profile: resolvedProfile }),
|
|
255
262
|
workspaceRoot,
|
|
256
263
|
...(runtime === undefined ? {} : { runtime }),
|
|
257
264
|
results,
|
|
258
|
-
...
|
|
265
|
+
...request,
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
async renameArtifact(build, output, displayName) {
|
|
269
|
+
const saved = presentationWrite.then(async () => {
|
|
270
|
+
const updated = await results.updatePresentation(build, { outputDisplayNames: { [output]: displayName } });
|
|
271
|
+
return updated.outputs[output]?.displayName;
|
|
259
272
|
});
|
|
273
|
+
presentationWrite = saved.catch(() => undefined);
|
|
274
|
+
return await saved;
|
|
260
275
|
},
|
|
261
276
|
async resolveHistoricalOutput(build, output) {
|
|
262
277
|
return await resolveBuildResultValue(results, build, output);
|
|
263
278
|
},
|
|
264
|
-
async openArtifact(build, output
|
|
279
|
+
async openArtifact(build, output) {
|
|
265
280
|
const resolved = await results.resolve(build, output);
|
|
266
281
|
if (resolved === undefined) return undefined;
|
|
267
|
-
|
|
268
|
-
|
|
282
|
+
if ((resolved.value.kind !== "build-file" && resolved.value.kind !== "external-file") || !isMedia(resolved.value.mediaType)) return undefined;
|
|
283
|
+
const file = resolved.value;
|
|
269
284
|
return {
|
|
270
|
-
mediaType:
|
|
271
|
-
size:
|
|
272
|
-
open: async (range) => await results.openFile(resolved.build,
|
|
285
|
+
mediaType: file.mediaType,
|
|
286
|
+
size: file.size,
|
|
287
|
+
open: async (range) => await results.openFile(resolved.build, file, range),
|
|
273
288
|
};
|
|
274
289
|
},
|
|
275
290
|
async close() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StudioArtifactView } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
/** Merge known references, without reading or comparing media bytes. */
|
|
4
|
+
export function mergeStudioArtifacts(items: readonly StudioArtifactView[]): StudioArtifactView[] {
|
|
5
|
+
const files = new Map<string, StudioArtifactView>();
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
const prior = files.get(item.id);
|
|
8
|
+
if (prior === undefined) {
|
|
9
|
+
files.set(item.id, item);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const latest = item.createdAt > prior.createdAt ? item : prior;
|
|
13
|
+
const origins = new Map([...prior.origins, ...item.origins].map((origin) =>
|
|
14
|
+
[JSON.stringify([origin.build, origin.output]), origin]));
|
|
15
|
+
files.set(item.id, { ...latest, highlighted: prior.highlighted || item.highlighted, origins: [...origins.values()] });
|
|
16
|
+
}
|
|
17
|
+
return [...files.values()].sort((left, right) => Number(right.highlighted) - Number(left.highlighted)
|
|
18
|
+
|| right.createdAt - left.createdAt || left.output.localeCompare(right.output));
|
|
19
|
+
}
|
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import type { CanonicalValue, ValueSchema } from "@hypit/protocol";
|
|
2
2
|
import { formatSvsValue } from "@hypit/svs";
|
|
3
|
+
import type { StudioInspectorField, StudioParameterOption, StudioNumberPresentation } from "@hypit/studio-adapter";
|
|
4
|
+
|
|
5
|
+
export const parameterOption = (option: StudioParameterOption): Exclude<StudioParameterOption, string> =>
|
|
6
|
+
typeof option === "string" ? { value: option, label: option } : option;
|
|
7
|
+
|
|
8
|
+
// Keep binary floating-point residue out of decimal controls (0.55 × 100 is 55).
|
|
9
|
+
const scaledDecimal = (value: number, scale: number): number =>
|
|
10
|
+
scale === 1 ? value : Number((value * scale).toPrecision(15));
|
|
11
|
+
|
|
12
|
+
export function parameterNumber(value: CanonicalValue, presentation: StudioNumberPresentation = {}): { value: number; suffix: string } {
|
|
13
|
+
let held = value;
|
|
14
|
+
let suffix = "";
|
|
15
|
+
if (presentation.suffixes !== undefined) {
|
|
16
|
+
if (typeof held !== "string") throw new Error("A suffixed number must be authored as text.");
|
|
17
|
+
const text = held.trim();
|
|
18
|
+
const found = [...presentation.suffixes].sort((a, b) => b.length - a.length).find(item => text.endsWith(item));
|
|
19
|
+
if (found === undefined) throw new Error("The authored unit is not supported by this field.");
|
|
20
|
+
suffix = found;
|
|
21
|
+
held = text.slice(0, text.length - suffix.length).trim();
|
|
22
|
+
}
|
|
23
|
+
if (typeof held !== "number" && (typeof held !== "string" || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/iu.test(held.trim()))) {
|
|
24
|
+
throw new Error("This field needs a numeric value.");
|
|
25
|
+
}
|
|
26
|
+
const number = scaledDecimal(Number(held), presentation.scale ?? 1);
|
|
27
|
+
if (!Number.isFinite(number)) throw new Error("This field needs a finite number.");
|
|
28
|
+
return { value: number, suffix };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Decode the displayed edit before validating and serializing the authored value. */
|
|
32
|
+
export function parameterAuthorValue(field: StudioInspectorField, displayed: CanonicalValue): CanonicalValue {
|
|
33
|
+
if (field.control !== "number") return displayed;
|
|
34
|
+
if (typeof displayed !== "number" || !Number.isFinite(displayed)) throw new Error(`${field.label} expects a finite number.`);
|
|
35
|
+
const presentation = field.number ?? {};
|
|
36
|
+
if (presentation.minimum !== undefined && displayed < presentation.minimum) throw new Error(`${field.label} must be at least ${presentation.minimum}.`);
|
|
37
|
+
if (presentation.maximum !== undefined && displayed > presentation.maximum) throw new Error(`${field.label} must be at most ${presentation.maximum}.`);
|
|
38
|
+
const value = scaledDecimal(displayed, 1 / (presentation.scale ?? 1));
|
|
39
|
+
if (!Number.isFinite(value)) throw new Error(`${field.label} cannot be represented.`);
|
|
40
|
+
return presentation.suffixes === undefined ? value : `${value}${parameterNumber(field.value, presentation).suffix}`;
|
|
41
|
+
}
|
|
3
42
|
|
|
4
43
|
function fail(path: string, expectation: string): never {
|
|
5
44
|
throw new Error(`${path} ${expectation}.`);
|
|
@@ -76,6 +115,7 @@ export function validateParameterValue(value: CanonicalValue, schema: ValueSchem
|
|
|
76
115
|
|
|
77
116
|
export function serializeParameterValue(value: CanonicalValue, language: "svml" | "svs" | "svrun"): string {
|
|
78
117
|
if (language === "svs") return formatSvsValue(value);
|
|
79
|
-
if (typeof value === "string"
|
|
118
|
+
if (typeof value === "string") return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
119
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
80
120
|
throw new Error(`${language.toUpperCase()} parameter bindings currently accept scalar values only.`);
|
|
81
121
|
}
|
|
@@ -238,8 +238,16 @@ function referencedParameters(input: {
|
|
|
238
238
|
const file = sourceFor(input.root, target.sourcePath, input.files);
|
|
239
239
|
if (file === undefined) return [];
|
|
240
240
|
return input.declarations.flatMap((declaration) => {
|
|
241
|
+
const nestedPath = target.references[declaration.name];
|
|
242
|
+
const nested = nestedPath === undefined || declaration.referenced === undefined ? [] : referencedParameters({
|
|
243
|
+
root: input.root, files: input.files, draft: input.draft, placements: input.placements,
|
|
244
|
+
referenceName: `${input.referenceName}.${declaration.name}`,
|
|
245
|
+
referencePath: nestedPath,
|
|
246
|
+
...(target.resolvedReferences[declaration.name] === undefined ? {} : { referenceRef: target.resolvedReferences[declaration.name] }),
|
|
247
|
+
declarations: declaration.referenced,
|
|
248
|
+
});
|
|
241
249
|
const range = target.attributeValueRanges[declaration.name];
|
|
242
|
-
if (range === undefined) return
|
|
250
|
+
if (range === undefined) return nested;
|
|
243
251
|
const preimage = file.text.slice(range.start, range.end);
|
|
244
252
|
const reference = target.references[declaration.name];
|
|
245
253
|
const value = reference === undefined ? (target.attributes[declaration.name] ?? preimage) : reference;
|
|
@@ -263,7 +271,7 @@ function referencedParameters(input: {
|
|
|
263
271
|
...(!writable
|
|
264
272
|
? { disabledReason: reference === undefined ? "该几何值由组件声明为只读。" : "引用由作者在 SVML 中绑定,面板不替换引用关系。" }
|
|
265
273
|
: {}),
|
|
266
|
-
} satisfies StudioSourceBinding];
|
|
274
|
+
} satisfies StudioSourceBinding, ...nested];
|
|
267
275
|
});
|
|
268
276
|
}
|
|
269
277
|
|
|
@@ -484,7 +492,7 @@ export function resolveTimelineEditHandles(
|
|
|
484
492
|
moveEffect?: "translate-window" | "move-start",
|
|
485
493
|
): StudioEditHandle => {
|
|
486
494
|
const unavailable = affected.find(({ endpoint }) => endpoint.authority.kind === "fixed");
|
|
487
|
-
if (unavailable !== undefined) return disabled(gesture, "
|
|
495
|
+
if (unavailable !== undefined) return disabled(gesture, "该时间表达未开放时间轴回写。");
|
|
488
496
|
const projected: ({ readonly missing: string } | StudioEditSource)[] = [];
|
|
489
497
|
for (const { endpoint, role } of affected) {
|
|
490
498
|
if (endpoint.authority.kind !== "parameter") continue;
|
|
@@ -527,24 +535,15 @@ export function resolveTimelineEditHandles(
|
|
|
527
535
|
}
|
|
528
536
|
const start = { endpoint: projection.start, role: "start" as const };
|
|
529
537
|
const end = { endpoint: projection.end, role: "end" as const };
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
? [start]
|
|
537
|
-
: projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start"
|
|
538
|
-
? [start, end]
|
|
539
|
-
: [start];
|
|
540
|
-
const trimEnd = projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start"
|
|
541
|
-
? [end]
|
|
542
|
-
: projection.start.authority.kind === "parameter" && projection.start.authority.relation === "before-end"
|
|
543
|
-
? [start, end]
|
|
544
|
-
: [end];
|
|
538
|
+
if (projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start") {
|
|
539
|
+
return [handle("move", [start], "translate-window"), handle("trim-end", [end])];
|
|
540
|
+
}
|
|
541
|
+
if (projection.start.authority.kind === "parameter" && projection.start.authority.relation === "before-end") {
|
|
542
|
+
return [handle("move", [end], "translate-window"), handle("trim-start", [start])];
|
|
543
|
+
}
|
|
545
544
|
return [
|
|
546
|
-
handle("move",
|
|
547
|
-
handle("trim-start",
|
|
548
|
-
handle("trim-end",
|
|
545
|
+
handle("move", [start, end], "translate-window"),
|
|
546
|
+
handle("trim-start", [start]),
|
|
547
|
+
handle("trim-end", [end]),
|
|
549
548
|
];
|
|
550
549
|
}
|
|
@@ -15,12 +15,13 @@ import type { ServedFile } from "./compile.js";
|
|
|
15
15
|
import type { StudioDomain } from "./domain.js";
|
|
16
16
|
import type { StudioCompanionRegistry } from "./studio-registry.js";
|
|
17
17
|
import { loadStudioRun } from "./run.js";
|
|
18
|
-
import { serializeParameterValue, validateParameterValue } from "./parameter-values.js";
|
|
18
|
+
import { parameterAuthorValue, parameterOption, serializeParameterValue, validateParameterValue } from "./parameter-values.js";
|
|
19
19
|
import { readStudioSession } from "./session.js";
|
|
20
|
-
import type { Range, StudioFailure, StudioLibraryView, StudioMutation, StudioSnapshot } from "./shared.js";
|
|
20
|
+
import type { Range, StudioFailure, StudioLibraryRequest, StudioLibraryView, StudioMutation, StudioSnapshot } from "./shared.js";
|
|
21
21
|
import { createStudioStoryboard } from "./storyboard.js";
|
|
22
22
|
import type { StudioStoryboard } from "./storyboard.js";
|
|
23
23
|
import { findSurfacePreview } from "./surface-preview.js";
|
|
24
|
+
import { formatTemporalPointEdit, semanticGestureSpan } from "./temporal-edit.js";
|
|
24
25
|
import { replaceSourceFiles } from "./source-transaction.js";
|
|
25
26
|
|
|
26
27
|
export type StudioPluginOptions = {
|
|
@@ -93,8 +94,9 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
93
94
|
const watchedFiles = new Set<string>();
|
|
94
95
|
const storyboards = new Map<string, Promise<StudioStoryboard>>();
|
|
95
96
|
|
|
96
|
-
const readLibrary = async (
|
|
97
|
-
return await options.buildLibrary?.library(
|
|
97
|
+
const readLibrary = async (request: StudioLibraryRequest): Promise<StudioLibraryView> => {
|
|
98
|
+
return await options.buildLibrary?.library(request) ?? {
|
|
99
|
+
section: request.section,
|
|
98
100
|
environment: options.workspaceRoot,
|
|
99
101
|
tasks: [],
|
|
100
102
|
artifacts: [],
|
|
@@ -271,7 +273,7 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
271
273
|
throw new Error("A timeline target must use valid whole frames.");
|
|
272
274
|
}
|
|
273
275
|
if (temporal.kind === "window") {
|
|
274
|
-
if (mutation.gesture === "move"
|
|
276
|
+
if (mutation.gesture === "move" && handle.semantic?.kind !== "selection"
|
|
275
277
|
&& endFrameExclusive - startFrame !== clip.endFrameExclusive - clip.startFrame) {
|
|
276
278
|
throw new Error("Move must preserve the Window duration.");
|
|
277
279
|
}
|
|
@@ -287,6 +289,7 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
287
289
|
|
|
288
290
|
const patches: Patch[] = [];
|
|
289
291
|
const semanticTarget = mutation.target.semantic;
|
|
292
|
+
if (handle.semantic !== undefined && semanticTarget === undefined) throw new Error("A semantic edit requires explicit target anchors.");
|
|
290
293
|
if (semanticTarget !== undefined) {
|
|
291
294
|
if (handle.semantic?.kind !== semanticTarget.kind) {
|
|
292
295
|
throw new Error(`Entity ${mutation.entityId} is not bound to a writable ${semanticTarget.kind}.`);
|
|
@@ -298,15 +301,9 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
298
301
|
|| script.narrativeId !== current.semantic.narrativeId) {
|
|
299
302
|
throw new Error("The timeline entity and writable Script do not belong to the selected Narrative.");
|
|
300
303
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex
|
|
305
|
-
|| current.semantic.anchors[startIndex]!.frame >= current.semantic.anchors[endIndex]!.frame) {
|
|
306
|
-
throw new Error("A Selection must span two ordered semantic Anchors with positive time.");
|
|
307
|
-
}
|
|
308
|
-
} else if (!current.semantic.anchors.some((anchor) => anchor.id === semanticTarget.anchorId)) {
|
|
309
|
-
throw new Error(`Moment Anchor ${semanticTarget.anchorId} does not exist in the current semantic Candidate.`);
|
|
304
|
+
const projected = semanticGestureSpan(current.semantic.anchors, handle, semanticTarget);
|
|
305
|
+
if (projected === undefined || projected.startFrame !== startFrame || projected.endFrameExclusive !== endFrameExclusive) {
|
|
306
|
+
throw new Error("The semantic edit does not produce the requested timeline projection.");
|
|
310
307
|
}
|
|
311
308
|
const absolute = resolve(options.workspaceRoot, script.sourcePath);
|
|
312
309
|
const source = await readFile(absolute, "utf8");
|
|
@@ -366,20 +363,15 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
366
363
|
}
|
|
367
364
|
return snapshot?.semantic?.moments.find((candidate) => candidate.id === id)?.frame;
|
|
368
365
|
};
|
|
369
|
-
const projectedPointValue = (endpoint: StudioTemporalInstantProjection, desired: number): string =>
|
|
370
|
-
|
|
371
|
-
const base = projectionBaseFrame(endpoint);
|
|
372
|
-
if (base === undefined) throw new Error(`The ${endpoint.reference} projection base is unavailable.`);
|
|
373
|
-
const offset = desired - base;
|
|
374
|
-
return offset === 0 ? endpoint.reference : `${endpoint.reference}${offset > 0 ? "+" : ""}${offset}f`;
|
|
375
|
-
};
|
|
366
|
+
const projectedPointValue = (endpoint: StudioTemporalInstantProjection, desired: number): string =>
|
|
367
|
+
formatTemporalPointEdit(endpoint.reference, desired, projectionBaseFrame(endpoint));
|
|
376
368
|
const writeEndpoint = (
|
|
377
369
|
endpoint: StudioTemporalInstantProjection,
|
|
378
370
|
desired: number,
|
|
379
371
|
role: "start" | "end",
|
|
380
372
|
): void => {
|
|
381
373
|
if (desired === endpoint.frame) return;
|
|
382
|
-
if (endpoint.authority.kind === "fixed") throw new Error(`The ${role} endpoint
|
|
374
|
+
if (endpoint.authority.kind === "fixed") throw new Error(`The ${role} endpoint has no timeline write target.`);
|
|
383
375
|
if (endpoint.authority.kind === "semantic") {
|
|
384
376
|
if (semanticFrame(endpoint) !== desired) throw new Error(`The ${role} endpoint does not match its semantic Anchor.`);
|
|
385
377
|
return;
|
|
@@ -413,20 +405,22 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
413
405
|
if (parameter === undefined) {
|
|
414
406
|
throw new Error(`Entity ${mutation.entityId} has no writable parameter ${mutation.parameterId}.`);
|
|
415
407
|
}
|
|
408
|
+
const authorValue = parameterAuthorValue(parameter, mutation.value);
|
|
416
409
|
if (parameter.schema !== undefined) {
|
|
417
|
-
validateParameterValue(
|
|
410
|
+
validateParameterValue(authorValue, parameter.schema, parameter.label);
|
|
418
411
|
} else if (parameter.control === "boolean" && typeof mutation.value !== "boolean") {
|
|
419
412
|
throw new Error(`${parameter.label} expects true or false.`);
|
|
420
413
|
} else if (parameter.control === "number" && (typeof mutation.value !== "number" || !Number.isFinite(mutation.value))) {
|
|
421
414
|
throw new Error(`${parameter.label} expects a number.`);
|
|
422
|
-
} else if ((parameter.control === "text" || parameter.control === "color"
|
|
415
|
+
} else if ((parameter.control === "text" || parameter.control === "color")
|
|
423
416
|
&& typeof mutation.value !== "string") {
|
|
424
417
|
throw new Error(`${parameter.label} expects text.`);
|
|
425
418
|
}
|
|
426
|
-
if (parameter.options !== undefined &&
|
|
419
|
+
if (parameter.options !== undefined && !parameter.options.some(option => parameterOption(option).value === authorValue)) {
|
|
427
420
|
throw new Error(`${parameter.label} does not accept ${mutation.value}.`);
|
|
428
421
|
}
|
|
429
|
-
|
|
422
|
+
if (parameter.control === "color") validateParameterValue(authorValue, { kind: "string", format: "color" }, parameter.label);
|
|
423
|
+
const encoded = serializeParameterValue(authorValue, parameter.language);
|
|
430
424
|
const replacement = `${parameter.source.prefix ?? ""}${encoded}${parameter.source.suffix ?? ""}`;
|
|
431
425
|
return replacement === parameter.source.preimage ? [] : [{
|
|
432
426
|
...parameter.source,
|
|
@@ -525,6 +519,28 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
525
519
|
})();
|
|
526
520
|
return;
|
|
527
521
|
}
|
|
522
|
+
if (request.method === "PUT" && url.pathname === "/__studio/artifact-name") {
|
|
523
|
+
void (async () => {
|
|
524
|
+
try {
|
|
525
|
+
const chunks: Buffer[] = [];
|
|
526
|
+
for await (const chunk of request) chunks.push(Buffer.from(chunk));
|
|
527
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as {
|
|
528
|
+
build?: unknown; output?: unknown; displayName?: unknown;
|
|
529
|
+
};
|
|
530
|
+
if (typeof body.build !== "string" || typeof body.output !== "string"
|
|
531
|
+
|| (typeof body.displayName !== "string" && body.displayName !== null)) {
|
|
532
|
+
json(response, 400, { error: "Expected Build, Output and displayName" });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (options.buildLibrary === undefined) throw new Error("Result Repository is unavailable");
|
|
536
|
+
const displayName = await options.buildLibrary.renameArtifact(body.build, body.output, body.displayName);
|
|
537
|
+
json(response, 200, { displayName: displayName ?? null });
|
|
538
|
+
} catch (error) {
|
|
539
|
+
json(response, 422, { error: error instanceof Error ? error.message : String(error) });
|
|
540
|
+
}
|
|
541
|
+
})();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
528
544
|
if (request.method === "POST" && url.pathname === "/__studio/mutation") {
|
|
529
545
|
void (async () => {
|
|
530
546
|
try {
|
|
@@ -577,8 +593,21 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
577
593
|
return;
|
|
578
594
|
}
|
|
579
595
|
if (url.pathname === "/__studio/library") {
|
|
596
|
+
const section = url.searchParams.get("section");
|
|
597
|
+
if (section !== "tasks" && section !== "artifacts") {
|
|
598
|
+
json(response, 400, { error: "Choose tasks or artifacts with the section parameter" });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const media = url.searchParams.get("media") ?? undefined;
|
|
602
|
+
if (media !== undefined && media !== "image" && media !== "video" && media !== "audio") {
|
|
603
|
+
json(response, 400, { error: "Choose image, video or audio with the media parameter" });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
580
606
|
const before = url.searchParams.get("before") ?? undefined;
|
|
581
|
-
|
|
607
|
+
const run = url.searchParams.get("run") ?? undefined;
|
|
608
|
+
const build = url.searchParams.get("build") ?? undefined;
|
|
609
|
+
void readLibrary({ section, ...(media === undefined ? {} : { media }), ...(before === undefined ? {} : { before }),
|
|
610
|
+
...(run === undefined ? {} : { run }), ...(build === undefined ? {} : { build }) }).then(
|
|
582
611
|
(view) => json(response, 200, view),
|
|
583
612
|
(error) => json(response, 500, { error: error instanceof Error ? error.message : String(error) }),
|
|
584
613
|
);
|
|
@@ -662,13 +691,12 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
|
|
|
662
691
|
void (async () => {
|
|
663
692
|
const build = url.searchParams.get("build");
|
|
664
693
|
const output = url.searchParams.get("output");
|
|
665
|
-
|
|
666
|
-
if (build === null || output === null || valuePath === null || options.buildLibrary === undefined) {
|
|
694
|
+
if (build === null || output === null || options.buildLibrary === undefined) {
|
|
667
695
|
response.statusCode = 404;
|
|
668
696
|
response.end();
|
|
669
697
|
return;
|
|
670
698
|
}
|
|
671
|
-
const artifact = await options.buildLibrary.openArtifact(build, output
|
|
699
|
+
const artifact = await options.buildLibrary.openArtifact(build, output);
|
|
672
700
|
if (artifact === undefined) {
|
|
673
701
|
response.statusCode = 404;
|
|
674
702
|
response.end();
|