@vosjs/cli 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ vos logout # remove ~/.config/vos/credentials
51
51
  vos render <config.json|url|take> [out] # config → video; the name picks mp4/webm; a program document's sound is mixed in
52
52
  vos still <config.json|url> [out.webp] # config → frames: --times 0,1.5,50% in one browser; .png/.jpg re-encoded
53
53
  vos info <config.json|url> # inspect a config
54
- vos check <config.json|url> # migrate → schema → syntax → compile → determinism and dialect lints, all local
54
+ vos check <config.json|url> # migrate → schema → syntax → compile → determinism, dialect and declared-file lints, all local
55
55
  vos preview <config.json|url> [--port N] # serve a local playback page
56
56
  vos versions # installed @vosjs/* versions
57
57
  ```
@@ -334,6 +334,8 @@ vos recipe push <FILE.md> --folder <slug> | --asset <id> # the one recipe writ
334
334
 
335
335
  `push` is polymorphic by a deterministic sniff, never a flag: a take directory (a `doc.json` carrying `source`) pushes recording and document through the take pipeline; a `config.json` (or a directory holding one) pushes the program, and a `doc.json` beside it that carries `program` (a program document: overlays, objects, audio, speed, tween edits, its own length; `program.config` omitted on disk) rides along, lint-gated. `fetch` and `pull` write a program document back the same way. `pull` takes `--since <versionId>` (walk the changelog from that base instead of the tracked one) and `--check` (print what changed and stop: nothing on disk moves) on both paths. A program push LANDS where `vos.json` says: a directory that tracks a vos iterates it, so the fetch, edit, check, push, pull loop needs no flags, and making something separate from the same config is the explicit `--remix-of` door. Program pushes take `--vos`, `--title`, `--slug`, `--desc`, `--tags`, `--folder`, `--remix-of`, `--base`, `--label`, `--note`, `--override <id>` (repeatable) and `--claimable`; take pushes take `--title`, `--label`, `--note`, `--folder`, `--override` and `--yes`. Both take `--wait`. A new vos pushed without `--folder` lands unfiled, at the root of the shelf, and the done line says so.
336
336
 
337
+ **A program's files are declared, never typed into a function.** `config.json` names each file it uses in `assets` (`"assets": { "logo": { "ref": "./logo.png", "kind": "image" }, "shots": { "ref": ["./shots/1.png", "./shots/2.png"] } }`) and reads `ctx.assets.logo`, or `"$assets.logo"` in an element, object or font. On disk a `ref` is a path beside the config, so `vos render`, `vos still` and `vos preview` serve the file to the page from disk. `vos push` uploads each local file (content-addressed, so a re-push sends nothing) and what it stores names the hosted file as `asset:<id>`, the one spelling the platform resolves for every render; the config on disk keeps its paths. `vos fetch --media` brings a program's hosted files home under `assets/` and re-points the manifest at them; without `--media` the manifest keeps `asset:<id>` and a local render reads each file with your key. `vos check` says when a `"$assets.<name>"` names nothing, when a declared path is not a file, and when a function names a hosted file in code instead of the manifest: a file named only in code does not load on a render of a private program and does not follow a remix. A `--claimable` push carries no files, so it is refused when the manifest names local ones.
338
+
337
339
  **Sound is a track, never an element.** A document's added sound lives in `doc.json` (`audio: [{ id, key, name, start, in, out, duration, gain, fadeIn, fadeOut }]`) for a program as for a take: `vos push` uploads each local file its keys name and keys the hosted asset; `vos render` of a program mixes it locally; the studio shows it as a track with gain and fades. An `audio` element embedded in the config is not a track (renders do not mix it, and a `data:` source can push a config past what a claimable push carries); `vos check` says so. `vos asset push score.ogg` uploads a sound on its own and prints the url a key takes; an unfiled sound that no document uses is removed after 7 days, so pass `--folder` or reference it.
338
340
 
339
341
  Both paths share the same base tracking and the same two 409 shapes. `stale_base` replays the platform's typed changelog: run `vos pull`, re-apply, push again. `protected_conflict` lists nodes a human edited in the studio: keep their values, or re-push with `--override <id>` only when the user asked for that exact change. The first push of a take asks before uploading (`--yes` for headless); agents never upload unprompted. The take's duration rides the upload, and the platform refuses a take over the hosted recording cap. Every push should carry `--label` (what changed, one line) and `--note` (why: the user's ask); the version history reads as a conversation, and an unlabelled push is a turn the human cannot read.
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ hostedLiteralWarnings,
4
+ localManifestRefs,
5
+ pullManifest,
6
+ uploadManifest
7
+ } from "./chunk-PUXY32AP.js";
8
+ import "./chunk-36P5RREC.js";
9
+ export {
10
+ hostedLiteralWarnings,
11
+ localManifestRefs,
12
+ pullManifest,
13
+ uploadManifest
14
+ };
15
+ //# sourceMappingURL=assetManifest-MM5WGOWG.js.map
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/flagUse.ts
4
+ var given = /* @__PURE__ */ new Set();
5
+ var used = /* @__PURE__ */ new Set();
6
+ function trackFlags(bag) {
7
+ return new Proxy(bag, {
8
+ get(target, key, receiver) {
9
+ if (typeof key === "string") used.add(key);
10
+ return Reflect.get(target, key, receiver);
11
+ },
12
+ has(target, key) {
13
+ if (typeof key === "string") used.add(key);
14
+ return Reflect.has(target, key);
15
+ },
16
+ // hasOwnProperty.call(flags, name) lands here.
17
+ getOwnPropertyDescriptor(target, key) {
18
+ if (typeof key === "string") used.add(key);
19
+ return Reflect.getOwnPropertyDescriptor(target, key);
20
+ },
21
+ // A spread, Object.keys or JSON.stringify hands every flag onward; what
22
+ // happens to them after that cannot be seen, so they all count as read.
23
+ ownKeys(target) {
24
+ for (const key of Reflect.ownKeys(target))
25
+ if (typeof key === "string") used.add(key);
26
+ return Reflect.ownKeys(target);
27
+ }
28
+ });
29
+ }
30
+ function noteGivenFlag(name) {
31
+ given.add(name);
32
+ }
33
+ function unusedFlags() {
34
+ return [...given].filter((name) => !used.has(name) && name !== "help");
35
+ }
36
+ function helpFlagsFor(help, verb) {
37
+ const out = /* @__PURE__ */ new Set();
38
+ let inVerb = false;
39
+ for (const line of help.split("\n")) {
40
+ const head = /^\s{2}vos\s+(\S+)/.exec(line);
41
+ if (head) inVerb = head[1] === verb;
42
+ else if (!/^\s{3,}\S/.test(line)) inVerb = false;
43
+ if (!inVerb) continue;
44
+ for (const m of line.matchAll(/--([a-z][a-z0-9-]*)/g)) out.add(m[1]);
45
+ }
46
+ return [...out];
47
+ }
48
+ function editDistance(a, b) {
49
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
50
+ for (let i = 1; i <= a.length; i++) {
51
+ let prev = row[0];
52
+ row[0] = i;
53
+ for (let j = 1; j <= b.length; j++) {
54
+ const keep = row[j];
55
+ row[j] = Math.min(
56
+ row[j] + 1,
57
+ row[j - 1] + 1,
58
+ prev + (a[i - 1] === b[j - 1] ? 0 : 1)
59
+ );
60
+ prev = keep;
61
+ }
62
+ }
63
+ return row[b.length];
64
+ }
65
+ function nearestFlag(name, candidates) {
66
+ let best = null;
67
+ let bestScore = Infinity;
68
+ for (const c of candidates) {
69
+ if (c === name) continue;
70
+ const d = c.includes(name) || name.includes(c) ? 1 : editDistance(name, c);
71
+ if (d < bestScore) {
72
+ bestScore = d;
73
+ best = c;
74
+ }
75
+ }
76
+ return best !== null && bestScore <= Math.max(2, Math.floor(name.length / 3)) ? best : null;
77
+ }
78
+ function unusedFlagsMessage(verb, documented) {
79
+ const unused = unusedFlags();
80
+ if (!unused.length) return null;
81
+ const parts = unused.map((name) => {
82
+ const near = nearestFlag(name, documented);
83
+ return `--${name}${near ? ` (did you mean --${near}?)` : ""}`;
84
+ });
85
+ const list = documented.length ? ` It reads: ${documented.map((f) => `--${f}`).join(" ")}.` : "";
86
+ return `vos ${verb} RAN, and ignored ${parts.join(", ")}: ${unused.length > 1 ? "they are" : "it is"} not something this verb reads, so what it printed above was made without ${unused.length > 1 ? "them" : "it"}.${list}`;
87
+ }
88
+
89
+ // src/args.ts
90
+ var UsageError = class extends Error {
91
+ };
92
+ function parseArgs(argv, booleanFlags) {
93
+ const positionals = [];
94
+ const flags = {};
95
+ for (let i = 0; i < argv.length; i++) {
96
+ const arg = argv[i];
97
+ if (arg === "--") {
98
+ positionals.push(...argv.slice(i + 1));
99
+ break;
100
+ }
101
+ if (arg.startsWith("--")) {
102
+ const eq = arg.indexOf("=");
103
+ if (eq !== -1) {
104
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
105
+ continue;
106
+ }
107
+ const name = arg.slice(2);
108
+ if (booleanFlags.has(name)) {
109
+ flags[name] = true;
110
+ continue;
111
+ }
112
+ const next = argv[i + 1];
113
+ if (next === void 0 || next.startsWith("--")) {
114
+ throw new UsageError(`--${name} expects a value`);
115
+ }
116
+ flags[name] = next;
117
+ i++;
118
+ continue;
119
+ }
120
+ positionals.push(arg);
121
+ }
122
+ for (const name of Object.keys(flags)) noteGivenFlag(name);
123
+ return { positionals, flags: trackFlags(flags) };
124
+ }
125
+ function numFlag(flags, name, fallback) {
126
+ const v = flags[name];
127
+ if (v === void 0) return fallback;
128
+ const n = Number(v);
129
+ if (!Number.isFinite(n))
130
+ throw new UsageError(`--${name} expects a number, got "${String(v)}"`);
131
+ return n;
132
+ }
133
+
134
+ // src/programAssets.ts
135
+ import { createHash } from "crypto";
136
+ import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
137
+ import { tmpdir } from "os";
138
+ import { basename, isAbsolute, join, resolve } from "path";
139
+ var ASSET_ROUTE = "/__assets/";
140
+ var HOSTED_REF = /^(?:asset:|(?:https?:\/\/[^/]+)?\/api\/assets\/)([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})(?:\/file(?:\?.*)?)?$/;
141
+ function hostedAssetId(ref) {
142
+ return HOSTED_REF.exec(ref)?.[1] ?? null;
143
+ }
144
+ function isUrl(ref) {
145
+ return /^(https?:|data:|blob:)/.test(ref);
146
+ }
147
+ function manifestOf(config) {
148
+ const assets = config.assets;
149
+ if (!assets || typeof assets !== "object" || Array.isArray(assets))
150
+ return null;
151
+ return Object.keys(assets).length > 0 ? assets : null;
152
+ }
153
+ function manifestRefs(config) {
154
+ const out = [];
155
+ for (const [name, decl] of Object.entries(manifestOf(config) ?? {})) {
156
+ if (Array.isArray(decl.ref)) {
157
+ decl.ref.forEach((ref, index) => out.push({ name, index, ref }));
158
+ } else if (typeof decl.ref === "string") {
159
+ out.push({ name, index: null, ref: decl.ref });
160
+ }
161
+ }
162
+ return out;
163
+ }
164
+ async function fetchHosted(id, opts) {
165
+ const dir = opts.cacheDir ?? join(tmpdir(), "vos-assets");
166
+ const file = join(dir, id);
167
+ if (existsSync(file) && statSync(file).size > 0) return file;
168
+ if (!opts.origin || !opts.key) {
169
+ opts.log(
170
+ `note: asset ${id} is hosted and no credential is set, so the render cannot load it (vos login, or keep the file beside the config and name its path)`
171
+ );
172
+ return null;
173
+ }
174
+ const res = await fetch(`${opts.origin}/api/assets/${id}/file`, {
175
+ headers: { authorization: `Bearer ${opts.key}` }
176
+ });
177
+ if (!res.ok) {
178
+ opts.log(
179
+ `note: asset ${id} could not be read (${res.status}), so the render cannot load it`
180
+ );
181
+ return null;
182
+ }
183
+ mkdirSync(dir, { recursive: true });
184
+ writeFileSync(file, new Uint8Array(await res.arrayBuffer()));
185
+ return file;
186
+ }
187
+ async function programAssets(config, opts) {
188
+ const refs = manifestRefs(config);
189
+ if (refs.length === 0) return null;
190
+ const files = {};
191
+ const served = /* @__PURE__ */ new Map();
192
+ const serve = (file) => {
193
+ const known = served.get(file);
194
+ if (known) return known;
195
+ const tag = createHash("sha256").update(file).digest("hex").slice(0, 10);
196
+ const path = `${ASSET_ROUTE}${tag}/${basename(file)}`;
197
+ files[path] = file;
198
+ served.set(file, path);
199
+ return path;
200
+ };
201
+ const reach = async (ref, where) => {
202
+ if (isUrl(ref)) return ref;
203
+ const hosted = hostedAssetId(ref);
204
+ if (hosted) {
205
+ const file2 = await fetchHosted(hosted, opts);
206
+ return file2 ? serve(file2) : ref;
207
+ }
208
+ const file = isAbsolute(ref) ? ref : resolve(opts.baseDir, ref);
209
+ if (!existsSync(file) || !statSync(file).isFile()) {
210
+ opts.log(
211
+ `note: ${where} names ${ref}, which is not a file beside the config, so the render cannot load it`
212
+ );
213
+ return ref;
214
+ }
215
+ return serve(file);
216
+ };
217
+ const assets = {};
218
+ for (const { name, index, ref } of refs) {
219
+ const url = await reach(
220
+ ref,
221
+ index === null ? `assets.${name}` : `assets.${name}[${index}]`
222
+ );
223
+ if (index === null) assets[name] = url;
224
+ else {
225
+ const list = assets[name] ??= [];
226
+ list[index] = url;
227
+ }
228
+ }
229
+ return { assets, files };
230
+ }
231
+ function missingManifestFiles(config, baseDir) {
232
+ const out = [];
233
+ for (const { name, index, ref } of manifestRefs(config)) {
234
+ if (isUrl(ref) || hostedAssetId(ref)) continue;
235
+ const file = isAbsolute(ref) ? ref : resolve(baseDir, ref);
236
+ if (existsSync(file) && statSync(file).isFile()) continue;
237
+ const where = index === null ? `assets.${name}` : `assets.${name}[${index}]`;
238
+ out.push(`${where} names ${ref}, which is not a file beside the config`);
239
+ }
240
+ return out;
241
+ }
242
+
243
+ // src/plugin/container.ts
244
+ var OCTET_STREAM = "application/octet-stream";
245
+ var TYPES_BY_EXTENSION = {
246
+ svg: "image/svg+xml",
247
+ png: "image/png",
248
+ jpg: "image/jpeg",
249
+ jpeg: "image/jpeg",
250
+ webp: "image/webp",
251
+ gif: "image/gif",
252
+ avif: "image/avif",
253
+ webm: "video/webm",
254
+ mp4: "video/mp4",
255
+ mov: "video/quicktime",
256
+ mp3: "audio/mpeg",
257
+ wav: "audio/wav",
258
+ ogg: "audio/ogg",
259
+ m4a: "audio/mp4",
260
+ glb: "model/gltf-binary"
261
+ };
262
+ var EXTENSION_BY_TYPE = {
263
+ "image/jpeg": ".jpg",
264
+ "video/mp4": ".mp4",
265
+ "audio/mp4": ".m4a"
266
+ };
267
+ var TAKE_MEDIA_EXTENSIONS = [
268
+ ".webm",
269
+ ".mp4",
270
+ ".mov",
271
+ ".m4a",
272
+ ".mp3",
273
+ ".ogg",
274
+ ".wav"
275
+ ];
276
+ var MEDIA_HEAD_BYTES = 16;
277
+ var ISO_BRANDS = {
278
+ "qt ": "video/quicktime",
279
+ "M4A ": "audio/mp4",
280
+ "M4B ": "audio/mp4",
281
+ avif: "image/avif",
282
+ avis: "image/avif"
283
+ };
284
+ var RIFF_FORMS = {
285
+ WEBP: "image/webp",
286
+ WAVE: "audio/wav",
287
+ "AVI ": "video/x-msvideo"
288
+ };
289
+ function sniffMediaType(head) {
290
+ const ascii = (at, len) => head.length >= at + len ? String.fromCharCode(...head.subarray(at, at + len)) : "";
291
+ const startsWith = (...bytes) => bytes.every((b, i) => head[i] === b);
292
+ if (startsWith(26, 69, 223, 163)) return "video/webm";
293
+ if (ascii(4, 4) === "ftyp") return ISO_BRANDS[ascii(8, 4)] ?? "video/mp4";
294
+ if (ascii(0, 4) === "RIFF") return RIFF_FORMS[ascii(8, 4)] ?? null;
295
+ if (startsWith(137, 80, 78, 71, 13, 10, 26, 10))
296
+ return "image/png";
297
+ if (startsWith(255, 216, 255)) return "image/jpeg";
298
+ if (ascii(0, 4) === "GIF8") return "image/gif";
299
+ if (ascii(0, 4) === "OggS") return "audio/ogg";
300
+ if (ascii(0, 3) === "ID3") return "audio/mpeg";
301
+ if (ascii(0, 4) === "glTF") return "model/gltf-binary";
302
+ return null;
303
+ }
304
+ function bareType(value) {
305
+ const type = (value ?? "").split(";")[0].trim().toLowerCase();
306
+ return !type || type === OCTET_STREAM || type === "binary/octet-stream" ? "" : type;
307
+ }
308
+ function mediaContentType(file) {
309
+ const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
310
+ return TYPES_BY_EXTENSION[ext] ?? OCTET_STREAM;
311
+ }
312
+ function extensionFor(contentType) {
313
+ const type = (contentType ?? "").split(";")[0].trim().toLowerCase();
314
+ if (EXTENSION_BY_TYPE[type]) return EXTENSION_BY_TYPE[type];
315
+ const ext = Object.keys(TYPES_BY_EXTENSION).find(
316
+ (e) => TYPES_BY_EXTENSION[e] === type
317
+ );
318
+ return ext ? `.${ext}` : "";
319
+ }
320
+ function resolveMediaType(claims) {
321
+ const sniffed = claims.head?.length ? sniffMediaType(claims.head) : null;
322
+ if (sniffed) return { type: sniffed, from: "bytes" };
323
+ const declared = bareType(claims.declared);
324
+ if (declared) return { type: declared, from: "declared" };
325
+ const named = mediaContentType(claims.filename ?? "");
326
+ if (named !== OCTET_STREAM) return { type: named, from: "filename" };
327
+ return { type: OCTET_STREAM, from: "unknown" };
328
+ }
329
+ function nameForType(filename, type) {
330
+ const ext = extensionFor(type);
331
+ if (!ext) return filename;
332
+ const current = /\.[a-z0-9]+$/i.exec(filename)?.[0] ?? "";
333
+ if (current && mediaContentType(`x${current}`) === type) return filename;
334
+ return `${filename.slice(0, filename.length - current.length)}${ext}`;
335
+ }
336
+ function containerMismatch(filename, head) {
337
+ const actual = sniffMediaType(head);
338
+ if (!actual) return null;
339
+ const claimed = mediaContentType(filename);
340
+ if (claimed === actual) return null;
341
+ return `${filename} is ${actual}, not the ${claimed} its name claims \u2014 rename it to ${nameForType(filename, actual)}`;
342
+ }
343
+
344
+ // src/plugin/args.ts
345
+ function parseArgs2(argv, booleanFlags, multiFlags = /* @__PURE__ */ new Set()) {
346
+ const positionals = [];
347
+ const flags = {};
348
+ const multi = {};
349
+ const put = (name, value) => {
350
+ if (multiFlags.has(name)) (multi[name] ??= []).push(value);
351
+ else flags[name] = value;
352
+ };
353
+ for (let i = 0; i < argv.length; i++) {
354
+ const arg = argv[i];
355
+ if (arg === "--") {
356
+ positionals.push(...argv.slice(i + 1));
357
+ break;
358
+ }
359
+ if (arg.startsWith("--")) {
360
+ const eq = arg.indexOf("=");
361
+ if (eq !== -1) {
362
+ put(arg.slice(2, eq), arg.slice(eq + 1));
363
+ continue;
364
+ }
365
+ const name = arg.slice(2);
366
+ if (booleanFlags.has(name)) {
367
+ flags[name] = true;
368
+ continue;
369
+ }
370
+ if (i + 1 >= argv.length || argv[i + 1].startsWith("--")) {
371
+ throw new UsageError(`--${name} expects a value`);
372
+ }
373
+ put(name, argv[i + 1]);
374
+ i++;
375
+ continue;
376
+ }
377
+ positionals.push(arg);
378
+ }
379
+ for (const name of Object.keys(flags)) noteGivenFlag(name);
380
+ for (const name of Object.keys(multi)) noteGivenFlag(name);
381
+ return { positionals, flags: trackFlags(flags), multi: trackFlags(multi) };
382
+ }
383
+ function numFlag2(flags, name, fallback) {
384
+ const v = flags[name];
385
+ if (typeof v !== "string") return fallback;
386
+ const n = Number(v);
387
+ if (!Number.isFinite(n))
388
+ throw new UsageError(`--${name} expects a number, got "${v}"`);
389
+ return n;
390
+ }
391
+ function strFlag(flags, name) {
392
+ const v = flags[name];
393
+ return typeof v === "string" ? v : void 0;
394
+ }
395
+ function hasFlag(flags, name) {
396
+ return Object.prototype.hasOwnProperty.call(flags, name);
397
+ }
398
+
399
+ // src/plugin/platform.ts
400
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
401
+ import { homedir } from "os";
402
+ import { join as join2 } from "path";
403
+
404
+ // src/plugin/hostAgent.ts
405
+ import { existsSync as existsSync2 } from "fs";
406
+ var SLUG = /^[a-z0-9][a-z0-9.-]{0,39}$/;
407
+ var MARKERS = [
408
+ ["cursor", (e) => !!e.CURSOR_TRACE_ID],
409
+ [
410
+ "cursor-cli",
411
+ (e) => !!e.CURSOR_AGENT || e.CURSOR_EXTENSION_HOST_ROLE === "agent-exec"
412
+ ],
413
+ ["gemini-cli", (e) => !!e.GEMINI_CLI],
414
+ ["codex", (e) => !!(e.CODEX_SANDBOX || e.CODEX_CI || e.CODEX_THREAD_ID)],
415
+ ["antigravity", (e) => !!e.ANTIGRAVITY_AGENT],
416
+ ["augment-cli", (e) => !!e.AUGMENT_AGENT],
417
+ ["opencode", (e) => !!e.OPENCODE_CLIENT],
418
+ [
419
+ "claude-code",
420
+ (e) => !!(e.CLAUDECODE || e.CLAUDE_CODE) && !e.CLAUDE_CODE_IS_COWORK
421
+ ],
422
+ ["claude-cowork", (e) => !!(e.CLAUDECODE || e.CLAUDE_CODE)],
423
+ ["replit", (e) => !!e.REPL_ID],
424
+ [
425
+ "github-copilot",
426
+ (e) => !!(e.COPILOT_MODEL || e.COPILOT_ALLOW_ALL || e.COPILOT_GITHUB_TOKEN)
427
+ ]
428
+ ];
429
+ var DEVIN_LOCAL_PATH = "/opt/.devin";
430
+ function slugFromAiAgent(value) {
431
+ const head = value.trim().toLowerCase().split("_")[0] ?? "";
432
+ if (head === "github-copilot-cli") return "github-copilot";
433
+ return SLUG.test(head) ? head : null;
434
+ }
435
+ function detectHostAgent(env = process.env, exists = existsSync2) {
436
+ if (env.AI_AGENT) {
437
+ const slug = slugFromAiAgent(env.AI_AGENT);
438
+ if (slug) return slug;
439
+ }
440
+ for (const [slug, test] of MARKERS) if (test(env)) return slug;
441
+ try {
442
+ if (exists(DEVIN_LOCAL_PATH)) return "devin";
443
+ } catch {
444
+ }
445
+ return null;
446
+ }
447
+
448
+ // src/plugin/platform.ts
449
+ function platformOrigin(flags = {}) {
450
+ const legacyEnv = process.env.VOS_API_BASE?.trim();
451
+ const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
452
+ if (!flags.origin && !flags.api && !process.env.VOS_ORIGIN && legacyEnv) {
453
+ process.stderr.write(
454
+ "note: VOS_API_BASE is deprecated \u2014 set VOS_ORIGIN (an origin, no /api suffix)\n"
455
+ );
456
+ }
457
+ return raw.replace(/\/+$/, "").replace(/\/api$/, "");
458
+ }
459
+ function parseVosId(input) {
460
+ if (!/^https?:\/\//.test(input)) {
461
+ if (/^[A-Za-z0-9_-]+$/.test(input)) return input;
462
+ throw new UsageError(`"${input}" is not a vos id or URL`);
463
+ }
464
+ let url;
465
+ try {
466
+ url = new URL(input);
467
+ } catch {
468
+ throw new UsageError(`"${input}" is not a valid URL`);
469
+ }
470
+ const query = url.searchParams.get("vos");
471
+ if (query) return query;
472
+ const path = url.pathname.match(/\/vos\/([A-Za-z0-9_-]+)/);
473
+ if (path) return path[1];
474
+ throw new UsageError(
475
+ `could not find a vos id in ${input} \u2014 expected /vos/{id} or ?vos={id}`
476
+ );
477
+ }
478
+ function deriveSlug(title) {
479
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
480
+ }
481
+ var CREDENTIALS_PATH = join2(homedir(), ".config", "vos", "credentials");
482
+ function resolveCredential(explicit) {
483
+ const flag = explicit?.trim();
484
+ if (flag) return flag;
485
+ const env = process.env.VOS_API_KEY?.trim();
486
+ if (env) return env;
487
+ try {
488
+ const first = readFileSync(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
489
+ return first || null;
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+ function requireCredential(explicit) {
495
+ const key = resolveCredential(explicit);
496
+ if (!key) {
497
+ throw new Error(
498
+ "no credential found \u2014 pass --key, set VOS_API_KEY, or run `vos login` (mint a content key at https://vos.so/app/api; a vos_rg_ remix grant works too)"
499
+ );
500
+ }
501
+ return key;
502
+ }
503
+ function writeCredential(key) {
504
+ mkdirSync2(join2(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
505
+ writeFileSync2(CREDENTIALS_PATH, `${key.trim()}
506
+ `, { mode: 384 });
507
+ return CREDENTIALS_PATH;
508
+ }
509
+ function clientId(env = process.env, exists) {
510
+ const explicit = env.VOS_CLIENT?.trim();
511
+ if (explicit && explicit.length <= 60 && !/[\r\n]/.test(explicit))
512
+ return explicit;
513
+ const host = detectHostAgent(env, exists);
514
+ return host ? `${host} vos-cli` : "vos-cli";
515
+ }
516
+ async function apiJson(origin, path, init = {}) {
517
+ const headers = {
518
+ accept: "application/json",
519
+ ...init.headers
520
+ };
521
+ if (init.key) headers.authorization = `Bearer ${init.key}`;
522
+ if (init.body !== void 0) headers["content-type"] = "application/json";
523
+ const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
524
+ const res = await fetch(`${origin}${path}`, {
525
+ method: init.method ?? "GET",
526
+ headers,
527
+ body: reqBody
528
+ });
529
+ let body = {};
530
+ try {
531
+ body = await res.json();
532
+ } catch {
533
+ }
534
+ return { status: res.status, body };
535
+ }
536
+ function apiError(what, r) {
537
+ const detail = typeof r.body.error === "string" ? r.body.error : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
538
+ const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
539
+ return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
540
+ }
541
+ var SYNC_STATE_NAME = "vos.json";
542
+ function readJsonFile(file) {
543
+ try {
544
+ return JSON.parse(readFileSync(file, "utf8"));
545
+ } catch {
546
+ return null;
547
+ }
548
+ }
549
+ function readSyncState(dir) {
550
+ const own = readJsonFile(join2(dir, SYNC_STATE_NAME));
551
+ if (own && typeof own.vosId === "string") {
552
+ return {
553
+ vosId: own.vosId,
554
+ versionId: typeof own.versionId === "string" ? own.versionId : null,
555
+ ...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
556
+ ...typeof own.title === "string" ? { title: own.title } : {},
557
+ ...typeof own.slug === "string" ? { slug: own.slug } : {},
558
+ ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
559
+ };
560
+ }
561
+ const push = readJsonFile(join2(dir, "push.json"));
562
+ if (push && typeof push.vosId === "string") {
563
+ return {
564
+ vosId: push.vosId,
565
+ versionId: typeof push.versionId === "string" ? push.versionId : null,
566
+ ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
567
+ };
568
+ }
569
+ const meta = readJsonFile(join2(dir, "meta.json"));
570
+ if (meta && typeof meta.id === "string") {
571
+ return {
572
+ vosId: meta.id,
573
+ versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
574
+ ...typeof meta.title === "string" ? { title: meta.title } : {},
575
+ ...typeof meta.slug === "string" ? { slug: meta.slug } : {},
576
+ ...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
577
+ };
578
+ }
579
+ return null;
580
+ }
581
+ function writeSyncState(dir, patch) {
582
+ const merged = {
583
+ ...readSyncState(dir) ?? { versionId: null },
584
+ ...patch
585
+ };
586
+ writeFileSync2(
587
+ join2(dir, SYNC_STATE_NAME),
588
+ `${JSON.stringify(merged, null, 2)}
589
+ `
590
+ );
591
+ return merged;
592
+ }
593
+ function formatChanges(changes) {
594
+ const lines = [];
595
+ for (const ch of changes) {
596
+ const label = ch.label ? ` \xB7 ${ch.label}` : "";
597
+ const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
598
+ lines.push(
599
+ `v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
600
+ );
601
+ if (ch.note) lines.push(` note: ${ch.note}`);
602
+ }
603
+ return lines;
604
+ }
605
+ function serverWarnings(body, log) {
606
+ const warnings = Array.isArray(body.warnings) ? body.warnings : [];
607
+ for (const w of warnings) if (typeof w === "string") log(`warning ${w}`);
608
+ }
609
+
610
+ export {
611
+ helpFlagsFor,
612
+ unusedFlagsMessage,
613
+ UsageError,
614
+ parseArgs,
615
+ numFlag,
616
+ ASSET_ROUTE,
617
+ hostedAssetId,
618
+ manifestRefs,
619
+ programAssets,
620
+ missingManifestFiles,
621
+ TAKE_MEDIA_EXTENSIONS,
622
+ MEDIA_HEAD_BYTES,
623
+ sniffMediaType,
624
+ extensionFor,
625
+ resolveMediaType,
626
+ nameForType,
627
+ containerMismatch,
628
+ parseArgs2,
629
+ numFlag2,
630
+ strFlag,
631
+ hasFlag,
632
+ platformOrigin,
633
+ parseVosId,
634
+ deriveSlug,
635
+ CREDENTIALS_PATH,
636
+ resolveCredential,
637
+ requireCredential,
638
+ writeCredential,
639
+ clientId,
640
+ apiJson,
641
+ apiError,
642
+ SYNC_STATE_NAME,
643
+ readSyncState,
644
+ writeSyncState,
645
+ formatChanges,
646
+ serverWarnings
647
+ };
648
+ //# sourceMappingURL=chunk-36P5RREC.js.map