@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/index.html +147 -138
- package/dist/server.mjs +139 -24
- package/node_modules/@higgsfield/higgsedit/dist/chunk-C6SASQHE.mjs +10 -76
- package/node_modules/@higgsfield/higgsedit/dist/chunk-QLPCYN6K.mjs +22 -5
- package/node_modules/@higgsfield/higgsedit/dist/chunk-VI6IV6JP.mjs +35 -19
- package/node_modules/@higgsfield/higgsedit/dist/higgsedit.mjs +14 -24
- package/node_modules/@higgsfield/higgsedit/dist/history.mjs +295 -0
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
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.
|
|
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
|
-
|
|
41878
|
+
let value;
|
|
41809
41879
|
try {
|
|
41810
|
-
|
|
41880
|
+
value = JSON.parse(result.stdout);
|
|
41811
41881
|
} catch {
|
|
41812
|
-
|
|
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.
|
|
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 = [
|
|
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 =
|
|
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`;
|
|
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
|
|
42430
|
-
const
|
|
42431
|
-
|
|
42432
|
-
|
|
42433
|
-
);
|
|
42434
|
-
return { seq
|
|
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}
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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)
|
|
242234
|
-
|
|
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
|
|
242527
|
-
const
|
|
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(
|
|
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
|
};
|