@osmo.inc/cli 0.1.16 → 0.1.17
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/dist/osmo.js +861 -736
- package/package.json +1 -1
- package/release.json +9 -9
- package/vendor/osmo-lite +0 -0
- package/vendor/platform.json +1 -1
package/dist/osmo.js
CHANGED
|
@@ -10,7 +10,7 @@ var __export = (target, all) => {
|
|
|
10
10
|
|
|
11
11
|
// src/runtime.ts
|
|
12
12
|
import fs from "node:fs";
|
|
13
|
-
import
|
|
13
|
+
import path2 from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
function assertCompatibleProject(config) {
|
|
@@ -21,10 +21,10 @@ function assertCompatibleProject(config) {
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
function nativeBinary() {
|
|
24
|
-
const bundled =
|
|
24
|
+
const bundled = path2.join(packageRoot, "vendor/osmo-lite");
|
|
25
25
|
if (fs.existsSync(bundled)) {
|
|
26
26
|
const platform = JSON.parse(
|
|
27
|
-
fs.readFileSync(
|
|
27
|
+
fs.readFileSync(path2.join(packageRoot, "vendor/platform.json"), "utf8")
|
|
28
28
|
);
|
|
29
29
|
if (platform.os !== process.platform || platform.arch !== process.arch)
|
|
30
30
|
throw new Error(
|
|
@@ -33,7 +33,7 @@ function nativeBinary() {
|
|
|
33
33
|
return bundled;
|
|
34
34
|
}
|
|
35
35
|
for (const profile of ["debug", "release"]) {
|
|
36
|
-
const candidate =
|
|
36
|
+
const candidate = path2.join(
|
|
37
37
|
runtimeRoot,
|
|
38
38
|
`apps/native/target/${profile}/osmo-lite`
|
|
39
39
|
);
|
|
@@ -57,22 +57,22 @@ function run(command, args, options = {}) {
|
|
|
57
57
|
child.once(
|
|
58
58
|
"exit",
|
|
59
59
|
(code, signal) => code === 0 ? resolve() : reject(
|
|
60
|
-
new Error(`${
|
|
60
|
+
new Error(`${path2.basename(command)} failed (${signal ?? code}).`)
|
|
61
61
|
)
|
|
62
62
|
);
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
function projectRoot(start = process.cwd()) {
|
|
66
|
-
let dir =
|
|
66
|
+
let dir = path2.resolve(start);
|
|
67
67
|
while (true) {
|
|
68
|
-
if (fs.existsSync(
|
|
68
|
+
if (fs.existsSync(path2.join(dir, ".osmo/project.json"))) {
|
|
69
69
|
const config = JSON.parse(
|
|
70
|
-
fs.readFileSync(
|
|
70
|
+
fs.readFileSync(path2.join(dir, ".osmo/project.json"), "utf8")
|
|
71
71
|
);
|
|
72
72
|
assertCompatibleProject(config);
|
|
73
73
|
return dir;
|
|
74
74
|
}
|
|
75
|
-
const parent =
|
|
75
|
+
const parent = path2.dirname(dir);
|
|
76
76
|
if (parent === dir)
|
|
77
77
|
throw new Error(
|
|
78
78
|
"No Osmo project found. Run osmo studio init in your project folder first."
|
|
@@ -81,11 +81,11 @@ function projectRoot(start = process.cwd()) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
function sceneInProject(root, input = "scene.tsx") {
|
|
84
|
-
const source = fs.realpathSync(
|
|
85
|
-
const relative =
|
|
86
|
-
if (relative.startsWith(".." +
|
|
84
|
+
const source = fs.realpathSync(path2.resolve(root, input));
|
|
85
|
+
const relative = path2.relative(fs.realpathSync(root), source);
|
|
86
|
+
if (relative.startsWith(".." + path2.sep) || path2.isAbsolute(relative) || relative === "..")
|
|
87
87
|
throw new Error("Scene must be inside the current Osmo project.");
|
|
88
|
-
return { source, scene: relative.split(
|
|
88
|
+
return { source, scene: relative.split(path2.sep).join("/") };
|
|
89
89
|
}
|
|
90
90
|
var packageRoot, manifest, version, sceneVersion, runtimeRoot, studioRoot;
|
|
91
91
|
var init_runtime = __esm({
|
|
@@ -93,12 +93,78 @@ var init_runtime = __esm({
|
|
|
93
93
|
"use strict";
|
|
94
94
|
packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
95
95
|
manifest = JSON.parse(
|
|
96
|
-
fs.readFileSync(
|
|
96
|
+
fs.readFileSync(path2.join(packageRoot, "package.json"), "utf8")
|
|
97
97
|
);
|
|
98
98
|
version = manifest.version;
|
|
99
99
|
sceneVersion = manifest.osmo.sceneVersion;
|
|
100
|
-
runtimeRoot = fs.existsSync(
|
|
101
|
-
studioRoot = fs.existsSync(
|
|
100
|
+
runtimeRoot = fs.existsSync(path2.join(packageRoot, "runtime")) ? path2.join(packageRoot, "runtime") : path2.resolve(packageRoot, "../..");
|
|
101
|
+
studioRoot = fs.existsSync(path2.join(packageRoot, "studio")) ? path2.join(packageRoot, "studio") : path2.join(runtimeRoot, "packages/studio/dist");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// src/check.ts
|
|
106
|
+
import ts from "typescript";
|
|
107
|
+
import path3 from "node:path";
|
|
108
|
+
import { pathToFileURL } from "node:url";
|
|
109
|
+
async function check(source, {
|
|
110
|
+
text: text2,
|
|
111
|
+
sources = {},
|
|
112
|
+
allowLegacyTracks = false
|
|
113
|
+
} = {}) {
|
|
114
|
+
const { validate } = await import(pathToFileURL(
|
|
115
|
+
path3.join(runtimeRoot, "packages/scene/scripts/validate-strict.mjs")
|
|
116
|
+
).href);
|
|
117
|
+
const strict = validate(source, "strict", {
|
|
118
|
+
text: text2,
|
|
119
|
+
sources,
|
|
120
|
+
requireTracks: !allowLegacyTracks
|
|
121
|
+
});
|
|
122
|
+
const scene = path3.join(runtimeRoot, "packages/scene/src");
|
|
123
|
+
const options = {
|
|
124
|
+
noEmit: true,
|
|
125
|
+
strict: true,
|
|
126
|
+
skipLibCheck: true,
|
|
127
|
+
target: ts.ScriptTarget.ES2022,
|
|
128
|
+
module: ts.ModuleKind.ESNext,
|
|
129
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
130
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
131
|
+
jsxImportSource: "@osmo/scene",
|
|
132
|
+
baseUrl: runtimeRoot,
|
|
133
|
+
types: [],
|
|
134
|
+
allowImportingTsExtensions: true,
|
|
135
|
+
allowJs: true,
|
|
136
|
+
paths: {
|
|
137
|
+
"@osmo/scene": [path3.join(scene, "index.ts")],
|
|
138
|
+
"@osmo/scene/*": [path3.join(scene, "*")]
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const host = ts.createCompilerHost(options), original = host.getSourceFile.bind(host);
|
|
142
|
+
const overrides = {
|
|
143
|
+
...sources,
|
|
144
|
+
...text2 === void 0 ? {} : { [path3.resolve(source)]: text2 }
|
|
145
|
+
};
|
|
146
|
+
host.getSourceFile = (file, language, ...rest) => Object.hasOwn(overrides, path3.resolve(file)) ? ts.createSourceFile(
|
|
147
|
+
file,
|
|
148
|
+
overrides[path3.resolve(file)],
|
|
149
|
+
language,
|
|
150
|
+
true,
|
|
151
|
+
ts.ScriptKind.TSX
|
|
152
|
+
) : original(file, language, ...rest);
|
|
153
|
+
const program = ts.createProgram([source], options, host);
|
|
154
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
155
|
+
const errors = [
|
|
156
|
+
...strict.errors,
|
|
157
|
+
...diagnostics.map((d) => {
|
|
158
|
+
const pos = d.file && d.start !== void 0 ? d.file.getLineAndCharacterOfPosition(d.start) : void 0;
|
|
159
|
+
return `${d.file?.fileName ?? ""}${pos ? `:${pos.line + 1}:${pos.character + 1}` : ""} ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
|
|
160
|
+
})
|
|
161
|
+
];
|
|
162
|
+
return { ok: !errors.length, errors };
|
|
163
|
+
}
|
|
164
|
+
var init_check = __esm({
|
|
165
|
+
"src/check.ts"() {
|
|
166
|
+
"use strict";
|
|
167
|
+
init_runtime();
|
|
102
168
|
}
|
|
103
169
|
});
|
|
104
170
|
|
|
@@ -149,76 +215,10 @@ var init_labels = __esm({
|
|
|
149
215
|
}
|
|
150
216
|
});
|
|
151
217
|
|
|
152
|
-
// src/check.ts
|
|
153
|
-
import ts3 from "typescript";
|
|
154
|
-
import path10 from "node:path";
|
|
155
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
156
|
-
async function check(source, {
|
|
157
|
-
text: text2,
|
|
158
|
-
sources = {},
|
|
159
|
-
allowLegacyTracks = false
|
|
160
|
-
} = {}) {
|
|
161
|
-
const { validate } = await import(pathToFileURL2(
|
|
162
|
-
path10.join(runtimeRoot, "packages/scene/scripts/validate-strict.mjs")
|
|
163
|
-
).href);
|
|
164
|
-
const strict = validate(source, "strict", {
|
|
165
|
-
text: text2,
|
|
166
|
-
sources,
|
|
167
|
-
requireTracks: !allowLegacyTracks
|
|
168
|
-
});
|
|
169
|
-
const scene = path10.join(runtimeRoot, "packages/scene/src");
|
|
170
|
-
const options = {
|
|
171
|
-
noEmit: true,
|
|
172
|
-
strict: true,
|
|
173
|
-
skipLibCheck: true,
|
|
174
|
-
target: ts3.ScriptTarget.ES2022,
|
|
175
|
-
module: ts3.ModuleKind.ESNext,
|
|
176
|
-
moduleResolution: ts3.ModuleResolutionKind.Bundler,
|
|
177
|
-
jsx: ts3.JsxEmit.ReactJSX,
|
|
178
|
-
jsxImportSource: "@osmo/scene",
|
|
179
|
-
baseUrl: runtimeRoot,
|
|
180
|
-
types: [],
|
|
181
|
-
allowImportingTsExtensions: true,
|
|
182
|
-
allowJs: true,
|
|
183
|
-
paths: {
|
|
184
|
-
"@osmo/scene": [path10.join(scene, "index.ts")],
|
|
185
|
-
"@osmo/scene/*": [path10.join(scene, "*")]
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
const host = ts3.createCompilerHost(options), original = host.getSourceFile.bind(host);
|
|
189
|
-
const overrides = {
|
|
190
|
-
...sources,
|
|
191
|
-
...text2 === void 0 ? {} : { [path10.resolve(source)]: text2 }
|
|
192
|
-
};
|
|
193
|
-
host.getSourceFile = (file, language, ...rest) => Object.hasOwn(overrides, path10.resolve(file)) ? ts3.createSourceFile(
|
|
194
|
-
file,
|
|
195
|
-
overrides[path10.resolve(file)],
|
|
196
|
-
language,
|
|
197
|
-
true,
|
|
198
|
-
ts3.ScriptKind.TSX
|
|
199
|
-
) : original(file, language, ...rest);
|
|
200
|
-
const program = ts3.createProgram([source], options, host);
|
|
201
|
-
const diagnostics = ts3.getPreEmitDiagnostics(program);
|
|
202
|
-
const errors = [
|
|
203
|
-
...strict.errors,
|
|
204
|
-
...diagnostics.map((d) => {
|
|
205
|
-
const pos = d.file && d.start !== void 0 ? d.file.getLineAndCharacterOfPosition(d.start) : void 0;
|
|
206
|
-
return `${d.file?.fileName ?? ""}${pos ? `:${pos.line + 1}:${pos.character + 1}` : ""} ${ts3.flattenDiagnosticMessageText(d.messageText, "\n")}`;
|
|
207
|
-
})
|
|
208
|
-
];
|
|
209
|
-
return { ok: !errors.length, errors };
|
|
210
|
-
}
|
|
211
|
-
var init_check = __esm({
|
|
212
|
-
"src/check.ts"() {
|
|
213
|
-
"use strict";
|
|
214
|
-
init_runtime();
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
218
|
// src/comments.ts
|
|
219
|
-
import { mkdir as
|
|
219
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2, rename as rename2, rm as rm3 } from "node:fs/promises";
|
|
220
220
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
221
|
-
import
|
|
221
|
+
import path7 from "node:path";
|
|
222
222
|
function validBounds(value) {
|
|
223
223
|
return value && ["x", "y", "width", "height"].every(
|
|
224
224
|
(key) => Number.isFinite(value[key]) && value[key] >= 0 && value[key] <= 1
|
|
@@ -244,7 +244,7 @@ function validAgentResponse(value) {
|
|
|
244
244
|
}
|
|
245
245
|
async function readComments(root) {
|
|
246
246
|
try {
|
|
247
|
-
const data = JSON.parse(await
|
|
247
|
+
const data = JSON.parse(await readFile3(filename(root), "utf8"));
|
|
248
248
|
if (data.version !== 1 || !Array.isArray(data.comments))
|
|
249
249
|
throw new Error("Unsupported comments file");
|
|
250
250
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -272,13 +272,13 @@ function commentText(value) {
|
|
|
272
272
|
return value.trim();
|
|
273
273
|
}
|
|
274
274
|
async function mutateComments(root, change) {
|
|
275
|
-
const folder =
|
|
276
|
-
await
|
|
277
|
-
const lock =
|
|
275
|
+
const folder = path7.join(root, ".osmo");
|
|
276
|
+
await mkdir3(folder, { recursive: true });
|
|
277
|
+
const lock = path7.join(folder, "comments.lock");
|
|
278
278
|
const start = Date.now();
|
|
279
279
|
while (true) {
|
|
280
280
|
try {
|
|
281
|
-
await
|
|
281
|
+
await mkdir3(lock);
|
|
282
282
|
break;
|
|
283
283
|
} catch (e) {
|
|
284
284
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -289,18 +289,18 @@ async function mutateComments(root, change) {
|
|
|
289
289
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
-
const temporary =
|
|
292
|
+
const temporary = path7.join(folder, `comments.${randomUUID2()}.tmp`);
|
|
293
293
|
try {
|
|
294
294
|
const data = await readComments(root);
|
|
295
295
|
const result = change(data.comments);
|
|
296
|
-
await
|
|
296
|
+
await writeFile2(temporary, JSON.stringify(data, null, 2) + "\n", {
|
|
297
297
|
flag: "wx"
|
|
298
298
|
});
|
|
299
299
|
await rename2(temporary, filename(root));
|
|
300
300
|
return result;
|
|
301
301
|
} finally {
|
|
302
|
-
await
|
|
303
|
-
await
|
|
302
|
+
await rm3(temporary, { force: true });
|
|
303
|
+
await rm3(lock, { recursive: true, force: true });
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
async function addComment(root, input) {
|
|
@@ -399,7 +399,7 @@ var filename, AGENT_REACTION_MIN_MS, graphemes, isoTime;
|
|
|
399
399
|
var init_comments = __esm({
|
|
400
400
|
"src/comments.ts"() {
|
|
401
401
|
"use strict";
|
|
402
|
-
filename = (root) =>
|
|
402
|
+
filename = (root) => path7.join(root, ".osmo/comments.json");
|
|
403
403
|
AGENT_REACTION_MIN_MS = 3e4;
|
|
404
404
|
graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
405
405
|
isoTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
@@ -948,7 +948,7 @@ __export(scene_document_exports, {
|
|
|
948
948
|
outlineDocument: () => outlineDocument
|
|
949
949
|
});
|
|
950
950
|
import fs2 from "node:fs/promises";
|
|
951
|
-
import
|
|
951
|
+
import path11 from "node:path";
|
|
952
952
|
import { createHash as createHash4 } from "node:crypto";
|
|
953
953
|
import ts5 from "typescript";
|
|
954
954
|
function createDocumentLoader(root) {
|
|
@@ -997,8 +997,8 @@ function createDocumentLoader(root) {
|
|
|
997
997
|
}
|
|
998
998
|
if (typeof value !== "string" || kind === "font" && value === "Inter")
|
|
999
999
|
return;
|
|
1000
|
-
const relative =
|
|
1001
|
-
|
|
1000
|
+
const relative = path11.posix.normalize(
|
|
1001
|
+
path11.posix.join(path11.posix.dirname(file), value)
|
|
1002
1002
|
);
|
|
1003
1003
|
if (document.assets[relative]) return;
|
|
1004
1004
|
document.assets[relative] = await registerAsset(relative, kind);
|
|
@@ -1094,7 +1094,7 @@ function createDocumentLoader(root) {
|
|
|
1094
1094
|
);
|
|
1095
1095
|
return {
|
|
1096
1096
|
bytes: await fs2.readFile(current.source),
|
|
1097
|
-
extension:
|
|
1097
|
+
extension: path11.extname(current.source).toLowerCase()
|
|
1098
1098
|
};
|
|
1099
1099
|
}
|
|
1100
1100
|
return { load, readAsset, resources };
|
|
@@ -1110,21 +1110,21 @@ var init_scene_document2 = __esm({
|
|
|
1110
1110
|
|
|
1111
1111
|
// src/project-resources.ts
|
|
1112
1112
|
import fs3 from "node:fs/promises";
|
|
1113
|
-
import
|
|
1114
|
-
import { execFile
|
|
1115
|
-
import { promisify
|
|
1113
|
+
import path12 from "node:path";
|
|
1114
|
+
import { execFile } from "node:child_process";
|
|
1115
|
+
import { promisify } from "node:util";
|
|
1116
1116
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1117
1117
|
import ts6 from "typescript";
|
|
1118
1118
|
async function resourceFile(root, relative) {
|
|
1119
|
-
if (typeof relative !== "string" ||
|
|
1119
|
+
if (typeof relative !== "string" || path12.isAbsolute(relative))
|
|
1120
1120
|
throw new Error("Resource paths must be relative.");
|
|
1121
|
-
const base = await fs3.realpath(root), file = await fs3.realpath(
|
|
1122
|
-
if (!inside(
|
|
1121
|
+
const base = await fs3.realpath(root), file = await fs3.realpath(path12.resolve(base, relative));
|
|
1122
|
+
if (!inside(path12.join(base, "resources"), file) || (await fs3.stat(file)).isDirectory())
|
|
1123
1123
|
throw new Error("Resource files must stay inside resources/.");
|
|
1124
1124
|
return file;
|
|
1125
1125
|
}
|
|
1126
1126
|
async function resourceFolder(root) {
|
|
1127
|
-
const base = await fs3.realpath(root), folder =
|
|
1127
|
+
const base = await fs3.realpath(root), folder = path12.join(base, "resources");
|
|
1128
1128
|
await fs3.mkdir(folder, { recursive: true });
|
|
1129
1129
|
if ((await fs3.lstat(folder)).isSymbolicLink())
|
|
1130
1130
|
throw new Error("resources/ must be a real directory.");
|
|
@@ -1133,7 +1133,7 @@ async function resourceFolder(root) {
|
|
|
1133
1133
|
async function listProjectResources(root) {
|
|
1134
1134
|
let entries;
|
|
1135
1135
|
try {
|
|
1136
|
-
entries = await fs3.readdir(
|
|
1136
|
+
entries = await fs3.readdir(path12.join(root, "resources"), {
|
|
1137
1137
|
withFileTypes: true
|
|
1138
1138
|
});
|
|
1139
1139
|
} catch (e) {
|
|
@@ -1149,8 +1149,8 @@ async function listProjectResources(root) {
|
|
|
1149
1149
|
throw new Error("Manifest exceeds 1 MB");
|
|
1150
1150
|
const meta = JSON.parse(await fs3.readFile(manifestFile, "utf8"));
|
|
1151
1151
|
const entry = `resources/${item.name}/${meta.entry}`, file = await resourceFile(root, entry);
|
|
1152
|
-
const kind = meta.kind ?? meta.category ?? kinds[
|
|
1153
|
-
if (!["shader", "image", "video", "model", "scene"].includes(kind) || kinds[
|
|
1152
|
+
const kind = meta.kind ?? meta.category ?? kinds[path12.extname(file).toLowerCase()];
|
|
1153
|
+
if (!["shader", "image", "video", "model", "scene"].includes(kind) || kinds[path12.extname(file).toLowerCase()] !== kind)
|
|
1154
1154
|
throw new Error("Unsupported resource type");
|
|
1155
1155
|
let effect;
|
|
1156
1156
|
if (kind === "shader" && meta.effect) {
|
|
@@ -1201,14 +1201,14 @@ async function listProjectResources(root) {
|
|
|
1201
1201
|
"video-references"
|
|
1202
1202
|
].includes(item.name) || item.isSymbolicLink())
|
|
1203
1203
|
continue;
|
|
1204
|
-
const file =
|
|
1204
|
+
const file = path12.join(folder, item.name);
|
|
1205
1205
|
if (item.isDirectory()) {
|
|
1206
1206
|
await scenes(file, depth + 1);
|
|
1207
1207
|
continue;
|
|
1208
1208
|
}
|
|
1209
1209
|
if (!item.isFile() || !item.name.endsWith(".tsx") || (await fs3.stat(file)).size > 1024 * 1024)
|
|
1210
1210
|
continue;
|
|
1211
|
-
const entry =
|
|
1211
|
+
const entry = path12.relative(root, file).split(path12.sep).join("/");
|
|
1212
1212
|
try {
|
|
1213
1213
|
const text2 = await fs3.readFile(file, "utf8"), ast = ts6.createSourceFile(
|
|
1214
1214
|
entry,
|
|
@@ -1247,7 +1247,7 @@ async function listProjectResources(root) {
|
|
|
1247
1247
|
else
|
|
1248
1248
|
resources.push({
|
|
1249
1249
|
id: `scene:${entry}`,
|
|
1250
|
-
title: item.name === "scene.tsx" ?
|
|
1250
|
+
title: item.name === "scene.tsx" ? path12.basename(folder) : item.name.slice(0, -4),
|
|
1251
1251
|
kind: "scene",
|
|
1252
1252
|
entry,
|
|
1253
1253
|
...info,
|
|
@@ -1271,7 +1271,7 @@ async function getProjectResource(root, id) {
|
|
|
1271
1271
|
async function resourcePreview(root, id) {
|
|
1272
1272
|
const resource = await getProjectResource(root, id);
|
|
1273
1273
|
if (!resource.preview) throw new Error("No preview for this resource");
|
|
1274
|
-
const file = await resourceFile(root, resource.preview), type = mime[
|
|
1274
|
+
const file = await resourceFile(root, resource.preview), type = mime[path12.extname(file).toLowerCase()];
|
|
1275
1275
|
if (!type) throw new Error("Unsupported preview type");
|
|
1276
1276
|
if ((await fs3.stat(file)).size > 12 * 1024 * 1024)
|
|
1277
1277
|
throw new Error("Preview is too large");
|
|
@@ -1279,14 +1279,14 @@ async function resourcePreview(root, id) {
|
|
|
1279
1279
|
}
|
|
1280
1280
|
async function dimensions(file, kind) {
|
|
1281
1281
|
if (!["image", "video"].includes(kind)) return {};
|
|
1282
|
-
if (
|
|
1282
|
+
if (path12.extname(file).toLowerCase() === ".svg") {
|
|
1283
1283
|
const text2 = await fs3.readFile(file, "utf8"), view = text2.match(
|
|
1284
1284
|
/viewBox=["']\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)/i
|
|
1285
1285
|
);
|
|
1286
1286
|
return view ? { width: Number(view[1]), height: Number(view[2]) } : {};
|
|
1287
1287
|
}
|
|
1288
1288
|
try {
|
|
1289
|
-
const { stdout } = await
|
|
1289
|
+
const { stdout } = await exec(
|
|
1290
1290
|
"ffprobe",
|
|
1291
1291
|
[
|
|
1292
1292
|
"-v",
|
|
@@ -1311,42 +1311,42 @@ async function dimensions(file, kind) {
|
|
|
1311
1311
|
}
|
|
1312
1312
|
}
|
|
1313
1313
|
async function importProjectResource(root, input) {
|
|
1314
|
-
const source = await fs3.realpath(
|
|
1314
|
+
const source = await fs3.realpath(path12.resolve(input)), kind = kinds[path12.extname(source).toLowerCase()];
|
|
1315
1315
|
if (!kind)
|
|
1316
1316
|
throw new Error(
|
|
1317
1317
|
"Supported files: scenes (.tsx), images, MP4/WebM/MOV, WGSL shaders and GLB models."
|
|
1318
1318
|
);
|
|
1319
1319
|
if (!(await fs3.stat(source)).isFile()) throw new Error("Import a file.");
|
|
1320
|
-
const folder = await resourceFolder(root), title =
|
|
1321
|
-
let id = slug(title), destination =
|
|
1320
|
+
const folder = await resourceFolder(root), title = path12.basename(source, path12.extname(source));
|
|
1321
|
+
let id = slug(title), destination = path12.join(folder, id);
|
|
1322
1322
|
try {
|
|
1323
1323
|
await fs3.mkdir(destination);
|
|
1324
1324
|
} catch (e) {
|
|
1325
1325
|
if (e.code !== "EEXIST") throw e;
|
|
1326
1326
|
id += `-${randomUUID3().slice(0, 8)}`;
|
|
1327
|
-
destination =
|
|
1327
|
+
destination = path12.join(folder, id);
|
|
1328
1328
|
await fs3.mkdir(destination);
|
|
1329
1329
|
}
|
|
1330
1330
|
try {
|
|
1331
|
-
const entry = kind === "scene" ?
|
|
1331
|
+
const entry = kind === "scene" ? path12.basename(source) : `asset${path12.extname(source).toLowerCase()}`;
|
|
1332
1332
|
if (kind === "scene") {
|
|
1333
1333
|
const { createDocumentLoader: createDocumentLoader2 } = await Promise.resolve().then(() => (init_scene_document2(), scene_document_exports));
|
|
1334
|
-
const base =
|
|
1334
|
+
const base = path12.dirname(source), doc = await createDocumentLoader2(base).load(source);
|
|
1335
1335
|
if (doc.root.type !== "Scene")
|
|
1336
1336
|
throw new Error("Scene resources must export a Scene.");
|
|
1337
1337
|
if (doc.nativeReason) throw new Error(doc.nativeReason);
|
|
1338
1338
|
const files = /* @__PURE__ */ new Set([...doc.files, ...Object.keys(doc.assets)]);
|
|
1339
1339
|
for (const relative of files) {
|
|
1340
|
-
const file = await fs3.realpath(
|
|
1340
|
+
const file = await fs3.realpath(path12.resolve(base, relative));
|
|
1341
1341
|
if (!inside(base, file))
|
|
1342
1342
|
throw new Error(
|
|
1343
1343
|
"Keep imported scene dependencies inside its folder."
|
|
1344
1344
|
);
|
|
1345
|
-
const dest =
|
|
1346
|
-
await fs3.mkdir(
|
|
1345
|
+
const dest = path12.join(destination, relative);
|
|
1346
|
+
await fs3.mkdir(path12.dirname(dest), { recursive: true });
|
|
1347
1347
|
await fs3.copyFile(file, dest);
|
|
1348
1348
|
}
|
|
1349
|
-
} else await fs3.copyFile(source,
|
|
1349
|
+
} else await fs3.copyFile(source, path12.join(destination, entry));
|
|
1350
1350
|
const meta = {
|
|
1351
1351
|
id,
|
|
1352
1352
|
title,
|
|
@@ -1363,7 +1363,7 @@ async function importProjectResource(root, input) {
|
|
|
1363
1363
|
);
|
|
1364
1364
|
}
|
|
1365
1365
|
await fs3.writeFile(
|
|
1366
|
-
|
|
1366
|
+
path12.join(destination, "resource.json"),
|
|
1367
1367
|
JSON.stringify(meta, null, 2) + "\n",
|
|
1368
1368
|
{ flag: "wx" }
|
|
1369
1369
|
);
|
|
@@ -1379,10 +1379,10 @@ async function importProjectResource(root, input) {
|
|
|
1379
1379
|
}
|
|
1380
1380
|
}
|
|
1381
1381
|
async function installResourceExamples(root) {
|
|
1382
|
-
const folder = await resourceFolder(root), source =
|
|
1382
|
+
const folder = await resourceFolder(root), source = path12.join(packageRoot, "src/resource-examples/");
|
|
1383
1383
|
for (const item of await fs3.readdir(source, { withFileTypes: true })) {
|
|
1384
1384
|
if (!item.isDirectory()) continue;
|
|
1385
|
-
const destination =
|
|
1385
|
+
const destination = path12.join(folder, item.name);
|
|
1386
1386
|
try {
|
|
1387
1387
|
await fs3.mkdir(destination);
|
|
1388
1388
|
} catch (e) {
|
|
@@ -1390,12 +1390,12 @@ async function installResourceExamples(root) {
|
|
|
1390
1390
|
throw e;
|
|
1391
1391
|
}
|
|
1392
1392
|
try {
|
|
1393
|
-
for (const file of (await fs3.readdir(
|
|
1393
|
+
for (const file of (await fs3.readdir(path12.join(source, item.name))).sort(
|
|
1394
1394
|
(a, b) => Number(a === "resource.json") - Number(b === "resource.json")
|
|
1395
1395
|
))
|
|
1396
1396
|
await fs3.copyFile(
|
|
1397
|
-
|
|
1398
|
-
|
|
1397
|
+
path12.join(source, item.name, file),
|
|
1398
|
+
path12.join(destination, file)
|
|
1399
1399
|
);
|
|
1400
1400
|
} catch (error) {
|
|
1401
1401
|
await fs3.rm(destination, { recursive: true, force: true });
|
|
@@ -1404,12 +1404,12 @@ async function installResourceExamples(root) {
|
|
|
1404
1404
|
}
|
|
1405
1405
|
return { status: "installed", ...await listProjectResources(root) };
|
|
1406
1406
|
}
|
|
1407
|
-
var
|
|
1407
|
+
var exec, kinds, mime, slug, inside;
|
|
1408
1408
|
var init_project_resources = __esm({
|
|
1409
1409
|
"src/project-resources.ts"() {
|
|
1410
1410
|
"use strict";
|
|
1411
1411
|
init_runtime();
|
|
1412
|
-
|
|
1412
|
+
exec = promisify(execFile);
|
|
1413
1413
|
kinds = {
|
|
1414
1414
|
".tsx": "scene",
|
|
1415
1415
|
".svg": "image",
|
|
@@ -1435,8 +1435,8 @@ var init_project_resources = __esm({
|
|
|
1435
1435
|
};
|
|
1436
1436
|
slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "resource";
|
|
1437
1437
|
inside = (root, file) => {
|
|
1438
|
-
const rel =
|
|
1439
|
-
return rel !== ".." && !rel.startsWith(".." +
|
|
1438
|
+
const rel = path12.relative(root, file);
|
|
1439
|
+
return rel !== ".." && !rel.startsWith(".." + path12.sep) && !path12.isAbsolute(rel);
|
|
1440
1440
|
};
|
|
1441
1441
|
}
|
|
1442
1442
|
});
|
|
@@ -4768,86 +4768,11 @@ var init_server = __esm({
|
|
|
4768
4768
|
}
|
|
4769
4769
|
});
|
|
4770
4770
|
|
|
4771
|
-
// src/cli.ts
|
|
4772
|
-
import path20 from "node:path";
|
|
4773
|
-
|
|
4774
|
-
// src/features.ts
|
|
4775
|
-
function studioEnabled(env = process.env) {
|
|
4776
|
-
return env.OSMO_STUDIO_ENABLED === "1";
|
|
4777
|
-
}
|
|
4778
|
-
function requireStudio() {
|
|
4779
|
-
if (!studioEnabled())
|
|
4780
|
-
throw Object.assign(
|
|
4781
|
-
new Error("Osmo Studio commands are not available in this release."),
|
|
4782
|
-
{ code: "feature_disabled" }
|
|
4783
|
-
);
|
|
4784
|
-
}
|
|
4785
|
-
|
|
4786
|
-
// src/help.ts
|
|
4787
|
-
function cliHelp(version2, studio) {
|
|
4788
|
-
const common = `Osmo ${version2}
|
|
4789
|
-
|
|
4790
|
-
osmo login [--name COMPUTER] [--json]
|
|
4791
|
-
osmo login status SESSION [--json]
|
|
4792
|
-
osmo whoami [--json]
|
|
4793
|
-
osmo logout [--json]
|
|
4794
|
-
osmo resources list [--category shader] [--json]
|
|
4795
|
-
osmo resources show ID [--version VERSION] [--preset PRESET] [--json]
|
|
4796
|
-
osmo resources add ID [--version VERSION] [--preset PRESET] [--json]
|
|
4797
|
-
osmo resources import FILE [--json]
|
|
4798
|
-
osmo resources local [--json]
|
|
4799
|
-
osmo resources examples [--json]
|
|
4800
|
-
osmo share --video FILE.mp4 [--project ID] [--name NAME] [--json]
|
|
4801
|
-
osmo unshare --project ID [--json]
|
|
4802
|
-
osmo comments list --project ID [--open] [--json]
|
|
4803
|
-
osmo comments reply ID "message" --project ID
|
|
4804
|
-
osmo comments resolve ID --project ID
|
|
4805
|
-
osmo comments reopen ID --project ID`;
|
|
4806
|
-
const studioCommands = `osmo studio init [folder]
|
|
4807
|
-
osmo studio publish [--name NAME]
|
|
4808
|
-
osmo studio push
|
|
4809
|
-
osmo studio pull
|
|
4810
|
-
osmo studio sync
|
|
4811
|
-
osmo studio clone PROJECT_ID folder
|
|
4812
|
-
osmo studio history
|
|
4813
|
-
osmo studio open [scene.tsx] [--port NUMBER] [--no-open]
|
|
4814
|
-
osmo studio open animations/player.tsx [--width 800 --height 450 --duration 4]
|
|
4815
|
-
osmo studio check [scene.tsx] [--json]
|
|
4816
|
-
osmo studio render [scene.tsx] --out animation.mp4 [--supersample 1|2]
|
|
4817
|
-
osmo studio render [scene.tsx] --frame NUMBER --out frame.png [--supersample 1|2]
|
|
4818
|
-
osmo studio deploy [scene.tsx]
|
|
4819
|
-
osmo studio comments list [--open] [--scene scene.tsx] [--json]
|
|
4820
|
-
osmo studio comments start ID [EMOJI]
|
|
4821
|
-
osmo studio comments stop ID
|
|
4822
|
-
osmo studio comments react ID [EMOJI|none]
|
|
4823
|
-
osmo studio comments reply ID "message"
|
|
4824
|
-
osmo studio comments resolve ID
|
|
4825
|
-
osmo studio comments reopen ID`;
|
|
4826
|
-
const video = "osmo share --video: share a rendered video from any tool, including drafts and work in progress. Video review and version history only; no editable source. Requires login and ffprobe, not init or Git. Without --project, creates a new review.";
|
|
4827
|
-
return `${common}${studio ? "\n" + studioCommands : ""}
|
|
4828
|
-
|
|
4829
|
-
${studio ? "Choose the sharing workflow:\n " : ""}${video}${studio ? "\n osmo studio deploy: publish a full-featured editable Osmo project with source files, assets, and tracked Git history, plus a review video. Requires osmo studio init, osmo login, and osmo studio publish. Prefer this command for Osmo Studio projects. Edit locally with Studio or code; the cloud review is read-only." : ""}
|
|
4830
|
-
${studio ? "\nNew animation: run osmo studio init, read AGENTS.md, then resources list --json before authoring.\n" : ""}
|
|
4831
|
-
osmo unshare: disable a project's public review and version links, preserving
|
|
4832
|
-
videos, comments, and history. Sharing a new video to the same project restores its review links.
|
|
4833
|
-
Previously obtained direct media URLs may still work.
|
|
4834
|
-
|
|
4835
|
-
Run project commands from the initialized project folder. Native renderer included
|
|
4836
|
-
in release archives; Node 22 and FFmpeg/ffprobe are prerequisites.
|
|
4837
|
-
Cloud commands need osmo login; OSMO_BASE_URL picks the cloud (default studio.osmo.inc).
|
|
4838
|
-
`;
|
|
4839
|
-
}
|
|
4840
|
-
|
|
4841
4771
|
// src/cli.ts
|
|
4842
4772
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4773
|
+
import path20 from "node:path";
|
|
4843
4774
|
import { parseArgs } from "node:util";
|
|
4844
4775
|
|
|
4845
|
-
// src/video-deploy.ts
|
|
4846
|
-
import { execFile } from "node:child_process";
|
|
4847
|
-
import { stat } from "node:fs/promises";
|
|
4848
|
-
import path2 from "node:path";
|
|
4849
|
-
import { promisify } from "node:util";
|
|
4850
|
-
|
|
4851
4776
|
// src/auth.ts
|
|
4852
4777
|
import {
|
|
4853
4778
|
mkdir,
|
|
@@ -5356,6 +5281,16 @@ ${result.next}`;
|
|
|
5356
5281
|
Next: ${result.next}`;
|
|
5357
5282
|
}
|
|
5358
5283
|
|
|
5284
|
+
// src/cli.ts
|
|
5285
|
+
init_check();
|
|
5286
|
+
|
|
5287
|
+
// src/cloud.ts
|
|
5288
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5289
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
5290
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2 } from "node:fs/promises";
|
|
5291
|
+
import path6 from "node:path";
|
|
5292
|
+
import ts3 from "typescript";
|
|
5293
|
+
|
|
5359
5294
|
// src/cloud-request.ts
|
|
5360
5295
|
async function call(origin, route, { method = "GET", body: body2, timeoutMs = 6e4, allow = [] } = {}) {
|
|
5361
5296
|
let response;
|
|
@@ -5380,345 +5315,9 @@ async function call(origin, route, { method = "GET", body: body2, timeoutMs = 6e
|
|
|
5380
5315
|
return { status: response.status, ...data };
|
|
5381
5316
|
}
|
|
5382
5317
|
|
|
5383
|
-
// src/upload-render.ts
|
|
5384
|
-
import { openAsBlob } from "node:fs";
|
|
5385
|
-
async function uploadRender(origin, project, file, metadata, log = () => {
|
|
5386
|
-
}) {
|
|
5387
|
-
const video = await openAsBlob(file, { type: metadata.contentType });
|
|
5388
|
-
if (!video.size || video.size > 2 * 1024 ** 3)
|
|
5389
|
-
throw new Error("Video must be non-empty and no larger than 2 GB.");
|
|
5390
|
-
let slot;
|
|
5391
|
-
try {
|
|
5392
|
-
log(`uploading video (${(video.size / 1048576).toFixed(1)} MB)\u2026`);
|
|
5393
|
-
slot = await call(
|
|
5394
|
-
origin,
|
|
5395
|
-
`/api/projects/${encodeURIComponent(project)}/render`,
|
|
5396
|
-
{
|
|
5397
|
-
method: "POST",
|
|
5398
|
-
body: { ...metadata, sizeInBytes: video.size }
|
|
5399
|
-
}
|
|
5400
|
-
);
|
|
5401
|
-
if (slot.type !== "success")
|
|
5402
|
-
throw new Error(slot.error ?? "The cloud did not accept the video.");
|
|
5403
|
-
const put = await fetch(slot.uploadUrl, {
|
|
5404
|
-
method: "PUT",
|
|
5405
|
-
headers: { "Content-Type": metadata.contentType },
|
|
5406
|
-
body: video,
|
|
5407
|
-
redirect: "error",
|
|
5408
|
-
signal: AbortSignal.timeout(9e5)
|
|
5409
|
-
});
|
|
5410
|
-
if (!put.ok) throw new Error(`Video upload failed (${put.status}).`);
|
|
5411
|
-
const render = slot.renderRowId;
|
|
5412
|
-
const done = await call(origin, `/api/renders/${render}/complete`, {
|
|
5413
|
-
method: "POST",
|
|
5414
|
-
body: { fileKey: slot.fileKey }
|
|
5415
|
-
});
|
|
5416
|
-
if (done.type !== "success")
|
|
5417
|
-
throw new Error(done.error ?? "The cloud did not finalize the video.");
|
|
5418
|
-
slot = null;
|
|
5419
|
-
if (done.sharingEnabled === false)
|
|
5420
|
-
throw new Error(
|
|
5421
|
-
"Video saved, but review sharing was disabled after this upload started. Upload again to restore sharing."
|
|
5422
|
-
);
|
|
5423
|
-
return {
|
|
5424
|
-
status: "deployed",
|
|
5425
|
-
project,
|
|
5426
|
-
render,
|
|
5427
|
-
link: `${origin}/share/project/${project}`,
|
|
5428
|
-
versionLink: `${origin}/share/${render}`
|
|
5429
|
-
};
|
|
5430
|
-
} finally {
|
|
5431
|
-
if (slot?.renderRowId)
|
|
5432
|
-
await call(origin, `/api/renders/${slot.renderRowId}/fail`, {
|
|
5433
|
-
method: "POST",
|
|
5434
|
-
body: { error: "deploy did not complete" }
|
|
5435
|
-
}).catch(() => void 0);
|
|
5436
|
-
}
|
|
5437
|
-
}
|
|
5438
|
-
|
|
5439
|
-
// src/video-deploy.ts
|
|
5440
|
-
var exec = promisify(execFile);
|
|
5441
|
-
function projectId(value) {
|
|
5442
|
-
if (typeof value !== "string" || !/^[a-z0-9]{10,40}$/.test(value))
|
|
5443
|
-
throw new Error("Provide a valid cloud project ID with --project.");
|
|
5444
|
-
return value;
|
|
5445
|
-
}
|
|
5446
|
-
async function unshare(project) {
|
|
5447
|
-
projectId(project);
|
|
5448
|
-
return call(
|
|
5449
|
-
authOrigin(),
|
|
5450
|
-
`/api/projects/${project}/share`,
|
|
5451
|
-
{ method: "DELETE" }
|
|
5452
|
-
);
|
|
5453
|
-
}
|
|
5454
|
-
async function probeVideo(file) {
|
|
5455
|
-
const info = await stat(file).catch(() => null);
|
|
5456
|
-
if (!info?.isFile() || !info.size || info.size > 2 * 1024 ** 3)
|
|
5457
|
-
throw new Error(
|
|
5458
|
-
"Video must be an existing, non-empty file no larger than 2 GB."
|
|
5459
|
-
);
|
|
5460
|
-
let data;
|
|
5461
|
-
try {
|
|
5462
|
-
const { stdout } = await exec(
|
|
5463
|
-
process.env.OSMO_FFPROBE ?? "ffprobe",
|
|
5464
|
-
["-v", "error", "-show_streams", "-show_format", "-of", "json", file],
|
|
5465
|
-
{ timeout: 3e4, maxBuffer: 2 * 1024 ** 2 }
|
|
5466
|
-
);
|
|
5467
|
-
data = JSON.parse(stdout);
|
|
5468
|
-
} catch (error) {
|
|
5469
|
-
if (error.code === "ENOENT")
|
|
5470
|
-
throw new Error(
|
|
5471
|
-
"ffprobe is required to inspect videos. Install FFmpeg (which includes ffprobe), then retry."
|
|
5472
|
-
);
|
|
5473
|
-
throw new Error(
|
|
5474
|
-
"Cannot read this video. Provide a playable H.264 MP4 file."
|
|
5475
|
-
);
|
|
5476
|
-
}
|
|
5477
|
-
const videos = (data.streams ?? []).filter(
|
|
5478
|
-
(s) => s.codec_type === "video" && !s.disposition?.attached_pic
|
|
5479
|
-
);
|
|
5480
|
-
const video = videos[0], audio = (data.streams ?? []).filter((s) => s.codec_type === "audio");
|
|
5481
|
-
if (!data.format?.format_name?.split(",").includes("mp4") || videos.length !== 1 || video?.codec_name !== "h264" || !["yuv420p", "yuvj420p"].includes(
|
|
5482
|
-
video?.pix_fmt
|
|
5483
|
-
) || audio.some((s) => s.codec_name !== "aac"))
|
|
5484
|
-
throw new Error(
|
|
5485
|
-
"Use an H.264 MP4 with 8-bit 4:2:0 video and AAC audio (or no audio). Export a compatible MP4 from your video tool, then retry."
|
|
5486
|
-
);
|
|
5487
|
-
const [n, d = "1"] = String(video.avg_frame_rate).split("/");
|
|
5488
|
-
const fps = Number(n) / Number(d), durationSeconds = Number(video.duration ?? data.format.duration);
|
|
5489
|
-
const rotation = Number(
|
|
5490
|
-
video.side_data_list?.find((s) => s.rotation !== void 0)?.rotation ?? video.tags?.rotate ?? 0
|
|
5491
|
-
);
|
|
5492
|
-
const portrait = Math.abs(rotation % 180) === 90;
|
|
5493
|
-
const width = portrait ? video.height : video.width, height = portrait ? video.width : video.height;
|
|
5494
|
-
if (![fps, durationSeconds, width, height].every(
|
|
5495
|
-
(x) => Number.isFinite(x) && x > 0
|
|
5496
|
-
) || fps > 1e3 || durationSeconds * fps > 2147483647)
|
|
5497
|
-
throw new Error("Video has invalid or unsupported timing or dimensions.");
|
|
5498
|
-
return {
|
|
5499
|
-
codec: "h264",
|
|
5500
|
-
contentType: "video/mp4",
|
|
5501
|
-
// The check above found both finite and positive.
|
|
5502
|
-
compositionWidth: width,
|
|
5503
|
-
compositionHeight: height,
|
|
5504
|
-
fps,
|
|
5505
|
-
durationSeconds,
|
|
5506
|
-
durationInFrames: Math.max(1, Math.round(durationSeconds * fps))
|
|
5507
|
-
};
|
|
5508
|
-
}
|
|
5509
|
-
async function shareVideo(file, {
|
|
5510
|
-
project,
|
|
5511
|
-
name,
|
|
5512
|
-
log = () => {
|
|
5513
|
-
}
|
|
5514
|
-
} = {}) {
|
|
5515
|
-
if (project !== void 0) projectId(project);
|
|
5516
|
-
if (name !== void 0 && (!name.trim() || name.trim().length > 200))
|
|
5517
|
-
throw new Error("--name must contain 1\u2013200 characters.");
|
|
5518
|
-
const source = path2.resolve(file), metadata = await probeVideo(source), origin = authOrigin();
|
|
5519
|
-
await currentAccount(origin);
|
|
5520
|
-
const title = name?.trim() ?? path2.basename(source).slice(0, 200);
|
|
5521
|
-
const id = project ?? (await call(origin, "/api/review-projects", {
|
|
5522
|
-
method: "POST",
|
|
5523
|
-
body: { name: title }
|
|
5524
|
-
})).id;
|
|
5525
|
-
try {
|
|
5526
|
-
return await uploadRender(
|
|
5527
|
-
origin,
|
|
5528
|
-
id,
|
|
5529
|
-
source,
|
|
5530
|
-
{ ...metadata, source: "video", name: title },
|
|
5531
|
-
log
|
|
5532
|
-
);
|
|
5533
|
-
} catch (error) {
|
|
5534
|
-
throw new Error(
|
|
5535
|
-
`${error.message}
|
|
5536
|
-
Retry with --project ${id} to use the same review project.`
|
|
5537
|
-
);
|
|
5538
|
-
}
|
|
5539
|
-
}
|
|
5540
|
-
async function videoComments(project, { open = false } = {}) {
|
|
5541
|
-
projectId(project);
|
|
5542
|
-
const origin = authOrigin(), comments = [];
|
|
5543
|
-
let cursor;
|
|
5544
|
-
do {
|
|
5545
|
-
const page = await call(
|
|
5546
|
-
origin,
|
|
5547
|
-
`/api/projects/${project}/share/versions${cursor ? `?before=${encodeURIComponent(cursor)}` : ""}`
|
|
5548
|
-
);
|
|
5549
|
-
for (const version2 of page.versions) {
|
|
5550
|
-
const data = await call(
|
|
5551
|
-
origin,
|
|
5552
|
-
`/api/renders/${version2.id}/review-comments`
|
|
5553
|
-
);
|
|
5554
|
-
comments.push(
|
|
5555
|
-
...data.comments.filter((c) => !open || c.status === "open").map((c) => ({
|
|
5556
|
-
...c,
|
|
5557
|
-
render: version2.id,
|
|
5558
|
-
link: `${origin}/share/${version2.id}`
|
|
5559
|
-
}))
|
|
5560
|
-
);
|
|
5561
|
-
}
|
|
5562
|
-
cursor = page.nextCursor;
|
|
5563
|
-
} while (cursor);
|
|
5564
|
-
return comments;
|
|
5565
|
-
}
|
|
5566
|
-
|
|
5567
|
-
// src/init.ts
|
|
5568
|
-
import {
|
|
5569
|
-
mkdir as mkdir4,
|
|
5570
|
-
readFile as readFile5,
|
|
5571
|
-
writeFile as writeFile4,
|
|
5572
|
-
copyFile,
|
|
5573
|
-
cp,
|
|
5574
|
-
access as access2,
|
|
5575
|
-
mkdtemp,
|
|
5576
|
-
rm as rm4
|
|
5577
|
-
} from "node:fs/promises";
|
|
5578
|
-
import path9 from "node:path";
|
|
5579
|
-
|
|
5580
|
-
// src/authoring.ts
|
|
5581
|
-
init_runtime();
|
|
5582
|
-
import { readFile as readFile2, writeFile as writeFile2, rm as rm2 } from "node:fs/promises";
|
|
5583
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
5584
|
-
import path4 from "node:path";
|
|
5585
|
-
var begin = "<!-- osmo:begin -->";
|
|
5586
|
-
var end = "<!-- osmo:end -->";
|
|
5587
|
-
var legacyPointer = "For Osmo animation tasks, read osmo.md before editing. Use osmo studio check, and read open Studio comments.";
|
|
5588
|
-
var legacyGuideHash = "815dae34cd7e5e6f44a575a88d80516d3736f9b74c76b9379fc09144c6aa06fb";
|
|
5589
|
-
var authoringGuide = `${begin}
|
|
5590
|
-
## Osmo Studio (${version})
|
|
5591
|
-
|
|
5592
|
-
Project setup: osmo studio init [folder]. During internal preview, Studio commands require OSMO_STUDIO_ENABLED=1.
|
|
5593
|
-
|
|
5594
|
-
TSX is the source of truth. Use the installed @osmo/scene Strict API; do not edit node_modules or the renderer.
|
|
5595
|
-
|
|
5596
|
-
### Check resources first
|
|
5597
|
-
At the start of every animation task, run osmo resources list --json. Inspect
|
|
5598
|
-
plausible matches with osmo resources show ID --json, including their previews.
|
|
5599
|
-
Prefer a suitable resource over recreating it; install only resources you use with
|
|
5600
|
-
osmo resources add ID --preset PRESET --json. If login is required, run osmo login
|
|
5601
|
-
--json and share the approval link. If the catalog is unavailable, say so and
|
|
5602
|
-
continue locally. Use the CLI compatibility result; older manifests may describe
|
|
5603
|
-
an earlier CLI. After install, use its local files, types, preset values and usage.
|
|
5604
|
-
|
|
5605
|
-
### Choose the primitive first
|
|
5606
|
-
| Visual pattern | Start with |
|
|
5607
|
-
| --- | --- |
|
|
5608
|
-
| Typography following a circle, curve or figure-eight | One Text with path; animate path.progress |
|
|
5609
|
-
| Elements moving/rotating together | Group transforms and shared motion tracks |
|
|
5610
|
-
| Panels, circles, borders or thin lines | Group fill/stroke/shape/rounding |
|
|
5611
|
-
| Vector curves, icons, cutouts or illustrations | Path with SVG d, fill and stroke; evenodd for holes |
|
|
5612
|
-
| Images, footage, soundtrack | Image, Video, Audio with local asset paths |
|
|
5613
|
-
| Parallax and solid cards | Camera, z and Group.depth |
|
|
5614
|
-
| Content on a 3D model | Model with Surface children; inspect GLB selectors first |
|
|
5615
|
-
| Procedural backgrounds or liquid glass | Check resources; Shader with local src and explicit uniforms; glass samples backdrop |
|
|
5616
|
-
| Softness or texture | effects={[blur({ radius: 5 }), grain({ amount: 0.04 })]} |
|
|
5617
|
-
|
|
5618
|
-
Prototype the native primitive before detailed tracking; explain a tested limitation before custom reconstruction. No approval step needed.
|
|
5619
|
-
### Layout before coordinates
|
|
5620
|
-
- Use Group row/column, align, justify, gap and padding for labels/cards/icon rows; position places the whole group.
|
|
5621
|
-
- Center labels with Text width/height, align="center", verticalAlign="center". Default bounds are the line box; opt into alignBounds="glyphs" for visible-letter centering. Changing text can shift glyph bounds.
|
|
5622
|
-
- Fit icons into equal Path width/height with fit="contain". Supply viewBox for the asset viewport, or omit it for tight geometry bounds, including negative origins. Fitting scales strokes; fill stretches; unsized paths retain coordinates.
|
|
5623
|
-
- Text and fitted paths can overflow; use a clipping Group to crop. See the sizing JSDoc before applying manual offsets.
|
|
5624
|
-
|
|
5625
|
-
### API reference: types with short explanations
|
|
5626
|
-
Read relevant declarations in node_modules/@osmo/scene/dist/ (JSDoc explains usage):
|
|
5627
|
-
- index.d.ts: public exports; elements.d.ts: all primitives, props and examples.
|
|
5628
|
-
- motion.d.ts: defineMotion, keyframes and interpolation.
|
|
5629
|
-
- knobs.d.ts + knob-schema.d.ts: Group controls, typed values and UI metadata.
|
|
5630
|
-
- effects.d.ts: built-in effects and the broader library effect API.
|
|
5631
|
-
|
|
5632
|
-
### Strict rules
|
|
5633
|
-
- Named imports from @osmo/scene; export default <Scene ...> in the scene entry.
|
|
5634
|
-
- Reuse a local default-exported Group: import Card from './animations/card'; <Card track={0} values={{ title: 'Hello' }} />. No children/knobs replacement; imports stay Strict.
|
|
5635
|
-
- Root Group/Text/Image/Video/Audio/Shader/Path/Rect/Model clips require literal track (0\u201399). start/duration are optional seconds; same-track clips cannot overlap. Tracks do not change paint order. Nested layers omit track. Motions use local clip time; trimming never stretches motion or changes media trimIn/playbackRate.
|
|
5636
|
-
- Optional names: Group label="Orbiting circles" (max two words); Scene tracks={{0:{label:"Shapes"}}} (one word per lane). Labels are static; track names stay with lanes. Keep stable IDs separate. Group hidden hides visuals; solo isolates siblings. Scene.tracks also accepts hidden (visuals), muted (audio), solo (isolate lanes); these affect preview and export.
|
|
5637
|
-
- Camera/Surface are not clips. Group sizing: "fill" follows parent, "auto" fits flow content.
|
|
5638
|
-
- Groups clip children to their shape by default. Set clip={false} to allow overflow.
|
|
5639
|
-
- Preview Group files with studio open --width 640 --height 360; storyboard.canvas.json stores per-card placement, size and literal knob values, referencing the original TSX.
|
|
5640
|
-
- Use explicit JSX, literal values/arrays/objects, and const defineMotion or defineKnobs declarations. Bind tracks/knobs directly to compatible props.
|
|
5641
|
-
- No arbitrary expressions, loops, callbacks, spreads, import aliases, custom components, helper calls or computed variables. Only blur({...}) and grain({...}) effect calls are allowed in JSX; pass an explicit object, even for defaults.
|
|
5642
|
-
- Shader.src must be a static relative local .wgsl path. Put complete uniform values inline; numeric uniforms can use motion/knob tracks. defineEffect remains unsupported.
|
|
5643
|
-
- Keyframe at values are increasing local seconds (scene unless inside a timed clip); easing belongs to the arriving key. Add a key at 0 to define a continuous track's starting value. Tracks do not loop automatically.
|
|
5644
|
-
- Group knobs can drive many descendant props; Group.values accepts motion tracks.
|
|
5645
|
-
- Units: pixels, degrees, seconds; +z toward viewer. Asset/font paths are relative to their declaring file.
|
|
5646
|
-
- Text.path handles the complete string; no width/align/lineHeight or line breaks. SVG geometry is static; progress and ellipse radii can animate.
|
|
5647
|
-
- Path draws SVG d in local pixels; use scale to resize. Geometry stays static; transforms, stroke width and effects can animate. Use fill="transparent" for lines.
|
|
5648
|
-
|
|
5649
|
-
### Workflow
|
|
5650
|
-
1. Read relevant types, edit the scene, then run osmo studio check scene.tsx.
|
|
5651
|
-
2. Preview: osmo studio open scene.tsx. Keep the server running; saves reload it. Reopening reuses the project URL, saved in gitignored .osmo/studio.json.
|
|
5652
|
-
3. Export: osmo studio render scene.tsx --out exports/animation.mp4 (single PNG: --frame 90 --out exports/frame.png, zero-based; use a new filename if one exists). Share for review: osmo studio deploy (after osmo login and osmo studio publish) renders, pushes editable source/assets and Git history, and prints a review link. Prefer this command for Osmo Studio projects. osmo share --video FILE.mp4 is only for sharing a rendered video from any tool, including drafts and work in progress: video versions and comments, no editable source; it needs login and ffprobe but no init, Git, or project.
|
|
5653
|
-
4. When asked to apply feedback: osmo studio comments list --open --json. Check the scene/timestamp and any older revision; edit and validate the fix. If complete and verified, reply only "Done." with osmo studio comments reply ID "Done." then run osmo studio comments resolve ID. Do not leave routine fixes open for review. Keep open only for ambiguity, an incomplete fix, or a specific uncertainty needing user verification. Reply with one short sentence stating the blocker or what to check.
|
|
5654
|
-
${end}
|
|
5655
|
-
`;
|
|
5656
|
-
async function optional(file) {
|
|
5657
|
-
try {
|
|
5658
|
-
return await readFile2(file, "utf8");
|
|
5659
|
-
} catch (error) {
|
|
5660
|
-
if (error.code === "ENOENT") return "";
|
|
5661
|
-
throw error;
|
|
5662
|
-
}
|
|
5663
|
-
}
|
|
5664
|
-
async function writeAuthoringGuide(root) {
|
|
5665
|
-
const file = path4.join(root, "AGENTS.md");
|
|
5666
|
-
const original = await optional(file);
|
|
5667
|
-
const hasBegin = original.includes(begin), hasEnd = original.includes(end);
|
|
5668
|
-
if (hasBegin !== hasEnd || hasBegin && original.indexOf(end) < original.indexOf(begin))
|
|
5669
|
-
throw new Error(
|
|
5670
|
-
"AGENTS.md has an incomplete Osmo section; repair its osmo:begin/osmo:end markers before running init."
|
|
5671
|
-
);
|
|
5672
|
-
let text2;
|
|
5673
|
-
if (hasBegin)
|
|
5674
|
-
text2 = original.slice(0, original.indexOf(begin)) + authoringGuide.trimEnd() + original.slice(original.indexOf(end) + end.length);
|
|
5675
|
-
else {
|
|
5676
|
-
const rest = original.split("\n").filter((line) => line !== legacyPointer).join("\n");
|
|
5677
|
-
text2 = rest + (rest && !rest.endsWith("\n\n") ? rest.endsWith("\n") ? "\n" : "\n\n" : "") + authoringGuide;
|
|
5678
|
-
}
|
|
5679
|
-
if (text2 !== original) await writeFile2(file, text2);
|
|
5680
|
-
const legacy = await optional(path4.join(root, "osmo.md"));
|
|
5681
|
-
if (legacy && createHash2("sha256").update(legacy).digest("hex") === legacyGuideHash)
|
|
5682
|
-
await rm2(path4.join(root, "osmo.md"));
|
|
5683
|
-
else if (legacy)
|
|
5684
|
-
console.log(
|
|
5685
|
-
"Existing osmo.md was preserved because it differs from the original generated guide. Osmo instructions now live in AGENTS.md."
|
|
5686
|
-
);
|
|
5687
|
-
}
|
|
5688
|
-
|
|
5689
|
-
// src/init.ts
|
|
5690
|
-
import os2 from "node:os";
|
|
5691
|
-
|
|
5692
|
-
// src/starter.ts
|
|
5693
|
-
init_runtime();
|
|
5694
|
-
import { access, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
5695
|
-
import path5 from "node:path";
|
|
5696
|
-
var template = path5.join(packageRoot, "src/starter/");
|
|
5697
|
-
async function writeStarter(root) {
|
|
5698
|
-
const scene = path5.join(root, "scene.tsx");
|
|
5699
|
-
try {
|
|
5700
|
-
await access(scene);
|
|
5701
|
-
return;
|
|
5702
|
-
} catch (error) {
|
|
5703
|
-
if (error.code !== "ENOENT") throw error;
|
|
5704
|
-
}
|
|
5705
|
-
await mkdir2(path5.join(root, "animations"), { recursive: true });
|
|
5706
|
-
try {
|
|
5707
|
-
await writeFile3(scene, await readFile3(path5.join(template, "scene.tsx")), {
|
|
5708
|
-
flag: "wx"
|
|
5709
|
-
});
|
|
5710
|
-
} catch (error) {
|
|
5711
|
-
if (error.code !== "EEXIST") throw error;
|
|
5712
|
-
}
|
|
5713
|
-
}
|
|
5714
|
-
|
|
5715
|
-
// src/init.ts
|
|
5716
|
-
init_runtime();
|
|
5717
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5718
|
-
|
|
5719
5318
|
// src/git.ts
|
|
5720
5319
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5721
|
-
import { createHash as
|
|
5320
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5722
5321
|
import {
|
|
5723
5322
|
existsSync,
|
|
5724
5323
|
mkdirSync,
|
|
@@ -5727,13 +5326,13 @@ import {
|
|
|
5727
5326
|
rmSync,
|
|
5728
5327
|
writeFileSync
|
|
5729
5328
|
} from "node:fs";
|
|
5730
|
-
import
|
|
5329
|
+
import path4 from "node:path";
|
|
5731
5330
|
import zlib from "node:zlib";
|
|
5732
5331
|
var git = (root, ...args) => execFileSync("git", ["-C", root, ...args], {
|
|
5733
5332
|
encoding: "utf8",
|
|
5734
5333
|
maxBuffer: 1 << 28
|
|
5735
5334
|
}).replace(/\n$/, "");
|
|
5736
|
-
var hasRepo = (root) => existsSync(
|
|
5335
|
+
var hasRepo = (root) => existsSync(path4.join(root, ".git"));
|
|
5737
5336
|
var initRepo = (root) => git(root, "init", "-q", "--object-format=sha256", "-b", "main");
|
|
5738
5337
|
var headOf = (root) => {
|
|
5739
5338
|
const r = spawnSync(
|
|
@@ -5754,7 +5353,7 @@ var objectFormat = (root) => {
|
|
|
5754
5353
|
);
|
|
5755
5354
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
5756
5355
|
};
|
|
5757
|
-
var objectId = (kind, content) =>
|
|
5356
|
+
var objectId = (kind, content) => createHash2("sha256").update(Buffer.from(`${kind} ${content.length}\0`)).update(content).digest("hex");
|
|
5758
5357
|
var identity = (author2) => author2 && typeof author2 === "object" ? [`user.name=${author2.name || author2.email}`, `user.email=${author2.email}`] : [
|
|
5759
5358
|
`user.name=${author2 ?? process.env.USER ?? "osmo"}`,
|
|
5760
5359
|
`user.email=${author2 ?? process.env.USER ?? "osmo"}@local`
|
|
@@ -5844,13 +5443,13 @@ function objectInfo(root, hashes) {
|
|
|
5844
5443
|
}
|
|
5845
5444
|
return out;
|
|
5846
5445
|
}
|
|
5847
|
-
var looseFile = (root, hash3) =>
|
|
5446
|
+
var looseFile = (root, hash3) => path4.join(root, ".git/objects", hash3.slice(0, 2), hash3.slice(2));
|
|
5848
5447
|
function looseObjectValid(root, hash3) {
|
|
5849
5448
|
const file = looseFile(root, hash3);
|
|
5850
5449
|
if (!existsSync(file)) return null;
|
|
5851
5450
|
try {
|
|
5852
5451
|
const inflated = zlib.inflateSync(readFileSync(file));
|
|
5853
|
-
if (
|
|
5452
|
+
if (createHash2("sha256").update(inflated).digest("hex") === hash3)
|
|
5854
5453
|
return true;
|
|
5855
5454
|
} catch {
|
|
5856
5455
|
}
|
|
@@ -5873,7 +5472,7 @@ function writeLooseObjects(root, objects) {
|
|
|
5873
5472
|
);
|
|
5874
5473
|
if (looseObjectValid(root, hash3)) continue;
|
|
5875
5474
|
const file = looseFile(root, hash3);
|
|
5876
|
-
mkdirSync(
|
|
5475
|
+
mkdirSync(path4.dirname(file), { recursive: true });
|
|
5877
5476
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
5878
5477
|
writeFileSync(
|
|
5879
5478
|
temporary,
|
|
@@ -5887,21 +5486,13 @@ function writeLooseObjects(root, objects) {
|
|
|
5887
5486
|
return written;
|
|
5888
5487
|
}
|
|
5889
5488
|
|
|
5890
|
-
// src/cloud.ts
|
|
5891
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
5892
|
-
import { mkdir as mkdir3, readdir, readFile as readFile4, rm as rm3 } from "node:fs/promises";
|
|
5893
|
-
import path8 from "node:path";
|
|
5894
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5895
|
-
import ts2 from "typescript";
|
|
5896
|
-
init_runtime();
|
|
5897
|
-
|
|
5898
5489
|
// src/outline.ts
|
|
5899
5490
|
init_runtime();
|
|
5900
5491
|
init_labels();
|
|
5901
|
-
import
|
|
5902
|
-
import
|
|
5903
|
-
import { pathToFileURL } from "node:url";
|
|
5904
|
-
var { isClipPrimitive } = await import(
|
|
5492
|
+
import ts2 from "typescript";
|
|
5493
|
+
import path5 from "node:path";
|
|
5494
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
5495
|
+
var { isClipPrimitive } = await import(pathToFileURL2(path5.join(runtimeRoot, "packages/scene/src/clip.mjs")).href);
|
|
5905
5496
|
var Reference = class {
|
|
5906
5497
|
constructor(set, name = "") {
|
|
5907
5498
|
this.set = set;
|
|
@@ -5923,35 +5514,35 @@ function merge(ranges) {
|
|
|
5923
5514
|
return result;
|
|
5924
5515
|
}
|
|
5925
5516
|
function inspectScene(text2, filename2, meta, definitions = {}) {
|
|
5926
|
-
const ast =
|
|
5517
|
+
const ast = ts2.createSourceFile(
|
|
5927
5518
|
filename2,
|
|
5928
5519
|
text2,
|
|
5929
|
-
|
|
5520
|
+
ts2.ScriptTarget.Latest,
|
|
5930
5521
|
true,
|
|
5931
|
-
|
|
5522
|
+
ts2.ScriptKind.TSX
|
|
5932
5523
|
);
|
|
5933
5524
|
const motions = /* @__PURE__ */ new Map(), knobs = /* @__PURE__ */ new Map(), rows = [], byId = /* @__PURE__ */ new Map(), used = /* @__PURE__ */ new Set();
|
|
5934
5525
|
const line = (n) => ast.getLineAndCharacterOfPosition(n.getStart(ast)).line + 1;
|
|
5935
5526
|
function read(n) {
|
|
5936
5527
|
if (!n) return true;
|
|
5937
|
-
if (
|
|
5528
|
+
if (ts2.isJsxExpression(n) || ts2.isParenthesizedExpression(n))
|
|
5938
5529
|
return read(n.expression);
|
|
5939
|
-
if (
|
|
5940
|
-
return
|
|
5941
|
-
if (n.kind ===
|
|
5942
|
-
if (n.kind ===
|
|
5943
|
-
if (n.kind ===
|
|
5944
|
-
if (
|
|
5945
|
-
return n.operator ===
|
|
5946
|
-
if (
|
|
5947
|
-
if (
|
|
5530
|
+
if (ts2.isStringLiteral(n) || ts2.isNumericLiteral(n))
|
|
5531
|
+
return ts2.isNumericLiteral(n) ? Number(n.text) : n.text;
|
|
5532
|
+
if (n.kind === ts2.SyntaxKind.TrueKeyword) return true;
|
|
5533
|
+
if (n.kind === ts2.SyntaxKind.FalseKeyword) return false;
|
|
5534
|
+
if (n.kind === ts2.SyntaxKind.NullKeyword) return null;
|
|
5535
|
+
if (ts2.isPrefixUnaryExpression(n))
|
|
5536
|
+
return n.operator === ts2.SyntaxKind.MinusToken ? -read(n.operand) : read(n.operand);
|
|
5537
|
+
if (ts2.isArrayLiteralExpression(n)) return n.elements.map(read);
|
|
5538
|
+
if (ts2.isObjectLiteralExpression(n))
|
|
5948
5539
|
return Object.fromEntries(
|
|
5949
|
-
n.properties.filter(
|
|
5540
|
+
n.properties.filter(ts2.isPropertyAssignment).map((p) => [p.name.text, read(p.initializer)])
|
|
5950
5541
|
);
|
|
5951
|
-
if (
|
|
5542
|
+
if (ts2.isPropertyAccessExpression(n) && ts2.isIdentifier(n.expression))
|
|
5952
5543
|
return new Reference(n.expression.text, n.name.text);
|
|
5953
|
-
if (
|
|
5954
|
-
if (
|
|
5544
|
+
if (ts2.isIdentifier(n)) return new Reference(n.text);
|
|
5545
|
+
if (ts2.isCallExpression(n))
|
|
5955
5546
|
return {
|
|
5956
5547
|
effect: n.expression.getText(ast),
|
|
5957
5548
|
inputs: read(n.arguments[0])
|
|
@@ -5959,10 +5550,10 @@ function inspectScene(text2, filename2, meta, definitions = {}) {
|
|
|
5959
5550
|
return void 0;
|
|
5960
5551
|
}
|
|
5961
5552
|
for (const st of ast.statements)
|
|
5962
|
-
if (
|
|
5553
|
+
if (ts2.isVariableStatement(st))
|
|
5963
5554
|
for (const d of st.declarationList.declarations) {
|
|
5964
5555
|
const call2 = d.initializer;
|
|
5965
|
-
if (!call2 || !
|
|
5556
|
+
if (!call2 || !ts2.isCallExpression(call2)) continue;
|
|
5966
5557
|
const definition = read(call2.arguments[0]);
|
|
5967
5558
|
if (call2.expression.getText(ast) === "defineKnobs")
|
|
5968
5559
|
knobs.set(d.name.text, definition);
|
|
@@ -6015,18 +5606,18 @@ function inspectScene(text2, filename2, meta, definitions = {}) {
|
|
|
6015
5606
|
}
|
|
6016
5607
|
}
|
|
6017
5608
|
function visit(n, parent, depth, scope, clock = { offset: 0, start: 0, end: meta.duration }) {
|
|
6018
|
-
if (
|
|
5609
|
+
if (ts2.isParenthesizedExpression(n) || ts2.isJsxExpression(n))
|
|
6019
5610
|
return n.expression ? visit(n.expression, parent, depth, scope, clock) : [];
|
|
6020
|
-
if (
|
|
5611
|
+
if (ts2.isJsxFragment(n))
|
|
6021
5612
|
return n.children.flatMap((c) => visit(c, parent, depth, scope, clock));
|
|
6022
|
-
if (!
|
|
6023
|
-
const opening3 =
|
|
5613
|
+
if (!ts2.isJsxElement(n) && !ts2.isJsxSelfClosingElement(n)) return [];
|
|
5614
|
+
const opening3 = ts2.isJsxElement(n) ? n.openingElement : n;
|
|
6024
5615
|
const tag = opening3.tagName.getText(ast), definition = definitions[tag];
|
|
6025
5616
|
const type = definition ? "Group" : tag;
|
|
6026
5617
|
if (type === "Fragment")
|
|
6027
|
-
return
|
|
5618
|
+
return ts2.isJsxElement(n) ? n.children.flatMap((c) => visit(c, parent, depth, scope, clock)) : [];
|
|
6028
5619
|
const props = Object.fromEntries(
|
|
6029
|
-
opening3.attributes.properties.filter(
|
|
5620
|
+
opening3.attributes.properties.filter(ts2.isJsxAttribute).map((a) => [a.name.text, read(a.initializer)])
|
|
6030
5621
|
);
|
|
6031
5622
|
if (parent === null && type === "Group") props.start = 0;
|
|
6032
5623
|
const id = typeof props.id === "string" ? `source:${props.id}` : `node-${rows.length}`, local = new Map(scope);
|
|
@@ -6046,14 +5637,14 @@ function inspectScene(text2, filename2, meta, definitions = {}) {
|
|
|
6046
5637
|
if (!["knobs", "values"].includes(name))
|
|
6047
5638
|
bindings(value, name, local, propertyTracks);
|
|
6048
5639
|
if (ownKnobs) bindings(props.values, "knobs", scope, propertyTracks);
|
|
6049
|
-
const rawChildren =
|
|
5640
|
+
const rawChildren = ts2.isJsxElement(n) ? n.children : [];
|
|
6050
5641
|
if (type === "Text") {
|
|
6051
5642
|
for (const child of rawChildren)
|
|
6052
|
-
if (
|
|
5643
|
+
if (ts2.isJsxExpression(child) && child.expression)
|
|
6053
5644
|
bindings(read(child), "text", local, propertyTracks);
|
|
6054
5645
|
}
|
|
6055
5646
|
const words = type === "Text" ? rawChildren.map(
|
|
6056
|
-
(c) =>
|
|
5647
|
+
(c) => ts2.isJsxText(c) ? c.text : ts2.isJsxExpression(c) ? resolve(read(c), local).value ?? "" : ""
|
|
6057
5648
|
).join(" ").replace(/\s+/g, " ").trim() : "";
|
|
6058
5649
|
const media = ["Video", "Audio"].includes(type);
|
|
6059
5650
|
const timedGroup = isClipPrimitive(type) && (props.start !== void 0 || props.duration !== void 0 || props.track !== void 0 || definition?.duration !== void 0 || definition?.start !== void 0);
|
|
@@ -6090,7 +5681,7 @@ function inspectScene(text2, filename2, meta, definitions = {}) {
|
|
|
6090
5681
|
parent,
|
|
6091
5682
|
depth,
|
|
6092
5683
|
type,
|
|
6093
|
-
label: words ? `\u201C${words.slice(0, 64)}${words.length > 64 ? "\u2026" : ""}\u201D` : typeof props.src === "string" ?
|
|
5684
|
+
label: words ? `\u201C${words.slice(0, 64)}${words.length > 64 ? "\u2026" : ""}\u201D` : typeof props.src === "string" ? path5.basename(props.src) : type,
|
|
6094
5685
|
line: line(opening3),
|
|
6095
5686
|
sourceStart: opening3.getStart(ast),
|
|
6096
5687
|
start,
|
|
@@ -6145,7 +5736,7 @@ function inspectScene(text2, filename2, meta, definitions = {}) {
|
|
|
6145
5736
|
return [id];
|
|
6146
5737
|
}
|
|
6147
5738
|
for (const st of ast.statements)
|
|
6148
|
-
if (
|
|
5739
|
+
if (ts2.isExportAssignment(st)) visit(st.expression, null, 0, /* @__PURE__ */ new Map());
|
|
6149
5740
|
function mapPaths(row, runtimePath) {
|
|
6150
5741
|
row.runtimePath = runtimePath;
|
|
6151
5742
|
let index = 0;
|
|
@@ -6198,8 +5789,123 @@ function reviewManifest(text2, scene, meta, revision2) {
|
|
|
6198
5789
|
)
|
|
6199
5790
|
};
|
|
6200
5791
|
}
|
|
6201
|
-
|
|
6202
|
-
// src/cloud.ts
|
|
5792
|
+
|
|
5793
|
+
// src/cloud.ts
|
|
5794
|
+
init_runtime();
|
|
5795
|
+
|
|
5796
|
+
// src/upload-render.ts
|
|
5797
|
+
import { openAsBlob } from "node:fs";
|
|
5798
|
+
async function uploadRender(origin, project, file, metadata, log = () => {
|
|
5799
|
+
}) {
|
|
5800
|
+
const video = await openAsBlob(file, { type: metadata.contentType });
|
|
5801
|
+
if (!video.size || video.size > 2 * 1024 ** 3)
|
|
5802
|
+
throw new Error("Video must be non-empty and no larger than 2 GB.");
|
|
5803
|
+
let slot;
|
|
5804
|
+
try {
|
|
5805
|
+
log(`uploading video (${(video.size / 1048576).toFixed(1)} MB)\u2026`);
|
|
5806
|
+
slot = await call(
|
|
5807
|
+
origin,
|
|
5808
|
+
`/api/projects/${encodeURIComponent(project)}/render`,
|
|
5809
|
+
{
|
|
5810
|
+
method: "POST",
|
|
5811
|
+
body: { ...metadata, sizeInBytes: video.size }
|
|
5812
|
+
}
|
|
5813
|
+
);
|
|
5814
|
+
if (slot.type !== "success")
|
|
5815
|
+
throw new Error(slot.error ?? "The cloud did not accept the video.");
|
|
5816
|
+
const put = await fetch(slot.uploadUrl, {
|
|
5817
|
+
method: "PUT",
|
|
5818
|
+
headers: { "Content-Type": metadata.contentType },
|
|
5819
|
+
body: video,
|
|
5820
|
+
redirect: "error",
|
|
5821
|
+
signal: AbortSignal.timeout(9e5)
|
|
5822
|
+
});
|
|
5823
|
+
if (!put.ok) throw new Error(`Video upload failed (${put.status}).`);
|
|
5824
|
+
const render = slot.renderRowId;
|
|
5825
|
+
const done = await call(origin, `/api/renders/${render}/complete`, {
|
|
5826
|
+
method: "POST",
|
|
5827
|
+
body: { fileKey: slot.fileKey }
|
|
5828
|
+
});
|
|
5829
|
+
if (done.type !== "success")
|
|
5830
|
+
throw new Error(done.error ?? "The cloud did not finalize the video.");
|
|
5831
|
+
slot = null;
|
|
5832
|
+
if (done.sharingEnabled === false)
|
|
5833
|
+
throw new Error(
|
|
5834
|
+
"Video saved, but review sharing was disabled after this upload started. Upload again to restore sharing."
|
|
5835
|
+
);
|
|
5836
|
+
return {
|
|
5837
|
+
status: "deployed",
|
|
5838
|
+
project,
|
|
5839
|
+
render,
|
|
5840
|
+
link: `${origin}/share/project/${project}`,
|
|
5841
|
+
versionLink: `${origin}/share/${render}`
|
|
5842
|
+
};
|
|
5843
|
+
} finally {
|
|
5844
|
+
if (slot?.renderRowId)
|
|
5845
|
+
await call(origin, `/api/renders/${slot.renderRowId}/fail`, {
|
|
5846
|
+
method: "POST",
|
|
5847
|
+
body: { error: "deploy did not complete" }
|
|
5848
|
+
}).catch(() => void 0);
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5852
|
+
// src/cloud.ts
|
|
5853
|
+
var sourceAccessLevels = [
|
|
5854
|
+
"private",
|
|
5855
|
+
"signed-in-view",
|
|
5856
|
+
"signed-in-edit"
|
|
5857
|
+
];
|
|
5858
|
+
function parseSourceAccess(value) {
|
|
5859
|
+
if (!sourceAccessLevels.includes(value))
|
|
5860
|
+
throw new Error(
|
|
5861
|
+
"Invalid --access. Use private, signed-in-view, or signed-in-edit."
|
|
5862
|
+
);
|
|
5863
|
+
return value;
|
|
5864
|
+
}
|
|
5865
|
+
async function permissions(root, action = "get", options = {}) {
|
|
5866
|
+
if (action === "set") parseSourceAccess(options.access ?? "");
|
|
5867
|
+
if (["invite", "revoke"].includes(action) && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(options.email ?? ""))
|
|
5868
|
+
throw new Error("Provide a valid collaborator email.");
|
|
5869
|
+
if (action === "invite" && !["viewer", "editor"].includes(options.role ?? ""))
|
|
5870
|
+
throw new Error("Use --role viewer or --role editor.");
|
|
5871
|
+
if (!["get", "set", "invite", "revoke"].includes(action))
|
|
5872
|
+
throw new Error("Unknown permissions action.");
|
|
5873
|
+
const { origin, id } = await cloudOf(root);
|
|
5874
|
+
const base = `/api/store/projects/${encodeURIComponent(id)}/permissions`;
|
|
5875
|
+
if (action === "set")
|
|
5876
|
+
await call(origin, `${base}/access`, {
|
|
5877
|
+
method: "PUT",
|
|
5878
|
+
body: { access: options.access }
|
|
5879
|
+
});
|
|
5880
|
+
if (action === "invite")
|
|
5881
|
+
await call(origin, `${base}/grants`, {
|
|
5882
|
+
method: "PUT",
|
|
5883
|
+
body: { email: options.email, role: options.role }
|
|
5884
|
+
});
|
|
5885
|
+
if (action === "revoke")
|
|
5886
|
+
await call(
|
|
5887
|
+
origin,
|
|
5888
|
+
`${base}/grants/${encodeURIComponent(options.email.trim().toLowerCase())}`,
|
|
5889
|
+
{ method: "DELETE" }
|
|
5890
|
+
);
|
|
5891
|
+
const result = await call(origin, base);
|
|
5892
|
+
parseSourceAccess(result.access);
|
|
5893
|
+
return {
|
|
5894
|
+
...result,
|
|
5895
|
+
link: `${origin}/share/project/${id}`,
|
|
5896
|
+
...action === "revoke" ? {
|
|
5897
|
+
note: "Removed the direct Studio grant. Workspace, project membership, and signed-in access still apply."
|
|
5898
|
+
} : {}
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
function formatPermissions(result) {
|
|
5902
|
+
return `Source access: ${result.access}
|
|
5903
|
+
Your role: ${result.role}
|
|
5904
|
+
Review: ${result.link}` + (result.grants?.length ? `
|
|
5905
|
+
Collaborators:
|
|
5906
|
+
${result.grants.map((g) => ` ${g.email}: ${g.role}`).join("\n")}` : "") + (result.note ? `
|
|
5907
|
+
${result.note}` : "");
|
|
5908
|
+
}
|
|
6203
5909
|
var PROJECT_ID = /^[a-z0-9]{10,40}$/;
|
|
6204
5910
|
function validOrigin(value) {
|
|
6205
5911
|
try {
|
|
@@ -6420,7 +6126,8 @@ function uncommittedCount(root) {
|
|
|
6420
6126
|
(l) => l && !l.slice(3).startsWith(".osmo/") && !excluded(l.slice(3))
|
|
6421
6127
|
).length;
|
|
6422
6128
|
}
|
|
6423
|
-
async function publish(root, { name } = {}) {
|
|
6129
|
+
async function publish(root, { name, access: access3 = "private" } = {}) {
|
|
6130
|
+
parseSourceAccess(access3);
|
|
6424
6131
|
if (!hasRepo(root))
|
|
6425
6132
|
throw new Error(
|
|
6426
6133
|
"No repository in this project. Run osmo studio init first."
|
|
@@ -6438,15 +6145,21 @@ async function publish(root, { name } = {}) {
|
|
|
6438
6145
|
await record(root, "record before publish", origin);
|
|
6439
6146
|
const project = await call(origin, "/api/store/projects", {
|
|
6440
6147
|
method: "POST",
|
|
6441
|
-
body: { name: name ??
|
|
6148
|
+
body: { name: name ?? path6.basename(root), access: access3 }
|
|
6442
6149
|
});
|
|
6443
6150
|
rememberPointer(root, { origin, id: project.id });
|
|
6151
|
+
if ((project.access ?? "private") !== access3)
|
|
6152
|
+
throw new Error(
|
|
6153
|
+
"The server did not confirm the requested source access. Update the backend before deploying; the project connection has been saved."
|
|
6154
|
+
);
|
|
6444
6155
|
return {
|
|
6445
6156
|
...await sync(root, { record: false }),
|
|
6446
6157
|
id: project.id,
|
|
6447
6158
|
name: project.name,
|
|
6448
6159
|
origin,
|
|
6449
|
-
page: `${origin}/app/projects/${project.id}
|
|
6160
|
+
page: `${origin}/app/projects/${project.id}`,
|
|
6161
|
+
access: project.access ?? "private",
|
|
6162
|
+
link: `${origin}/share/project/${project.id}`
|
|
6450
6163
|
};
|
|
6451
6164
|
}
|
|
6452
6165
|
async function push(root, { log = () => {
|
|
@@ -6645,7 +6358,7 @@ async function clone(id, dir, origin = authOrigin()) {
|
|
|
6645
6358
|
throw new Error(
|
|
6646
6359
|
info.kind === "review" ? "This is a video-only review with no editable source to clone. Use osmo studio deploy to publish full projects with source and Git history." : "This project was made in the web editor. The CLI can only clone projects published from the CLI."
|
|
6647
6360
|
);
|
|
6648
|
-
await
|
|
6361
|
+
await mkdir2(dir, { recursive: true });
|
|
6649
6362
|
try {
|
|
6650
6363
|
initRepo(dir);
|
|
6651
6364
|
const pack = await call(
|
|
@@ -6668,7 +6381,7 @@ async function clone(id, dir, origin = authOrigin()) {
|
|
|
6668
6381
|
dir
|
|
6669
6382
|
};
|
|
6670
6383
|
} catch (error) {
|
|
6671
|
-
await
|
|
6384
|
+
await rm2(dir, { recursive: true, force: true });
|
|
6672
6385
|
throw error;
|
|
6673
6386
|
}
|
|
6674
6387
|
}
|
|
@@ -6791,28 +6504,28 @@ async function sync(root, {
|
|
|
6791
6504
|
return { ...result, steps };
|
|
6792
6505
|
}
|
|
6793
6506
|
function sceneMeta(source, text2) {
|
|
6794
|
-
const sf =
|
|
6507
|
+
const sf = ts3.createSourceFile(
|
|
6795
6508
|
source,
|
|
6796
6509
|
text2,
|
|
6797
|
-
|
|
6510
|
+
ts3.ScriptTarget.ES2022,
|
|
6798
6511
|
true,
|
|
6799
|
-
|
|
6512
|
+
ts3.ScriptKind.TSX
|
|
6800
6513
|
);
|
|
6801
6514
|
let meta = null;
|
|
6802
6515
|
const visit = (node) => {
|
|
6803
6516
|
if (meta) return;
|
|
6804
|
-
if ((
|
|
6517
|
+
if ((ts3.isJsxOpeningElement(node) || ts3.isJsxSelfClosingElement(node)) && node.tagName.getText() === "Scene") {
|
|
6805
6518
|
const props = {};
|
|
6806
6519
|
for (const attr of node.attributes.properties) {
|
|
6807
|
-
if (!
|
|
6520
|
+
if (!ts3.isJsxAttribute(attr) || !attr.initializer || !ts3.isJsxExpression(attr.initializer) || !attr.initializer.expression)
|
|
6808
6521
|
continue;
|
|
6809
6522
|
const e = attr.initializer.expression;
|
|
6810
|
-
if (
|
|
6523
|
+
if (ts3.isNumericLiteral(e)) props[attr.name.getText()] = Number(e.text);
|
|
6811
6524
|
}
|
|
6812
6525
|
meta = props;
|
|
6813
6526
|
return;
|
|
6814
6527
|
}
|
|
6815
|
-
|
|
6528
|
+
ts3.forEachChild(node, visit);
|
|
6816
6529
|
};
|
|
6817
6530
|
visit(sf);
|
|
6818
6531
|
if (!meta || !(meta.width > 0) || !(meta.height > 0) || !(meta.duration > 0))
|
|
@@ -6832,26 +6545,50 @@ async function deploy(root, {
|
|
|
6832
6545
|
check: check2,
|
|
6833
6546
|
log = () => {
|
|
6834
6547
|
},
|
|
6835
|
-
quiet = false
|
|
6548
|
+
quiet = false,
|
|
6549
|
+
access: access3
|
|
6836
6550
|
}) {
|
|
6551
|
+
if (access3 !== void 0) parseSourceAccess(access3);
|
|
6552
|
+
const existing = pointerFromGit(root);
|
|
6553
|
+
if (existing && access3 !== void 0)
|
|
6554
|
+
throw new Error(
|
|
6555
|
+
`This project is already published. Change access with osmo studio permissions set --access ${access3}.`
|
|
6556
|
+
);
|
|
6837
6557
|
const result = await check2(source);
|
|
6838
6558
|
if (!result.ok) throw new Error(result.errors.join("\n"));
|
|
6839
|
-
const
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6559
|
+
const currentPermissions = existing ? await call(
|
|
6560
|
+
existing.origin,
|
|
6561
|
+
`/api/store/projects/${encodeURIComponent(existing.id)}`
|
|
6562
|
+
) : void 0;
|
|
6563
|
+
if (currentPermissions?.role === "viewer")
|
|
6564
|
+
throw new Error(
|
|
6565
|
+
"This project is read-only. Ask its owner for editor access before deploying."
|
|
6566
|
+
);
|
|
6567
|
+
let effectiveAccess = currentPermissions?.access ?? access3 ?? "private";
|
|
6568
|
+
const cacheDir = path6.join(root, ".osmo/cache");
|
|
6569
|
+
await mkdir2(cacheDir, { recursive: true });
|
|
6570
|
+
const out = path6.join(cacheDir, `deploy-${Date.now()}.mp4`);
|
|
6843
6571
|
log("rendering\u2026");
|
|
6844
6572
|
try {
|
|
6845
6573
|
await run(nativeBinary(), [source, "--export", out, "--supersample", "2"], {
|
|
6846
6574
|
env: environment(),
|
|
6847
6575
|
stdio: quiet ? ["ignore", "ignore", "inherit"] : "inherit"
|
|
6848
6576
|
});
|
|
6577
|
+
if (!existing) {
|
|
6578
|
+
const published = await publish(root, { access: effectiveAccess });
|
|
6579
|
+
effectiveAccess = published.access;
|
|
6580
|
+
if (!["pushed", "up-to-date"].includes(published.status))
|
|
6581
|
+
throw new Error(
|
|
6582
|
+
`Project created but initial synchronization failed (${published.status}); run osmo studio sync before deploying again.`
|
|
6583
|
+
);
|
|
6584
|
+
}
|
|
6585
|
+
const { origin, id } = await cloudOf(root);
|
|
6849
6586
|
if (!isClean(root)) await record(root, `deploy: ${scene}`, origin);
|
|
6850
6587
|
const pushed = await push(root, { log });
|
|
6851
6588
|
if (pushed.status !== "pushed" && pushed.status !== "up-to-date")
|
|
6852
6589
|
return { ...pushed, hint: "osmo studio sync, then deploy again" };
|
|
6853
6590
|
const head = headOf(root);
|
|
6854
|
-
const sceneText = await
|
|
6591
|
+
const sceneText = await readFile2(source, "utf8");
|
|
6855
6592
|
const meta = sceneMeta(source, sceneText);
|
|
6856
6593
|
const review = reviewManifest(sceneText, scene, meta, head);
|
|
6857
6594
|
const uploaded = await uploadRender(
|
|
@@ -6867,7 +6604,7 @@ async function deploy(root, {
|
|
|
6867
6604
|
reviewManifest: review,
|
|
6868
6605
|
contentType: "video/mp4",
|
|
6869
6606
|
source: "cli",
|
|
6870
|
-
name: `${
|
|
6607
|
+
name: `${path6.basename(root)}.mp4`,
|
|
6871
6608
|
gitCommit: head
|
|
6872
6609
|
},
|
|
6873
6610
|
log
|
|
@@ -6875,6 +6612,7 @@ async function deploy(root, {
|
|
|
6875
6612
|
const renderId = uploaded.render;
|
|
6876
6613
|
return {
|
|
6877
6614
|
status: "deployed",
|
|
6615
|
+
access: effectiveAccess,
|
|
6878
6616
|
head,
|
|
6879
6617
|
link: `${origin}/share/project/${id}`,
|
|
6880
6618
|
versionLink: `${origin}/share/${renderId}`,
|
|
@@ -6884,7 +6622,7 @@ async function deploy(root, {
|
|
|
6884
6622
|
uploaded: pushed.uploaded ?? 0
|
|
6885
6623
|
};
|
|
6886
6624
|
} finally {
|
|
6887
|
-
await
|
|
6625
|
+
await rm2(out, { force: true });
|
|
6888
6626
|
}
|
|
6889
6627
|
}
|
|
6890
6628
|
async function publicJson(url) {
|
|
@@ -6960,85 +6698,310 @@ async function cloudCommentAction(root, id, action, text2) {
|
|
|
6960
6698
|
const { origin } = await cloudOf(root);
|
|
6961
6699
|
return reviewCommentAction(origin, comment, action, text2);
|
|
6962
6700
|
}
|
|
6963
|
-
async function reviewCommentAction(origin, comment, action, text2) {
|
|
6964
|
-
const post = async (route, body2, allowMissing = false) => {
|
|
6965
|
-
const response = await fetch(`${origin}${route}`, {
|
|
6966
|
-
method: "POST",
|
|
6967
|
-
headers: { "Content-Type": "application/json" },
|
|
6968
|
-
body: JSON.stringify(body2),
|
|
6969
|
-
signal: AbortSignal.timeout(3e4)
|
|
6970
|
-
});
|
|
6971
|
-
if (allowMissing && response.status === 404) return null;
|
|
6972
|
-
if (!response.ok)
|
|
6973
|
-
throw new Error(
|
|
6974
|
-
`Cloud ${response.status}: ${(await response.json().catch(() => ({}))).error ?? "request failed"}`
|
|
6975
|
-
);
|
|
6976
|
-
return response.json().catch(() => ({}));
|
|
6977
|
-
};
|
|
6978
|
-
if (action === "reply") {
|
|
6979
|
-
const me = await currentAccount(origin);
|
|
6980
|
-
const name = me.user.name || me.user.email;
|
|
6981
|
-
const reply = await post(
|
|
6982
|
-
`/api/renders/${comment.render}/review-comments`,
|
|
6983
|
-
{
|
|
6984
|
-
scene: comment.scene,
|
|
6985
|
-
revision: comment.revision,
|
|
6986
|
-
time: comment.time,
|
|
6987
|
-
text: text2,
|
|
6988
|
-
author: { name: name.slice(0, 80) },
|
|
6989
|
-
parentId: comment.id
|
|
6990
|
-
},
|
|
6991
|
-
true
|
|
6701
|
+
async function reviewCommentAction(origin, comment, action, text2) {
|
|
6702
|
+
const post = async (route, body2, allowMissing = false) => {
|
|
6703
|
+
const response = await fetch(`${origin}${route}`, {
|
|
6704
|
+
method: "POST",
|
|
6705
|
+
headers: { "Content-Type": "application/json" },
|
|
6706
|
+
body: JSON.stringify(body2),
|
|
6707
|
+
signal: AbortSignal.timeout(3e4)
|
|
6708
|
+
});
|
|
6709
|
+
if (allowMissing && response.status === 404) return null;
|
|
6710
|
+
if (!response.ok)
|
|
6711
|
+
throw new Error(
|
|
6712
|
+
`Cloud ${response.status}: ${(await response.json().catch(() => ({}))).error ?? "request failed"}`
|
|
6713
|
+
);
|
|
6714
|
+
return response.json().catch(() => ({}));
|
|
6715
|
+
};
|
|
6716
|
+
if (action === "reply") {
|
|
6717
|
+
const me = await currentAccount(origin);
|
|
6718
|
+
const name = me.user.name || me.user.email;
|
|
6719
|
+
const reply = await post(
|
|
6720
|
+
`/api/renders/${comment.render}/review-comments`,
|
|
6721
|
+
{
|
|
6722
|
+
scene: comment.scene,
|
|
6723
|
+
revision: comment.revision,
|
|
6724
|
+
time: comment.time,
|
|
6725
|
+
text: text2,
|
|
6726
|
+
author: { name: name.slice(0, 80) },
|
|
6727
|
+
parentId: comment.id
|
|
6728
|
+
},
|
|
6729
|
+
true
|
|
6730
|
+
);
|
|
6731
|
+
if (reply === null)
|
|
6732
|
+
await post(`/api/renders/${comment.render}/comments`, {
|
|
6733
|
+
body: text2,
|
|
6734
|
+
frame: comment.frame,
|
|
6735
|
+
authorName: name,
|
|
6736
|
+
parentId: comment.id
|
|
6737
|
+
});
|
|
6738
|
+
return { id: comment.id, status: comment.status };
|
|
6739
|
+
}
|
|
6740
|
+
await post(`/api/renders/${comment.render}/comments/${comment.id}/resolve`, {
|
|
6741
|
+
resolved: action === "resolve"
|
|
6742
|
+
});
|
|
6743
|
+
return { id: comment.id, status: action === "resolve" ? "resolved" : "open" };
|
|
6744
|
+
}
|
|
6745
|
+
function format(result) {
|
|
6746
|
+
const steps = result.steps?.length ? `
|
|
6747
|
+
${result.steps.join(" \u2192 ")}` : "";
|
|
6748
|
+
const pending = result.uncommitted ? `
|
|
6749
|
+
${result.uncommitted} uncommitted file${result.uncommitted === 1 ? "" : "s"} not included; run osmo studio sync to record and push them.` : "";
|
|
6750
|
+
return formatStatus(result) + steps + pending;
|
|
6751
|
+
}
|
|
6752
|
+
function formatStatus(result) {
|
|
6753
|
+
switch (result.status) {
|
|
6754
|
+
case "pushed":
|
|
6755
|
+
return `Pushed ${result.commits} revision${result.commits === 1 ? "" : "s"} (${result.objects} objects${result.uploaded ? `, ${result.uploaded} media file${result.uploaded === 1 ? "" : "s"}` : ""}). Head ${result.head.slice(0, 12)}.`;
|
|
6756
|
+
case "fast-forward":
|
|
6757
|
+
return `Pulled ${result.revisions} revision${result.revisions === 1 ? "" : "s"}. Head ${result.head.slice(0, 12)}.`;
|
|
6758
|
+
case "up-to-date":
|
|
6759
|
+
return `Up to date${result.head ? ` at ${result.head.slice(0, 12)}` : ""}.`;
|
|
6760
|
+
case "refused":
|
|
6761
|
+
return `Refused: ${result.reason}. Cloud is at ${result.cloud?.slice(0, 12)}, you are at ${result.head.slice(0, 12)}. Run ${result.hint}.`;
|
|
6762
|
+
case "invalid":
|
|
6763
|
+
return `The cloud refused the revision: ${result.reason}.
|
|
6764
|
+
${result.hint === "osmo studio sync" ? "Run osmo studio sync: it folds your unpushed revisions into one and tries again." : `Then ${result.hint}.`}`;
|
|
6765
|
+
case "diverged":
|
|
6766
|
+
return `Diverged: you are at ${result.head.slice(0, 12)}, the cloud at ${result.cloud.slice(0, 12)}. Run ${result.hint}.`;
|
|
6767
|
+
case "conflict":
|
|
6768
|
+
return `Conflict: the cloud's changes overlap yours. ${result.hint}.`;
|
|
6769
|
+
case "history":
|
|
6770
|
+
return result.log;
|
|
6771
|
+
case "deployed":
|
|
6772
|
+
return `Source access: ${result.access}
|
|
6773
|
+
Deployed ${result.head.slice(0, 12)} (${result.pushed}${result.commits ? `, ${result.commits} revision${result.commits === 1 ? "" : "s"}` : ""}).
|
|
6774
|
+
Review: ${result.link}`;
|
|
6775
|
+
default:
|
|
6776
|
+
return JSON.stringify(result);
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6780
|
+
// src/cli.ts
|
|
6781
|
+
init_comments();
|
|
6782
|
+
|
|
6783
|
+
// src/features.ts
|
|
6784
|
+
function studioEnabled(env = process.env) {
|
|
6785
|
+
return env.OSMO_STUDIO_ENABLED === "1";
|
|
6786
|
+
}
|
|
6787
|
+
function requireStudio() {
|
|
6788
|
+
if (!studioEnabled())
|
|
6789
|
+
throw Object.assign(
|
|
6790
|
+
new Error("Osmo Studio commands are not available in this release."),
|
|
6791
|
+
{ code: "feature_disabled" }
|
|
6792
|
+
);
|
|
6793
|
+
}
|
|
6794
|
+
|
|
6795
|
+
// src/help.ts
|
|
6796
|
+
function cliHelp(version2, studio) {
|
|
6797
|
+
const common = `Osmo ${version2}
|
|
6798
|
+
|
|
6799
|
+
osmo login [--name COMPUTER] [--json]
|
|
6800
|
+
osmo login status SESSION [--json]
|
|
6801
|
+
osmo whoami [--json]
|
|
6802
|
+
osmo logout [--json]
|
|
6803
|
+
osmo resources list [--category shader] [--json]
|
|
6804
|
+
osmo resources show ID [--version VERSION] [--preset PRESET] [--json]
|
|
6805
|
+
osmo resources add ID [--version VERSION] [--preset PRESET] [--json]
|
|
6806
|
+
osmo resources import FILE [--json]
|
|
6807
|
+
osmo resources local [--json]
|
|
6808
|
+
osmo resources examples [--json]
|
|
6809
|
+
osmo share --video FILE.mp4 [--project ID] [--name NAME] [--json]
|
|
6810
|
+
osmo unshare --project ID [--json]
|
|
6811
|
+
osmo comments list --project ID [--open] [--json]
|
|
6812
|
+
osmo comments reply ID "message" --project ID
|
|
6813
|
+
osmo comments resolve ID --project ID
|
|
6814
|
+
osmo comments reopen ID --project ID`;
|
|
6815
|
+
const studioCommands = `osmo studio init [folder]
|
|
6816
|
+
osmo studio publish [--name NAME] [--access private|signed-in-view|signed-in-edit]
|
|
6817
|
+
osmo studio push
|
|
6818
|
+
osmo studio pull
|
|
6819
|
+
osmo studio sync
|
|
6820
|
+
osmo studio clone PROJECT_ID folder
|
|
6821
|
+
osmo studio history
|
|
6822
|
+
osmo studio open [scene.tsx] [--port NUMBER] [--no-open]
|
|
6823
|
+
osmo studio open animations/player.tsx [--width 800 --height 450 --duration 4]
|
|
6824
|
+
osmo studio check [scene.tsx] [--json]
|
|
6825
|
+
osmo studio render [scene.tsx] --out animation.mp4 [--supersample 1|2]
|
|
6826
|
+
osmo studio render [scene.tsx] --frame NUMBER --out frame.png [--supersample 1|2]
|
|
6827
|
+
osmo studio deploy [scene.tsx] [--access private|signed-in-view|signed-in-edit]
|
|
6828
|
+
osmo studio permissions [--json]
|
|
6829
|
+
osmo studio permissions set --access private|signed-in-view|signed-in-edit [--json]
|
|
6830
|
+
osmo studio permissions invite EMAIL --role viewer|editor [--json]
|
|
6831
|
+
osmo studio permissions revoke EMAIL [--json]
|
|
6832
|
+
osmo studio comments list [--open] [--scene scene.tsx] [--json]
|
|
6833
|
+
osmo studio comments start ID [EMOJI]
|
|
6834
|
+
osmo studio comments stop ID
|
|
6835
|
+
osmo studio comments react ID [EMOJI|none]
|
|
6836
|
+
osmo studio comments reply ID "message"
|
|
6837
|
+
osmo studio comments resolve ID
|
|
6838
|
+
osmo studio comments reopen ID`;
|
|
6839
|
+
const video = "osmo share --video: share a rendered video from any tool, including drafts and work in progress. Video review and version history only; no editable source. Requires login and ffprobe, not init or Git. Without --project, creates a new review.";
|
|
6840
|
+
return `${common}${studio ? "\n" + studioCommands : ""}
|
|
6841
|
+
|
|
6842
|
+
${studio ? "Choose the sharing workflow:\n " : ""}${video}${studio ? "\n osmo studio deploy: publish a full-featured editable Osmo project with source files, assets, and tracked Git history, plus a review video. Requires osmo studio init and osmo login. First deploy creates the cloud project; source access defaults to private. Later deploys preserve permissions; use permissions set to change them. Prefer this command for Osmo Studio projects. Edit locally with Studio or code; the cloud review is read-only." : ""}
|
|
6843
|
+
${studio ? "\nNew animation: run osmo studio init, read AGENTS.md, then resources list --json before authoring.\n" : ""}
|
|
6844
|
+
osmo unshare: disable a project's public review and version links, preserving
|
|
6845
|
+
videos, comments, and history. Sharing a new video to the same project restores its review links.
|
|
6846
|
+
Previously obtained direct media URLs may still work.
|
|
6847
|
+
|
|
6848
|
+
Run project commands from the initialized project folder. Native renderer included
|
|
6849
|
+
in release archives; Node 22 and FFmpeg/ffprobe are prerequisites.
|
|
6850
|
+
Cloud commands need osmo login; OSMO_BASE_URL picks the cloud (default studio.osmo.inc).
|
|
6851
|
+
`;
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
// src/init.ts
|
|
6855
|
+
import {
|
|
6856
|
+
mkdir as mkdir5,
|
|
6857
|
+
readFile as readFile6,
|
|
6858
|
+
writeFile as writeFile5,
|
|
6859
|
+
copyFile,
|
|
6860
|
+
cp,
|
|
6861
|
+
access as access2,
|
|
6862
|
+
mkdtemp,
|
|
6863
|
+
rm as rm5
|
|
6864
|
+
} from "node:fs/promises";
|
|
6865
|
+
import path10 from "node:path";
|
|
6866
|
+
|
|
6867
|
+
// src/authoring.ts
|
|
6868
|
+
init_runtime();
|
|
6869
|
+
import { readFile as readFile4, writeFile as writeFile3, rm as rm4 } from "node:fs/promises";
|
|
6870
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6871
|
+
import path8 from "node:path";
|
|
6872
|
+
var begin = "<!-- osmo:begin -->";
|
|
6873
|
+
var end = "<!-- osmo:end -->";
|
|
6874
|
+
var legacyPointer = "For Osmo animation tasks, read osmo.md before editing. Use osmo studio check, and read open Studio comments.";
|
|
6875
|
+
var legacyGuideHash = "815dae34cd7e5e6f44a575a88d80516d3736f9b74c76b9379fc09144c6aa06fb";
|
|
6876
|
+
var authoringGuide = `${begin}
|
|
6877
|
+
## Osmo Studio (${version})
|
|
6878
|
+
|
|
6879
|
+
Project setup: osmo studio init [folder]. During internal preview, Studio commands require OSMO_STUDIO_ENABLED=1.
|
|
6880
|
+
|
|
6881
|
+
TSX is the source of truth. Use the installed @osmo/scene Strict API; do not edit node_modules or the renderer.
|
|
6882
|
+
|
|
6883
|
+
### Check resources first
|
|
6884
|
+
At the start of every animation task, run osmo resources list --json. Inspect
|
|
6885
|
+
plausible matches with osmo resources show ID --json, including their previews.
|
|
6886
|
+
Prefer a suitable resource over recreating it; install only resources you use with
|
|
6887
|
+
osmo resources add ID --preset PRESET --json. If login is required, run osmo login
|
|
6888
|
+
--json and share the approval link. If the catalog is unavailable, say so and
|
|
6889
|
+
continue locally. Use the CLI compatibility result; older manifests may describe
|
|
6890
|
+
an earlier CLI. After install, use its local files, types, preset values and usage.
|
|
6891
|
+
|
|
6892
|
+
### Choose the primitive first
|
|
6893
|
+
| Visual pattern | Start with |
|
|
6894
|
+
| --- | --- |
|
|
6895
|
+
| Typography following a circle, curve or figure-eight | One Text with path; animate path.progress |
|
|
6896
|
+
| Elements moving/rotating together | Group transforms and shared motion tracks |
|
|
6897
|
+
| Panels, circles, borders or thin lines | Group fill/stroke/shape/rounding |
|
|
6898
|
+
| Vector curves, icons, cutouts or illustrations | Path with SVG d, fill and stroke; evenodd for holes |
|
|
6899
|
+
| Images, footage, soundtrack | Image, Video, Audio with local asset paths |
|
|
6900
|
+
| Parallax and solid cards | Camera, z and Group.depth |
|
|
6901
|
+
| Content on a 3D model | Model with Surface children; inspect GLB selectors first |
|
|
6902
|
+
| Procedural backgrounds or liquid glass | Check resources; Shader with local src and explicit uniforms; glass samples backdrop |
|
|
6903
|
+
| Softness or texture | effects={[blur({ radius: 5 }), grain({ amount: 0.04 })]} |
|
|
6904
|
+
|
|
6905
|
+
Prototype the native primitive before detailed tracking; explain a tested limitation before custom reconstruction. No approval step needed.
|
|
6906
|
+
### Layout before coordinates
|
|
6907
|
+
- Use Group row/column, align, justify, gap and padding for labels/cards/icon rows; position places the whole group.
|
|
6908
|
+
- Center labels with Text width/height, align="center", verticalAlign="center". Default bounds are the line box; opt into alignBounds="glyphs" for visible-letter centering. Changing text can shift glyph bounds.
|
|
6909
|
+
- Fit icons into equal Path width/height with fit="contain". Supply viewBox for the asset viewport, or omit it for tight geometry bounds, including negative origins. Fitting scales strokes; fill stretches; unsized paths retain coordinates.
|
|
6910
|
+
- Text and fitted paths can overflow; use a clipping Group to crop. See the sizing JSDoc before applying manual offsets.
|
|
6911
|
+
|
|
6912
|
+
### API reference: types with short explanations
|
|
6913
|
+
Read relevant declarations in node_modules/@osmo/scene/dist/ (JSDoc explains usage):
|
|
6914
|
+
- index.d.ts: public exports; elements.d.ts: all primitives, props and examples.
|
|
6915
|
+
- motion.d.ts: defineMotion, keyframes and interpolation.
|
|
6916
|
+
- knobs.d.ts + knob-schema.d.ts: Group controls, typed values and UI metadata.
|
|
6917
|
+
- effects.d.ts: built-in effects and the broader library effect API.
|
|
6918
|
+
|
|
6919
|
+
### Strict rules
|
|
6920
|
+
- Named imports from @osmo/scene; export default <Scene ...> in the scene entry.
|
|
6921
|
+
- Reuse a local default-exported Group: import Card from './animations/card'; <Card track={0} values={{ title: 'Hello' }} />. No children/knobs replacement; imports stay Strict.
|
|
6922
|
+
- Root Group/Text/Image/Video/Audio/Shader/Path/Rect/Model clips require literal track (0\u201399). start/duration are optional seconds; same-track clips cannot overlap. Tracks do not change paint order. Nested layers omit track. Motions use local clip time; trimming never stretches motion or changes media trimIn/playbackRate.
|
|
6923
|
+
- Optional names: Group label="Orbiting circles" (max two words); Scene tracks={{0:{label:"Shapes"}}} (one word per lane). Labels are static; track names stay with lanes. Keep stable IDs separate. Group hidden hides visuals; solo isolates siblings. Scene.tracks also accepts hidden (visuals), muted (audio), solo (isolate lanes); these affect preview and export.
|
|
6924
|
+
- Camera/Surface are not clips. Group sizing: "fill" follows parent, "auto" fits flow content.
|
|
6925
|
+
- Groups clip children to their shape by default. Set clip={false} to allow overflow.
|
|
6926
|
+
- Preview Group files with studio open --width 640 --height 360; storyboard.canvas.json stores per-card placement, size and literal knob values, referencing the original TSX.
|
|
6927
|
+
- Use explicit JSX, literal values/arrays/objects, and const defineMotion or defineKnobs declarations. Bind tracks/knobs directly to compatible props.
|
|
6928
|
+
- No arbitrary expressions, loops, callbacks, spreads, import aliases, custom components, helper calls or computed variables. Only blur({...}) and grain({...}) effect calls are allowed in JSX; pass an explicit object, even for defaults.
|
|
6929
|
+
- Shader.src must be a static relative local .wgsl path. Put complete uniform values inline; numeric uniforms can use motion/knob tracks. defineEffect remains unsupported.
|
|
6930
|
+
- Keyframe at values are increasing local seconds (scene unless inside a timed clip); easing belongs to the arriving key. Add a key at 0 to define a continuous track's starting value. Tracks do not loop automatically.
|
|
6931
|
+
- Group knobs can drive many descendant props; Group.values accepts motion tracks.
|
|
6932
|
+
- Units: pixels, degrees, seconds; +z toward viewer. Asset/font paths are relative to their declaring file.
|
|
6933
|
+
- Text.path handles the complete string; no width/align/lineHeight or line breaks. SVG geometry is static; progress and ellipse radii can animate.
|
|
6934
|
+
- Path draws SVG d in local pixels; use scale to resize. Geometry stays static; transforms, stroke width and effects can animate. Use fill="transparent" for lines.
|
|
6935
|
+
|
|
6936
|
+
### Workflow
|
|
6937
|
+
1. Read relevant types, edit the scene, then run osmo studio check scene.tsx.
|
|
6938
|
+
2. Preview: osmo studio open scene.tsx. Keep the server running; saves reload it. Reopening reuses the project URL, saved in gitignored .osmo/studio.json.
|
|
6939
|
+
3. Export: osmo studio render scene.tsx --out exports/animation.mp4 (single PNG: --frame 90 --out exports/frame.png, zero-based; use a new filename if one exists). Share for review: osmo studio deploy (after osmo login and osmo studio publish) renders, pushes editable source/assets and Git history, and prints a review link. Prefer this command for Osmo Studio projects. osmo share --video FILE.mp4 is only for sharing a rendered video from any tool, including drafts and work in progress: video versions and comments, no editable source; it needs login and ffprobe but no init, Git, or project.
|
|
6940
|
+
4. When asked to apply feedback: osmo studio comments list --open --json. Check the scene/timestamp and any older revision; edit and validate the fix. If complete and verified, reply only "Done." with osmo studio comments reply ID "Done." then run osmo studio comments resolve ID. Do not leave routine fixes open for review. Keep open only for ambiguity, an incomplete fix, or a specific uncertainty needing user verification. Reply with one short sentence stating the blocker or what to check.
|
|
6941
|
+
${end}
|
|
6942
|
+
`;
|
|
6943
|
+
async function optional(file) {
|
|
6944
|
+
try {
|
|
6945
|
+
return await readFile4(file, "utf8");
|
|
6946
|
+
} catch (error) {
|
|
6947
|
+
if (error.code === "ENOENT") return "";
|
|
6948
|
+
throw error;
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
async function writeAuthoringGuide(root) {
|
|
6952
|
+
const file = path8.join(root, "AGENTS.md");
|
|
6953
|
+
const original = await optional(file);
|
|
6954
|
+
const hasBegin = original.includes(begin), hasEnd = original.includes(end);
|
|
6955
|
+
if (hasBegin !== hasEnd || hasBegin && original.indexOf(end) < original.indexOf(begin))
|
|
6956
|
+
throw new Error(
|
|
6957
|
+
"AGENTS.md has an incomplete Osmo section; repair its osmo:begin/osmo:end markers before running init."
|
|
6992
6958
|
);
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
});
|
|
7000
|
-
return { id: comment.id, status: comment.status };
|
|
6959
|
+
let text2;
|
|
6960
|
+
if (hasBegin)
|
|
6961
|
+
text2 = original.slice(0, original.indexOf(begin)) + authoringGuide.trimEnd() + original.slice(original.indexOf(end) + end.length);
|
|
6962
|
+
else {
|
|
6963
|
+
const rest = original.split("\n").filter((line) => line !== legacyPointer).join("\n");
|
|
6964
|
+
text2 = rest + (rest && !rest.endsWith("\n\n") ? rest.endsWith("\n") ? "\n" : "\n\n" : "") + authoringGuide;
|
|
7001
6965
|
}
|
|
7002
|
-
await
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
const pending = result.uncommitted ? `
|
|
7011
|
-
${result.uncommitted} uncommitted file${result.uncommitted === 1 ? "" : "s"} not included; run osmo studio sync to record and push them.` : "";
|
|
7012
|
-
return formatStatus(result) + steps + pending;
|
|
6966
|
+
if (text2 !== original) await writeFile3(file, text2);
|
|
6967
|
+
const legacy = await optional(path8.join(root, "osmo.md"));
|
|
6968
|
+
if (legacy && createHash3("sha256").update(legacy).digest("hex") === legacyGuideHash)
|
|
6969
|
+
await rm4(path8.join(root, "osmo.md"));
|
|
6970
|
+
else if (legacy)
|
|
6971
|
+
console.log(
|
|
6972
|
+
"Existing osmo.md was preserved because it differs from the original generated guide. Osmo instructions now live in AGENTS.md."
|
|
6973
|
+
);
|
|
7013
6974
|
}
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
6975
|
+
|
|
6976
|
+
// src/init.ts
|
|
6977
|
+
import os2 from "node:os";
|
|
6978
|
+
|
|
6979
|
+
// src/starter.ts
|
|
6980
|
+
init_runtime();
|
|
6981
|
+
import { access, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
6982
|
+
import path9 from "node:path";
|
|
6983
|
+
var template = path9.join(packageRoot, "src/starter/");
|
|
6984
|
+
async function writeStarter(root) {
|
|
6985
|
+
const scene = path9.join(root, "scene.tsx");
|
|
6986
|
+
try {
|
|
6987
|
+
await access(scene);
|
|
6988
|
+
return;
|
|
6989
|
+
} catch (error) {
|
|
6990
|
+
if (error.code !== "ENOENT") throw error;
|
|
6991
|
+
}
|
|
6992
|
+
await mkdir4(path9.join(root, "animations"), { recursive: true });
|
|
6993
|
+
try {
|
|
6994
|
+
await writeFile4(scene, await readFile5(path9.join(template, "scene.tsx")), {
|
|
6995
|
+
flag: "wx"
|
|
6996
|
+
});
|
|
6997
|
+
} catch (error) {
|
|
6998
|
+
if (error.code !== "EEXIST") throw error;
|
|
7038
6999
|
}
|
|
7039
7000
|
}
|
|
7040
7001
|
|
|
7041
7002
|
// src/init.ts
|
|
7003
|
+
init_runtime();
|
|
7004
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7042
7005
|
async function exists(p) {
|
|
7043
7006
|
try {
|
|
7044
7007
|
await access2(p);
|
|
@@ -7049,7 +7012,7 @@ async function exists(p) {
|
|
|
7049
7012
|
}
|
|
7050
7013
|
async function create(p, value) {
|
|
7051
7014
|
try {
|
|
7052
|
-
await
|
|
7015
|
+
await writeFile5(p, value, { flag: "wx" });
|
|
7053
7016
|
return true;
|
|
7054
7017
|
} catch (e) {
|
|
7055
7018
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -7058,7 +7021,7 @@ async function create(p, value) {
|
|
|
7058
7021
|
}
|
|
7059
7022
|
async function init(root = process.cwd()) {
|
|
7060
7023
|
nativeBinary();
|
|
7061
|
-
const gitProbe = await mkdtemp(
|
|
7024
|
+
const gitProbe = await mkdtemp(path10.join(os2.tmpdir(), "osmo-git-check-"));
|
|
7062
7025
|
try {
|
|
7063
7026
|
const result = spawnSync3(
|
|
7064
7027
|
"git",
|
|
@@ -7070,7 +7033,7 @@ async function init(root = process.cwd()) {
|
|
|
7070
7033
|
"Osmo projects require Git with SHA-256 support (Git 2.29+). Install or update Git, then retry osmo studio init. Sharing an existing MP4 with osmo share --video does not require Git."
|
|
7071
7034
|
);
|
|
7072
7035
|
} finally {
|
|
7073
|
-
await
|
|
7036
|
+
await rm5(gitProbe, { recursive: true, force: true });
|
|
7074
7037
|
}
|
|
7075
7038
|
for (const [name, variable] of [
|
|
7076
7039
|
["ffmpeg", "OSMO_FFMPEG"],
|
|
@@ -7095,15 +7058,15 @@ async function init(root = process.cwd()) {
|
|
|
7095
7058
|
);
|
|
7096
7059
|
}
|
|
7097
7060
|
}
|
|
7098
|
-
root =
|
|
7099
|
-
await
|
|
7061
|
+
root = path10.resolve(root);
|
|
7062
|
+
await mkdir5(root, { recursive: true });
|
|
7100
7063
|
if (hasRepo(root) && objectFormat(root) !== "sha256")
|
|
7101
7064
|
throw new Error(
|
|
7102
7065
|
`${root} is already a git repository with ${objectFormat(root) ?? "unknown"} objects. Osmo projects are SHA-256 repositories: run osmo studio init in a new folder.`
|
|
7103
7066
|
);
|
|
7104
|
-
const manifestPath =
|
|
7105
|
-
const manifest2 = await exists(manifestPath) ? JSON.parse(await
|
|
7106
|
-
name:
|
|
7067
|
+
const manifestPath = path10.join(root, "package.json");
|
|
7068
|
+
const manifest2 = await exists(manifestPath) ? JSON.parse(await readFile6(manifestPath, "utf8")) : {
|
|
7069
|
+
name: path10.basename(root).toLowerCase().replace(/[^a-z0-9-]/g, "-") || "osmo-project",
|
|
7107
7070
|
private: true,
|
|
7108
7071
|
type: "module"
|
|
7109
7072
|
};
|
|
@@ -7113,58 +7076,58 @@ async function init(root = process.cwd()) {
|
|
|
7113
7076
|
throw new Error(
|
|
7114
7077
|
`Existing @osmo/scene dependency (${previous}) was preserved. Initialize a new folder for this release.`
|
|
7115
7078
|
);
|
|
7116
|
-
const configFile =
|
|
7079
|
+
const configFile = path10.join(root, ".osmo/project.json");
|
|
7117
7080
|
if (await exists(configFile)) {
|
|
7118
|
-
const config = JSON.parse(await
|
|
7081
|
+
const config = JSON.parse(await readFile6(configFile, "utf8"));
|
|
7119
7082
|
assertCompatibleProject(config);
|
|
7120
7083
|
}
|
|
7121
|
-
const vendor =
|
|
7122
|
-
await
|
|
7123
|
-
await
|
|
7124
|
-
const target =
|
|
7084
|
+
const vendor = path10.join(root, ".osmo/vendor");
|
|
7085
|
+
await mkdir5(vendor, { recursive: true });
|
|
7086
|
+
await mkdir5(path10.join(root, "assets"), { recursive: true });
|
|
7087
|
+
const target = path10.join(vendor, `osmo-scene-${sceneVersion}.tgz`);
|
|
7125
7088
|
let refreshScene = false;
|
|
7126
|
-
const bundled =
|
|
7089
|
+
const bundled = path10.join(
|
|
7127
7090
|
packageRoot,
|
|
7128
7091
|
"vendor",
|
|
7129
7092
|
`osmo-scene-${sceneVersion}.tgz`
|
|
7130
7093
|
);
|
|
7131
7094
|
if (await exists(bundled)) {
|
|
7132
|
-
if (!await exists(target) || !(await
|
|
7095
|
+
if (!await exists(target) || !(await readFile6(target)).equals(await readFile6(bundled))) {
|
|
7133
7096
|
await copyFile(bundled, target);
|
|
7134
7097
|
refreshScene = true;
|
|
7135
7098
|
}
|
|
7136
7099
|
} else {
|
|
7137
|
-
const scratch = await mkdtemp(
|
|
7100
|
+
const scratch = await mkdtemp(path10.join(os2.tmpdir(), "osmo-scene-pack-"));
|
|
7138
7101
|
try {
|
|
7139
7102
|
await run("npm", ["run", "build"], {
|
|
7140
|
-
cwd:
|
|
7103
|
+
cwd: path10.join(runtimeRoot, "packages/scene")
|
|
7141
7104
|
});
|
|
7142
|
-
const sceneRoot =
|
|
7105
|
+
const sceneRoot = path10.join(runtimeRoot, "packages/scene");
|
|
7143
7106
|
const { scripts, devDependencies, ...scenePackage } = JSON.parse(
|
|
7144
|
-
await
|
|
7107
|
+
await readFile6(path10.join(sceneRoot, "package.json"), "utf8")
|
|
7145
7108
|
);
|
|
7146
|
-
await
|
|
7147
|
-
|
|
7109
|
+
await writeFile5(
|
|
7110
|
+
path10.join(scratch, "package.json"),
|
|
7148
7111
|
JSON.stringify({ ...scenePackage, files: ["dist"] }, null, 2)
|
|
7149
7112
|
);
|
|
7150
|
-
await cp(
|
|
7113
|
+
await cp(path10.join(sceneRoot, "dist"), path10.join(scratch, "dist"), {
|
|
7151
7114
|
recursive: true
|
|
7152
7115
|
});
|
|
7153
7116
|
await run("npm", ["pack", "--pack-destination", scratch], {
|
|
7154
7117
|
cwd: scratch
|
|
7155
7118
|
});
|
|
7156
|
-
const packed =
|
|
7157
|
-
if (!await exists(target) || !(await
|
|
7119
|
+
const packed = path10.join(scratch, `osmo-scene-${sceneVersion}.tgz`);
|
|
7120
|
+
if (!await exists(target) || !(await readFile6(target)).equals(await readFile6(packed))) {
|
|
7158
7121
|
await copyFile(packed, target);
|
|
7159
7122
|
refreshScene = true;
|
|
7160
7123
|
}
|
|
7161
7124
|
} finally {
|
|
7162
|
-
await
|
|
7125
|
+
await rm5(scratch, { recursive: true, force: true });
|
|
7163
7126
|
}
|
|
7164
7127
|
}
|
|
7165
7128
|
if (!previous) {
|
|
7166
7129
|
manifest2.dependencies = { ...manifest2.dependencies, "@osmo/scene": spec };
|
|
7167
|
-
await
|
|
7130
|
+
await writeFile5(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
7168
7131
|
}
|
|
7169
7132
|
const tsconfig = JSON.stringify(
|
|
7170
7133
|
{
|
|
@@ -7183,7 +7146,7 @@ async function init(root = process.cwd()) {
|
|
|
7183
7146
|
null,
|
|
7184
7147
|
2
|
|
7185
7148
|
) + "\n";
|
|
7186
|
-
await create(
|
|
7149
|
+
await create(path10.join(root, "tsconfig.json"), tsconfig);
|
|
7187
7150
|
await writeStarter(root);
|
|
7188
7151
|
await create(
|
|
7189
7152
|
configFile,
|
|
@@ -7194,8 +7157,8 @@ async function init(root = process.cwd()) {
|
|
|
7194
7157
|
) + "\n"
|
|
7195
7158
|
);
|
|
7196
7159
|
await writeAuthoringGuide(root);
|
|
7197
|
-
const ignorePath =
|
|
7198
|
-
const ignore = await exists(ignorePath) ? await
|
|
7160
|
+
const ignorePath = path10.join(root, ".gitignore");
|
|
7161
|
+
const ignore = await exists(ignorePath) ? await readFile6(ignorePath, "utf8") : "";
|
|
7199
7162
|
const rules = [
|
|
7200
7163
|
"node_modules/",
|
|
7201
7164
|
"exports/",
|
|
@@ -7212,7 +7175,7 @@ async function init(root = process.cwd()) {
|
|
|
7212
7175
|
];
|
|
7213
7176
|
const missing = rules.filter((rule) => !ignore.split("\n").includes(rule));
|
|
7214
7177
|
if (missing.length)
|
|
7215
|
-
await
|
|
7178
|
+
await writeFile5(
|
|
7216
7179
|
ignorePath,
|
|
7217
7180
|
ignore + (ignore && !ignore.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n"
|
|
7218
7181
|
);
|
|
@@ -7237,11 +7200,6 @@ Node 22 is required. Video/audio and MP4 export require FFmpeg and ffprobe on PA
|
|
|
7237
7200
|
);
|
|
7238
7201
|
}
|
|
7239
7202
|
|
|
7240
|
-
// src/cli.ts
|
|
7241
|
-
init_check();
|
|
7242
|
-
init_runtime();
|
|
7243
|
-
init_comments();
|
|
7244
|
-
|
|
7245
7203
|
// src/resources.ts
|
|
7246
7204
|
import { createHash as createHash5 } from "node:crypto";
|
|
7247
7205
|
import {
|
|
@@ -7253,7 +7211,7 @@ import {
|
|
|
7253
7211
|
rename as rename3,
|
|
7254
7212
|
rm as rm6
|
|
7255
7213
|
} from "node:fs/promises";
|
|
7256
|
-
import
|
|
7214
|
+
import path13 from "node:path";
|
|
7257
7215
|
import { unzipSync } from "fflate";
|
|
7258
7216
|
init_runtime();
|
|
7259
7217
|
init_project_resources();
|
|
@@ -7411,7 +7369,7 @@ function unpack(zip, meta) {
|
|
|
7411
7369
|
invalid();
|
|
7412
7370
|
return { files, manifest: manifest2 };
|
|
7413
7371
|
}
|
|
7414
|
-
async function
|
|
7372
|
+
async function stat(file) {
|
|
7415
7373
|
try {
|
|
7416
7374
|
return await lstat2(file);
|
|
7417
7375
|
} catch (error) {
|
|
@@ -7420,14 +7378,14 @@ async function stat2(file) {
|
|
|
7420
7378
|
}
|
|
7421
7379
|
}
|
|
7422
7380
|
async function install(root, id, files) {
|
|
7423
|
-
const folder =
|
|
7381
|
+
const folder = path13.join(root, "resources");
|
|
7424
7382
|
await mkdir6(folder, { recursive: true });
|
|
7425
7383
|
if (!(await lstat2(folder)).isDirectory())
|
|
7426
7384
|
fail(
|
|
7427
7385
|
"resource_conflict",
|
|
7428
7386
|
"resources must be a real directory, not a symlink."
|
|
7429
7387
|
);
|
|
7430
|
-
const lock =
|
|
7388
|
+
const lock = path13.join(folder, `.${id}.install.lock`);
|
|
7431
7389
|
try {
|
|
7432
7390
|
await mkdir6(lock);
|
|
7433
7391
|
} catch (error) {
|
|
@@ -7440,12 +7398,12 @@ async function install(root, id, files) {
|
|
|
7440
7398
|
}
|
|
7441
7399
|
let stage2;
|
|
7442
7400
|
try {
|
|
7443
|
-
const destination =
|
|
7401
|
+
const destination = path13.join(folder, id), existing = await stat(destination);
|
|
7444
7402
|
if (existing) {
|
|
7445
7403
|
if (!existing.isDirectory())
|
|
7446
7404
|
fail("resource_conflict", `resources/${id} must be a real directory.`);
|
|
7447
7405
|
for (const name of names) {
|
|
7448
|
-
const file =
|
|
7406
|
+
const file = path13.join(destination, name), info = await stat(file);
|
|
7449
7407
|
if (!info?.isFile() || !(await readFile7(file)).equals(Buffer.from(files[name])))
|
|
7450
7408
|
fail(
|
|
7451
7409
|
"resource_conflict",
|
|
@@ -7454,10 +7412,10 @@ async function install(root, id, files) {
|
|
|
7454
7412
|
}
|
|
7455
7413
|
return "already_installed";
|
|
7456
7414
|
}
|
|
7457
|
-
stage2 = await mkdtemp2(
|
|
7415
|
+
stage2 = await mkdtemp2(path13.join(folder, `.${id}-`));
|
|
7458
7416
|
for (const name of names)
|
|
7459
|
-
await writeFile6(
|
|
7460
|
-
if (await
|
|
7417
|
+
await writeFile6(path13.join(stage2, name), files[name], { flag: "wx" });
|
|
7418
|
+
if (await stat(destination))
|
|
7461
7419
|
fail(
|
|
7462
7420
|
"resource_conflict",
|
|
7463
7421
|
`resources/${id} appeared during installation. Existing files were preserved.`
|
|
@@ -7557,6 +7515,141 @@ ${result.compatibility.note}
|
|
|
7557
7515
|
${result.next}`;
|
|
7558
7516
|
}
|
|
7559
7517
|
|
|
7518
|
+
// src/cli.ts
|
|
7519
|
+
init_runtime();
|
|
7520
|
+
|
|
7521
|
+
// src/video-deploy.ts
|
|
7522
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
7523
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
7524
|
+
import path14 from "node:path";
|
|
7525
|
+
import { promisify as promisify2 } from "node:util";
|
|
7526
|
+
var exec2 = promisify2(execFile2);
|
|
7527
|
+
function projectId(value) {
|
|
7528
|
+
if (typeof value !== "string" || !/^[a-z0-9]{10,40}$/.test(value))
|
|
7529
|
+
throw new Error("Provide a valid cloud project ID with --project.");
|
|
7530
|
+
return value;
|
|
7531
|
+
}
|
|
7532
|
+
async function unshare(project) {
|
|
7533
|
+
projectId(project);
|
|
7534
|
+
return call(
|
|
7535
|
+
authOrigin(),
|
|
7536
|
+
`/api/projects/${project}/share`,
|
|
7537
|
+
{ method: "DELETE" }
|
|
7538
|
+
);
|
|
7539
|
+
}
|
|
7540
|
+
async function probeVideo(file) {
|
|
7541
|
+
const info = await stat2(file).catch(() => null);
|
|
7542
|
+
if (!info?.isFile() || !info.size || info.size > 2 * 1024 ** 3)
|
|
7543
|
+
throw new Error(
|
|
7544
|
+
"Video must be an existing, non-empty file no larger than 2 GB."
|
|
7545
|
+
);
|
|
7546
|
+
let data;
|
|
7547
|
+
try {
|
|
7548
|
+
const { stdout } = await exec2(
|
|
7549
|
+
process.env.OSMO_FFPROBE ?? "ffprobe",
|
|
7550
|
+
["-v", "error", "-show_streams", "-show_format", "-of", "json", file],
|
|
7551
|
+
{ timeout: 3e4, maxBuffer: 2 * 1024 ** 2 }
|
|
7552
|
+
);
|
|
7553
|
+
data = JSON.parse(stdout);
|
|
7554
|
+
} catch (error) {
|
|
7555
|
+
if (error.code === "ENOENT")
|
|
7556
|
+
throw new Error(
|
|
7557
|
+
"ffprobe is required to inspect videos. Install FFmpeg (which includes ffprobe), then retry."
|
|
7558
|
+
);
|
|
7559
|
+
throw new Error(
|
|
7560
|
+
"Cannot read this video. Provide a playable H.264 MP4 file."
|
|
7561
|
+
);
|
|
7562
|
+
}
|
|
7563
|
+
const videos = (data.streams ?? []).filter(
|
|
7564
|
+
(s) => s.codec_type === "video" && !s.disposition?.attached_pic
|
|
7565
|
+
);
|
|
7566
|
+
const video = videos[0], audio = (data.streams ?? []).filter((s) => s.codec_type === "audio");
|
|
7567
|
+
if (!data.format?.format_name?.split(",").includes("mp4") || videos.length !== 1 || video?.codec_name !== "h264" || !["yuv420p", "yuvj420p"].includes(
|
|
7568
|
+
video?.pix_fmt
|
|
7569
|
+
) || audio.some((s) => s.codec_name !== "aac"))
|
|
7570
|
+
throw new Error(
|
|
7571
|
+
"Use an H.264 MP4 with 8-bit 4:2:0 video and AAC audio (or no audio). Export a compatible MP4 from your video tool, then retry."
|
|
7572
|
+
);
|
|
7573
|
+
const [n, d = "1"] = String(video.avg_frame_rate).split("/");
|
|
7574
|
+
const fps = Number(n) / Number(d), durationSeconds = Number(video.duration ?? data.format.duration);
|
|
7575
|
+
const rotation = Number(
|
|
7576
|
+
video.side_data_list?.find((s) => s.rotation !== void 0)?.rotation ?? video.tags?.rotate ?? 0
|
|
7577
|
+
);
|
|
7578
|
+
const portrait = Math.abs(rotation % 180) === 90;
|
|
7579
|
+
const width = portrait ? video.height : video.width, height = portrait ? video.width : video.height;
|
|
7580
|
+
if (![fps, durationSeconds, width, height].every(
|
|
7581
|
+
(x) => Number.isFinite(x) && x > 0
|
|
7582
|
+
) || fps > 1e3 || durationSeconds * fps > 2147483647)
|
|
7583
|
+
throw new Error("Video has invalid or unsupported timing or dimensions.");
|
|
7584
|
+
return {
|
|
7585
|
+
codec: "h264",
|
|
7586
|
+
contentType: "video/mp4",
|
|
7587
|
+
// The check above found both finite and positive.
|
|
7588
|
+
compositionWidth: width,
|
|
7589
|
+
compositionHeight: height,
|
|
7590
|
+
fps,
|
|
7591
|
+
durationSeconds,
|
|
7592
|
+
durationInFrames: Math.max(1, Math.round(durationSeconds * fps))
|
|
7593
|
+
};
|
|
7594
|
+
}
|
|
7595
|
+
async function shareVideo(file, {
|
|
7596
|
+
project,
|
|
7597
|
+
name,
|
|
7598
|
+
log = () => {
|
|
7599
|
+
}
|
|
7600
|
+
} = {}) {
|
|
7601
|
+
if (project !== void 0) projectId(project);
|
|
7602
|
+
if (name !== void 0 && (!name.trim() || name.trim().length > 200))
|
|
7603
|
+
throw new Error("--name must contain 1\u2013200 characters.");
|
|
7604
|
+
const source = path14.resolve(file), metadata = await probeVideo(source), origin = authOrigin();
|
|
7605
|
+
await currentAccount(origin);
|
|
7606
|
+
const title = name?.trim() ?? path14.basename(source).slice(0, 200);
|
|
7607
|
+
const id = project ?? (await call(origin, "/api/review-projects", {
|
|
7608
|
+
method: "POST",
|
|
7609
|
+
body: { name: title }
|
|
7610
|
+
})).id;
|
|
7611
|
+
try {
|
|
7612
|
+
return await uploadRender(
|
|
7613
|
+
origin,
|
|
7614
|
+
id,
|
|
7615
|
+
source,
|
|
7616
|
+
{ ...metadata, source: "video", name: title },
|
|
7617
|
+
log
|
|
7618
|
+
);
|
|
7619
|
+
} catch (error) {
|
|
7620
|
+
throw new Error(
|
|
7621
|
+
`${error.message}
|
|
7622
|
+
Retry with --project ${id} to use the same review project.`
|
|
7623
|
+
);
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
async function videoComments(project, { open = false } = {}) {
|
|
7627
|
+
projectId(project);
|
|
7628
|
+
const origin = authOrigin(), comments = [];
|
|
7629
|
+
let cursor;
|
|
7630
|
+
do {
|
|
7631
|
+
const page = await call(
|
|
7632
|
+
origin,
|
|
7633
|
+
`/api/projects/${project}/share/versions${cursor ? `?before=${encodeURIComponent(cursor)}` : ""}`
|
|
7634
|
+
);
|
|
7635
|
+
for (const version2 of page.versions) {
|
|
7636
|
+
const data = await call(
|
|
7637
|
+
origin,
|
|
7638
|
+
`/api/renders/${version2.id}/review-comments`
|
|
7639
|
+
);
|
|
7640
|
+
comments.push(
|
|
7641
|
+
...data.comments.filter((c) => !open || c.status === "open").map((c) => ({
|
|
7642
|
+
...c,
|
|
7643
|
+
render: version2.id,
|
|
7644
|
+
link: `${origin}/share/${version2.id}`
|
|
7645
|
+
}))
|
|
7646
|
+
);
|
|
7647
|
+
}
|
|
7648
|
+
cursor = page.nextCursor;
|
|
7649
|
+
} while (cursor);
|
|
7650
|
+
return comments;
|
|
7651
|
+
}
|
|
7652
|
+
|
|
7560
7653
|
// src/cli.ts
|
|
7561
7654
|
var help = cliHelp(version, studioEnabled());
|
|
7562
7655
|
try {
|
|
@@ -7576,6 +7669,8 @@ try {
|
|
|
7576
7669
|
category: { type: "string" },
|
|
7577
7670
|
preset: { type: "string" },
|
|
7578
7671
|
name: { type: "string" },
|
|
7672
|
+
access: { type: "string" },
|
|
7673
|
+
role: { type: "string" },
|
|
7579
7674
|
port: { type: "string" },
|
|
7580
7675
|
"no-open": { type: "boolean" },
|
|
7581
7676
|
out: { type: "string" },
|
|
@@ -7593,6 +7688,15 @@ try {
|
|
|
7593
7688
|
throw new Error(
|
|
7594
7689
|
"Use --video with osmo share, and --project with osmo share, osmo unshare, or osmo comments." + (studioEnabled() ? " For full editable projects, use osmo studio deploy." : "")
|
|
7595
7690
|
);
|
|
7691
|
+
if (!flags.help && flags.access !== void 0) {
|
|
7692
|
+
if (args[0] !== "studio" || !(["deploy", "publish"].includes(args[1]) || args[1] === "permissions" && args[2] === "set"))
|
|
7693
|
+
throw new Error(
|
|
7694
|
+
"--access is supported only by studio publish, first deploy, and permissions set."
|
|
7695
|
+
);
|
|
7696
|
+
parseSourceAccess(flags.access);
|
|
7697
|
+
}
|
|
7698
|
+
if (!flags.help && flags.role !== void 0 && !(args[0] === "studio" && args[1] === "permissions" && args[2] === "invite"))
|
|
7699
|
+
throw new Error("--role is supported only by studio permissions invite.");
|
|
7596
7700
|
if (flags.version && !resourceMode) console.log(version);
|
|
7597
7701
|
else if (flags.help || !args.length) console.log(help);
|
|
7598
7702
|
else if (["login", "whoami", "logout"].includes(args[0])) {
|
|
@@ -7660,6 +7764,21 @@ ${c.text}${c.replies.map((r) => "\n \u21B3 " + r.text).join("")}`
|
|
|
7660
7764
|
flags.json ? JSON.stringify(result) : `Cloned ${result.name} (${result.revisions} revision${result.revisions === 1 ? "" : "s"}) into ${args[3]}.
|
|
7661
7765
|
Next: cd ${args[3]} && osmo studio open scene.tsx`
|
|
7662
7766
|
);
|
|
7767
|
+
} else if (args[0] === "studio" && args[1] === "permissions") {
|
|
7768
|
+
const action = args[2] ?? "get";
|
|
7769
|
+
const valid = action === "get" && args.length <= 3 || action === "set" && args.length === 3 && flags.access || ["invite", "revoke"].includes(action) && args.length === 4;
|
|
7770
|
+
if (!valid)
|
|
7771
|
+
throw new Error(
|
|
7772
|
+
"Usage: osmo studio permissions [--json] | set --access LEVEL | invite EMAIL --role viewer|editor | revoke EMAIL"
|
|
7773
|
+
);
|
|
7774
|
+
const result = await permissions(projectRoot(), action, {
|
|
7775
|
+
access: flags.access,
|
|
7776
|
+
email: args[3],
|
|
7777
|
+
role: flags.role
|
|
7778
|
+
});
|
|
7779
|
+
console.log(
|
|
7780
|
+
flags.json ? JSON.stringify(result) : formatPermissions(result)
|
|
7781
|
+
);
|
|
7663
7782
|
} else if (args[0] === "studio" && ["publish", "push", "pull", "sync", "history"].includes(args[1])) {
|
|
7664
7783
|
if (args.length !== 2)
|
|
7665
7784
|
throw new Error(
|
|
@@ -7668,7 +7787,10 @@ Next: cd ${args[3]} && osmo studio open scene.tsx`
|
|
|
7668
7787
|
const root = projectRoot();
|
|
7669
7788
|
let result;
|
|
7670
7789
|
if (args[1] === "publish")
|
|
7671
|
-
result = await publish(root, {
|
|
7790
|
+
result = await publish(root, {
|
|
7791
|
+
name: flags.name,
|
|
7792
|
+
access: flags.access === void 0 ? void 0 : parseSourceAccess(flags.access)
|
|
7793
|
+
});
|
|
7672
7794
|
else if (args[1] === "push")
|
|
7673
7795
|
result = await push(root, { log: flags.json ? () => {
|
|
7674
7796
|
} : console.log });
|
|
@@ -7680,6 +7802,8 @@ Next: cd ${args[3]} && osmo studio open scene.tsx`
|
|
|
7680
7802
|
if (result)
|
|
7681
7803
|
console.log(
|
|
7682
7804
|
flags.json ? JSON.stringify(result) : args[1] === "publish" ? `Published as ${result.id}. Page: ${result.page}
|
|
7805
|
+
Source access: ${result.access}
|
|
7806
|
+
Review: ${result.link}
|
|
7683
7807
|
${format(result)}` : format(result)
|
|
7684
7808
|
);
|
|
7685
7809
|
if (result && ["refused", "diverged", "conflict", "invalid"].includes(result.status))
|
|
@@ -7782,7 +7906,8 @@ ${c.text}${c.replies.map((r) => "\n \u21B3 " + r.text).join("")}`
|
|
|
7782
7906
|
check,
|
|
7783
7907
|
log: flags.json ? () => {
|
|
7784
7908
|
} : console.error,
|
|
7785
|
-
quiet: !!flags.json
|
|
7909
|
+
quiet: !!flags.json,
|
|
7910
|
+
access: flags.access === void 0 ? void 0 : parseSourceAccess(flags.access)
|
|
7786
7911
|
});
|
|
7787
7912
|
console.log(flags.json ? JSON.stringify(result) : format(result));
|
|
7788
7913
|
if (result.status !== "deployed") process.exitCode = 1;
|