@monkey-mini-app/host 0.1.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/dist/index.d.ts +782 -0
- package/dist/index.js +3603 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3603 @@
|
|
|
1
|
+
import * as fs4 from 'fs';
|
|
2
|
+
import fs4__default, { mkdtempSync, readdirSync, statSync, mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs';
|
|
3
|
+
import os, { tmpdir, homedir } from 'os';
|
|
4
|
+
import * as path8 from 'path';
|
|
5
|
+
import path8__default from 'path';
|
|
6
|
+
import fsp, { rm } from 'fs/promises';
|
|
7
|
+
import * as Diff from 'diff';
|
|
8
|
+
import { transform } from 'sucrase';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import git from 'isomorphic-git';
|
|
12
|
+
import { createServer } from 'http';
|
|
13
|
+
import { createServer as createServer$1 } from 'net';
|
|
14
|
+
import { getRequestListener } from '@hono/node-server';
|
|
15
|
+
import { Hono } from 'hono';
|
|
16
|
+
import { cors } from 'hono/cors';
|
|
17
|
+
import { createInstance } from 'i18next';
|
|
18
|
+
|
|
19
|
+
// src/agent-cwd.ts
|
|
20
|
+
|
|
21
|
+
// src/errors.ts
|
|
22
|
+
var HostError = class extends Error {
|
|
23
|
+
code;
|
|
24
|
+
constructor(code, message, options) {
|
|
25
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
26
|
+
this.name = new.target.name;
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var HostConfigError = class extends HostError {
|
|
31
|
+
constructor(message, options) {
|
|
32
|
+
super(options?.code ?? "HOST_CONFIG_INVALID", message, options);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/agent-cwd.ts
|
|
37
|
+
var CWD_TYPES = /* @__PURE__ */ new Set(["app", "process", "temp", "custom"]);
|
|
38
|
+
function isAgentCwdType(value) {
|
|
39
|
+
return typeof value === "string" && CWD_TYPES.has(value);
|
|
40
|
+
}
|
|
41
|
+
function assertEnterableDir(abs, label) {
|
|
42
|
+
if (!path8__default.isAbsolute(abs)) {
|
|
43
|
+
throw new HostError("INVALID_AGENT_CWD", `agent: ${label} must be an absolute path: ${abs}`);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
if (!statSync(abs).isDirectory()) {
|
|
47
|
+
throw new HostError("INVALID_AGENT_CWD", `agent: ${label} is not a directory: ${abs}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
if (cause instanceof HostError) throw cause;
|
|
51
|
+
throw new HostError(
|
|
52
|
+
"INVALID_AGENT_CWD",
|
|
53
|
+
`agent: ${label} is not an accessible directory: ${abs}`,
|
|
54
|
+
{ cause }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return abs;
|
|
58
|
+
}
|
|
59
|
+
function resolveAgentCwd(input, ctx = {}) {
|
|
60
|
+
const rawPath = typeof input.cwd === "string" ? input.cwd.trim() : "";
|
|
61
|
+
const hasPath = rawPath.length > 0;
|
|
62
|
+
const type = input.cwdType;
|
|
63
|
+
if (type !== void 0 && !isAgentCwdType(type)) {
|
|
64
|
+
throw new HostError(
|
|
65
|
+
"INVALID_AGENT_CWD",
|
|
66
|
+
`agent: cwdType must be app|process|temp|custom (got ${JSON.stringify(type)})`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (hasPath && type !== void 0 && type !== "custom") {
|
|
70
|
+
throw new HostError(
|
|
71
|
+
"INVALID_AGENT_CWD",
|
|
72
|
+
`agent: cwd path conflicts with cwdType=${type}; omit cwdType or set cwdType:"custom"`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (hasPath) {
|
|
76
|
+
if (!path8__default.isAbsolute(rawPath)) {
|
|
77
|
+
throw new HostError(
|
|
78
|
+
"INVALID_AGENT_CWD",
|
|
79
|
+
`agent: cwd must be an absolute path (got ${JSON.stringify(rawPath)})`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return assertEnterableDir(rawPath, "cwd");
|
|
83
|
+
}
|
|
84
|
+
const effective = type ?? "process";
|
|
85
|
+
switch (effective) {
|
|
86
|
+
case "custom":
|
|
87
|
+
throw new HostError("INVALID_AGENT_CWD", 'agent: cwdType "custom" requires cwd (absolute path)');
|
|
88
|
+
case "app": {
|
|
89
|
+
const appDir = typeof ctx.appDir === "string" ? ctx.appDir.trim() : "";
|
|
90
|
+
if (!appDir) {
|
|
91
|
+
throw new HostError(
|
|
92
|
+
"INVALID_AGENT_CWD",
|
|
93
|
+
'agent: cwdType "app" requires AppCallContext.appDir'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return assertEnterableDir(path8__default.resolve(appDir), "appDir");
|
|
97
|
+
}
|
|
98
|
+
case "process":
|
|
99
|
+
return assertEnterableDir(path8__default.resolve(process.cwd()), "process.cwd()");
|
|
100
|
+
case "temp": {
|
|
101
|
+
const dir = mkdtempSync(path8__default.join(tmpdir(), "mma-agent-"));
|
|
102
|
+
return assertEnterableDir(dir, "temp");
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
const _exhaustive = effective;
|
|
106
|
+
throw new HostError("INVALID_AGENT_CWD", `agent: unhandled cwdType ${_exhaustive}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/app-runtime.ts
|
|
112
|
+
function effectiveSignal(optsSignal, call) {
|
|
113
|
+
return optsSignal ?? call?.signal;
|
|
114
|
+
}
|
|
115
|
+
function clampMode(theme) {
|
|
116
|
+
return theme === "dark" ? "dark" : "light";
|
|
117
|
+
}
|
|
118
|
+
function appThemeFile(dir) {
|
|
119
|
+
return path8__default.join(dir, "theme.json");
|
|
120
|
+
}
|
|
121
|
+
function readAppTheme(dir) {
|
|
122
|
+
try {
|
|
123
|
+
const j = JSON.parse(fs4__default.readFileSync(appThemeFile(dir), "utf8"));
|
|
124
|
+
if (!j.theme) return null;
|
|
125
|
+
return {
|
|
126
|
+
theme: clampMode(j.theme),
|
|
127
|
+
palette: typeof j.palette === "string" ? j.palette : "default"
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function writeAppTheme(dir, val) {
|
|
134
|
+
if (!val) {
|
|
135
|
+
try {
|
|
136
|
+
fs4__default.unlinkSync(appThemeFile(dir));
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const next = {
|
|
142
|
+
theme: clampMode(val.theme),
|
|
143
|
+
palette: typeof val.palette === "string" ? val.palette : "default"
|
|
144
|
+
};
|
|
145
|
+
fs4__default.mkdirSync(dir, { recursive: true });
|
|
146
|
+
fs4__default.writeFileSync(appThemeFile(dir), JSON.stringify(next, null, 2));
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
var APP_ID_RE = /^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/;
|
|
150
|
+
function isAppId(value) {
|
|
151
|
+
return APP_ID_RE.test(value);
|
|
152
|
+
}
|
|
153
|
+
function asAppId(value) {
|
|
154
|
+
if (!isAppId(value)) {
|
|
155
|
+
throw new HostError("INVALID_APP_ID", `invalid AppId: ${value}`);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function isAbsolutePath(value) {
|
|
160
|
+
return path8__default.isAbsolute(value);
|
|
161
|
+
}
|
|
162
|
+
function asAbsolutePath(value) {
|
|
163
|
+
if (!isAbsolutePath(value)) {
|
|
164
|
+
throw new HostError("INVALID_PATH", `path must be absolute: ${value}`);
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function assertNever(value, message) {
|
|
169
|
+
throw new HostError("UNREACHABLE", message ?? `unexpected value: ${String(value)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/capabilities.ts
|
|
173
|
+
function missingCap(name) {
|
|
174
|
+
throw new HostError("CAPABILITY_UNAVAILABLE", `${name}: host capability not available`);
|
|
175
|
+
}
|
|
176
|
+
function bindCapsToContext(ctx, caps) {
|
|
177
|
+
return {
|
|
178
|
+
bash: async (command) => caps.bash ? caps.bash(ctx, command) : missingCap("bash"),
|
|
179
|
+
llm: async (prompt, opts) => caps.llm ? caps.llm(ctx, prompt, opts) : missingCap("llm"),
|
|
180
|
+
agent: async (goal, opts) => caps.agent ? caps.agent(ctx, goal, opts) : missingCap("agent"),
|
|
181
|
+
tool: async (name, args) => caps.tool ? caps.tool(ctx, name, args) : missingCap("tool"),
|
|
182
|
+
mcp: async (name, args) => caps.mcp ? caps.mcp(ctx, name, args) : missingCap("mcp"),
|
|
183
|
+
credentials: () => caps.credentials ? caps.credentials(ctx) : {},
|
|
184
|
+
config: () => caps.config ? caps.config(ctx) : {},
|
|
185
|
+
listTools: () => caps.listTools ? caps.listTools(ctx) : []
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function joinUnder(root, ...parts) {
|
|
189
|
+
return asAbsolutePath(path8__default.join(root, ...parts));
|
|
190
|
+
}
|
|
191
|
+
var WorkspacePaths = class _WorkspacePaths {
|
|
192
|
+
static Rel = {
|
|
193
|
+
apps: "apps",
|
|
194
|
+
hostConfig: "host.json",
|
|
195
|
+
ui: "ui.tsx",
|
|
196
|
+
api: "main.api.ts",
|
|
197
|
+
manifest: "manifest.json",
|
|
198
|
+
storage: "storage",
|
|
199
|
+
uiCache: ".ui-cache"
|
|
200
|
+
};
|
|
201
|
+
root;
|
|
202
|
+
constructor(root) {
|
|
203
|
+
this.root = asAbsolutePath(path8__default.resolve(asAbsolutePath(root)));
|
|
204
|
+
}
|
|
205
|
+
appsDir() {
|
|
206
|
+
return joinUnder(this.root, _WorkspacePaths.Rel.apps);
|
|
207
|
+
}
|
|
208
|
+
appDir(id) {
|
|
209
|
+
return joinUnder(this.root, _WorkspacePaths.Rel.apps, asAppId(id));
|
|
210
|
+
}
|
|
211
|
+
hostConfigFile() {
|
|
212
|
+
return joinUnder(this.root, _WorkspacePaths.Rel.hostConfig);
|
|
213
|
+
}
|
|
214
|
+
uiCacheDir() {
|
|
215
|
+
return joinUnder(this.root, _WorkspacePaths.Rel.uiCache);
|
|
216
|
+
}
|
|
217
|
+
appFile(id, rel) {
|
|
218
|
+
const base = this.appDir(id);
|
|
219
|
+
if (!rel || path8__default.isAbsolute(rel)) {
|
|
220
|
+
throw new HostError("INVALID_PATH", `unsafe relative path: ${rel}`);
|
|
221
|
+
}
|
|
222
|
+
const resolved = path8__default.resolve(base, rel);
|
|
223
|
+
const prefix = base.endsWith(path8__default.sep) ? base : `${base}${path8__default.sep}`;
|
|
224
|
+
if (resolved !== base && !resolved.startsWith(prefix)) {
|
|
225
|
+
throw new HostError("INVALID_PATH", `unsafe relative path: ${rel}`);
|
|
226
|
+
}
|
|
227
|
+
return asAbsolutePath(resolved);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function detectLineEnding(content) {
|
|
231
|
+
const crlfIdx = content.indexOf("\r\n");
|
|
232
|
+
const lfIdx = content.indexOf("\n");
|
|
233
|
+
if (lfIdx === -1) return "\n";
|
|
234
|
+
if (crlfIdx === -1) return "\n";
|
|
235
|
+
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
|
236
|
+
}
|
|
237
|
+
function normalizeToLF(text) {
|
|
238
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
239
|
+
}
|
|
240
|
+
function restoreLineEndings(text, ending) {
|
|
241
|
+
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
242
|
+
}
|
|
243
|
+
function normalizeForFuzzyMatch(text) {
|
|
244
|
+
return text.normalize("NFKC").split("\n").map((line) => line.trimEnd()).join("\n").replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-").replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
245
|
+
}
|
|
246
|
+
function splitLinesWithEndings(content) {
|
|
247
|
+
return content.match(/[^\n]*\n|[^\n]+/g) ?? [];
|
|
248
|
+
}
|
|
249
|
+
function getLineSpans(content) {
|
|
250
|
+
let offset = 0;
|
|
251
|
+
return splitLinesWithEndings(content).map((line) => {
|
|
252
|
+
const span = { start: offset, end: offset + line.length };
|
|
253
|
+
offset = span.end;
|
|
254
|
+
return span;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function getReplacementLineRange(lines, replacement) {
|
|
258
|
+
const replacementStart = replacement.matchIndex;
|
|
259
|
+
const replacementEnd = replacement.matchIndex + replacement.matchLength;
|
|
260
|
+
let startLine = -1;
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
const line = lines[i];
|
|
263
|
+
if (replacementStart >= line.start && replacementStart < line.end) {
|
|
264
|
+
startLine = i;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (startLine === -1) {
|
|
269
|
+
throw new Error("Replacement range is outside the base content.");
|
|
270
|
+
}
|
|
271
|
+
let endLine = startLine;
|
|
272
|
+
while (endLine < lines.length && lines[endLine].end < replacementEnd) {
|
|
273
|
+
endLine++;
|
|
274
|
+
}
|
|
275
|
+
if (endLine >= lines.length) {
|
|
276
|
+
throw new Error("Replacement range is outside the base content.");
|
|
277
|
+
}
|
|
278
|
+
return { startLine, endLine: endLine + 1 };
|
|
279
|
+
}
|
|
280
|
+
function applyReplacements(content, replacements, offset = 0) {
|
|
281
|
+
let result = content;
|
|
282
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
283
|
+
const replacement = replacements[i];
|
|
284
|
+
const matchIndex = replacement.matchIndex - offset;
|
|
285
|
+
result = result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength);
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
function applyReplacementsPreservingUnchangedLines(originalContent, baseContent, replacements) {
|
|
290
|
+
const originalLines = splitLinesWithEndings(originalContent);
|
|
291
|
+
const baseLines = getLineSpans(baseContent);
|
|
292
|
+
if (originalLines.length !== baseLines.length) {
|
|
293
|
+
throw new Error("Cannot preserve unchanged lines because the base content has a different line count.");
|
|
294
|
+
}
|
|
295
|
+
const groups = [];
|
|
296
|
+
const sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex);
|
|
297
|
+
for (const replacement of sortedReplacements) {
|
|
298
|
+
const range = getReplacementLineRange(baseLines, replacement);
|
|
299
|
+
const current = groups[groups.length - 1];
|
|
300
|
+
if (current && range.startLine < current.endLine) {
|
|
301
|
+
current.endLine = Math.max(current.endLine, range.endLine);
|
|
302
|
+
current.replacements.push(replacement);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
groups.push({ ...range, replacements: [replacement] });
|
|
306
|
+
}
|
|
307
|
+
let originalLineIndex = 0;
|
|
308
|
+
let result = "";
|
|
309
|
+
for (const group of groups) {
|
|
310
|
+
result += originalLines.slice(originalLineIndex, group.startLine).join("");
|
|
311
|
+
const groupStartOffset = baseLines[group.startLine].start;
|
|
312
|
+
const groupEndOffset = baseLines[group.endLine - 1].end;
|
|
313
|
+
result += applyReplacements(
|
|
314
|
+
baseContent.slice(groupStartOffset, groupEndOffset),
|
|
315
|
+
group.replacements,
|
|
316
|
+
groupStartOffset
|
|
317
|
+
);
|
|
318
|
+
originalLineIndex = group.endLine;
|
|
319
|
+
}
|
|
320
|
+
result += originalLines.slice(originalLineIndex).join("");
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
function fuzzyFindText(content, oldText) {
|
|
324
|
+
const exactIndex = content.indexOf(oldText);
|
|
325
|
+
if (exactIndex !== -1) {
|
|
326
|
+
return {
|
|
327
|
+
found: true,
|
|
328
|
+
index: exactIndex,
|
|
329
|
+
matchLength: oldText.length,
|
|
330
|
+
usedFuzzyMatch: false,
|
|
331
|
+
contentForReplacement: content
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
335
|
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
336
|
+
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
|
337
|
+
if (fuzzyIndex === -1) {
|
|
338
|
+
return {
|
|
339
|
+
found: false,
|
|
340
|
+
index: -1,
|
|
341
|
+
matchLength: 0,
|
|
342
|
+
usedFuzzyMatch: false,
|
|
343
|
+
contentForReplacement: content
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
found: true,
|
|
348
|
+
index: fuzzyIndex,
|
|
349
|
+
matchLength: fuzzyOldText.length,
|
|
350
|
+
usedFuzzyMatch: true,
|
|
351
|
+
contentForReplacement: fuzzyContent
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function countOccurrences(content, oldText) {
|
|
355
|
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
356
|
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
357
|
+
return fuzzyContent.split(fuzzyOldText).length - 1;
|
|
358
|
+
}
|
|
359
|
+
function getNotFoundError(path12, editIndex, totalEdits) {
|
|
360
|
+
if (totalEdits === 1) {
|
|
361
|
+
return new Error(
|
|
362
|
+
`Could not find the exact text in ${path12}. The old text must match exactly including all whitespace and newlines.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return new Error(
|
|
366
|
+
`Could not find edits[${editIndex}] in ${path12}. The oldText must match exactly including all whitespace and newlines.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
function getDuplicateError(path12, editIndex, totalEdits, occurrences) {
|
|
370
|
+
if (totalEdits === 1) {
|
|
371
|
+
return new Error(
|
|
372
|
+
`Found ${occurrences} occurrences of the text in ${path12}. The text must be unique. Please provide more context to make it unique.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return new Error(
|
|
376
|
+
`Found ${occurrences} occurrences of edits[${editIndex}] in ${path12}. Each oldText must be unique. Please provide more context to make it unique.`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
function getEmptyOldTextError(path12, editIndex, totalEdits) {
|
|
380
|
+
if (totalEdits === 1) {
|
|
381
|
+
return new Error(`oldText must not be empty in ${path12}.`);
|
|
382
|
+
}
|
|
383
|
+
return new Error(`edits[${editIndex}].oldText must not be empty in ${path12}.`);
|
|
384
|
+
}
|
|
385
|
+
function getNoChangeError(path12, totalEdits) {
|
|
386
|
+
if (totalEdits === 1) {
|
|
387
|
+
return new Error(
|
|
388
|
+
`No changes made to ${path12}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
return new Error(`No changes made to ${path12}. The replacements produced identical content.`);
|
|
392
|
+
}
|
|
393
|
+
function applyEditsToNormalizedContent(normalizedContent, edits, path12) {
|
|
394
|
+
const normalizedEdits = edits.map((edit) => ({
|
|
395
|
+
oldText: normalizeToLF(edit.oldText),
|
|
396
|
+
newText: normalizeToLF(edit.newText)
|
|
397
|
+
}));
|
|
398
|
+
for (let i = 0; i < normalizedEdits.length; i++) {
|
|
399
|
+
if (normalizedEdits[i].oldText.length === 0) {
|
|
400
|
+
throw getEmptyOldTextError(path12, i, normalizedEdits.length);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText));
|
|
404
|
+
const usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch);
|
|
405
|
+
const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent;
|
|
406
|
+
const matchedEdits = [];
|
|
407
|
+
for (let i = 0; i < normalizedEdits.length; i++) {
|
|
408
|
+
const edit = normalizedEdits[i];
|
|
409
|
+
const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText);
|
|
410
|
+
if (!matchResult.found) {
|
|
411
|
+
throw getNotFoundError(path12, i, normalizedEdits.length);
|
|
412
|
+
}
|
|
413
|
+
const occurrences = countOccurrences(replacementBaseContent, edit.oldText);
|
|
414
|
+
if (occurrences > 1) {
|
|
415
|
+
throw getDuplicateError(path12, i, normalizedEdits.length, occurrences);
|
|
416
|
+
}
|
|
417
|
+
matchedEdits.push({
|
|
418
|
+
editIndex: i,
|
|
419
|
+
matchIndex: matchResult.index,
|
|
420
|
+
matchLength: matchResult.matchLength,
|
|
421
|
+
newText: edit.newText
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);
|
|
425
|
+
for (let i = 1; i < matchedEdits.length; i++) {
|
|
426
|
+
const previous = matchedEdits[i - 1];
|
|
427
|
+
const current = matchedEdits[i];
|
|
428
|
+
if (previous.matchIndex + previous.matchLength > current.matchIndex) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path12}. Merge them into one edit or target disjoint regions.`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const baseContent = normalizedContent;
|
|
435
|
+
const newContent = usedFuzzyMatch ? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits) : applyReplacements(replacementBaseContent, matchedEdits);
|
|
436
|
+
if (baseContent === newContent) {
|
|
437
|
+
throw getNoChangeError(path12, normalizedEdits.length);
|
|
438
|
+
}
|
|
439
|
+
return { baseContent, newContent };
|
|
440
|
+
}
|
|
441
|
+
function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
442
|
+
const parts = Diff.diffLines(oldContent, newContent);
|
|
443
|
+
const output = [];
|
|
444
|
+
const oldLines = oldContent.split("\n");
|
|
445
|
+
const newLines = newContent.split("\n");
|
|
446
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
447
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
448
|
+
let oldLineNum = 1;
|
|
449
|
+
let newLineNum = 1;
|
|
450
|
+
let lastWasChange = false;
|
|
451
|
+
let firstChangedLine;
|
|
452
|
+
for (let i = 0; i < parts.length; i++) {
|
|
453
|
+
const part = parts[i];
|
|
454
|
+
const raw = part.value.split("\n");
|
|
455
|
+
if (raw[raw.length - 1] === "") {
|
|
456
|
+
raw.pop();
|
|
457
|
+
}
|
|
458
|
+
if (part.added || part.removed) {
|
|
459
|
+
if (firstChangedLine === void 0) {
|
|
460
|
+
firstChangedLine = newLineNum;
|
|
461
|
+
}
|
|
462
|
+
for (const line of raw) {
|
|
463
|
+
if (part.added) {
|
|
464
|
+
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
465
|
+
output.push(`+${lineNum} ${line}`);
|
|
466
|
+
newLineNum++;
|
|
467
|
+
} else {
|
|
468
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
469
|
+
output.push(`-${lineNum} ${line}`);
|
|
470
|
+
oldLineNum++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
lastWasChange = true;
|
|
474
|
+
} else {
|
|
475
|
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
476
|
+
const hasLeadingChange = lastWasChange;
|
|
477
|
+
const hasTrailingChange = nextPartIsChange;
|
|
478
|
+
if (hasLeadingChange && hasTrailingChange) {
|
|
479
|
+
if (raw.length <= contextLines * 2) {
|
|
480
|
+
for (const line of raw) {
|
|
481
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
482
|
+
output.push(` ${lineNum} ${line}`);
|
|
483
|
+
oldLineNum++;
|
|
484
|
+
newLineNum++;
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
const leadingLines = raw.slice(0, contextLines);
|
|
488
|
+
const trailingLines = raw.slice(raw.length - contextLines);
|
|
489
|
+
const skippedLines = raw.length - leadingLines.length - trailingLines.length;
|
|
490
|
+
for (const line of leadingLines) {
|
|
491
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
492
|
+
output.push(` ${lineNum} ${line}`);
|
|
493
|
+
oldLineNum++;
|
|
494
|
+
newLineNum++;
|
|
495
|
+
}
|
|
496
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
497
|
+
oldLineNum += skippedLines;
|
|
498
|
+
newLineNum += skippedLines;
|
|
499
|
+
for (const line of trailingLines) {
|
|
500
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
501
|
+
output.push(` ${lineNum} ${line}`);
|
|
502
|
+
oldLineNum++;
|
|
503
|
+
newLineNum++;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} else if (hasLeadingChange) {
|
|
507
|
+
const shownLines = raw.slice(0, contextLines);
|
|
508
|
+
const skippedLines = raw.length - shownLines.length;
|
|
509
|
+
for (const line of shownLines) {
|
|
510
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
511
|
+
output.push(` ${lineNum} ${line}`);
|
|
512
|
+
oldLineNum++;
|
|
513
|
+
newLineNum++;
|
|
514
|
+
}
|
|
515
|
+
if (skippedLines > 0) {
|
|
516
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
517
|
+
oldLineNum += skippedLines;
|
|
518
|
+
newLineNum += skippedLines;
|
|
519
|
+
}
|
|
520
|
+
} else if (hasTrailingChange) {
|
|
521
|
+
const skippedLines = Math.max(0, raw.length - contextLines);
|
|
522
|
+
if (skippedLines > 0) {
|
|
523
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
524
|
+
oldLineNum += skippedLines;
|
|
525
|
+
newLineNum += skippedLines;
|
|
526
|
+
}
|
|
527
|
+
for (const line of raw.slice(skippedLines)) {
|
|
528
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
529
|
+
output.push(` ${lineNum} ${line}`);
|
|
530
|
+
oldLineNum++;
|
|
531
|
+
newLineNum++;
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
oldLineNum += raw.length;
|
|
535
|
+
newLineNum += raw.length;
|
|
536
|
+
}
|
|
537
|
+
lastWasChange = false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return { diff: output.join("\n"), firstChangedLine };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/apps/manifest.ts
|
|
544
|
+
function isRecord(value) {
|
|
545
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
546
|
+
}
|
|
547
|
+
function requireString(raw, key) {
|
|
548
|
+
const value = raw[key];
|
|
549
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
550
|
+
throw new HostError("INVALID_MANIFEST", `manifest missing ${key}`);
|
|
551
|
+
}
|
|
552
|
+
return value;
|
|
553
|
+
}
|
|
554
|
+
function parseManifest(raw) {
|
|
555
|
+
let parsed;
|
|
556
|
+
try {
|
|
557
|
+
parsed = JSON.parse(raw);
|
|
558
|
+
} catch (cause) {
|
|
559
|
+
throw new HostError("INVALID_MANIFEST", "manifest is not valid JSON", { cause });
|
|
560
|
+
}
|
|
561
|
+
if (!isRecord(parsed)) {
|
|
562
|
+
throw new HostError("INVALID_MANIFEST", "manifest must be an object");
|
|
563
|
+
}
|
|
564
|
+
let id;
|
|
565
|
+
try {
|
|
566
|
+
id = asAppId(requireString(parsed, "id"));
|
|
567
|
+
} catch (cause) {
|
|
568
|
+
throw new HostError("INVALID_MANIFEST", "manifest id is not a valid AppId", { cause });
|
|
569
|
+
}
|
|
570
|
+
const permissions = Array.isArray(parsed.permissions) ? parsed.permissions.filter((p) => typeof p === "string") : [];
|
|
571
|
+
const description = typeof parsed.description === "string" ? parsed.description : void 0;
|
|
572
|
+
const acronym = typeof parsed.acronym === "string" ? parsed.acronym : void 0;
|
|
573
|
+
const themeRaw = parsed.theme;
|
|
574
|
+
const theme = isRecord(themeRaw) && typeof themeRaw.followsHost === "boolean" ? { followsHost: themeRaw.followsHost } : void 0;
|
|
575
|
+
return {
|
|
576
|
+
id,
|
|
577
|
+
name: requireString(parsed, "name"),
|
|
578
|
+
version: requireString(parsed, "version"),
|
|
579
|
+
entry: requireString(parsed, "entry"),
|
|
580
|
+
description,
|
|
581
|
+
permissions,
|
|
582
|
+
acronym,
|
|
583
|
+
theme
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function acronymOf(name, manifestAcronym) {
|
|
587
|
+
if (manifestAcronym && /^[a-zA-Z0-9]{2}$/.test(manifestAcronym)) {
|
|
588
|
+
return manifestAcronym.toUpperCase();
|
|
589
|
+
}
|
|
590
|
+
return name.replace(/[^a-zA-Z0-9]/g, "").slice(0, 2).toUpperCase();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/apps/app-files.ts
|
|
594
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "storage", ".ui-build", ".ui-cache"]);
|
|
595
|
+
var READ_DEFAULT_MAX_LINES = 2e3;
|
|
596
|
+
function splitLines(text) {
|
|
597
|
+
if (text.length === 0) return [];
|
|
598
|
+
const parts = text.split("\n");
|
|
599
|
+
if (parts.length > 0 && parts[parts.length - 1] === "" && text.endsWith("\n")) {
|
|
600
|
+
parts.pop();
|
|
601
|
+
}
|
|
602
|
+
return parts;
|
|
603
|
+
}
|
|
604
|
+
function formatNumbered(lines, startLine) {
|
|
605
|
+
const width = String(startLine + lines.length - 1).length;
|
|
606
|
+
return lines.map((line, i) => `${String(startLine + i).padStart(width, " ")}|${line}`).join("\n");
|
|
607
|
+
}
|
|
608
|
+
function resolveLineWindow(totalLines, range) {
|
|
609
|
+
if (totalLines === 0) {
|
|
610
|
+
return { start: 1, end: 0, truncated: false };
|
|
611
|
+
}
|
|
612
|
+
let start = range?.startLine;
|
|
613
|
+
let end = range?.endLine;
|
|
614
|
+
if (start !== void 0) {
|
|
615
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
616
|
+
throw new HostError("INVALID_RANGE", `startLine must be an integer >= 1 (got ${start})`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (end !== void 0) {
|
|
620
|
+
if (!Number.isInteger(end) || end < 1) {
|
|
621
|
+
throw new HostError("INVALID_RANGE", `endLine must be an integer >= 1 (got ${end})`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (start === void 0 && end === void 0) {
|
|
625
|
+
start = 1;
|
|
626
|
+
end = totalLines;
|
|
627
|
+
} else if (start === void 0) {
|
|
628
|
+
start = 1;
|
|
629
|
+
} else if (end === void 0) {
|
|
630
|
+
end = totalLines;
|
|
631
|
+
}
|
|
632
|
+
if (start > totalLines) {
|
|
633
|
+
throw new HostError(
|
|
634
|
+
"INVALID_RANGE",
|
|
635
|
+
`startLine ${start} is beyond end of file (${totalLines} lines)`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (end < start) {
|
|
639
|
+
throw new HostError(
|
|
640
|
+
"INVALID_RANGE",
|
|
641
|
+
`endLine ${end} must be >= startLine ${start}`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
let truncated = false;
|
|
645
|
+
if (range?.endLine === void 0 && end - start + 1 > READ_DEFAULT_MAX_LINES) {
|
|
646
|
+
end = start + READ_DEFAULT_MAX_LINES - 1;
|
|
647
|
+
truncated = true;
|
|
648
|
+
}
|
|
649
|
+
if (end > totalLines) end = totalLines;
|
|
650
|
+
return { start, end, truncated };
|
|
651
|
+
}
|
|
652
|
+
function stripBom(raw) {
|
|
653
|
+
if (raw.charCodeAt(0) === 65279) {
|
|
654
|
+
return { bom: "\uFEFF", text: raw.slice(1) };
|
|
655
|
+
}
|
|
656
|
+
return { bom: "", text: raw };
|
|
657
|
+
}
|
|
658
|
+
function assertSafeRel(rel) {
|
|
659
|
+
const cleaned = rel.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
660
|
+
if (!cleaned || cleaned.includes("..") || path8__default.isAbsolute(cleaned) || cleaned.startsWith("/")) {
|
|
661
|
+
throw new HostError("PATH_ESCAPE", `unsafe relative path: ${rel}`);
|
|
662
|
+
}
|
|
663
|
+
return cleaned;
|
|
664
|
+
}
|
|
665
|
+
function walkFiles(dir, base, out) {
|
|
666
|
+
let names;
|
|
667
|
+
try {
|
|
668
|
+
names = readdirSync(dir);
|
|
669
|
+
} catch {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
for (const name of names) {
|
|
673
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
674
|
+
const full = path8__default.join(dir, name);
|
|
675
|
+
let st;
|
|
676
|
+
try {
|
|
677
|
+
st = statSync(full);
|
|
678
|
+
} catch {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const rel = path8__default.relative(base, full).split(path8__default.sep).join("/");
|
|
682
|
+
if (st.isDirectory()) {
|
|
683
|
+
walkFiles(full, base, out);
|
|
684
|
+
} else if (st.isFile()) {
|
|
685
|
+
out.push({ path: rel, size: st.size });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function listAppFiles(paths, appId) {
|
|
690
|
+
const id = asAppId(appId);
|
|
691
|
+
const dir = paths.appDir(id);
|
|
692
|
+
if (!existsSync(dir)) {
|
|
693
|
+
throw new HostError("APP_NOT_FOUND", `app not found: ${id}`);
|
|
694
|
+
}
|
|
695
|
+
const out = [];
|
|
696
|
+
walkFiles(dir, dir, out);
|
|
697
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
function readAppFile(paths, appId, relPath, range) {
|
|
701
|
+
const id = asAppId(appId);
|
|
702
|
+
const rel = assertSafeRel(relPath);
|
|
703
|
+
const full = paths.appFile(id, rel);
|
|
704
|
+
if (!existsSync(full)) {
|
|
705
|
+
throw new HostError("FILE_NOT_FOUND", `file not found: ${rel}`);
|
|
706
|
+
}
|
|
707
|
+
const raw = readFileSync(full, "utf8");
|
|
708
|
+
const { text } = stripBom(raw);
|
|
709
|
+
const allLines = splitLines(text);
|
|
710
|
+
const totalLines = allLines.length;
|
|
711
|
+
const { start, end, truncated } = resolveLineWindow(totalLines, range);
|
|
712
|
+
const wholeFile = totalLines === 0 || start === 1 && end === totalLines && !truncated && !range?.numbered;
|
|
713
|
+
let content;
|
|
714
|
+
if (wholeFile) {
|
|
715
|
+
content = text;
|
|
716
|
+
} else if (totalLines === 0) {
|
|
717
|
+
content = "";
|
|
718
|
+
} else {
|
|
719
|
+
const slice = allLines.slice(start - 1, end);
|
|
720
|
+
content = range?.numbered ? formatNumbered(slice, start) : slice.join("\n");
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
path: rel,
|
|
724
|
+
content,
|
|
725
|
+
bytes: Buffer.byteLength(content, "utf8"),
|
|
726
|
+
totalLines,
|
|
727
|
+
startLine: totalLines === 0 ? 1 : start,
|
|
728
|
+
endLine: end,
|
|
729
|
+
...truncated ? { truncated: true } : {}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function writeAppFile(paths, appId, relPath, content) {
|
|
733
|
+
const id = asAppId(appId);
|
|
734
|
+
const rel = assertSafeRel(relPath);
|
|
735
|
+
if (rel === "manifest.json" || rel.endsWith("/manifest.json")) {
|
|
736
|
+
parseManifest(content);
|
|
737
|
+
}
|
|
738
|
+
const full = paths.appFile(id, rel);
|
|
739
|
+
const created = !existsSync(full);
|
|
740
|
+
mkdirSync(path8__default.dirname(full), { recursive: true });
|
|
741
|
+
writeFileSync(full, content, "utf8");
|
|
742
|
+
return { path: rel, bytes: Buffer.byteLength(content, "utf8"), created };
|
|
743
|
+
}
|
|
744
|
+
function editAppFile(paths, appId, relPath, edits) {
|
|
745
|
+
const id = asAppId(appId);
|
|
746
|
+
const rel = assertSafeRel(relPath);
|
|
747
|
+
if (!edits.length) {
|
|
748
|
+
throw new HostError("INVALID_EDIT", "edits must contain at least one replacement");
|
|
749
|
+
}
|
|
750
|
+
const full = paths.appFile(id, rel);
|
|
751
|
+
if (!existsSync(full)) {
|
|
752
|
+
throw new HostError("FILE_NOT_FOUND", `file not found: ${rel}`);
|
|
753
|
+
}
|
|
754
|
+
const raw = readFileSync(full, "utf8");
|
|
755
|
+
const { bom, text } = stripBom(raw);
|
|
756
|
+
const ending = detectLineEnding(text);
|
|
757
|
+
const normalized = normalizeToLF(text);
|
|
758
|
+
let applied;
|
|
759
|
+
try {
|
|
760
|
+
applied = applyEditsToNormalizedContent(normalized, edits, rel);
|
|
761
|
+
} catch (cause) {
|
|
762
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
763
|
+
throw new HostError("EDIT_FAILED", message, { cause });
|
|
764
|
+
}
|
|
765
|
+
const restored = restoreLineEndings(applied.newContent, ending);
|
|
766
|
+
const next = bom + restored;
|
|
767
|
+
if (rel === "manifest.json" || rel.endsWith("/manifest.json")) {
|
|
768
|
+
parseManifest(next);
|
|
769
|
+
}
|
|
770
|
+
writeFileSync(full, next, "utf8");
|
|
771
|
+
const { diff } = generateDiffString(applied.baseContent, applied.newContent);
|
|
772
|
+
return { path: rel, bytes: Buffer.byteLength(next, "utf8"), diff };
|
|
773
|
+
}
|
|
774
|
+
function deleteAppFile(paths, appId, relPath) {
|
|
775
|
+
const id = asAppId(appId);
|
|
776
|
+
const rel = assertSafeRel(relPath);
|
|
777
|
+
if (rel === "manifest.json") {
|
|
778
|
+
throw new HostError("FORBIDDEN", "cannot delete manifest.json; remove the app instead");
|
|
779
|
+
}
|
|
780
|
+
const full = paths.appFile(id, rel);
|
|
781
|
+
if (!existsSync(full)) {
|
|
782
|
+
throw new HostError("FILE_NOT_FOUND", `file not found: ${rel}`);
|
|
783
|
+
}
|
|
784
|
+
unlinkSync(full);
|
|
785
|
+
return { path: rel };
|
|
786
|
+
}
|
|
787
|
+
function compileAppSource(src) {
|
|
788
|
+
const input = src.replace(/^\uFEFF/, "");
|
|
789
|
+
try {
|
|
790
|
+
return transform(input, {
|
|
791
|
+
transforms: ["typescript", "imports"],
|
|
792
|
+
disableESTransforms: true,
|
|
793
|
+
filePath: "app.ts"
|
|
794
|
+
}).code;
|
|
795
|
+
} catch (cause) {
|
|
796
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
797
|
+
throw new HostError("COMPILE_FAILED", `compileAppSource: ${message}`, { cause });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/apps/ctx-http.ts
|
|
802
|
+
var HTTP_DEFAULT_TIMEOUT_MS = 8e3;
|
|
803
|
+
var HTTP_MAX_BYTES = 8 * 1024 * 1024;
|
|
804
|
+
function errText(e) {
|
|
805
|
+
return e instanceof Error ? e.message : String(e);
|
|
806
|
+
}
|
|
807
|
+
function withQuery(url, query) {
|
|
808
|
+
if (!query) {
|
|
809
|
+
return url;
|
|
810
|
+
}
|
|
811
|
+
const u = new URL(url);
|
|
812
|
+
for (const [key, value] of Object.entries(query)) {
|
|
813
|
+
if (value == null) continue;
|
|
814
|
+
u.searchParams.set(key, String(value));
|
|
815
|
+
}
|
|
816
|
+
return u.toString();
|
|
817
|
+
}
|
|
818
|
+
function encodeBody(body, headers) {
|
|
819
|
+
if (body == null) {
|
|
820
|
+
return { body: void 0, headers };
|
|
821
|
+
}
|
|
822
|
+
if (typeof body === "string") {
|
|
823
|
+
return { body, headers };
|
|
824
|
+
}
|
|
825
|
+
if (!headers.has("content-type")) {
|
|
826
|
+
headers.set("content-type", "application/json");
|
|
827
|
+
}
|
|
828
|
+
return { body: JSON.stringify(body), headers };
|
|
829
|
+
}
|
|
830
|
+
function headersToObject(h) {
|
|
831
|
+
const out = {};
|
|
832
|
+
h.forEach((value, key) => {
|
|
833
|
+
out[key] = value;
|
|
834
|
+
});
|
|
835
|
+
return out;
|
|
836
|
+
}
|
|
837
|
+
function parseJson(contentType, text) {
|
|
838
|
+
if (!/json/i.test(contentType)) {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
return JSON.parse(text);
|
|
843
|
+
} catch {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function httpRequest(urlOrReq, opts) {
|
|
848
|
+
const req = typeof urlOrReq === "string" ? { url: urlOrReq, ...opts } : { ...urlOrReq };
|
|
849
|
+
const rawUrl = req.url.trim();
|
|
850
|
+
if (!rawUrl) {
|
|
851
|
+
throw new HostError("HTTP_INVALID_URL", "http: url required");
|
|
852
|
+
}
|
|
853
|
+
let url;
|
|
854
|
+
try {
|
|
855
|
+
url = new URL(withQuery(rawUrl, req.query));
|
|
856
|
+
} catch (cause) {
|
|
857
|
+
throw new HostError("HTTP_INVALID_URL", "http: invalid url", { cause });
|
|
858
|
+
}
|
|
859
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
860
|
+
throw new HostError("HTTP_INVALID_URL", "http: only http/https");
|
|
861
|
+
}
|
|
862
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
863
|
+
const headers = new Headers(req.headers ?? {});
|
|
864
|
+
if (!headers.has("user-agent")) {
|
|
865
|
+
headers.set("user-agent", "monkey-mini-app/0.1");
|
|
866
|
+
}
|
|
867
|
+
const encoded = method === "GET" || method === "HEAD" ? { body: void 0, headers } : encodeBody(req.body, headers);
|
|
868
|
+
const timeout = Number(req.timeout);
|
|
869
|
+
const ms = Number.isFinite(timeout) && timeout > 0 ? Math.min(timeout, 12e4) : HTTP_DEFAULT_TIMEOUT_MS;
|
|
870
|
+
const sig = req.signal;
|
|
871
|
+
if (sig?.aborted) {
|
|
872
|
+
throw new HostError("CANCELLED", "cancelled");
|
|
873
|
+
}
|
|
874
|
+
let res;
|
|
875
|
+
try {
|
|
876
|
+
const timeoutSignal = AbortSignal.timeout(ms);
|
|
877
|
+
const signal = sig ? AbortSignal.any([sig, timeoutSignal]) : timeoutSignal;
|
|
878
|
+
res = await fetch(url, {
|
|
879
|
+
method,
|
|
880
|
+
headers: encoded.headers,
|
|
881
|
+
body: encoded.body,
|
|
882
|
+
redirect: "follow",
|
|
883
|
+
signal
|
|
884
|
+
});
|
|
885
|
+
} catch (cause) {
|
|
886
|
+
if (sig?.aborted) {
|
|
887
|
+
throw new HostError("CANCELLED", "cancelled", { cause });
|
|
888
|
+
}
|
|
889
|
+
const msg = errText(cause);
|
|
890
|
+
if (/aborted|timeout/i.test(msg)) {
|
|
891
|
+
throw new HostError("HTTP_TIMEOUT", "http: timeout", { cause });
|
|
892
|
+
}
|
|
893
|
+
throw new HostError("HTTP_FAILED", `http: ${msg}`, { cause });
|
|
894
|
+
}
|
|
895
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
896
|
+
if (Number.isFinite(contentLength) && contentLength > HTTP_MAX_BYTES) {
|
|
897
|
+
throw new HostError("HTTP_TOO_LARGE", "http: response too large");
|
|
898
|
+
}
|
|
899
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
900
|
+
if (buf.byteLength > HTTP_MAX_BYTES) {
|
|
901
|
+
throw new HostError("HTTP_TOO_LARGE", "http: response too large");
|
|
902
|
+
}
|
|
903
|
+
const text = buf.toString("utf8");
|
|
904
|
+
const headerObj = headersToObject(res.headers);
|
|
905
|
+
return {
|
|
906
|
+
ok: res.ok,
|
|
907
|
+
status: res.status,
|
|
908
|
+
headers: headerObj,
|
|
909
|
+
text,
|
|
910
|
+
json: parseJson(headerObj["content-type"] ?? "", text)
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/apps/apps-manager.ts
|
|
915
|
+
function isRecord2(value) {
|
|
916
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
917
|
+
}
|
|
918
|
+
function defineDashboard(def) {
|
|
919
|
+
if (!def.name || !def.description) {
|
|
920
|
+
throw new HostError("INVALID_DASHBOARD", "defineDashboard requires name and description");
|
|
921
|
+
}
|
|
922
|
+
if (!def.api || typeof def.api !== "object") {
|
|
923
|
+
throw new HostError("INVALID_DASHBOARD", "defineDashboard.api must be an object");
|
|
924
|
+
}
|
|
925
|
+
return def;
|
|
926
|
+
}
|
|
927
|
+
function publicAppConfig(config) {
|
|
928
|
+
return {
|
|
929
|
+
theme: config.theme,
|
|
930
|
+
palette: config.palette,
|
|
931
|
+
locale: config.locale,
|
|
932
|
+
chatLanguage: config.chatLanguage,
|
|
933
|
+
hostPort: config.hostPort,
|
|
934
|
+
llm: config.llm
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function makeFileStorage(appDir, fileName) {
|
|
938
|
+
const fp = path8__default.join(appDir, "storage", fileName);
|
|
939
|
+
const read = () => {
|
|
940
|
+
try {
|
|
941
|
+
const parsed = JSON.parse(readFileSync(fp, "utf8"));
|
|
942
|
+
return isRecord2(parsed) ? parsed : {};
|
|
943
|
+
} catch {
|
|
944
|
+
return {};
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
const write = (obj) => {
|
|
948
|
+
mkdirSync(path8__default.dirname(fp), { recursive: true });
|
|
949
|
+
writeFileSync(fp, JSON.stringify(obj, null, 2));
|
|
950
|
+
};
|
|
951
|
+
return {
|
|
952
|
+
async get(key) {
|
|
953
|
+
const obj = read();
|
|
954
|
+
return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : null;
|
|
955
|
+
},
|
|
956
|
+
async set(key, value) {
|
|
957
|
+
const obj = read();
|
|
958
|
+
obj[key] = value;
|
|
959
|
+
write(obj);
|
|
960
|
+
},
|
|
961
|
+
async delete(key) {
|
|
962
|
+
const obj = read();
|
|
963
|
+
delete obj[key];
|
|
964
|
+
write(obj);
|
|
965
|
+
},
|
|
966
|
+
async clear() {
|
|
967
|
+
write({});
|
|
968
|
+
},
|
|
969
|
+
table(name) {
|
|
970
|
+
const safe = name.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
971
|
+
return makeFileStorage(appDir, `${safe}.storage.json`);
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function resolveAppModule(fromFile, spec, appDir) {
|
|
976
|
+
const root = path8__default.resolve(appDir);
|
|
977
|
+
const base = spec.startsWith(".") ? path8__default.resolve(path8__default.dirname(fromFile), spec) : path8__default.resolve(root, spec);
|
|
978
|
+
const resolved = path8__default.resolve(base);
|
|
979
|
+
const prefix = root.endsWith(path8__default.sep) ? root : `${root}${path8__default.sep}`;
|
|
980
|
+
if (resolved !== root && !resolved.startsWith(prefix)) {
|
|
981
|
+
throw new HostError("BACKEND_IMPORT", `backend import escapes app dir: ${spec}`);
|
|
982
|
+
}
|
|
983
|
+
const candidates = [
|
|
984
|
+
resolved,
|
|
985
|
+
`${resolved}.ts`,
|
|
986
|
+
`${resolved}.js`,
|
|
987
|
+
`${resolved}.tsx`,
|
|
988
|
+
path8__default.join(resolved, "index.ts"),
|
|
989
|
+
path8__default.join(resolved, "index.js"),
|
|
990
|
+
path8__default.join(resolved, "index.tsx")
|
|
991
|
+
];
|
|
992
|
+
for (const c of candidates) {
|
|
993
|
+
if (existsSync(c) && statSync(c).isFile()) {
|
|
994
|
+
return c;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
throw new HostError(
|
|
998
|
+
"BACKEND_IMPORT",
|
|
999
|
+
`cannot resolve '${spec}' from ${path8__default.relative(appDir, fromFile)}`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
function asDashboardDef(value) {
|
|
1003
|
+
const exported = isRecord2(value) && "default" in value ? value.default : value;
|
|
1004
|
+
if (!isRecord2(exported)) {
|
|
1005
|
+
throw new HostError("INVALID_DASHBOARD", "main.api must export a dashboard");
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof exported.name !== "string" || typeof exported.description !== "string") {
|
|
1008
|
+
throw new HostError("INVALID_DASHBOARD", "dashboard requires name and description");
|
|
1009
|
+
}
|
|
1010
|
+
if (!isRecord2(exported.api)) {
|
|
1011
|
+
throw new HostError("INVALID_DASHBOARD", "dashboard.api must be an object");
|
|
1012
|
+
}
|
|
1013
|
+
const api = {};
|
|
1014
|
+
for (const [key, fn] of Object.entries(exported.api)) {
|
|
1015
|
+
if (typeof fn === "function") {
|
|
1016
|
+
api[key] = fn;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const state = isRecord2(exported.state) ? exported.state : void 0;
|
|
1020
|
+
return { name: exported.name, description: exported.description, api, state };
|
|
1021
|
+
}
|
|
1022
|
+
function throwIfAborted(signal) {
|
|
1023
|
+
if (signal?.aborted) {
|
|
1024
|
+
throw new HostError("CANCELLED", "cancelled");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
var AppsManager = class {
|
|
1028
|
+
constructor(paths, capabilities, git2, config) {
|
|
1029
|
+
this.paths = paths;
|
|
1030
|
+
this.capabilities = capabilities;
|
|
1031
|
+
this.git = git2;
|
|
1032
|
+
this.config = config;
|
|
1033
|
+
}
|
|
1034
|
+
paths;
|
|
1035
|
+
capabilities;
|
|
1036
|
+
git;
|
|
1037
|
+
config;
|
|
1038
|
+
dashboardCache = /* @__PURE__ */ new Map();
|
|
1039
|
+
uiCompiler = null;
|
|
1040
|
+
/** Wire UI compiler for invalidate + reload (createHost calls this). */
|
|
1041
|
+
setUiCompiler(compiler) {
|
|
1042
|
+
this.uiCompiler = compiler;
|
|
1043
|
+
}
|
|
1044
|
+
dirOf(appId) {
|
|
1045
|
+
return this.paths.appDir(asAppId(appId));
|
|
1046
|
+
}
|
|
1047
|
+
async list() {
|
|
1048
|
+
const appsDir = this.paths.appsDir();
|
|
1049
|
+
let names;
|
|
1050
|
+
try {
|
|
1051
|
+
names = readdirSync(appsDir);
|
|
1052
|
+
} catch {
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
const out = [];
|
|
1056
|
+
for (const name of names) {
|
|
1057
|
+
const full = path8__default.join(appsDir, name);
|
|
1058
|
+
try {
|
|
1059
|
+
if (!statSync(full).isDirectory()) continue;
|
|
1060
|
+
if (!isAppId(name)) continue;
|
|
1061
|
+
const item = await this.readAppItem(name);
|
|
1062
|
+
if (item) out.push(item);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return out;
|
|
1067
|
+
}
|
|
1068
|
+
async get(appId) {
|
|
1069
|
+
try {
|
|
1070
|
+
return await this.readAppItem(asAppId(appId));
|
|
1071
|
+
} catch {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async register(appId, files) {
|
|
1076
|
+
const id = asAppId(appId);
|
|
1077
|
+
if (!files["manifest.json"]) {
|
|
1078
|
+
throw new HostError("MISSING_MANIFEST", "register requires manifest.json");
|
|
1079
|
+
}
|
|
1080
|
+
parseManifest(files["manifest.json"]);
|
|
1081
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
1082
|
+
if (rel.includes("..") || path8__default.isAbsolute(rel)) {
|
|
1083
|
+
throw new HostError("PATH_ESCAPE", `unsafe relative path: ${rel}`);
|
|
1084
|
+
}
|
|
1085
|
+
const dest = this.paths.appFile(id, rel);
|
|
1086
|
+
mkdirSync(path8__default.dirname(dest), { recursive: true });
|
|
1087
|
+
writeFileSync(dest, content, "utf8");
|
|
1088
|
+
}
|
|
1089
|
+
const dir = this.dirOf(id);
|
|
1090
|
+
await this.afterMutate(dir, { commitMessage: "registerAppFromFiles" });
|
|
1091
|
+
const item = await this.readAppItem(id);
|
|
1092
|
+
if (!item) {
|
|
1093
|
+
throw new HostError("APP_NOT_FOUND", `failed to register ${id}`);
|
|
1094
|
+
}
|
|
1095
|
+
return item;
|
|
1096
|
+
}
|
|
1097
|
+
async listFiles(appId) {
|
|
1098
|
+
asAppId(appId);
|
|
1099
|
+
if (!await this.get(appId)) {
|
|
1100
|
+
throw new HostError("APP_NOT_FOUND", `app not found: ${appId}`);
|
|
1101
|
+
}
|
|
1102
|
+
return listAppFiles(this.paths, appId);
|
|
1103
|
+
}
|
|
1104
|
+
async readFile(appId, relPath, range) {
|
|
1105
|
+
asAppId(appId);
|
|
1106
|
+
if (!await this.get(appId)) {
|
|
1107
|
+
throw new HostError("APP_NOT_FOUND", `app not found: ${appId}`);
|
|
1108
|
+
}
|
|
1109
|
+
return readAppFile(this.paths, appId, relPath, range);
|
|
1110
|
+
}
|
|
1111
|
+
async writeFile(appId, relPath, content, opts) {
|
|
1112
|
+
asAppId(appId);
|
|
1113
|
+
if (!await this.get(appId)) {
|
|
1114
|
+
throw new HostError("APP_NOT_FOUND", `app not found: ${appId}`);
|
|
1115
|
+
}
|
|
1116
|
+
const written = writeAppFile(this.paths, appId, relPath, content);
|
|
1117
|
+
const after = await this.afterMutate(this.dirOf(appId), {
|
|
1118
|
+
commitMessage: `write ${written.path}`,
|
|
1119
|
+
commit: opts?.commit
|
|
1120
|
+
});
|
|
1121
|
+
return { ...written, ...after };
|
|
1122
|
+
}
|
|
1123
|
+
async editFile(appId, relPath, edits, opts) {
|
|
1124
|
+
asAppId(appId);
|
|
1125
|
+
if (!await this.get(appId)) {
|
|
1126
|
+
throw new HostError("APP_NOT_FOUND", `app not found: ${appId}`);
|
|
1127
|
+
}
|
|
1128
|
+
const edited = editAppFile(this.paths, appId, relPath, edits);
|
|
1129
|
+
const after = await this.afterMutate(this.dirOf(appId), {
|
|
1130
|
+
commitMessage: `edit ${edited.path}`,
|
|
1131
|
+
commit: opts?.commit
|
|
1132
|
+
});
|
|
1133
|
+
return { ...edited, ...after };
|
|
1134
|
+
}
|
|
1135
|
+
async deleteFile(appId, relPath, opts) {
|
|
1136
|
+
asAppId(appId);
|
|
1137
|
+
if (!await this.get(appId)) {
|
|
1138
|
+
throw new HostError("APP_NOT_FOUND", `app not found: ${appId}`);
|
|
1139
|
+
}
|
|
1140
|
+
const deleted = deleteAppFile(this.paths, appId, relPath);
|
|
1141
|
+
const after = await this.afterMutate(this.dirOf(appId), {
|
|
1142
|
+
commitMessage: `delete ${deleted.path}`,
|
|
1143
|
+
commit: opts?.commit
|
|
1144
|
+
});
|
|
1145
|
+
return { ...deleted, ...after };
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Validate + sync-compile api/ui. On success, auto-commit if the worktree is dirty.
|
|
1149
|
+
* Replaces the old lightweight mini_app_validate tool.
|
|
1150
|
+
*/
|
|
1151
|
+
async reload(appId) {
|
|
1152
|
+
const errors = [];
|
|
1153
|
+
if (!isAppId(appId)) {
|
|
1154
|
+
errors.push("appId must be reverse-DNS (e.g. com.example.todo)");
|
|
1155
|
+
return { ok: false, errors, path: "" };
|
|
1156
|
+
}
|
|
1157
|
+
const dir = this.dirOf(appId);
|
|
1158
|
+
const app = await this.get(appId);
|
|
1159
|
+
if (!app) {
|
|
1160
|
+
errors.push("app not registered; call mini_app_register({ appId, files })");
|
|
1161
|
+
return { ok: false, errors, path: dir };
|
|
1162
|
+
}
|
|
1163
|
+
const manPath = this.paths.appFile(asAppId(appId), WorkspacePaths.Rel.manifest);
|
|
1164
|
+
try {
|
|
1165
|
+
parseManifest(readFileSync(manPath, "utf8"));
|
|
1166
|
+
} catch (cause) {
|
|
1167
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1168
|
+
errors.push(`manifest: ${message}`);
|
|
1169
|
+
}
|
|
1170
|
+
this.invalidate(dir);
|
|
1171
|
+
let apiOk = false;
|
|
1172
|
+
try {
|
|
1173
|
+
const apiPath = path8__default.join(dir, "main.api.ts");
|
|
1174
|
+
const apiJs = path8__default.join(dir, "main.api.js");
|
|
1175
|
+
const srcPath = existsSync(apiPath) ? apiPath : apiJs;
|
|
1176
|
+
if (!existsSync(srcPath)) {
|
|
1177
|
+
errors.push("missing main.api.ts");
|
|
1178
|
+
} else {
|
|
1179
|
+
compileAppSource(readFileSync(srcPath, "utf8"));
|
|
1180
|
+
this.loadMainApi(dir);
|
|
1181
|
+
apiOk = true;
|
|
1182
|
+
}
|
|
1183
|
+
} catch (cause) {
|
|
1184
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1185
|
+
errors.push(`main.api: ${message}`);
|
|
1186
|
+
}
|
|
1187
|
+
let uiOk = false;
|
|
1188
|
+
if (this.uiCompiler) {
|
|
1189
|
+
try {
|
|
1190
|
+
await this.uiCompiler.compile(dir, { locale: this.config.locale });
|
|
1191
|
+
uiOk = true;
|
|
1192
|
+
} catch (cause) {
|
|
1193
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1194
|
+
errors.push(`ui: ${message}`);
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
errors.push("ui compiler not wired");
|
|
1198
|
+
}
|
|
1199
|
+
const ok = errors.length === 0;
|
|
1200
|
+
let committed = null;
|
|
1201
|
+
if (ok) {
|
|
1202
|
+
await this.git.init(dir);
|
|
1203
|
+
if (await this.git.isDirty(dir)) {
|
|
1204
|
+
try {
|
|
1205
|
+
const { commitId } = await this.git.commit(dir, "reload");
|
|
1206
|
+
committed = { commitId, message: "reload" };
|
|
1207
|
+
} catch (cause) {
|
|
1208
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1209
|
+
errors.push(`commit: ${message}`);
|
|
1210
|
+
return {
|
|
1211
|
+
ok: false,
|
|
1212
|
+
errors,
|
|
1213
|
+
path: dir,
|
|
1214
|
+
compiled: { api: apiOk, ui: uiOk },
|
|
1215
|
+
committed: null
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
ok: errors.length === 0,
|
|
1222
|
+
errors,
|
|
1223
|
+
path: dir,
|
|
1224
|
+
compiled: { api: apiOk, ui: uiOk },
|
|
1225
|
+
committed
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async afterMutate(appDir, opts) {
|
|
1229
|
+
this.invalidate(appDir);
|
|
1230
|
+
await this.git.init(appDir);
|
|
1231
|
+
if (opts.commit === false) {
|
|
1232
|
+
return { committed: null };
|
|
1233
|
+
}
|
|
1234
|
+
try {
|
|
1235
|
+
const { commitId } = await this.git.commit(appDir, opts.commitMessage);
|
|
1236
|
+
return { committed: { commitId, message: opts.commitMessage } };
|
|
1237
|
+
} catch {
|
|
1238
|
+
return { committed: null };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async remove(appId) {
|
|
1242
|
+
const id = asAppId(appId);
|
|
1243
|
+
const dir = this.dirOf(id);
|
|
1244
|
+
this.invalidate(dir);
|
|
1245
|
+
await rm(dir, { recursive: true, force: true });
|
|
1246
|
+
}
|
|
1247
|
+
load(appId) {
|
|
1248
|
+
const dir = this.dirOf(appId);
|
|
1249
|
+
const mtime = this.dashboardMtime(dir);
|
|
1250
|
+
const hit = this.dashboardCache.get(dir);
|
|
1251
|
+
if (hit && hit.mtime === mtime) {
|
|
1252
|
+
return hit;
|
|
1253
|
+
}
|
|
1254
|
+
const def = this.loadMainApi(dir);
|
|
1255
|
+
const storage = makeFileStorage(dir, "main.storage.json");
|
|
1256
|
+
const ctx = this.buildCtx(storage, def, asAppId(appId));
|
|
1257
|
+
const rec = { mtime, def, ctx };
|
|
1258
|
+
this.dashboardCache.set(dir, rec);
|
|
1259
|
+
return rec;
|
|
1260
|
+
}
|
|
1261
|
+
async call(appId, method, args, signal) {
|
|
1262
|
+
const { def, ctx } = this.load(appId);
|
|
1263
|
+
const fn = def.api[method];
|
|
1264
|
+
if (typeof fn !== "function") {
|
|
1265
|
+
throw new HostError("METHOD_NOT_FOUND", `Method not found: ${method}`);
|
|
1266
|
+
}
|
|
1267
|
+
ctx.signal = signal;
|
|
1268
|
+
throwIfAborted(signal);
|
|
1269
|
+
return await fn(ctx, args ?? {});
|
|
1270
|
+
}
|
|
1271
|
+
invalidate(appDir) {
|
|
1272
|
+
this.dashboardCache.delete(appDir);
|
|
1273
|
+
this.uiCompiler?.invalidate(appDir);
|
|
1274
|
+
}
|
|
1275
|
+
async readAppItem(id) {
|
|
1276
|
+
const dir = this.dirOf(id);
|
|
1277
|
+
const manPath = this.paths.appFile(id, WorkspacePaths.Rel.manifest);
|
|
1278
|
+
let raw;
|
|
1279
|
+
try {
|
|
1280
|
+
raw = readFileSync(manPath, "utf8");
|
|
1281
|
+
} catch {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
let man;
|
|
1285
|
+
try {
|
|
1286
|
+
man = parseManifest(raw);
|
|
1287
|
+
} catch {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
const commits = await this.git.commitCount(dir);
|
|
1291
|
+
return {
|
|
1292
|
+
id,
|
|
1293
|
+
name: man.name,
|
|
1294
|
+
description: man.description ?? "",
|
|
1295
|
+
version: man.version,
|
|
1296
|
+
acronym: acronymOf(man.name, man.acronym),
|
|
1297
|
+
commits
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
dashboardMtime(appDir) {
|
|
1301
|
+
let max = 0;
|
|
1302
|
+
const bump = (p) => {
|
|
1303
|
+
try {
|
|
1304
|
+
const t = statSync(p).mtimeMs;
|
|
1305
|
+
if (t > max) max = t;
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
bump(path8__default.join(appDir, "manifest.json"));
|
|
1310
|
+
bump(path8__default.join(appDir, "main.api.ts"));
|
|
1311
|
+
bump(path8__default.join(appDir, "main.api.js"));
|
|
1312
|
+
try {
|
|
1313
|
+
const lib = path8__default.join(appDir, "lib");
|
|
1314
|
+
for (const n of readdirSync(lib)) {
|
|
1315
|
+
if (n.endsWith(".ts") || n.endsWith(".js")) {
|
|
1316
|
+
bump(path8__default.join(lib, n));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
return max;
|
|
1322
|
+
}
|
|
1323
|
+
loadMainApi(appDir) {
|
|
1324
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
1325
|
+
const fp = path8__default.join(appDir, "main.api.ts");
|
|
1326
|
+
const fpJs = path8__default.join(appDir, "main.api.js");
|
|
1327
|
+
const srcPath = existsSync(fp) ? fp : fpJs;
|
|
1328
|
+
if (!existsSync(srcPath)) {
|
|
1329
|
+
throw new HostError("MISSING_MAIN_API", "missing main.api.ts");
|
|
1330
|
+
}
|
|
1331
|
+
const exported = this.loadAppFile(srcPath, appDir, loaded);
|
|
1332
|
+
return asDashboardDef(exported);
|
|
1333
|
+
}
|
|
1334
|
+
loadAppFile(file, appDir, loaded) {
|
|
1335
|
+
const hit = loaded.get(file);
|
|
1336
|
+
if (hit !== void 0) {
|
|
1337
|
+
return hit;
|
|
1338
|
+
}
|
|
1339
|
+
const mod = { exports: {} };
|
|
1340
|
+
loaded.set(file, mod.exports);
|
|
1341
|
+
const src = compileAppSource(readFileSync(file, "utf8"));
|
|
1342
|
+
const req = (spec) => {
|
|
1343
|
+
if (spec === "@monkeyagent/dashboard") {
|
|
1344
|
+
return { defineDashboard, default: defineDashboard };
|
|
1345
|
+
}
|
|
1346
|
+
if (spec.startsWith(".") || spec.startsWith("lib/") || spec.startsWith("components/")) {
|
|
1347
|
+
const next = resolveAppModule(file, spec, appDir);
|
|
1348
|
+
return this.loadAppFile(next, appDir, loaded);
|
|
1349
|
+
}
|
|
1350
|
+
throw new HostError(
|
|
1351
|
+
"BACKEND_IMPORT",
|
|
1352
|
+
`backend cannot import '${spec}'. Only @monkeyagent/dashboard and relative ./lib ./components`
|
|
1353
|
+
);
|
|
1354
|
+
};
|
|
1355
|
+
const fn = new Function(
|
|
1356
|
+
"module",
|
|
1357
|
+
"exports",
|
|
1358
|
+
"require",
|
|
1359
|
+
`${src}
|
|
1360
|
+
return module.exports;`
|
|
1361
|
+
);
|
|
1362
|
+
const exported = fn(mod, mod.exports, req);
|
|
1363
|
+
const value = exported ?? mod.exports;
|
|
1364
|
+
loaded.set(file, value);
|
|
1365
|
+
return value;
|
|
1366
|
+
}
|
|
1367
|
+
buildCtx(storage, def, appId) {
|
|
1368
|
+
const caps = this.capabilities;
|
|
1369
|
+
const appDir = this.paths.appDir(appId);
|
|
1370
|
+
const box = {};
|
|
1371
|
+
const callCtx = {
|
|
1372
|
+
appId,
|
|
1373
|
+
appDir,
|
|
1374
|
+
hostLlm: this.config.llm ?? void 0
|
|
1375
|
+
};
|
|
1376
|
+
Object.defineProperty(callCtx, "signal", {
|
|
1377
|
+
enumerable: true,
|
|
1378
|
+
configurable: true,
|
|
1379
|
+
get: () => box.signal
|
|
1380
|
+
});
|
|
1381
|
+
const bound = bindCapsToContext(callCtx, caps);
|
|
1382
|
+
const { credentials: getCredentials, config: getConfig, ...capMethods } = bound;
|
|
1383
|
+
const ctx = {
|
|
1384
|
+
appId,
|
|
1385
|
+
appDir,
|
|
1386
|
+
storage,
|
|
1387
|
+
state: def.state ?? {},
|
|
1388
|
+
credentials: {},
|
|
1389
|
+
log: (...a) => {
|
|
1390
|
+
console.log("[mini-api]", ...a);
|
|
1391
|
+
},
|
|
1392
|
+
push: (_method, _params) => {
|
|
1393
|
+
},
|
|
1394
|
+
...capMethods,
|
|
1395
|
+
http: (url, opts) => httpRequest(url, { ...opts, signal: box.signal }),
|
|
1396
|
+
system: {
|
|
1397
|
+
async metrics() {
|
|
1398
|
+
const cpus = os.cpus();
|
|
1399
|
+
const load = os.loadavg();
|
|
1400
|
+
const total = os.totalmem();
|
|
1401
|
+
const free = os.freemem();
|
|
1402
|
+
return {
|
|
1403
|
+
platform: os.platform(),
|
|
1404
|
+
arch: os.arch(),
|
|
1405
|
+
hostname: os.hostname(),
|
|
1406
|
+
uptimeSec: Math.floor(os.uptime()),
|
|
1407
|
+
loadavg: { "1m": load[0], "5m": load[1], "15m": load[2] },
|
|
1408
|
+
memory: { total, free, used: total - free, usedRatio: total ? (total - free) / total : 0 },
|
|
1409
|
+
cpu: { count: cpus.length, model: cpus[0]?.model ?? "unknown", speedMHz: cpus[0]?.speed ?? 0 },
|
|
1410
|
+
collectedAt: Date.now()
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
config: {}
|
|
1415
|
+
};
|
|
1416
|
+
Object.defineProperty(ctx, "signal", {
|
|
1417
|
+
enumerable: true,
|
|
1418
|
+
get: () => box.signal,
|
|
1419
|
+
set: (value) => {
|
|
1420
|
+
box.signal = value;
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
Object.defineProperty(ctx, "credentials", {
|
|
1424
|
+
enumerable: true,
|
|
1425
|
+
get: () => getCredentials()
|
|
1426
|
+
});
|
|
1427
|
+
Object.defineProperty(ctx, "config", {
|
|
1428
|
+
enumerable: true,
|
|
1429
|
+
get: () => caps.config ? getConfig() : publicAppConfig(this.config)
|
|
1430
|
+
});
|
|
1431
|
+
return ctx;
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
function listStorageTables(dir) {
|
|
1435
|
+
try {
|
|
1436
|
+
const names = fs4__default.existsSync(dir) ? fs4__default.readdirSync(dir).filter((n) => n.endsWith(".json")) : [];
|
|
1437
|
+
return names.map((n) => {
|
|
1438
|
+
const fp = path8__default.join(dir, n);
|
|
1439
|
+
const st = fs4__default.statSync(fp);
|
|
1440
|
+
return {
|
|
1441
|
+
name: n.replace(/\.json$/, ""),
|
|
1442
|
+
size: st.size,
|
|
1443
|
+
updatedAt: st.mtime.toISOString()
|
|
1444
|
+
};
|
|
1445
|
+
}).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1446
|
+
} catch {
|
|
1447
|
+
return [];
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function storageTablePath(dir, table) {
|
|
1451
|
+
return path8__default.join(dir, `${path8__default.basename(String(table ?? ""))}.json`);
|
|
1452
|
+
}
|
|
1453
|
+
function readJsonFile(fp, fallback = null) {
|
|
1454
|
+
try {
|
|
1455
|
+
return JSON.parse(fs4__default.readFileSync(fp, "utf8"));
|
|
1456
|
+
} catch {
|
|
1457
|
+
return fallback;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
1461
|
+
var uiDistDir = null;
|
|
1462
|
+
var esbuildReady = null;
|
|
1463
|
+
function distLooksValid(dir) {
|
|
1464
|
+
return fs4.existsSync(path8.join(dir, "index.js"));
|
|
1465
|
+
}
|
|
1466
|
+
function resolveUiDistDir() {
|
|
1467
|
+
if (uiDistDir) return uiDistDir;
|
|
1468
|
+
const tryResolve = (fromFile) => {
|
|
1469
|
+
try {
|
|
1470
|
+
const req = createRequire(fromFile);
|
|
1471
|
+
const pkgJson = req.resolve("@monkey-mini-app/ui/package.json");
|
|
1472
|
+
const dir = path8.join(path8.dirname(pkgJson), "dist");
|
|
1473
|
+
return distLooksValid(dir) ? dir : null;
|
|
1474
|
+
} catch {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
const candidates = [
|
|
1479
|
+
tryResolve(path8.join(path8.dirname(fileURLToPath(import.meta.url)), "ui-compiler.ts")),
|
|
1480
|
+
tryResolve(import.meta.url),
|
|
1481
|
+
// bundled as packages/dsh/lib/index.js → walk up to repo packages/ui/dist
|
|
1482
|
+
(() => {
|
|
1483
|
+
const here = path8.dirname(fileURLToPath(import.meta.url));
|
|
1484
|
+
const guesses = [
|
|
1485
|
+
path8.resolve(here, "../../../ui/dist"),
|
|
1486
|
+
path8.resolve(here, "../../ui/dist"),
|
|
1487
|
+
path8.resolve(here, "../../../../packages/ui/dist")
|
|
1488
|
+
];
|
|
1489
|
+
for (const g of guesses) {
|
|
1490
|
+
if (distLooksValid(g)) return g;
|
|
1491
|
+
}
|
|
1492
|
+
return null;
|
|
1493
|
+
})()
|
|
1494
|
+
];
|
|
1495
|
+
for (const dir of candidates) {
|
|
1496
|
+
if (dir) {
|
|
1497
|
+
uiDistDir = dir;
|
|
1498
|
+
return dir;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
try {
|
|
1502
|
+
const pkgJson = requireFromHere.resolve("@monkey-mini-app/ui/package.json");
|
|
1503
|
+
const dir = path8.join(path8.dirname(pkgJson), "dist");
|
|
1504
|
+
if (distLooksValid(dir)) {
|
|
1505
|
+
uiDistDir = dir;
|
|
1506
|
+
return dir;
|
|
1507
|
+
}
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1510
|
+
throw new HostError(
|
|
1511
|
+
"UI_DIST_MISSING",
|
|
1512
|
+
"@monkey-mini-app/ui dist not found \u2014 run: node scripts/build-ui.mjs && ensure @monkey-mini-app/ui is a dependency of the running plugin"
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
async function loadEsbuild() {
|
|
1516
|
+
try {
|
|
1517
|
+
const mod2 = await import(
|
|
1518
|
+
/* @vite-ignore */
|
|
1519
|
+
'esbuild'
|
|
1520
|
+
);
|
|
1521
|
+
if (typeof mod2.build === "function") return mod2;
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
const mod = await import(
|
|
1525
|
+
/* @vite-ignore */
|
|
1526
|
+
'esbuild-wasm'
|
|
1527
|
+
);
|
|
1528
|
+
if (typeof mod.initialize === "function") {
|
|
1529
|
+
await mod.initialize();
|
|
1530
|
+
}
|
|
1531
|
+
return mod;
|
|
1532
|
+
}
|
|
1533
|
+
function getEsbuild() {
|
|
1534
|
+
if (!esbuildReady) esbuildReady = loadEsbuild();
|
|
1535
|
+
return esbuildReady;
|
|
1536
|
+
}
|
|
1537
|
+
function uiLocale(locale) {
|
|
1538
|
+
return locale === "en" ? "en" : "zh";
|
|
1539
|
+
}
|
|
1540
|
+
function appIdOf(appDir) {
|
|
1541
|
+
try {
|
|
1542
|
+
const man = JSON.parse(fs4.readFileSync(path8.join(appDir, "manifest.json"), "utf8"));
|
|
1543
|
+
if (typeof man.id === "string" && man.id) return man.id;
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
return path8.basename(path8.resolve(appDir));
|
|
1547
|
+
}
|
|
1548
|
+
function findUiEntry(appDir) {
|
|
1549
|
+
for (const name of ["ui.tsx", "ui.ts", "App.tsx", "App.ts"]) {
|
|
1550
|
+
const p = path8.join(appDir, name);
|
|
1551
|
+
if (fs4.existsSync(p)) return p;
|
|
1552
|
+
}
|
|
1553
|
+
throw new HostError("MISSING_UI_ENTRY", "missing ui entry (ui.tsx / App.tsx)");
|
|
1554
|
+
}
|
|
1555
|
+
function makeUiPlugin(distDir, appId) {
|
|
1556
|
+
const req = createRequire(path8.join(distDir, "index.js"));
|
|
1557
|
+
return {
|
|
1558
|
+
name: "monkey-mini-app-ui",
|
|
1559
|
+
setup(build) {
|
|
1560
|
+
build.onResolve({ filter: /(?:^|[\\/])main\.api\.(ts|js)$/ }, () => ({
|
|
1561
|
+
path: "main.api.ts",
|
|
1562
|
+
namespace: "mma-forbidden"
|
|
1563
|
+
}));
|
|
1564
|
+
build.onLoad({ filter: /.*/, namespace: "mma-forbidden" }, () => ({
|
|
1565
|
+
errors: [
|
|
1566
|
+
{
|
|
1567
|
+
text: "UI cannot import main.api.ts; use useDashboardApi() from @monkeyagent/host"
|
|
1568
|
+
}
|
|
1569
|
+
]
|
|
1570
|
+
}));
|
|
1571
|
+
build.onResolve({ filter: /^@monkey-mini-app\/ui$/ }, () => ({
|
|
1572
|
+
path: path8.join(distDir, "index.js")
|
|
1573
|
+
}));
|
|
1574
|
+
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => {
|
|
1575
|
+
try {
|
|
1576
|
+
return { path: req.resolve(args.path) };
|
|
1577
|
+
} catch {
|
|
1578
|
+
return { path: args.path, external: true };
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
build.onResolve({ filter: /^@monkeyagent\/host$/ }, () => ({
|
|
1582
|
+
namespace: "mma-host",
|
|
1583
|
+
path: "useDashboardApi"
|
|
1584
|
+
}));
|
|
1585
|
+
build.onLoad({ filter: /.*/, namespace: "mma-host" }, () => ({
|
|
1586
|
+
contents: `
|
|
1587
|
+
import { useCallback } from "react";
|
|
1588
|
+
const __MMA_APP_ID = ${JSON.stringify(appId)};
|
|
1589
|
+
export function useDashboardApi() {
|
|
1590
|
+
const call = useCallback(async (m, a) => {
|
|
1591
|
+
const j = await fetch("/api/call", {
|
|
1592
|
+
method: "POST",
|
|
1593
|
+
headers: { "content-type": "application/json" },
|
|
1594
|
+
body: JSON.stringify({ appId: __MMA_APP_ID, method: m, args: a || {} }),
|
|
1595
|
+
}).then((r) => r.json());
|
|
1596
|
+
if (!j.ok) throw new Error(j.error || "call failed");
|
|
1597
|
+
return j.value;
|
|
1598
|
+
}, []);
|
|
1599
|
+
return { call };
|
|
1600
|
+
}
|
|
1601
|
+
`,
|
|
1602
|
+
loader: "js"
|
|
1603
|
+
}));
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function walkMtime(dir, bump) {
|
|
1608
|
+
for (const n of fs4.readdirSync(dir)) {
|
|
1609
|
+
const full = path8.join(dir, n);
|
|
1610
|
+
const st = fs4.statSync(full);
|
|
1611
|
+
if (st.isDirectory()) {
|
|
1612
|
+
if (n === "storage" || n === ".git" || n === "node_modules" || n === ".ui-build") continue;
|
|
1613
|
+
walkMtime(full, bump);
|
|
1614
|
+
} else if (/\.(tsx?|jsx?|json|css)$/.test(n)) {
|
|
1615
|
+
bump(full);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
var UiCompiler = class {
|
|
1620
|
+
constructor(paths) {
|
|
1621
|
+
this.paths = paths;
|
|
1622
|
+
}
|
|
1623
|
+
paths;
|
|
1624
|
+
buildCache = /* @__PURE__ */ new Map();
|
|
1625
|
+
invalidate(appDir) {
|
|
1626
|
+
this.buildCache.delete(appDir);
|
|
1627
|
+
}
|
|
1628
|
+
cacheSize() {
|
|
1629
|
+
return this.buildCache.size;
|
|
1630
|
+
}
|
|
1631
|
+
async compile(appDir, options) {
|
|
1632
|
+
const locale = uiLocale(options.locale);
|
|
1633
|
+
const sig = `${this.cacheSig(appDir)}-${locale}`;
|
|
1634
|
+
const hit = this.buildCache.get(appDir);
|
|
1635
|
+
if (hit && hit.sig === sig) return hit.files;
|
|
1636
|
+
const cacheKey = this.cacheKey(appDir, sig);
|
|
1637
|
+
const cacheDir = path8.join(this.paths.uiCacheDir(), cacheKey);
|
|
1638
|
+
try {
|
|
1639
|
+
if (fs4.existsSync(path8.join(cacheDir, "entry.js"))) {
|
|
1640
|
+
const names = fs4.readdirSync(cacheDir).filter((n) => n.endsWith(".js"));
|
|
1641
|
+
const files2 = names.sort((a, b) => a === "entry.js" ? -1 : b === "entry.js" ? 1 : a.localeCompare(b)).map((n) => ({ name: n, contents: fs4.readFileSync(path8.join(cacheDir, n)) }));
|
|
1642
|
+
this.buildCache.set(appDir, { sig, files: files2 });
|
|
1643
|
+
return files2;
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
}
|
|
1647
|
+
const distDir = resolveUiDistDir();
|
|
1648
|
+
const esbuild = await getEsbuild();
|
|
1649
|
+
const entry = findUiEntry(appDir);
|
|
1650
|
+
const uiRel = path8.basename(entry);
|
|
1651
|
+
const wrapper = `
|
|
1652
|
+
import { createRoot } from "react-dom/client";
|
|
1653
|
+
import { UiProvider } from "@monkey-mini-app/ui";
|
|
1654
|
+
import Ui from "./${uiRel}";
|
|
1655
|
+
const rootEl = document.getElementById("root");
|
|
1656
|
+
if (rootEl) {
|
|
1657
|
+
rootEl.className = "";
|
|
1658
|
+
rootEl.removeAttribute("role");
|
|
1659
|
+
rootEl.removeAttribute("aria-label");
|
|
1660
|
+
rootEl.replaceChildren();
|
|
1661
|
+
createRoot(rootEl).render(<UiProvider locale=${JSON.stringify(locale)}><Ui /></UiProvider>);
|
|
1662
|
+
}
|
|
1663
|
+
`;
|
|
1664
|
+
let res;
|
|
1665
|
+
try {
|
|
1666
|
+
res = await esbuild.build({
|
|
1667
|
+
stdin: {
|
|
1668
|
+
contents: wrapper,
|
|
1669
|
+
resolveDir: appDir,
|
|
1670
|
+
sourcefile: "entry.tsx",
|
|
1671
|
+
loader: "tsx"
|
|
1672
|
+
},
|
|
1673
|
+
outfile: path8.join(appDir, ".ui-build", "entry.js"),
|
|
1674
|
+
bundle: true,
|
|
1675
|
+
format: "esm",
|
|
1676
|
+
write: false,
|
|
1677
|
+
platform: "browser",
|
|
1678
|
+
target: "es2020",
|
|
1679
|
+
plugins: [makeUiPlugin(distDir, appIdOf(appDir))],
|
|
1680
|
+
loader: { ".tsx": "tsx", ".ts": "ts" },
|
|
1681
|
+
jsx: "automatic",
|
|
1682
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
1683
|
+
minify: true,
|
|
1684
|
+
legalComments: "none",
|
|
1685
|
+
logLevel: "silent"
|
|
1686
|
+
});
|
|
1687
|
+
} catch (cause) {
|
|
1688
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1689
|
+
throw new HostError("UI_COMPILE_FAILED", message, { cause });
|
|
1690
|
+
}
|
|
1691
|
+
const output = res.outputFiles;
|
|
1692
|
+
if (!output || output.length === 0) {
|
|
1693
|
+
throw new HostError("UI_COMPILE_FAILED", "esbuild produced no output");
|
|
1694
|
+
}
|
|
1695
|
+
const files = output.map((o) => ({ name: path8.basename(o.path), contents: o.contents })).sort((a, b) => {
|
|
1696
|
+
const am = a.name === "entry.js" ? 0 : 1;
|
|
1697
|
+
const bm = b.name === "entry.js" ? 0 : 1;
|
|
1698
|
+
return am - bm || a.name.localeCompare(b.name);
|
|
1699
|
+
});
|
|
1700
|
+
try {
|
|
1701
|
+
fs4.mkdirSync(cacheDir, { recursive: true });
|
|
1702
|
+
for (const f of files) {
|
|
1703
|
+
fs4.writeFileSync(path8.join(cacheDir, f.name), f.contents);
|
|
1704
|
+
}
|
|
1705
|
+
} catch {
|
|
1706
|
+
}
|
|
1707
|
+
this.buildCache.set(appDir, { sig, files });
|
|
1708
|
+
return files;
|
|
1709
|
+
}
|
|
1710
|
+
cacheKey(appDir, sig) {
|
|
1711
|
+
const id = path8.basename(path8.resolve(appDir)).replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1712
|
+
return `${id}-${sig}`;
|
|
1713
|
+
}
|
|
1714
|
+
cacheSig(appDir) {
|
|
1715
|
+
let max = 0;
|
|
1716
|
+
let count = 0;
|
|
1717
|
+
const bump = (fp) => {
|
|
1718
|
+
try {
|
|
1719
|
+
const t = fs4.statSync(fp).mtimeMs;
|
|
1720
|
+
if (t > max) max = t;
|
|
1721
|
+
count++;
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
for (const name of ["ui.tsx", "ui.ts", "App.tsx", "App.ts", "manifest.json"]) {
|
|
1726
|
+
bump(path8.join(appDir, name));
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
walkMtime(appDir, bump);
|
|
1730
|
+
} catch {
|
|
1731
|
+
}
|
|
1732
|
+
try {
|
|
1733
|
+
walkMtime(resolveUiDistDir(), (fp) => {
|
|
1734
|
+
if (/\.(tsx?|jsx?|mjs|cjs|js|css|json)$/.test(path8.basename(fp))) {
|
|
1735
|
+
bump(fp);
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
} catch {
|
|
1739
|
+
}
|
|
1740
|
+
return `${max.toString(36)}-${count.toString(36)}`;
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
// src/config/defaults.ts
|
|
1745
|
+
var DEFAULT_HOST_CONFIG_SEED = Object.freeze({
|
|
1746
|
+
runtimeRoot: "~/.monkey-mini-app/runtime",
|
|
1747
|
+
hostPort: 17880,
|
|
1748
|
+
theme: "light",
|
|
1749
|
+
palette: "default",
|
|
1750
|
+
locale: "zh-CN",
|
|
1751
|
+
chatLanguage: "zh-CN",
|
|
1752
|
+
llm: null
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// src/types.ts
|
|
1756
|
+
var THEME_IDS = ["light", "dark"];
|
|
1757
|
+
var PALETTE_IDS = [
|
|
1758
|
+
"default",
|
|
1759
|
+
"tokyo",
|
|
1760
|
+
"forest",
|
|
1761
|
+
"matcha",
|
|
1762
|
+
"yellow",
|
|
1763
|
+
"zoro",
|
|
1764
|
+
"hokage",
|
|
1765
|
+
"slate"
|
|
1766
|
+
];
|
|
1767
|
+
var LOCALE_IDS = ["zh-CN", "en"];
|
|
1768
|
+
|
|
1769
|
+
// src/config/parse.ts
|
|
1770
|
+
function isRecord3(value) {
|
|
1771
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1772
|
+
}
|
|
1773
|
+
function requireField(raw, key) {
|
|
1774
|
+
if (!(key in raw) || raw[key] === void 0) {
|
|
1775
|
+
throw new HostConfigError(`host config missing ${key}`);
|
|
1776
|
+
}
|
|
1777
|
+
return raw[key];
|
|
1778
|
+
}
|
|
1779
|
+
function parseEnum(value, key, allowed) {
|
|
1780
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
1781
|
+
throw new HostConfigError(`host config ${key} is invalid`);
|
|
1782
|
+
}
|
|
1783
|
+
return value;
|
|
1784
|
+
}
|
|
1785
|
+
function parseHostPort(value) {
|
|
1786
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65535) {
|
|
1787
|
+
throw new HostConfigError("host config hostPort is invalid");
|
|
1788
|
+
}
|
|
1789
|
+
return value;
|
|
1790
|
+
}
|
|
1791
|
+
function parsePalette(value) {
|
|
1792
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1793
|
+
throw new HostConfigError("host config palette is invalid");
|
|
1794
|
+
}
|
|
1795
|
+
return value.trim();
|
|
1796
|
+
}
|
|
1797
|
+
function parseLlm(value) {
|
|
1798
|
+
if (value === null) {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
if (!isRecord3(value)) {
|
|
1802
|
+
throw new HostConfigError("host config llm is invalid");
|
|
1803
|
+
}
|
|
1804
|
+
const provider = value.provider;
|
|
1805
|
+
const model = value.model;
|
|
1806
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
1807
|
+
throw new HostConfigError("host config llm.provider is invalid");
|
|
1808
|
+
}
|
|
1809
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
1810
|
+
throw new HostConfigError("host config llm.model is invalid");
|
|
1811
|
+
}
|
|
1812
|
+
return { provider, model };
|
|
1813
|
+
}
|
|
1814
|
+
function parseHostConfig(raw) {
|
|
1815
|
+
if (!isRecord3(raw)) {
|
|
1816
|
+
throw new HostConfigError("host config must be an object");
|
|
1817
|
+
}
|
|
1818
|
+
const runtimeRootRaw = requireField(raw, "runtimeRoot");
|
|
1819
|
+
if (typeof runtimeRootRaw !== "string") {
|
|
1820
|
+
throw new HostConfigError("host config runtimeRoot is invalid");
|
|
1821
|
+
}
|
|
1822
|
+
let runtimeRoot;
|
|
1823
|
+
try {
|
|
1824
|
+
runtimeRoot = asAbsolutePath(runtimeRootRaw);
|
|
1825
|
+
} catch (cause) {
|
|
1826
|
+
throw new HostConfigError("host config runtimeRoot must be an absolute path", { cause });
|
|
1827
|
+
}
|
|
1828
|
+
return {
|
|
1829
|
+
runtimeRoot,
|
|
1830
|
+
hostPort: parseHostPort(requireField(raw, "hostPort")),
|
|
1831
|
+
theme: parseEnum(requireField(raw, "theme"), "theme", THEME_IDS),
|
|
1832
|
+
palette: parsePalette(requireField(raw, "palette")),
|
|
1833
|
+
locale: parseEnum(requireField(raw, "locale"), "locale", LOCALE_IDS),
|
|
1834
|
+
chatLanguage: parseEnum(
|
|
1835
|
+
requireField(raw, "chatLanguage"),
|
|
1836
|
+
"chatLanguage",
|
|
1837
|
+
LOCALE_IDS
|
|
1838
|
+
),
|
|
1839
|
+
llm: parseLlm(requireField(raw, "llm"))
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/config/bootstrap.ts
|
|
1844
|
+
function expandHome(p) {
|
|
1845
|
+
if (p === "~") {
|
|
1846
|
+
return homedir();
|
|
1847
|
+
}
|
|
1848
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) {
|
|
1849
|
+
return path8__default.join(homedir(), p.slice(2));
|
|
1850
|
+
}
|
|
1851
|
+
return p;
|
|
1852
|
+
}
|
|
1853
|
+
function resolveRuntimeRoot(p) {
|
|
1854
|
+
return path8__default.resolve(expandHome(p));
|
|
1855
|
+
}
|
|
1856
|
+
function bootstrapHostConfig(input) {
|
|
1857
|
+
const merged = { ...DEFAULT_HOST_CONFIG_SEED };
|
|
1858
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1859
|
+
if (value !== void 0) {
|
|
1860
|
+
merged[key] = value;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (typeof merged.runtimeRoot === "string") {
|
|
1864
|
+
merged.runtimeRoot = resolveRuntimeRoot(merged.runtimeRoot);
|
|
1865
|
+
}
|
|
1866
|
+
return parseHostConfig(merged);
|
|
1867
|
+
}
|
|
1868
|
+
function loadHostConfig(paths) {
|
|
1869
|
+
const file = paths.hostConfigFile();
|
|
1870
|
+
let text;
|
|
1871
|
+
try {
|
|
1872
|
+
text = readFileSync(file, "utf8");
|
|
1873
|
+
} catch (cause) {
|
|
1874
|
+
throw new HostConfigError(`host config not found: ${file}`, {
|
|
1875
|
+
code: "HOST_CONFIG_NOT_FOUND",
|
|
1876
|
+
cause
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
let raw;
|
|
1880
|
+
try {
|
|
1881
|
+
raw = JSON.parse(text);
|
|
1882
|
+
} catch (cause) {
|
|
1883
|
+
throw new HostConfigError(`host config is not valid JSON: ${file}`, {
|
|
1884
|
+
cause
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
return parseHostConfig(raw);
|
|
1888
|
+
}
|
|
1889
|
+
function writeHostConfig(paths, config) {
|
|
1890
|
+
const payload = {
|
|
1891
|
+
runtimeRoot: config.runtimeRoot,
|
|
1892
|
+
hostPort: config.hostPort,
|
|
1893
|
+
theme: config.theme,
|
|
1894
|
+
palette: config.palette,
|
|
1895
|
+
locale: config.locale,
|
|
1896
|
+
chatLanguage: config.chatLanguage,
|
|
1897
|
+
llm: config.llm
|
|
1898
|
+
};
|
|
1899
|
+
writeFileSync(paths.hostConfigFile(), `${JSON.stringify(payload, null, 2)}
|
|
1900
|
+
`, "utf8");
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/events/host-events.ts
|
|
1904
|
+
var HostEventBus = class {
|
|
1905
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1906
|
+
seq = 0;
|
|
1907
|
+
subscribe(listener) {
|
|
1908
|
+
this.listeners.add(listener);
|
|
1909
|
+
return () => {
|
|
1910
|
+
this.listeners.delete(listener);
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
emit(event) {
|
|
1914
|
+
for (const listener of [...this.listeners]) {
|
|
1915
|
+
try {
|
|
1916
|
+
listener(event);
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
/** Monotonic id for SSE `id:` fields. */
|
|
1922
|
+
nextId() {
|
|
1923
|
+
this.seq += 1;
|
|
1924
|
+
return this.seq;
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
function formatSse(event, id) {
|
|
1928
|
+
return `id: ${id}
|
|
1929
|
+
event: ${event.type}
|
|
1930
|
+
data: ${JSON.stringify({
|
|
1931
|
+
appId: event.appId,
|
|
1932
|
+
title: event.title
|
|
1933
|
+
})}
|
|
1934
|
+
|
|
1935
|
+
`;
|
|
1936
|
+
}
|
|
1937
|
+
var COMMIT_TTL_MS = 6e4;
|
|
1938
|
+
var DEFAULT_AUTHOR = {
|
|
1939
|
+
name: "mini-agent",
|
|
1940
|
+
email: "agent@local"
|
|
1941
|
+
};
|
|
1942
|
+
var GITIGNORE = `storage/
|
|
1943
|
+
.DS_Store
|
|
1944
|
+
node_modules/
|
|
1945
|
+
.ui-build/
|
|
1946
|
+
`;
|
|
1947
|
+
async function resolveHead(dir) {
|
|
1948
|
+
try {
|
|
1949
|
+
return await git.resolveRef({ fs: fs4__default, dir, ref: "HEAD" });
|
|
1950
|
+
} catch {
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
async function blobOidAtPath(dir, treeOid, filepath) {
|
|
1955
|
+
const parts = filepath.split("/").filter(Boolean);
|
|
1956
|
+
let oid = treeOid;
|
|
1957
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1958
|
+
const { tree } = await git.readTree({ fs: fs4__default, dir, oid });
|
|
1959
|
+
const entry = tree.find((e) => e.path === parts[i]);
|
|
1960
|
+
if (!entry) return null;
|
|
1961
|
+
if (i === parts.length - 1) {
|
|
1962
|
+
return entry.type === "blob" ? entry.oid : null;
|
|
1963
|
+
}
|
|
1964
|
+
if (entry.type !== "tree") return null;
|
|
1965
|
+
oid = entry.oid;
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
async function readBlobText(dir, oid) {
|
|
1970
|
+
try {
|
|
1971
|
+
const { blob } = await git.readBlob({ fs: fs4__default, dir, oid });
|
|
1972
|
+
const buf = Buffer.from(blob);
|
|
1973
|
+
if (buf.includes(0)) {
|
|
1974
|
+
return null;
|
|
1975
|
+
}
|
|
1976
|
+
return buf.toString("utf8");
|
|
1977
|
+
} catch {
|
|
1978
|
+
return null;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
async function treeFileMap(dir, treeOid, prefix = "") {
|
|
1982
|
+
const map = /* @__PURE__ */ new Map();
|
|
1983
|
+
const tree = await git.readTree({ fs: fs4__default, dir, oid: treeOid });
|
|
1984
|
+
for (const entry of tree.tree) {
|
|
1985
|
+
const fp = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
1986
|
+
if (entry.type === "blob") {
|
|
1987
|
+
map.set(fp, entry.oid);
|
|
1988
|
+
} else if (entry.type === "tree") {
|
|
1989
|
+
const sub = await treeFileMap(dir, entry.oid, fp);
|
|
1990
|
+
for (const [k, v] of sub) {
|
|
1991
|
+
map.set(k, v);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return map;
|
|
1996
|
+
}
|
|
1997
|
+
function countLines(text) {
|
|
1998
|
+
if (text === "") {
|
|
1999
|
+
return 0;
|
|
2000
|
+
}
|
|
2001
|
+
return text.replace(/\n$/, "").split("\n").length;
|
|
2002
|
+
}
|
|
2003
|
+
function lineStats(before, after) {
|
|
2004
|
+
if (before === null && after === null) {
|
|
2005
|
+
return { add: -1, del: -1 };
|
|
2006
|
+
}
|
|
2007
|
+
if (before === null) {
|
|
2008
|
+
return { add: countLines(after ?? ""), del: 0 };
|
|
2009
|
+
}
|
|
2010
|
+
if (after === null) {
|
|
2011
|
+
return { add: 0, del: countLines(before) };
|
|
2012
|
+
}
|
|
2013
|
+
const a = before.replace(/\n$/, "").split("\n");
|
|
2014
|
+
const b = after.replace(/\n$/, "").split("\n");
|
|
2015
|
+
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
2016
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2017
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2018
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const lcs = dp[a.length][b.length];
|
|
2022
|
+
return { add: b.length - lcs, del: a.length - lcs };
|
|
2023
|
+
}
|
|
2024
|
+
function unifiedDiff(before, after) {
|
|
2025
|
+
const a = before == null ? [] : before.replace(/\n$/, "").split("\n");
|
|
2026
|
+
const b = after == null ? [] : after.replace(/\n$/, "").split("\n");
|
|
2027
|
+
const out = [];
|
|
2028
|
+
let i = 0;
|
|
2029
|
+
let j = 0;
|
|
2030
|
+
while (i < a.length || j < b.length) {
|
|
2031
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
2032
|
+
out.push(` ${a[i]}`);
|
|
2033
|
+
i++;
|
|
2034
|
+
j++;
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
let foundJ = -1;
|
|
2038
|
+
for (let jj = j; jj < Math.min(j + 40, b.length); jj++) {
|
|
2039
|
+
if (a[i] !== void 0 && a[i] === b[jj]) {
|
|
2040
|
+
foundJ = jj;
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
let foundI = -1;
|
|
2045
|
+
for (let ii = i; ii < Math.min(i + 40, a.length); ii++) {
|
|
2046
|
+
if (b[j] !== void 0 && b[j] === a[ii]) {
|
|
2047
|
+
foundI = ii;
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (foundJ >= 0 && (foundI < 0 || foundJ - j <= foundI - i)) {
|
|
2052
|
+
while (j < foundJ) {
|
|
2053
|
+
out.push(`+${b[j]}`);
|
|
2054
|
+
j++;
|
|
2055
|
+
}
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
if (foundI >= 0) {
|
|
2059
|
+
while (i < foundI) {
|
|
2060
|
+
out.push(`-${a[i]}`);
|
|
2061
|
+
i++;
|
|
2062
|
+
}
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (i < a.length) {
|
|
2066
|
+
out.push(`-${a[i]}`);
|
|
2067
|
+
i++;
|
|
2068
|
+
}
|
|
2069
|
+
if (j < b.length) {
|
|
2070
|
+
out.push(`+${b[j]}`);
|
|
2071
|
+
j++;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return out.join("\n");
|
|
2075
|
+
}
|
|
2076
|
+
async function ensureGitignore(dir) {
|
|
2077
|
+
const p = path8__default.join(dir, ".gitignore");
|
|
2078
|
+
let existing = "";
|
|
2079
|
+
try {
|
|
2080
|
+
existing = await fsp.readFile(p, "utf8");
|
|
2081
|
+
} catch {
|
|
2082
|
+
await fsp.writeFile(p, GITIGNORE, "utf8");
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const missing = GITIGNORE.split("\n").filter((line) => {
|
|
2086
|
+
const t = line.trim();
|
|
2087
|
+
return t && !existing.split("\n").some((e) => e.trim() === t);
|
|
2088
|
+
});
|
|
2089
|
+
if (missing.length > 0) {
|
|
2090
|
+
const next = existing.endsWith("\n") || existing.length === 0 ? `${existing}${missing.join("\n")}
|
|
2091
|
+
` : `${existing}
|
|
2092
|
+
${missing.join("\n")}
|
|
2093
|
+
`;
|
|
2094
|
+
await fsp.writeFile(p, next, "utf8");
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
async function listBackupRefs(dir) {
|
|
2098
|
+
const refsDir = path8__default.join(dir, ".git", "refs", "backup");
|
|
2099
|
+
try {
|
|
2100
|
+
const names = await fsp.readdir(refsDir);
|
|
2101
|
+
const out = [];
|
|
2102
|
+
for (const name of names) {
|
|
2103
|
+
const oid = (await fsp.readFile(path8__default.join(refsDir, name), "utf8")).trim();
|
|
2104
|
+
out.push({ name: `backup/${name}`, oid });
|
|
2105
|
+
}
|
|
2106
|
+
return out;
|
|
2107
|
+
} catch {
|
|
2108
|
+
return [];
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async function stageAll(dir) {
|
|
2112
|
+
const status = await git.statusMatrix({ fs: fs4__default, dir });
|
|
2113
|
+
for (const [filepath, headStatus, workdirStatus] of status) {
|
|
2114
|
+
if (filepath === ".") continue;
|
|
2115
|
+
try {
|
|
2116
|
+
if (workdirStatus === 0) {
|
|
2117
|
+
if (headStatus !== 0) {
|
|
2118
|
+
await git.remove({ fs: fs4__default, dir, filepath });
|
|
2119
|
+
}
|
|
2120
|
+
} else {
|
|
2121
|
+
await git.add({ fs: fs4__default, dir, filepath });
|
|
2122
|
+
}
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
var GitHistory = class {
|
|
2128
|
+
commitCountCache = /* @__PURE__ */ new Map();
|
|
2129
|
+
invalidateCount(dir) {
|
|
2130
|
+
this.commitCountCache.delete(dir);
|
|
2131
|
+
}
|
|
2132
|
+
async init(dir) {
|
|
2133
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
2134
|
+
const gitdir = path8__default.join(dir, ".git");
|
|
2135
|
+
try {
|
|
2136
|
+
await fsp.access(gitdir);
|
|
2137
|
+
} catch {
|
|
2138
|
+
await git.init({ fs: fs4__default, dir, defaultBranch: "main" });
|
|
2139
|
+
}
|
|
2140
|
+
await ensureGitignore(dir);
|
|
2141
|
+
try {
|
|
2142
|
+
await git.resolveRef({ fs: fs4__default, dir, ref: "main" });
|
|
2143
|
+
} catch {
|
|
2144
|
+
await git.add({ fs: fs4__default, dir, filepath: ".gitignore" });
|
|
2145
|
+
await git.commit({
|
|
2146
|
+
fs: fs4__default,
|
|
2147
|
+
dir,
|
|
2148
|
+
message: "init",
|
|
2149
|
+
author: DEFAULT_AUTHOR
|
|
2150
|
+
});
|
|
2151
|
+
this.invalidateCount(dir);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
async commit(dir, message, opts) {
|
|
2155
|
+
await stageAll(dir);
|
|
2156
|
+
const commitId = await git.commit({
|
|
2157
|
+
fs: fs4__default,
|
|
2158
|
+
dir,
|
|
2159
|
+
message,
|
|
2160
|
+
author: opts?.author ?? DEFAULT_AUTHOR
|
|
2161
|
+
});
|
|
2162
|
+
this.invalidateCount(dir);
|
|
2163
|
+
return { commitId };
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* True when worktree or index differs from HEAD.
|
|
2167
|
+
* Content-hashes tracked files that statusMatrix marks clean — isomorphic-git
|
|
2168
|
+
* can miss same-size / same-mtime edits (see stageAll comment).
|
|
2169
|
+
*/
|
|
2170
|
+
async isDirty(dir) {
|
|
2171
|
+
try {
|
|
2172
|
+
const status = await git.statusMatrix({ fs: fs4__default, dir });
|
|
2173
|
+
for (const [filepath, head, workdir, stage] of status) {
|
|
2174
|
+
if (filepath === ".") continue;
|
|
2175
|
+
if (head !== workdir || workdir !== stage) return true;
|
|
2176
|
+
}
|
|
2177
|
+
const headOid = await resolveHead(dir);
|
|
2178
|
+
if (!headOid) return false;
|
|
2179
|
+
const { commit } = await git.readCommit({ fs: fs4__default, dir, oid: headOid });
|
|
2180
|
+
for (const [filepath, head, workdir] of status) {
|
|
2181
|
+
if (filepath === "." || head !== 1 || workdir !== 1) continue;
|
|
2182
|
+
try {
|
|
2183
|
+
const workBuf = await fsp.readFile(path8__default.join(dir, filepath));
|
|
2184
|
+
const { oid: workOid } = await git.hashBlob({ object: workBuf });
|
|
2185
|
+
const headBlobOid = await blobOidAtPath(dir, commit.tree, filepath);
|
|
2186
|
+
if (!headBlobOid || headBlobOid !== workOid) return true;
|
|
2187
|
+
} catch {
|
|
2188
|
+
return true;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return false;
|
|
2192
|
+
} catch {
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
async listCommits(dir, opts) {
|
|
2197
|
+
const limit = opts?.limit ?? 100;
|
|
2198
|
+
const nodesMap = /* @__PURE__ */ new Map();
|
|
2199
|
+
const tips = [];
|
|
2200
|
+
let head = "";
|
|
2201
|
+
try {
|
|
2202
|
+
head = await git.resolveRef({ fs: fs4__default, dir, ref: "HEAD" });
|
|
2203
|
+
tips.push({ name: "main", commitId: head });
|
|
2204
|
+
} catch {
|
|
2205
|
+
return { head: "", nodes: [], tips: [] };
|
|
2206
|
+
}
|
|
2207
|
+
const walk = async (oid, remaining) => {
|
|
2208
|
+
if (remaining <= 0 || nodesMap.has(oid)) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const commit = await git.readCommit({ fs: fs4__default, dir, oid });
|
|
2212
|
+
const parentIds = commit.commit.parent ?? [];
|
|
2213
|
+
nodesMap.set(oid, {
|
|
2214
|
+
id: oid,
|
|
2215
|
+
parentIds,
|
|
2216
|
+
message: commit.commit.message.trim(),
|
|
2217
|
+
time: new Date(commit.commit.author.timestamp * 1e3).toISOString()
|
|
2218
|
+
});
|
|
2219
|
+
for (const parent of parentIds) {
|
|
2220
|
+
await walk(parent, remaining - 1);
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
await walk(head, limit);
|
|
2224
|
+
const backups = await listBackupRefs(dir);
|
|
2225
|
+
for (const b of backups) {
|
|
2226
|
+
tips.push({ name: b.name, commitId: b.oid });
|
|
2227
|
+
await walk(b.oid, limit);
|
|
2228
|
+
}
|
|
2229
|
+
return {
|
|
2230
|
+
head,
|
|
2231
|
+
nodes: [...nodesMap.values()],
|
|
2232
|
+
tips
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
async revert(dir, commitId, opts) {
|
|
2236
|
+
const target = await git.readCommit({ fs: fs4__default, dir, oid: commitId });
|
|
2237
|
+
const parents = target.commit.parent;
|
|
2238
|
+
if (parents.length !== 1) {
|
|
2239
|
+
throw new HostError("REVERT_CONFLICT", "cannot revert a commit without exactly one parent");
|
|
2240
|
+
}
|
|
2241
|
+
const parentOid = parents[0];
|
|
2242
|
+
const parentCommit = await git.readCommit({ fs: fs4__default, dir, oid: parentOid });
|
|
2243
|
+
const before = await treeFileMap(dir, parentCommit.commit.tree);
|
|
2244
|
+
const after = await treeFileMap(dir, target.commit.tree);
|
|
2245
|
+
const allPaths = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
|
|
2246
|
+
const applyParentTree = async () => {
|
|
2247
|
+
for (const fp of allPaths) {
|
|
2248
|
+
const b = before.get(fp);
|
|
2249
|
+
const a = after.get(fp);
|
|
2250
|
+
if (b === a) continue;
|
|
2251
|
+
const abs = path8__default.join(dir, fp);
|
|
2252
|
+
if (!b) {
|
|
2253
|
+
try {
|
|
2254
|
+
await fsp.unlink(abs);
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
try {
|
|
2258
|
+
await git.remove({ fs: fs4__default, dir, filepath: fp });
|
|
2259
|
+
} catch {
|
|
2260
|
+
}
|
|
2261
|
+
} else {
|
|
2262
|
+
const { blob } = await git.readBlob({ fs: fs4__default, dir, oid: b });
|
|
2263
|
+
await fsp.mkdir(path8__default.dirname(abs), { recursive: true });
|
|
2264
|
+
await fsp.writeFile(abs, Buffer.from(blob));
|
|
2265
|
+
await git.add({ fs: fs4__default, dir, filepath: fp });
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
await applyParentTree();
|
|
2270
|
+
await stageAll(dir);
|
|
2271
|
+
const newId = await git.commit({
|
|
2272
|
+
fs: fs4__default,
|
|
2273
|
+
dir,
|
|
2274
|
+
message: opts?.message ?? `revert: ${commitId.slice(0, 7)}`,
|
|
2275
|
+
author: DEFAULT_AUTHOR
|
|
2276
|
+
});
|
|
2277
|
+
this.invalidateCount(dir);
|
|
2278
|
+
return { commitId: newId };
|
|
2279
|
+
}
|
|
2280
|
+
async resetTo(dir, commitId, opts) {
|
|
2281
|
+
const createBackup = opts?.createBackupRef !== false;
|
|
2282
|
+
let backupRef;
|
|
2283
|
+
if (createBackup) {
|
|
2284
|
+
try {
|
|
2285
|
+
const head = await git.resolveRef({ fs: fs4__default, dir, ref: "HEAD" });
|
|
2286
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2287
|
+
const ref = `refs/backup/${stamp}`;
|
|
2288
|
+
await git.writeRef({ fs: fs4__default, dir, ref, value: head });
|
|
2289
|
+
backupRef = `backup/${stamp}`;
|
|
2290
|
+
} catch {
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
await git.branch({
|
|
2294
|
+
fs: fs4__default,
|
|
2295
|
+
dir,
|
|
2296
|
+
ref: "main",
|
|
2297
|
+
object: commitId,
|
|
2298
|
+
force: true,
|
|
2299
|
+
checkout: true
|
|
2300
|
+
});
|
|
2301
|
+
this.invalidateCount(dir);
|
|
2302
|
+
return { backupRef };
|
|
2303
|
+
}
|
|
2304
|
+
async commitCount(dir) {
|
|
2305
|
+
const hit = this.commitCountCache.get(dir);
|
|
2306
|
+
if (hit && Date.now() - hit.at < COMMIT_TTL_MS) {
|
|
2307
|
+
return hit.count;
|
|
2308
|
+
}
|
|
2309
|
+
let count = 0;
|
|
2310
|
+
try {
|
|
2311
|
+
const head = await resolveHead(dir);
|
|
2312
|
+
if (!head) {
|
|
2313
|
+
this.commitCountCache.set(dir, { at: Date.now(), count: 0 });
|
|
2314
|
+
return 0;
|
|
2315
|
+
}
|
|
2316
|
+
const commits = await git.log({ fs: fs4__default, dir, depth: 1e5 });
|
|
2317
|
+
count = commits.length;
|
|
2318
|
+
} catch {
|
|
2319
|
+
count = 0;
|
|
2320
|
+
}
|
|
2321
|
+
this.commitCountCache.set(dir, { at: Date.now(), count });
|
|
2322
|
+
return count;
|
|
2323
|
+
}
|
|
2324
|
+
async log(dir, limit) {
|
|
2325
|
+
try {
|
|
2326
|
+
const commits = await git.log({ fs: fs4__default, dir, depth: Math.max(1, limit) });
|
|
2327
|
+
return commits.map((c) => ({
|
|
2328
|
+
id: c.oid,
|
|
2329
|
+
time: new Date(c.commit.author.timestamp * 1e3).toISOString(),
|
|
2330
|
+
message: c.commit.message.trim().split("\n")[0] ?? ""
|
|
2331
|
+
}));
|
|
2332
|
+
} catch {
|
|
2333
|
+
return [];
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
async fileStats(dir, id) {
|
|
2337
|
+
try {
|
|
2338
|
+
const commit = await git.readCommit({ fs: fs4__default, dir, oid: id });
|
|
2339
|
+
const afterMap = await treeFileMap(dir, commit.commit.tree);
|
|
2340
|
+
let beforeMap = /* @__PURE__ */ new Map();
|
|
2341
|
+
const parents = commit.commit.parent ?? [];
|
|
2342
|
+
if (parents.length > 0) {
|
|
2343
|
+
const parent = await git.readCommit({ fs: fs4__default, dir, oid: parents[0] });
|
|
2344
|
+
beforeMap = await treeFileMap(dir, parent.commit.tree);
|
|
2345
|
+
}
|
|
2346
|
+
const paths = /* @__PURE__ */ new Set([...beforeMap.keys(), ...afterMap.keys()]);
|
|
2347
|
+
const out = [];
|
|
2348
|
+
for (const fp of [...paths].sort()) {
|
|
2349
|
+
const bOid = beforeMap.get(fp);
|
|
2350
|
+
const aOid = afterMap.get(fp);
|
|
2351
|
+
if (bOid === aOid) continue;
|
|
2352
|
+
const beforeTxt = bOid ? await readBlobText(dir, bOid) : null;
|
|
2353
|
+
const afterTxt = aOid ? await readBlobText(dir, aOid) : null;
|
|
2354
|
+
if (bOid && beforeTxt === null || aOid && afterTxt === null) {
|
|
2355
|
+
out.push({
|
|
2356
|
+
path: fp,
|
|
2357
|
+
add: aOid ? -1 : 0,
|
|
2358
|
+
del: bOid ? -1 : 0
|
|
2359
|
+
});
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
out.push({
|
|
2363
|
+
path: fp,
|
|
2364
|
+
...lineStats(bOid ? beforeTxt : null, aOid ? afterTxt : null)
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
return out;
|
|
2368
|
+
} catch {
|
|
2369
|
+
return [];
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async filePreview(dir, id, filepath, maxLines = 18) {
|
|
2373
|
+
try {
|
|
2374
|
+
const commit = await git.readCommit({ fs: fs4__default, dir, oid: id });
|
|
2375
|
+
const afterMap = await treeFileMap(dir, commit.commit.tree);
|
|
2376
|
+
let beforeMap = /* @__PURE__ */ new Map();
|
|
2377
|
+
const parents = commit.commit.parent ?? [];
|
|
2378
|
+
if (parents.length > 0) {
|
|
2379
|
+
const parent = await git.readCommit({ fs: fs4__default, dir, oid: parents[0] });
|
|
2380
|
+
beforeMap = await treeFileMap(dir, parent.commit.tree);
|
|
2381
|
+
}
|
|
2382
|
+
const bOid = beforeMap.get(filepath);
|
|
2383
|
+
const aOid = afterMap.get(filepath);
|
|
2384
|
+
if (!bOid && !aOid) {
|
|
2385
|
+
return "";
|
|
2386
|
+
}
|
|
2387
|
+
const before = bOid ? await readBlobText(dir, bOid) : null;
|
|
2388
|
+
const after = aOid ? await readBlobText(dir, aOid) : null;
|
|
2389
|
+
if (before === null && bOid) {
|
|
2390
|
+
return "";
|
|
2391
|
+
}
|
|
2392
|
+
if (after === null && aOid) {
|
|
2393
|
+
return "";
|
|
2394
|
+
}
|
|
2395
|
+
const diff = unifiedDiff(bOid ? before : null, aOid ? after : null);
|
|
2396
|
+
const changeLines = diff.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-"));
|
|
2397
|
+
const lines = (changeLines.length ? changeLines : diff.split("\n")).filter((l) => l.length > 0);
|
|
2398
|
+
if (lines.length <= maxLines) {
|
|
2399
|
+
return lines.join("\n");
|
|
2400
|
+
}
|
|
2401
|
+
return `${lines.slice(0, maxLines).join("\n")}
|
|
2402
|
+
\u2026 (+${lines.length - maxLines})`;
|
|
2403
|
+
} catch {
|
|
2404
|
+
return "";
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// src/theme-resource.ts
|
|
2410
|
+
var EMPTY_THEME_RESOURCE = {
|
|
2411
|
+
runnerCss: () => ""
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
// src/http/app-runner-html.ts
|
|
2415
|
+
function appRunnerHtml(appId, themeCss = "") {
|
|
2416
|
+
const safe = JSON.stringify(appId);
|
|
2417
|
+
const title = appId.replace(/[&<>"']/g, (ch) => {
|
|
2418
|
+
switch (ch) {
|
|
2419
|
+
case "&":
|
|
2420
|
+
return "&";
|
|
2421
|
+
case "<":
|
|
2422
|
+
return "<";
|
|
2423
|
+
case ">":
|
|
2424
|
+
return ">";
|
|
2425
|
+
case '"':
|
|
2426
|
+
return """;
|
|
2427
|
+
default:
|
|
2428
|
+
return "'";
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
const themeBlock = themeCss.trim() ? `${themeCss.trim()}
|
|
2432
|
+
` : "";
|
|
2433
|
+
return `<!doctype html>
|
|
2434
|
+
<html>
|
|
2435
|
+
<head>
|
|
2436
|
+
<meta charset="utf-8"/>
|
|
2437
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
2438
|
+
<title>${title}</title>
|
|
2439
|
+
<style>
|
|
2440
|
+
${themeBlock}html,body,#root{margin:0;height:100%;background:var(--background,#fff);color:var(--foreground,#111);font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);}
|
|
2441
|
+
.err{padding:24px;color:#b91c1c;white-space:pre-wrap;}
|
|
2442
|
+
#root.boot{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;}
|
|
2443
|
+
#root.boot .art{position:relative;width:88px;height:72px;color:var(--foreground,#111);}
|
|
2444
|
+
#root.boot .art svg{display:block;width:88px;height:64px;}
|
|
2445
|
+
#root.boot .dots{display:flex;gap:5px;justify-content:center;margin-top:2px;}
|
|
2446
|
+
#root.boot .dots i{width:6px;height:6px;border-radius:50%;background:var(--primary,#2563eb);opacity:.35;animation:mma-dot 1s ease-in-out infinite;}
|
|
2447
|
+
#root.boot .dots i:nth-child(2){animation-delay:.15s;}
|
|
2448
|
+
#root.boot .dots i:nth-child(3){animation-delay:.3s;}
|
|
2449
|
+
@keyframes mma-dot{0%,80%,100%{transform:translateY(0);opacity:.3}40%{transform:translateY(-5px);opacity:1}}
|
|
2450
|
+
</style>
|
|
2451
|
+
</head>
|
|
2452
|
+
<body>
|
|
2453
|
+
<div id="root" class="boot" role="status" aria-label="loading">
|
|
2454
|
+
<div class="art" aria-hidden="true">
|
|
2455
|
+
<svg viewBox="0 0 88 64" fill="none">
|
|
2456
|
+
<rect x="10" y="8" width="68" height="48" rx="10" stroke="currentColor" stroke-width="1.6" opacity=".35"/>
|
|
2457
|
+
<rect x="10" y="8" width="68" height="12" rx="10" fill="currentColor" opacity=".08"/>
|
|
2458
|
+
<circle cx="20" cy="14" r="2.2" fill="currentColor" opacity=".35"/>
|
|
2459
|
+
<rect x="26" y="12.2" width="18" height="3.6" rx="1.8" fill="currentColor" opacity=".22"/>
|
|
2460
|
+
<rect x="20" y="28" width="28" height="4" rx="2" fill="currentColor" opacity=".16"/>
|
|
2461
|
+
<rect x="20" y="36" width="40" height="4" rx="2" fill="currentColor" opacity=".1"/>
|
|
2462
|
+
<rect x="20" y="44" width="22" height="4" rx="2" fill="currentColor" opacity=".08"/>
|
|
2463
|
+
<path d="M62 40c6 0 10 5 10 10" stroke="var(--primary,#2563eb)" stroke-width="1.8" stroke-linecap="round" opacity=".85"/>
|
|
2464
|
+
<circle cx="72" cy="50" r="3.2" fill="var(--primary,#2563eb)" opacity=".9"/>
|
|
2465
|
+
</svg>
|
|
2466
|
+
<div class="dots"><i></i><i></i><i></i></div>
|
|
2467
|
+
</div>
|
|
2468
|
+
</div>
|
|
2469
|
+
<script type="module">
|
|
2470
|
+
const APP_ID = ${safe};
|
|
2471
|
+
(() => {
|
|
2472
|
+
const q = new URLSearchParams(location.search);
|
|
2473
|
+
const th = q.get("theme") || "light";
|
|
2474
|
+
const pal = q.get("palette") || "default";
|
|
2475
|
+
const dock = q.get("dock") || "fill";
|
|
2476
|
+
document.documentElement.setAttribute("data-theme", th);
|
|
2477
|
+
document.documentElement.classList.toggle("dark", th === "dark");
|
|
2478
|
+
document.documentElement.setAttribute("data-palette", pal);
|
|
2479
|
+
document.documentElement.setAttribute("data-dock", dock);
|
|
2480
|
+
window.addEventListener("message", (ev) => {
|
|
2481
|
+
const d = ev.data;
|
|
2482
|
+
if (!d || d.type !== "mma-set-env") return;
|
|
2483
|
+
if (d.theme) {
|
|
2484
|
+
document.documentElement.setAttribute("data-theme", d.theme);
|
|
2485
|
+
document.documentElement.classList.toggle("dark", d.theme === "dark");
|
|
2486
|
+
}
|
|
2487
|
+
if (d.palette) document.documentElement.setAttribute("data-palette", d.palette);
|
|
2488
|
+
if (d.dock) document.documentElement.setAttribute("data-dock", d.dock);
|
|
2489
|
+
});
|
|
2490
|
+
})();
|
|
2491
|
+
const cssLink = document.createElement("link");
|
|
2492
|
+
cssLink.rel = "stylesheet";
|
|
2493
|
+
cssLink.href = "/ui.css";
|
|
2494
|
+
document.head.appendChild(cssLink);
|
|
2495
|
+
try {
|
|
2496
|
+
await import("/api/app/" + encodeURIComponent(APP_ID) + "/ui/entry.js");
|
|
2497
|
+
} catch (e) {
|
|
2498
|
+
const rootEl = document.getElementById("root");
|
|
2499
|
+
if (rootEl) {
|
|
2500
|
+
rootEl.className = "err";
|
|
2501
|
+
rootEl.textContent = String((e && (e.stack || e.message)) || e);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
</script>
|
|
2505
|
+
</body>
|
|
2506
|
+
</html>`;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/http/http-gateway.ts
|
|
2510
|
+
function geistFontPath(name) {
|
|
2511
|
+
for (let dir = resolveUiDistDir(); dir !== path8__default.dirname(dir); dir = path8__default.dirname(dir)) {
|
|
2512
|
+
const fp = path8__default.join(dir, "..", "..", "node_modules", "@fontsource-variable", "geist", "files", name);
|
|
2513
|
+
const p = path8__default.resolve(fp);
|
|
2514
|
+
if (fs4__default.existsSync(p)) return p;
|
|
2515
|
+
}
|
|
2516
|
+
return path8__default.join(resolveUiDistDir(), "..", "..", "node_modules", "@fontsource-variable", "geist", "files", name);
|
|
2517
|
+
}
|
|
2518
|
+
function listenHttp(server, port) {
|
|
2519
|
+
return new Promise((resolve2, reject) => {
|
|
2520
|
+
const onError = (err) => {
|
|
2521
|
+
server.off("listening", onListening);
|
|
2522
|
+
reject(err);
|
|
2523
|
+
};
|
|
2524
|
+
const onListening = () => {
|
|
2525
|
+
server.off("error", onError);
|
|
2526
|
+
const addr = server.address();
|
|
2527
|
+
resolve2(typeof addr === "object" && addr ? addr.port : port);
|
|
2528
|
+
};
|
|
2529
|
+
server.once("error", onError);
|
|
2530
|
+
server.once("listening", onListening);
|
|
2531
|
+
server.listen(port, "127.0.0.1");
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
function closeHttp(server) {
|
|
2535
|
+
return new Promise((resolve2, reject) => {
|
|
2536
|
+
server.closeAllConnections?.();
|
|
2537
|
+
server.close((err) => {
|
|
2538
|
+
if (err) reject(err);
|
|
2539
|
+
else resolve2();
|
|
2540
|
+
});
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
function isRecord4(value) {
|
|
2544
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2545
|
+
}
|
|
2546
|
+
function publicHostConfig(config, hostPort) {
|
|
2547
|
+
return {
|
|
2548
|
+
theme: config.theme,
|
|
2549
|
+
palette: config.palette,
|
|
2550
|
+
locale: config.locale,
|
|
2551
|
+
chatLanguage: config.chatLanguage,
|
|
2552
|
+
hostPort,
|
|
2553
|
+
llm: config.llm
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
function errorMessage(cause) {
|
|
2557
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
2558
|
+
}
|
|
2559
|
+
function probePort(port) {
|
|
2560
|
+
return new Promise((resolve2, reject) => {
|
|
2561
|
+
const probe = createServer$1();
|
|
2562
|
+
const onError = (err) => {
|
|
2563
|
+
probe.off("listening", onListening);
|
|
2564
|
+
reject(err);
|
|
2565
|
+
};
|
|
2566
|
+
const onListening = () => {
|
|
2567
|
+
probe.off("error", onError);
|
|
2568
|
+
probe.close((err) => err ? reject(err) : resolve2());
|
|
2569
|
+
};
|
|
2570
|
+
probe.once("error", onError);
|
|
2571
|
+
probe.once("listening", onListening);
|
|
2572
|
+
probe.listen(port, "127.0.0.1");
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
function mergeHostConfigPatch(cur, body) {
|
|
2576
|
+
const theme = typeof body.theme === "string" && THEME_IDS.includes(body.theme) ? body.theme : cur.theme;
|
|
2577
|
+
const palette = typeof body.palette === "string" && body.palette.trim().length > 0 ? body.palette.trim() : cur.palette;
|
|
2578
|
+
const locale = typeof body.locale === "string" && LOCALE_IDS.includes(body.locale) ? body.locale : cur.locale;
|
|
2579
|
+
const chatLanguage = typeof body.chatLanguage === "string" && LOCALE_IDS.includes(body.chatLanguage) ? body.chatLanguage : cur.chatLanguage;
|
|
2580
|
+
let hostPort = cur.hostPort;
|
|
2581
|
+
if (body.hostPort !== void 0) {
|
|
2582
|
+
const n = Number(body.hostPort);
|
|
2583
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
2584
|
+
throw new HostError("INVALID_HOST_CONFIG", "hostPort must be an integer 0\u201365535");
|
|
2585
|
+
}
|
|
2586
|
+
hostPort = n;
|
|
2587
|
+
}
|
|
2588
|
+
let llm = cur.llm;
|
|
2589
|
+
if (body.llm === null) {
|
|
2590
|
+
llm = null;
|
|
2591
|
+
} else if (isRecord4(body.llm)) {
|
|
2592
|
+
const provider = body.llm.provider;
|
|
2593
|
+
const model = body.llm.model;
|
|
2594
|
+
if (typeof provider !== "string" || !provider.trim() || typeof model !== "string" || !model.trim()) {
|
|
2595
|
+
throw new HostError("INVALID_HOST_CONFIG", "llm requires non-empty provider and model");
|
|
2596
|
+
}
|
|
2597
|
+
llm = { provider: provider.trim(), model: model.trim() };
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
runtimeRoot: cur.runtimeRoot,
|
|
2601
|
+
hostPort,
|
|
2602
|
+
theme,
|
|
2603
|
+
palette,
|
|
2604
|
+
locale,
|
|
2605
|
+
chatLanguage,
|
|
2606
|
+
llm
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
var HttpGateway = class {
|
|
2610
|
+
constructor(apps, config, paths, compiler, git2, themes = EMPTY_THEME_RESOURCE, events, onHostPortChanged) {
|
|
2611
|
+
this.apps = apps;
|
|
2612
|
+
this.config = config;
|
|
2613
|
+
this.paths = paths;
|
|
2614
|
+
this.compiler = compiler;
|
|
2615
|
+
this.git = git2;
|
|
2616
|
+
this.themes = themes;
|
|
2617
|
+
this.events = events;
|
|
2618
|
+
this.onHostPortChanged = onHostPortChanged;
|
|
2619
|
+
this.app = this.buildApp();
|
|
2620
|
+
}
|
|
2621
|
+
apps;
|
|
2622
|
+
config;
|
|
2623
|
+
paths;
|
|
2624
|
+
compiler;
|
|
2625
|
+
git;
|
|
2626
|
+
themes;
|
|
2627
|
+
events;
|
|
2628
|
+
onHostPortChanged;
|
|
2629
|
+
app;
|
|
2630
|
+
server = null;
|
|
2631
|
+
boundPort = 0;
|
|
2632
|
+
get port() {
|
|
2633
|
+
return this.boundPort;
|
|
2634
|
+
}
|
|
2635
|
+
async listen(port) {
|
|
2636
|
+
if (this.server) {
|
|
2637
|
+
return this.boundPort;
|
|
2638
|
+
}
|
|
2639
|
+
const listener = getRequestListener((request, env) => this.app.fetch(request, env));
|
|
2640
|
+
const server = createServer(listener);
|
|
2641
|
+
try {
|
|
2642
|
+
this.boundPort = await listenHttp(server, port);
|
|
2643
|
+
this.server = server;
|
|
2644
|
+
} catch (cause) {
|
|
2645
|
+
server.close();
|
|
2646
|
+
throw new HostError("HOST_LISTEN_FAILED", `failed to listen on ${port}`, { cause });
|
|
2647
|
+
}
|
|
2648
|
+
return this.boundPort;
|
|
2649
|
+
}
|
|
2650
|
+
async close() {
|
|
2651
|
+
const server = this.server;
|
|
2652
|
+
this.server = null;
|
|
2653
|
+
this.boundPort = 0;
|
|
2654
|
+
if (server) {
|
|
2655
|
+
await closeHttp(server);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
buildApp() {
|
|
2659
|
+
const app = new Hono();
|
|
2660
|
+
app.use(
|
|
2661
|
+
"*",
|
|
2662
|
+
cors({
|
|
2663
|
+
origin: "*",
|
|
2664
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
2665
|
+
allowHeaders: ["content-type"]
|
|
2666
|
+
})
|
|
2667
|
+
);
|
|
2668
|
+
app.onError((err, c) => {
|
|
2669
|
+
if (err instanceof HostError) {
|
|
2670
|
+
return c.json({ ok: false, error: err.message, code: err.code }, 400);
|
|
2671
|
+
}
|
|
2672
|
+
return c.json({ error: errorMessage(err) }, 500);
|
|
2673
|
+
});
|
|
2674
|
+
app.notFound((c) => c.json({ error: "not_found" }, 404));
|
|
2675
|
+
app.get("/health", (c) => c.json({ ok: true, hostPort: this.boundPort }));
|
|
2676
|
+
app.get("/api/events", (c) => {
|
|
2677
|
+
const bus = this.events;
|
|
2678
|
+
if (!bus) {
|
|
2679
|
+
return c.text("events bus unavailable", 503);
|
|
2680
|
+
}
|
|
2681
|
+
const signal = c.req.raw.signal;
|
|
2682
|
+
const stream = new ReadableStream({
|
|
2683
|
+
start(controller) {
|
|
2684
|
+
const enc = new TextEncoder();
|
|
2685
|
+
controller.enqueue(enc.encode("retry: 3000\n\n"));
|
|
2686
|
+
const unsub = bus.subscribe((event) => {
|
|
2687
|
+
try {
|
|
2688
|
+
controller.enqueue(enc.encode(formatSse(event, bus.nextId())));
|
|
2689
|
+
} catch {
|
|
2690
|
+
unsub();
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
const onAbort = () => {
|
|
2694
|
+
unsub();
|
|
2695
|
+
try {
|
|
2696
|
+
controller.close();
|
|
2697
|
+
} catch {
|
|
2698
|
+
}
|
|
2699
|
+
};
|
|
2700
|
+
if (signal.aborted) {
|
|
2701
|
+
onAbort();
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
return new Response(stream, {
|
|
2708
|
+
headers: {
|
|
2709
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
2710
|
+
"Cache-Control": "no-cache",
|
|
2711
|
+
Connection: "keep-alive"
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
});
|
|
2715
|
+
app.get(
|
|
2716
|
+
"/api/host-config",
|
|
2717
|
+
(c) => c.json({ ok: true, ...publicHostConfig(this.config, this.boundPort) })
|
|
2718
|
+
);
|
|
2719
|
+
app.post("/api/host-config", async (c) => {
|
|
2720
|
+
let body;
|
|
2721
|
+
try {
|
|
2722
|
+
body = await c.req.json();
|
|
2723
|
+
} catch {
|
|
2724
|
+
return c.json({ ok: false, error: "invalid json" }, 400);
|
|
2725
|
+
}
|
|
2726
|
+
if (!isRecord4(body)) {
|
|
2727
|
+
return c.json({ ok: false, error: "invalid json" }, 400);
|
|
2728
|
+
}
|
|
2729
|
+
let next;
|
|
2730
|
+
try {
|
|
2731
|
+
next = mergeHostConfigPatch(this.config, body);
|
|
2732
|
+
} catch (cause) {
|
|
2733
|
+
if (cause instanceof HostError) {
|
|
2734
|
+
return c.json({ ok: false, error: cause.message, code: cause.code }, 400);
|
|
2735
|
+
}
|
|
2736
|
+
throw cause;
|
|
2737
|
+
}
|
|
2738
|
+
const portChanged = next.hostPort !== this.boundPort && next.hostPort !== 0;
|
|
2739
|
+
if (portChanged) {
|
|
2740
|
+
try {
|
|
2741
|
+
await probePort(next.hostPort);
|
|
2742
|
+
} catch (cause) {
|
|
2743
|
+
return c.json(
|
|
2744
|
+
{
|
|
2745
|
+
ok: false,
|
|
2746
|
+
error: `port in use or bind failed: ${errorMessage(cause)}`,
|
|
2747
|
+
hostPort: this.boundPort,
|
|
2748
|
+
...publicHostConfig(this.config, this.boundPort)
|
|
2749
|
+
},
|
|
2750
|
+
409
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
Object.assign(this.config, next);
|
|
2755
|
+
writeHostConfig(this.paths, this.config);
|
|
2756
|
+
const responsePort = portChanged ? next.hostPort : this.boundPort;
|
|
2757
|
+
if (portChanged) {
|
|
2758
|
+
const target = next.hostPort;
|
|
2759
|
+
setImmediate(() => {
|
|
2760
|
+
void (async () => {
|
|
2761
|
+
try {
|
|
2762
|
+
await this.close();
|
|
2763
|
+
await this.listen(target);
|
|
2764
|
+
this.onHostPortChanged?.(this.boundPort);
|
|
2765
|
+
} catch (cause) {
|
|
2766
|
+
console.warn("[monkey-mini-app] rebound failed", cause);
|
|
2767
|
+
}
|
|
2768
|
+
})();
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
return c.json({ ok: true, ...publicHostConfig(this.config, responsePort) });
|
|
2772
|
+
});
|
|
2773
|
+
app.get("/api/apps", async (c) => {
|
|
2774
|
+
const apps = await this.apps.list();
|
|
2775
|
+
return c.json({ apps });
|
|
2776
|
+
});
|
|
2777
|
+
app.get("/api/palettes", async (c) => {
|
|
2778
|
+
const palettes = await this.themes.listCustomPalettes?.() ?? [];
|
|
2779
|
+
return c.json({ palettes });
|
|
2780
|
+
});
|
|
2781
|
+
app.post("/api/call", async (c) => {
|
|
2782
|
+
let body;
|
|
2783
|
+
try {
|
|
2784
|
+
body = await c.req.json();
|
|
2785
|
+
} catch {
|
|
2786
|
+
return c.json({ ok: false, error: "invalid json" }, 400);
|
|
2787
|
+
}
|
|
2788
|
+
if (!isRecord4(body)) {
|
|
2789
|
+
return c.json({ ok: false, error: "invalid json" }, 400);
|
|
2790
|
+
}
|
|
2791
|
+
const appId = typeof body.appId === "string" ? body.appId : "";
|
|
2792
|
+
const method = typeof body.method === "string" ? body.method : "";
|
|
2793
|
+
if (!appId || !method) {
|
|
2794
|
+
return c.json({ ok: false, error: "missing appId or method" }, 400);
|
|
2795
|
+
}
|
|
2796
|
+
try {
|
|
2797
|
+
const value = await this.apps.call(appId, method, body.args, c.req.raw.signal);
|
|
2798
|
+
return c.json({ ok: true, value });
|
|
2799
|
+
} catch (cause) {
|
|
2800
|
+
return c.json({ ok: false, error: errorMessage(cause) }, 400);
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
app.get("/api/apps/:appId/history", async (c) => {
|
|
2804
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2805
|
+
const dir = this.apps.dirOf(appId);
|
|
2806
|
+
const limitRaw = Number(c.req.query("limit"));
|
|
2807
|
+
const limit = Math.min(Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 50, 200);
|
|
2808
|
+
const list = await this.git.log(dir, limit);
|
|
2809
|
+
const commits = await Promise.all(
|
|
2810
|
+
list.map(async (entry) => ({
|
|
2811
|
+
...entry,
|
|
2812
|
+
files: await this.git.fileStats(dir, entry.id)
|
|
2813
|
+
}))
|
|
2814
|
+
);
|
|
2815
|
+
return c.json({ ok: true, commits });
|
|
2816
|
+
});
|
|
2817
|
+
app.get("/api/apps/:appId/history/:commitId", async (c) => {
|
|
2818
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2819
|
+
const commitId = c.req.param("commitId");
|
|
2820
|
+
const dir = this.apps.dirOf(appId);
|
|
2821
|
+
const stats = await this.git.fileStats(dir, commitId);
|
|
2822
|
+
const log = await this.git.log(dir, 200);
|
|
2823
|
+
const meta = log.find((entry) => entry.id === commitId || entry.id.startsWith(commitId));
|
|
2824
|
+
const files = await Promise.all(
|
|
2825
|
+
stats.map(async (s) => ({
|
|
2826
|
+
...s,
|
|
2827
|
+
preview: await this.git.filePreview(dir, commitId, s.path)
|
|
2828
|
+
}))
|
|
2829
|
+
);
|
|
2830
|
+
return c.json({
|
|
2831
|
+
ok: true,
|
|
2832
|
+
commit: {
|
|
2833
|
+
id: meta?.id || commitId,
|
|
2834
|
+
time: meta?.time || "",
|
|
2835
|
+
message: meta?.message || "",
|
|
2836
|
+
files
|
|
2837
|
+
}
|
|
2838
|
+
});
|
|
2839
|
+
});
|
|
2840
|
+
app.get("/api/apps/:appId/storage", (c) => {
|
|
2841
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2842
|
+
const dir = path8__default.join(this.apps.dirOf(appId), WorkspacePaths.Rel.storage);
|
|
2843
|
+
return c.json({ ok: true, tables: listStorageTables(dir) });
|
|
2844
|
+
});
|
|
2845
|
+
app.get("/api/apps/:appId/storage/:table", (c) => {
|
|
2846
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2847
|
+
const table = c.req.param("table");
|
|
2848
|
+
const dir = path8__default.join(this.apps.dirOf(appId), WorkspacePaths.Rel.storage);
|
|
2849
|
+
const fp = storageTablePath(dir, table);
|
|
2850
|
+
return c.json({ ok: true, table, value: readJsonFile(fp, null) });
|
|
2851
|
+
});
|
|
2852
|
+
app.get("/api/apps/:appId/theme", (c) => {
|
|
2853
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2854
|
+
const dir = this.apps.dirOf(appId);
|
|
2855
|
+
return c.json({ ok: true, appId, theme: readAppTheme(dir) });
|
|
2856
|
+
});
|
|
2857
|
+
app.post("/api/apps/:appId/theme", async (c) => {
|
|
2858
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2859
|
+
const dir = this.apps.dirOf(appId);
|
|
2860
|
+
let body = {};
|
|
2861
|
+
try {
|
|
2862
|
+
body = await c.req.json();
|
|
2863
|
+
} catch {
|
|
2864
|
+
}
|
|
2865
|
+
const rec = isRecord4(body) ? body : {};
|
|
2866
|
+
const saved = rec.reset ? writeAppTheme(dir, null) : writeAppTheme(dir, {
|
|
2867
|
+
theme: String(rec.theme || "light"),
|
|
2868
|
+
palette: String(rec.palette || "default")
|
|
2869
|
+
});
|
|
2870
|
+
return c.json({ ok: true, appId, theme: saved });
|
|
2871
|
+
});
|
|
2872
|
+
app.delete("/api/app/:appId", async (c) => {
|
|
2873
|
+
const appId = asAppId(c.req.param("appId"));
|
|
2874
|
+
await this.apps.remove(appId);
|
|
2875
|
+
return c.json({ ok: true });
|
|
2876
|
+
});
|
|
2877
|
+
app.get("/", async (c) => {
|
|
2878
|
+
let items = [];
|
|
2879
|
+
try {
|
|
2880
|
+
items = await this.apps.list();
|
|
2881
|
+
} catch {
|
|
2882
|
+
items = [];
|
|
2883
|
+
}
|
|
2884
|
+
const esc = (s) => s.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
2885
|
+
const cards = items.map((a) => `<a class="card" href="/app/${encodeURIComponent(a.id)}">
|
|
2886
|
+
<div class="mono">${esc(a.acronym || a.name.slice(0, 2).toUpperCase())}</div>
|
|
2887
|
+
<div>
|
|
2888
|
+
<div class="name">${esc(a.name)}</div>
|
|
2889
|
+
<div class="desc">${esc(a.description || a.id)}</div>
|
|
2890
|
+
<div class="meta">${esc(a.id)} \xB7 ${a.commits} commits</div>
|
|
2891
|
+
</div>
|
|
2892
|
+
</a>`).join("\n");
|
|
2893
|
+
const html = `<!doctype html>
|
|
2894
|
+
<html><head>
|
|
2895
|
+
<meta charset="utf-8"/>
|
|
2896
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
2897
|
+
<title>${esc(this.config.theme ?? "monkey-mini-app")} \xB7 \u5C0F\u7A0B\u5E8F</title>
|
|
2898
|
+
<link rel="stylesheet" href="/ui.css"/>
|
|
2899
|
+
<style>
|
|
2900
|
+
html,body{margin:0;height:100%;background:var(--background,#fff);color:var(--foreground,#111);font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);}
|
|
2901
|
+
body{padding:40px;}
|
|
2902
|
+
h1{font-size:1.5rem;margin:0 0 4px;}
|
|
2903
|
+
.sub{color:var(--muted-foreground,#666);margin:0 0 24px;font-size:.9rem;}
|
|
2904
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;max-width:1100px;}
|
|
2905
|
+
.card{display:flex;gap:14px;padding:16px;border:1px solid var(--border,#e2e2e2);border-radius:14px;text-decoration:none;color:inherit;background:var(--card,#fff);transition:border-color .15s, transform .15s;}
|
|
2906
|
+
.card:hover{border-color:var(--primary,#2563eb);transform:translateY(-2px);}
|
|
2907
|
+
.mono{width:48px;height:48px;border-radius:12px;background:var(--muted,#f2f2f2);display:flex;align-items:center;justify-content:center;font-weight:600;color:var(--primary,#2563eb);flex-shrink:0;}
|
|
2908
|
+
.name{font-weight:600;font-size:1rem;}
|
|
2909
|
+
.desc{color:var(--muted-foreground,#666);font-size:.85rem;margin-top:2px;}
|
|
2910
|
+
.meta{color:var(--muted-foreground,#999);font-size:.72rem;margin-top:8px;font-family:monospace;}
|
|
2911
|
+
</style>
|
|
2912
|
+
</head><body>
|
|
2913
|
+
<h1>\u5C0F\u7A0B\u5E8F</h1>
|
|
2914
|
+
<p class="sub">\u5171 ${items.length} \u4E2A \xB7 \u70B9\u5F00\u8FDB\u5165</p>
|
|
2915
|
+
<div class="grid">${cards}</div>
|
|
2916
|
+
</body></html>`;
|
|
2917
|
+
return c.html(html);
|
|
2918
|
+
});
|
|
2919
|
+
app.get("/ui.css", (c) => {
|
|
2920
|
+
try {
|
|
2921
|
+
const css = fs4__default.readFileSync(path8__default.join(resolveUiDistDir(), "globals.css"), "utf8");
|
|
2922
|
+
return c.body(css, 200, { "Content-Type": "text/css; charset=utf-8" });
|
|
2923
|
+
} catch (cause) {
|
|
2924
|
+
return c.text(`ui.css missing: ${errorMessage(cause)}`, 500);
|
|
2925
|
+
}
|
|
2926
|
+
});
|
|
2927
|
+
app.get("/files/:name", (c) => {
|
|
2928
|
+
const name = path8__default.basename(c.req.param("name") || "");
|
|
2929
|
+
try {
|
|
2930
|
+
const p = geistFontPath(name);
|
|
2931
|
+
const buf = fs4__default.readFileSync(p);
|
|
2932
|
+
const ext = path8__default.extname(name).toLowerCase();
|
|
2933
|
+
return c.body(buf, 200, { "Content-Type": ext === ".woff2" ? "font/woff2" : "application/octet-stream" });
|
|
2934
|
+
} catch (cause) {
|
|
2935
|
+
return c.text(`font missing: ${errorMessage(cause)}`, 404);
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
app.get("/api/app/:appId/ui/:name", async (c) => {
|
|
2939
|
+
const appId = c.req.param("appId");
|
|
2940
|
+
const name = path8__default.basename(c.req.param("name") || "");
|
|
2941
|
+
if (!name.endsWith(".js")) {
|
|
2942
|
+
return c.json({ error: `bundle file missing: ${name}` }, 404);
|
|
2943
|
+
}
|
|
2944
|
+
let files;
|
|
2945
|
+
try {
|
|
2946
|
+
files = await this.compiler.compile(this.apps.dirOf(appId), {
|
|
2947
|
+
locale: this.config.locale
|
|
2948
|
+
});
|
|
2949
|
+
} catch (cause) {
|
|
2950
|
+
const status = cause instanceof HostError ? 400 : 500;
|
|
2951
|
+
return c.json({ error: errorMessage(cause) }, status);
|
|
2952
|
+
}
|
|
2953
|
+
const file = files.find((f) => f.name === name);
|
|
2954
|
+
if (!file) {
|
|
2955
|
+
return c.json({ error: `bundle file missing: ${name}` }, 404);
|
|
2956
|
+
}
|
|
2957
|
+
return c.body(Buffer.from(file.contents).toString("utf8"), 200, {
|
|
2958
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
2959
|
+
"Cache-Control": "no-cache"
|
|
2960
|
+
});
|
|
2961
|
+
});
|
|
2962
|
+
app.get("/app/:appId", (c) => {
|
|
2963
|
+
const html = appRunnerHtml(c.req.param("appId"), this.themes.runnerCss());
|
|
2964
|
+
return c.html(html);
|
|
2965
|
+
});
|
|
2966
|
+
return app;
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
|
|
2970
|
+
// src/tools/tool-facade.ts
|
|
2971
|
+
function isRecord5(value) {
|
|
2972
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2973
|
+
}
|
|
2974
|
+
function requireString2(args, key) {
|
|
2975
|
+
const value = args[key];
|
|
2976
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2977
|
+
throw new HostError("INVALID_TOOL_ARGS", `missing ${key}`);
|
|
2978
|
+
}
|
|
2979
|
+
return value;
|
|
2980
|
+
}
|
|
2981
|
+
function asFiles(value) {
|
|
2982
|
+
if (!isRecord5(value)) {
|
|
2983
|
+
throw new HostError("INVALID_TOOL_ARGS", "files must be an object of strings");
|
|
2984
|
+
}
|
|
2985
|
+
const out = {};
|
|
2986
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2987
|
+
if (typeof v !== "string") {
|
|
2988
|
+
throw new HostError("INVALID_TOOL_ARGS", `files.${k} must be a string`);
|
|
2989
|
+
}
|
|
2990
|
+
out[k] = v;
|
|
2991
|
+
}
|
|
2992
|
+
return out;
|
|
2993
|
+
}
|
|
2994
|
+
function asEdits(value) {
|
|
2995
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2996
|
+
throw new HostError("INVALID_TOOL_ARGS", "edits must be a non-empty array");
|
|
2997
|
+
}
|
|
2998
|
+
return value.map((item, i) => {
|
|
2999
|
+
if (!isRecord5(item)) {
|
|
3000
|
+
throw new HostError("INVALID_TOOL_ARGS", `edits[${i}] must be an object`);
|
|
3001
|
+
}
|
|
3002
|
+
if (typeof item.oldText !== "string" || typeof item.newText !== "string") {
|
|
3003
|
+
throw new HostError("INVALID_TOOL_ARGS", `edits[${i}] requires oldText and newText strings`);
|
|
3004
|
+
}
|
|
3005
|
+
return { oldText: item.oldText, newText: item.newText };
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
function optionalPositiveInt(value, key) {
|
|
3009
|
+
if (value === void 0 || value === null) return void 0;
|
|
3010
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
3011
|
+
throw new HostError("INVALID_TOOL_ARGS", `${key} must be an integer`);
|
|
3012
|
+
}
|
|
3013
|
+
return value;
|
|
3014
|
+
}
|
|
3015
|
+
function parseReadRange(args) {
|
|
3016
|
+
const startLine = optionalPositiveInt(args.startLine, "startLine");
|
|
3017
|
+
const endLine = optionalPositiveInt(args.endLine, "endLine");
|
|
3018
|
+
const offset = optionalPositiveInt(args.offset, "offset");
|
|
3019
|
+
const limit = optionalPositiveInt(args.limit, "limit");
|
|
3020
|
+
const numbered = args.numbered === true ? true : void 0;
|
|
3021
|
+
const start = startLine ?? offset;
|
|
3022
|
+
let end = endLine;
|
|
3023
|
+
if (end === void 0 && start !== void 0 && limit !== void 0) {
|
|
3024
|
+
if (limit < 1) {
|
|
3025
|
+
throw new HostError("INVALID_TOOL_ARGS", "limit must be >= 1");
|
|
3026
|
+
}
|
|
3027
|
+
end = start + limit - 1;
|
|
3028
|
+
} else if (end === void 0 && start === void 0 && limit !== void 0) {
|
|
3029
|
+
if (limit < 1) {
|
|
3030
|
+
throw new HostError("INVALID_TOOL_ARGS", "limit must be >= 1");
|
|
3031
|
+
}
|
|
3032
|
+
return { startLine: 1, endLine: limit, numbered };
|
|
3033
|
+
}
|
|
3034
|
+
if (start === void 0 && end === void 0 && numbered === void 0) {
|
|
3035
|
+
return {};
|
|
3036
|
+
}
|
|
3037
|
+
return { startLine: start, endLine: end, numbered };
|
|
3038
|
+
}
|
|
3039
|
+
var APP_ID_SCHEMA = { type: "string" };
|
|
3040
|
+
function isMiniAppToolName(name) {
|
|
3041
|
+
return name.startsWith("mini_app_");
|
|
3042
|
+
}
|
|
3043
|
+
var ToolFacade = class {
|
|
3044
|
+
constructor(apps, git2, paths, events) {
|
|
3045
|
+
this.apps = apps;
|
|
3046
|
+
this.git = git2;
|
|
3047
|
+
this.paths = paths;
|
|
3048
|
+
this.events = events;
|
|
3049
|
+
}
|
|
3050
|
+
apps;
|
|
3051
|
+
git;
|
|
3052
|
+
paths;
|
|
3053
|
+
events;
|
|
3054
|
+
definitions() {
|
|
3055
|
+
return [
|
|
3056
|
+
{
|
|
3057
|
+
name: "mini_app_list",
|
|
3058
|
+
description: "List registered monkey-mini-app applications",
|
|
3059
|
+
inputSchema: { type: "object", properties: {} },
|
|
3060
|
+
execute: (args, signal) => this.invoke("mini_app_list", args, signal)
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
name: "mini_app_get",
|
|
3064
|
+
description: "Get app manifest summary and absolute directory path",
|
|
3065
|
+
inputSchema: {
|
|
3066
|
+
type: "object",
|
|
3067
|
+
properties: { appId: APP_ID_SCHEMA },
|
|
3068
|
+
required: ["appId"]
|
|
3069
|
+
},
|
|
3070
|
+
execute: (args, signal) => this.invoke("mini_app_get", args, signal)
|
|
3071
|
+
},
|
|
3072
|
+
{
|
|
3073
|
+
name: "mini_app_reload",
|
|
3074
|
+
description: "Validate + sync-compile main.api and ui for an app (replaces mini_app_validate). Returns compile errors if any. On success, auto-commits if the worktree is dirty. Call after a round of edits to verify and warm the UI cache.",
|
|
3075
|
+
inputSchema: {
|
|
3076
|
+
type: "object",
|
|
3077
|
+
properties: { appId: APP_ID_SCHEMA },
|
|
3078
|
+
required: ["appId"]
|
|
3079
|
+
},
|
|
3080
|
+
execute: (args, signal) => this.invoke("mini_app_reload", args, signal)
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
name: "mini_app_register",
|
|
3084
|
+
description: "Create a mini-app scaffold under runtime/apps/<appId>/. Prefer this for NEW apps (requires manifest.json). For edits to an existing app, use mini_app_read + mini_app_edit (or mini_app_write). files keys are relative paths (manifest.json, ui.tsx, main.api.ts, lib/..., components/...). No .. or absolute paths.",
|
|
3085
|
+
inputSchema: {
|
|
3086
|
+
type: "object",
|
|
3087
|
+
properties: {
|
|
3088
|
+
appId: APP_ID_SCHEMA,
|
|
3089
|
+
files: {
|
|
3090
|
+
type: "object",
|
|
3091
|
+
// dsh-tools requires boolean additionalProperties (not a nested schema).
|
|
3092
|
+
additionalProperties: true,
|
|
3093
|
+
description: "Relative path \u2192 UTF-8 source text (string values). Example keys: manifest.json, ui.tsx, main.api.ts, lib/parse.ts, components/Card.tsx"
|
|
3094
|
+
}
|
|
3095
|
+
},
|
|
3096
|
+
required: ["appId", "files"]
|
|
3097
|
+
},
|
|
3098
|
+
execute: (args, signal) => this.invoke("mini_app_register", args, signal)
|
|
3099
|
+
},
|
|
3100
|
+
{
|
|
3101
|
+
name: "mini_app_list_files",
|
|
3102
|
+
description: "List source files in a mini-app (relative paths + sizes). Skips .git/storage/node_modules.",
|
|
3103
|
+
inputSchema: {
|
|
3104
|
+
type: "object",
|
|
3105
|
+
properties: { appId: APP_ID_SCHEMA },
|
|
3106
|
+
required: ["appId"]
|
|
3107
|
+
},
|
|
3108
|
+
execute: (args, signal) => this.invoke("mini_app_list_files", args, signal)
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
name: "mini_app_read",
|
|
3112
|
+
description: "Read one mini-app source file. Optional 1-indexed inclusive line window: startLine/endLine (omit both = whole file; only startLine = to EOF; only endLine = from line 1). Aliases: offset\u2261startLine, limit\u2261line count. Returns { path, content, bytes, totalLines, startLine, endLine, truncated? }. Set numbered:true to prefix lines as N|text (default raw \u2014 better for mini_app_edit).",
|
|
3113
|
+
inputSchema: {
|
|
3114
|
+
type: "object",
|
|
3115
|
+
properties: {
|
|
3116
|
+
appId: APP_ID_SCHEMA,
|
|
3117
|
+
path: { type: "string", description: "Relative path e.g. ui.tsx, main.api.ts, lib/x.ts" },
|
|
3118
|
+
startLine: {
|
|
3119
|
+
type: "number",
|
|
3120
|
+
description: "1-indexed start line (inclusive). Omit with endLine for full file."
|
|
3121
|
+
},
|
|
3122
|
+
endLine: {
|
|
3123
|
+
type: "number",
|
|
3124
|
+
description: "1-indexed end line (inclusive). Omit to read through EOF."
|
|
3125
|
+
},
|
|
3126
|
+
offset: {
|
|
3127
|
+
type: "number",
|
|
3128
|
+
description: "Alias of startLine (Pi/Claude style)."
|
|
3129
|
+
},
|
|
3130
|
+
limit: {
|
|
3131
|
+
type: "number",
|
|
3132
|
+
description: "Max lines to read from startLine/offset (alternative to endLine)."
|
|
3133
|
+
},
|
|
3134
|
+
numbered: {
|
|
3135
|
+
type: "boolean",
|
|
3136
|
+
description: "If true, content lines are prefixed with N| (absolute line numbers)."
|
|
3137
|
+
}
|
|
3138
|
+
},
|
|
3139
|
+
required: ["appId", "path"]
|
|
3140
|
+
},
|
|
3141
|
+
execute: (args, signal) => this.invoke("mini_app_read", args, signal)
|
|
3142
|
+
},
|
|
3143
|
+
{
|
|
3144
|
+
name: "mini_app_edit",
|
|
3145
|
+
description: "Surgically edit an existing mini-app file with exact text replacement (Pi-style). Pass edits: [{ oldText, newText }, ...]. Each oldText must match uniquely. Prefer this over mini_app_write for small changes. Default auto-commits; set commit:false to batch then mini_app_reload.",
|
|
3146
|
+
inputSchema: {
|
|
3147
|
+
type: "object",
|
|
3148
|
+
properties: {
|
|
3149
|
+
appId: APP_ID_SCHEMA,
|
|
3150
|
+
path: { type: "string" },
|
|
3151
|
+
edits: {
|
|
3152
|
+
type: "array",
|
|
3153
|
+
items: {
|
|
3154
|
+
type: "object",
|
|
3155
|
+
properties: {
|
|
3156
|
+
oldText: { type: "string" },
|
|
3157
|
+
newText: { type: "string" }
|
|
3158
|
+
},
|
|
3159
|
+
required: ["oldText", "newText"]
|
|
3160
|
+
},
|
|
3161
|
+
description: "One or more unique replacements matched against the original file"
|
|
3162
|
+
},
|
|
3163
|
+
commit: {
|
|
3164
|
+
type: "boolean",
|
|
3165
|
+
description: "Auto-commit after edit (default true). Set false to batch changes."
|
|
3166
|
+
}
|
|
3167
|
+
},
|
|
3168
|
+
required: ["appId", "path", "edits"]
|
|
3169
|
+
},
|
|
3170
|
+
execute: (args, signal) => this.invoke("mini_app_edit", args, signal)
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
name: "mini_app_write",
|
|
3174
|
+
description: "Create or overwrite one mini-app file with full contents. Use for new files or large rewrites; prefer mini_app_edit for small surgical changes. Default auto-commits; set commit:false to batch.",
|
|
3175
|
+
inputSchema: {
|
|
3176
|
+
type: "object",
|
|
3177
|
+
properties: {
|
|
3178
|
+
appId: APP_ID_SCHEMA,
|
|
3179
|
+
path: { type: "string" },
|
|
3180
|
+
content: { type: "string" },
|
|
3181
|
+
commit: { type: "boolean" }
|
|
3182
|
+
},
|
|
3183
|
+
required: ["appId", "path", "content"]
|
|
3184
|
+
},
|
|
3185
|
+
execute: (args, signal) => this.invoke("mini_app_write", args, signal)
|
|
3186
|
+
},
|
|
3187
|
+
{
|
|
3188
|
+
name: "mini_app_delete",
|
|
3189
|
+
description: "Delete one mini-app source file (cannot delete manifest.json). Default auto-commits; set commit:false to batch.",
|
|
3190
|
+
inputSchema: {
|
|
3191
|
+
type: "object",
|
|
3192
|
+
properties: {
|
|
3193
|
+
appId: APP_ID_SCHEMA,
|
|
3194
|
+
path: { type: "string" },
|
|
3195
|
+
commit: { type: "boolean" }
|
|
3196
|
+
},
|
|
3197
|
+
required: ["appId", "path"]
|
|
3198
|
+
},
|
|
3199
|
+
execute: (args, signal) => this.invoke("mini_app_delete", args, signal)
|
|
3200
|
+
},
|
|
3201
|
+
{
|
|
3202
|
+
name: "mini_app_open",
|
|
3203
|
+
description: "Open the mini-app in the dsh \u5C0F\u7A0B\u5E8F side panel. The web Host will pop open and focus this app.",
|
|
3204
|
+
inputSchema: {
|
|
3205
|
+
type: "object",
|
|
3206
|
+
properties: {
|
|
3207
|
+
appId: APP_ID_SCHEMA,
|
|
3208
|
+
title: { type: "string" }
|
|
3209
|
+
},
|
|
3210
|
+
required: ["appId"]
|
|
3211
|
+
},
|
|
3212
|
+
execute: (args, signal) => this.invoke("mini_app_open", args, signal)
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
name: "mini_app_call",
|
|
3216
|
+
description: "Call a mini-app api method. args is a plain object. Do not curl the host HTTP API.",
|
|
3217
|
+
inputSchema: {
|
|
3218
|
+
type: "object",
|
|
3219
|
+
properties: {
|
|
3220
|
+
appId: APP_ID_SCHEMA,
|
|
3221
|
+
method: { type: "string" },
|
|
3222
|
+
args: { type: "object" }
|
|
3223
|
+
},
|
|
3224
|
+
required: ["appId", "method"]
|
|
3225
|
+
},
|
|
3226
|
+
execute: (args, signal) => this.invoke("mini_app_call", args, signal)
|
|
3227
|
+
},
|
|
3228
|
+
{
|
|
3229
|
+
name: "mini_app_history_commit",
|
|
3230
|
+
description: "Commit current app working tree (single-branch main)",
|
|
3231
|
+
inputSchema: {
|
|
3232
|
+
type: "object",
|
|
3233
|
+
properties: {
|
|
3234
|
+
appId: APP_ID_SCHEMA,
|
|
3235
|
+
message: { type: "string" }
|
|
3236
|
+
},
|
|
3237
|
+
required: ["appId", "message"]
|
|
3238
|
+
},
|
|
3239
|
+
execute: (args, signal) => this.invoke("mini_app_history_commit", args, signal)
|
|
3240
|
+
},
|
|
3241
|
+
{
|
|
3242
|
+
name: "mini_app_history_list",
|
|
3243
|
+
description: "List commit tree (nodes + parentIds, includes backup tips after reset)",
|
|
3244
|
+
inputSchema: {
|
|
3245
|
+
type: "object",
|
|
3246
|
+
properties: {
|
|
3247
|
+
appId: APP_ID_SCHEMA,
|
|
3248
|
+
limit: { type: "number" }
|
|
3249
|
+
},
|
|
3250
|
+
required: ["appId"]
|
|
3251
|
+
},
|
|
3252
|
+
execute: (args, signal) => this.invoke("mini_app_history_list", args, signal)
|
|
3253
|
+
},
|
|
3254
|
+
{
|
|
3255
|
+
name: "mini_app_history_reset",
|
|
3256
|
+
description: "Reset main to commitId; creates backup ref; does not delete commits",
|
|
3257
|
+
inputSchema: {
|
|
3258
|
+
type: "object",
|
|
3259
|
+
properties: {
|
|
3260
|
+
appId: APP_ID_SCHEMA,
|
|
3261
|
+
commitId: { type: "string" }
|
|
3262
|
+
},
|
|
3263
|
+
required: ["appId", "commitId"]
|
|
3264
|
+
},
|
|
3265
|
+
execute: (args, signal) => this.invoke("mini_app_history_reset", args, signal)
|
|
3266
|
+
},
|
|
3267
|
+
{
|
|
3268
|
+
name: "mini_app_history_revert",
|
|
3269
|
+
description: "Forward-commit that undoes a past commit (git revert semantics)",
|
|
3270
|
+
inputSchema: {
|
|
3271
|
+
type: "object",
|
|
3272
|
+
properties: {
|
|
3273
|
+
appId: APP_ID_SCHEMA,
|
|
3274
|
+
commitId: { type: "string" }
|
|
3275
|
+
},
|
|
3276
|
+
required: ["appId", "commitId"]
|
|
3277
|
+
},
|
|
3278
|
+
execute: (args, signal) => this.invoke("mini_app_history_revert", args, signal)
|
|
3279
|
+
}
|
|
3280
|
+
];
|
|
3281
|
+
}
|
|
3282
|
+
async invoke(name, args = {}, signal) {
|
|
3283
|
+
switch (name) {
|
|
3284
|
+
case "mini_app_list":
|
|
3285
|
+
return { apps: await this.apps.list(), runtimeRoot: this.paths.root };
|
|
3286
|
+
case "mini_app_get":
|
|
3287
|
+
return this.handleGet(args);
|
|
3288
|
+
case "mini_app_reload":
|
|
3289
|
+
return this.handleReload(args);
|
|
3290
|
+
case "mini_app_register":
|
|
3291
|
+
return this.handleRegister(args);
|
|
3292
|
+
case "mini_app_list_files":
|
|
3293
|
+
return this.handleListFiles(args);
|
|
3294
|
+
case "mini_app_read":
|
|
3295
|
+
return this.handleRead(args);
|
|
3296
|
+
case "mini_app_edit":
|
|
3297
|
+
return this.handleEdit(args);
|
|
3298
|
+
case "mini_app_write":
|
|
3299
|
+
return this.handleWrite(args);
|
|
3300
|
+
case "mini_app_delete":
|
|
3301
|
+
return this.handleDelete(args);
|
|
3302
|
+
case "mini_app_open":
|
|
3303
|
+
return this.handleOpen(args);
|
|
3304
|
+
case "mini_app_call":
|
|
3305
|
+
return this.handleCall(args, signal);
|
|
3306
|
+
case "mini_app_history_commit":
|
|
3307
|
+
return this.handleHistoryCommit(args);
|
|
3308
|
+
case "mini_app_history_list":
|
|
3309
|
+
return this.handleHistoryList(args);
|
|
3310
|
+
case "mini_app_history_reset":
|
|
3311
|
+
return this.handleHistoryReset(args);
|
|
3312
|
+
case "mini_app_history_revert":
|
|
3313
|
+
return this.handleHistoryRevert(args);
|
|
3314
|
+
default:
|
|
3315
|
+
throw new HostError("UNKNOWN_TOOL", `UNKNOWN_TOOL: ${name}`);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
async handleGet(args) {
|
|
3319
|
+
const appId = requireString2(args, "appId");
|
|
3320
|
+
const app = await this.apps.get(appId);
|
|
3321
|
+
if (!app) {
|
|
3322
|
+
return { ok: false, error: "NOT_FOUND" };
|
|
3323
|
+
}
|
|
3324
|
+
return { ok: true, app, path: this.apps.dirOf(appId) };
|
|
3325
|
+
}
|
|
3326
|
+
async handleReload(args) {
|
|
3327
|
+
const appId = requireString2(args, "appId");
|
|
3328
|
+
return this.apps.reload(appId);
|
|
3329
|
+
}
|
|
3330
|
+
async handleRegister(args) {
|
|
3331
|
+
const appId = requireString2(args, "appId");
|
|
3332
|
+
const files = asFiles(args.files);
|
|
3333
|
+
const app = await this.apps.register(appId, files);
|
|
3334
|
+
return { ok: true, path: this.apps.dirOf(appId), app };
|
|
3335
|
+
}
|
|
3336
|
+
async handleListFiles(args) {
|
|
3337
|
+
const appId = requireString2(args, "appId");
|
|
3338
|
+
const files = await this.apps.listFiles(appId);
|
|
3339
|
+
return { ok: true, appId, files };
|
|
3340
|
+
}
|
|
3341
|
+
async handleRead(args) {
|
|
3342
|
+
const appId = requireString2(args, "appId");
|
|
3343
|
+
const path12 = requireString2(args, "path");
|
|
3344
|
+
const range = parseReadRange(args);
|
|
3345
|
+
try {
|
|
3346
|
+
const file = await this.apps.readFile(appId, path12, range);
|
|
3347
|
+
return { ok: true, ...file };
|
|
3348
|
+
} catch (cause) {
|
|
3349
|
+
if (cause instanceof HostError && cause.code === "INVALID_RANGE") {
|
|
3350
|
+
return { ok: false, error: cause.message, code: cause.code };
|
|
3351
|
+
}
|
|
3352
|
+
throw cause;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
async handleEdit(args) {
|
|
3356
|
+
const appId = requireString2(args, "appId");
|
|
3357
|
+
const path12 = requireString2(args, "path");
|
|
3358
|
+
const edits = asEdits(args.edits);
|
|
3359
|
+
const commit = args.commit === false ? false : void 0;
|
|
3360
|
+
try {
|
|
3361
|
+
const result = await this.apps.editFile(appId, path12, edits, { commit });
|
|
3362
|
+
return { ok: true, ...result };
|
|
3363
|
+
} catch (cause) {
|
|
3364
|
+
if (cause instanceof HostError && cause.code === "EDIT_FAILED") {
|
|
3365
|
+
return { ok: false, error: cause.message, code: cause.code };
|
|
3366
|
+
}
|
|
3367
|
+
throw cause;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
async handleWrite(args) {
|
|
3371
|
+
const appId = requireString2(args, "appId");
|
|
3372
|
+
const path12 = requireString2(args, "path");
|
|
3373
|
+
const content = requireString2(args, "content");
|
|
3374
|
+
const commit = args.commit === false ? false : void 0;
|
|
3375
|
+
const result = await this.apps.writeFile(appId, path12, content, { commit });
|
|
3376
|
+
return { ok: true, ...result };
|
|
3377
|
+
}
|
|
3378
|
+
async handleDelete(args) {
|
|
3379
|
+
const appId = requireString2(args, "appId");
|
|
3380
|
+
const path12 = requireString2(args, "path");
|
|
3381
|
+
const commit = args.commit === false ? false : void 0;
|
|
3382
|
+
const result = await this.apps.deleteFile(appId, path12, { commit });
|
|
3383
|
+
return { ok: true, ...result };
|
|
3384
|
+
}
|
|
3385
|
+
async handleOpen(args) {
|
|
3386
|
+
const appId = requireString2(args, "appId");
|
|
3387
|
+
const app = await this.apps.get(appId);
|
|
3388
|
+
if (!app) {
|
|
3389
|
+
return { ok: false, error: "NOT_FOUND" };
|
|
3390
|
+
}
|
|
3391
|
+
const title = typeof args.title === "string" && args.title ? args.title : app.name;
|
|
3392
|
+
this.events?.emit({ type: "app:open", appId: app.id, title });
|
|
3393
|
+
return { ok: true, appId: app.id, title };
|
|
3394
|
+
}
|
|
3395
|
+
async handleCall(args, signal) {
|
|
3396
|
+
const appId = requireString2(args, "appId");
|
|
3397
|
+
const method = requireString2(args, "method");
|
|
3398
|
+
const callArgs = isRecord5(args.args) ? args.args : args.args === void 0 ? {} : args.args;
|
|
3399
|
+
try {
|
|
3400
|
+
const value = await this.apps.call(appId, method, callArgs, signal);
|
|
3401
|
+
return { ok: true, value };
|
|
3402
|
+
} catch (cause) {
|
|
3403
|
+
if (signal?.aborted) {
|
|
3404
|
+
return { ok: false, error: "cancelled", cancelled: true };
|
|
3405
|
+
}
|
|
3406
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3407
|
+
return { ok: false, error: message };
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
async handleHistoryCommit(args) {
|
|
3411
|
+
const appId = requireString2(args, "appId");
|
|
3412
|
+
const message = requireString2(args, "message");
|
|
3413
|
+
const dir = this.apps.dirOf(appId);
|
|
3414
|
+
await this.git.init(dir);
|
|
3415
|
+
return this.git.commit(dir, message);
|
|
3416
|
+
}
|
|
3417
|
+
async handleHistoryList(args) {
|
|
3418
|
+
const appId = requireString2(args, "appId");
|
|
3419
|
+
const limit = typeof args.limit === "number" ? args.limit : void 0;
|
|
3420
|
+
const dir = this.apps.dirOf(appId);
|
|
3421
|
+
await this.git.init(dir);
|
|
3422
|
+
return this.git.listCommits(dir, { limit });
|
|
3423
|
+
}
|
|
3424
|
+
async handleHistoryReset(args) {
|
|
3425
|
+
const appId = requireString2(args, "appId");
|
|
3426
|
+
const commitId = requireString2(args, "commitId");
|
|
3427
|
+
const dir = this.apps.dirOf(appId);
|
|
3428
|
+
await this.git.init(dir);
|
|
3429
|
+
return this.git.resetTo(dir, commitId);
|
|
3430
|
+
}
|
|
3431
|
+
async handleHistoryRevert(args) {
|
|
3432
|
+
const appId = requireString2(args, "appId");
|
|
3433
|
+
const commitId = requireString2(args, "commitId");
|
|
3434
|
+
const dir = this.apps.dirOf(appId);
|
|
3435
|
+
await this.git.init(dir);
|
|
3436
|
+
return this.git.revert(dir, commitId);
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
|
|
3440
|
+
// src/host.ts
|
|
3441
|
+
var Host = class {
|
|
3442
|
+
constructor(capabilities, lifecycle, paths, config, services, http) {
|
|
3443
|
+
this.capabilities = capabilities;
|
|
3444
|
+
this.lifecycle = lifecycle;
|
|
3445
|
+
this.paths = paths;
|
|
3446
|
+
this.config = config;
|
|
3447
|
+
this.services = services;
|
|
3448
|
+
this.http = http;
|
|
3449
|
+
}
|
|
3450
|
+
capabilities;
|
|
3451
|
+
lifecycle;
|
|
3452
|
+
paths;
|
|
3453
|
+
config;
|
|
3454
|
+
services;
|
|
3455
|
+
http;
|
|
3456
|
+
boundPort = 0;
|
|
3457
|
+
attached = false;
|
|
3458
|
+
active = false;
|
|
3459
|
+
get port() {
|
|
3460
|
+
return this.boundPort;
|
|
3461
|
+
}
|
|
3462
|
+
async apply(ctx) {
|
|
3463
|
+
if (!this.attached) {
|
|
3464
|
+
await this.lifecycle.attach(ctx, this.services);
|
|
3465
|
+
this.attached = true;
|
|
3466
|
+
}
|
|
3467
|
+
return this.start();
|
|
3468
|
+
}
|
|
3469
|
+
async start() {
|
|
3470
|
+
this.active = true;
|
|
3471
|
+
if (this.boundPort > 0) {
|
|
3472
|
+
return { port: this.boundPort };
|
|
3473
|
+
}
|
|
3474
|
+
try {
|
|
3475
|
+
this.boundPort = await this.http.listen(this.config.hostPort);
|
|
3476
|
+
} catch (cause) {
|
|
3477
|
+
if (cause instanceof HostError) {
|
|
3478
|
+
throw cause;
|
|
3479
|
+
}
|
|
3480
|
+
throw new HostError(
|
|
3481
|
+
"HOST_LISTEN_FAILED",
|
|
3482
|
+
`failed to listen on ${this.config.hostPort}`,
|
|
3483
|
+
{ cause }
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
this.lifecycle.onHostPortChanged?.(this.boundPort);
|
|
3487
|
+
return { port: this.boundPort };
|
|
3488
|
+
}
|
|
3489
|
+
async stop() {
|
|
3490
|
+
if (!this.active && this.boundPort === 0 && !this.attached) {
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
this.active = false;
|
|
3494
|
+
this.boundPort = 0;
|
|
3495
|
+
try {
|
|
3496
|
+
await this.http.close();
|
|
3497
|
+
} finally {
|
|
3498
|
+
const detach = this.lifecycle.detach;
|
|
3499
|
+
this.attached = false;
|
|
3500
|
+
if (detach) {
|
|
3501
|
+
await detach();
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
};
|
|
3506
|
+
|
|
3507
|
+
// src/create-host.ts
|
|
3508
|
+
function createHost(capabilities, lifecycle, options) {
|
|
3509
|
+
const config = parseHostConfig(options.config);
|
|
3510
|
+
const paths = new WorkspacePaths(config.runtimeRoot);
|
|
3511
|
+
const git2 = new GitHistory();
|
|
3512
|
+
const apps = new AppsManager(paths, capabilities, git2, config);
|
|
3513
|
+
const events = new HostEventBus();
|
|
3514
|
+
const tools = new ToolFacade(apps, git2, paths, events);
|
|
3515
|
+
const compiler = new UiCompiler(paths);
|
|
3516
|
+
apps.setUiCompiler(compiler);
|
|
3517
|
+
const themes = options.themes ?? EMPTY_THEME_RESOURCE;
|
|
3518
|
+
const http = new HttpGateway(
|
|
3519
|
+
apps,
|
|
3520
|
+
config,
|
|
3521
|
+
paths,
|
|
3522
|
+
compiler,
|
|
3523
|
+
git2,
|
|
3524
|
+
themes,
|
|
3525
|
+
events,
|
|
3526
|
+
(port) => lifecycle.onHostPortChanged?.(port)
|
|
3527
|
+
);
|
|
3528
|
+
const services = { apps, git: git2, tools, paths, config };
|
|
3529
|
+
return new Host(capabilities, lifecycle, paths, config, services, http);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
// src/locales/en.json
|
|
3533
|
+
var en_default = {
|
|
3534
|
+
config: {
|
|
3535
|
+
missingFile: "Host config file not found: {{path}}",
|
|
3536
|
+
missingHint: "Run the install script or mma init to create host.json."
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
|
|
3540
|
+
// src/locales/zh-CN.json
|
|
3541
|
+
var zh_CN_default = {
|
|
3542
|
+
config: {
|
|
3543
|
+
missingFile: "\u672A\u627E\u5230 host \u914D\u7F6E\u6587\u4EF6\uFF1A{{path}}",
|
|
3544
|
+
missingHint: "\u8BF7\u5148\u8FD0\u884C\u5B89\u88C5\u811A\u672C\u6216 mma init \u751F\u6210 host.json\u3002"
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
|
|
3548
|
+
// src/i18n/index.ts
|
|
3549
|
+
var resources = {
|
|
3550
|
+
"zh-CN": { translation: zh_CN_default },
|
|
3551
|
+
en: { translation: en_default }
|
|
3552
|
+
};
|
|
3553
|
+
function isLocaleId(value) {
|
|
3554
|
+
return LOCALE_IDS.includes(value);
|
|
3555
|
+
}
|
|
3556
|
+
function failOnMissingKey() {
|
|
3557
|
+
return process.env.NODE_ENV !== "production";
|
|
3558
|
+
}
|
|
3559
|
+
function createHostI18n(locale) {
|
|
3560
|
+
if (!isLocaleId(locale)) {
|
|
3561
|
+
throw new HostError("I18N_INVALID_LOCALE", `unsupported locale: ${String(locale)}`);
|
|
3562
|
+
}
|
|
3563
|
+
const i18n = createInstance();
|
|
3564
|
+
void i18n.init({
|
|
3565
|
+
lng: locale,
|
|
3566
|
+
fallbackLng: false,
|
|
3567
|
+
supportedLngs: LOCALE_IDS,
|
|
3568
|
+
nonExplicitSupportedLngs: false,
|
|
3569
|
+
load: "currentOnly",
|
|
3570
|
+
defaultNS: "translation",
|
|
3571
|
+
ns: ["translation"],
|
|
3572
|
+
resources,
|
|
3573
|
+
interpolation: { escapeValue: false },
|
|
3574
|
+
returnNull: false,
|
|
3575
|
+
returnEmptyString: false,
|
|
3576
|
+
initImmediate: false,
|
|
3577
|
+
showSupportNotice: false
|
|
3578
|
+
});
|
|
3579
|
+
if (!i18n.isInitialized) {
|
|
3580
|
+
throw new HostError("I18N_INIT_FAILED", `i18n failed to initialize for locale: ${locale}`);
|
|
3581
|
+
}
|
|
3582
|
+
return {
|
|
3583
|
+
locale,
|
|
3584
|
+
t(key, params) {
|
|
3585
|
+
if (!i18n.exists(key)) {
|
|
3586
|
+
if (failOnMissingKey()) {
|
|
3587
|
+
throw new HostError("I18N_MISSING_KEY", `missing i18n key: ${key}`);
|
|
3588
|
+
}
|
|
3589
|
+
return key;
|
|
3590
|
+
}
|
|
3591
|
+
const value = i18n.t(key, params ?? {});
|
|
3592
|
+
if (typeof value !== "string") {
|
|
3593
|
+
throw new HostError("I18N_INVALID_VALUE", `i18n key did not resolve to a string: ${key}`);
|
|
3594
|
+
}
|
|
3595
|
+
return value;
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// src/index.ts
|
|
3601
|
+
var packageName = "@monkey-mini-app/host";
|
|
3602
|
+
|
|
3603
|
+
export { AppsManager, DEFAULT_HOST_CONFIG_SEED, EMPTY_THEME_RESOURCE, GitHistory, Host, HostConfigError, HostError, HostEventBus, HttpGateway, LOCALE_IDS, PALETTE_IDS, THEME_IDS, ToolFacade, UiCompiler, WorkspacePaths, acronymOf, appRunnerHtml, applyEditsToNormalizedContent, asAbsolutePath, asAppId, assertNever, bindCapsToContext, bootstrapHostConfig, createHost, createHostI18n, effectiveSignal, formatSse, fuzzyFindText, isAbsolutePath, isAgentCwdType, isAppId, isMiniAppToolName, listStorageTables, loadHostConfig, packageName, parseHostConfig, parseManifest, readAppTheme, readJsonFile, resolveAgentCwd, resolveUiDistDir, storageTablePath, writeAppTheme, writeHostConfig };
|