@vosjs/cli 0.83.2 → 0.84.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
@@ -192,6 +192,12 @@ vos look https://app.acme.com/projects --session acme --click '[data-testid="new
192
192
 
193
193
  It writes `<out>/look.png` (the viewport; `--out look` by default) and lists every control in view (buttons, links, tabs, menu items, checkboxes, switches, options, fields, editable regions, and anything with a test id) with its tag, role, accessible name, test id, link path and query, rect in viewport px, whether it is disabled, inside an open dialog or covered by something on top of it, and ONE proposed selector: its test id; else its `aria-label`, inside the open dialog it sits in; else a link's own `href`, query kept (two tiles to `?vos=a` and `?vos=b` are two links); else role and name (`role=button[name="Continue"]`, what `page.locator` reads), narrowed by its dialog or landmark when that is what makes it one; else a field's own `name` or `placeholder`, a written id, or its words inside its dialog or landmark. Never a position in the tree. Each proposal is counted with the recorder's own selector engine and marked `UNIQUE` (it reaches this element and no other) or `AMBIGUOUS` (it reaches others too, and the recorder would take the first). A control whose words hold an address, a key or a card number is listed with those words withheld, and the frame's exposures are said the way a rehearsal says them. Looking never spends or destroys: a `--click` whose target's words begin with a stem that does (delet, remov, revok, eras, discard, publish, deploy, purchas, subscri, unsubscri, pay, buy, generat, submit, send, transfer, upgrad, confirm, leav, destroy, so `Allow deleting` and `Revoke` are caught with `Delete`), or say move to trash, empty trash or cancel subscription, is refused in words before it is pressed (exit 2), and `--type` fills a field without pressing Enter. A look cannot know every spend: `Make a video` or `Create` on a primary button may start paid work and passes, because refusing every Make would refuse half of every app. The list is a floor under looking; in a recorded flow, a step's `guard: "spend"` in `actions.json` is the fence. A sign-in in front of the page is the wall check's verdict, exit 4; the sign-in page is still looked at, since it is what a `setup` step scripts past. `--wait <ms>` adds a pause before the look for a page that animates in; `--json` prints the whole look as one done event (`url`, `screenshot`, `controls`, `exposures`, `wall`).
194
194
 
195
+ **Past a step that creates: `vos look --actions actions.json --until <step id>`.** The state a flow is about often exists only after a click that makes something (the dialog that shows a new key appears after Create), and `--click` takes one hand-written step. `--until` walks the script's own steps the way a rehearsal does, on the same door (its `setup`, its `mask`, `--session` or `--storage-state`, a `guard: "spend"` step hovered and never pressed), stops after the step whose `id` it names, and looks at the page as that step left it; the steps after it never run. The URL and viewport are the script's unless given. The walk clicks for real, so the script's `teardown` runs after the look and its line ends the report. A step skipped on the way is named and exits 2 (the page may not be the one the script reaches), a step that is never reached says why, and an `--until` that names no step lists the ids the script has. It never takes `--click` or `--type` beside it.
196
+
197
+ ```bash
198
+ vos look --actions actions.json --until create --session acme # the page after the step with "id": "create"
199
+ ```
200
+
195
201
  **`setup`: the steps that run before the camera rolls.** A sign-in form, a cookie banner, the "choose your editor" modal, an onboarding tour: things a take must get past and must not show. `actions.json` takes `setup: [...]` beside `steps`, with the verbs `goto`, `click`, `type`, `press`, `wait`, and the guard `if`. They run after the first navigation and before a frame is captured, as plain actions with no cursor, no frames, no pace and nothing in `meta.steps`; then the recorder opens `url` again and the take begins where the setup left it. A `type` step's `text` may be `{ "env": "DEMO_PASSWORD" }`, read from the shell at run time and never logged or stored (the log names the field, never the value; a literal typed into a password field is refused by `validate`, because `actions.json` is committed and pushed with the take). A selector that never appears fails the take before anything is recorded (exit 2), because a take that begins at a half-finished sign-in is the wall by another name; rehearse the setup with `--dry-run` like everything else. This is rung 2 of the session ladder made scriptable: a local or self-hosted instance with a seeded user, no state file needed.
196
202
 
197
203
  ```json
@@ -544,7 +550,7 @@ vos assets ls [--kind <kind>] [--q <text>] [--sort size] | usage | why <id> #
544
550
  vos recipe push <FILE.md> --folder <slug> | --asset <id> # the one recipe write: create, or replace in place
545
551
  ```
546
552
 
547
- `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`, `--yes`, `--bake` and `--claimable`. Both take `--wait`. A take push first asks the platform which `@vosjs/studio-core` it renders with (`GET /api/engine`, public, `{ studioCore, core }`) and refuses (exit 2), before anything uploads, a take that uses a field newer than it: the program a push compiles plays right, but the platform's studio lowers the stored document again with its own release, so it would show an anchored caption at its fallback start or play a clip with a loudness target at its gain, and every version saved from the studio would keep it that way. `--bake` sends a copy any renderer plays: every such field the take uses, said in what an older release reads (an anchored caption at the start it plays at in this cut with its `{elapsed}` filled, a loudness target as its gain, up to 1), whatever the platform answered, behind, current or nothing at all, and it never touches `doc.json`. The table of fields and their releases is `DOC_FEATURES` in `@vosjs/studio-core`. A platform that cannot say (no such endpoint, a refusal, no answer in 2 s) is warned of and pushed to. A new vos pushed without `--folder` lands unfiled, at the root of the shelf, and the done line says so. `--folder` takes a project on your own shelf (its slug or id) or anyone's project by its address, `<handle>/<slug>` as its page on vos.so names it: the server answers where the push lands, CONTINUING that project when the key is its owner's and otherwise STARTING the person's own from it (a private project made once, named after it, with the recipes they may read copied in, never its videos). The push says which.
553
+ `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`, `--desc`, `--prompt`, `--label`, `--note`, `--folder`, `--override`, `--yes`, `--bake` and `--claimable`. Both take `--wait`. `--desc` (up to 500 characters, checked before anything uploads) and `--prompt` are the watch page's words, kept on the vos and never on a version: a first push creates the vos with them, a later push that names them replaces them, one that names neither leaves them as they are, on a program and a take alike. A take push first asks the platform which `@vosjs/studio-core` it renders with (`GET /api/engine`, public, `{ studioCore, core }`) and refuses (exit 2), before anything uploads, a take that uses a field newer than it: the program a push compiles plays right, but the platform's studio lowers the stored document again with its own release, so it would show an anchored caption at its fallback start or play a clip with a loudness target at its gain, and every version saved from the studio would keep it that way. `--bake` sends a copy any renderer plays: every such field the take uses, said in what an older release reads (an anchored caption at the start it plays at in this cut with its `{elapsed}` filled, a loudness target as its gain, up to 1), whatever the platform answered, behind, current or nothing at all, and it never touches `doc.json`. The table of fields and their releases is `DOC_FEATURES` in `@vosjs/studio-core`. A platform that cannot say (no such endpoint, a refusal, no answer in 2 s) is warned of and pushed to. A new vos pushed without `--folder` lands unfiled, at the root of the shelf, and the done line says so. `--folder` takes a project on your own shelf (its slug or id) or anyone's project by its address, `<handle>/<slug>` as its page on vos.so names it: the server answers where the push lands, CONTINUING that project when the key is its owner's and otherwise STARTING the person's own from it (a private project made once, named after it, with the recipes they may read copied in, never its videos). The push says which.
548
554
 
549
555
  **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 in the config's own directory or below, so `vos render`, `vos still` and `vos preview` serve the file to the page from disk. A path that climbs out of that directory (`../`, an absolute path, a link that points away) is not served and not uploaded, and a config loaded from a URL reads no local file: a program's code runs in a page, and what the page is served the program can send anywhere. `vos push` uploads each local file (content-addressed, so a re-push sends nothing: pictures, SVG, video, sound, `.glb` models, fonts and HDR environments) 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. A file an element names as `src` or a font names as `url` is lifted into the manifest on the way (the element then names `"$assets.<name>"`), and a path in `data` that is a file beside the config is uploaded and rewritten to the hosted file's path, so every place a program can name a file crosses. A file typed inside a function cannot be rewritten: `vos push` says so, with the manifest line to write. `vos fetch --media` brings a program's hosted files home under `assets/` and re-points the manifest at them (`assets/.hosted.json` remembers which hosted file each one is, so a later fetch replaces one that changed); 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 them too, through the claim's own upload session (below).
550
556
 
@@ -148,9 +148,9 @@ function landedLines(l) {
148
148
  }
149
149
 
150
150
  // src/plugin/run.ts
151
- import { mkdir as mkdir13, readFile as readFile18, rm as rm6 } from "fs/promises";
151
+ import { mkdir as mkdir13, readFile as readFile19, rm as rm7 } from "fs/promises";
152
152
  import { existsSync as existsSync30 } from "fs";
153
- import { dirname as dirname11, join as join33, resolve as resolve21 } from "path";
153
+ import { dirname as dirname12, join as join33, resolve as resolve21 } from "path";
154
154
  import {
155
155
  END_CARD_FROM as END_CARD_FROM2,
156
156
  docOutputDuration as docOutputDuration5,
@@ -7926,10 +7926,19 @@ function printChanges(r, changes, protectedIds) {
7926
7926
  r.event({ event: "protected", nodes: protectedIds });
7927
7927
  }
7928
7928
  }
7929
+ var DESCRIPTION_MAX = 500;
7930
+ function descriptionProblem(desc) {
7931
+ if (desc === void 0) return null;
7932
+ if (!desc.trim())
7933
+ return "--desc is empty: give the sentence that says what the video shows";
7934
+ if (desc.length > DESCRIPTION_MAX)
7935
+ return `--desc is ${desc.length} characters and a vos keeps ${DESCRIPTION_MAX}: shorten it`;
7936
+ return null;
7937
+ }
7929
7938
  function takePushRefusal(ask) {
7930
7939
  if (ask.programOnly.length) {
7931
7940
  const list = ask.programOnly.map((f) => `--${f}`).join(", ");
7932
- return `${list} ${ask.programOnly.length > 1 ? "belong" : "belongs"} to a PROGRAM push (a config.json); a take push reads --vos, --title, --label, --note, --folder, --override and --yes`;
7941
+ return `${list} ${ask.programOnly.length > 1 ? "belong" : "belongs"} to a PROGRAM push (a config.json); a take push reads --vos, --title, --desc, --prompt, --label, --note, --folder, --override and --yes`;
7933
7942
  }
7934
7943
  if (ask.vos !== void 0 && ask.trackedVosId !== null && ask.trackedVosId !== ask.vos)
7935
7944
  return `this take tracks vos ${ask.trackedVosId} (its vos.json), not ${ask.vos}. Drop --vos to push there, or remove vos.json to adopt ${ask.vos} instead`;
@@ -8104,7 +8113,8 @@ async function pushTake(dir, flags, r) {
8104
8113
  ...folderId ? { folderId } : {},
8105
8114
  ...flags.label ? { label: flags.label } : {},
8106
8115
  ...flags.note ? { note: flags.note } : {},
8107
- ...flags.prompt ? { prompt: flags.prompt } : {}
8116
+ ...flags.prompt ? { prompt: flags.prompt } : {},
8117
+ ...flags.description ? { description: flags.description } : {}
8108
8118
  }
8109
8119
  });
8110
8120
  if (created.status !== 201) {
@@ -8155,14 +8165,18 @@ async function pushTake(dir, flags, r) {
8155
8165
  );
8156
8166
  }
8157
8167
  serverWarnings(pushed.json, (l) => r.log(l));
8158
- if (flags.prompt) {
8168
+ const words2 = {
8169
+ ...flags.prompt ? { prompt: flags.prompt } : {},
8170
+ ...flags.description ? { description: flags.description } : {}
8171
+ };
8172
+ if (Object.keys(words2).length) {
8159
8173
  const set = await api(ctx, `/vos/${state.vosId}`, {
8160
8174
  method: "PATCH",
8161
- body: { prompt: flags.prompt }
8175
+ body: words2
8162
8176
  });
8163
8177
  if (set.status !== 200)
8164
8178
  r.warn(
8165
- `could not set the prompt (${set.status}): ${String(set.json.error ?? "")}${detailLine(set.json)}`
8179
+ `could not set the ${Object.keys(words2).join(" and ")} (${set.status}): ${String(set.json.error ?? "")}${detailLine(set.json)}`
8166
8180
  );
8167
8181
  }
8168
8182
  const version = pushed.json.version;
@@ -8380,6 +8394,7 @@ async function pushTakeClaimable(dir, flags, r) {
8380
8394
  const body = { title, config, doc: docForPush };
8381
8395
  if (state?.vosId) body.remixOfId = state.vosId;
8382
8396
  if (flags.prompt) body.prompt = flags.prompt;
8397
+ if (flags.description) body.description = flags.description;
8383
8398
  if (flags.share) body.share = true;
8384
8399
  const origin = platformOrigin(flags);
8385
8400
  r.log(`creating a claimable push of ${title}\u2026`);
@@ -8918,9 +8933,11 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
8918
8933
  });
8919
8934
  if (pushTarget.kind === "refuse") throw new UsageError(pushTarget.why);
8920
8935
  const vosId = pushTarget.kind === "version" ? pushTarget.vosId : null;
8921
- if (vosId && (desc || tagsFlag || folderRef)) {
8936
+ const descProblem = descriptionProblem(desc);
8937
+ if (descProblem) throw new UsageError(descProblem);
8938
+ if (vosId && (tagsFlag || folderRef)) {
8922
8939
  throw new UsageError(
8923
- "--desc/--tags/--folder apply when CREATING a vos. On an existing one: vos folder move <id> --to <folder> for filing; edit title/description/tags on vos.so"
8940
+ "--tags/--folder apply when CREATING a vos. On an existing one: vos folder move <id> --to <folder> for filing; edit tags on vos.so"
8924
8941
  );
8925
8942
  }
8926
8943
  if (vosId) {
@@ -8966,13 +8983,18 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
8966
8983
  if (res.status !== 201)
8967
8984
  throw new Error(apiError(`push version to ${vosId}`, res));
8968
8985
  serverWarnings(res.body, say(r));
8969
- if (prompt) {
8986
+ const words2 = {
8987
+ ...prompt ? { prompt } : {},
8988
+ ...desc ? { description: desc } : {}
8989
+ };
8990
+ if (Object.keys(words2).length) {
8970
8991
  const set = await apiJson(origin, `/api/vos/${vosId}`, {
8971
8992
  method: "PATCH",
8972
8993
  key,
8973
- body: { prompt }
8994
+ body: words2
8974
8995
  });
8975
- if (set.status !== 200) r.warn(apiError("set the prompt", set));
8996
+ if (set.status !== 200)
8997
+ r.warn(apiError(`set the ${Object.keys(words2).join(" and ")}`, set));
8976
8998
  }
8977
8999
  const version = res.body.version ?? {};
8978
9000
  if (typeof version.id === "string") {
@@ -13129,8 +13151,8 @@ async function cmdActions(argv) {
13129
13151
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
13130
13152
  viewport = { width: Number(m[1]), height: Number(m[2]) };
13131
13153
  }
13132
- const { readFile: readFile19 } = await import("fs/promises");
13133
- const text = await readFile19(resolve14(input), "utf8");
13154
+ const { readFile: readFile20 } = await import("fs/promises");
13155
+ const text = await readFile20(resolve14(input), "utf8");
13134
13156
  const { records, problems } = parseAgentBrowserLog(text);
13135
13157
  for (const p of problems) r.log(` ${p}`);
13136
13158
  const result = convertAgentBrowser(records, {
@@ -13167,9 +13189,9 @@ function summary2(result) {
13167
13189
  }
13168
13190
  async function cmdActionsScript(input, flags) {
13169
13191
  const r = createReporter(flags.json === true);
13170
- const { readFile: readFile19 } = await import("fs/promises");
13192
+ const { readFile: readFile20 } = await import("fs/promises");
13171
13193
  const { shotList, formatShotList } = await import("./shotList-PL7XP6AC.js");
13172
- const raw = JSON.parse(await readFile19(resolve14(input), "utf8"));
13194
+ const raw = JSON.parse(await readFile20(resolve14(input), "utf8"));
13173
13195
  const errors = validateActions(raw);
13174
13196
  if (errors.length) throw new UsageError(`${input}:
13175
13197
  ${errors.join("\n ")}`);
@@ -16911,9 +16933,10 @@ async function cmdAudio(argv) {
16911
16933
  }
16912
16934
 
16913
16935
  // src/plugin/look.ts
16914
- import { mkdir as mkdir12 } from "fs/promises";
16936
+ import { mkdir as mkdir12, mkdtemp as mkdtemp3, readFile as readFile18, rm as rm6 } from "fs/promises";
16915
16937
  import { existsSync as existsSync27 } from "fs";
16916
- import { join as join30, resolve as resolve20 } from "path";
16938
+ import { tmpdir as tmpdir4 } from "os";
16939
+ import { dirname as dirname10, join as join30, resolve as resolve20 } from "path";
16917
16940
  import { headersFor, urlSchemeProblem } from "@vosjs/render-core/record";
16918
16941
  function quote(value) {
16919
16942
  return `"${value.replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
@@ -17316,19 +17339,19 @@ async function lookAt(page, url, opts) {
17316
17339
  });
17317
17340
  const ran = [];
17318
17341
  if (!wall || wall.level === "soft") {
17319
- for (const step2 of opts.steps ?? []) {
17320
- const all = page.locator(step2.selector);
17342
+ for (const step of opts.steps ?? []) {
17343
+ const all = page.locator(step.selector);
17321
17344
  const target = all.first();
17322
17345
  try {
17323
17346
  await target.waitFor({ state: "visible", timeout: STEP_TIMEOUT_MS });
17324
17347
  } catch {
17325
17348
  throw new UsageError(
17326
- `--${step2.do} ${step2.selector}: no visible element matches it after ${STEP_TIMEOUT_MS / 1e3} s. Look without the step and take a selector from the list`
17349
+ `--${step.do} ${step.selector}: no visible element matches it after ${STEP_TIMEOUT_MS / 1e3} s. Look without the step and take a selector from the list`
17327
17350
  );
17328
17351
  }
17329
17352
  const count = await all.count();
17330
17353
  const note = count > 1 ? `the first of ${count} matches` : void 0;
17331
- if (step2.do === "click") {
17354
+ if (step.do === "click") {
17332
17355
  const words2 = await target.evaluate(
17333
17356
  (el) => [
17334
17357
  el.getAttribute("aria-label"),
@@ -17344,25 +17367,37 @@ async function lookAt(page, url, opts) {
17344
17367
  if (word) {
17345
17368
  const label = words2.find((w) => spendWord([w])) ?? word;
17346
17369
  throw new LookRefusal(
17347
- refusalLine(step2.selector, label.slice(0, 60), word)
17370
+ refusalLine(step.selector, label.slice(0, 60), word)
17348
17371
  );
17349
17372
  }
17350
- log(`click ${step2.selector}${note ? ` (${note})` : ""}`);
17373
+ log(`click ${step.selector}${note ? ` (${note})` : ""}`);
17351
17374
  await target.click({ timeout: STEP_TIMEOUT_MS });
17352
17375
  } else {
17353
17376
  log(
17354
- `type ${step2.text.length} chars into ${step2.selector}${note ? ` (${note})` : ""}`
17377
+ `type ${step.text.length} chars into ${step.selector}${note ? ` (${note})` : ""}`
17355
17378
  );
17356
- await target.fill(step2.text, { timeout: STEP_TIMEOUT_MS });
17379
+ await target.fill(step.text, { timeout: STEP_TIMEOUT_MS });
17357
17380
  }
17358
17381
  ran.push({
17359
- do: step2.do,
17360
- selector: step2.selector,
17382
+ do: step.do,
17383
+ selector: step.selector,
17361
17384
  ...note ? { note } : {}
17362
17385
  });
17363
17386
  settled = await settle(page);
17364
17387
  }
17365
17388
  }
17389
+ return inspectPage(page, {
17390
+ url,
17391
+ outDir: opts.outDir,
17392
+ waitMs: opts.waitMs,
17393
+ wall,
17394
+ steps: ran,
17395
+ settled
17396
+ });
17397
+ }
17398
+ async function inspectPage(page, opts) {
17399
+ const { url, wall, settled } = opts;
17400
+ const ran = opts.steps;
17366
17401
  if (opts.waitMs && opts.waitMs > 0) await page.waitForTimeout(opts.waitMs);
17367
17402
  await mkdir12(opts.outDir, { recursive: true });
17368
17403
  const screenshot = join30(opts.outDir, "look.png");
@@ -17420,7 +17455,27 @@ async function lookAt(page, url, opts) {
17420
17455
  settled
17421
17456
  };
17422
17457
  }
17423
- var USAGE5 = "vos look <url> [--session <name> | --storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--out <dir>] [--json]";
17458
+ var USAGE5 = "vos look <url> [--session <name> | --storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--out <dir>] [--json]\n vos look [url] --actions actions.json --until <step id> [--session <name> | --storage-state <file>] [--viewport WxH] [--headed] [--wait <ms>] [--out <dir>] [--json]";
17459
+ function untilProblem(given, steps) {
17460
+ if (given.until !== void 0 && given.actions === void 0)
17461
+ return "--until names a step of a script: pass the script with --actions actions.json";
17462
+ if (given.actions === void 0) return null;
17463
+ if (given.clicks)
17464
+ return "--actions walks the script; --click and --type walk by hand. Pass one";
17465
+ if (given.until === void 0)
17466
+ return "--actions needs --until <step id>: the step whose page you want to see";
17467
+ if (!steps) return null;
17468
+ if (steps.some((s) => s.id === given.until)) return null;
17469
+ const ids = steps.map((s) => s.id).filter((id) => !!id);
17470
+ return `--until ${given.until}: no step in ${given.actions} has that id. ${ids.length ? `Its ids: ${ids.join(", ")}` : 'Its steps have no ids; give the step you want an "id"'}`;
17471
+ }
17472
+ function walkedSteps(steps) {
17473
+ return steps.map((s) => ({
17474
+ do: s.id ? `${s.do} (${s.id})` : s.do,
17475
+ selector: s.selector ?? "",
17476
+ ...s.skipped ? { note: `SKIPPED: ${s.reason ?? "not found"}` } : s.guard ? { note: `guarded (${s.guard}), hovered, not pressed` } : {}
17477
+ }));
17478
+ }
17424
17479
  function lookReport(r) {
17425
17480
  const inDialog = r.controls.filter((c) => c.inDialog).length;
17426
17481
  const head = `Looked at ${r.landed} (${r.viewport.width}\xD7${r.viewport.height}): ${r.screenshot}${r.settled ? "" : "\n note: the page never held still for 0.8 s; looked at it as it was"}`;
@@ -17449,12 +17504,30 @@ async function cmdLook(argv) {
17449
17504
  );
17450
17505
  void multi.click;
17451
17506
  void multi.type;
17452
- const url = positionals[0];
17507
+ const actionsArg = strFlag(flags, "actions");
17508
+ const untilId = strFlag(flags, "until");
17509
+ const early = untilProblem({
17510
+ actions: actionsArg,
17511
+ until: untilId,
17512
+ clicks: steps.length
17513
+ });
17514
+ if (early) throw new UsageError(early);
17515
+ let actions;
17516
+ if (actionsArg) {
17517
+ actions = await loadLookActions(resolve20(actionsArg));
17518
+ const bad = untilProblem(
17519
+ { actions: actionsArg, until: untilId, clicks: 0 },
17520
+ actions.steps
17521
+ );
17522
+ if (bad) throw new UsageError(bad);
17523
+ }
17524
+ const url = positionals[0] ?? actions?.url;
17453
17525
  if (!url) throw new UsageError(USAGE5);
17454
17526
  const scheme = urlSchemeProblem(url);
17455
17527
  if (scheme) throw new UsageError(scheme);
17456
17528
  const r = createReporter(flags.json === true);
17457
- const viewport = parseViewport2(strFlag(flags, "viewport"));
17529
+ const viewportFlag = strFlag(flags, "viewport");
17530
+ const viewport = !viewportFlag && actions?.viewport ? actions.viewport : parseViewport2(viewportFlag);
17458
17531
  const outDir = resolve20(strFlag(flags, "out") ?? "look");
17459
17532
  const headed = flags.headed === true;
17460
17533
  const waitRaw = strFlag(flags, "wait");
@@ -17485,6 +17558,22 @@ async function cmdLook(argv) {
17485
17558
  throw new UsageError(e instanceof Error ? e.message : String(e));
17486
17559
  }
17487
17560
  const browserArgs = (multi["browser-arg"] ?? []).filter(Boolean);
17561
+ if (actions && untilId && actionsArg)
17562
+ return lookUntil({
17563
+ r,
17564
+ url,
17565
+ actions,
17566
+ actionsPath: resolve20(actionsArg),
17567
+ untilId,
17568
+ viewport,
17569
+ outDir,
17570
+ waitMs,
17571
+ headed,
17572
+ storageState,
17573
+ sessionName,
17574
+ headers,
17575
+ browserArgs
17576
+ });
17488
17577
  const inContext = async (ctx) => {
17489
17578
  if (Object.keys(headers).length)
17490
17579
  await ctx.route("**/*", async (route) => {
@@ -17531,6 +17620,108 @@ async function cmdLook(argv) {
17531
17620
  );
17532
17621
  return result.wall?.level === "hard" ? EXIT_WALL : EXIT_OK;
17533
17622
  }
17623
+ async function loadLookActions(file) {
17624
+ let raw;
17625
+ try {
17626
+ raw = JSON.parse(await readFile18(file, "utf8"));
17627
+ } catch (e) {
17628
+ throw new UsageError(
17629
+ `--actions ${file}: ${e instanceof Error ? e.message : String(e)}`
17630
+ );
17631
+ }
17632
+ const errors = validateActions(raw);
17633
+ if (!errors.length) errors.push(...jsSyntaxProblems(raw));
17634
+ if (errors.length)
17635
+ throw new UsageError(`invalid actions file:
17636
+ ${errors.join("\n ")}`);
17637
+ return raw;
17638
+ }
17639
+ async function lookUntil(o) {
17640
+ const { r } = o;
17641
+ const actions = { ...o.actions, viewport: o.viewport };
17642
+ let wall = null;
17643
+ let looked = null;
17644
+ const scratch = await mkdtemp3(join30(tmpdir4(), "vos-look-"));
17645
+ r.log(
17646
+ `walking ${o.actionsPath} to step ${o.untilId} (a rehearsal: guarded steps hovered, never pressed)\u2026`
17647
+ );
17648
+ const browser = await launchBrowser(o.browserArgs, { headless: !o.headed });
17649
+ let rec;
17650
+ try {
17651
+ const walk = (context2) => recordTake(browser, o.url, actions, takePaths(scratch), r.log, {
17652
+ actionsDir: dirname10(o.actionsPath),
17653
+ storageState: o.storageState,
17654
+ headers: o.headers,
17655
+ context: context2,
17656
+ dryRun: true,
17657
+ stopOnHang: true,
17658
+ onEvent: (e) => r.event(e),
17659
+ onArrival: (arrival) => {
17660
+ const verdict = wallVerdict(arrival);
17661
+ if (verdict && verdict.level === "hard") throw new WallError(verdict);
17662
+ if (verdict) r.log(` WARNING: ${verdict.message}`);
17663
+ wall = verdict;
17664
+ return verdict;
17665
+ },
17666
+ until: {
17667
+ id: o.untilId,
17668
+ at: async (raw) => {
17669
+ const page = raw;
17670
+ const settled = await settle(page);
17671
+ looked = await inspectPage(page, {
17672
+ url: o.url,
17673
+ outDir: o.outDir,
17674
+ waitMs: o.waitMs,
17675
+ wall,
17676
+ steps: [],
17677
+ settled
17678
+ });
17679
+ }
17680
+ }
17681
+ });
17682
+ rec = o.sessionName ? await withSession(o.sessionName, (ctx) => walk(ctx), {
17683
+ headless: !o.headed,
17684
+ viewport: o.viewport
17685
+ }) : await walk(void 0);
17686
+ } finally {
17687
+ await browser.close();
17688
+ await rm6(scratch, { recursive: true, force: true });
17689
+ }
17690
+ const result = looked;
17691
+ const ran = walkedSteps(rec.meta.steps ?? []);
17692
+ const skipped = rec.skipped.length;
17693
+ const teardown = rec.teardown ? `
17694
+ ${teardownLine(rec.teardown)}` : "";
17695
+ if (!result) {
17696
+ const why = rec.stopped ? rec.stopped.reason : "the walk ended before it reached that step";
17697
+ r.done(
17698
+ {
17699
+ ok: false,
17700
+ until: o.untilId,
17701
+ steps: ran,
17702
+ skipped: rec.skipped,
17703
+ ...rec.teardown ? { teardown: rec.teardown } : {}
17704
+ },
17705
+ `NOT LOOKED: step ${o.untilId} was never reached: ${why}.${ran.length ? `
17706
+ walked: ${ran.map((s) => `${s.do}${s.selector ? ` ${s.selector}` : ""}${s.note ? ` (${s.note})` : ""}`).join(", then ")}` : ""}
17707
+ Rehearse the script (vos record --actions ${o.actionsPath} --dry-run) to see where it stops.${teardown}`
17708
+ );
17709
+ return EXIT_USAGE;
17710
+ }
17711
+ const full = { ...result, steps: ran };
17712
+ const warn = skipped ? `
17713
+ WARNING: ${skipped} step${skipped === 1 ? " was" : "s were"} SKIPPED on the way, so this may not be the page the script reaches. Fix ${skipped === 1 ? "it" : "them"}, then look again.` : "";
17714
+ r.done(
17715
+ {
17716
+ ok: skipped === 0 && (!full.wall || full.wall.level === "soft"),
17717
+ until: o.untilId,
17718
+ ...full,
17719
+ ...rec.teardown ? { teardown: rec.teardown } : {}
17720
+ },
17721
+ `${lookReport(full)}${warn}${teardown}`
17722
+ );
17723
+ return skipped ? EXIT_USAGE : EXIT_OK;
17724
+ }
17534
17725
 
17535
17726
  // src/plugin/machineCmd.ts
17536
17727
  import { spawnSync as spawnSync4 } from "child_process";
@@ -17548,7 +17739,7 @@ import { chromium as chromium2 } from "playwright";
17548
17739
 
17549
17740
  // src/plugin/machine.ts
17550
17741
  import { existsSync as existsSync28, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
17551
- import { dirname as dirname10, join as join31 } from "path";
17742
+ import { dirname as dirname11, join as join31 } from "path";
17552
17743
  var AGENT_NAMES = [
17553
17744
  "claude",
17554
17745
  "cursor",
@@ -17768,7 +17959,7 @@ function bundledSkillsVersion(dir) {
17768
17959
  }
17769
17960
  }
17770
17961
  function bundledSkillsDir(from) {
17771
- let dir = dirname10(from);
17962
+ let dir = dirname11(from);
17772
17963
  for (let i = 0; i < 6; i++) {
17773
17964
  const pkg = join31(dir, "package.json");
17774
17965
  if (existsSync28(pkg)) {
@@ -17781,7 +17972,7 @@ function bundledSkillsDir(from) {
17781
17972
  } catch {
17782
17973
  }
17783
17974
  }
17784
- const up = dirname10(dir);
17975
+ const up = dirname11(dir);
17785
17976
  if (up === dir) break;
17786
17977
  dir = up;
17787
17978
  }
@@ -18503,7 +18694,7 @@ lint-gated, so a bad override fails like a bad doc.json):
18503
18694
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
18504
18695
  `;
18505
18696
  async function loadActions(file) {
18506
- const raw = JSON.parse(await readFile18(file, "utf8"));
18697
+ const raw = JSON.parse(await readFile19(file, "utf8"));
18507
18698
  const errors = validateActions(raw);
18508
18699
  if (!errors.length) errors.push(...jsSyntaxProblems(raw));
18509
18700
  if (errors.length)
@@ -18725,7 +18916,7 @@ function reRecord(outDir, flags, r) {
18725
18916
  ...from ? {
18726
18917
  reuse: {
18727
18918
  from,
18728
- doc: JSON.parse(await readFile18(from, "utf8"))
18919
+ doc: JSON.parse(await readFile19(from, "utf8"))
18729
18920
  }
18730
18921
  } : {}
18731
18922
  });
@@ -18832,7 +19023,7 @@ async function cmdRecord(argv) {
18832
19023
  );
18833
19024
  const hostedActions = await scriptForHost(
18834
19025
  actions,
18835
- dirname11(resolve21(actionsPath))
19026
+ dirname12(resolve21(actionsPath))
18836
19027
  ).catch((e) => {
18837
19028
  throw new UsageError(e instanceof Error ? e.message : String(e));
18838
19029
  });
@@ -18942,7 +19133,7 @@ async function cmdRecord(argv) {
18942
19133
  sessionName,
18943
19134
  actions,
18944
19135
  (context2) => recordTake(browser2, url, actions, takePaths(outDir), r.log, {
18945
- actionsDir: dirname11(resolve21(actionsPath)),
19136
+ actionsDir: dirname12(resolve21(actionsPath)),
18946
19137
  storageState,
18947
19138
  headers,
18948
19139
  context: context2,
@@ -19050,7 +19241,7 @@ ${lines.join("\n")}
19050
19241
  sessionName,
19051
19242
  actions,
19052
19243
  (context2) => recordTake(browser, url, actions, paths, r.log, {
19053
- actionsDir: dirname11(resolve21(actionsPath)),
19244
+ actionsDir: dirname12(resolve21(actionsPath)),
19054
19245
  maxDurationSeconds,
19055
19246
  storageState,
19056
19247
  headers,
@@ -19189,7 +19380,7 @@ async function cmdCreate2(argv) {
19189
19380
  sessionName,
19190
19381
  actions,
19191
19382
  (context2) => recordTake(browser, url, actions, paths, r.log, {
19192
- actionsDir: dirname11(resolve21(actionsPath)),
19383
+ actionsDir: dirname12(resolve21(actionsPath)),
19193
19384
  maxDurationSeconds,
19194
19385
  storageState,
19195
19386
  headers,
@@ -19316,7 +19507,7 @@ async function cmdPlan(argvIn) {
19316
19507
  );
19317
19508
  const r = createReporter(flags.json === true);
19318
19509
  if (flags.fresh === true) {
19319
- await rm6(join33(dir, "doc.json"), { force: true });
19510
+ await rm7(join33(dir, "doc.json"), { force: true });
19320
19511
  r.log("note: --fresh discarded the existing doc.json");
19321
19512
  }
19322
19513
  let reuse;
@@ -19329,7 +19520,7 @@ async function cmdPlan(argvIn) {
19329
19520
  }
19330
19521
  reuse = {
19331
19522
  from,
19332
- doc: JSON.parse(await readFile18(from, "utf8"))
19523
+ doc: JSON.parse(await readFile19(from, "utf8"))
19333
19524
  };
19334
19525
  }
19335
19526
  const style = await resolveStyleRef(flags);
@@ -19801,7 +19992,7 @@ async function resolveDocRef(ref, flags, what) {
19801
19992
  return {
19802
19993
  from: file,
19803
19994
  doc: migrateHostedDoc4(
19804
- JSON.parse(await readFile18(file, "utf8"))
19995
+ JSON.parse(await readFile19(file, "utf8"))
19805
19996
  )
19806
19997
  };
19807
19998
  }
@@ -19853,7 +20044,7 @@ async function cmdDigest(argv) {
19853
20044
  const take = await loadTake(dir);
19854
20045
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
19855
20046
  const transcriptPath = strFlag(flags, "transcript");
19856
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile18(transcriptPath, "utf8"))) : null;
20047
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile19(transcriptPath, "utf8"))) : null;
19857
20048
  const style = await resolveStyleRef(flags);
19858
20049
  const noFrames = flags["no-frames"] === true;
19859
20050
  let browser = null;
@@ -19966,7 +20157,7 @@ async function cmdCallout(argv) {
19966
20157
  `no register: put a BRAND.md beside the take (vos brand <url> --out BRAND.md; its bgA and accent are the ground and the accent), or pass --ground #hex --accent #hex`
19967
20158
  );
19968
20159
  const codeFile = strFlag(flags, "code-file");
19969
- const code = codeFile ? await readFile18(codeFile, "utf8") : strFlag(flags, "code");
20160
+ const code = codeFile ? await readFile19(codeFile, "utf8") : strFlag(flags, "code");
19970
20161
  const words2 = {
19971
20162
  kicker: strFlag(flags, "kicker"),
19972
20163
  title: strFlag(flags, "title"),
@@ -20079,7 +20270,7 @@ async function cmdValidate(argv) {
20079
20270
  }
20080
20271
  if (!isTakeDir(target) && existsSync30(join33(target, "config.json"))) {
20081
20272
  const parsed = JSON.parse(
20082
- await readFile18(join33(target, "config.json"), "utf8")
20273
+ await readFile19(join33(target, "config.json"), "utf8")
20083
20274
  );
20084
20275
  const pre = preflightConfig(parsed);
20085
20276
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -20237,16 +20428,19 @@ async function cmdPush3(argv) {
20237
20428
  const dir = positionals[0];
20238
20429
  if (!dir)
20239
20430
  throw new UsageError(
20240
- "vos push <take> [--vos|--title|--label|--note|--prompt|--folder|--override|--yes|--key|--api|--bake|--claimable [--share]]"
20431
+ "vos push <take> [--vos|--title|--desc|--label|--note|--prompt|--folder|--override|--yes|--key|--api|--bake|--claimable [--share]]"
20241
20432
  );
20242
20433
  const refusal = takePushRefusal({
20243
20434
  vos: strFlag(flags, "vos"),
20244
20435
  trackedVosId: readSyncState(resolve21(dir))?.vosId ?? null,
20245
- programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
20436
+ programOnly: ["slug", "tags", "base", "remix-of", "still"].filter(
20246
20437
  (name) => hasFlag(flags, name)
20247
20438
  )
20248
20439
  });
20249
20440
  if (refusal) throw new UsageError(refusal);
20441
+ const description = strFlag(flags, "desc");
20442
+ const descProblem = descriptionProblem(description);
20443
+ if (descProblem) throw new UsageError(descProblem);
20250
20444
  const r = createReporter(flags.json === true);
20251
20445
  if (flags.claimable === true) {
20252
20446
  const iterating = ["vos", "folder", "override", "base"].filter(
@@ -20263,6 +20457,7 @@ async function cmdPush3(argv) {
20263
20457
  origin: strFlag(flags, "origin"),
20264
20458
  title: strFlag(flags, "title"),
20265
20459
  prompt: strFlag(flags, "prompt"),
20460
+ description,
20266
20461
  share: flags.share === true,
20267
20462
  bake: flags.bake === true
20268
20463
  },
@@ -20293,6 +20488,7 @@ reference and the only credential.`
20293
20488
  folder: strFlag(flags, "folder"),
20294
20489
  overrides,
20295
20490
  prompt: strFlag(flags, "prompt"),
20491
+ description,
20296
20492
  bake: flags.bake === true
20297
20493
  },
20298
20494
  r
@@ -20684,4 +20880,4 @@ export {
20684
20880
  verbHelp,
20685
20881
  run
20686
20882
  };
20687
- //# sourceMappingURL=chunk-HDJFCTOG.js.map
20883
+ //# sourceMappingURL=chunk-DM7ADA77.js.map