@easytwin/devkit 0.1.1 → 0.1.3

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/bin.js CHANGED
@@ -7,14 +7,14 @@ import { readFile } from "fs/promises";
7
7
  import { createInterface } from "readline/promises";
8
8
  import { stdin, stdout } from "process";
9
9
  import { pathToFileURL } from "url";
10
- import path6 from "path";
10
+ import path10 from "path";
11
11
 
12
12
  // src/config.ts
13
13
  import { promises as fs } from "fs";
14
14
  import path from "path";
15
15
  var CONFIG_FILE_NAME = "easytwin.config.json";
16
16
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
17
- var TEST_BASE_URL = "http://saas-twin-test.k8s.dtstack.cn/";
17
+ var TEST_BASE_URL = "http://172.16.125.3:10100/";
18
18
  var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
19
19
  var MOCK_APP_ID = "test";
20
20
  var MOCK_APP_SECRET = "test";
@@ -63,8 +63,40 @@ function validateConfigShape(value) {
63
63
  if (v.env === "prod" || v.env === "test") config.env = v.env;
64
64
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
65
65
  if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
66
+ const scenes = parseConfigScenes(v.scenes);
67
+ if (scenes) config.scenes = scenes;
66
68
  return config;
67
69
  }
70
+ function parseConfigScenes(value) {
71
+ if (value === void 0) return void 0;
72
+ if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
73
+ return value.map((item, i) => {
74
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
75
+ throw new ConfigError(`scenes[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
76
+ }
77
+ const it = item;
78
+ if (typeof it.id !== "string" || it.id.length === 0) {
79
+ throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 id`);
80
+ }
81
+ if (typeof it.name !== "string" || it.name.length === 0) {
82
+ throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 name`);
83
+ }
84
+ const scene = { id: it.id, name: it.name };
85
+ if (it.linkedSceneId !== void 0) {
86
+ if (typeof it.linkedSceneId !== "string") throw new ConfigError(`scenes[${i}].linkedSceneId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
87
+ if (it.linkedSceneId.length > 0) scene.linkedSceneId = it.linkedSceneId;
88
+ }
89
+ if (it.snapshotUrl !== void 0) {
90
+ if (typeof it.snapshotUrl !== "string") throw new ConfigError(`scenes[${i}].snapshotUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
91
+ if (it.snapshotUrl.length > 0) scene.snapshotUrl = it.snapshotUrl;
92
+ }
93
+ if (it.defaultLoading !== void 0) {
94
+ if (typeof it.defaultLoading !== "boolean") throw new ConfigError(`scenes[${i}].defaultLoading \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
95
+ scene.defaultLoading = it.defaultLoading;
96
+ }
97
+ return scene;
98
+ });
99
+ }
68
100
  async function readConfigFile(cwd) {
69
101
  const file = configFilePath(cwd);
70
102
  let raw;
@@ -87,7 +119,8 @@ function resolveConfig(file, env = process.env) {
87
119
  return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
88
120
  }
89
121
  async function loadConfig(cwd, env = process.env) {
90
- return resolveConfig(await readConfigFile(cwd), env);
122
+ const file = await readConfigFile(cwd);
123
+ return resolveConfig(file, env);
91
124
  }
92
125
  async function writeConfigFile(cwd, config) {
93
126
  const file = configFilePath(cwd);
@@ -95,6 +128,7 @@ async function writeConfigFile(cwd, config) {
95
128
  if (config.env) body.env = config.env;
96
129
  if (config.baseUrl) body.baseUrl = config.baseUrl;
97
130
  if (config.ossUrl) body.ossUrl = config.ossUrl;
131
+ if (config.scenes !== void 0) body.scenes = config.scenes;
98
132
  await fs.mkdir(cwd, { recursive: true });
99
133
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
100
134
  return file;
@@ -122,18 +156,29 @@ async function initConfig(input, cwd) {
122
156
  const gitignore = await appendGitignore(cwd);
123
157
  return { configFile, gitignore };
124
158
  }
159
+ async function writeConfigScenes(cwd, scenes) {
160
+ const file = await readConfigFile(cwd);
161
+ await writeConfigFile(cwd, { ...file, scenes });
162
+ }
125
163
 
126
164
  // src/client.ts
127
165
  import http from "http";
128
166
  import https from "https";
129
167
  import { URL as URL2 } from "url";
130
- var AUTH_HEADER = "Authorization";
131
- var BEARER_PREFIX = "Bearer";
132
- var APP_ID_HEADER = "X-Easytwin-App-Id";
168
+ var APP_ID_HEADER = "x-app-id";
169
+ var AUTH_HEADER = "x-app-secret";
170
+ function enc(id) {
171
+ return encodeURIComponent(id);
172
+ }
133
173
  var ENDPOINTS = {
134
- scenes: "/api/scenes",
135
- scene: (id) => `/api/scenes/${encodeURIComponent(id)}`,
136
- upload: "/api/upload"
174
+ /** POST 已关联场景列表(分享路径,空 body)。 */
175
+ linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
176
+ /** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
177
+ workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
178
+ /** POST 一次推送 create/update/delete(分享路径)。 */
179
+ workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
180
+ /** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
181
+ workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
137
182
  };
138
183
  var EasyTwinApiError = class extends Error {
139
184
  status;
@@ -161,28 +206,40 @@ function parseResponseBody(buffer) {
161
206
  return text;
162
207
  }
163
208
  }
209
+ function isEnvelope(value) {
210
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "success" in value;
211
+ }
212
+ function unwrapBody(parsed, status) {
213
+ if (!isEnvelope(parsed)) return parsed;
214
+ if (parsed.success === false) {
215
+ throw new EasyTwinApiError(status, messageFromBody(parsed) ?? "\u8BF7\u6C42\u5931\u8D25", parsed);
216
+ }
217
+ if (parsed.success === true && "data" in parsed) return parsed.data;
218
+ return parsed;
219
+ }
164
220
  var EasyTwinClient = class {
165
221
  baseUrl;
166
- /** 本地测试模式:true scene/upload 走本地 mock,不发网络请求(见 config.ts)。 */
167
- mock;
222
+ /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
223
+ ossUrl;
224
+ /** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
168
225
  appId;
226
+ /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
227
+ mock;
169
228
  appSecret;
170
229
  constructor(config) {
171
230
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
231
+ this.ossUrl = config.ossUrl.replace(/\/+$/, "");
172
232
  this.mock = config.mock;
173
233
  this.appId = config.appId;
174
234
  this.appSecret = config.appSecret;
175
235
  }
176
236
  /** 全仓唯一认证头注入点。 */
177
237
  authHeaders() {
178
- return {
179
- [AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
180
- [APP_ID_HEADER]: this.appId
181
- };
238
+ return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
182
239
  }
183
240
  /** JSON 请求(原生 fetch)。 */
184
- async request(path7, options = {}) {
185
- const url = `${this.baseUrl}${path7}`;
241
+ async request(path11, options = {}) {
242
+ const url = `${this.baseUrl}${path11}`;
186
243
  const headers = { ...this.authHeaders(), ...options.headers };
187
244
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
188
245
  headers["Content-Type"] = "application/json";
@@ -204,8 +261,8 @@ var EasyTwinClient = class {
204
261
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
205
262
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
206
263
  */
207
- async upload(path7, options) {
208
- const url = new URL2(`${this.baseUrl}${path7}`);
264
+ async upload(path11, options) {
265
+ const url = new URL2(`${this.baseUrl}${path11}`);
209
266
  const mod = url.protocol === "https:" ? https : http;
210
267
  const headers = {
211
268
  ...this.authHeaders(),
@@ -238,8 +295,8 @@ var EasyTwinClient = class {
238
295
  return this.handleStatus(raw.status, raw.body);
239
296
  }
240
297
  handleStatus(status, body) {
241
- if (status >= 200 && status < 300) return parseResponseBody(body);
242
298
  const parsed = parseResponseBody(body);
299
+ if (status >= 200 && status < 300) return unwrapBody(parsed, status);
243
300
  throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
244
301
  }
245
302
  };
@@ -248,17 +305,89 @@ var EasyTwinClient = class {
248
305
  import { promises as fs2 } from "fs";
249
306
  import path2 from "path";
250
307
  import { fileURLToPath } from "url";
251
- function normalizeSceneList(data) {
252
- const list = Array.isArray(data) ? data : data?.list;
253
- if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
308
+ function asRecord(value) {
309
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
310
+ }
311
+ function asString(value) {
312
+ return typeof value === "string" ? value : "";
313
+ }
314
+ function asBoolean(value) {
315
+ return value === true;
316
+ }
317
+ function extractSceneArray(data) {
318
+ if (Array.isArray(data)) return data;
319
+ const root = asRecord(data);
320
+ if (Array.isArray(root.data)) return root.data;
321
+ if (Array.isArray(root.list)) return root.list;
322
+ if (Array.isArray(root.scenes)) return root.scenes;
323
+ const nested = asRecord(root.data);
324
+ if (Array.isArray(nested.scenes)) return nested.scenes;
325
+ if (Array.isArray(nested.list)) return nested.list;
326
+ throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
327
+ }
328
+ function normalizeLinkedScenes(data) {
329
+ const list = extractSceneArray(data);
254
330
  return list.map((item) => {
255
- const it = item ?? {};
256
- return { id: String(it.id ?? ""), name: String(it.name ?? it.id ?? "") };
331
+ const it = asRecord(item);
332
+ const sceneKey = asString(it.sceneKey);
333
+ const sourceProjectName = asString(it.sourceProjectName);
334
+ const sourceSceneName = asString(it.sourceSceneName);
335
+ return {
336
+ id: asString(it.id),
337
+ sceneKey,
338
+ name: sourceProjectName || sourceSceneName || sceneKey,
339
+ defaultLoading: asBoolean(it.defaultLoading),
340
+ sourceSceneId: asString(it.sourceSceneId),
341
+ sourceProjectId: asString(it.sourceProjectId),
342
+ sourceSceneName,
343
+ sourceProjectName,
344
+ sourceLost: asBoolean(it.sourceLost),
345
+ snapshotUrl: asString(it.snapshotUrl),
346
+ snapshotUpdatedAt: asString(it.snapshotUpdatedAt)
347
+ };
257
348
  });
258
349
  }
259
- function normalizeSceneDetail(data) {
260
- const it = data ?? {};
261
- return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: data };
350
+ function normalizeSceneList(data) {
351
+ return normalizeLinkedScenes(data).map((s) => ({
352
+ id: s.sceneKey,
353
+ name: s.name,
354
+ linkedSceneId: s.id,
355
+ snapshotUrl: s.snapshotUrl,
356
+ defaultLoading: s.defaultLoading
357
+ }));
358
+ }
359
+ function toConfigScenes(scenes) {
360
+ return scenes.map((s) => {
361
+ const item = { id: s.id, name: s.name };
362
+ if (s.linkedSceneId) item.linkedSceneId = s.linkedSceneId;
363
+ if (s.snapshotUrl) item.snapshotUrl = s.snapshotUrl;
364
+ if (s.defaultLoading !== void 0) item.defaultLoading = s.defaultLoading;
365
+ return item;
366
+ });
367
+ }
368
+ function resolveSnapshotUrl(ossUrl, snapshotUrl) {
369
+ if (/^https?:\/\//i.test(snapshotUrl)) return snapshotUrl;
370
+ const base = ossUrl.replace(/\/+$/, "");
371
+ const rel = snapshotUrl.replace(/^\/+/, "");
372
+ return `${base}/${rel}`;
373
+ }
374
+ async function fetchSnapshotJson(ossUrl, snapshotUrl) {
375
+ if (snapshotUrl.length === 0) throw new Error("\u573A\u666F\u5FEB\u7167\u5730\u5740\u4E3A\u7A7A");
376
+ const url = resolveSnapshotUrl(ossUrl, snapshotUrl);
377
+ let res;
378
+ try {
379
+ res = await fetch(url);
380
+ } catch (err) {
381
+ throw new EasyTwinApiError(0, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:${url}:${err instanceof Error ? err.message : String(err)}`);
382
+ }
383
+ if (!res.ok) {
384
+ throw new EasyTwinApiError(res.status, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:HTTP ${res.status} ${url}`);
385
+ }
386
+ try {
387
+ return await res.json();
388
+ } catch {
389
+ throw new Error(`\u573A\u666F\u5FEB\u7167\u4E0D\u662F\u5408\u6CD5 JSON:${url}`);
390
+ }
262
391
  }
263
392
  var EXAMPLE_SCENE_FILE = "scene.example.json";
264
393
  var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
@@ -290,22 +419,34 @@ function deriveExampleSceneId(payload) {
290
419
  async function exampleSceneSummary(exampleFile) {
291
420
  return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
292
421
  }
422
+ async function fetchLinkedScenes(client) {
423
+ return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
424
+ }
293
425
  async function listScenes(client, options = {}) {
294
- if (client.mock) return [await exampleSceneSummary(options.exampleFile)];
295
- const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
296
- return normalizeSceneList(data);
426
+ const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
427
+ if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
428
+ return scenes;
297
429
  }
298
430
  async function pullScene(client, id, options = {}) {
299
431
  if (client.mock) {
300
- const payload = await loadExampleScene(options.exampleFile);
301
- const sceneId = deriveExampleSceneId(payload);
432
+ const payload2 = await loadExampleScene(options.exampleFile);
433
+ const sceneId = deriveExampleSceneId(payload2);
302
434
  if (sceneId !== id) {
303
435
  throw new Error(`\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F:\u793A\u4F8B\u573A\u666F id \u4E3A ${sceneId},\u6536\u5230 ${id}(\u672C\u5730\u4EC5\u63D0\u4F9B ${EXAMPLE_SCENE_FILE} \u8FD9\u4E00\u4E2A\u573A\u666F)`);
304
436
  }
305
- return { id: sceneId, name: MOCK_SCENE_NAME, payload };
437
+ return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
438
+ }
439
+ const data = await fetchLinkedScenes(client);
440
+ const scenes = normalizeLinkedScenes(data);
441
+ const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
442
+ if (!hit) {
443
+ const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
444
+ throw new Error(
445
+ available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
446
+ );
306
447
  }
307
- const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
308
- return normalizeSceneDetail(data);
448
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
449
+ return { id: hit.sceneKey, name: hit.name, payload };
309
450
  }
310
451
  async function saveSceneJson(scene, out) {
311
452
  await fs2.mkdir(path2.dirname(out), { recursive: true });
@@ -316,7 +457,43 @@ async function saveSceneJson(scene, out) {
316
457
  // src/upload.ts
317
458
  import { promises as fs3 } from "fs";
318
459
  import path3 from "path";
319
- var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".git"]);
460
+ var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
461
+ ".git",
462
+ "node_modules",
463
+ "dist",
464
+ ".easytwin",
465
+ ".cursor",
466
+ ".claude",
467
+ ".qoder",
468
+ ".vscode"
469
+ ]);
470
+ var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
471
+ function isTsconfigFile(base) {
472
+ return /^tsconfig(\..+)?\.json$/i.test(base);
473
+ }
474
+ var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
475
+ function isWorkspaceSpecFile(relPath) {
476
+ const base = normalizePath(relPath).split("/").pop() ?? "";
477
+ return /\.spec\.ts$/i.test(base);
478
+ }
479
+ function isIgnoredWorkspacePath(relPath) {
480
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
481
+ if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
482
+ const base = parts[parts.length - 1];
483
+ if (base === void 0) return false;
484
+ if (WORKSPACE_IGNORED_FILES.has(base)) return true;
485
+ if (isTsconfigFile(base)) return true;
486
+ return base.toLowerCase().endsWith(".scene.json");
487
+ }
488
+ function defaultWorkspaceIgnore(relPath) {
489
+ return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
490
+ }
491
+ function combineUploadIgnore(extra) {
492
+ return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
493
+ }
494
+ async function collectWorkspaceCodeFiles(dir, ignore) {
495
+ return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
496
+ }
320
497
  async function collectFiles(dir, ignore = () => false) {
321
498
  const files = [];
322
499
  async function walk(current, rel) {
@@ -325,7 +502,7 @@ async function collectFiles(dir, ignore = () => false) {
325
502
  const relPath = path3.posix.join(rel, entry.name);
326
503
  if (ignore(relPath)) continue;
327
504
  if (entry.isDirectory()) {
328
- if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
505
+ if (entry.name === ".git") continue;
329
506
  await walk(path3.join(current, entry.name), relPath);
330
507
  } else if (entry.isFile()) {
331
508
  const absPath = path3.join(current, entry.name);
@@ -338,67 +515,344 @@ async function collectFiles(dir, ignore = () => false) {
338
515
  files.sort((a, b) => a.relPath.localeCompare(b.relPath));
339
516
  return files;
340
517
  }
341
- function buildMultipartBody(boundary, fields, file) {
342
- const parts = [];
343
- for (const [key, value] of Object.entries(fields)) {
344
- parts.push(
345
- Buffer.from(`--${boundary}\r
346
- Content-Disposition: form-data; name="${key}"\r
347
- \r
348
- ${value}\r
349
- `, "utf8")
350
- );
518
+ function asRecord2(value) {
519
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
520
+ }
521
+ function asString2(value) {
522
+ return typeof value === "string" ? value : "";
523
+ }
524
+ function normalizePath(relPath) {
525
+ return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
526
+ }
527
+ function normalizeWorkspaceFiles(data) {
528
+ if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
529
+ return data.map((item) => {
530
+ const it = asRecord2(item);
531
+ return { id: asString2(it.id), filePath: asString2(it.filePath), content: asString2(it.content) };
532
+ });
533
+ }
534
+ function extensionOf(relPath) {
535
+ const base = relPath.split("/").pop() ?? "";
536
+ const i = base.lastIndexOf(".");
537
+ if (i <= 0) return "";
538
+ return base.slice(i).toLowerCase();
539
+ }
540
+ function allowedExtensionSet(list) {
541
+ return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
542
+ }
543
+ var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
544
+ function isWorkspaceCodeFile(relPath) {
545
+ return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
546
+ }
547
+ function countsFromPlan(plan) {
548
+ return {
549
+ created: plan.creates.length,
550
+ updated: plan.updates.length,
551
+ deleted: plan.deletes.length,
552
+ unchanged: plan.unchanged.length
553
+ };
554
+ }
555
+ function planWorkspaceUpload(local, remote) {
556
+ const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
557
+ const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
558
+ const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
559
+ const creates = [];
560
+ const updates = [];
561
+ const unchanged = [];
562
+ for (const item of local) {
563
+ const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
564
+ if (!remoteFile) creates.push(item);
565
+ else if (remoteFile.content !== item.content) {
566
+ updates.push({ file: item.file, id: remoteFile.id, content: item.content });
567
+ } else {
568
+ unchanged.push(item.file);
569
+ }
351
570
  }
352
- parts.push(
353
- Buffer.from(
354
- `--${boundary}\r
355
- Content-Disposition: form-data; name="file"; filename="${file.name}"\r
356
- Content-Type: application/octet-stream\r
357
- \r
358
- `,
359
- "utf8"
360
- )
361
- );
362
- parts.push(file.content);
363
- parts.push(Buffer.from(`\r
364
- --${boundary}--\r
365
- `, "utf8"));
366
- return Buffer.concat(parts);
571
+ return { creates, updates, deletes, unchanged };
572
+ }
573
+ function formatUploadResult(result) {
574
+ const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
575
+ return `${prefix}\u672C\u5730 ${result.fileCount} \u4E2A\u6587\u4EF6,\u5171 ${result.byteCount} bytes;\u65B0\u589E ${result.created},\u66F4\u65B0 ${result.updated},\u5220\u9664 ${result.deleted},\u672A\u53D8 ${result.unchanged}`;
367
576
  }
368
577
  async function mockUpload(dir, options) {
369
- const ignore = options.ignore ?? (() => false);
370
- const files = await collectFiles(dir, ignore);
578
+ const ignore = combineUploadIgnore(options.ignore);
579
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
371
580
  const total = files.reduce((sum, f) => sum + f.size, 0);
372
581
  options.onProgress?.({ phase: "collect", current: total, total });
373
582
  options.onProgress?.({ phase: "upload", current: total, total });
374
- return { fileCount: files.length, byteCount: total, mock: true };
583
+ return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
584
+ }
585
+ function buildWorkspacePushBody(plan) {
586
+ const body = {};
587
+ if (plan.creates.length > 0) {
588
+ body.create = plan.creates.map((c) => ({
589
+ filePath: normalizePath(c.file.relPath),
590
+ content: c.content
591
+ }));
592
+ }
593
+ if (plan.updates.length > 0) {
594
+ body.update = plan.updates.map((p) => ({
595
+ id: p.id,
596
+ content: p.content,
597
+ filePath: normalizePath(p.file.relPath)
598
+ }));
599
+ }
600
+ if (plan.deletes.length > 0) {
601
+ body.delete = plan.deletes.map((d) => d.id);
602
+ }
603
+ return Object.keys(body).length > 0 ? body : null;
375
604
  }
376
605
  async function uploadDirectory(client, dir, options = {}) {
377
606
  if (client.mock) return mockUpload(dir, options);
378
- const ignore = options.ignore ?? (() => false);
379
- const files = await collectFiles(dir, ignore);
607
+ const ignore = combineUploadIgnore(options.ignore);
608
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
380
609
  const total = files.reduce((sum, f) => sum + f.size, 0);
381
610
  options.onProgress?.({ phase: "collect", current: total, total });
382
- let sent = 0;
611
+ const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
612
+ const local = [];
383
613
  for (const file of files) {
384
- const content = await fs3.readFile(file.absPath);
385
- const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
386
- const body = buildMultipartBody(
387
- boundary,
388
- // TODO(swagger): multipart 字段名/结构待定,当前为占位约定。
389
- { path: file.relPath },
390
- { name: path3.basename(file.relPath), content }
391
- );
392
- await client.upload(ENDPOINTS.upload, { boundary, body });
614
+ local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
615
+ }
616
+ const plan = planWorkspaceUpload(local, remote);
617
+ const counts = countsFromPlan(plan);
618
+ const pushBody = buildWorkspacePushBody(plan);
619
+ if (pushBody) {
620
+ await client.request(ENDPOINTS.workspaceCodePush(), {
621
+ method: "POST",
622
+ body: JSON.stringify(pushBody)
623
+ });
624
+ }
625
+ let sent = 0;
626
+ const bump = (file) => {
393
627
  sent += file.size;
394
628
  options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
629
+ };
630
+ for (const c of plan.creates) bump(c.file);
631
+ for (const p of plan.updates) bump(p.file);
632
+ for (const u of plan.unchanged) bump(u);
633
+ if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
634
+ return { fileCount: files.length, byteCount: total, ...counts };
635
+ }
636
+
637
+ // src/workspace.ts
638
+ import { promises as fs4 } from "fs";
639
+ import path5 from "path";
640
+
641
+ // src/assetsPath.ts
642
+ import path4 from "path";
643
+ function resolveWorkspaceAssetPath(cwd, relPath) {
644
+ const root = path4.resolve(cwd);
645
+ const input = relPath.trim();
646
+ if (!input) throw new Error("assets \u8DEF\u5F84\u4E3A\u7A7A");
647
+ if (path4.isAbsolute(input)) throw new Error(`assets \u53EA\u63A5\u53D7\u5DE5\u4F5C\u533A\u76F8\u5BF9\u8DEF\u5F84,\u62D2\u7EDD\u7EDD\u5BF9\u8DEF\u5F84: ${relPath}`);
648
+ const resolved = path4.resolve(root, input);
649
+ const rel = path4.relative(root, resolved);
650
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
651
+ throw new Error(`assets \u8DEF\u5F84\u9003\u9038\u5DE5\u4F5C\u533A: ${relPath}`);
652
+ }
653
+ return resolved;
654
+ }
655
+
656
+ // src/workspace.ts
657
+ var DEFAULT_ENTRY_PATH = "src/main.ts";
658
+ var DEFAULT_MAIN_TS = `import { TwinApp, type TwinAppContext } from "@easytwin/apps";
659
+
660
+ export default class App extends TwinApp {
661
+ async init(ctx: TwinAppContext) {
662
+ // engine / container \u5DF2\u6709;scene / camera \u4E3A null
663
+ void ctx;
664
+ }
665
+
666
+ async onSceneLoaded(ctx: TwinAppContext) {
667
+ // \u573A\u666F\u5B57\u6BB5\u6709\u503C;\u5728\u6B64\u52A0\u7269\u4F53 / \u7ED1\u4E8B\u4EF6,\u5E76\u7528 ctx.sceneCleanup \u5BF9\u79F0\u62C6\u9664
668
+ void ctx;
669
+ }
670
+
671
+ onUpdate(ctx: TwinAppContext, delta: number, elapsed: number) {
672
+ void ctx;
673
+ void delta;
674
+ void elapsed;
675
+ }
676
+
677
+ onDispose(ctx: TwinAppContext) {
678
+ void ctx;
679
+ }
680
+ }
681
+ `;
682
+ function defaultPullIgnore(relPath) {
683
+ return defaultWorkspaceIgnore(relPath);
684
+ }
685
+ function isSafeRelPath(relPath) {
686
+ const n = normalizePath(relPath);
687
+ if (!n) return false;
688
+ if (n.toLowerCase() === "easytwin.config.json") return false;
689
+ const base = n.split("/").pop() ?? "";
690
+ if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
691
+ if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
692
+ const parts = n.split("/");
693
+ if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
694
+ return true;
695
+ }
696
+ function normalizeEol(text) {
697
+ return text.replace(/\r\n/g, "\n");
698
+ }
699
+ function combineIgnore(extra) {
700
+ return (relPath) => defaultPullIgnore(relPath) || (extra?.(relPath) ?? false);
701
+ }
702
+ async function readLocalText(absPath) {
703
+ try {
704
+ return await fs4.readFile(absPath, "utf8");
705
+ } catch (err) {
706
+ const code = err.code;
707
+ if (code === "ENOENT") return void 0;
708
+ throw err;
395
709
  }
396
- return { fileCount: files.length, byteCount: sent };
710
+ }
711
+ async function fetchRemoteFiles(client) {
712
+ if (client.mock) return [];
713
+ return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
714
+ }
715
+ async function planWorkspacePull(client, dir, options = {}) {
716
+ const ignore = combineIgnore(options.ignore);
717
+ const remoteRaw = await fetchRemoteFiles(client);
718
+ const skippedRemotePaths = [];
719
+ const remoteByPath = /* @__PURE__ */ new Map();
720
+ for (const file of remoteRaw) {
721
+ const rel = normalizePath(file.filePath);
722
+ if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
723
+ skippedRemotePaths.push(rel || file.filePath);
724
+ continue;
725
+ }
726
+ remoteByPath.set(rel, file.content);
727
+ }
728
+ let localFiles = [];
729
+ try {
730
+ localFiles = await collectFiles(dir, ignore);
731
+ } catch (err) {
732
+ const code = err.code;
733
+ if (code !== "ENOENT") throw err;
734
+ }
735
+ const localByPath = /* @__PURE__ */ new Map();
736
+ for (const file of localFiles) {
737
+ const rel = normalizePath(file.relPath);
738
+ if (!isWorkspaceCodeFile(rel)) continue;
739
+ const content = await readLocalText(file.absPath);
740
+ if (content !== void 0) localByPath.set(rel, content);
741
+ }
742
+ const changes = [];
743
+ const seen = /* @__PURE__ */ new Set();
744
+ for (const [rel, remoteContent] of remoteByPath) {
745
+ seen.add(rel);
746
+ const localContent = localByPath.get(rel);
747
+ if (localContent === void 0) {
748
+ changes.push({ path: rel, kind: "remote-only", remoteContent });
749
+ } else if (normalizeEol(localContent) === normalizeEol(remoteContent)) {
750
+ changes.push({ path: rel, kind: "identical", localContent, remoteContent });
751
+ } else {
752
+ changes.push({ path: rel, kind: "modified", localContent, remoteContent });
753
+ }
754
+ }
755
+ for (const [rel, localContent] of localByPath) {
756
+ if (seen.has(rel)) continue;
757
+ changes.push({ path: rel, kind: "local-only", localContent });
758
+ }
759
+ const remoteEmpty = remoteByPath.size === 0;
760
+ let seededDefaults = false;
761
+ if (remoteEmpty) {
762
+ const localMain = localByPath.get(DEFAULT_ENTRY_PATH);
763
+ if (localMain === void 0) {
764
+ changes.push({ path: DEFAULT_ENTRY_PATH, kind: "seed", remoteContent: DEFAULT_MAIN_TS });
765
+ seededDefaults = true;
766
+ }
767
+ }
768
+ changes.sort((a, b) => a.path.localeCompare(b.path));
769
+ return {
770
+ remoteEmpty,
771
+ seededDefaults,
772
+ mock: client.mock || void 0,
773
+ skippedRemotePaths,
774
+ changes
775
+ };
776
+ }
777
+ async function applyWorkspacePull(dir, plan, options = {}) {
778
+ const written = [];
779
+ const skippedConflicts = [];
780
+ for (const change of plan.changes) {
781
+ if (change.kind === "identical" || change.kind === "local-only") continue;
782
+ if (change.kind === "modified" && !options.force) {
783
+ skippedConflicts.push(change.path);
784
+ continue;
785
+ }
786
+ const content = change.remoteContent ?? "";
787
+ const abs = resolveWorkspaceAssetPath(dir, change.path);
788
+ await fs4.mkdir(path5.dirname(abs), { recursive: true });
789
+ await fs4.writeFile(abs, content, "utf8");
790
+ written.push(change.path);
791
+ }
792
+ return { written, skippedConflicts };
793
+ }
794
+ async function pullWorkspace(client, dir, options = {}) {
795
+ const plan = await planWorkspacePull(client, dir, { ignore: options.ignore });
796
+ if (options.dryRun) {
797
+ const skippedConflicts = plan.changes.filter((c) => c.kind === "modified" && !options.force).map((c) => c.path);
798
+ return { plan, written: [], skippedConflicts, dryRun: true, mock: plan.mock };
799
+ }
800
+ const applied = await applyWorkspacePull(dir, plan, { force: options.force });
801
+ return { plan, ...applied, mock: plan.mock };
802
+ }
803
+ var KIND_LABEL = {
804
+ identical: "same ",
805
+ "remote-only": "create",
806
+ "local-only": "keep ",
807
+ modified: "modify",
808
+ seed: "seed "
809
+ };
810
+ function formatWorkspacePullPlan(plan) {
811
+ const lines = [];
812
+ if (plan.mock) lines.push("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u6309\u8FDC\u7AEF\u4E3A\u7A7A\u5904\u7406");
813
+ const remoteCount = plan.changes.filter((c) => c.kind !== "local-only" && c.kind !== "seed").length;
814
+ if (plan.remoteEmpty) {
815
+ lines.push(
816
+ plan.seededDefaults ? `\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u5C06\u5199\u5165\u9ED8\u8BA4 ${DEFAULT_ENTRY_PATH}` : "\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u672C\u5730\u5DF2\u6709\u5165\u53E3,\u672A\u6539\u52A8\u9ED8\u8BA4\u6A21\u677F"
817
+ );
818
+ } else {
819
+ lines.push(`\u8FDC\u7AEF ${remoteCount} \u4E2A\u6587\u4EF6`);
820
+ }
821
+ for (const change of plan.changes) {
822
+ let extra = "";
823
+ if (change.kind === "modified") extra = " (\u51B2\u7A81,\u9ED8\u8BA4\u4E0D\u8986\u76D6)";
824
+ if (change.kind === "local-only") extra = " (\u4EC5\u672C\u5730)";
825
+ if (change.kind === "seed") extra = " (\u9ED8\u8BA4 TwinApp)";
826
+ lines.push(` ${KIND_LABEL[change.kind]} ${change.path}${extra}`);
827
+ }
828
+ if (plan.skippedRemotePaths.length > 0) {
829
+ lines.push(`\u8DF3\u8FC7\u975E\u6CD5/\u51ED\u636E\u8DEF\u5F84: ${plan.skippedRemotePaths.join(", ")}`);
830
+ }
831
+ return lines.join("\n");
832
+ }
833
+ function formatWorkspacePullResult(result) {
834
+ const lines = [formatWorkspacePullPlan(result.plan)];
835
+ if (result.dryRun) {
836
+ lines.push("dry-run:\u672A\u5199\u76D8");
837
+ if (result.skippedConflicts.length > 0) {
838
+ lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force)`);
839
+ }
840
+ return lines.join("\n");
841
+ }
842
+ if (result.written.length > 0) {
843
+ lines.push(`\u5DF2\u5199\u5165 ${result.written.length} \u4E2A\u6587\u4EF6: ${result.written.join(", ")}`);
844
+ } else {
845
+ lines.push("\u672A\u5199\u5165\u6587\u4EF6");
846
+ }
847
+ if (result.skippedConflicts.length > 0) {
848
+ lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force): ${result.skippedConflicts.join(", ")}`);
849
+ }
850
+ return lines.join("\n");
397
851
  }
398
852
 
399
853
  // src/skills.ts
400
- import { existsSync, promises as fs4 } from "fs";
401
- import path4 from "path";
854
+ import { existsSync, promises as fs5 } from "fs";
855
+ import path6 from "path";
402
856
  import { fileURLToPath as fileURLToPath2 } from "url";
403
857
  var SKILL_NAMES = [
404
858
  "easytwin-render",
@@ -406,7 +860,8 @@ var SKILL_NAMES = [
406
860
  "easytwin-develop",
407
861
  "easytwin-bootstrap",
408
862
  "easytwin-scene",
409
- "easytwin-upload"
863
+ "easytwin-upload",
864
+ "easytwin-test"
410
865
  ];
411
866
  var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
412
867
  var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
@@ -421,7 +876,8 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
421
876
  skipLibCheck: true,
422
877
  noEmit: true,
423
878
  paths: {
424
- "@easytwin/runtime": [".easytwin/types"]
879
+ "@easytwin/runtime": [".easytwin/types"],
880
+ "@easytwin/apps": [".easytwin/types/apps"]
425
881
  }
426
882
  },
427
883
  include: ["src/**/*.ts"]
@@ -433,22 +889,55 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
433
889
  var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
434
890
  "skipLibCheck": true,
435
891
  "paths": {
436
- "@easytwin/runtime": [".easytwin/types"]
892
+ "@easytwin/runtime": [".easytwin/types"],
893
+ "@easytwin/apps": [".easytwin/types/apps"]
437
894
  }`;
438
- var RUN_CONTEXT_DECL = `
439
- /** \u9884\u89C8\u9875 Run \u6309\u94AE\u6CE8\u5165\u7684\u8FD0\u884C\u4E0A\u4E0B\u6587(\u89C1 D12)\u3002 */
440
- export interface EasyTwinRunContext {
895
+ var APPS_TYPES_FILE = "apps.d.ts";
896
+ var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
897
+
898
+ export type TwinAppContext = {
899
+ app: {
900
+ id: string;
901
+ mode: "preview" | "publish";
902
+ };
441
903
  engine: RuntimeEngine;
442
- runtime: typeof import("@easytwin/runtime");
443
- sceneJson: SceneJson;
904
+ container: HTMLElement;
905
+ runtimeScene: RuntimeScene | null;
906
+ scene: RuntimeScene["sceneObject"] | null;
907
+ camera: RuntimeScene["camera"]["main"] | null;
908
+ sceneManager: {
909
+ currentSceneId: string;
910
+ runtime: SceneManager | null;
911
+ loadScene(sceneId: string): Promise<void>;
912
+ };
913
+ logger: {
914
+ log(...args: unknown[]): void;
915
+ info(...args: unknown[]): void;
916
+ warn(...args: unknown[]): void;
917
+ error(...args: unknown[]): void;
918
+ };
919
+ assets: {
920
+ text(path: string): Promise<string>;
921
+ json<T = unknown>(path: string): Promise<T>;
922
+ };
923
+ cleanup(fn: () => void | Promise<void>): void;
924
+ sceneCleanup(fn: () => void | Promise<void>): void;
925
+ };
926
+
927
+ export declare abstract class TwinApp {
928
+ init?(ctx: TwinAppContext): void | Promise<void>;
929
+ onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
930
+ onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
931
+ onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
932
+ onDispose?(ctx: TwinAppContext): void | Promise<void>;
933
+ onError?(ctx: TwinAppContext, error: unknown): boolean | void;
444
934
  }
935
+
936
+ export declare function defineApp<T>(app: T): T;
445
937
  `;
446
938
  function buildRuntimeTypesContent(sourceDts) {
447
- const trimmed = sourceDts.replace(/\s+$/, "");
448
- if (trimmed.includes("export interface EasyTwinRunContext")) return `${trimmed}
939
+ return `${sourceDts.replace(/\s+$/, "")}
449
940
  `;
450
- return `${trimmed}
451
- ${RUN_CONTEXT_DECL}`;
452
941
  }
453
942
  function normalizeTargets(target = "all") {
454
943
  if (target === "all") return ["cursor", "claude", "codex", "qoder"];
@@ -459,69 +948,69 @@ function resolveSkillsSourceDir() {
459
948
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
460
949
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
461
950
  }
462
- const here = path4.dirname(fileURLToPath2(import.meta.url));
463
- return path4.resolve(here, "..", "skills");
951
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
952
+ return path6.resolve(here, "..", "skills");
464
953
  }
465
954
  function resolveRuntimeTypesSourceFile() {
466
955
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
467
956
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
468
957
  }
469
- const here = path4.dirname(fileURLToPath2(import.meta.url));
470
- const fromDist = path4.join(here, "runtime-types", "index.d.ts");
471
- const fromSrc = path4.resolve(here, "lib", "index.d.ts");
958
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
959
+ const fromDist = path6.join(here, "runtime-types", "index.d.ts");
960
+ const fromSrc = path6.resolve(here, "lib", "index.d.ts");
472
961
  if (existsSync(fromDist)) return fromDist;
473
962
  if (existsSync(fromSrc)) return fromSrc;
474
963
  throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
475
964
  }
476
965
  async function readJson(file) {
477
- return JSON.parse(await fs4.readFile(file, "utf8"));
966
+ return JSON.parse(await fs5.readFile(file, "utf8"));
478
967
  }
479
968
  async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
480
- const marker = path4.join(skillsDir, ".easytwin-source.json");
969
+ const marker = path6.join(skillsDir, ".easytwin-source.json");
481
970
  try {
482
971
  const meta = await readJson(marker);
483
972
  if (typeof meta.version === "string") return meta.version;
484
973
  } catch {
485
974
  }
486
975
  try {
487
- const pkg = await readJson(path4.resolve(skillsDir, "..", "package.json"));
976
+ const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
488
977
  return pkg.version;
489
978
  } catch {
490
979
  return "0.0.0";
491
980
  }
492
981
  }
493
982
  async function copyDir(src, dest) {
494
- await fs4.mkdir(dest, { recursive: true });
495
- const entries = await fs4.readdir(src, { withFileTypes: true });
983
+ await fs5.mkdir(dest, { recursive: true });
984
+ const entries = await fs5.readdir(src, { withFileTypes: true });
496
985
  for (const entry of entries) {
497
- const s = path4.join(src, entry.name);
498
- const d = path4.join(dest, entry.name);
986
+ const s = path6.join(src, entry.name);
987
+ const d = path6.join(dest, entry.name);
499
988
  if (entry.isDirectory()) await copyDir(s, d);
500
- else if (entry.isFile()) await fs4.copyFile(s, d);
989
+ else if (entry.isFile()) await fs5.copyFile(s, d);
501
990
  }
502
991
  }
503
992
  async function dirMatches(src, dest) {
504
993
  let sourceEntries;
505
994
  try {
506
- sourceEntries = await fs4.readdir(src);
995
+ sourceEntries = await fs5.readdir(src);
507
996
  } catch {
508
997
  return false;
509
998
  }
510
999
  for (const name of sourceEntries) {
511
- const s = path4.join(src, name);
512
- const d = path4.join(dest, name);
513
- const sStat = await fs4.stat(s);
1000
+ const s = path6.join(src, name);
1001
+ const d = path6.join(dest, name);
1002
+ const sStat = await fs5.stat(s);
514
1003
  if (sStat.isDirectory()) {
515
1004
  if (!await dirMatches(s, d)) return false;
516
1005
  } else {
517
1006
  let dStat;
518
1007
  try {
519
- dStat = await fs4.stat(d);
1008
+ dStat = await fs5.stat(d);
520
1009
  } catch {
521
1010
  return false;
522
1011
  }
523
1012
  if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
524
- if (!(await fs4.readFile(s)).equals(await fs4.readFile(d))) return false;
1013
+ if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
525
1014
  }
526
1015
  }
527
1016
  return true;
@@ -532,18 +1021,18 @@ function actionFor(exists, matches) {
532
1021
  }
533
1022
  async function writeMeta(destDir, version) {
534
1023
  const meta = { name: "@easytwin/devkit", version };
535
- await fs4.writeFile(path4.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
1024
+ await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
536
1025
  }
537
1026
  async function syncToDir(target, sourceRoot, targetRoot, version) {
538
1027
  const entries = [];
539
1028
  for (const name of SKILL_NAMES) {
540
- const src = path4.join(sourceRoot, name);
541
- const dest = path4.join(targetRoot, name);
542
- const exists = await fs4.stat(dest).then(() => true).catch(() => false);
1029
+ const src = path6.join(sourceRoot, name);
1030
+ const dest = path6.join(targetRoot, name);
1031
+ const exists = await fs5.stat(dest).then(() => true).catch(() => false);
543
1032
  const matches = await dirMatches(src, dest);
544
1033
  const action = actionFor(exists, matches);
545
1034
  if (action !== "unchanged") {
546
- await fs4.rm(dest, { recursive: true, force: true });
1035
+ await fs5.rm(dest, { recursive: true, force: true });
547
1036
  await copyDir(src, dest);
548
1037
  }
549
1038
  entries.push({ name, action });
@@ -562,19 +1051,20 @@ function buildCodexSegment(version) {
562
1051
  "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
563
1052
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
564
1053
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
565
- "- `easytwin-upload`:\u5168\u91CF\u4E0A\u4F20\u5F00\u53D1\u4EA7\u7269\u76EE\u5F55(\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
1054
+ "- `easytwin-upload`:\u62C9\u53D6/\u4E0A\u4F20\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5199\u9ED8\u8BA4\u5165\u53E3;\u4E0A\u4F20\u5168\u91CF\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
1055
+ "- `easytwin-test`:\u5199\u5DE5\u4F5C\u533A `*.spec.ts`,\u9884\u89C8\u9875\u6309\u94AE/\u8F93\u5165\u6846\u8C03\u7528\u5BFC\u51FA\u51FD\u6570(\u4E0D\u540C\u6B65;CLI `easytwin preview` \u6216\u63D2\u4EF6)\u3002",
566
1056
  "",
567
1057
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
568
1058
  CODEX_MARKER_END
569
1059
  ].join("\n");
570
1060
  }
571
1061
  async function syncToCodex(sourceRoot, cwd, version) {
572
- const agentsFile = path4.join(cwd, "AGENTS.md");
1062
+ const agentsFile = path6.join(cwd, "AGENTS.md");
573
1063
  const segment = buildCodexSegment(version);
574
1064
  let content = "";
575
1065
  let exists = true;
576
1066
  try {
577
- content = await fs4.readFile(agentsFile, "utf8");
1067
+ content = await fs5.readFile(agentsFile, "utf8");
578
1068
  } catch {
579
1069
  exists = false;
580
1070
  }
@@ -592,39 +1082,50 @@ async function syncToCodex(sourceRoot, cwd, version) {
592
1082
  next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
593
1083
  }
594
1084
  if (action !== "unchanged") {
595
- await fs4.mkdir(cwd, { recursive: true });
596
- await fs4.writeFile(agentsFile, next, "utf8");
1085
+ await fs5.mkdir(cwd, { recursive: true });
1086
+ await fs5.writeFile(agentsFile, next, "utf8");
597
1087
  }
598
1088
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
599
1089
  }
600
1090
  async function syncRuntimeTypes(cwd, typesSourceFile) {
601
- const destDir = path4.join(cwd, ".easytwin", "types");
602
- const destFile = path4.join(destDir, "index.d.ts");
603
- const content = buildRuntimeTypesContent(await fs4.readFile(typesSourceFile, "utf8"));
604
- let exists = true;
605
- let current = "";
1091
+ const destDir = path6.join(cwd, ".easytwin", "types");
1092
+ const destFile = path6.join(destDir, "index.d.ts");
1093
+ const appsFile = path6.join(destDir, APPS_TYPES_FILE);
1094
+ const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
1095
+ let runtimeCurrent = "";
1096
+ let appsCurrent = "";
1097
+ let runtimeExists = true;
1098
+ let appsExists = true;
606
1099
  try {
607
- current = await fs4.readFile(destFile, "utf8");
1100
+ runtimeCurrent = await fs5.readFile(destFile, "utf8");
608
1101
  } catch {
609
- exists = false;
1102
+ runtimeExists = false;
610
1103
  }
611
- const action = actionFor(exists, current === content);
612
- if (action !== "unchanged") {
613
- await fs4.mkdir(destDir, { recursive: true });
614
- await fs4.writeFile(destFile, content, "utf8");
1104
+ try {
1105
+ appsCurrent = await fs5.readFile(appsFile, "utf8");
1106
+ } catch {
1107
+ appsExists = false;
1108
+ }
1109
+ const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
1110
+ const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
1111
+ const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
1112
+ if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
1113
+ await fs5.mkdir(destDir, { recursive: true });
1114
+ if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
1115
+ if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
615
1116
  }
616
- const tsconfigPath = path4.join(cwd, "tsconfig.json");
1117
+ const tsconfigPath = path6.join(cwd, "tsconfig.json");
617
1118
  let tsconfig;
618
1119
  let pathsHint;
619
1120
  try {
620
- const existing = await fs4.readFile(tsconfigPath, "utf8");
1121
+ const existing = await fs5.readFile(tsconfigPath, "utf8");
621
1122
  if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
622
1123
  else {
623
1124
  tsconfig = "manual-paths";
624
1125
  pathsHint = TSCONFIG_PATHS_HINT;
625
1126
  }
626
1127
  } catch {
627
- await fs4.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1128
+ await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
628
1129
  tsconfig = "created";
629
1130
  }
630
1131
  return { action, tsconfig, pathsHint };
@@ -635,9 +1136,9 @@ async function syncSkills(options) {
635
1136
  const version = options.version ?? await readDevkitVersion(sourceRoot);
636
1137
  const summaries = [];
637
1138
  for (const target of targets) {
638
- if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
639
- else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
640
- else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path4.join(options.cwd, ".qoder", "skills"), version));
1139
+ if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
1140
+ else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
1141
+ else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
641
1142
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
642
1143
  }
643
1144
  const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
@@ -646,11 +1147,33 @@ async function syncSkills(options) {
646
1147
 
647
1148
  // src/bundle.ts
648
1149
  import * as esbuild from "esbuild-wasm";
649
- import { promises as fs5 } from "fs";
650
- import path5 from "path";
1150
+ import { promises as fs6 } from "fs";
1151
+ import path7 from "path";
1152
+
1153
+ // src/apps.ts
1154
+ var PORTABLE_RUNTIME_EXPORTS = [
1155
+ "THREE",
1156
+ "RuntimeEngine",
1157
+ "SceneManager",
1158
+ "LoadSceneMode",
1159
+ "convertObjToComponentJson"
1160
+ ];
1161
+ function portableRuntimeWarning(names) {
1162
+ const extra = [...new Set(names)].filter(
1163
+ (n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
1164
+ );
1165
+ const ns = [...names].includes("*");
1166
+ if (!ns && extra.length === 0) return void 0;
1167
+ const detail = ns ? "namespace import *" : extra.sort().join(", ");
1168
+ return `\u68C0\u6D4B\u5230 @easytwin/runtime \u5BFC\u51FA ${detail} \u4E0D\u5728\u5728\u7EBF\u7F16\u8BD1\u767D\u540D\u5355(${PORTABLE_RUNTIME_EXPORTS.join(", ")})\u5185\u3002\u672C\u5730\u9884\u89C8\u53EF\u7528,\u4E0A\u4F20\u5230\u5728\u7EBF TwinApp \u53EF\u80FD\u7F16\u4E0D\u8FC7\u3002`;
1169
+ }
1170
+
1171
+ // src/bundle.ts
651
1172
  var USER_ENTRY = "src/main.ts";
652
1173
  var DEFAULT_BUNDLE_OUT = "dist/main.js";
653
1174
  var RUNTIME_MODULE = "@easytwin/runtime";
1175
+ var APPS_MODULE = "@easytwin/apps";
1176
+ var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
654
1177
  var BundleError = class extends Error {
655
1178
  constructor(message) {
656
1179
  super(message);
@@ -658,7 +1181,7 @@ var BundleError = class extends Error {
658
1181
  }
659
1182
  };
660
1183
  function isRelativeOrAbsolute(spec) {
661
- return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path5.isAbsolute(spec);
1184
+ return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
662
1185
  }
663
1186
  function whitelistPlugin() {
664
1187
  return {
@@ -667,11 +1190,11 @@ function whitelistPlugin() {
667
1190
  build2.onResolve({ filter: /.*/ }, (args) => {
668
1191
  if (args.kind === "entry-point") return void 0;
669
1192
  if (isRelativeOrAbsolute(args.path)) return void 0;
670
- if (args.path === RUNTIME_MODULE) return { path: args.path, external: true };
1193
+ if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
671
1194
  return {
672
1195
  errors: [
673
1196
  {
674
- text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
1197
+ text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime \u4E0E @easytwin/apps\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
675
1198
  }
676
1199
  ]
677
1200
  };
@@ -685,14 +1208,29 @@ function formatEsbuildMessages(messages) {
685
1208
  return `${loc}${m.text}`;
686
1209
  }).join("\n");
687
1210
  }
688
- async function bundleUserCode(options) {
689
- const cwd = path5.resolve(options.cwd);
690
- const entry = path5.join(cwd, USER_ENTRY);
1211
+ function collectBundledRuntimeExports(code) {
1212
+ const names = [];
1213
+ if (/import\s+\*\s+as\s+[\w$]+\s+from\s*["']@easytwin\/runtime["']/.test(code)) names.push("*");
1214
+ const named = /import\s*\{([^}]+)\}\s*from\s*["']@easytwin\/runtime["']/g;
1215
+ for (const match of code.matchAll(named)) {
1216
+ const body = match[1] ?? "";
1217
+ for (const part of body.split(",")) {
1218
+ const token = part.replace(/\btype\b/g, "").trim();
1219
+ if (!token) continue;
1220
+ const id = token.split(/\s+as\s+/)[0]?.trim();
1221
+ if (id) names.push(id);
1222
+ }
1223
+ }
1224
+ return names;
1225
+ }
1226
+ async function bundleWorkspaceModule(options) {
1227
+ const cwd = path7.resolve(options.cwd);
1228
+ const entry = path7.isAbsolute(options.entry) ? options.entry : path7.join(cwd, options.entry);
691
1229
  try {
692
- await fs5.access(entry);
1230
+ await fs6.access(entry);
693
1231
  } catch {
694
1232
  throw new BundleError(
695
- `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default async function main(ctx)\`\u3002`
1233
+ options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
696
1234
  );
697
1235
  }
698
1236
  let result;
@@ -723,21 +1261,1167 @@ async function bundleUserCode(options) {
723
1261
  if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
724
1262
  const code = file.text;
725
1263
  const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
1264
+ const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
1265
+ if (portable) warnings.push(portable);
726
1266
  if (options.outFile) {
727
- const outFile = path5.isAbsolute(options.outFile) ? options.outFile : path5.join(cwd, options.outFile);
728
- await fs5.mkdir(path5.dirname(outFile), { recursive: true });
729
- await fs5.writeFile(outFile, code, "utf8");
1267
+ const outFile = path7.isAbsolute(options.outFile) ? options.outFile : path7.join(cwd, options.outFile);
1268
+ await fs6.mkdir(path7.dirname(outFile), { recursive: true });
1269
+ await fs6.writeFile(outFile, code, "utf8");
730
1270
  }
731
1271
  return { code, warnings };
732
1272
  }
1273
+ async function bundleUserCode(options) {
1274
+ const cwd = path7.resolve(options.cwd);
1275
+ const entry = path7.join(cwd, USER_ENTRY);
1276
+ return bundleWorkspaceModule({
1277
+ cwd: options.cwd,
1278
+ entry: USER_ENTRY,
1279
+ outFile: options.outFile,
1280
+ missingEntryMessage: `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
1281
+ });
1282
+ }
733
1283
  async function typesMissingHint(cwd) {
734
1284
  try {
735
- await fs5.access(path5.join(cwd, ".easytwin", "types", "index.d.ts"));
1285
+ await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
736
1286
  return void 0;
737
1287
  } catch {
738
1288
  return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
739
1289
  }
740
1290
  }
1291
+ var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
1292
+ var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1293
+ function decodeVLQValues(str) {
1294
+ const values = [];
1295
+ let i = 0;
1296
+ while (i < str.length) {
1297
+ let result = 0;
1298
+ let shift = 0;
1299
+ let continuation = true;
1300
+ while (continuation) {
1301
+ if (i >= str.length) return values;
1302
+ const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
1303
+ if (digit < 0) return values;
1304
+ continuation = (digit & 32) !== 0;
1305
+ result += (digit & 31) << shift;
1306
+ shift += 5;
1307
+ }
1308
+ values.push(result & 1 ? -(result >> 1) : result >> 1);
1309
+ }
1310
+ return values;
1311
+ }
1312
+ function decodeMappings(map) {
1313
+ const sources = map.sources ?? [];
1314
+ const lines = (map.mappings ?? "").split(";");
1315
+ let sourceIndex = 0;
1316
+ let originalLine = 0;
1317
+ let originalColumn = 0;
1318
+ const decoded = [];
1319
+ for (const line of lines) {
1320
+ let generatedColumn = 0;
1321
+ const segs = [];
1322
+ if (line) {
1323
+ for (const raw of line.split(",")) {
1324
+ if (!raw) continue;
1325
+ const nums = decodeVLQValues(raw);
1326
+ if (nums[0] === void 0) continue;
1327
+ generatedColumn += nums[0];
1328
+ if (nums.length >= 4) {
1329
+ sourceIndex += nums[1] ?? 0;
1330
+ originalLine += nums[2] ?? 0;
1331
+ originalColumn += nums[3] ?? 0;
1332
+ segs.push({
1333
+ generatedColumn,
1334
+ source: sources[sourceIndex] ?? USER_ENTRY,
1335
+ originalLine,
1336
+ originalColumn
1337
+ });
1338
+ }
1339
+ }
1340
+ }
1341
+ decoded.push(segs);
1342
+ }
1343
+ return decoded;
1344
+ }
1345
+ function originalPositionFor(map, line, column) {
1346
+ const decoded = decodeMappings(map);
1347
+ for (let i = line - 1; i >= 0; i--) {
1348
+ const segs = decoded[i];
1349
+ if (!segs || segs.length === 0) continue;
1350
+ const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
1351
+ let best = segs[0];
1352
+ for (const seg of segs) {
1353
+ if (seg.generatedColumn <= col) best = seg;
1354
+ else break;
1355
+ }
1356
+ if (!best) continue;
1357
+ return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
1358
+ }
1359
+ return void 0;
1360
+ }
1361
+ function extractInlineSourceMap(code) {
1362
+ const m = code.match(SOURCEMAP_RE);
1363
+ if (!m?.[1]) return void 0;
1364
+ try {
1365
+ return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
1366
+ } catch {
1367
+ return void 0;
1368
+ }
1369
+ }
1370
+ function remapErrorStack(stack, bundledCode) {
1371
+ const map = extractInlineSourceMap(bundledCode);
1372
+ if (!map?.mappings) return stack;
1373
+ return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
1374
+ const orig = originalPositionFor(map, Number(line), Number(col));
1375
+ if (!orig) return full;
1376
+ return `${orig.source}:${orig.line}:${orig.column}`;
1377
+ });
1378
+ }
1379
+
1380
+ // src/previewServer.ts
1381
+ import { existsSync as existsSync2 } from "fs";
1382
+ import { promises as fs8 } from "fs";
1383
+ import http2 from "http";
1384
+ import path9 from "path";
1385
+ import { fileURLToPath as fileURLToPath3 } from "url";
1386
+
1387
+ // src/previewHtml.ts
1388
+ function escapeJsonForScript(json) {
1389
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1390
+ }
1391
+ var PREVIEW_STYLE = `
1392
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
1393
+ body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
1394
+ #stage { position: relative; flex: 1; min-height: 0; }
1395
+ #twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
1396
+ #twin-root canvas { display: block; }
1397
+ #status {
1398
+ position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
1399
+ padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
1400
+ }
1401
+ #status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
1402
+ /* \u4F5C\u8005\u6837\u5F0F display:flex \u4F1A\u8986\u76D6 UA \u7684 [hidden]{display:none},\u5FC5\u987B\u663E\u5F0F\u58F0\u660E\u624D\u80FD\u9690\u85CF\u52A0\u8F7D\u6587\u6848 */
1403
+ #status[hidden] { display: none; }
1404
+ #mock-banner {
1405
+ position: absolute; top: 0; left: 0; right: 0; z-index: 2;
1406
+ padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
1407
+ border-bottom: 1px solid #f0c36d; pointer-events: none;
1408
+ }
1409
+ #debug-log {
1410
+ display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
1411
+ max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
1412
+ background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
1413
+ white-space: pre-wrap; word-break: break-all; pointer-events: auto;
1414
+ }
1415
+ #debug-log.open { display: block; }
1416
+ #run-bar {
1417
+ flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
1418
+ padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
1419
+ }
1420
+ #run-bar button {
1421
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
1422
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
1423
+ }
1424
+ #run-bar button:disabled { opacity: .55; cursor: default; }
1425
+ #btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
1426
+ #run-status { color: #bbb; min-width: 4em; }
1427
+ #run-bar .spacer { flex: 1; }
1428
+ #test-panel {
1429
+ flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
1430
+ padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
1431
+ max-height: 30%; overflow: auto;
1432
+ }
1433
+ #test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
1434
+ #test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
1435
+ #test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
1436
+ #test-panel .test-empty { color: #777; }
1437
+ #test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
1438
+ #test-panel input.test-input {
1439
+ width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
1440
+ padding: 3px 6px; border-radius: 4px; font: 12px inherit;
1441
+ }
1442
+ #test-panel button {
1443
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
1444
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
1445
+ }
1446
+ #test-panel button:disabled { opacity: .55; cursor: default; }
1447
+ `;
1448
+ var RENDER_SCRIPT = `
1449
+ var statusEl = document.getElementById("status");
1450
+ var logEl = document.getElementById("debug-log");
1451
+ var engine = null;
1452
+ var runtime = null;
1453
+ var sceneJson = null;
1454
+ var host = null;
1455
+ var running = false;
1456
+ var runningTest = false;
1457
+ var testsReady = false;
1458
+ var hostKind = __HOST_KIND__;
1459
+ var vscodeApi = null;
1460
+ if (hostKind === "vscode") {
1461
+ try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
1462
+ }
1463
+ var pending = {};
1464
+ var seq = 0;
1465
+ var origFetch = window.fetch.bind(window);
1466
+ function now() { return new Date().toISOString().slice(11, 23); }
1467
+ function log(line) {
1468
+ var text = "[" + now() + "] " + line;
1469
+ if (logEl) {
1470
+ logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
1471
+ logEl.scrollTop = logEl.scrollHeight;
1472
+ }
1473
+ if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
1474
+ console.log("[EasyTwin]", line);
1475
+ }
1476
+ function nextId() { return String(++seq); }
1477
+ function handleHostMessage(msg) {
1478
+ if (!msg) return;
1479
+ if (msg.type === "proxy-fetch-result") {
1480
+ var p = pending[msg.id];
1481
+ if (!p) return;
1482
+ delete pending[msg.id];
1483
+ if (msg.error) p.reject(new Error(msg.error));
1484
+ else p.resolve(msg);
1485
+ return;
1486
+ }
1487
+ if (msg.type === "bundle-result") {
1488
+ if (!msg.ok) {
1489
+ log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
1490
+ setRunStatus("\u7F16\u8BD1\u5931\u8D25");
1491
+ setRunBusy(false);
1492
+ if (logEl) logEl.classList.add("open");
1493
+ return;
1494
+ }
1495
+ for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
1496
+ runUserCode(msg.code);
1497
+ return;
1498
+ }
1499
+ if (msg.type === "read-asset-result") {
1500
+ var ap = pending[msg.id];
1501
+ if (!ap) return;
1502
+ delete pending[msg.id];
1503
+ if (msg.error) ap.reject(new Error(msg.error));
1504
+ else ap.resolve(msg.text);
1505
+ return;
1506
+ }
1507
+ if (msg.type === "run-error-mapped") {
1508
+ log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
1509
+ return;
1510
+ }
1511
+ if (msg.type === "tests") {
1512
+ renderTests(msg.tests || []);
1513
+ return;
1514
+ }
1515
+ if (msg.type === "test-bundle") {
1516
+ if (!msg.ok) {
1517
+ log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
1518
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
1519
+ runningTest = false;
1520
+ setRunBusy(running);
1521
+ if (logEl) logEl.classList.add("open");
1522
+ return;
1523
+ }
1524
+ for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
1525
+ runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
1526
+ log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
1527
+ setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
1528
+ }).catch(function (err) {
1529
+ log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
1530
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
1531
+ if (logEl) logEl.classList.add("open");
1532
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
1533
+ }).then(function () {
1534
+ runningTest = false;
1535
+ setRunBusy(running);
1536
+ });
1537
+ }
1538
+ }
1539
+ function httpJson(url, init) {
1540
+ return origFetch(url, init).then(function (res) {
1541
+ return res.json().then(function (body) {
1542
+ if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
1543
+ return body;
1544
+ });
1545
+ });
1546
+ }
1547
+ function postToHost(msg) {
1548
+ if (vscodeApi) {
1549
+ vscodeApi.postMessage(msg);
1550
+ return;
1551
+ }
1552
+ if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
1553
+ if (msg.type === "run") {
1554
+ httpJson("/api/bundle", { method: "POST" }).then(function (body) {
1555
+ handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
1556
+ }).catch(function (err) {
1557
+ handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
1558
+ });
1559
+ return;
1560
+ }
1561
+ if (msg.type === "run-error") {
1562
+ httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
1563
+ handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
1564
+ }).catch(function (err) {
1565
+ log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
1566
+ });
1567
+ return;
1568
+ }
1569
+ if (msg.type === "read-asset") {
1570
+ httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
1571
+ handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
1572
+ }).catch(function (err) {
1573
+ handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
1574
+ });
1575
+ return;
1576
+ }
1577
+ if (msg.type === "list-tests") {
1578
+ httpJson("/api/tests").then(function (body) {
1579
+ handleHostMessage({ type: "tests", tests: body.tests || [] });
1580
+ }).catch(function (err) {
1581
+ log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
1582
+ });
1583
+ return;
1584
+ }
1585
+ if (msg.type === "run-test") {
1586
+ httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
1587
+ handleHostMessage(Object.assign({ type: "test-bundle" }, body));
1588
+ }).catch(function (err) {
1589
+ handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
1590
+ });
1591
+ }
1592
+ }
1593
+ function showStatus(text, isError) {
1594
+ if (!statusEl) return;
1595
+ statusEl.textContent = text;
1596
+ statusEl.className = isError ? "error" : "";
1597
+ statusEl.hidden = false;
1598
+ if (isError && logEl) logEl.classList.add("open");
1599
+ }
1600
+ function hideStatus() { if (statusEl) statusEl.hidden = true; }
1601
+ function formatError(err) {
1602
+ var msg = err && err.message ? err.message : String(err);
1603
+ var stack = err && err.stack ? "\\n" + err.stack : "";
1604
+ return msg + stack;
1605
+ }
1606
+ window.addEventListener("message", function (ev) {
1607
+ handleHostMessage(ev.data);
1608
+ });
1609
+ function b64ToBuf(b64) {
1610
+ var bin = atob(b64);
1611
+ var bytes = new Uint8Array(bin.length);
1612
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1613
+ return bytes.buffer;
1614
+ }
1615
+ function proxyFetch(url, method) {
1616
+ return new Promise(function (resolve, reject) {
1617
+ if (!vscodeApi) {
1618
+ reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
1619
+ return;
1620
+ }
1621
+ var id = nextId();
1622
+ pending[id] = { resolve: resolve, reject: reject };
1623
+ vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
1624
+ }).then(function (msg) {
1625
+ var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
1626
+ return new Response(buf, {
1627
+ status: msg.status || 0,
1628
+ statusText: msg.statusText || "",
1629
+ headers: msg.headers || {}
1630
+ });
1631
+ });
1632
+ }
1633
+ function isHttpUrl(url) {
1634
+ return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
1635
+ }
1636
+ function isPlainHttp(url) {
1637
+ return typeof url === "string" && url.indexOf("http://") === 0;
1638
+ }
1639
+ if (hostKind === "vscode") {
1640
+ window.fetch = function (input, init) {
1641
+ var url = typeof input === "string" ? input : (input && input.url);
1642
+ log("fetch " + url);
1643
+ if (isPlainHttp(url)) {
1644
+ return proxyFetch(url, init && init.method).then(function (res) {
1645
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
1646
+ return res;
1647
+ });
1648
+ }
1649
+ return origFetch(input, init).catch(function (err) {
1650
+ log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
1651
+ if (!isHttpUrl(url)) throw err;
1652
+ return proxyFetch(url, init && init.method).then(function (res) {
1653
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
1654
+ return res;
1655
+ });
1656
+ });
1657
+ };
1658
+ var origOpen = XMLHttpRequest.prototype.open;
1659
+ var origSend = XMLHttpRequest.prototype.send;
1660
+ XMLHttpRequest.prototype.open = function (method, url) {
1661
+ this.__etMethod = method;
1662
+ this.__etUrl = String(url);
1663
+ return origOpen.apply(this, arguments);
1664
+ };
1665
+ XMLHttpRequest.prototype.send = function (body) {
1666
+ var xhr = this;
1667
+ var url = xhr.__etUrl;
1668
+ if (!isPlainHttp(url)) return origSend.call(this, body);
1669
+ log("xhr proxy " + xhr.__etMethod + " " + url);
1670
+ proxyFetch(url, xhr.__etMethod).then(function (res) {
1671
+ return res.arrayBuffer().then(function (buf) {
1672
+ var text = "";
1673
+ try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
1674
+ Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
1675
+ Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
1676
+ Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
1677
+ Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
1678
+ var rt = xhr.responseType;
1679
+ var response = buf;
1680
+ if (rt === "" || rt === "text") response = text;
1681
+ else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
1682
+ Object.defineProperty(xhr, "response", { configurable: true, value: response });
1683
+ Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
1684
+ if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
1685
+ xhr.dispatchEvent(new Event("load"));
1686
+ xhr.dispatchEvent(new Event("loadend"));
1687
+ });
1688
+ }).catch(function (err) {
1689
+ log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
1690
+ if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
1691
+ xhr.dispatchEvent(new Event("error"));
1692
+ xhr.dispatchEvent(new Event("loadend"));
1693
+ });
1694
+ };
1695
+ function patchHttpSrc(proto, prop) {
1696
+ var desc = Object.getOwnPropertyDescriptor(proto, prop);
1697
+ if (!desc || typeof desc.set !== "function") return;
1698
+ Object.defineProperty(proto, prop, {
1699
+ configurable: true,
1700
+ enumerable: desc.enumerable,
1701
+ get: function () { return desc.get.call(this); },
1702
+ set: function (value) {
1703
+ var el = this;
1704
+ var url = String(value);
1705
+ if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
1706
+ log("media proxy " + url);
1707
+ proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
1708
+ desc.set.call(el, URL.createObjectURL(blob));
1709
+ }).catch(function (err) {
1710
+ log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
1711
+ try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
1712
+ });
1713
+ }
1714
+ });
1715
+ }
1716
+ patchHttpSrc(HTMLImageElement.prototype, "src");
1717
+ patchHttpSrc(HTMLMediaElement.prototype, "src");
1718
+ }
1719
+ document.getElementById("btn-debug").addEventListener("click", function () {
1720
+ if (logEl) logEl.classList.toggle("open");
1721
+ });
1722
+ var btnDevtools = document.getElementById("btn-devtools");
1723
+ var btnOutput = document.getElementById("btn-output");
1724
+ if (hostKind === "http") {
1725
+ if (btnDevtools) btnDevtools.hidden = true;
1726
+ if (btnOutput) btnOutput.hidden = true;
1727
+ }
1728
+ if (btnDevtools) btnDevtools.addEventListener("click", function () {
1729
+ postToHost({ type: "open-devtools" });
1730
+ });
1731
+ if (btnOutput) btnOutput.addEventListener("click", function () {
1732
+ postToHost({ type: "show-output" });
1733
+ });
1734
+ function setRunStatus(text) {
1735
+ var el = document.getElementById("run-status");
1736
+ if (el) el.textContent = text;
1737
+ }
1738
+ function setRunBusy(busy) {
1739
+ running = busy;
1740
+ var btn = document.getElementById("btn-run");
1741
+ if (btn) btn.disabled = !!busy || runningTest;
1742
+ syncTestControls();
1743
+ }
1744
+ function syncTestControls() {
1745
+ var panel = document.getElementById("test-panel");
1746
+ if (!panel) return;
1747
+ var disabled = !testsReady || running || runningTest;
1748
+ var nodes = panel.querySelectorAll("button.test-run, input.test-input");
1749
+ for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
1750
+ }
1751
+ function renderTests(tests) {
1752
+ var list = document.getElementById("test-list");
1753
+ if (!list) return;
1754
+ list.textContent = "";
1755
+ if (!tests || !tests.length) {
1756
+ var empty = document.createElement("span");
1757
+ empty.className = "test-empty";
1758
+ empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
1759
+ list.appendChild(empty);
1760
+ return;
1761
+ }
1762
+ var groups = {};
1763
+ var order = [];
1764
+ for (var i = 0; i < tests.length; i++) {
1765
+ var t = tests[i];
1766
+ if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
1767
+ groups[t.file].push(t);
1768
+ }
1769
+ for (var g = 0; g < order.length; g++) {
1770
+ var file = order[g];
1771
+ var heading = document.createElement("span");
1772
+ heading.className = "test-file";
1773
+ heading.textContent = file;
1774
+ list.appendChild(heading);
1775
+ var items = groups[file];
1776
+ for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
1777
+ }
1778
+ syncTestControls();
1779
+ }
1780
+ function makeTestControl(t) {
1781
+ var wrap = document.createElement("span");
1782
+ wrap.className = "test-item";
1783
+ if (t.hasInput) {
1784
+ var input = document.createElement("input");
1785
+ input.type = "text";
1786
+ input.className = "test-input";
1787
+ input.placeholder = t.inputName || "input";
1788
+ var btn = document.createElement("button");
1789
+ btn.type = "button";
1790
+ btn.className = "test-run";
1791
+ btn.textContent = t.name;
1792
+ btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
1793
+ input.addEventListener("keydown", function (ev) {
1794
+ if (ev.key === "Enter") requestRunTest(t.id, input.value);
1795
+ });
1796
+ wrap.appendChild(input);
1797
+ wrap.appendChild(btn);
1798
+ } else {
1799
+ var only = document.createElement("button");
1800
+ only.type = "button";
1801
+ only.className = "test-run";
1802
+ only.textContent = t.name;
1803
+ only.addEventListener("click", function () { requestRunTest(t.id); });
1804
+ wrap.appendChild(only);
1805
+ }
1806
+ return wrap;
1807
+ }
1808
+ function requestRunTest(id, input) {
1809
+ if (!testsReady || running || runningTest) return;
1810
+ if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
1811
+ runningTest = true;
1812
+ setRunBusy(running);
1813
+ setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
1814
+ log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
1815
+ postToHost({ type: "run-test", id: id, input: input });
1816
+ }
1817
+ async function runExportedTest(code, exportName, input, hasInput) {
1818
+ var blob = new Blob([code], { type: "text/javascript" });
1819
+ var url = URL.createObjectURL(blob);
1820
+ try {
1821
+ var mod = await import(url);
1822
+ var fn = mod[exportName];
1823
+ if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
1824
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
1825
+ var ctx = host.getTestContext();
1826
+ var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
1827
+ await Promise.resolve(result);
1828
+ } finally {
1829
+ URL.revokeObjectURL(url);
1830
+ }
1831
+ }
1832
+ function applyRenderPatch() {
1833
+ // runtime \u6E32\u67D3\u5206\u652F:\u7248\u672C <= 0.0.31 \u8D70\u666E\u901A/\u540E\u5904\u7406\u6E32\u67D3,\u7248\u672C > 0.0.31 \u8981\u6C42\u5B58\u5728 postprocessingComponent,
1834
+ // \u5426\u5219\u6240\u6709\u5206\u652F\u90FD\u88AB\u8DF3\u8FC7(renderer.info.render.frame \u6052\u4E3A 0)\u5BFC\u81F4\u9ED1\u5C4F\u3002\u6B64\u5904\u6309 runtime \u8BED\u4E49\u515C\u5E95\u3002
1835
+ var scene = engine && engine.mainScene;
1836
+ if (
1837
+ scene && scene.rootComponent && scene.rootComponent.version &&
1838
+ !scene.postprocessingComponent &&
1839
+ runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
1840
+ ) {
1841
+ log("\u8865\u6E32\u67D3:\u573A\u666F\u7248\u672C " + scene.rootComponent.version + " \u65E0\u540E\u5904\u7406\u7EC4\u4EF6,runtime \u9AD8\u7248\u672C\u6E32\u67D3\u5206\u652F\u4E3A\u7A7A,\u6CE8\u518C\u6BCF\u5E27\u6E32\u67D3\u56DE\u8C03");
1842
+ scene.registerRenderCallback(function () {
1843
+ scene.renderer.render(scene.sceneObject, scene.camera.main);
1844
+ });
1845
+ }
1846
+ }
1847
+ async function loadHostScene(nextEngine, nextJson) {
1848
+ engine = nextEngine;
1849
+ var sceneManager = engine.getManager(runtime.SceneManager);
1850
+ await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
1851
+ applyRenderPatch();
1852
+ }
1853
+ function readAsset(relPath) {
1854
+ return new Promise(function (resolve, reject) {
1855
+ var id = nextId();
1856
+ pending[id] = { resolve: resolve, reject: reject };
1857
+ postToHost({ type: "read-asset", id: id, path: relPath });
1858
+ });
1859
+ }
1860
+ async function runUserCode(code) {
1861
+ setRunStatus("\u8FD0\u884C\u4E2D\u2026");
1862
+ try {
1863
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
1864
+ await host.run(code);
1865
+ log("Run \u5B8C\u6210");
1866
+ setRunStatus("\u8FD0\u884C\u4E2D");
1867
+ } catch (err) {
1868
+ log("Run \u5931\u8D25 " + formatError(err));
1869
+ setRunStatus("\u5931\u8D25");
1870
+ if (logEl) logEl.classList.add("open");
1871
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
1872
+ } finally {
1873
+ setRunBusy(false);
1874
+ }
1875
+ }
1876
+ document.getElementById("btn-run").addEventListener("click", function () {
1877
+ if (running || runningTest) return;
1878
+ if (!engine) {
1879
+ log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
1880
+ return;
1881
+ }
1882
+ setRunBusy(true);
1883
+ setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
1884
+ log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
1885
+ postToHost({ type: "run" });
1886
+ });
1887
+ document.getElementById("btn-refresh-tests").addEventListener("click", function () {
1888
+ log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
1889
+ postToHost({ type: "list-tests" });
1890
+ });
1891
+ function readEmbeddedTests() {
1892
+ var el = document.getElementById("workspace-tests");
1893
+ if (!el || !el.textContent) return [];
1894
+ try { return JSON.parse(el.textContent); } catch (e) { return []; }
1895
+ }
1896
+ renderTests(readEmbeddedTests());
1897
+ function normalizeHierarchyConfig(vo) {
1898
+ var objs = vo && vo.objs ? vo.objs : [];
1899
+ for (var i = 0; i < objs.length; i++) {
1900
+ var hc = objs[i].hierarchyConfig || {};
1901
+ if (typeof hc.active !== "boolean") {
1902
+ hc.active = hc.inActive === true ? false : hc.visible !== false;
1903
+ }
1904
+ if (typeof hc.lock !== "boolean") hc.lock = false;
1905
+ if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
1906
+ objs[i].hierarchyConfig = hc;
1907
+ }
1908
+ return vo;
1909
+ }
1910
+ function toSceneJson(runtime, raw) {
1911
+ var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
1912
+ if (vo && vo.sceneComponent) {
1913
+ return {
1914
+ id: vo.id || "preview",
1915
+ name: vo.name || "\u573A\u666F\u9884\u89C8",
1916
+ sceneComponent: vo.sceneComponent
1917
+ };
1918
+ }
1919
+ vo = normalizeHierarchyConfig(vo);
1920
+ var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
1921
+ var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
1922
+ return {
1923
+ id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
1924
+ name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
1925
+ sceneComponent: runtime.convertObjToComponentJson(vo)
1926
+ };
1927
+ }
1928
+ try {
1929
+ showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
1930
+ log("runtimeUri=" + __RUNTIME_URI__);
1931
+ log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
1932
+ log("1/4 import twin-runtime / TwinApp host");
1933
+ runtime = await import("@easytwin/runtime");
1934
+ var hostMod = await import(__HOST_URI__);
1935
+ log("2/4 parse scene JSON");
1936
+ var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
1937
+ sceneJson = toSceneJson(runtime, sceneVo);
1938
+ log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
1939
+ log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
1940
+ host = new hostMod.TwinAppPreviewHost({
1941
+ runtime: runtime,
1942
+ containerId: "twin-root",
1943
+ ossUrl: __BASE_OSS_URL__,
1944
+ appId: __APP_ID__,
1945
+ sceneId: sceneJson.id,
1946
+ sceneJson: sceneJson,
1947
+ customComponentDeps: {
1948
+ "@easytwin/runtime": runtime,
1949
+ "@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
1950
+ react: { createElement: function () { return null; }, Fragment: "div" }
1951
+ },
1952
+ loadScene: loadHostScene,
1953
+ readAsset: readAsset,
1954
+ log: log
1955
+ });
1956
+ await host.boot();
1957
+ engine = host.getEngine();
1958
+ log("4/4 loadScene");
1959
+ log("\u5B8C\u6210");
1960
+ hideStatus();
1961
+ testsReady = true;
1962
+ setRunBusy(false);
1963
+ } catch (err) {
1964
+ log("\u5931\u8D25 " + formatError(err));
1965
+ showStatus("\u573A\u666F\u6E32\u67D3\u5931\u8D25: " + (err && err.message ? err.message : String(err)) + "\\n(\u70B9\u53F3\u4E0B\u89D2\u300C\u8C03\u8BD5\u65E5\u5FD7\u300D\u67E5\u770B\u6B65\u9AA4\u4E0E\u8BF7\u6C42 URL;\u300C\u5F00\u53D1\u8005\u5DE5\u5177\u300D\u6253\u5F00 webview DevTools)", true);
1966
+ }
1967
+ window.addEventListener("pagehide", function () {
1968
+ if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
1969
+ });
1970
+ `;
1971
+ function originOf(url) {
1972
+ return new URL(url).origin;
1973
+ }
1974
+ function buildPreviewHtml(options) {
1975
+ const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
1976
+ const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
1977
+ const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
1978
+ const appId = options.appId ?? "preview";
1979
+ const hostKind = options.host ?? "vscode";
1980
+ const apiOrigin = originOf(baseUrl);
1981
+ const ossOrigin = originOf(ossUrl);
1982
+ const runtimeOrigin = originOf(runtimeUri);
1983
+ const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
1984
+ const renderScript = RENDER_SCRIPT.replaceAll("__RUNTIME_URI__", JSON.stringify(runtimeUri)).replaceAll("__HOST_URI__", JSON.stringify(hostUri)).replaceAll("__BASE_OSS_URL__", JSON.stringify(ossUrl)).replaceAll("__APP_ID__", JSON.stringify(appId)).replaceAll("__HOST_KIND__", JSON.stringify(hostKind));
1985
+ const importMap = JSON.stringify({
1986
+ imports: {
1987
+ "@easytwin/runtime": runtimeUri,
1988
+ "@easytwin/apps": appsUri
1989
+ }
1990
+ });
1991
+ const testsJson = JSON.stringify(options.tests ?? []);
1992
+ const mockBanner = mock ? `<div id="mock-banner"><b>\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F</b>:\u573A\u666F\u6570\u636E\u6765\u81EA\u672C\u5730 <code>scene.example.json</code>,\u672A\u8BF7\u6C42\u573A\u666F\u63A5\u53E3;\u4E09\u7EF4\u9884\u89C8\u8D70\u6253\u5305\u7684 twin runtime,\u7CFB\u7EDF\u5E93\u6309\u5B98\u65B9 OSS \u5728\u7EBF\u52A0\u8F7D\u3002</div>` : "";
1993
+ return `<!DOCTYPE html>
1994
+ <html lang="zh-CN">
1995
+ <head>
1996
+ <meta charset="UTF-8">
1997
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc}; worker-src blob: data: 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc}; child-src blob: data:; img-src ${apiOrigin} ${ossOrigin} ${resourceSrc} https: http: data: blob:; media-src ${apiOrigin} ${ossOrigin} https: http: data: blob:; connect-src 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc} https: http: data: blob:; font-src ${apiOrigin} ${ossOrigin} https: data:;">
1998
+ <title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
1999
+ <style>${PREVIEW_STYLE}</style>
2000
+ <script type="importmap">${importMap}</script>
2001
+ </head>
2002
+ <body>
2003
+ <div id="stage">
2004
+ <div id="twin-root"></div>
2005
+ ${mockBanner}
2006
+ <div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
2007
+ <pre id="debug-log"></pre>
2008
+ </div>
2009
+ <div id="test-panel">
2010
+ <span class="test-label">\u6D4B\u8BD5</span>
2011
+ <div id="test-list"></div>
2012
+ <button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
2013
+ </div>
2014
+ <div id="run-bar">
2015
+ <button type="button" id="btn-run">Run</button>
2016
+ <span id="run-status">\u5C31\u7EEA</span>
2017
+ <span class="spacer"></span>
2018
+ <button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
2019
+ <button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
2020
+ <button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
2021
+ </div>
2022
+ <script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
2023
+ <script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
2024
+ <script type="module">
2025
+ ${renderScript}
2026
+ </script>
2027
+ </body>
2028
+ </html>`;
2029
+ }
2030
+
2031
+ // src/workspaceTests.ts
2032
+ import { promises as fs7 } from "fs";
2033
+ import path8 from "path";
2034
+ var IDENT = "[A-Za-z_$][\\w$]*";
2035
+ var FUNCTION_EXPORT = new RegExp(
2036
+ `export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
2037
+ "g"
2038
+ );
2039
+ var CONST_EXPORT = new RegExp(
2040
+ `export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
2041
+ "g"
2042
+ );
2043
+ function stripTsCommentsAndStringsKeepCode(source) {
2044
+ let out = "";
2045
+ let i = 0;
2046
+ const n = source.length;
2047
+ while (i < n) {
2048
+ const c = source[i];
2049
+ const next = source[i + 1];
2050
+ if (c === "/" && next === "/") {
2051
+ i += 2;
2052
+ while (i < n && source[i] !== "\n") i++;
2053
+ continue;
2054
+ }
2055
+ if (c === "/" && next === "*") {
2056
+ i += 2;
2057
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
2058
+ i = Math.min(n, i + 2);
2059
+ out += " ";
2060
+ continue;
2061
+ }
2062
+ if (c === "'" || c === '"' || c === "`") {
2063
+ const quote = c;
2064
+ out += " ";
2065
+ i++;
2066
+ while (i < n) {
2067
+ const ch = source[i];
2068
+ if (ch === "\\") {
2069
+ i += 2;
2070
+ continue;
2071
+ }
2072
+ if (ch === quote) {
2073
+ i++;
2074
+ break;
2075
+ }
2076
+ i++;
2077
+ }
2078
+ continue;
2079
+ }
2080
+ out += c;
2081
+ i++;
2082
+ }
2083
+ return out;
2084
+ }
2085
+ function matchingClose(src, openIndex, open, close) {
2086
+ let depth = 0;
2087
+ let angle = 0;
2088
+ let brace = 0;
2089
+ for (let i = openIndex; i < src.length; i++) {
2090
+ const c = src[i];
2091
+ if (c === open) depth++;
2092
+ else if (c === close) {
2093
+ depth--;
2094
+ if (depth === 0 && angle <= 0 && brace <= 0) return i;
2095
+ } else if (c === "<") angle++;
2096
+ else if (c === ">" && angle > 0) angle--;
2097
+ else if (c === "{") brace++;
2098
+ else if (c === "}" && brace > 0) brace--;
2099
+ }
2100
+ return -1;
2101
+ }
2102
+ function splitTopLevelParams(list) {
2103
+ const params = [];
2104
+ let current = "";
2105
+ let paren = 0;
2106
+ let angle = 0;
2107
+ let brace = 0;
2108
+ let bracket = 0;
2109
+ for (const c of list) {
2110
+ if (c === "(") paren++;
2111
+ else if (c === ")") paren--;
2112
+ else if (c === "<") angle++;
2113
+ else if (c === ">" && angle > 0) angle--;
2114
+ else if (c === "{") brace++;
2115
+ else if (c === "}" && brace > 0) brace--;
2116
+ else if (c === "[") bracket++;
2117
+ else if (c === "]" && bracket > 0) bracket--;
2118
+ if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
2119
+ if (current.trim()) params.push(current.trim());
2120
+ current = "";
2121
+ continue;
2122
+ }
2123
+ current += c;
2124
+ }
2125
+ if (current.trim()) params.push(current.trim());
2126
+ return params;
2127
+ }
2128
+ function paramName(raw) {
2129
+ let s = raw.trim();
2130
+ if (!s || s === "this") return void 0;
2131
+ if (s.startsWith("{") || s.startsWith("[")) return "input";
2132
+ s = s.replace(/^\.\.\./, "");
2133
+ const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
2134
+ if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
2135
+ return token;
2136
+ }
2137
+ function collectFromPattern(source, pattern) {
2138
+ const found = [];
2139
+ pattern.lastIndex = 0;
2140
+ let match;
2141
+ while (match = pattern.exec(source)) {
2142
+ const name = match[1];
2143
+ if (!name) continue;
2144
+ const openIndex = match.index + match[0].length - 1;
2145
+ const closeIndex = matchingClose(source, openIndex, "(", ")");
2146
+ if (closeIndex < 0) continue;
2147
+ const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
2148
+ const hasInput = params.length >= 2;
2149
+ const second = hasInput ? paramName(params[1] ?? "") : void 0;
2150
+ found.push({
2151
+ id: name,
2152
+ file: "",
2153
+ name,
2154
+ hasInput,
2155
+ inputName: hasInput ? second : void 0
2156
+ });
2157
+ }
2158
+ return found;
2159
+ }
2160
+ function parseExportedTestFunctions(source) {
2161
+ const text = stripTsCommentsAndStringsKeepCode(source);
2162
+ const seen = /* @__PURE__ */ new Set();
2163
+ const out = [];
2164
+ for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
2165
+ if (seen.has(item.name)) continue;
2166
+ seen.add(item.name);
2167
+ out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
2168
+ }
2169
+ return out;
2170
+ }
2171
+ function workspaceTestId(file, name) {
2172
+ return `${normalizePath(file)}:${name}`;
2173
+ }
2174
+ async function listWorkspaceTests(cwd) {
2175
+ const root = path8.resolve(cwd);
2176
+ let files;
2177
+ try {
2178
+ files = await collectFiles(root, isIgnoredWorkspacePath);
2179
+ } catch (err) {
2180
+ const code = err.code;
2181
+ if (code === "ENOENT") return [];
2182
+ throw err;
2183
+ }
2184
+ const tests = [];
2185
+ for (const file of files) {
2186
+ if (!isWorkspaceSpecFile(file.relPath)) continue;
2187
+ const posix = normalizePath(file.relPath);
2188
+ const source = await fs7.readFile(file.absPath, "utf8");
2189
+ for (const fn of parseExportedTestFunctions(source)) {
2190
+ tests.push({
2191
+ id: workspaceTestId(posix, fn.name),
2192
+ file: posix,
2193
+ name: fn.name,
2194
+ hasInput: fn.hasInput,
2195
+ inputName: fn.inputName
2196
+ });
2197
+ }
2198
+ }
2199
+ tests.sort((a, b) => a.id.localeCompare(b.id));
2200
+ return tests;
2201
+ }
2202
+ async function bundleWorkspaceTestFile(options) {
2203
+ const file = normalizePath(options.file);
2204
+ return bundleWorkspaceModule({
2205
+ cwd: options.cwd,
2206
+ entry: file,
2207
+ missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
2208
+ });
2209
+ }
2210
+
2211
+ // src/previewServer.ts
2212
+ var DEFAULT_PREVIEW_PORT = 4173;
2213
+ var DEFAULT_PREVIEW_HOST = "127.0.0.1";
2214
+ var RUNTIME_FILES = /* @__PURE__ */ new Set(["twin-runtime.js", "twin-apps.js", "twin-app-host.js"]);
2215
+ function formatPreviewReadyMessage(info) {
2216
+ const lines = [
2217
+ `EasyTwin \u9884\u89C8: ${info.url}`,
2218
+ `\u573A\u666F: ${info.sceneId}`,
2219
+ "\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u4E0A\u8FF0\u5730\u5740\u3002\u70B9 Run \u8FD0\u884C TwinApp;\u6D4B\u8BD5\u6309\u94AE\u5728 Run \u680F\u4E0A\u65B9\u3002Ctrl+C \u505C\u6B62\u3002"
2220
+ ];
2221
+ if (info.mock) lines.unshift("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u573A\u666F\u6570\u636E\u6765\u81EA\u672C\u5730\u793A\u4F8B,\u672A\u8BF7\u6C42\u573A\u666F\u63A5\u53E3");
2222
+ return lines.join("\n");
2223
+ }
2224
+ function resolvePreviewSceneId(scenes, requested) {
2225
+ const id = requested?.trim() ?? "";
2226
+ if (id.length > 0) return id;
2227
+ const list = scenes ?? [];
2228
+ const preferred = list.find((s) => s.defaultLoading === true) ?? list[0];
2229
+ if (preferred) return preferred.id;
2230
+ throw new Error("\u672A\u6307\u5B9A\u573A\u666F\u3002\u8BF7\u4F20\u5165 Scene Key,\u6216\u5148\u8FD0\u884C easytwin scene list");
2231
+ }
2232
+ function resolvePreviewRuntimeDir() {
2233
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
2234
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u9884\u89C8 runtime:\u63D2\u4EF6/CJS \u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 runtimeDir");
2235
+ }
2236
+ const here = path9.dirname(fileURLToPath3(import.meta.url));
2237
+ const candidates = [
2238
+ path9.join(here, "runtime"),
2239
+ path9.resolve(here, "..", "dist", "runtime"),
2240
+ path9.resolve(here, "runtime")
2241
+ ];
2242
+ for (const dir of candidates) {
2243
+ if (existsSync2(path9.join(dir, "twin-runtime.js"))) return dir;
2244
+ }
2245
+ throw new Error(
2246
+ `\u672A\u627E\u5230\u9884\u89C8 runtime(twin-runtime.js)\u3002\u5DF2\u5C1D\u8BD5:${candidates.join(", ")}\u3002\u8BF7\u5148\u6784\u5EFA @easytwin/devkit\u3002`
2247
+ );
2248
+ }
2249
+ function mimeFor(file) {
2250
+ if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
2251
+ return "application/octet-stream";
2252
+ }
2253
+ function sendJson(res, body, status = 200) {
2254
+ res.statusCode = status;
2255
+ res.setHeader("content-type", "application/json; charset=utf-8");
2256
+ res.setHeader("cache-control", "no-store");
2257
+ res.end(JSON.stringify(body));
2258
+ }
2259
+ function sendText(res, body, status, contentType) {
2260
+ res.statusCode = status;
2261
+ res.setHeader("content-type", contentType);
2262
+ res.end(body);
2263
+ }
2264
+ async function readJsonBody(req) {
2265
+ const chunks = [];
2266
+ for await (const chunk of req) chunks.push(chunk);
2267
+ if (chunks.length === 0) return {};
2268
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
2269
+ if (raw.length === 0) return {};
2270
+ return JSON.parse(raw);
2271
+ }
2272
+ function asRecord3(value) {
2273
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2274
+ }
2275
+ async function startPreviewServer(options) {
2276
+ const cwd = path9.resolve(options.cwd);
2277
+ const config = await loadConfig(cwd);
2278
+ const fileConfig = await readConfigFile(cwd);
2279
+ const runtimeDir = options.runtimeDir ?? resolvePreviewRuntimeDir();
2280
+ if (!existsSync2(path9.join(runtimeDir, "twin-runtime.js"))) {
2281
+ throw new Error(`\u9884\u89C8 runtime \u76EE\u5F55\u7F3A\u5C11 twin-runtime.js:${runtimeDir}`);
2282
+ }
2283
+ let sceneId;
2284
+ try {
2285
+ sceneId = resolvePreviewSceneId(fileConfig.scenes, options.sceneId);
2286
+ } catch (err) {
2287
+ if (config.mock) {
2288
+ sceneId = deriveExampleSceneId(await loadExampleScene());
2289
+ } else {
2290
+ throw err;
2291
+ }
2292
+ }
2293
+ const client = new EasyTwinClient(config);
2294
+ const scene = await pullScene(client, sceneId);
2295
+ const tests = await listWorkspaceTests(cwd);
2296
+ const hostname = options.hostname ?? DEFAULT_PREVIEW_HOST;
2297
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
2298
+ `));
2299
+ let lastBundledCode;
2300
+ const originRef = { url: "" };
2301
+ const html = () => buildPreviewHtml({
2302
+ baseUrl: config.baseUrl,
2303
+ ossUrl: config.ossUrl,
2304
+ sceneJson: JSON.stringify(scene.payload ?? scene, null, 2),
2305
+ mock: config.mock,
2306
+ runtimeUri: `${originRef.url}/runtime/twin-runtime.js`,
2307
+ appsUri: `${originRef.url}/runtime/twin-apps.js`,
2308
+ hostUri: `${originRef.url}/runtime/twin-app-host.js`,
2309
+ appId: config.appId,
2310
+ host: "http",
2311
+ tests
2312
+ });
2313
+ const server = http2.createServer((req, res) => {
2314
+ void handle(req, res);
2315
+ });
2316
+ async function handle(req, res) {
2317
+ try {
2318
+ const url = new URL(req.url ?? "/", originRef.url || `http://${hostname}`);
2319
+ const method = req.method ?? "GET";
2320
+ if (method === "GET" && url.pathname === "/") {
2321
+ sendText(res, html(), 200, "text/html; charset=utf-8");
2322
+ return;
2323
+ }
2324
+ if (method === "GET" && url.pathname.startsWith("/runtime/")) {
2325
+ const name = path9.posix.basename(url.pathname);
2326
+ if (!RUNTIME_FILES.has(name)) {
2327
+ sendText(res, "not found", 404, "text/plain; charset=utf-8");
2328
+ return;
2329
+ }
2330
+ const file = path9.join(runtimeDir, name);
2331
+ const buf = await fs8.readFile(file);
2332
+ res.statusCode = 200;
2333
+ res.setHeader("content-type", mimeFor(name));
2334
+ res.setHeader("cache-control", "no-store");
2335
+ res.end(buf);
2336
+ return;
2337
+ }
2338
+ if (method === "POST" && url.pathname === "/api/bundle") {
2339
+ try {
2340
+ const result = await bundleUserCode({ cwd });
2341
+ lastBundledCode = result.code;
2342
+ for (const warning of result.warnings) log(`[run] \u8B66\u544A ${warning}`);
2343
+ sendJson(res, { ok: true, code: result.code, warnings: result.warnings });
2344
+ } catch (err) {
2345
+ const error = err instanceof Error ? err.message : String(err);
2346
+ log(`[run] \u7F16\u8BD1\u5931\u8D25 ${error}`);
2347
+ sendJson(res, { ok: false, error });
2348
+ }
2349
+ return;
2350
+ }
2351
+ if (method === "GET" && url.pathname === "/api/tests") {
2352
+ sendJson(res, { tests: await listWorkspaceTests(cwd) });
2353
+ return;
2354
+ }
2355
+ if (method === "POST" && url.pathname === "/api/test-bundle") {
2356
+ const body = asRecord3(await readJsonBody(req));
2357
+ const id = typeof body.id === "string" ? body.id : "";
2358
+ const input = typeof body.input === "string" ? body.input : void 0;
2359
+ try {
2360
+ const listed = await listWorkspaceTests(cwd);
2361
+ const item = listed.find((t) => t.id === id);
2362
+ if (!item) throw new Error(`\u672A\u627E\u5230\u6D4B\u8BD5 ${id}`);
2363
+ log(`[test] bundle ${item.file} :: ${item.name}`);
2364
+ const result = await bundleWorkspaceTestFile({ cwd, file: item.file });
2365
+ lastBundledCode = result.code;
2366
+ for (const warning of result.warnings) log(`[test] \u8B66\u544A ${warning}`);
2367
+ sendJson(res, {
2368
+ ok: true,
2369
+ code: result.code,
2370
+ exportName: item.name,
2371
+ input,
2372
+ hasInput: item.hasInput,
2373
+ warnings: result.warnings
2374
+ });
2375
+ } catch (err) {
2376
+ const error = err instanceof Error ? err.message : String(err);
2377
+ log(`[test] \u7F16\u8BD1\u5931\u8D25 ${error}`);
2378
+ sendJson(res, { ok: false, error });
2379
+ }
2380
+ return;
2381
+ }
2382
+ if (method === "GET" && url.pathname === "/api/assets") {
2383
+ const rel = url.searchParams.get("path") ?? "";
2384
+ try {
2385
+ const file = resolveWorkspaceAssetPath(cwd, rel);
2386
+ sendJson(res, { text: await fs8.readFile(file, "utf8") });
2387
+ } catch (err) {
2388
+ const error = err instanceof Error ? err.message : String(err);
2389
+ log(`[assets] FAIL ${rel} ${error}`);
2390
+ sendJson(res, { error });
2391
+ }
2392
+ return;
2393
+ }
2394
+ if (method === "POST" && url.pathname === "/api/remap-error") {
2395
+ const body = asRecord3(await readJsonBody(req));
2396
+ const stack = typeof body.stack === "string" ? body.stack : "";
2397
+ sendJson(res, { stack: lastBundledCode ? remapErrorStack(stack, lastBundledCode) : stack });
2398
+ return;
2399
+ }
2400
+ sendText(res, "not found", 404, "text/plain; charset=utf-8");
2401
+ } catch (err) {
2402
+ const error = err instanceof Error ? err.message : String(err);
2403
+ log(`[preview] ${error}`);
2404
+ if (!res.headersSent) sendJson(res, { error }, 500);
2405
+ }
2406
+ }
2407
+ const port = options.port ?? DEFAULT_PREVIEW_PORT;
2408
+ await new Promise((resolve, reject) => {
2409
+ server.once("error", reject);
2410
+ server.listen(port, hostname, () => resolve());
2411
+ });
2412
+ const address = server.address();
2413
+ const bound = typeof address === "object" && address !== null ? address.port : port;
2414
+ originRef.url = `http://${hostname}:${bound}`;
2415
+ return {
2416
+ url: originRef.url,
2417
+ port: bound,
2418
+ sceneId,
2419
+ mock: config.mock,
2420
+ close: () => new Promise((resolve, reject) => {
2421
+ server.close((err) => err ? reject(err) : resolve());
2422
+ })
2423
+ };
2424
+ }
741
2425
 
742
2426
  // src/bin.ts
743
2427
  async function readVersion() {
@@ -748,7 +2432,7 @@ async function readVersion() {
748
2432
  return "0.0.0";
749
2433
  }
750
2434
  }
751
- var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20\u8D70\u672C\u5730 mock";
2435
+ var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20/\u5DE5\u4F5C\u533A\u62C9\u53D6\u8D70\u672C\u5730 mock";
752
2436
  async function prompt(question) {
753
2437
  const rl = createInterface({ input: stdin, output: stdout });
754
2438
  try {
@@ -783,31 +2467,41 @@ function buildProgram(cwd, version = "0.0.0") {
783
2467
  program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
784
2468
  program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--env <prod|test>", "\u670D\u52A1\u73AF\u5883(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
785
2469
  const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
786
- scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F").action(async () => {
2470
+ scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
787
2471
  const config = await loadConfig(cwd);
788
2472
  if (config.mock) console.log(MOCK_MODE_HINT);
789
2473
  const client = new EasyTwinClient(config);
790
- const scenes = await listScenes(client);
2474
+ const scenes = await listScenes(client, { cwd });
791
2475
  if (scenes.length === 0) {
792
2476
  console.log("(\u65E0\u573A\u666F)");
793
- return;
2477
+ } else {
2478
+ for (const s of scenes) console.log(`${s.id} ${s.name}`);
794
2479
  }
795
- for (const s of scenes) console.log(`${s.id} ${s.name}`);
2480
+ console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
796
2481
  });
797
2482
  scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ./<id>.scene.json)").action(async (id, opts) => {
798
2483
  const config = await loadConfig(cwd);
799
2484
  if (config.mock) console.log(MOCK_MODE_HINT);
800
2485
  const client = new EasyTwinClient(config);
801
2486
  const scene2 = await pullScene(client, id);
802
- const out = opts.out ?? path6.join(cwd, `${id}.scene.json`);
2487
+ const out = opts.out ?? path10.join(cwd, `${id}.scene.json`);
803
2488
  const file = await saveSceneJson(scene2, out);
804
2489
  console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
805
2490
  });
806
- program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
2491
+ program.command("pull").description("\u62C9\u53D6\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5219\u5199\u5165\u9ED8\u8BA4 src/main.ts;\u51B2\u7A81\u9ED8\u8BA4\u4E0D\u8986\u76D6)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").option("--force", "\u7528\u8FDC\u7AEF\u5185\u5BB9\u8986\u76D6\u672C\u5730\u51B2\u7A81\u6587\u4EF6").option("--dry-run", "\u53EA\u6253\u5370 diff,\u4E0D\u5199\u76D8").action(async (dirArg, opts) => {
2492
+ const config = await loadConfig(cwd);
2493
+ if (config.mock) console.log(MOCK_MODE_HINT);
2494
+ const client = new EasyTwinClient(config);
2495
+ const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
2496
+ const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
2497
+ console.log(formatWorkspacePullResult(result));
2498
+ });
2499
+ program.command("upload").description("\u5BF9\u7167\u8FDC\u7AEF\u5168\u91CF\u5BF9\u9F50\u5DE5\u4F5C\u533A(\u8DF3\u8FC7\u63D2\u4EF6\u4EA7\u7269,\u4E0D\u53EF\u9006)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").action(async (dirArg) => {
807
2500
  const config = await loadConfig(cwd);
808
2501
  if (config.mock) console.log(MOCK_MODE_HINT);
809
2502
  const client = new EasyTwinClient(config);
810
- await uploadDirectory(client, dir, {
2503
+ const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
2504
+ const result = await uploadDirectory(client, target, {
811
2505
  onProgress: (p) => {
812
2506
  if (p.phase === "upload") {
813
2507
  process.stderr.write(`\r\u5DF2\u4E0A\u4F20 ${p.current}/${p.total} bytes${p.file ? ` (${p.file})` : ""}`);
@@ -815,14 +2509,29 @@ function buildProgram(cwd, version = "0.0.0") {
815
2509
  }
816
2510
  });
817
2511
  process.stderr.write("\n");
2512
+ console.log(formatUploadResult(result));
818
2513
  });
819
- program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8\u4F9D\u8D56 @easytwin/runtime)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
2514
+ program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8 @easytwin/runtime \u4E0E @easytwin/apps)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
820
2515
  const hint = await typesMissingHint(cwd);
821
2516
  if (hint) console.warn(hint);
822
- const outFile = path6.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
823
- await bundleUserCode({ cwd, outFile });
2517
+ const outFile = path10.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
2518
+ const result = await bundleUserCode({ cwd, outFile });
2519
+ for (const warning of result.warnings) console.warn(warning);
824
2520
  console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
825
2521
  });
2522
+ program.command("preview").description("\u542F\u52A8\u672C\u5730\u9884\u89C8\u9875(\u4E09\u7EF4\u573A\u666F + Run + \u6D4B\u8BD5\u63A7\u4EF6),\u6253\u5370 URL \u4F9B\u6D4F\u89C8\u5668\u6253\u5F00").argument("[sceneId]", "\u573A\u666F Scene Key(\u7F3A\u7701\u914D\u7F6E scenes \u4E2D defaultLoading \u6216\u9996\u9879)").option("-p, --port <port>", "\u7AEF\u53E3(\u7F3A\u7701 4173,\u53EA\u7ED1 127.0.0.1)", "4173").action(async (sceneId, opts) => {
2523
+ const port = Number.parseInt(opts.port, 10);
2524
+ if (!Number.isFinite(port) || port < 0) throw new Error(`\u65E0\u6548\u7AEF\u53E3:${opts.port}`);
2525
+ const server = await startPreviewServer({ cwd, sceneId, port });
2526
+ console.log(formatPreviewReadyMessage(server));
2527
+ await new Promise((resolve, reject) => {
2528
+ const stop = () => {
2529
+ server.close().then(resolve, reject);
2530
+ };
2531
+ process.once("SIGINT", stop);
2532
+ process.once("SIGTERM", stop);
2533
+ });
2534
+ });
826
2535
  const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
827
2536
  skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex/qoder),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|qoder|all", "all").action(async (opts) => {
828
2537
  const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
@@ -856,4 +2565,3 @@ export {
856
2565
  main,
857
2566
  runInit
858
2567
  };
859
- //# sourceMappingURL=bin.js.map