@mindstudio-ai/local-model-tunnel 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -11
- package/dist/chunk-C3JPRLSS.js +1485 -0
- package/dist/chunk-C3JPRLSS.js.map +1 -0
- package/dist/{chunk-KLOTDVWL.js → chunk-QALGC7T7.js} +56 -310
- package/dist/chunk-QALGC7T7.js.map +1 -0
- package/dist/chunk-WFQXIMTS.js +378 -0
- package/dist/chunk-WFQXIMTS.js.map +1 -0
- package/dist/cli.js +18 -2
- package/dist/cli.js.map +1 -1
- package/dist/headless.d.ts +113 -0
- package/dist/headless.js +8 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -2
- package/dist/{tui-UNFZSO7R.js → tui-4PJCFILV.js} +1467 -318
- package/dist/tui-4PJCFILV.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-KLOTDVWL.js.map +0 -1
- package/dist/tui-UNFZSO7R.js.map +0 -1
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import Conf from "conf";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
var config = new Conf({
|
|
6
|
+
projectName: "mindstudio-local",
|
|
7
|
+
cwd: path.join(os.homedir(), ".mindstudio-local-tunnel"),
|
|
8
|
+
configName: "config",
|
|
9
|
+
defaults: {
|
|
10
|
+
environment: "prod",
|
|
11
|
+
providerBaseUrls: {},
|
|
12
|
+
providerInstallPaths: {},
|
|
13
|
+
localInterfaces: {},
|
|
14
|
+
environments: {
|
|
15
|
+
prod: {
|
|
16
|
+
apiBaseUrl: "https://api.mindstudio.ai"
|
|
17
|
+
},
|
|
18
|
+
local: {
|
|
19
|
+
apiBaseUrl: "http://localhost:3129"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
function getEnvironment() {
|
|
25
|
+
return config.get("environment");
|
|
26
|
+
}
|
|
27
|
+
function getEnvConfig() {
|
|
28
|
+
const env = getEnvironment();
|
|
29
|
+
return config.get(`environments.${env}`);
|
|
30
|
+
}
|
|
31
|
+
function setEnvConfig(key, value) {
|
|
32
|
+
const env = getEnvironment();
|
|
33
|
+
config.set(`environments.${env}.${key}`, value);
|
|
34
|
+
}
|
|
35
|
+
function getApiKey() {
|
|
36
|
+
return getEnvConfig().apiKey;
|
|
37
|
+
}
|
|
38
|
+
function setApiKey(key) {
|
|
39
|
+
setEnvConfig("apiKey", key);
|
|
40
|
+
}
|
|
41
|
+
function getUserId() {
|
|
42
|
+
return getEnvConfig().userId;
|
|
43
|
+
}
|
|
44
|
+
function setUserId(id) {
|
|
45
|
+
setEnvConfig("userId", id);
|
|
46
|
+
}
|
|
47
|
+
function getApiBaseUrl() {
|
|
48
|
+
return getEnvConfig().apiBaseUrl;
|
|
49
|
+
}
|
|
50
|
+
function getConfigPath() {
|
|
51
|
+
return config.path;
|
|
52
|
+
}
|
|
53
|
+
function getProviderBaseUrl(name, defaultUrl) {
|
|
54
|
+
const urls = config.get("providerBaseUrls");
|
|
55
|
+
return urls[name] ?? defaultUrl;
|
|
56
|
+
}
|
|
57
|
+
function setProviderBaseUrl(name, url) {
|
|
58
|
+
const urls = config.get("providerBaseUrls");
|
|
59
|
+
urls[name] = url;
|
|
60
|
+
config.set("providerBaseUrls", urls);
|
|
61
|
+
}
|
|
62
|
+
function getProviderInstallPath(name) {
|
|
63
|
+
const paths = config.get("providerInstallPaths");
|
|
64
|
+
return paths[name];
|
|
65
|
+
}
|
|
66
|
+
function setProviderInstallPath(name, installPath) {
|
|
67
|
+
const paths = config.get("providerInstallPaths");
|
|
68
|
+
paths[name] = installPath;
|
|
69
|
+
config.set("providerInstallPaths", paths);
|
|
70
|
+
}
|
|
71
|
+
function getLocalInterfacesDir() {
|
|
72
|
+
return path.join(os.homedir(), ".mindstudio-local-tunnel", "interfaces");
|
|
73
|
+
}
|
|
74
|
+
function getLocalInterfacePath(key) {
|
|
75
|
+
const interfaces = config.get("localInterfaces");
|
|
76
|
+
return interfaces[key];
|
|
77
|
+
}
|
|
78
|
+
function setLocalInterfacePath(key, dirPath) {
|
|
79
|
+
const interfaces = config.get("localInterfaces");
|
|
80
|
+
interfaces[key] = dirPath;
|
|
81
|
+
config.set("localInterfaces", interfaces);
|
|
82
|
+
}
|
|
83
|
+
function deleteLocalInterfacePath(key) {
|
|
84
|
+
const interfaces = config.get("localInterfaces");
|
|
85
|
+
delete interfaces[key];
|
|
86
|
+
config.set("localInterfaces", interfaces);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/dev/logger.ts
|
|
90
|
+
import fs from "fs";
|
|
91
|
+
var LEVELS = {
|
|
92
|
+
error: 0,
|
|
93
|
+
warn: 1,
|
|
94
|
+
info: 2,
|
|
95
|
+
debug: 3
|
|
96
|
+
};
|
|
97
|
+
var currentLevel = LEVELS.error;
|
|
98
|
+
var writeFn = () => {
|
|
99
|
+
};
|
|
100
|
+
function timestamp() {
|
|
101
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
102
|
+
}
|
|
103
|
+
function write(level, msg, data) {
|
|
104
|
+
if (LEVELS[level] > currentLevel) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const parts = [`[${timestamp()}]`, level.toUpperCase().padEnd(5), msg];
|
|
108
|
+
if (data) {
|
|
109
|
+
parts.push(JSON.stringify(data));
|
|
110
|
+
}
|
|
111
|
+
writeFn(parts.join(" "));
|
|
112
|
+
}
|
|
113
|
+
var log = {
|
|
114
|
+
error(msg, data) {
|
|
115
|
+
write("error", msg, data);
|
|
116
|
+
},
|
|
117
|
+
warn(msg, data) {
|
|
118
|
+
write("warn", msg, data);
|
|
119
|
+
},
|
|
120
|
+
info(msg, data) {
|
|
121
|
+
write("info", msg, data);
|
|
122
|
+
},
|
|
123
|
+
debug(msg, data) {
|
|
124
|
+
write("debug", msg, data);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
function initLoggerHeadless(level = "info") {
|
|
128
|
+
currentLevel = LEVELS[level];
|
|
129
|
+
writeFn = (line) => {
|
|
130
|
+
process.stderr.write(line + "\n");
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function initLoggerInteractive(level = "error") {
|
|
134
|
+
currentLevel = LEVELS[level];
|
|
135
|
+
let fd = null;
|
|
136
|
+
writeFn = (line) => {
|
|
137
|
+
try {
|
|
138
|
+
if (fd === null) {
|
|
139
|
+
fd = fs.openSync(".mindstudio-dev.log", "a");
|
|
140
|
+
}
|
|
141
|
+
fs.writeSync(fd, line + "\n");
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/dev/api.ts
|
|
148
|
+
function getHeaders(sessionId) {
|
|
149
|
+
const apiKey = getApiKey();
|
|
150
|
+
if (!apiKey) {
|
|
151
|
+
throw new Error("Not authenticated. Run mindstudio-local to set up.");
|
|
152
|
+
}
|
|
153
|
+
const headers = {
|
|
154
|
+
Authorization: `Bearer ${apiKey}`,
|
|
155
|
+
"Content-Type": "application/json"
|
|
156
|
+
};
|
|
157
|
+
if (sessionId) headers["x-dev-session"] = sessionId;
|
|
158
|
+
return headers;
|
|
159
|
+
}
|
|
160
|
+
function basePath(appId) {
|
|
161
|
+
return `${getApiBaseUrl()}/_internal/v2/apps/${appId}/dev`;
|
|
162
|
+
}
|
|
163
|
+
async function apiRequest(method, url, headers, body) {
|
|
164
|
+
const start = Date.now();
|
|
165
|
+
const logTag = `${method} ${url.replace(getApiBaseUrl(), "")}`;
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
method,
|
|
168
|
+
headers,
|
|
169
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
170
|
+
});
|
|
171
|
+
const duration = Date.now() - start;
|
|
172
|
+
if (response.status === 204) {
|
|
173
|
+
log.debug(`api ${logTag} \u2192 204 (${duration}ms)`);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const error = await response.text();
|
|
178
|
+
log.error(`api ${logTag} \u2192 ${response.status} (${duration}ms)`, { error });
|
|
179
|
+
throw new ApiError(`${logTag} failed: ${response.status} ${error}`, response.status);
|
|
180
|
+
}
|
|
181
|
+
const data = await response.json();
|
|
182
|
+
log.info(`api ${logTag} \u2192 ${response.status} (${duration}ms)`);
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
async function startDevSession(appId, opts) {
|
|
186
|
+
const body = {};
|
|
187
|
+
if (opts?.branch) body.branch = opts.branch;
|
|
188
|
+
if (opts?.proxyUrl) body.proxyUrl = opts.proxyUrl;
|
|
189
|
+
if (opts?.methods) body.methods = opts.methods;
|
|
190
|
+
return apiRequest("POST", `${basePath(appId)}/manage/start`, getHeaders(), body);
|
|
191
|
+
}
|
|
192
|
+
async function stopDevSession(appId, sessionId) {
|
|
193
|
+
await apiRequest("POST", `${basePath(appId)}/manage/stop`, getHeaders(sessionId));
|
|
194
|
+
}
|
|
195
|
+
async function pollDevRequest(appId, sessionId, proxyUrl) {
|
|
196
|
+
const url = proxyUrl ? `${basePath(appId)}/poll?proxyUrl=${encodeURIComponent(proxyUrl)}` : `${basePath(appId)}/poll`;
|
|
197
|
+
try {
|
|
198
|
+
return await apiRequest("GET", url, getHeaders(sessionId));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (err instanceof ApiError) {
|
|
201
|
+
throw new DevPollError(err.message, err.statusCode);
|
|
202
|
+
}
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function submitDevResult(appId, sessionId, requestId, result) {
|
|
207
|
+
await apiRequest(
|
|
208
|
+
"POST",
|
|
209
|
+
`${basePath(appId)}/result/${requestId}`,
|
|
210
|
+
getHeaders(sessionId),
|
|
211
|
+
result
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
async function syncSchema(appId, sessionId, tables) {
|
|
215
|
+
return apiRequest(
|
|
216
|
+
"POST",
|
|
217
|
+
`${basePath(appId)}/manage/sync-schema`,
|
|
218
|
+
getHeaders(sessionId),
|
|
219
|
+
{ tables }
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
async function resetDevDatabase(appId, sessionId, mode = "snapshot") {
|
|
223
|
+
const data = await apiRequest(
|
|
224
|
+
"POST",
|
|
225
|
+
`${basePath(appId)}/manage/reset?mode=${mode}`,
|
|
226
|
+
getHeaders(sessionId)
|
|
227
|
+
);
|
|
228
|
+
return data.databases;
|
|
229
|
+
}
|
|
230
|
+
async function impersonate(appId, sessionId, roles) {
|
|
231
|
+
return apiRequest(
|
|
232
|
+
"POST",
|
|
233
|
+
`${basePath(appId)}/manage/impersonate`,
|
|
234
|
+
getHeaders(sessionId),
|
|
235
|
+
{ roles: roles && roles.length > 0 ? roles : null }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
async function refreshContext(appId, sessionId) {
|
|
239
|
+
const data = await apiRequest(
|
|
240
|
+
"POST",
|
|
241
|
+
`${basePath(appId)}/manage/refresh-context`,
|
|
242
|
+
getHeaders(sessionId)
|
|
243
|
+
);
|
|
244
|
+
return data.clientContext;
|
|
245
|
+
}
|
|
246
|
+
async function fetchCallbackToken(appId, sessionId) {
|
|
247
|
+
const data = await apiRequest(
|
|
248
|
+
"POST",
|
|
249
|
+
`${basePath(appId)}/manage/token`,
|
|
250
|
+
getHeaders(sessionId)
|
|
251
|
+
);
|
|
252
|
+
return data.authorizationToken;
|
|
253
|
+
}
|
|
254
|
+
var ApiError = class extends Error {
|
|
255
|
+
constructor(message, statusCode) {
|
|
256
|
+
super(message);
|
|
257
|
+
this.statusCode = statusCode;
|
|
258
|
+
this.name = "ApiError";
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var DevPollError = class extends Error {
|
|
262
|
+
constructor(message, statusCode) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.statusCode = statusCode;
|
|
265
|
+
this.name = "DevPollError";
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// src/dev/events.ts
|
|
270
|
+
import { EventEmitter } from "events";
|
|
271
|
+
var DevEventEmitter = class extends EventEmitter {
|
|
272
|
+
emitStart(event) {
|
|
273
|
+
this.emit("dev:start", event);
|
|
274
|
+
}
|
|
275
|
+
emitComplete(event) {
|
|
276
|
+
this.emit("dev:complete", event);
|
|
277
|
+
}
|
|
278
|
+
emitSessionExpired() {
|
|
279
|
+
this.emit("dev:session-expired");
|
|
280
|
+
}
|
|
281
|
+
emitAuthRefreshStart(url) {
|
|
282
|
+
this.emit("dev:auth-refresh-start", url);
|
|
283
|
+
}
|
|
284
|
+
emitAuthRefreshSuccess() {
|
|
285
|
+
this.emit("dev:auth-refresh-success");
|
|
286
|
+
}
|
|
287
|
+
emitAuthRefreshFailed() {
|
|
288
|
+
this.emit("dev:auth-refresh-failed");
|
|
289
|
+
}
|
|
290
|
+
emitConnectionWarning(message) {
|
|
291
|
+
this.emit("dev:connection-warning", message);
|
|
292
|
+
}
|
|
293
|
+
emitConnectionRestored() {
|
|
294
|
+
this.emit("dev:connection-restored");
|
|
295
|
+
}
|
|
296
|
+
emitImpersonate(event) {
|
|
297
|
+
this.emit("dev:impersonate", event);
|
|
298
|
+
}
|
|
299
|
+
emitScenarioStart(event) {
|
|
300
|
+
this.emit("dev:scenario-start", event);
|
|
301
|
+
}
|
|
302
|
+
emitScenarioComplete(event) {
|
|
303
|
+
this.emit("dev:scenario-complete", event);
|
|
304
|
+
}
|
|
305
|
+
onStart(handler) {
|
|
306
|
+
this.on("dev:start", handler);
|
|
307
|
+
return () => this.off("dev:start", handler);
|
|
308
|
+
}
|
|
309
|
+
onComplete(handler) {
|
|
310
|
+
this.on("dev:complete", handler);
|
|
311
|
+
return () => this.off("dev:complete", handler);
|
|
312
|
+
}
|
|
313
|
+
onSessionExpired(handler) {
|
|
314
|
+
this.on("dev:session-expired", handler);
|
|
315
|
+
return () => this.off("dev:session-expired", handler);
|
|
316
|
+
}
|
|
317
|
+
onAuthRefreshStart(handler) {
|
|
318
|
+
this.on("dev:auth-refresh-start", handler);
|
|
319
|
+
return () => this.off("dev:auth-refresh-start", handler);
|
|
320
|
+
}
|
|
321
|
+
onAuthRefreshSuccess(handler) {
|
|
322
|
+
this.on("dev:auth-refresh-success", handler);
|
|
323
|
+
return () => this.off("dev:auth-refresh-success", handler);
|
|
324
|
+
}
|
|
325
|
+
onAuthRefreshFailed(handler) {
|
|
326
|
+
this.on("dev:auth-refresh-failed", handler);
|
|
327
|
+
return () => this.off("dev:auth-refresh-failed", handler);
|
|
328
|
+
}
|
|
329
|
+
onConnectionWarning(handler) {
|
|
330
|
+
this.on("dev:connection-warning", handler);
|
|
331
|
+
return () => this.off("dev:connection-warning", handler);
|
|
332
|
+
}
|
|
333
|
+
onConnectionRestored(handler) {
|
|
334
|
+
this.on("dev:connection-restored", handler);
|
|
335
|
+
return () => this.off("dev:connection-restored", handler);
|
|
336
|
+
}
|
|
337
|
+
onImpersonate(handler) {
|
|
338
|
+
this.on("dev:impersonate", handler);
|
|
339
|
+
return () => this.off("dev:impersonate", handler);
|
|
340
|
+
}
|
|
341
|
+
onScenarioStart(handler) {
|
|
342
|
+
this.on("dev:scenario-start", handler);
|
|
343
|
+
return () => this.off("dev:scenario-start", handler);
|
|
344
|
+
}
|
|
345
|
+
onScenarioComplete(handler) {
|
|
346
|
+
this.on("dev:scenario-complete", handler);
|
|
347
|
+
return () => this.off("dev:scenario-complete", handler);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
var devRequestEvents = new DevEventEmitter();
|
|
351
|
+
|
|
352
|
+
// src/dev/transpiler.ts
|
|
353
|
+
import { unlink, mkdir, readdir } from "fs/promises";
|
|
354
|
+
import { existsSync } from "fs";
|
|
355
|
+
import { resolve, dirname, basename, join } from "path";
|
|
356
|
+
import { build } from "esbuild";
|
|
357
|
+
var Transpiler = class {
|
|
358
|
+
projectRoot;
|
|
359
|
+
outputFiles = /* @__PURE__ */ new Set();
|
|
360
|
+
constructor(projectRoot) {
|
|
361
|
+
this.projectRoot = projectRoot;
|
|
362
|
+
this.cleanupOrphans();
|
|
363
|
+
}
|
|
364
|
+
/** Remove any .__ms_dev__.mjs files found in the project source tree (not in node_modules/.cache). */
|
|
365
|
+
async cleanupOrphans() {
|
|
366
|
+
try {
|
|
367
|
+
await removeOrphanedDevFiles(this.projectRoot);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Transpile a method file to ESM JavaScript.
|
|
373
|
+
* Returns the absolute path to the output .mjs file.
|
|
374
|
+
* Output is written inside the nearest node_modules/.cache/mindstudio-dev/
|
|
375
|
+
* so ESM resolver can find packages and the repo stays clean.
|
|
376
|
+
*/
|
|
377
|
+
async transpile(methodPath) {
|
|
378
|
+
const start = Date.now();
|
|
379
|
+
const absolutePath = resolve(this.projectRoot, methodPath);
|
|
380
|
+
const name = basename(absolutePath).replace(/\.[^.]+$/, "");
|
|
381
|
+
log.debug("transpiler Transpiling", { methodPath });
|
|
382
|
+
const nodeModulesDir = findNearestNodeModules(dirname(absolutePath));
|
|
383
|
+
if (!nodeModulesDir) {
|
|
384
|
+
log.error("transpiler No node_modules found", { methodPath, searchStart: dirname(absolutePath) });
|
|
385
|
+
throw new Error(
|
|
386
|
+
`No node_modules found near ${methodPath}. Run npm install first.`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
log.debug("transpiler Found node_modules", { path: nodeModulesDir });
|
|
390
|
+
const outDir = join(nodeModulesDir, ".cache", "mindstudio-dev");
|
|
391
|
+
await mkdir(outDir, { recursive: true });
|
|
392
|
+
const outfile = join(outDir, `${name}.__ms_dev__.mjs`);
|
|
393
|
+
await build({
|
|
394
|
+
entryPoints: [absolutePath],
|
|
395
|
+
bundle: true,
|
|
396
|
+
format: "esm",
|
|
397
|
+
platform: "node",
|
|
398
|
+
target: "node22",
|
|
399
|
+
outfile,
|
|
400
|
+
external: ["@mindstudio-ai/agent"],
|
|
401
|
+
absWorkingDir: this.projectRoot,
|
|
402
|
+
logLevel: "silent"
|
|
403
|
+
});
|
|
404
|
+
this.outputFiles.add(outfile);
|
|
405
|
+
log.info(`transpiler Transpiled in ${Date.now() - start}ms`, { methodPath, outfile });
|
|
406
|
+
return outfile;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Clean up all transpiled output files.
|
|
410
|
+
*/
|
|
411
|
+
async cleanup() {
|
|
412
|
+
log.debug("transpiler Cleaning up", { fileCount: this.outputFiles.size });
|
|
413
|
+
for (const file of this.outputFiles) {
|
|
414
|
+
await unlink(file).catch(() => {
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
this.outputFiles.clear();
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
async function removeOrphanedDevFiles(dir) {
|
|
421
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
424
|
+
const fullPath = join(dir, entry.name);
|
|
425
|
+
if (entry.isDirectory()) {
|
|
426
|
+
await removeOrphanedDevFiles(fullPath);
|
|
427
|
+
} else if (entry.name.endsWith(".__ms_dev__.mjs")) {
|
|
428
|
+
log.debug("transpiler Removing orphaned file", { path: fullPath });
|
|
429
|
+
await unlink(fullPath).catch(() => {
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function findNearestNodeModules(startDir) {
|
|
435
|
+
let dir = startDir;
|
|
436
|
+
while (true) {
|
|
437
|
+
const candidate = join(dir, "node_modules");
|
|
438
|
+
if (existsSync(candidate)) {
|
|
439
|
+
return candidate;
|
|
440
|
+
}
|
|
441
|
+
const parent = dirname(dir);
|
|
442
|
+
if (parent === dir) break;
|
|
443
|
+
dir = parent;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/dev/executor.ts
|
|
449
|
+
import { spawn } from "child_process";
|
|
450
|
+
import { writeFile, unlink as unlink2 } from "fs/promises";
|
|
451
|
+
import { join as join2 } from "path";
|
|
452
|
+
import { tmpdir } from "os";
|
|
453
|
+
import { randomBytes } from "crypto";
|
|
454
|
+
var EXECUTION_TIMEOUT_MS = 3e4;
|
|
455
|
+
function buildBootstrapScript(opts) {
|
|
456
|
+
return `
|
|
457
|
+
global.ai = {
|
|
458
|
+
auth: ${JSON.stringify(opts.auth)},
|
|
459
|
+
databases: ${JSON.stringify(opts.databases)},
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
function serializeError(err) {
|
|
463
|
+
if (!err) return { message: 'Unknown error' };
|
|
464
|
+
|
|
465
|
+
const serialized = {
|
|
466
|
+
message: String(err.message ?? err),
|
|
467
|
+
stack: err.stack,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// Capture common extra properties from SDK/HTTP errors
|
|
471
|
+
if (err.code !== undefined) serialized.code = err.code;
|
|
472
|
+
if (err.statusCode !== undefined) serialized.statusCode = err.statusCode;
|
|
473
|
+
if (err.status !== undefined) serialized.status = err.status;
|
|
474
|
+
if (err.response !== undefined) {
|
|
475
|
+
try { serialized.response = typeof err.response === 'string' ? err.response : JSON.stringify(err.response); } catch {}
|
|
476
|
+
}
|
|
477
|
+
if (err.body !== undefined) {
|
|
478
|
+
try { serialized.body = typeof err.body === 'string' ? err.body : JSON.stringify(err.body); } catch {}
|
|
479
|
+
}
|
|
480
|
+
if (err.cause !== undefined) {
|
|
481
|
+
serialized.cause = serializeError(err.cause);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Capture any other enumerable properties
|
|
485
|
+
for (const key of Object.keys(err)) {
|
|
486
|
+
if (!(key in serialized)) {
|
|
487
|
+
try {
|
|
488
|
+
const val = err[key];
|
|
489
|
+
if (val !== undefined && typeof val !== 'function') {
|
|
490
|
+
serialized[key] = typeof val === 'object' ? JSON.stringify(val) : val;
|
|
491
|
+
}
|
|
492
|
+
} catch {}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return serialized;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Capture console output from method code
|
|
500
|
+
const _stdout = [];
|
|
501
|
+
console.log = (...args) => _stdout.push(args.map(String).join(' '));
|
|
502
|
+
console.warn = (...args) => _stdout.push(args.map(String).join(' '));
|
|
503
|
+
console.error = (...args) => _stdout.push(args.map(String).join(' '));
|
|
504
|
+
|
|
505
|
+
const _startTime = Date.now();
|
|
506
|
+
|
|
507
|
+
const { ${opts.methodExport} } = await import(${JSON.stringify(opts.transpiledPath + "?t=" + Date.now())});
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const returnValue = await ${opts.methodExport}(${JSON.stringify(opts.input)});
|
|
511
|
+
const _stats = { memoryUsedBytes: process.memoryUsage().heapUsed, executionTimeMs: Date.now() - _startTime };
|
|
512
|
+
process.stdout.write(JSON.stringify({ success: true, output: returnValue, stdout: _stdout, stats: _stats }));
|
|
513
|
+
} catch (err) {
|
|
514
|
+
const _stats = { memoryUsedBytes: process.memoryUsage().heapUsed, executionTimeMs: Date.now() - _startTime };
|
|
515
|
+
process.stdout.write(JSON.stringify({
|
|
516
|
+
success: false,
|
|
517
|
+
error: serializeError(err),
|
|
518
|
+
stdout: _stdout,
|
|
519
|
+
stats: _stats,
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
async function executeMethod(opts) {
|
|
525
|
+
const tempFile = join2(
|
|
526
|
+
tmpdir(),
|
|
527
|
+
`ms-dev-${randomBytes(8).toString("hex")}.mjs`
|
|
528
|
+
);
|
|
529
|
+
const script = buildBootstrapScript(opts);
|
|
530
|
+
try {
|
|
531
|
+
await writeFile(tempFile, script, "utf-8");
|
|
532
|
+
log.debug("executor Spawning node process", { methodExport: opts.methodExport, cwd: opts.projectRoot, tempFile });
|
|
533
|
+
return await new Promise((resolve2, reject) => {
|
|
534
|
+
const stdoutChunks = [];
|
|
535
|
+
const stderrChunks = [];
|
|
536
|
+
const child = spawn("node", [tempFile], {
|
|
537
|
+
cwd: opts.projectRoot,
|
|
538
|
+
env: {
|
|
539
|
+
...process.env,
|
|
540
|
+
// Auth + config env vars read by @mindstudio-ai/agent SDK
|
|
541
|
+
// for platform callbacks (db queries, etc.)
|
|
542
|
+
CALLBACK_TOKEN: opts.authorizationToken,
|
|
543
|
+
REMOTE_HOSTNAME: opts.apiBaseUrl,
|
|
544
|
+
...opts.streamId ? { STREAM_ID: opts.streamId } : {}
|
|
545
|
+
},
|
|
546
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
547
|
+
});
|
|
548
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
549
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
550
|
+
const timeout = setTimeout(() => {
|
|
551
|
+
log.warn("executor Timeout after 30s, sending SIGKILL", { methodExport: opts.methodExport });
|
|
552
|
+
child.kill("SIGKILL");
|
|
553
|
+
reject(new Error("Method execution timed out after 30s"));
|
|
554
|
+
}, EXECUTION_TIMEOUT_MS);
|
|
555
|
+
child.on("close", (code) => {
|
|
556
|
+
clearTimeout(timeout);
|
|
557
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8").trim();
|
|
558
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
559
|
+
log.debug("executor Process exited", { code, stdoutLen: stdout.length, stderrLen: stderr.length });
|
|
560
|
+
if (stdout) {
|
|
561
|
+
try {
|
|
562
|
+
resolve2(JSON.parse(stdout));
|
|
563
|
+
return;
|
|
564
|
+
} catch {
|
|
565
|
+
log.warn("executor Invalid JSON from stdout", { stdout: stdout.slice(0, 200) });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const errorMessage = stderr || stdout || `Method process exited with code ${code ?? "unknown"}`;
|
|
569
|
+
resolve2({
|
|
570
|
+
success: false,
|
|
571
|
+
error: { message: errorMessage }
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
child.on("error", (err) => {
|
|
575
|
+
clearTimeout(timeout);
|
|
576
|
+
log.error("executor Process error", { error: err.message });
|
|
577
|
+
reject(err);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
} finally {
|
|
581
|
+
await unlink2(tempFile).catch(() => {
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/api.ts
|
|
587
|
+
function getHeaders2() {
|
|
588
|
+
const apiKey = getApiKey();
|
|
589
|
+
if (!apiKey) {
|
|
590
|
+
throw new Error("Not authenticated. Run: mindstudio-local auth");
|
|
591
|
+
}
|
|
592
|
+
const headers = {
|
|
593
|
+
Authorization: `Bearer ${apiKey}`,
|
|
594
|
+
"Content-Type": "application/json"
|
|
595
|
+
};
|
|
596
|
+
const userId = getUserId();
|
|
597
|
+
if (userId) {
|
|
598
|
+
headers["x-user-id"] = userId;
|
|
599
|
+
}
|
|
600
|
+
return headers;
|
|
601
|
+
}
|
|
602
|
+
async function pollForRequest(modelIds) {
|
|
603
|
+
const baseUrl = getApiBaseUrl();
|
|
604
|
+
const modelIdsParam = modelIds.join(",");
|
|
605
|
+
const response = await fetch(
|
|
606
|
+
`${baseUrl}/v1/local-models/poll?modelIds=${encodeURIComponent(modelIdsParam)}`,
|
|
607
|
+
{
|
|
608
|
+
method: "GET",
|
|
609
|
+
headers: getHeaders2()
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
if (response.status === 204) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
if (!response.ok) {
|
|
616
|
+
const error = await response.text();
|
|
617
|
+
throw new Error(`Poll failed: ${response.status} ${error}`);
|
|
618
|
+
}
|
|
619
|
+
const data = await response.json();
|
|
620
|
+
return data.request;
|
|
621
|
+
}
|
|
622
|
+
async function submitProgress(requestId, content, type = "chunk") {
|
|
623
|
+
const baseUrl = getApiBaseUrl();
|
|
624
|
+
const response = await fetch(
|
|
625
|
+
`${baseUrl}/v1/local-models/requests/${requestId}/progress`,
|
|
626
|
+
{
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: getHeaders2(),
|
|
629
|
+
body: JSON.stringify({ type, content })
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
console.warn(`Progress update failed: ${response.status}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function submitResult(requestId, success, result, error) {
|
|
637
|
+
const baseUrl = getApiBaseUrl();
|
|
638
|
+
const response = await fetch(
|
|
639
|
+
`${baseUrl}/v1/local-models/requests/${requestId}/result`,
|
|
640
|
+
{
|
|
641
|
+
method: "POST",
|
|
642
|
+
headers: getHeaders2(),
|
|
643
|
+
body: JSON.stringify({ success, result, error })
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
const errorText = await response.text();
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Result submission failed: ${response.status} ${errorText}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async function verifyApiKey() {
|
|
654
|
+
const baseUrl = getApiBaseUrl();
|
|
655
|
+
try {
|
|
656
|
+
const response = await fetch(`${baseUrl}/v1/local-models/verify-api-key`, {
|
|
657
|
+
method: "GET",
|
|
658
|
+
headers: getHeaders2()
|
|
659
|
+
});
|
|
660
|
+
return response.status === 204 || response.ok;
|
|
661
|
+
} catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function syncModels(models) {
|
|
666
|
+
const baseUrl = getApiBaseUrl();
|
|
667
|
+
const response = await fetch(`${baseUrl}/v1/local-models/models/sync`, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: getHeaders2(),
|
|
670
|
+
body: JSON.stringify({ models })
|
|
671
|
+
});
|
|
672
|
+
if (!response.ok) {
|
|
673
|
+
const errorText = await response.text();
|
|
674
|
+
throw new Error(`Sync failed: ${response.status} ${errorText}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function getSyncedModels() {
|
|
678
|
+
const baseUrl = getApiBaseUrl();
|
|
679
|
+
const response = await fetch(`${baseUrl}/v1/local-models/models`, {
|
|
680
|
+
method: "GET",
|
|
681
|
+
headers: getHeaders2()
|
|
682
|
+
});
|
|
683
|
+
if (!response.ok) {
|
|
684
|
+
const errorText = await response.text();
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Failed to fetch synced models: ${response.status} ${errorText}`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const data = await response.json();
|
|
690
|
+
return data.models;
|
|
691
|
+
}
|
|
692
|
+
async function requestDeviceAuth() {
|
|
693
|
+
const baseUrl = getApiBaseUrl();
|
|
694
|
+
const response = await fetch(`${baseUrl}/developer/v2/request-auth-url`, {
|
|
695
|
+
method: "GET",
|
|
696
|
+
headers: { "Content-Type": "application/json" }
|
|
697
|
+
});
|
|
698
|
+
if (!response.ok) {
|
|
699
|
+
const error = await response.text();
|
|
700
|
+
throw new Error(`Device auth request failed: ${response.status} ${error}`);
|
|
701
|
+
}
|
|
702
|
+
const data = await response.json();
|
|
703
|
+
return data;
|
|
704
|
+
}
|
|
705
|
+
async function pollDeviceAuth(token) {
|
|
706
|
+
const baseUrl = getApiBaseUrl();
|
|
707
|
+
const response = await fetch(`${baseUrl}/developer/v2/poll-auth-url`, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: { "Content-Type": "application/json" },
|
|
710
|
+
body: JSON.stringify({ token })
|
|
711
|
+
});
|
|
712
|
+
if (!response.ok) {
|
|
713
|
+
const error = await response.text();
|
|
714
|
+
throw new Error(`Device auth poll failed: ${response.status} ${error}`);
|
|
715
|
+
}
|
|
716
|
+
const data = await response.json();
|
|
717
|
+
return data;
|
|
718
|
+
}
|
|
719
|
+
async function getEditorSessions() {
|
|
720
|
+
const baseUrl = getApiBaseUrl();
|
|
721
|
+
const response = await fetch(`${baseUrl}/v1/local-editor/sessions`, {
|
|
722
|
+
method: "GET",
|
|
723
|
+
headers: getHeaders2()
|
|
724
|
+
});
|
|
725
|
+
if (!response.ok) {
|
|
726
|
+
const errorText = await response.text();
|
|
727
|
+
throw new Error(
|
|
728
|
+
`Failed to fetch editor sessions: ${response.status} ${errorText}`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const data = await response.json();
|
|
732
|
+
return data.editors;
|
|
733
|
+
}
|
|
734
|
+
async function disconnectHeartbeat() {
|
|
735
|
+
const baseUrl = getApiBaseUrl();
|
|
736
|
+
const response = await fetch(`${baseUrl}/v1/local-models/disconnect`, {
|
|
737
|
+
method: "POST",
|
|
738
|
+
headers: getHeaders2()
|
|
739
|
+
});
|
|
740
|
+
if (!response.ok) {
|
|
741
|
+
const error = await response.text();
|
|
742
|
+
throw new Error(`Heartbeat disconnect failed: ${response.status} ${error}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/dev/runner.ts
|
|
747
|
+
var DevRunner = class {
|
|
748
|
+
constructor(appId, projectRoot, startOpts = {}) {
|
|
749
|
+
this.appId = appId;
|
|
750
|
+
this.projectRoot = projectRoot;
|
|
751
|
+
this.startOpts = startOpts;
|
|
752
|
+
}
|
|
753
|
+
isRunning = false;
|
|
754
|
+
session = null;
|
|
755
|
+
transpiler = null;
|
|
756
|
+
backoffMs = 1e3;
|
|
757
|
+
hadConnectionWarning = false;
|
|
758
|
+
proxyUrl;
|
|
759
|
+
proxy = null;
|
|
760
|
+
// proxyUrl is sent on every poll request so the platform dashboard can
|
|
761
|
+
// show the developer's preview URL. Also included in the start request
|
|
762
|
+
// so the dashboard sees it immediately without waiting for the first poll.
|
|
763
|
+
setProxyUrl(url) {
|
|
764
|
+
this.proxyUrl = url;
|
|
765
|
+
this.startOpts.proxyUrl = url;
|
|
766
|
+
}
|
|
767
|
+
setProxy(proxy) {
|
|
768
|
+
this.proxy = proxy;
|
|
769
|
+
}
|
|
770
|
+
async start() {
|
|
771
|
+
if (this.isRunning) {
|
|
772
|
+
throw new Error("DevRunner is already running");
|
|
773
|
+
}
|
|
774
|
+
log.info("runner Starting session", { appId: this.appId, branch: this.startOpts.branch });
|
|
775
|
+
const session = await startDevSession(this.appId, this.startOpts);
|
|
776
|
+
this.session = session;
|
|
777
|
+
this.transpiler = new Transpiler(this.projectRoot);
|
|
778
|
+
this.isRunning = true;
|
|
779
|
+
this.backoffMs = 1e3;
|
|
780
|
+
log.info("runner Session started", { sessionId: session.sessionId, branch: session.branch });
|
|
781
|
+
this.pollLoop();
|
|
782
|
+
return session;
|
|
783
|
+
}
|
|
784
|
+
async stop() {
|
|
785
|
+
log.info("runner Stopping session");
|
|
786
|
+
this.isRunning = false;
|
|
787
|
+
if (this.session) {
|
|
788
|
+
try {
|
|
789
|
+
await stopDevSession(this.appId, this.session.sessionId);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
log.warn("runner Failed to stop session cleanly", { error: err instanceof Error ? err.message : String(err) });
|
|
792
|
+
}
|
|
793
|
+
this.session = null;
|
|
794
|
+
}
|
|
795
|
+
if (this.transpiler) {
|
|
796
|
+
await this.transpiler.cleanup();
|
|
797
|
+
this.transpiler = null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
getSession() {
|
|
801
|
+
return this.session;
|
|
802
|
+
}
|
|
803
|
+
// Set role override for subsequent method executions.
|
|
804
|
+
async setImpersonation(roles) {
|
|
805
|
+
if (!this.session) return;
|
|
806
|
+
log.info("runner Impersonating", { roles });
|
|
807
|
+
const result = await impersonate(this.appId, this.session.sessionId, roles);
|
|
808
|
+
await this.refreshClientContext();
|
|
809
|
+
devRequestEvents.emitImpersonate({ roles: result.roles });
|
|
810
|
+
}
|
|
811
|
+
// Clear role override — revert to session's default roles.
|
|
812
|
+
async clearImpersonation() {
|
|
813
|
+
if (!this.session) return;
|
|
814
|
+
log.info("runner Clearing impersonation");
|
|
815
|
+
const result = await impersonate(this.appId, this.session.sessionId, null);
|
|
816
|
+
await this.refreshClientContext();
|
|
817
|
+
devRequestEvents.emitImpersonate({ roles: result.roles });
|
|
818
|
+
}
|
|
819
|
+
// Fetch fresh clientContext from platform and update the proxy.
|
|
820
|
+
// Called after impersonation changes so the browser gets a new ms_iface token.
|
|
821
|
+
async refreshClientContext() {
|
|
822
|
+
if (!this.session || !this.proxy) return;
|
|
823
|
+
try {
|
|
824
|
+
const context = await refreshContext(this.appId, this.session.sessionId);
|
|
825
|
+
this.session.clientContext = context;
|
|
826
|
+
this.proxy.updateClientContext(context);
|
|
827
|
+
} catch (err) {
|
|
828
|
+
log.warn("runner Failed to refresh client context", { error: err instanceof Error ? err.message : String(err) });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Run a scenario: truncate tables → execute seed → impersonate roles.
|
|
832
|
+
// Called directly (not via poll loop) by the TUI or headless stdin.
|
|
833
|
+
async runScenario(scenario) {
|
|
834
|
+
if (!this.session || !this.transpiler) {
|
|
835
|
+
return { success: false, databases: [], error: "Session not started" };
|
|
836
|
+
}
|
|
837
|
+
const startTime = Date.now();
|
|
838
|
+
const scenarioName = scenario.name ?? scenario.export;
|
|
839
|
+
devRequestEvents.emitScenarioStart({
|
|
840
|
+
id: scenario.id,
|
|
841
|
+
name: scenarioName,
|
|
842
|
+
timestamp: startTime
|
|
843
|
+
});
|
|
844
|
+
log.info("runner Running scenario", { id: scenario.id, name: scenarioName });
|
|
845
|
+
try {
|
|
846
|
+
log.debug("runner Truncating database for scenario");
|
|
847
|
+
const databases = await resetDevDatabase(this.appId, this.session.sessionId, "truncate");
|
|
848
|
+
this.session.databases = databases;
|
|
849
|
+
log.debug("runner Transpiling scenario", { path: scenario.path });
|
|
850
|
+
const transpiledPath = await this.transpiler.transpile(scenario.path);
|
|
851
|
+
log.debug("runner Fetching callback token for scenario");
|
|
852
|
+
const authorizationToken = await fetchCallbackToken(this.appId, this.session.sessionId);
|
|
853
|
+
log.debug("runner Executing scenario seed", { export: scenario.export });
|
|
854
|
+
const result = await executeMethod({
|
|
855
|
+
transpiledPath,
|
|
856
|
+
methodExport: scenario.export,
|
|
857
|
+
input: {},
|
|
858
|
+
auth: this.session.auth,
|
|
859
|
+
databases: this.session.databases,
|
|
860
|
+
authorizationToken,
|
|
861
|
+
apiBaseUrl: getApiBaseUrl(),
|
|
862
|
+
projectRoot: this.projectRoot
|
|
863
|
+
});
|
|
864
|
+
if (!result.success) {
|
|
865
|
+
const error = result.error?.message ?? "Scenario seed failed";
|
|
866
|
+
log.error("runner Scenario seed failed", { id: scenario.id, error });
|
|
867
|
+
devRequestEvents.emitScenarioComplete({
|
|
868
|
+
id: scenario.id,
|
|
869
|
+
success: false,
|
|
870
|
+
duration: Date.now() - startTime,
|
|
871
|
+
roles: scenario.roles,
|
|
872
|
+
error
|
|
873
|
+
});
|
|
874
|
+
return { success: false, databases, error };
|
|
875
|
+
}
|
|
876
|
+
if (scenario.roles.length > 0) {
|
|
877
|
+
log.debug("runner Impersonating for scenario", { roles: scenario.roles });
|
|
878
|
+
await impersonate(this.appId, this.session.sessionId, scenario.roles);
|
|
879
|
+
await this.refreshClientContext();
|
|
880
|
+
}
|
|
881
|
+
const duration = Date.now() - startTime;
|
|
882
|
+
log.info("runner Scenario complete", { id: scenario.id, duration, roles: scenario.roles });
|
|
883
|
+
devRequestEvents.emitScenarioComplete({
|
|
884
|
+
id: scenario.id,
|
|
885
|
+
success: true,
|
|
886
|
+
duration,
|
|
887
|
+
roles: scenario.roles
|
|
888
|
+
});
|
|
889
|
+
return { success: true, databases };
|
|
890
|
+
} catch (err) {
|
|
891
|
+
const error = err instanceof Error ? err.message : "Unknown error";
|
|
892
|
+
log.error("runner Scenario failed", { id: scenario.id, error });
|
|
893
|
+
devRequestEvents.emitScenarioComplete({
|
|
894
|
+
id: scenario.id,
|
|
895
|
+
success: false,
|
|
896
|
+
duration: Date.now() - startTime,
|
|
897
|
+
roles: scenario.roles,
|
|
898
|
+
error
|
|
899
|
+
});
|
|
900
|
+
return { success: false, databases: this.session.databases, error };
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async pollLoop() {
|
|
904
|
+
while (this.isRunning) {
|
|
905
|
+
try {
|
|
906
|
+
const request = await pollDevRequest(
|
|
907
|
+
this.appId,
|
|
908
|
+
this.session.sessionId,
|
|
909
|
+
this.proxyUrl
|
|
910
|
+
);
|
|
911
|
+
if (this.hadConnectionWarning) {
|
|
912
|
+
this.hadConnectionWarning = false;
|
|
913
|
+
log.info("runner Connection restored");
|
|
914
|
+
devRequestEvents.emitConnectionRestored();
|
|
915
|
+
}
|
|
916
|
+
if (request) {
|
|
917
|
+
this.handleRequest(request);
|
|
918
|
+
}
|
|
919
|
+
this.backoffMs = 1e3;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (error instanceof DevPollError && error.statusCode === 404) {
|
|
922
|
+
log.error("runner Session expired (404)");
|
|
923
|
+
devRequestEvents.emitSessionExpired();
|
|
924
|
+
this.isRunning = false;
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if ((error instanceof DevPollError || error instanceof ApiError) && error.statusCode === 401) {
|
|
928
|
+
log.warn("runner Auth token expired (401), attempting refresh");
|
|
929
|
+
const refreshed = await this.refreshAuth();
|
|
930
|
+
if (refreshed) {
|
|
931
|
+
this.backoffMs = 1e3;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
log.error("runner Auth refresh failed, stopping");
|
|
935
|
+
devRequestEvents.emitSessionExpired();
|
|
936
|
+
this.isRunning = false;
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (!this.hadConnectionWarning) {
|
|
940
|
+
this.hadConnectionWarning = true;
|
|
941
|
+
log.warn("runner Connection lost, retrying...");
|
|
942
|
+
devRequestEvents.emitConnectionWarning(
|
|
943
|
+
"Lost connection to platform, retrying..."
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
log.debug("runner Backing off", { ms: this.backoffMs });
|
|
947
|
+
await this.sleep(this.backoffMs);
|
|
948
|
+
this.backoffMs = Math.min(this.backoffMs * 2, 3e4);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async handleRequest(request) {
|
|
953
|
+
const startTime = Date.now();
|
|
954
|
+
devRequestEvents.emitStart({
|
|
955
|
+
id: request.requestId,
|
|
956
|
+
type: request.type,
|
|
957
|
+
method: request.methodExport,
|
|
958
|
+
timestamp: startTime
|
|
959
|
+
});
|
|
960
|
+
log.info("runner Request received", { requestId: request.requestId, method: request.methodExport });
|
|
961
|
+
try {
|
|
962
|
+
log.debug("runner Transpiling", { path: request.methodPath });
|
|
963
|
+
const transpiledPath = await this.transpiler.transpile(request.methodPath);
|
|
964
|
+
const auth = request.roleOverride ? {
|
|
965
|
+
userId: this.session.auth.userId,
|
|
966
|
+
roleAssignments: request.roleOverride.map((roleName) => ({
|
|
967
|
+
userId: this.session.auth.userId,
|
|
968
|
+
roleName
|
|
969
|
+
}))
|
|
970
|
+
} : this.session.auth;
|
|
971
|
+
const result = await executeMethod({
|
|
972
|
+
transpiledPath,
|
|
973
|
+
methodExport: request.methodExport,
|
|
974
|
+
input: request.input,
|
|
975
|
+
auth,
|
|
976
|
+
databases: this.session.databases,
|
|
977
|
+
authorizationToken: request.authorizationToken,
|
|
978
|
+
apiBaseUrl: getApiBaseUrl(),
|
|
979
|
+
projectRoot: this.projectRoot,
|
|
980
|
+
streamId: request.streamId
|
|
981
|
+
});
|
|
982
|
+
const devResult = {
|
|
983
|
+
type: "execute",
|
|
984
|
+
success: result.success,
|
|
985
|
+
output: result.output,
|
|
986
|
+
error: result.error,
|
|
987
|
+
stdout: result.stdout,
|
|
988
|
+
stats: result.stats
|
|
989
|
+
};
|
|
990
|
+
await submitDevResult(
|
|
991
|
+
this.appId,
|
|
992
|
+
this.session.sessionId,
|
|
993
|
+
request.requestId,
|
|
994
|
+
devResult
|
|
995
|
+
);
|
|
996
|
+
const duration = Date.now() - startTime;
|
|
997
|
+
log.info("runner Request complete", { requestId: request.requestId, success: result.success, duration });
|
|
998
|
+
devRequestEvents.emitComplete({
|
|
999
|
+
id: request.requestId,
|
|
1000
|
+
success: result.success,
|
|
1001
|
+
duration,
|
|
1002
|
+
error: result.error ? formatErrorForDisplay(result.error) : void 0
|
|
1003
|
+
});
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1006
|
+
const duration = Date.now() - startTime;
|
|
1007
|
+
log.error("runner Request failed", { requestId: request.requestId, duration, error: message });
|
|
1008
|
+
try {
|
|
1009
|
+
await submitDevResult(
|
|
1010
|
+
this.appId,
|
|
1011
|
+
this.session.sessionId,
|
|
1012
|
+
request.requestId,
|
|
1013
|
+
{
|
|
1014
|
+
type: "execute",
|
|
1015
|
+
success: false,
|
|
1016
|
+
error: { message }
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
} catch (submitErr) {
|
|
1020
|
+
log.error("runner Failed to submit error result", { error: submitErr instanceof Error ? submitErr.message : String(submitErr) });
|
|
1021
|
+
}
|
|
1022
|
+
devRequestEvents.emitComplete({
|
|
1023
|
+
id: request.requestId,
|
|
1024
|
+
success: false,
|
|
1025
|
+
duration: Date.now() - startTime,
|
|
1026
|
+
error: message
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Attempt to refresh expired auth credentials via the device auth flow.
|
|
1032
|
+
* Opens the browser for the user to re-authorize, polls for the new token.
|
|
1033
|
+
* Returns true if refresh succeeded.
|
|
1034
|
+
*/
|
|
1035
|
+
async refreshAuth() {
|
|
1036
|
+
const POLL_INTERVAL = 2e3;
|
|
1037
|
+
const MAX_ATTEMPTS = 30;
|
|
1038
|
+
try {
|
|
1039
|
+
log.info("runner Auth expired, requesting re-authentication");
|
|
1040
|
+
const { url, token } = await requestDeviceAuth();
|
|
1041
|
+
devRequestEvents.emitAuthRefreshStart(url);
|
|
1042
|
+
try {
|
|
1043
|
+
const open = (await import("open")).default;
|
|
1044
|
+
await open(url);
|
|
1045
|
+
} catch {
|
|
1046
|
+
log.warn("runner Could not open browser for auth \u2014 user must visit URL manually");
|
|
1047
|
+
}
|
|
1048
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
1049
|
+
await this.sleep(POLL_INTERVAL);
|
|
1050
|
+
if (!this.isRunning) return false;
|
|
1051
|
+
const result = await pollDeviceAuth(token);
|
|
1052
|
+
if (result.status === "completed" && result.apiKey) {
|
|
1053
|
+
setApiKey(result.apiKey);
|
|
1054
|
+
if (result.userId) {
|
|
1055
|
+
setUserId(result.userId);
|
|
1056
|
+
}
|
|
1057
|
+
log.info("runner Auth refreshed successfully");
|
|
1058
|
+
devRequestEvents.emitAuthRefreshSuccess();
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
if (result.status === "expired") {
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
log.error("runner Auth refresh timed out or was denied");
|
|
1066
|
+
devRequestEvents.emitAuthRefreshFailed();
|
|
1067
|
+
return false;
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
log.error("runner Auth refresh failed", { error: err instanceof Error ? err.message : String(err) });
|
|
1070
|
+
devRequestEvents.emitAuthRefreshFailed();
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
sleep(ms) {
|
|
1075
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
function formatErrorForDisplay(error) {
|
|
1079
|
+
const parts = [];
|
|
1080
|
+
if (error.message) {
|
|
1081
|
+
parts.push(String(error.message));
|
|
1082
|
+
}
|
|
1083
|
+
const code = error.code ?? error.statusCode ?? error.status;
|
|
1084
|
+
if (code !== void 0) {
|
|
1085
|
+
parts.push(`(code: ${code})`);
|
|
1086
|
+
}
|
|
1087
|
+
if (error.body) {
|
|
1088
|
+
parts.push(`Response: ${String(error.body).slice(0, 200)}`);
|
|
1089
|
+
} else if (error.response) {
|
|
1090
|
+
parts.push(`Response: ${String(error.response).slice(0, 200)}`);
|
|
1091
|
+
}
|
|
1092
|
+
if (error.cause && typeof error.cause === "object") {
|
|
1093
|
+
const cause = error.cause;
|
|
1094
|
+
if (cause.message) {
|
|
1095
|
+
parts.push(`Caused by: ${cause.message}`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return parts.join("\n");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/dev/proxy.ts
|
|
1102
|
+
import http from "http";
|
|
1103
|
+
var DevProxy = class {
|
|
1104
|
+
constructor(upstreamPort, clientContext, bindAddress = "127.0.0.1") {
|
|
1105
|
+
this.upstreamPort = upstreamPort;
|
|
1106
|
+
this.clientContext = clientContext;
|
|
1107
|
+
this.bindAddress = bindAddress;
|
|
1108
|
+
}
|
|
1109
|
+
server = null;
|
|
1110
|
+
proxyPort = null;
|
|
1111
|
+
updateClientContext(context) {
|
|
1112
|
+
this.clientContext = context;
|
|
1113
|
+
log.info("proxy Client context updated");
|
|
1114
|
+
}
|
|
1115
|
+
async start(preferredPort) {
|
|
1116
|
+
const server = http.createServer((req, res) => {
|
|
1117
|
+
this.handleRequest(req, res);
|
|
1118
|
+
});
|
|
1119
|
+
server.on("upgrade", (req, socket, head) => {
|
|
1120
|
+
this.handleUpgrade(req, socket, head);
|
|
1121
|
+
});
|
|
1122
|
+
const portsToTry = preferredPort ? [preferredPort, 0] : [0];
|
|
1123
|
+
for (const port of portsToTry) {
|
|
1124
|
+
try {
|
|
1125
|
+
const assignedPort = await this.listenOnPort(server, port);
|
|
1126
|
+
this.server = server;
|
|
1127
|
+
this.proxyPort = assignedPort;
|
|
1128
|
+
log.info("proxy Started", { port: assignedPort, bind: this.bindAddress });
|
|
1129
|
+
return assignedPort;
|
|
1130
|
+
} catch {
|
|
1131
|
+
log.warn("proxy Port in use, trying next", { port });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
throw new Error("Failed to start proxy server");
|
|
1135
|
+
}
|
|
1136
|
+
listenOnPort(server, port) {
|
|
1137
|
+
return new Promise((resolve2, reject) => {
|
|
1138
|
+
const onError = (err) => {
|
|
1139
|
+
server.removeListener("error", onError);
|
|
1140
|
+
reject(err);
|
|
1141
|
+
};
|
|
1142
|
+
server.on("error", onError);
|
|
1143
|
+
server.listen(port, this.bindAddress, () => {
|
|
1144
|
+
server.removeListener("error", onError);
|
|
1145
|
+
const addr = server.address();
|
|
1146
|
+
if (!addr || typeof addr === "string") {
|
|
1147
|
+
reject(new Error("Failed to get proxy server address"));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
resolve2(addr.port);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
stop() {
|
|
1155
|
+
if (this.server) {
|
|
1156
|
+
log.info("proxy Stopping");
|
|
1157
|
+
this.server.close();
|
|
1158
|
+
this.server = null;
|
|
1159
|
+
this.proxyPort = null;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
getPort() {
|
|
1163
|
+
return this.proxyPort;
|
|
1164
|
+
}
|
|
1165
|
+
handleRequest(clientReq, clientRes) {
|
|
1166
|
+
const origin = clientReq.headers.origin;
|
|
1167
|
+
if (clientReq.method === "OPTIONS" && origin) {
|
|
1168
|
+
clientRes.writeHead(204, {
|
|
1169
|
+
"Access-Control-Allow-Origin": origin,
|
|
1170
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
1171
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
1172
|
+
"Access-Control-Allow-Headers": "*"
|
|
1173
|
+
});
|
|
1174
|
+
clientRes.end();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const options = {
|
|
1178
|
+
hostname: "127.0.0.1",
|
|
1179
|
+
port: this.upstreamPort,
|
|
1180
|
+
path: clientReq.url,
|
|
1181
|
+
method: clientReq.method,
|
|
1182
|
+
headers: { ...clientReq.headers, host: `localhost:${this.upstreamPort}` }
|
|
1183
|
+
};
|
|
1184
|
+
const upstreamReq = http.request(options, (upstreamRes) => {
|
|
1185
|
+
const contentType = upstreamRes.headers["content-type"] ?? "";
|
|
1186
|
+
const isHtml = contentType.startsWith("text/html");
|
|
1187
|
+
if (isHtml) {
|
|
1188
|
+
const chunks = [];
|
|
1189
|
+
upstreamRes.on("data", (chunk) => chunks.push(chunk));
|
|
1190
|
+
upstreamRes.on("end", () => {
|
|
1191
|
+
let html = Buffer.concat(chunks).toString("utf-8");
|
|
1192
|
+
html = injectClientContext(html, this.clientContext);
|
|
1193
|
+
const headers = { ...upstreamRes.headers };
|
|
1194
|
+
headers["content-length"] = String(Buffer.byteLength(html, "utf-8"));
|
|
1195
|
+
headers["cache-control"] = "no-store, no-cache, must-revalidate";
|
|
1196
|
+
delete headers["content-encoding"];
|
|
1197
|
+
delete headers["etag"];
|
|
1198
|
+
if (origin) {
|
|
1199
|
+
headers["access-control-allow-origin"] = origin;
|
|
1200
|
+
headers["access-control-allow-private-network"] = "true";
|
|
1201
|
+
}
|
|
1202
|
+
log.debug("proxy HTML injected", { path: clientReq.url, size: html.length });
|
|
1203
|
+
clientRes.writeHead(upstreamRes.statusCode ?? 200, headers);
|
|
1204
|
+
clientRes.end(html);
|
|
1205
|
+
});
|
|
1206
|
+
} else {
|
|
1207
|
+
const headers = { ...upstreamRes.headers };
|
|
1208
|
+
headers["cache-control"] = "no-store, no-cache, must-revalidate";
|
|
1209
|
+
delete headers["etag"];
|
|
1210
|
+
if (origin) {
|
|
1211
|
+
headers["access-control-allow-origin"] = origin;
|
|
1212
|
+
headers["access-control-allow-private-network"] = "true";
|
|
1213
|
+
}
|
|
1214
|
+
clientRes.writeHead(upstreamRes.statusCode ?? 200, headers);
|
|
1215
|
+
upstreamRes.pipe(clientRes);
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
upstreamReq.on("error", (err) => {
|
|
1219
|
+
log.warn("proxy Upstream error", { path: clientReq.url, error: err.message });
|
|
1220
|
+
clientRes.writeHead(502);
|
|
1221
|
+
clientRes.end(`Proxy error: ${err.message}`);
|
|
1222
|
+
});
|
|
1223
|
+
clientReq.pipe(upstreamReq);
|
|
1224
|
+
}
|
|
1225
|
+
handleUpgrade(clientReq, clientSocket, head) {
|
|
1226
|
+
log.debug("proxy WebSocket upgrade", { path: clientReq.url });
|
|
1227
|
+
const options = {
|
|
1228
|
+
hostname: "127.0.0.1",
|
|
1229
|
+
port: this.upstreamPort,
|
|
1230
|
+
path: clientReq.url,
|
|
1231
|
+
method: clientReq.method,
|
|
1232
|
+
headers: { ...clientReq.headers, host: `localhost:${this.upstreamPort}` }
|
|
1233
|
+
};
|
|
1234
|
+
const upstreamReq = http.request(options);
|
|
1235
|
+
upstreamReq.on("upgrade", (upstreamRes, upstreamSocket, upgradeHead) => {
|
|
1236
|
+
let responseHead = `HTTP/${upstreamRes.httpVersion} ${upstreamRes.statusCode} ${upstreamRes.statusMessage}\r
|
|
1237
|
+
`;
|
|
1238
|
+
for (let i = 0; i < upstreamRes.rawHeaders.length; i += 2) {
|
|
1239
|
+
responseHead += `${upstreamRes.rawHeaders[i]}: ${upstreamRes.rawHeaders[i + 1]}\r
|
|
1240
|
+
`;
|
|
1241
|
+
}
|
|
1242
|
+
responseHead += "\r\n";
|
|
1243
|
+
clientSocket.write(responseHead);
|
|
1244
|
+
if (upgradeHead.length > 0) {
|
|
1245
|
+
clientSocket.write(upgradeHead);
|
|
1246
|
+
}
|
|
1247
|
+
if (head.length > 0) {
|
|
1248
|
+
upstreamSocket.write(head);
|
|
1249
|
+
}
|
|
1250
|
+
upstreamSocket.pipe(clientSocket);
|
|
1251
|
+
clientSocket.pipe(upstreamSocket);
|
|
1252
|
+
clientSocket.on("close", () => upstreamSocket.destroy());
|
|
1253
|
+
upstreamSocket.on("close", () => clientSocket.destroy());
|
|
1254
|
+
clientSocket.on("error", () => upstreamSocket.destroy());
|
|
1255
|
+
upstreamSocket.on("error", () => clientSocket.destroy());
|
|
1256
|
+
});
|
|
1257
|
+
upstreamReq.on("error", () => {
|
|
1258
|
+
clientSocket.destroy();
|
|
1259
|
+
});
|
|
1260
|
+
upstreamReq.end();
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
function injectClientContext(html, context) {
|
|
1264
|
+
const script = `<script>window.__MINDSTUDIO__=${JSON.stringify(context)};</script>`;
|
|
1265
|
+
if (html.includes("</head>")) {
|
|
1266
|
+
return html.replace("</head>", `${script}
|
|
1267
|
+
</head>`);
|
|
1268
|
+
}
|
|
1269
|
+
return script + "\n" + html;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/dev/app-config.ts
|
|
1273
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
1274
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
1275
|
+
function detectAppConfig(cwd = process.cwd()) {
|
|
1276
|
+
const appJsonPath = join3(cwd, "mindstudio.json");
|
|
1277
|
+
if (!existsSync2(appJsonPath)) {
|
|
1278
|
+
log.debug("config mindstudio.json not found", { path: appJsonPath });
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
const raw = readFileSync(appJsonPath, "utf-8");
|
|
1283
|
+
const parsed = JSON.parse(raw);
|
|
1284
|
+
if (!parsed.name || !Array.isArray(parsed.methods)) {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
const config2 = {
|
|
1288
|
+
appId: parsed.appId,
|
|
1289
|
+
name: parsed.name,
|
|
1290
|
+
description: parsed.description,
|
|
1291
|
+
roles: parsed.roles ?? [],
|
|
1292
|
+
tables: parsed.tables ?? [],
|
|
1293
|
+
methods: parsed.methods,
|
|
1294
|
+
scenarios: parsed.scenarios ?? [],
|
|
1295
|
+
interfaces: parsed.interfaces ?? []
|
|
1296
|
+
};
|
|
1297
|
+
log.debug("config Detected mindstudio.json", {
|
|
1298
|
+
appId: config2.appId,
|
|
1299
|
+
roles: config2.roles.length,
|
|
1300
|
+
methods: config2.methods.length,
|
|
1301
|
+
tables: config2.tables.length,
|
|
1302
|
+
scenarios: config2.scenarios.length,
|
|
1303
|
+
interfaces: config2.interfaces.length
|
|
1304
|
+
});
|
|
1305
|
+
return config2;
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
log.warn("config Failed to parse mindstudio.json", { error: err instanceof Error ? err.message : String(err) });
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function getWebInterfaceConfig(appConfig, cwd = process.cwd()) {
|
|
1312
|
+
const webInterface = appConfig.interfaces.find(
|
|
1313
|
+
(i) => i.type === "web" && i.enabled !== false
|
|
1314
|
+
);
|
|
1315
|
+
if (!webInterface) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
const configPath = join3(cwd, webInterface.path);
|
|
1319
|
+
if (!existsSync2(configPath)) {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
1324
|
+
const parsed = JSON.parse(raw);
|
|
1325
|
+
const web = parsed.web;
|
|
1326
|
+
if (!web || typeof web !== "object") {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
devPort: typeof web.devPort === "number" ? web.devPort : void 0,
|
|
1331
|
+
devCommand: typeof web.devCommand === "string" ? web.devCommand : void 0
|
|
1332
|
+
};
|
|
1333
|
+
} catch {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function getWebProjectDir(appConfig, cwd = process.cwd()) {
|
|
1338
|
+
const webInterface = appConfig.interfaces.find(
|
|
1339
|
+
(i) => i.type === "web" && i.enabled !== false
|
|
1340
|
+
);
|
|
1341
|
+
if (!webInterface) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
return dirname2(join3(cwd, webInterface.path));
|
|
1345
|
+
}
|
|
1346
|
+
function readTableSources(appConfig, cwd = process.cwd()) {
|
|
1347
|
+
const results = [];
|
|
1348
|
+
for (const table of appConfig.tables) {
|
|
1349
|
+
const filePath = join3(cwd, table.path);
|
|
1350
|
+
if (!existsSync2(filePath)) {
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
const source = readFileSync(filePath, "utf-8");
|
|
1355
|
+
const name = table.export;
|
|
1356
|
+
results.push({ name, source });
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return results;
|
|
1361
|
+
}
|
|
1362
|
+
function findDirsNeedingInstall(appConfig, cwd = process.cwd()) {
|
|
1363
|
+
const dirs = [];
|
|
1364
|
+
if (appConfig.methods.length > 0) {
|
|
1365
|
+
const firstMethodPath = appConfig.methods[0].path;
|
|
1366
|
+
const parts = firstMethodPath.split("/");
|
|
1367
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
1368
|
+
const candidate = join3(cwd, ...parts.slice(0, i));
|
|
1369
|
+
if (existsSync2(join3(candidate, "package.json"))) {
|
|
1370
|
+
if (!existsSync2(join3(candidate, "node_modules"))) {
|
|
1371
|
+
dirs.push(candidate);
|
|
1372
|
+
}
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const webProjectDir = getWebProjectDir(appConfig, cwd);
|
|
1378
|
+
if (webProjectDir && existsSync2(join3(webProjectDir, "package.json"))) {
|
|
1379
|
+
if (!existsSync2(join3(webProjectDir, "node_modules"))) {
|
|
1380
|
+
dirs.push(webProjectDir);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return dirs;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/dev/utils.ts
|
|
1387
|
+
import { execSync } from "child_process";
|
|
1388
|
+
function stablePort(appId) {
|
|
1389
|
+
let hash = 0;
|
|
1390
|
+
for (let i = 0; i < appId.length; i++) {
|
|
1391
|
+
hash = (hash << 5) - hash + appId.charCodeAt(i) | 0;
|
|
1392
|
+
}
|
|
1393
|
+
return 3100 + Math.abs(hash) % 900;
|
|
1394
|
+
}
|
|
1395
|
+
function detectGitBranch() {
|
|
1396
|
+
try {
|
|
1397
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1398
|
+
encoding: "utf-8",
|
|
1399
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1400
|
+
}).trim() || void 0;
|
|
1401
|
+
} catch {
|
|
1402
|
+
return void 0;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/dev/table-watcher.ts
|
|
1407
|
+
import { watch } from "fs";
|
|
1408
|
+
import { join as join4, dirname as dirname3, basename as basename2 } from "path";
|
|
1409
|
+
function watchTableFiles(tables, cwd, onChanged) {
|
|
1410
|
+
if (tables.length === 0) return () => {
|
|
1411
|
+
};
|
|
1412
|
+
const dirToFiles = /* @__PURE__ */ new Map();
|
|
1413
|
+
for (const table of tables) {
|
|
1414
|
+
const absPath = join4(cwd, table.path);
|
|
1415
|
+
const dir = dirname3(absPath);
|
|
1416
|
+
const file = basename2(absPath);
|
|
1417
|
+
if (!dirToFiles.has(dir)) dirToFiles.set(dir, /* @__PURE__ */ new Set());
|
|
1418
|
+
dirToFiles.get(dir).add(file);
|
|
1419
|
+
}
|
|
1420
|
+
let syncTimer;
|
|
1421
|
+
const cleanups = [];
|
|
1422
|
+
cleanups.push(() => clearTimeout(syncTimer));
|
|
1423
|
+
for (const [dir, expectedFiles] of dirToFiles) {
|
|
1424
|
+
try {
|
|
1425
|
+
const w = watch(dir, (_eventType, filename) => {
|
|
1426
|
+
if (filename && !expectedFiles.has(filename)) return;
|
|
1427
|
+
clearTimeout(syncTimer);
|
|
1428
|
+
syncTimer = setTimeout(onChanged, 500);
|
|
1429
|
+
});
|
|
1430
|
+
cleanups.push(() => w.close());
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
log.info("table-watcher Watching directories", {
|
|
1435
|
+
dirs: dirToFiles.size,
|
|
1436
|
+
tables: tables.length
|
|
1437
|
+
});
|
|
1438
|
+
return () => {
|
|
1439
|
+
for (const cleanup of cleanups) cleanup();
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
export {
|
|
1444
|
+
getEnvironment,
|
|
1445
|
+
getApiKey,
|
|
1446
|
+
setApiKey,
|
|
1447
|
+
getUserId,
|
|
1448
|
+
setUserId,
|
|
1449
|
+
getApiBaseUrl,
|
|
1450
|
+
getConfigPath,
|
|
1451
|
+
getProviderBaseUrl,
|
|
1452
|
+
setProviderBaseUrl,
|
|
1453
|
+
getProviderInstallPath,
|
|
1454
|
+
setProviderInstallPath,
|
|
1455
|
+
getLocalInterfacesDir,
|
|
1456
|
+
getLocalInterfacePath,
|
|
1457
|
+
setLocalInterfacePath,
|
|
1458
|
+
deleteLocalInterfacePath,
|
|
1459
|
+
log,
|
|
1460
|
+
initLoggerHeadless,
|
|
1461
|
+
initLoggerInteractive,
|
|
1462
|
+
syncSchema,
|
|
1463
|
+
devRequestEvents,
|
|
1464
|
+
pollForRequest,
|
|
1465
|
+
submitProgress,
|
|
1466
|
+
submitResult,
|
|
1467
|
+
verifyApiKey,
|
|
1468
|
+
syncModels,
|
|
1469
|
+
getSyncedModels,
|
|
1470
|
+
requestDeviceAuth,
|
|
1471
|
+
pollDeviceAuth,
|
|
1472
|
+
getEditorSessions,
|
|
1473
|
+
disconnectHeartbeat,
|
|
1474
|
+
DevRunner,
|
|
1475
|
+
DevProxy,
|
|
1476
|
+
detectAppConfig,
|
|
1477
|
+
getWebInterfaceConfig,
|
|
1478
|
+
getWebProjectDir,
|
|
1479
|
+
readTableSources,
|
|
1480
|
+
findDirsNeedingInstall,
|
|
1481
|
+
stablePort,
|
|
1482
|
+
detectGitBranch,
|
|
1483
|
+
watchTableFiles
|
|
1484
|
+
};
|
|
1485
|
+
//# sourceMappingURL=chunk-C3JPRLSS.js.map
|