@higgsfield/video-editor-mcp 1.0.4 → 1.0.6

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/server.mjs CHANGED
@@ -41515,6 +41515,39 @@ function registerAppResource(server, name, uri, config2, readCallback) {
41515
41515
  );
41516
41516
  }
41517
41517
 
41518
+ // src/tools/video-editor-local/auth.ts
41519
+ var AUTH_DEADLINE_MS = 4 * 6e4;
41520
+ var ERROR_MESSAGE = "Could not connect to Higgsfield. Complete browser sign-in, then try again.";
41521
+ var profileSchema = external_exports.object({ id: external_exports.string().regex(/\S/) });
41522
+ function createEditorAuth(readProfile) {
41523
+ let state = { status: "idle" };
41524
+ let pending;
41525
+ return {
41526
+ status: () => state,
41527
+ start() {
41528
+ if (pending) return state;
41529
+ state = { status: "pending" };
41530
+ const controller = new AbortController();
41531
+ const timer = setTimeout(() => {
41532
+ state = { status: "error", message: ERROR_MESSAGE };
41533
+ controller.abort();
41534
+ }, AUTH_DEADLINE_MS);
41535
+ timer.unref();
41536
+ pending = Promise.resolve().then(() => readProfile(controller.signal)).then((result) => {
41537
+ if (controller.signal.aborted) return;
41538
+ if (result.isError) throw new Error("Identity lookup failed");
41539
+ state = { status: "ready", profile: profileSchema.parse(result.structuredContent) };
41540
+ }).catch(() => {
41541
+ if (!controller.signal.aborted) state = { status: "error", message: ERROR_MESSAGE };
41542
+ }).finally(() => {
41543
+ clearTimeout(timer);
41544
+ pending = void 0;
41545
+ });
41546
+ return state;
41547
+ }
41548
+ };
41549
+ }
41550
+
41518
41551
  // src/tools/video-editor-local/hosted-proxy.ts
41519
41552
  var HOSTED_MCP_URL = "https://openai-mcp.higgsfield.ai/mcp";
41520
41553
  var HOSTED_PANEL_TOOLS = [
@@ -41572,7 +41605,7 @@ function registerHostedProxyTools(server, url2, session = new LocalBrowserSessio
41572
41605
  const previous = connected?.client;
41573
41606
  const next = new Client({
41574
41607
  name: "higgsfield-video-editor-local",
41575
- version: "1.0.4"
41608
+ version: "1.0.6"
41576
41609
  });
41577
41610
  const transport = new StreamableHTTPClientTransport(new URL(url2), {
41578
41611
  requestInit: { headers: { Authorization: `Bearer ${token}` } }
@@ -41587,6 +41620,43 @@ function registerHostedProxyTools(server, url2, session = new LocalBrowserSessio
41587
41620
  }), 6e4).unref();
41588
41621
  return pending;
41589
41622
  }
41623
+ const identity2 = createEditorAuth(async (signal) => {
41624
+ const token = await session.token();
41625
+ signal.throwIfAborted();
41626
+ const next = new Client({ name: "higgsfield-video-editor-identity", version: "1" });
41627
+ const transport = new StreamableHTTPClientTransport(new URL(url2), {
41628
+ requestInit: { headers: { Authorization: `Bearer ${token}` } }
41629
+ });
41630
+ const abort = () => {
41631
+ void next.close().catch(() => void 0);
41632
+ };
41633
+ signal.addEventListener("abort", abort, { once: true });
41634
+ try {
41635
+ await next.connect(transport, { signal, timeout: 3e4 });
41636
+ return await next.callTool({ name: "get_profile", arguments: {} }, void 0, {
41637
+ signal,
41638
+ timeout: 3e4
41639
+ });
41640
+ } finally {
41641
+ signal.removeEventListener("abort", abort);
41642
+ await next.close().catch(() => void 0);
41643
+ }
41644
+ });
41645
+ registerAppTool(
41646
+ server,
41647
+ "video_editing_auth",
41648
+ {
41649
+ title: "Connect the local video editor",
41650
+ description: "Start or inspect the local editor's Higgsfield sign-in without waiting for the browser.",
41651
+ inputSchema: external_exports.strictObject({ action: external_exports.enum(["start", "status"]) }),
41652
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
41653
+ _meta: { ui: { visibility: ["app"] } }
41654
+ },
41655
+ async ({ action }) => {
41656
+ const state = action === "start" ? identity2.start() : identity2.status();
41657
+ return { content: [], structuredContent: state };
41658
+ }
41659
+ );
41590
41660
  for (const name of HOSTED_PANEL_TOOLS)
41591
41661
  registerAppTool(
41592
41662
  server,
@@ -41805,12 +41875,15 @@ function parseCliJson(result) {
41805
41875
  throw new Error(
41806
41876
  (result.stderr || result.stdout).trim().slice(-4e3) || `higgsedit exited ${result.code}`
41807
41877
  );
41808
- const text = result.stdout.trim();
41878
+ let value;
41809
41879
  try {
41810
- return JSON.parse(text);
41880
+ value = JSON.parse(result.stdout);
41811
41881
  } catch {
41812
- return text.slice(-2e4);
41882
+ throw new Error("higgsedit returned invalid or incomplete JSON.");
41813
41883
  }
41884
+ if (value === null || typeof value !== "object" || Array.isArray(value))
41885
+ throw new Error("higgsedit returned an unexpected JSON response; expected an object.");
41886
+ return value;
41814
41887
  }
41815
41888
  var cliJson = async (args, cwd, timeoutMs) => parseCliJson(await runCli(args, cwd, timeoutMs));
41816
41889
 
@@ -41823,8 +41896,8 @@ var directCspOrigins = () => cspOrigins;
41823
41896
  var CORS = {
41824
41897
  "access-control-allow-origin": "*",
41825
41898
  "access-control-allow-methods": "GET, HEAD, POST, PUT, DELETE, OPTIONS",
41826
- "access-control-allow-headers": "range, content-type",
41827
- "access-control-expose-headers": "content-range, content-length, accept-ranges, content-type",
41899
+ "access-control-allow-headers": "range, content-type, if-none-match, if-modified-since, if-range",
41900
+ "access-control-expose-headers": "content-range, content-length, accept-ranges, content-type, etag, last-modified, cache-control",
41828
41901
  // An https page reaching loopback (Private Network Access / Local Network Access preflights).
41829
41902
  "access-control-allow-private-network": "true",
41830
41903
  "cross-origin-resource-policy": "cross-origin",
@@ -41836,7 +41909,8 @@ var FORWARDED = [
41836
41909
  "content-length",
41837
41910
  "accept",
41838
41911
  "if-none-match",
41839
- "if-modified-since"
41912
+ "if-modified-since",
41913
+ "if-range"
41840
41914
  ];
41841
41915
  var DirectServer = class {
41842
41916
  constructor(origin, ports = DIRECT_PORTS) {
@@ -42175,7 +42249,7 @@ var restyleStyleSchema = external_exports.object({
42175
42249
  });
42176
42250
 
42177
42251
  // src/tools/video-editor-local/standalone-resource.ts
42178
- var packageVersion = "1.0.4";
42252
+ var packageVersion = "1.0.6";
42179
42253
  var VIDEO_EDITOR_RESOURCE_URI = packageVersion ? `ui://higgsfield/video-editor-v3-${encodeURIComponent(packageVersion)}.html` : "ui://higgsfield/video-editor-v3.html";
42180
42254
  function panelHtmlPath(env = process.env) {
42181
42255
  return env.VIDEO_EDITOR_LOCAL_HTML?.trim() || join5(import.meta.dirname, "index.html");
@@ -42226,10 +42300,48 @@ import { mkdtemp, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
42226
42300
  import { tmpdir } from "node:os";
42227
42301
  import { join as join6 } from "node:path";
42228
42302
  var MAX_CHUNK = 2 << 20;
42229
- var KEPT_HEADERS = ["content-type", "content-range", "content-length", "accept-ranges"];
42303
+ var KEPT_HEADERS = [
42304
+ "content-type",
42305
+ "content-range",
42306
+ "content-length",
42307
+ "accept-ranges",
42308
+ "etag",
42309
+ "last-modified",
42310
+ "cache-control"
42311
+ ];
42312
+ var CONDITIONAL_HEADERS = ["if-none-match", "if-modified-since", "if-range"];
42313
+ async function readReply(res, method) {
42314
+ if (method === "HEAD" || res.status === 304 || res.status === 204) return Buffer.alloc(0);
42315
+ if (Number(res.headers.get("content-length")) > MAX_CHUNK) {
42316
+ await res.body?.cancel();
42317
+ throw new Error("response exceeds the tool chunk limit; ask for a range");
42318
+ }
42319
+ const reader = res.body?.getReader();
42320
+ if (!reader) return Buffer.alloc(0);
42321
+ const chunks = [];
42322
+ let size = 0;
42323
+ try {
42324
+ for (; ; ) {
42325
+ const { done, value } = await reader.read();
42326
+ if (done) return Buffer.concat(chunks, size);
42327
+ size += value.byteLength;
42328
+ if (size > MAX_CHUNK) {
42329
+ await reader.cancel();
42330
+ throw new Error("response exceeds the tool chunk limit; ask for a range");
42331
+ }
42332
+ chunks.push(value);
42333
+ }
42334
+ } finally {
42335
+ reader.releaseLock();
42336
+ }
42337
+ }
42230
42338
  async function httpTool(origin, call) {
42231
42339
  checkPath(call.path);
42232
42340
  const headers = new Headers();
42341
+ for (const name of CONDITIONAL_HEADERS) {
42342
+ const value = call.headers?.[name];
42343
+ if (value !== void 0) headers.set(name, value);
42344
+ }
42233
42345
  if (call.rangeStart !== void 0) {
42234
42346
  const end = Math.min(call.rangeEnd ?? Infinity, call.rangeStart + MAX_CHUNK - 1);
42235
42347
  headers.set("range", `bytes=${call.rangeStart}-${end}`);
@@ -42252,9 +42364,7 @@ async function send(origin, method, path2, headers, body, signal) {
42252
42364
  await res.body?.cancel();
42253
42365
  throw new Error(STREAMING_IMPORT_REQUIRED);
42254
42366
  }
42255
- const bytes = Buffer.from(await res.arrayBuffer());
42256
- if (bytes.length > MAX_CHUNK && res.status !== 206)
42257
- throw new Error(`${path2}: ${bytes.length} bytes without a range; ask for a range`);
42367
+ const bytes = await readReply(res, method);
42258
42368
  const kept = {};
42259
42369
  for (const name of KEPT_HEADERS) {
42260
42370
  const value = res.headers.get(name);
@@ -42375,7 +42485,6 @@ var reload = (undo) => ({
42375
42485
  detectedAt: Date.now(),
42376
42486
  undo
42377
42487
  });
42378
- var NO_UNDO = { canUndo: false, canRedo: false, undoName: null, redoName: null };
42379
42488
  var EventFeed = class {
42380
42489
  constructor(origin) {
42381
42490
  this.origin = origin;
@@ -42424,14 +42533,14 @@ var EventFeed = class {
42424
42533
  holds(after) {
42425
42534
  return after >= (this.log[0]?.seq ?? this.seq + 1) - 1 && after <= this.seq;
42426
42535
  }
42427
- /** Events after `after`; waits up to `waitMs` for one. `after < 0` returns the current position at once. */
42536
+ /** Events after `after`; a new reader gets one full resync with current undo, not historical edits. */
42428
42537
  async next(after, waitMs) {
42429
- if (after >= 0 && !this.holds(after)) {
42430
- const undo = await fetch(`${this.origin}/api/history`).then(
42431
- (r) => r.json(),
42432
- () => NO_UNDO
42433
- );
42434
- return { seq: this.seq, events: [reload(undo)] };
42538
+ if (after < 0 || !this.holds(after)) {
42539
+ const seq = this.seq;
42540
+ const response = await fetch(`${this.origin}/api/history`, { signal: this.abort.signal });
42541
+ if (!response.ok) throw new Error(`history: ${response.status}`);
42542
+ const undo = await response.json();
42543
+ return { seq, events: [reload(undo)] };
42435
42544
  }
42436
42545
  if (after >= 0 && after >= this.seq) {
42437
42546
  await new Promise((resolve3) => {
@@ -43005,7 +43114,7 @@ async function initProject(path2, roots, size = "1080x1920", fps = 30) {
43005
43114
  if (await hasProjectFile(dir)) return dir;
43006
43115
  cliPath();
43007
43116
  await mkdir5(dir, { recursive: true });
43008
- await cliJson(["init", dir, `--size=${size}`, `--fps=${fps}`], dir);
43117
+ await cliJson(["init", dir, `--size=${size}`, `--fps=${fps}`, "--json"], dir);
43009
43118
  return dir;
43010
43119
  }
43011
43120
  async function newFile(dir, name, place) {
@@ -43218,7 +43327,7 @@ ${LOCAL_PROJECT_GUIDE}`);
43218
43327
  annotations: { readOnlyHint: true, openWorldHint: false }
43219
43328
  },
43220
43329
  async ({ project }) => scoped(project, async (session) => {
43221
- const outline = await cliJson(["outline", ".", "--json"], session.root);
43330
+ const outline = external_exports.looseObject({ text: external_exports.string().optional() }).parse(await cliJson(["outline", ".", "--json"], session.root));
43222
43331
  const { text, ...rest } = outline;
43223
43332
  return reply({ ...describe3(session), ...rest }, text ?? JSON.stringify(rest));
43224
43333
  })
@@ -43374,6 +43483,7 @@ ${LOCAL_PROJECT_GUIDE}`);
43374
43483
  [
43375
43484
  "sheet",
43376
43485
  ".",
43486
+ "--json",
43377
43487
  `--times=${times.join(",")}`,
43378
43488
  "-o",
43379
43489
  out,
@@ -43722,6 +43832,11 @@ function registerPanelRelay(server, store, stateDirectory, direct) {
43722
43832
  path: external_exports.string().min(1).max(4096),
43723
43833
  rangeStart: external_exports.number().int().nonnegative().optional(),
43724
43834
  rangeEnd: external_exports.number().int().nonnegative().optional(),
43835
+ headers: external_exports.object({
43836
+ "if-none-match": external_exports.string().max(8192).optional(),
43837
+ "if-modified-since": external_exports.string().max(128).optional(),
43838
+ "if-range": external_exports.string().max(128).optional()
43839
+ }).optional(),
43725
43840
  body: external_exports.string().max(64 * 1024 * 1024).optional(),
43726
43841
  project: sessionArg
43727
43842
  },
@@ -44515,7 +44630,7 @@ function createEditorUpdates(options, start = (done) => new Promise((resolve3, r
44515
44630
  }
44516
44631
  }
44517
44632
  return {
44518
- runningVersion: options?.version ?? "1.0.4" ?? "Source checkout",
44633
+ runningVersion: options?.version ?? "1.0.6" ?? "Source checkout",
44519
44634
  readyVersion: options && selectedDist(options) !== options.dist ? state.active : null,
44520
44635
  channel: options?.channel ?? null,
44521
44636
  supported: options !== null,
@@ -44579,7 +44694,7 @@ var handle = serveStdio(
44579
44694
  const server = new McpServer(
44580
44695
  {
44581
44696
  name: "Higgsfield Video Editor (local)",
44582
- version: "1.0.4"
44697
+ version: "1.0.6"
44583
44698
  },
44584
44699
  { instructions: VIDEO_EDITOR_LOCAL_INSTRUCTIONS }
44585
44700
  );
@@ -17,7 +17,7 @@ import {
17
17
  } from "./chunk-QLPCYN6K.mjs";
18
18
  import {
19
19
  analyze,
20
- applyEdit
20
+ applyEdits
21
21
  } from "./chunk-VI6IV6JP.mjs";
22
22
  import {
23
23
  inside,
@@ -94,73 +94,7 @@ var WIDGET_DIST = join(EDITOR, "dist-mcp");
94
94
  var WIDGET = join(WIDGET_DIST, "mcp-app.js");
95
95
 
96
96
  // src/history.ts
97
- import { readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
98
- import { join as join2 } from "node:path";
99
- var EditHistory = class {
100
- constructor(dir, onWrite) {
101
- this.dir = dir;
102
- this.onWrite = onWrite;
103
- }
104
- dir;
105
- onWrite;
106
- undoStack = [];
107
- redoStack = [];
108
- begin(name) {
109
- const entry = { name, files: /* @__PURE__ */ new Map() };
110
- return {
111
- onWrite: (path, text) => {
112
- const prior = entry.files.get(path);
113
- entry.files.set(path, { before: prior?.before ?? readFileSync2(join2(this.dir, path), "utf8"), after: text });
114
- this.onWrite?.(path, text);
115
- },
116
- commit: () => {
117
- if (!entry.files.size) return;
118
- this.undoStack.push(entry);
119
- this.redoStack.length = 0;
120
- },
121
- rollback: () => {
122
- for (const [path, texts] of entry.files) this.write(path, texts.before);
123
- }
124
- };
125
- }
126
- state() {
127
- return {
128
- canUndo: this.undoStack.length > 0,
129
- canRedo: this.redoStack.length > 0,
130
- undoName: this.undoStack.at(-1)?.name ?? null,
131
- redoName: this.redoStack.at(-1)?.name ?? null
132
- };
133
- }
134
- undo() {
135
- return this.step(this.undoStack, this.redoStack, "after", "before");
136
- }
137
- redo() {
138
- return this.step(this.redoStack, this.undoStack, "before", "after");
139
- }
140
- step(from, to, expect, write) {
141
- const entry = from.at(-1);
142
- if (!entry) return { ok: false, reason: `nothing to ${write === "before" ? "undo" : "redo"}` };
143
- for (const [path, texts] of entry.files) {
144
- let current;
145
- try {
146
- current = readFileSync2(join2(this.dir, path), "utf8");
147
- } catch {
148
- return { ok: false, reason: `${path} no longer exists` };
149
- }
150
- if (current !== texts[expect]) return { ok: false, reason: `${path} changed outside the editor since ${entry.name}` };
151
- }
152
- for (const [path, texts] of entry.files) this.write(path, texts[write]);
153
- from.pop();
154
- to.push(entry);
155
- return { ok: true, written: [...entry.files.keys()] };
156
- }
157
- write(path, text) {
158
- const target = join2(this.dir, path);
159
- this.onWrite?.(path, text);
160
- writeFileSync(`${target}.higgsedit-${process.pid}.tmp`, text);
161
- renameSync(`${target}.higgsedit-${process.pid}.tmp`, target);
162
- }
163
- };
97
+ import { EditHistory } from "./history.mjs";
164
98
 
165
99
  import { Proxies } from "./proxy.mjs";
166
100
 
@@ -300,15 +234,15 @@ async function startDev(dir, port = 0, options = {}) {
300
234
  if (url.pathname !== "/api/edit" || req.method !== "POST") return false;
301
235
  const body = JSON.parse((await readBody(req)).toString());
302
236
  const tx = history.begin(body.name ?? "Edit");
303
- const results = [];
304
- for (const { key, edit } of body.edits) {
305
- const r = applyEdit(dir, key, edit, { onWrite: tx.onWrite, ...body.dryRun ? { dryRun: true } : {} });
306
- results.push(r);
307
- if (!r.ok) break;
237
+ try {
238
+ const results = applyEdits(dir, body.edits, { onWrite: tx.onWrite, ...body.dryRun ? { dryRun: true } : {} });
239
+ if (results.every((r) => r.ok)) tx.commit();
240
+ else tx.rollback();
241
+ json(res, 200, { ok: results.every((r) => r.ok), results, undo: history.state() });
242
+ } catch (error) {
243
+ tx.rollback();
244
+ throw error;
308
245
  }
309
- if (results.every((r) => r.ok)) tx.commit();
310
- else tx.rollback();
311
- json(res, 200, { ok: results.every((r) => r.ok), results, undo: history.state() });
312
246
  return true;
313
247
  },
314
248
  (req, res, url) => {
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
2
3
  import { createRequire as __higgseditRequire } from "node:module";
3
4
  import { fileURLToPath as __higgseditPath } from "node:url";
4
5
  const require = __higgseditRequire(import.meta.url);
@@ -131,10 +132,14 @@ var TYPES = {
131
132
  };
132
133
  function sendFile(req, res, file) {
133
134
  let size;
135
+ let etag;
136
+ let modified;
134
137
  try {
135
- const st = statSync(file);
138
+ const st = statSync(file, { bigint: true });
136
139
  if (!st.isFile()) throw new Error("not a file");
137
- size = st.size;
140
+ size = Number(st.size);
141
+ etag = `"${createHash("sha256").update(`${st.dev}:${st.ino}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`).digest("hex")}"`;
142
+ modified = Math.floor(Number(st.mtimeNs > st.ctimeNs ? st.mtimeNs : st.ctimeNs) / 1e9) * 1e3;
138
143
  } catch {
139
144
  res.statusCode = 404;
140
145
  res.end("not found");
@@ -143,11 +148,23 @@ function sendFile(req, res, file) {
143
148
  res.setHeader("content-type", TYPES[extname(file).toLowerCase()] ?? "application/octet-stream");
144
149
  res.setHeader("accept-ranges", "bytes");
145
150
  res.setHeader("cache-control", "no-cache");
146
- const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "");
151
+ res.setHeader("etag", etag);
152
+ res.setHeader("last-modified", new Date(modified).toUTCString());
153
+ const readable = req.method === "GET" || req.method === "HEAD";
154
+ const noneMatch = req.headers["if-none-match"];
155
+ const unmodified = noneMatch !== void 0 ? noneMatch.split(",").some((value) => value.trim() === "*" || value.trim().replace(/^W\//, "") === etag) : Date.parse(req.headers["if-modified-since"] ?? "") >= modified;
156
+ if (readable && unmodified) {
157
+ res.statusCode = 304;
158
+ res.end();
159
+ return;
160
+ }
161
+ const ifRange = req.headers["if-range"];
162
+ const sameRange = !ifRange || ifRange === etag || modified <= Date.now() - 6e4 && typeof ifRange === "string" && Date.parse(ifRange) === modified;
163
+ const range = req.method === "GET" && sameRange ? /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "") : null;
147
164
  if (range) {
148
- const start = range[1] ? Number(range[1]) : size - Number(range[2]);
165
+ const start = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
149
166
  const end = range[1] && range[2] ? Math.min(Number(range[2]), size - 1) : size - 1;
150
- if (start > end || start >= size) {
167
+ if (!range[1] && !Number(range[2]) || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end || start >= size) {
151
168
  res.statusCode = 416;
152
169
  res.setHeader("content-range", `bytes */${size}`);
153
170
  res.end();
@@ -242191,7 +242191,7 @@ function literalAt(root, path, at) {
242191
242191
  }
242192
242192
  return node;
242193
242193
  }
242194
- function setProp(project, landed, scope, name, value) {
242194
+ function setProp(project, landed, scope, name, value, validateOnly = false) {
242195
242195
  const { tag } = landed;
242196
242196
  const at = line(tag);
242197
242197
  const [prop, ...path] = name.split(".");
@@ -242210,18 +242210,18 @@ function setProp(project, landed, scope, name, value) {
242210
242210
  if (ctx.component && ctx.component.callSites.length > 1) {
242211
242211
  refuse("shared-component", `<${tagName(tag)}> is inside <${ctx.component.name}>, used at ${ctx.component.callSites.length} call sites; pass ${prop} as a prop to edit one instance`, tag);
242212
242212
  }
242213
- addAttribute(tag, prop, initializerText(value));
242213
+ if (!validateOnly) addAttribute(tag, prop, initializerText(value));
242214
242214
  return at;
242215
242215
  }
242216
242216
  const init = attr.getInitializer();
242217
242217
  if (!init) {
242218
242218
  if (path.length) refuse("invalid", `${prop} is a bare boolean`, tag);
242219
242219
  if (remove) {
242220
- attr.remove();
242220
+ if (!validateOnly) attr.remove();
242221
242221
  return at;
242222
242222
  }
242223
242223
  if (value === true) return at;
242224
- attr.setInitializer(initializerText(value));
242224
+ if (!validateOnly) attr.setInitializer(initializerText(value));
242225
242225
  return at;
242226
242226
  }
242227
242227
  const expr = init.isKind(import_ts_morph3.SyntaxKind.JsxExpression) ? init.getExpression() : init;
@@ -242230,12 +242230,13 @@ function setProp(project, landed, scope, name, value) {
242230
242230
  if (ctx.component && ctx.component.callSites.length > 1) {
242231
242231
  refuse("shared-component", `${prop} is a literal inside <${ctx.component.name}>, which has ${ctx.component.callSites.length} call sites; pass it as a prop to edit one instance`, attr, attr.getText());
242232
242232
  }
242233
- if (remove) attr.remove();
242234
- else if (path.length) {
242233
+ if (remove) {
242234
+ if (!validateOnly) attr.remove();
242235
+ } else if (path.length) {
242235
242236
  const node = literalAt(expr, path, attr);
242236
- node.replaceWithText(literalText(value));
242237
+ if (!validateOnly) node.replaceWithText(literalText(value));
242237
242238
  } else {
242238
- attr.setInitializer(initializerText(value));
242239
+ if (!validateOnly) attr.setInitializer(initializerText(value));
242239
242240
  }
242240
242241
  return at;
242241
242242
  }
@@ -242244,7 +242245,7 @@ function setProp(project, landed, scope, name, value) {
242244
242245
  const forwarded = forwardedProp(expr, ctx);
242245
242246
  if (forwarded) {
242246
242247
  const site = callSiteFor(ctx, scope, attr);
242247
- return setProp(project, site, site.scope, [forwarded, ...path].join("."), value);
242248
+ return setProp(project, site, site.scope, [forwarded, ...path].join("."), value, validateOnly);
242248
242249
  }
242249
242250
  return refuse("expression", `code-driven: ${prop} is bound to an expression`, attr, attr.getText());
242250
242251
  }
@@ -242262,13 +242263,14 @@ function saidRange(child) {
242262
242263
  return [child.getPos() + lead, child.getEnd() - trail];
242263
242264
  }
242264
242265
  var jsxText = (text) => /^[^\s{}<>&][^\n{}<>&]*[^\s{}<>&]$|^[^\s{}<>&]$/.test(text) ? text : `{${JSON.stringify(text)}}`;
242265
- function setText(project, landed, scope, text) {
242266
+ function setText(project, landed, scope, text, validateOnly = false) {
242266
242267
  const { tag } = landed;
242267
242268
  const at = line(tag);
242268
242269
  const element = elementOf(tag);
242269
242270
  const sf = tag.getSourceFile();
242270
242271
  if (element.isKind(import_ts_morph3.SyntaxKind.JsxSelfClosingElement)) {
242271
242272
  if (tagName(tag) !== "text") refuse("invalid", `<${tagName(tag)}> has no text`, tag);
242273
+ if (validateOnly) return at;
242272
242274
  const opening = element.getText().replace(/\s*\/>$/, ">");
242273
242275
  sf.replaceText([element.getStart(), element.getEnd()], `${opening}${jsxText(text)}</${tagName(tag)}>`);
242274
242276
  return at;
@@ -242279,7 +242281,7 @@ function setText(project, landed, scope, text) {
242279
242281
  if (expressions.length) {
242280
242282
  const e = expressions[0].asKindOrThrow(import_ts_morph3.SyntaxKind.JsxExpression).getExpressionOrThrow();
242281
242283
  if (isLiteral(e) && said.length === 1) {
242282
- e.replaceWithText(literalText(text));
242284
+ if (!validateOnly) e.replaceWithText(literalText(text));
242283
242285
  return at;
242284
242286
  }
242285
242287
  const loopVar = ctx.inLoop ? mentions(e, ctx.loopNames) : void 0;
@@ -242287,14 +242289,15 @@ function setText(project, landed, scope, text) {
242287
242289
  const forwarded = said.length === 1 ? forwardedProp(e, ctx) : void 0;
242288
242290
  if (forwarded) {
242289
242291
  const site = callSiteFor(ctx, scope, e);
242290
- if (forwarded === "children") return setText(project, site, site.scope, text);
242291
- return setProp(project, site, site.scope, forwarded, text);
242292
+ if (forwarded === "children") return setText(project, site, site.scope, text, validateOnly);
242293
+ return setProp(project, site, site.scope, forwarded, text, validateOnly);
242292
242294
  }
242293
242295
  refuse("expression", `code-driven: the text is computed by {${e.getText()}}`, e, e.getText());
242294
242296
  }
242295
242297
  if (ctx.component && ctx.component.callSites.length > 1) {
242296
242298
  refuse("shared-component", `this text is inside <${ctx.component.name}>, used at ${ctx.component.callSites.length} call sites`, tag);
242297
242299
  }
242300
+ if (validateOnly) return at;
242298
242301
  if (!said.length) {
242299
242302
  sf.insertText(element.getOpeningElement().getEnd(), jsxText(text));
242300
242303
  return at;
@@ -242407,7 +242410,22 @@ function finish(project, target, newId, options) {
242407
242410
  return { ok: true, written, target: target(), ...newId ? { id: newId } : {} };
242408
242411
  }
242409
242412
  function applyEdit(dir, key, edit, options = {}) {
242413
+ return applyProjectEdit(new SourceProject(dir), key, edit, options);
242414
+ }
242415
+ function applyEdits(dir, edits, options = {}) {
242410
242416
  const project = new SourceProject(dir);
242417
+ const results = [];
242418
+ for (const { key, edit } of edits) {
242419
+ const before = new Map([...project.files].map(([path, file]) => [path, file.sourceFile.getFullText()]));
242420
+ const result = applyProjectEdit(project, key, edit, { dryRun: true });
242421
+ if (result.ok) result.written = [...project.files].filter(([path, file]) => file.sourceFile.getFullText() !== before.get(path)).map(([path]) => path);
242422
+ results.push(result);
242423
+ if (!result.ok) return results;
242424
+ }
242425
+ if (!options.dryRun) project.save(options.onWrite);
242426
+ return results;
242427
+ }
242428
+ function applyProjectEdit(project, key, edit, options) {
242411
242429
  try {
242412
242430
  if (edit.op === "set-const") {
242413
242431
  const landed2 = setConst(project, edit);
@@ -242521,11 +242539,9 @@ function describeProp(project, found, scope, name) {
242521
242539
  const init = attr?.getInitializer();
242522
242540
  const expr = init?.isKind(import_ts_morph3.SyntaxKind.JsxExpression) ? init.getExpression() : init;
242523
242541
  const source = attr?.getText();
242524
- const probe = new SourceProject(project.dir, [...project.files.keys()]);
242525
242542
  try {
242526
- const target = findById(probe, idOf(found.tag));
242527
- const origin = line(target.tag);
242528
- const landed = setProp(probe, target, scope, name, 0);
242543
+ const origin = line(found.tag);
242544
+ const landed = setProp(project, found, scope, name, 0, true);
242529
242545
  const followed = landed.file !== origin.file || landed.line !== origin.line;
242530
242546
  if (!attr) return { name, kind: "absent", editable: true };
242531
242547
  if (followed && landed.id) {
@@ -242549,12 +242565,11 @@ var forwardedName = (expr) => {
242549
242565
  return void 0;
242550
242566
  };
242551
242567
  function describeText(project, found, scope) {
242552
- const probe = new SourceProject(project.dir, [...project.files.keys()]);
242553
242568
  const element = elementOf(found.tag);
242554
242569
  const said = element.isKind(import_ts_morph3.SyntaxKind.JsxElement) ? element.getJsxChildren().filter(isSaid) : [];
242555
242570
  const source = said.map((c) => c.getText()).join("").trim();
242556
242571
  try {
242557
- setText(probe, findById(probe, idOf(found.tag)), scope, "x");
242572
+ setText(project, found, scope, "x", true);
242558
242573
  const literal = said.length === 1 && said[0].isKind(import_ts_morph3.SyntaxKind.JsxText);
242559
242574
  return { name: "text", kind: literal || !said.length ? "literal" : "call-site", editable: true, ...source ? { source } : {} };
242560
242575
  } catch (error) {
@@ -242573,6 +242588,7 @@ export {
242573
242588
  stampProject,
242574
242589
  parseKey,
242575
242590
  applyEdit,
242591
+ applyEdits,
242576
242592
  analyze,
242577
242593
  sourceFacts
242578
242594
  };