@easytwin/devkit 0.1.2 → 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/index.js CHANGED
@@ -4,9 +4,6 @@ import path from "path";
4
4
  var CONFIG_FILE_NAME = "easytwin.config.json";
5
5
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
6
6
  var TEST_BASE_URL = "http://172.16.125.3:10100/";
7
- var TEST_OP_ACCOUNT_ID = "25";
8
- var TEST_OP_USER_ID = "25";
9
- var TEST_SPACE_ID = "54";
10
7
  var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
11
8
  var MOCK_APP_ID = "test";
12
9
  var MOCK_APP_SECRET = "test";
@@ -51,29 +48,14 @@ function validateConfigShape(value) {
51
48
  if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
52
49
  throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
53
50
  }
54
- const opAccountId = optionalGatewayId(v.opAccountId);
55
- const opUserId = optionalGatewayId(v.opUserId);
56
- const spaceId = optionalGatewayId(v.spaceId);
57
51
  const config = { appId: v.appId, appSecret: v.appSecret };
58
52
  if (v.env === "prod" || v.env === "test") config.env = v.env;
59
53
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
60
54
  if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
61
- if (opAccountId) config.opAccountId = opAccountId;
62
- if (opUserId) config.opUserId = opUserId;
63
- if (spaceId) config.spaceId = spaceId;
64
55
  const scenes = parseConfigScenes(v.scenes);
65
56
  if (scenes) config.scenes = scenes;
66
57
  return config;
67
58
  }
68
- function optionalGatewayId(value) {
69
- if (typeof value === "string") {
70
- const t = value.trim();
71
- return t.length > 0 ? t : void 0;
72
- }
73
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
74
- if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
75
- return void 0;
76
- }
77
59
  function parseConfigScenes(value) {
78
60
  if (value === void 0) return void 0;
79
61
  if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
@@ -123,30 +105,11 @@ function resolveConfig(file, env = process.env) {
123
105
  if (!appId) throw new ConfigError("\u7F3A\u5C11 appId:\u914D\u7F6E\u6587\u4EF6\u4E2D\u672A\u63D0\u4F9B,\u4E14\u672A\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EASYTWIN_APP_ID");
124
106
  if (!appSecret) throw new ConfigError("\u7F3A\u5C11 appSecret:\u914D\u7F6E\u6587\u4EF6\u4E2D\u672A\u63D0\u4F9B,\u4E14\u672A\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EASYTWIN_APP_SECRET");
125
107
  const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
126
- const useTestGateway = easyEnv === "test";
127
- const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
128
- const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
129
- const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
130
- const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
131
- if (opAccountId) resolved.opAccountId = opAccountId;
132
- if (opUserId) resolved.opUserId = opUserId;
133
- if (spaceId) resolved.spaceId = spaceId;
134
- return resolved;
108
+ return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
135
109
  }
136
110
  async function loadConfig(cwd, env = process.env) {
137
111
  const file = await readConfigFile(cwd);
138
- const resolved = resolveConfig(file, env);
139
- if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
140
- if (!file.opAccountId || !file.opUserId || !file.spaceId) {
141
- await writeConfigFile(cwd, {
142
- ...file,
143
- opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
144
- opUserId: file.opUserId ?? TEST_OP_USER_ID,
145
- spaceId: file.spaceId ?? TEST_SPACE_ID
146
- });
147
- }
148
- }
149
- return resolved;
112
+ return resolveConfig(file, env);
150
113
  }
151
114
  async function writeConfigFile(cwd, config) {
152
115
  const file = configFilePath(cwd);
@@ -154,12 +117,6 @@ async function writeConfigFile(cwd, config) {
154
117
  if (config.env) body.env = config.env;
155
118
  if (config.baseUrl) body.baseUrl = config.baseUrl;
156
119
  if (config.ossUrl) body.ossUrl = config.ossUrl;
157
- const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
158
- const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
159
- const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
160
- if (opAccountId) body.opAccountId = opAccountId;
161
- if (opUserId) body.opUserId = opUserId;
162
- if (spaceId) body.spaceId = spaceId;
163
120
  if (config.scenes !== void 0) body.scenes = config.scenes;
164
121
  await fs.mkdir(cwd, { recursive: true });
165
122
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
@@ -197,22 +154,20 @@ async function writeConfigScenes(cwd, scenes) {
197
154
  import http from "http";
198
155
  import https from "https";
199
156
  import { URL as URL2 } from "url";
157
+ var APP_ID_HEADER = "x-app-id";
200
158
  var AUTH_HEADER = "x-app-secret";
201
- var OP_ACCOUNT_ID_HEADER = "op-account-id";
202
- var OP_USER_ID_HEADER = "op-user-id";
203
- var SPACE_ID_HEADER = "space-id";
204
159
  function enc(id) {
205
160
  return encodeURIComponent(id);
206
161
  }
207
162
  var ENDPOINTS = {
208
- /** GET 已关联场景列表。 */
209
- linkedScenes: (applicationId) => `/api/twin/v1/sdk-application-scenes/${enc(applicationId)}/scenes`,
210
- /** GET 工作区全部代码文件。 */
211
- workspaceCode: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}`,
212
- /** GET 工作区文件约束。 */
213
- workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`,
214
- /** POST 新建 / PATCH 批量更新 / DELETE 批量删除。 */
215
- workspaceCodeFiles: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/files`
163
+ /** POST 已关联场景列表(分享路径,空 body)。 */
164
+ linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
165
+ /** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
166
+ workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
167
+ /** POST 一次推送 create/update/delete(分享路径)。 */
168
+ workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
169
+ /** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
170
+ workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
216
171
  };
217
172
  var EasyTwinApiError = class extends Error {
218
173
  status;
@@ -255,35 +210,25 @@ var EasyTwinClient = class {
255
210
  baseUrl;
256
211
  /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
257
212
  ossUrl;
258
- /** 接入凭证 App ID;同时作为场景/代码 API applicationId 路径参数。 */
213
+ /** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
259
214
  appId;
260
215
  /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
261
216
  mock;
262
217
  appSecret;
263
- opAccountId;
264
- opUserId;
265
- spaceId;
266
218
  constructor(config) {
267
219
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
268
220
  this.ossUrl = config.ossUrl.replace(/\/+$/, "");
269
221
  this.mock = config.mock;
270
222
  this.appId = config.appId;
271
223
  this.appSecret = config.appSecret;
272
- this.opAccountId = config.opAccountId;
273
- this.opUserId = config.opUserId;
274
- this.spaceId = config.spaceId;
275
224
  }
276
225
  /** 全仓唯一认证头注入点。 */
277
226
  authHeaders() {
278
- const headers = { [AUTH_HEADER]: this.appSecret };
279
- if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
280
- if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
281
- if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
282
- return headers;
227
+ return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
283
228
  }
284
229
  /** JSON 请求(原生 fetch)。 */
285
- async request(path8, options = {}) {
286
- const url = `${this.baseUrl}${path8}`;
230
+ async request(path9, options = {}) {
231
+ const url = `${this.baseUrl}${path9}`;
287
232
  const headers = { ...this.authHeaders(), ...options.headers };
288
233
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
289
234
  headers["Content-Type"] = "application/json";
@@ -305,8 +250,8 @@ var EasyTwinClient = class {
305
250
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
306
251
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
307
252
  */
308
- async upload(path8, options) {
309
- const url = new URL2(`${this.baseUrl}${path8}`);
253
+ async upload(path9, options) {
254
+ const url = new URL2(`${this.baseUrl}${path9}`);
310
255
  const mod = url.protocol === "https:" ? https : http;
311
256
  const headers = {
312
257
  ...this.authHeaders(),
@@ -463,8 +408,11 @@ function deriveExampleSceneId(payload) {
463
408
  async function exampleSceneSummary(exampleFile) {
464
409
  return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
465
410
  }
411
+ async function fetchLinkedScenes(client) {
412
+ return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
413
+ }
466
414
  async function listScenes(client, options = {}) {
467
- const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" }));
415
+ const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
468
416
  if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
469
417
  return scenes;
470
418
  }
@@ -477,7 +425,7 @@ async function pullScene(client, id, options = {}) {
477
425
  }
478
426
  return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
479
427
  }
480
- const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
428
+ const data = await fetchLinkedScenes(client);
481
429
  const scenes = normalizeLinkedScenes(data);
482
430
  const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
483
431
  if (!hit) {
@@ -548,7 +496,43 @@ function parseSceneStructure(scene) {
548
496
  // src/upload.ts
549
497
  import { promises as fs3 } from "fs";
550
498
  import path3 from "path";
551
- var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".git"]);
499
+ var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
500
+ ".git",
501
+ "node_modules",
502
+ "dist",
503
+ ".easytwin",
504
+ ".cursor",
505
+ ".claude",
506
+ ".qoder",
507
+ ".vscode"
508
+ ]);
509
+ var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
510
+ function isTsconfigFile(base) {
511
+ return /^tsconfig(\..+)?\.json$/i.test(base);
512
+ }
513
+ var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
514
+ function isWorkspaceSpecFile(relPath) {
515
+ const base = normalizePath(relPath).split("/").pop() ?? "";
516
+ return /\.spec\.ts$/i.test(base);
517
+ }
518
+ function isIgnoredWorkspacePath(relPath) {
519
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
520
+ if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
521
+ const base = parts[parts.length - 1];
522
+ if (base === void 0) return false;
523
+ if (WORKSPACE_IGNORED_FILES.has(base)) return true;
524
+ if (isTsconfigFile(base)) return true;
525
+ return base.toLowerCase().endsWith(".scene.json");
526
+ }
527
+ function defaultWorkspaceIgnore(relPath) {
528
+ return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
529
+ }
530
+ function combineUploadIgnore(extra) {
531
+ return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
532
+ }
533
+ async function collectWorkspaceCodeFiles(dir, ignore) {
534
+ return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
535
+ }
552
536
  async function collectFiles(dir, ignore = () => false) {
553
537
  const files = [];
554
538
  async function walk(current, rel) {
@@ -557,7 +541,7 @@ async function collectFiles(dir, ignore = () => false) {
557
541
  const relPath = path3.posix.join(rel, entry.name);
558
542
  if (ignore(relPath)) continue;
559
543
  if (entry.isDirectory()) {
560
- if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
544
+ if (entry.name === ".git") continue;
561
545
  await walk(path3.join(current, entry.name), relPath);
562
546
  } else if (entry.isFile()) {
563
547
  const absPath = path3.join(current, entry.name);
@@ -639,6 +623,10 @@ function extensionOf(relPath) {
639
623
  function allowedExtensionSet(list) {
640
624
  return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
641
625
  }
626
+ var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
627
+ function isWorkspaceCodeFile(relPath) {
628
+ return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
629
+ }
642
630
  function directoryDepth(relPath) {
643
631
  const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
644
632
  return Math.max(0, parts.length - 1);
@@ -665,65 +653,94 @@ function assertUploadable(files, config) {
665
653
  }
666
654
  if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
667
655
  }
656
+ function countsFromPlan(plan) {
657
+ return {
658
+ created: plan.creates.length,
659
+ updated: plan.updates.length,
660
+ deleted: plan.deletes.length,
661
+ unchanged: plan.unchanged.length
662
+ };
663
+ }
664
+ function planWorkspaceUpload(local, remote) {
665
+ const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
666
+ const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
667
+ const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
668
+ const creates = [];
669
+ const updates = [];
670
+ const unchanged = [];
671
+ for (const item of local) {
672
+ const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
673
+ if (!remoteFile) creates.push(item);
674
+ else if (remoteFile.content !== item.content) {
675
+ updates.push({ file: item.file, id: remoteFile.id, content: item.content });
676
+ } else {
677
+ unchanged.push(item.file);
678
+ }
679
+ }
680
+ return { creates, updates, deletes, unchanged };
681
+ }
682
+ function formatUploadResult(result) {
683
+ const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
684
+ 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}`;
685
+ }
668
686
  async function mockUpload(dir, options) {
669
- const ignore = options.ignore ?? (() => false);
670
- const files = await collectFiles(dir, ignore);
687
+ const ignore = combineUploadIgnore(options.ignore);
688
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
671
689
  const total = files.reduce((sum, f) => sum + f.size, 0);
672
690
  options.onProgress?.({ phase: "collect", current: total, total });
673
691
  options.onProgress?.({ phase: "upload", current: total, total });
674
- return { fileCount: files.length, byteCount: total, mock: true };
692
+ return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
693
+ }
694
+ function buildWorkspacePushBody(plan) {
695
+ const body = {};
696
+ if (plan.creates.length > 0) {
697
+ body.create = plan.creates.map((c) => ({
698
+ filePath: normalizePath(c.file.relPath),
699
+ content: c.content
700
+ }));
701
+ }
702
+ if (plan.updates.length > 0) {
703
+ body.update = plan.updates.map((p) => ({
704
+ id: p.id,
705
+ content: p.content,
706
+ filePath: normalizePath(p.file.relPath)
707
+ }));
708
+ }
709
+ if (plan.deletes.length > 0) {
710
+ body.delete = plan.deletes.map((d) => d.id);
711
+ }
712
+ return Object.keys(body).length > 0 ? body : null;
675
713
  }
676
714
  async function uploadDirectory(client, dir, options = {}) {
677
715
  if (client.mock) return mockUpload(dir, options);
678
- const ignore = options.ignore ?? (() => false);
679
- const files = await collectFiles(dir, ignore);
716
+ const ignore = combineUploadIgnore(options.ignore);
717
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
680
718
  const total = files.reduce((sum, f) => sum + f.size, 0);
681
719
  options.onProgress?.({ phase: "collect", current: total, total });
682
- const appId = client.appId;
683
- const wsConfig = normalizeWorkspaceConfig(await client.request(ENDPOINTS.workspaceConfig(appId), { method: "GET" }));
684
- assertUploadable(files, wsConfig);
685
- const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
686
- const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
687
- const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
688
- const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
689
- if (toDelete.length > 0) {
690
- await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
691
- method: "DELETE",
692
- body: JSON.stringify({ ids: toDelete })
693
- });
694
- }
695
- const creates = [];
696
- const patches = [];
697
- const unchanged = [];
720
+ const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
721
+ const local = [];
698
722
  for (const file of files) {
699
- const content = await fs3.readFile(file.absPath, "utf8");
700
- const remoteFile = remoteByPath.get(normalizePath(file.relPath));
701
- if (!remoteFile) creates.push({ file, content });
702
- else if (remoteFile.content !== content) patches.push({ file, id: remoteFile.id, content });
703
- else unchanged.push(file);
723
+ local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
724
+ }
725
+ const plan = planWorkspaceUpload(local, remote);
726
+ const counts = countsFromPlan(plan);
727
+ const pushBody = buildWorkspacePushBody(plan);
728
+ if (pushBody) {
729
+ await client.request(ENDPOINTS.workspaceCodePush(), {
730
+ method: "POST",
731
+ body: JSON.stringify(pushBody)
732
+ });
704
733
  }
705
734
  let sent = 0;
706
735
  const bump = (file) => {
707
736
  sent += file.size;
708
737
  options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
709
738
  };
710
- if (patches.length > 0) {
711
- await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
712
- method: "PATCH",
713
- body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
714
- });
715
- for (const p of patches) bump(p.file);
716
- }
717
- for (const c of creates) {
718
- await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
719
- method: "POST",
720
- body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
721
- });
722
- bump(c.file);
723
- }
724
- for (const u of unchanged) bump(u);
739
+ for (const c of plan.creates) bump(c.file);
740
+ for (const p of plan.updates) bump(p.file);
741
+ for (const u of plan.unchanged) bump(u);
725
742
  if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
726
- return { fileCount: files.length, byteCount: total };
743
+ return { fileCount: files.length, byteCount: total, ...counts };
727
744
  }
728
745
 
729
746
  // src/workspace.ts
@@ -771,42 +788,20 @@ export default class App extends TwinApp {
771
788
  }
772
789
  }
773
790
  `;
774
- var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
775
- ".git",
776
- "node_modules",
777
- "dist",
778
- ".easytwin",
779
- ".cursor",
780
- ".claude",
781
- ".qoder",
782
- ".vscode"
783
- ]);
784
- var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
785
- var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
786
791
  function defaultPullIgnore(relPath) {
787
- const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
788
- if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
789
- const base = parts[parts.length - 1];
790
- return base !== void 0 && PULL_IGNORED_FILES.has(base);
792
+ return defaultWorkspaceIgnore(relPath);
791
793
  }
792
794
  function isSafeRelPath(relPath) {
793
795
  const n = normalizePath(relPath);
794
796
  if (!n) return false;
795
797
  if (n.toLowerCase() === "easytwin.config.json") return false;
798
+ const base = n.split("/").pop() ?? "";
799
+ if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
796
800
  if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
797
801
  const parts = n.split("/");
798
802
  if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
799
803
  return true;
800
804
  }
801
- function extensionOf2(relPath) {
802
- const base = relPath.split("/").pop() ?? "";
803
- const i = base.lastIndexOf(".");
804
- if (i <= 0) return "";
805
- return base.slice(i).toLowerCase();
806
- }
807
- function allowedExtensionSet2(list) {
808
- return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
809
- }
810
805
  function normalizeEol(text) {
811
806
  return text.replace(/\r\n/g, "\n");
812
807
  }
@@ -822,30 +817,18 @@ async function readLocalText(absPath) {
822
817
  throw err;
823
818
  }
824
819
  }
825
- async function loadAllowedExtensions(client) {
826
- if (client.mock) return DEFAULT_PULL_EXTENSIONS;
827
- try {
828
- const cfg = normalizeWorkspaceConfig(
829
- await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
830
- );
831
- return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
832
- } catch {
833
- return DEFAULT_PULL_EXTENSIONS;
834
- }
835
- }
836
820
  async function fetchRemoteFiles(client) {
837
821
  if (client.mock) return [];
838
- return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(client.appId), { method: "GET" }));
822
+ return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
839
823
  }
840
824
  async function planWorkspacePull(client, dir, options = {}) {
841
825
  const ignore = combineIgnore(options.ignore);
842
- const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
843
826
  const remoteRaw = await fetchRemoteFiles(client);
844
827
  const skippedRemotePaths = [];
845
828
  const remoteByPath = /* @__PURE__ */ new Map();
846
829
  for (const file of remoteRaw) {
847
830
  const rel = normalizePath(file.filePath);
848
- if (!isSafeRelPath(rel) || ignore(rel)) {
831
+ if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
849
832
  skippedRemotePaths.push(rel || file.filePath);
850
833
  continue;
851
834
  }
@@ -861,7 +844,7 @@ async function planWorkspacePull(client, dir, options = {}) {
861
844
  const localByPath = /* @__PURE__ */ new Map();
862
845
  for (const file of localFiles) {
863
846
  const rel = normalizePath(file.relPath);
864
- if (!allowed.has(extensionOf2(rel))) continue;
847
+ if (!isWorkspaceCodeFile(rel)) continue;
865
848
  const content = await readLocalText(file.absPath);
866
849
  if (content !== void 0) localByPath.set(rel, content);
867
850
  }
@@ -982,407 +965,71 @@ function workspacePullPendingWrites(plan, force = false) {
982
965
  return plan.changes.filter((c) => c.kind === "seed" || c.kind === "remote-only" || force && c.kind === "modified").map((c) => c.path);
983
966
  }
984
967
 
985
- // src/skills.ts
986
- import { existsSync, promises as fs5 } from "fs";
968
+ // src/workspaceTests.ts
969
+ import { promises as fs6 } from "fs";
970
+ import path7 from "path";
971
+
972
+ // src/bundle.ts
973
+ import * as esbuild from "esbuild-wasm";
974
+ import { promises as fs5 } from "fs";
987
975
  import path6 from "path";
988
- import { fileURLToPath as fileURLToPath2 } from "url";
989
- var SKILL_NAMES = [
990
- "easytwin-render",
991
- "easytwin-core",
992
- "easytwin-develop",
993
- "easytwin-bootstrap",
994
- "easytwin-scene",
995
- "easytwin-upload"
996
- ];
997
- var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
998
- var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
999
- var META_FILE_NAME = ".easytwin-meta.json";
1000
- var EASYTWIN_TYPES_DIR = ".easytwin/types";
1001
- var MINIMAL_TSCONFIG = `${JSON.stringify(
1002
- {
1003
- compilerOptions: {
1004
- target: "ES2022",
1005
- module: "ESNext",
1006
- moduleResolution: "bundler",
1007
- strict: true,
1008
- skipLibCheck: true,
1009
- noEmit: true,
1010
- paths: {
1011
- "@easytwin/runtime": [".easytwin/types"],
1012
- "@easytwin/apps": [".easytwin/types/apps"]
1013
- }
1014
- },
1015
- include: ["src/**/*.ts"]
1016
- },
1017
- null,
1018
- 2
1019
- )}
1020
- `;
1021
- var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
1022
- "skipLibCheck": true,
1023
- "paths": {
1024
- "@easytwin/runtime": [".easytwin/types"],
1025
- "@easytwin/apps": [".easytwin/types/apps"]
1026
- }`;
1027
- var APPS_TYPES_FILE = "apps.d.ts";
1028
- var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
1029
976
 
1030
- export type TwinAppContext = {
1031
- app: {
1032
- id: string;
1033
- mode: "preview" | "publish";
1034
- };
1035
- engine: RuntimeEngine;
1036
- container: HTMLElement;
1037
- runtimeScene: RuntimeScene | null;
1038
- scene: RuntimeScene["sceneObject"] | null;
1039
- camera: RuntimeScene["camera"]["main"] | null;
1040
- sceneManager: {
1041
- currentSceneId: string;
1042
- runtime: SceneManager | null;
1043
- loadScene(sceneId: string): Promise<void>;
1044
- };
1045
- logger: {
1046
- log(...args: unknown[]): void;
1047
- info(...args: unknown[]): void;
1048
- warn(...args: unknown[]): void;
1049
- error(...args: unknown[]): void;
1050
- };
1051
- assets: {
1052
- text(path: string): Promise<string>;
1053
- json<T = unknown>(path: string): Promise<T>;
1054
- };
1055
- cleanup(fn: () => void | Promise<void>): void;
1056
- sceneCleanup(fn: () => void | Promise<void>): void;
977
+ // src/apps.ts
978
+ var PORTABLE_RUNTIME_EXPORTS = [
979
+ "THREE",
980
+ "RuntimeEngine",
981
+ "SceneManager",
982
+ "LoadSceneMode",
983
+ "convertObjToComponentJson"
984
+ ];
985
+ var TwinApp = class {
1057
986
  };
1058
-
1059
- export declare abstract class TwinApp {
1060
- init?(ctx: TwinAppContext): void | Promise<void>;
1061
- onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
1062
- onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
1063
- onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
1064
- onDispose?(ctx: TwinAppContext): void | Promise<void>;
1065
- onError?(ctx: TwinAppContext, error: unknown): boolean | void;
1066
- }
1067
-
1068
- export declare function defineApp<T>(app: T): T;
1069
- `;
1070
- function buildRuntimeTypesContent(sourceDts) {
1071
- return `${sourceDts.replace(/\s+$/, "")}
1072
- `;
1073
- }
1074
- function normalizeTargets(target = "all") {
1075
- if (target === "all") return ["cursor", "claude", "codex", "qoder"];
1076
- if (Array.isArray(target)) return [...new Set(target)];
1077
- return [target];
987
+ function defineApp(app) {
988
+ return app;
1078
989
  }
1079
- function resolveSkillsSourceDir() {
1080
- if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1081
- throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
1082
- }
1083
- const here = path6.dirname(fileURLToPath2(import.meta.url));
1084
- return path6.resolve(here, "..", "skills");
990
+ var LIFECYCLE_NAMES = [
991
+ "init",
992
+ "onUpdate",
993
+ "onBeforeSceneUnload",
994
+ "onSceneLoaded",
995
+ "onDispose",
996
+ "onError"
997
+ ];
998
+ function isLifecycleApp(value) {
999
+ if (typeof value !== "object" || value === null) return false;
1000
+ return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
1085
1001
  }
1086
- function resolveRuntimeTypesSourceFile() {
1087
- if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1088
- throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
1002
+ function createAppInstance(appExport) {
1003
+ if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
1004
+ return new appExport();
1089
1005
  }
1090
- const here = path6.dirname(fileURLToPath2(import.meta.url));
1091
- const fromDist = path6.join(here, "runtime-types", "index.d.ts");
1092
- const fromSrc = path6.resolve(here, "lib", "index.d.ts");
1093
- if (existsSync(fromDist)) return fromDist;
1094
- if (existsSync(fromSrc)) return fromSrc;
1095
- throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
1096
- }
1097
- async function readJson(file) {
1098
- return JSON.parse(await fs5.readFile(file, "utf8"));
1006
+ if (isLifecycleApp(appExport)) return appExport;
1007
+ throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
1099
1008
  }
1100
- async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
1101
- const marker = path6.join(skillsDir, ".easytwin-source.json");
1102
- try {
1103
- const meta = await readJson(marker);
1104
- if (typeof meta.version === "string") return meta.version;
1105
- } catch {
1106
- }
1107
- try {
1108
- const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
1109
- return pkg.version;
1110
- } catch {
1111
- return "0.0.0";
1112
- }
1009
+ function portableRuntimeWarning(names) {
1010
+ const extra = [...new Set(names)].filter(
1011
+ (n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
1012
+ );
1013
+ const ns = [...names].includes("*");
1014
+ if (!ns && extra.length === 0) return void 0;
1015
+ const detail = ns ? "namespace import *" : extra.sort().join(", ");
1016
+ 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`;
1113
1017
  }
1114
- async function copyDir(src, dest) {
1115
- await fs5.mkdir(dest, { recursive: true });
1116
- const entries = await fs5.readdir(src, { withFileTypes: true });
1117
- for (const entry of entries) {
1118
- const s = path6.join(src, entry.name);
1119
- const d = path6.join(dest, entry.name);
1120
- if (entry.isDirectory()) await copyDir(s, d);
1121
- else if (entry.isFile()) await fs5.copyFile(s, d);
1018
+
1019
+ // src/bundle.ts
1020
+ var USER_ENTRY = "src/main.ts";
1021
+ var DEFAULT_BUNDLE_OUT = "dist/main.js";
1022
+ var RUNTIME_MODULE = "@easytwin/runtime";
1023
+ var APPS_MODULE = "@easytwin/apps";
1024
+ var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
1025
+ var BundleError = class extends Error {
1026
+ constructor(message) {
1027
+ super(message);
1028
+ this.name = "BundleError";
1122
1029
  }
1123
- }
1124
- async function dirMatches(src, dest) {
1125
- let sourceEntries;
1126
- try {
1127
- sourceEntries = await fs5.readdir(src);
1128
- } catch {
1129
- return false;
1130
- }
1131
- for (const name of sourceEntries) {
1132
- const s = path6.join(src, name);
1133
- const d = path6.join(dest, name);
1134
- const sStat = await fs5.stat(s);
1135
- if (sStat.isDirectory()) {
1136
- if (!await dirMatches(s, d)) return false;
1137
- } else {
1138
- let dStat;
1139
- try {
1140
- dStat = await fs5.stat(d);
1141
- } catch {
1142
- return false;
1143
- }
1144
- if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
1145
- if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
1146
- }
1147
- }
1148
- return true;
1149
- }
1150
- function actionFor(exists, matches) {
1151
- if (!exists) return "created";
1152
- return matches ? "unchanged" : "updated";
1153
- }
1154
- async function writeMeta(destDir, version) {
1155
- const meta = { name: "@easytwin/devkit", version };
1156
- await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
1157
- }
1158
- async function syncToDir(target, sourceRoot, targetRoot, version) {
1159
- const entries = [];
1160
- for (const name of SKILL_NAMES) {
1161
- const src = path6.join(sourceRoot, name);
1162
- const dest = path6.join(targetRoot, name);
1163
- const exists = await fs5.stat(dest).then(() => true).catch(() => false);
1164
- const matches = await dirMatches(src, dest);
1165
- const action = actionFor(exists, matches);
1166
- if (action !== "unchanged") {
1167
- await fs5.rm(dest, { recursive: true, force: true });
1168
- await copyDir(src, dest);
1169
- }
1170
- entries.push({ name, action });
1171
- }
1172
- await writeMeta(targetRoot, version);
1173
- return { target, entries };
1174
- }
1175
- function buildCodexSegment(version) {
1176
- return [
1177
- CODEX_MARKER_BEGIN,
1178
- "<!-- \u672C\u6BB5\u7531 `easytwin skills sync` \u7BA1\u7406,\u53EF\u6574\u6BB5\u66FF\u6362;\u624B\u52A8\u4FEE\u6539\u4F1A\u88AB\u4E0B\u6B21\u540C\u6B65\u8986\u76D6\u3002 -->",
1179
- `EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
1180
- "",
1181
- "- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
1182
- "- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
1183
- "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
1184
- "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
1185
- "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
1186
- "- `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",
1187
- "",
1188
- "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
1189
- CODEX_MARKER_END
1190
- ].join("\n");
1191
- }
1192
- async function syncToCodex(sourceRoot, cwd, version) {
1193
- const agentsFile = path6.join(cwd, "AGENTS.md");
1194
- const segment = buildCodexSegment(version);
1195
- let content = "";
1196
- let exists = true;
1197
- try {
1198
- content = await fs5.readFile(agentsFile, "utf8");
1199
- } catch {
1200
- exists = false;
1201
- }
1202
- const start = content.indexOf(CODEX_MARKER_BEGIN);
1203
- const end = content.indexOf(CODEX_MARKER_END);
1204
- let action;
1205
- let next;
1206
- if (start === -1 || end === -1 || end < start) {
1207
- action = exists ? "updated" : "created";
1208
- next = content.length > 0 && !content.endsWith("\n") ? content + "\n\n" : content + (content.length > 0 ? "\n" : "");
1209
- next += segment + "\n";
1210
- } else {
1211
- const current = content.slice(start, end + CODEX_MARKER_END.length);
1212
- action = current === segment ? "unchanged" : "updated";
1213
- next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
1214
- }
1215
- if (action !== "unchanged") {
1216
- await fs5.mkdir(cwd, { recursive: true });
1217
- await fs5.writeFile(agentsFile, next, "utf8");
1218
- }
1219
- return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
1220
- }
1221
- async function syncRuntimeTypes(cwd, typesSourceFile) {
1222
- const destDir = path6.join(cwd, ".easytwin", "types");
1223
- const destFile = path6.join(destDir, "index.d.ts");
1224
- const appsFile = path6.join(destDir, APPS_TYPES_FILE);
1225
- const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
1226
- let runtimeCurrent = "";
1227
- let appsCurrent = "";
1228
- let runtimeExists = true;
1229
- let appsExists = true;
1230
- try {
1231
- runtimeCurrent = await fs5.readFile(destFile, "utf8");
1232
- } catch {
1233
- runtimeExists = false;
1234
- }
1235
- try {
1236
- appsCurrent = await fs5.readFile(appsFile, "utf8");
1237
- } catch {
1238
- appsExists = false;
1239
- }
1240
- const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
1241
- const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
1242
- const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
1243
- if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
1244
- await fs5.mkdir(destDir, { recursive: true });
1245
- if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
1246
- if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
1247
- }
1248
- const tsconfigPath = path6.join(cwd, "tsconfig.json");
1249
- let tsconfig;
1250
- let pathsHint;
1251
- try {
1252
- const existing = await fs5.readFile(tsconfigPath, "utf8");
1253
- if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
1254
- else {
1255
- tsconfig = "manual-paths";
1256
- pathsHint = TSCONFIG_PATHS_HINT;
1257
- }
1258
- } catch {
1259
- await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1260
- tsconfig = "created";
1261
- }
1262
- return { action, tsconfig, pathsHint };
1263
- }
1264
- async function syncSkills(options) {
1265
- const targets = normalizeTargets(options.targets ?? "all");
1266
- const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
1267
- const version = options.version ?? await readDevkitVersion(sourceRoot);
1268
- const summaries = [];
1269
- for (const target of targets) {
1270
- if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
1271
- else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
1272
- else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
1273
- else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
1274
- }
1275
- const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
1276
- return { summaries, types };
1277
- }
1278
- async function detectSkillsStatus(cwd, sourceDir) {
1279
- const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
1280
- const version = await readDevkitVersion(sourceRoot);
1281
- const targets = [];
1282
- const dirTargets = ["cursor", "claude", "qoder"];
1283
- const DIR_ROOTS = {
1284
- cursor: ".cursor/skills",
1285
- claude: ".claude/skills",
1286
- qoder: ".qoder/skills"
1287
- };
1288
- for (const target of dirTargets) {
1289
- const root = path6.join(cwd, DIR_ROOTS[target]);
1290
- const missing = [];
1291
- for (const name of SKILL_NAMES) {
1292
- if (!await fs5.stat(path6.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
1293
- }
1294
- let stale = false;
1295
- try {
1296
- const meta = await readJson(path6.join(root, META_FILE_NAME));
1297
- stale = meta.version !== version;
1298
- } catch {
1299
- stale = missing.length === 0;
1300
- }
1301
- targets.push({
1302
- target,
1303
- synced: missing.length === 0 && !stale,
1304
- reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
1305
- });
1306
- }
1307
- let agentsContent = "";
1308
- try {
1309
- agentsContent = await fs5.readFile(path6.join(cwd, "AGENTS.md"), "utf8");
1310
- } catch {
1311
- }
1312
- const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
1313
- targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
1314
- let typesSynced = false;
1315
- try {
1316
- const dts = await fs5.readFile(path6.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
1317
- const appsDts = await fs5.readFile(path6.join(cwd, ".easytwin", "types", APPS_TYPES_FILE), "utf8");
1318
- typesSynced = dts.includes("registerEngineTickListener") && appsDts.includes("export declare abstract class TwinApp");
1319
- } catch {
1320
- typesSynced = false;
1321
- }
1322
- return { cwd, targets, typesSynced };
1323
- }
1324
-
1325
- // src/bundle.ts
1326
- import * as esbuild from "esbuild-wasm";
1327
- import { promises as fs6 } from "fs";
1328
- import path7 from "path";
1329
-
1330
- // src/apps.ts
1331
- var PORTABLE_RUNTIME_EXPORTS = [
1332
- "THREE",
1333
- "RuntimeEngine",
1334
- "SceneManager",
1335
- "LoadSceneMode",
1336
- "convertObjToComponentJson"
1337
- ];
1338
- var TwinApp = class {
1339
- };
1340
- function defineApp(app) {
1341
- return app;
1342
- }
1343
- var LIFECYCLE_NAMES = [
1344
- "init",
1345
- "onUpdate",
1346
- "onBeforeSceneUnload",
1347
- "onSceneLoaded",
1348
- "onDispose",
1349
- "onError"
1350
- ];
1351
- function isLifecycleApp(value) {
1352
- if (typeof value !== "object" || value === null) return false;
1353
- return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
1354
- }
1355
- function createAppInstance(appExport) {
1356
- if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
1357
- return new appExport();
1358
- }
1359
- if (isLifecycleApp(appExport)) return appExport;
1360
- throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
1361
- }
1362
- function portableRuntimeWarning(names) {
1363
- const extra = [...new Set(names)].filter(
1364
- (n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
1365
- );
1366
- const ns = [...names].includes("*");
1367
- if (!ns && extra.length === 0) return void 0;
1368
- const detail = ns ? "namespace import *" : extra.sort().join(", ");
1369
- 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`;
1370
- }
1371
-
1372
- // src/bundle.ts
1373
- var USER_ENTRY = "src/main.ts";
1374
- var DEFAULT_BUNDLE_OUT = "dist/main.js";
1375
- var RUNTIME_MODULE = "@easytwin/runtime";
1376
- var APPS_MODULE = "@easytwin/apps";
1377
- var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
1378
- var BundleError = class extends Error {
1379
- constructor(message) {
1380
- super(message);
1381
- this.name = "BundleError";
1382
- }
1383
- };
1384
- function isRelativeOrAbsolute(spec) {
1385
- return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
1030
+ };
1031
+ function isRelativeOrAbsolute(spec) {
1032
+ return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path6.isAbsolute(spec);
1386
1033
  }
1387
1034
  function whitelistPlugin() {
1388
1035
  return {
@@ -1424,14 +1071,14 @@ function collectBundledRuntimeExports(code) {
1424
1071
  }
1425
1072
  return names;
1426
1073
  }
1427
- async function bundleUserCode(options) {
1428
- const cwd = path7.resolve(options.cwd);
1429
- const entry = path7.join(cwd, USER_ENTRY);
1074
+ async function bundleWorkspaceModule(options) {
1075
+ const cwd = path6.resolve(options.cwd);
1076
+ const entry = path6.isAbsolute(options.entry) ? options.entry : path6.join(cwd, options.entry);
1430
1077
  try {
1431
- await fs6.access(entry);
1078
+ await fs5.access(entry);
1432
1079
  } catch {
1433
1080
  throw new BundleError(
1434
- `\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`
1081
+ options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
1435
1082
  );
1436
1083
  }
1437
1084
  let result;
@@ -1465,107 +1112,637 @@ async function bundleUserCode(options) {
1465
1112
  const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
1466
1113
  if (portable) warnings.push(portable);
1467
1114
  if (options.outFile) {
1468
- const outFile = path7.isAbsolute(options.outFile) ? options.outFile : path7.join(cwd, options.outFile);
1469
- await fs6.mkdir(path7.dirname(outFile), { recursive: true });
1470
- await fs6.writeFile(outFile, code, "utf8");
1115
+ const outFile = path6.isAbsolute(options.outFile) ? options.outFile : path6.join(cwd, options.outFile);
1116
+ await fs5.mkdir(path6.dirname(outFile), { recursive: true });
1117
+ await fs5.writeFile(outFile, code, "utf8");
1118
+ }
1119
+ return { code, warnings };
1120
+ }
1121
+ async function bundleUserCode(options) {
1122
+ const cwd = path6.resolve(options.cwd);
1123
+ const entry = path6.join(cwd, USER_ENTRY);
1124
+ return bundleWorkspaceModule({
1125
+ cwd: options.cwd,
1126
+ entry: USER_ENTRY,
1127
+ outFile: options.outFile,
1128
+ 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`
1129
+ });
1130
+ }
1131
+ async function typesMissingHint(cwd) {
1132
+ try {
1133
+ await fs5.access(path6.join(cwd, ".easytwin", "types", "index.d.ts"));
1134
+ return void 0;
1135
+ } catch {
1136
+ return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
1137
+ }
1138
+ }
1139
+ var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
1140
+ var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1141
+ function decodeVLQValues(str) {
1142
+ const values = [];
1143
+ let i = 0;
1144
+ while (i < str.length) {
1145
+ let result = 0;
1146
+ let shift = 0;
1147
+ let continuation = true;
1148
+ while (continuation) {
1149
+ if (i >= str.length) return values;
1150
+ const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
1151
+ if (digit < 0) return values;
1152
+ continuation = (digit & 32) !== 0;
1153
+ result += (digit & 31) << shift;
1154
+ shift += 5;
1155
+ }
1156
+ values.push(result & 1 ? -(result >> 1) : result >> 1);
1157
+ }
1158
+ return values;
1159
+ }
1160
+ function decodeMappings(map) {
1161
+ const sources = map.sources ?? [];
1162
+ const lines = (map.mappings ?? "").split(";");
1163
+ let sourceIndex = 0;
1164
+ let originalLine = 0;
1165
+ let originalColumn = 0;
1166
+ const decoded = [];
1167
+ for (const line of lines) {
1168
+ let generatedColumn = 0;
1169
+ const segs = [];
1170
+ if (line) {
1171
+ for (const raw of line.split(",")) {
1172
+ if (!raw) continue;
1173
+ const nums = decodeVLQValues(raw);
1174
+ if (nums[0] === void 0) continue;
1175
+ generatedColumn += nums[0];
1176
+ if (nums.length >= 4) {
1177
+ sourceIndex += nums[1] ?? 0;
1178
+ originalLine += nums[2] ?? 0;
1179
+ originalColumn += nums[3] ?? 0;
1180
+ segs.push({
1181
+ generatedColumn,
1182
+ source: sources[sourceIndex] ?? USER_ENTRY,
1183
+ originalLine,
1184
+ originalColumn
1185
+ });
1186
+ }
1187
+ }
1188
+ }
1189
+ decoded.push(segs);
1190
+ }
1191
+ return decoded;
1192
+ }
1193
+ function originalPositionFor(map, line, column) {
1194
+ const decoded = decodeMappings(map);
1195
+ for (let i = line - 1; i >= 0; i--) {
1196
+ const segs = decoded[i];
1197
+ if (!segs || segs.length === 0) continue;
1198
+ const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
1199
+ let best = segs[0];
1200
+ for (const seg of segs) {
1201
+ if (seg.generatedColumn <= col) best = seg;
1202
+ else break;
1203
+ }
1204
+ if (!best) continue;
1205
+ return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
1206
+ }
1207
+ return void 0;
1208
+ }
1209
+ function extractInlineSourceMap(code) {
1210
+ const m = code.match(SOURCEMAP_RE);
1211
+ if (!m?.[1]) return void 0;
1212
+ try {
1213
+ return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
1214
+ } catch {
1215
+ return void 0;
1216
+ }
1217
+ }
1218
+ function remapErrorStack(stack, bundledCode) {
1219
+ const map = extractInlineSourceMap(bundledCode);
1220
+ if (!map?.mappings) return stack;
1221
+ return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
1222
+ const orig = originalPositionFor(map, Number(line), Number(col));
1223
+ if (!orig) return full;
1224
+ return `${orig.source}:${orig.line}:${orig.column}`;
1225
+ });
1226
+ }
1227
+
1228
+ // src/workspaceTests.ts
1229
+ var IDENT = "[A-Za-z_$][\\w$]*";
1230
+ var FUNCTION_EXPORT = new RegExp(
1231
+ `export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
1232
+ "g"
1233
+ );
1234
+ var CONST_EXPORT = new RegExp(
1235
+ `export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
1236
+ "g"
1237
+ );
1238
+ function stripTsCommentsAndStringsKeepCode(source) {
1239
+ let out = "";
1240
+ let i = 0;
1241
+ const n = source.length;
1242
+ while (i < n) {
1243
+ const c = source[i];
1244
+ const next = source[i + 1];
1245
+ if (c === "/" && next === "/") {
1246
+ i += 2;
1247
+ while (i < n && source[i] !== "\n") i++;
1248
+ continue;
1249
+ }
1250
+ if (c === "/" && next === "*") {
1251
+ i += 2;
1252
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
1253
+ i = Math.min(n, i + 2);
1254
+ out += " ";
1255
+ continue;
1256
+ }
1257
+ if (c === "'" || c === '"' || c === "`") {
1258
+ const quote = c;
1259
+ out += " ";
1260
+ i++;
1261
+ while (i < n) {
1262
+ const ch = source[i];
1263
+ if (ch === "\\") {
1264
+ i += 2;
1265
+ continue;
1266
+ }
1267
+ if (ch === quote) {
1268
+ i++;
1269
+ break;
1270
+ }
1271
+ i++;
1272
+ }
1273
+ continue;
1274
+ }
1275
+ out += c;
1276
+ i++;
1277
+ }
1278
+ return out;
1279
+ }
1280
+ function matchingClose(src, openIndex, open, close) {
1281
+ let depth = 0;
1282
+ let angle = 0;
1283
+ let brace = 0;
1284
+ for (let i = openIndex; i < src.length; i++) {
1285
+ const c = src[i];
1286
+ if (c === open) depth++;
1287
+ else if (c === close) {
1288
+ depth--;
1289
+ if (depth === 0 && angle <= 0 && brace <= 0) return i;
1290
+ } else if (c === "<") angle++;
1291
+ else if (c === ">" && angle > 0) angle--;
1292
+ else if (c === "{") brace++;
1293
+ else if (c === "}" && brace > 0) brace--;
1294
+ }
1295
+ return -1;
1296
+ }
1297
+ function splitTopLevelParams(list) {
1298
+ const params = [];
1299
+ let current = "";
1300
+ let paren = 0;
1301
+ let angle = 0;
1302
+ let brace = 0;
1303
+ let bracket = 0;
1304
+ for (const c of list) {
1305
+ if (c === "(") paren++;
1306
+ else if (c === ")") paren--;
1307
+ else if (c === "<") angle++;
1308
+ else if (c === ">" && angle > 0) angle--;
1309
+ else if (c === "{") brace++;
1310
+ else if (c === "}" && brace > 0) brace--;
1311
+ else if (c === "[") bracket++;
1312
+ else if (c === "]" && bracket > 0) bracket--;
1313
+ if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
1314
+ if (current.trim()) params.push(current.trim());
1315
+ current = "";
1316
+ continue;
1317
+ }
1318
+ current += c;
1319
+ }
1320
+ if (current.trim()) params.push(current.trim());
1321
+ return params;
1322
+ }
1323
+ function paramName(raw) {
1324
+ let s = raw.trim();
1325
+ if (!s || s === "this") return void 0;
1326
+ if (s.startsWith("{") || s.startsWith("[")) return "input";
1327
+ s = s.replace(/^\.\.\./, "");
1328
+ const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
1329
+ if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
1330
+ return token;
1331
+ }
1332
+ function collectFromPattern(source, pattern) {
1333
+ const found = [];
1334
+ pattern.lastIndex = 0;
1335
+ let match;
1336
+ while (match = pattern.exec(source)) {
1337
+ const name = match[1];
1338
+ if (!name) continue;
1339
+ const openIndex = match.index + match[0].length - 1;
1340
+ const closeIndex = matchingClose(source, openIndex, "(", ")");
1341
+ if (closeIndex < 0) continue;
1342
+ const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
1343
+ const hasInput = params.length >= 2;
1344
+ const second = hasInput ? paramName(params[1] ?? "") : void 0;
1345
+ found.push({
1346
+ id: name,
1347
+ file: "",
1348
+ name,
1349
+ hasInput,
1350
+ inputName: hasInput ? second : void 0
1351
+ });
1352
+ }
1353
+ return found;
1354
+ }
1355
+ function parseExportedTestFunctions(source) {
1356
+ const text = stripTsCommentsAndStringsKeepCode(source);
1357
+ const seen = /* @__PURE__ */ new Set();
1358
+ const out = [];
1359
+ for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
1360
+ if (seen.has(item.name)) continue;
1361
+ seen.add(item.name);
1362
+ out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
1363
+ }
1364
+ return out;
1365
+ }
1366
+ function workspaceTestId(file, name) {
1367
+ return `${normalizePath(file)}:${name}`;
1368
+ }
1369
+ async function listWorkspaceTests(cwd) {
1370
+ const root = path7.resolve(cwd);
1371
+ let files;
1372
+ try {
1373
+ files = await collectFiles(root, isIgnoredWorkspacePath);
1374
+ } catch (err) {
1375
+ const code = err.code;
1376
+ if (code === "ENOENT") return [];
1377
+ throw err;
1378
+ }
1379
+ const tests = [];
1380
+ for (const file of files) {
1381
+ if (!isWorkspaceSpecFile(file.relPath)) continue;
1382
+ const posix = normalizePath(file.relPath);
1383
+ const source = await fs6.readFile(file.absPath, "utf8");
1384
+ for (const fn of parseExportedTestFunctions(source)) {
1385
+ tests.push({
1386
+ id: workspaceTestId(posix, fn.name),
1387
+ file: posix,
1388
+ name: fn.name,
1389
+ hasInput: fn.hasInput,
1390
+ inputName: fn.inputName
1391
+ });
1392
+ }
1393
+ }
1394
+ tests.sort((a, b) => a.id.localeCompare(b.id));
1395
+ return tests;
1396
+ }
1397
+ async function bundleWorkspaceTestFile(options) {
1398
+ const file = normalizePath(options.file);
1399
+ return bundleWorkspaceModule({
1400
+ cwd: options.cwd,
1401
+ entry: file,
1402
+ missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
1403
+ });
1404
+ }
1405
+
1406
+ // src/skills.ts
1407
+ import { existsSync, promises as fs7 } from "fs";
1408
+ import path8 from "path";
1409
+ import { fileURLToPath as fileURLToPath2 } from "url";
1410
+ var SKILL_NAMES = [
1411
+ "easytwin-render",
1412
+ "easytwin-core",
1413
+ "easytwin-develop",
1414
+ "easytwin-bootstrap",
1415
+ "easytwin-scene",
1416
+ "easytwin-upload",
1417
+ "easytwin-test"
1418
+ ];
1419
+ var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
1420
+ var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
1421
+ var META_FILE_NAME = ".easytwin-meta.json";
1422
+ var EASYTWIN_TYPES_DIR = ".easytwin/types";
1423
+ var MINIMAL_TSCONFIG = `${JSON.stringify(
1424
+ {
1425
+ compilerOptions: {
1426
+ target: "ES2022",
1427
+ module: "ESNext",
1428
+ moduleResolution: "bundler",
1429
+ strict: true,
1430
+ skipLibCheck: true,
1431
+ noEmit: true,
1432
+ paths: {
1433
+ "@easytwin/runtime": [".easytwin/types"],
1434
+ "@easytwin/apps": [".easytwin/types/apps"]
1435
+ }
1436
+ },
1437
+ include: ["src/**/*.ts"]
1438
+ },
1439
+ null,
1440
+ 2
1441
+ )}
1442
+ `;
1443
+ var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
1444
+ "skipLibCheck": true,
1445
+ "paths": {
1446
+ "@easytwin/runtime": [".easytwin/types"],
1447
+ "@easytwin/apps": [".easytwin/types/apps"]
1448
+ }`;
1449
+ var APPS_TYPES_FILE = "apps.d.ts";
1450
+ var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
1451
+
1452
+ export type TwinAppContext = {
1453
+ app: {
1454
+ id: string;
1455
+ mode: "preview" | "publish";
1456
+ };
1457
+ engine: RuntimeEngine;
1458
+ container: HTMLElement;
1459
+ runtimeScene: RuntimeScene | null;
1460
+ scene: RuntimeScene["sceneObject"] | null;
1461
+ camera: RuntimeScene["camera"]["main"] | null;
1462
+ sceneManager: {
1463
+ currentSceneId: string;
1464
+ runtime: SceneManager | null;
1465
+ loadScene(sceneId: string): Promise<void>;
1466
+ };
1467
+ logger: {
1468
+ log(...args: unknown[]): void;
1469
+ info(...args: unknown[]): void;
1470
+ warn(...args: unknown[]): void;
1471
+ error(...args: unknown[]): void;
1472
+ };
1473
+ assets: {
1474
+ text(path: string): Promise<string>;
1475
+ json<T = unknown>(path: string): Promise<T>;
1476
+ };
1477
+ cleanup(fn: () => void | Promise<void>): void;
1478
+ sceneCleanup(fn: () => void | Promise<void>): void;
1479
+ };
1480
+
1481
+ export declare abstract class TwinApp {
1482
+ init?(ctx: TwinAppContext): void | Promise<void>;
1483
+ onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
1484
+ onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
1485
+ onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
1486
+ onDispose?(ctx: TwinAppContext): void | Promise<void>;
1487
+ onError?(ctx: TwinAppContext, error: unknown): boolean | void;
1488
+ }
1489
+
1490
+ export declare function defineApp<T>(app: T): T;
1491
+ `;
1492
+ function buildRuntimeTypesContent(sourceDts) {
1493
+ return `${sourceDts.replace(/\s+$/, "")}
1494
+ `;
1495
+ }
1496
+ function normalizeTargets(target = "all") {
1497
+ if (target === "all") return ["cursor", "claude", "codex", "qoder"];
1498
+ if (Array.isArray(target)) return [...new Set(target)];
1499
+ return [target];
1500
+ }
1501
+ function resolveSkillsSourceDir() {
1502
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1503
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
1504
+ }
1505
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
1506
+ return path8.resolve(here, "..", "skills");
1507
+ }
1508
+ function resolveRuntimeTypesSourceFile() {
1509
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1510
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
1511
+ }
1512
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
1513
+ const fromDist = path8.join(here, "runtime-types", "index.d.ts");
1514
+ const fromSrc = path8.resolve(here, "lib", "index.d.ts");
1515
+ if (existsSync(fromDist)) return fromDist;
1516
+ if (existsSync(fromSrc)) return fromSrc;
1517
+ throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
1518
+ }
1519
+ async function readJson(file) {
1520
+ return JSON.parse(await fs7.readFile(file, "utf8"));
1521
+ }
1522
+ async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
1523
+ const marker = path8.join(skillsDir, ".easytwin-source.json");
1524
+ try {
1525
+ const meta = await readJson(marker);
1526
+ if (typeof meta.version === "string") return meta.version;
1527
+ } catch {
1528
+ }
1529
+ try {
1530
+ const pkg = await readJson(path8.resolve(skillsDir, "..", "package.json"));
1531
+ return pkg.version;
1532
+ } catch {
1533
+ return "0.0.0";
1534
+ }
1535
+ }
1536
+ async function copyDir(src, dest) {
1537
+ await fs7.mkdir(dest, { recursive: true });
1538
+ const entries = await fs7.readdir(src, { withFileTypes: true });
1539
+ for (const entry of entries) {
1540
+ const s = path8.join(src, entry.name);
1541
+ const d = path8.join(dest, entry.name);
1542
+ if (entry.isDirectory()) await copyDir(s, d);
1543
+ else if (entry.isFile()) await fs7.copyFile(s, d);
1544
+ }
1545
+ }
1546
+ async function dirMatches(src, dest) {
1547
+ let sourceEntries;
1548
+ try {
1549
+ sourceEntries = await fs7.readdir(src);
1550
+ } catch {
1551
+ return false;
1552
+ }
1553
+ for (const name of sourceEntries) {
1554
+ const s = path8.join(src, name);
1555
+ const d = path8.join(dest, name);
1556
+ const sStat = await fs7.stat(s);
1557
+ if (sStat.isDirectory()) {
1558
+ if (!await dirMatches(s, d)) return false;
1559
+ } else {
1560
+ let dStat;
1561
+ try {
1562
+ dStat = await fs7.stat(d);
1563
+ } catch {
1564
+ return false;
1565
+ }
1566
+ if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
1567
+ if (!(await fs7.readFile(s)).equals(await fs7.readFile(d))) return false;
1568
+ }
1569
+ }
1570
+ return true;
1571
+ }
1572
+ function actionFor(exists, matches) {
1573
+ if (!exists) return "created";
1574
+ return matches ? "unchanged" : "updated";
1575
+ }
1576
+ async function writeMeta(destDir, version) {
1577
+ const meta = { name: "@easytwin/devkit", version };
1578
+ await fs7.writeFile(path8.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
1579
+ }
1580
+ async function syncToDir(target, sourceRoot, targetRoot, version) {
1581
+ const entries = [];
1582
+ for (const name of SKILL_NAMES) {
1583
+ const src = path8.join(sourceRoot, name);
1584
+ const dest = path8.join(targetRoot, name);
1585
+ const exists = await fs7.stat(dest).then(() => true).catch(() => false);
1586
+ const matches = await dirMatches(src, dest);
1587
+ const action = actionFor(exists, matches);
1588
+ if (action !== "unchanged") {
1589
+ await fs7.rm(dest, { recursive: true, force: true });
1590
+ await copyDir(src, dest);
1591
+ }
1592
+ entries.push({ name, action });
1593
+ }
1594
+ await writeMeta(targetRoot, version);
1595
+ return { target, entries };
1596
+ }
1597
+ function buildCodexSegment(version) {
1598
+ return [
1599
+ CODEX_MARKER_BEGIN,
1600
+ "<!-- \u672C\u6BB5\u7531 `easytwin skills sync` \u7BA1\u7406,\u53EF\u6574\u6BB5\u66FF\u6362;\u624B\u52A8\u4FEE\u6539\u4F1A\u88AB\u4E0B\u6B21\u540C\u6B65\u8986\u76D6\u3002 -->",
1601
+ `EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
1602
+ "",
1603
+ "- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
1604
+ "- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
1605
+ "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
1606
+ "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
1607
+ "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
1608
+ "- `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",
1609
+ "- `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",
1610
+ "",
1611
+ "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
1612
+ CODEX_MARKER_END
1613
+ ].join("\n");
1614
+ }
1615
+ async function syncToCodex(sourceRoot, cwd, version) {
1616
+ const agentsFile = path8.join(cwd, "AGENTS.md");
1617
+ const segment = buildCodexSegment(version);
1618
+ let content = "";
1619
+ let exists = true;
1620
+ try {
1621
+ content = await fs7.readFile(agentsFile, "utf8");
1622
+ } catch {
1623
+ exists = false;
1624
+ }
1625
+ const start = content.indexOf(CODEX_MARKER_BEGIN);
1626
+ const end = content.indexOf(CODEX_MARKER_END);
1627
+ let action;
1628
+ let next;
1629
+ if (start === -1 || end === -1 || end < start) {
1630
+ action = exists ? "updated" : "created";
1631
+ next = content.length > 0 && !content.endsWith("\n") ? content + "\n\n" : content + (content.length > 0 ? "\n" : "");
1632
+ next += segment + "\n";
1633
+ } else {
1634
+ const current = content.slice(start, end + CODEX_MARKER_END.length);
1635
+ action = current === segment ? "unchanged" : "updated";
1636
+ next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
1637
+ }
1638
+ if (action !== "unchanged") {
1639
+ await fs7.mkdir(cwd, { recursive: true });
1640
+ await fs7.writeFile(agentsFile, next, "utf8");
1471
1641
  }
1472
- return { code, warnings };
1642
+ return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
1473
1643
  }
1474
- async function typesMissingHint(cwd) {
1644
+ async function syncRuntimeTypes(cwd, typesSourceFile) {
1645
+ const destDir = path8.join(cwd, ".easytwin", "types");
1646
+ const destFile = path8.join(destDir, "index.d.ts");
1647
+ const appsFile = path8.join(destDir, APPS_TYPES_FILE);
1648
+ const content = buildRuntimeTypesContent(await fs7.readFile(typesSourceFile, "utf8"));
1649
+ let runtimeCurrent = "";
1650
+ let appsCurrent = "";
1651
+ let runtimeExists = true;
1652
+ let appsExists = true;
1475
1653
  try {
1476
- await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
1477
- return void 0;
1654
+ runtimeCurrent = await fs7.readFile(destFile, "utf8");
1478
1655
  } catch {
1479
- return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
1656
+ runtimeExists = false;
1480
1657
  }
1481
- }
1482
- var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
1483
- var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1484
- function decodeVLQValues(str) {
1485
- const values = [];
1486
- let i = 0;
1487
- while (i < str.length) {
1488
- let result = 0;
1489
- let shift = 0;
1490
- let continuation = true;
1491
- while (continuation) {
1492
- if (i >= str.length) return values;
1493
- const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
1494
- if (digit < 0) return values;
1495
- continuation = (digit & 32) !== 0;
1496
- result += (digit & 31) << shift;
1497
- shift += 5;
1658
+ try {
1659
+ appsCurrent = await fs7.readFile(appsFile, "utf8");
1660
+ } catch {
1661
+ appsExists = false;
1662
+ }
1663
+ const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
1664
+ const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
1665
+ const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
1666
+ if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
1667
+ await fs7.mkdir(destDir, { recursive: true });
1668
+ if (runtimeAction !== "unchanged") await fs7.writeFile(destFile, content, "utf8");
1669
+ if (appsAction !== "unchanged") await fs7.writeFile(appsFile, APPS_DTS, "utf8");
1670
+ }
1671
+ const tsconfigPath = path8.join(cwd, "tsconfig.json");
1672
+ let tsconfig;
1673
+ let pathsHint;
1674
+ try {
1675
+ const existing = await fs7.readFile(tsconfigPath, "utf8");
1676
+ if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
1677
+ else {
1678
+ tsconfig = "manual-paths";
1679
+ pathsHint = TSCONFIG_PATHS_HINT;
1498
1680
  }
1499
- values.push(result & 1 ? -(result >> 1) : result >> 1);
1681
+ } catch {
1682
+ await fs7.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1683
+ tsconfig = "created";
1500
1684
  }
1501
- return values;
1685
+ return { action, tsconfig, pathsHint };
1502
1686
  }
1503
- function decodeMappings(map) {
1504
- const sources = map.sources ?? [];
1505
- const lines = (map.mappings ?? "").split(";");
1506
- let sourceIndex = 0;
1507
- let originalLine = 0;
1508
- let originalColumn = 0;
1509
- const decoded = [];
1510
- for (const line of lines) {
1511
- let generatedColumn = 0;
1512
- const segs = [];
1513
- if (line) {
1514
- for (const raw of line.split(",")) {
1515
- if (!raw) continue;
1516
- const nums = decodeVLQValues(raw);
1517
- if (nums[0] === void 0) continue;
1518
- generatedColumn += nums[0];
1519
- if (nums.length >= 4) {
1520
- sourceIndex += nums[1] ?? 0;
1521
- originalLine += nums[2] ?? 0;
1522
- originalColumn += nums[3] ?? 0;
1523
- segs.push({
1524
- generatedColumn,
1525
- source: sources[sourceIndex] ?? USER_ENTRY,
1526
- originalLine,
1527
- originalColumn
1528
- });
1529
- }
1530
- }
1531
- }
1532
- decoded.push(segs);
1687
+ async function syncSkills(options) {
1688
+ const targets = normalizeTargets(options.targets ?? "all");
1689
+ const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
1690
+ const version = options.version ?? await readDevkitVersion(sourceRoot);
1691
+ const summaries = [];
1692
+ for (const target of targets) {
1693
+ if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path8.join(options.cwd, ".cursor", "skills"), version));
1694
+ else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path8.join(options.cwd, ".claude", "skills"), version));
1695
+ else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path8.join(options.cwd, ".qoder", "skills"), version));
1696
+ else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
1533
1697
  }
1534
- return decoded;
1698
+ const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
1699
+ return { summaries, types };
1535
1700
  }
1536
- function originalPositionFor(map, line, column) {
1537
- const decoded = decodeMappings(map);
1538
- for (let i = line - 1; i >= 0; i--) {
1539
- const segs = decoded[i];
1540
- if (!segs || segs.length === 0) continue;
1541
- const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
1542
- let best = segs[0];
1543
- for (const seg of segs) {
1544
- if (seg.generatedColumn <= col) best = seg;
1545
- else break;
1701
+ async function detectSkillsStatus(cwd, sourceDir) {
1702
+ const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
1703
+ const version = await readDevkitVersion(sourceRoot);
1704
+ const targets = [];
1705
+ const dirTargets = ["cursor", "claude", "qoder"];
1706
+ const DIR_ROOTS = {
1707
+ cursor: ".cursor/skills",
1708
+ claude: ".claude/skills",
1709
+ qoder: ".qoder/skills"
1710
+ };
1711
+ for (const target of dirTargets) {
1712
+ const root = path8.join(cwd, DIR_ROOTS[target]);
1713
+ const missing = [];
1714
+ for (const name of SKILL_NAMES) {
1715
+ if (!await fs7.stat(path8.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
1546
1716
  }
1547
- if (!best) continue;
1548
- return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
1717
+ let stale = false;
1718
+ try {
1719
+ const meta = await readJson(path8.join(root, META_FILE_NAME));
1720
+ stale = meta.version !== version;
1721
+ } catch {
1722
+ stale = missing.length === 0;
1723
+ }
1724
+ targets.push({
1725
+ target,
1726
+ synced: missing.length === 0 && !stale,
1727
+ reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
1728
+ });
1549
1729
  }
1550
- return void 0;
1551
- }
1552
- function extractInlineSourceMap(code) {
1553
- const m = code.match(SOURCEMAP_RE);
1554
- if (!m?.[1]) return void 0;
1730
+ let agentsContent = "";
1555
1731
  try {
1556
- return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
1732
+ agentsContent = await fs7.readFile(path8.join(cwd, "AGENTS.md"), "utf8");
1557
1733
  } catch {
1558
- return void 0;
1559
1734
  }
1560
- }
1561
- function remapErrorStack(stack, bundledCode) {
1562
- const map = extractInlineSourceMap(bundledCode);
1563
- if (!map?.mappings) return stack;
1564
- return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
1565
- const orig = originalPositionFor(map, Number(line), Number(col));
1566
- if (!orig) return full;
1567
- return `${orig.source}:${orig.line}:${orig.column}`;
1568
- });
1735
+ const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
1736
+ targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
1737
+ let typesSynced = false;
1738
+ try {
1739
+ const dts = await fs7.readFile(path8.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
1740
+ const appsDts = await fs7.readFile(path8.join(cwd, ".easytwin", "types", APPS_TYPES_FILE), "utf8");
1741
+ typesSynced = dts.includes("registerEngineTickListener") && appsDts.includes("export declare abstract class TwinApp");
1742
+ } catch {
1743
+ typesSynced = false;
1744
+ }
1745
+ return { cwd, targets, typesSynced };
1569
1746
  }
1570
1747
 
1571
1748
  // src/twinAppHost.ts
@@ -1598,6 +1775,20 @@ var TwinAppPreviewHost = class {
1598
1775
  getEngine() {
1599
1776
  return this.engine;
1600
1777
  }
1778
+ /** 给 spec 调用:场景字段取当前引擎主场景(不要求已 Run TwinApp)。 */
1779
+ getTestContext() {
1780
+ if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
1781
+ const ctx = this.getContext();
1782
+ const runtimeScene = this.engine.mainScene ?? null;
1783
+ ctx.runtimeScene = runtimeScene;
1784
+ ctx.scene = runtimeScene?.sceneObject ?? null;
1785
+ ctx.camera = runtimeScene?.camera?.main ?? null;
1786
+ return ctx;
1787
+ }
1788
+ async invokeTest(fn, input) {
1789
+ if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
1790
+ return fn(this.getTestContext(), input);
1791
+ }
1601
1792
  async boot() {
1602
1793
  const { runtime, containerId, ossUrl, customComponentDeps } = this.options;
1603
1794
  this.engine = await runtime.RuntimeEngine.create({
@@ -1814,10 +2005,669 @@ var TwinAppPreviewHost = class {
1814
2005
  return this.ctx;
1815
2006
  }
1816
2007
  };
2008
+
2009
+ // src/previewHtml.ts
2010
+ function escapeHtml(text) {
2011
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2012
+ }
2013
+ function escapeJsonForScript(json) {
2014
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2015
+ }
2016
+ function buildStatusHtml(message) {
2017
+ return `<!DOCTYPE html>
2018
+ <html lang="zh-CN">
2019
+ <head>
2020
+ <meta charset="UTF-8">
2021
+ </head>
2022
+ <body>
2023
+ <p>${escapeHtml(message)}</p>
2024
+ </body>
2025
+ </html>`;
2026
+ }
2027
+ var PREVIEW_STYLE = `
2028
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
2029
+ body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
2030
+ #stage { position: relative; flex: 1; min-height: 0; }
2031
+ #twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
2032
+ #twin-root canvas { display: block; }
2033
+ #status {
2034
+ position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
2035
+ padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
2036
+ }
2037
+ #status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
2038
+ /* \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 */
2039
+ #status[hidden] { display: none; }
2040
+ #mock-banner {
2041
+ position: absolute; top: 0; left: 0; right: 0; z-index: 2;
2042
+ padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
2043
+ border-bottom: 1px solid #f0c36d; pointer-events: none;
2044
+ }
2045
+ #debug-log {
2046
+ display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
2047
+ max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
2048
+ background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
2049
+ white-space: pre-wrap; word-break: break-all; pointer-events: auto;
2050
+ }
2051
+ #debug-log.open { display: block; }
2052
+ #run-bar {
2053
+ flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
2054
+ padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
2055
+ }
2056
+ #run-bar button {
2057
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
2058
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
2059
+ }
2060
+ #run-bar button:disabled { opacity: .55; cursor: default; }
2061
+ #btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
2062
+ #run-status { color: #bbb; min-width: 4em; }
2063
+ #run-bar .spacer { flex: 1; }
2064
+ #test-panel {
2065
+ flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
2066
+ padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
2067
+ max-height: 30%; overflow: auto;
2068
+ }
2069
+ #test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
2070
+ #test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
2071
+ #test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
2072
+ #test-panel .test-empty { color: #777; }
2073
+ #test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
2074
+ #test-panel input.test-input {
2075
+ width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
2076
+ padding: 3px 6px; border-radius: 4px; font: 12px inherit;
2077
+ }
2078
+ #test-panel button {
2079
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
2080
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
2081
+ }
2082
+ #test-panel button:disabled { opacity: .55; cursor: default; }
2083
+ `;
2084
+ var RENDER_SCRIPT = `
2085
+ var statusEl = document.getElementById("status");
2086
+ var logEl = document.getElementById("debug-log");
2087
+ var engine = null;
2088
+ var runtime = null;
2089
+ var sceneJson = null;
2090
+ var host = null;
2091
+ var running = false;
2092
+ var runningTest = false;
2093
+ var testsReady = false;
2094
+ var hostKind = __HOST_KIND__;
2095
+ var vscodeApi = null;
2096
+ if (hostKind === "vscode") {
2097
+ try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
2098
+ }
2099
+ var pending = {};
2100
+ var seq = 0;
2101
+ var origFetch = window.fetch.bind(window);
2102
+ function now() { return new Date().toISOString().slice(11, 23); }
2103
+ function log(line) {
2104
+ var text = "[" + now() + "] " + line;
2105
+ if (logEl) {
2106
+ logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
2107
+ logEl.scrollTop = logEl.scrollHeight;
2108
+ }
2109
+ if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
2110
+ console.log("[EasyTwin]", line);
2111
+ }
2112
+ function nextId() { return String(++seq); }
2113
+ function handleHostMessage(msg) {
2114
+ if (!msg) return;
2115
+ if (msg.type === "proxy-fetch-result") {
2116
+ var p = pending[msg.id];
2117
+ if (!p) return;
2118
+ delete pending[msg.id];
2119
+ if (msg.error) p.reject(new Error(msg.error));
2120
+ else p.resolve(msg);
2121
+ return;
2122
+ }
2123
+ if (msg.type === "bundle-result") {
2124
+ if (!msg.ok) {
2125
+ log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
2126
+ setRunStatus("\u7F16\u8BD1\u5931\u8D25");
2127
+ setRunBusy(false);
2128
+ if (logEl) logEl.classList.add("open");
2129
+ return;
2130
+ }
2131
+ for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
2132
+ runUserCode(msg.code);
2133
+ return;
2134
+ }
2135
+ if (msg.type === "read-asset-result") {
2136
+ var ap = pending[msg.id];
2137
+ if (!ap) return;
2138
+ delete pending[msg.id];
2139
+ if (msg.error) ap.reject(new Error(msg.error));
2140
+ else ap.resolve(msg.text);
2141
+ return;
2142
+ }
2143
+ if (msg.type === "run-error-mapped") {
2144
+ log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
2145
+ return;
2146
+ }
2147
+ if (msg.type === "tests") {
2148
+ renderTests(msg.tests || []);
2149
+ return;
2150
+ }
2151
+ if (msg.type === "test-bundle") {
2152
+ if (!msg.ok) {
2153
+ log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
2154
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
2155
+ runningTest = false;
2156
+ setRunBusy(running);
2157
+ if (logEl) logEl.classList.add("open");
2158
+ return;
2159
+ }
2160
+ for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
2161
+ runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
2162
+ log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
2163
+ setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
2164
+ }).catch(function (err) {
2165
+ log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
2166
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
2167
+ if (logEl) logEl.classList.add("open");
2168
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
2169
+ }).then(function () {
2170
+ runningTest = false;
2171
+ setRunBusy(running);
2172
+ });
2173
+ }
2174
+ }
2175
+ function httpJson(url, init) {
2176
+ return origFetch(url, init).then(function (res) {
2177
+ return res.json().then(function (body) {
2178
+ if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
2179
+ return body;
2180
+ });
2181
+ });
2182
+ }
2183
+ function postToHost(msg) {
2184
+ if (vscodeApi) {
2185
+ vscodeApi.postMessage(msg);
2186
+ return;
2187
+ }
2188
+ if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
2189
+ if (msg.type === "run") {
2190
+ httpJson("/api/bundle", { method: "POST" }).then(function (body) {
2191
+ handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
2192
+ }).catch(function (err) {
2193
+ handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
2194
+ });
2195
+ return;
2196
+ }
2197
+ if (msg.type === "run-error") {
2198
+ httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
2199
+ handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
2200
+ }).catch(function (err) {
2201
+ log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
2202
+ });
2203
+ return;
2204
+ }
2205
+ if (msg.type === "read-asset") {
2206
+ httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
2207
+ handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
2208
+ }).catch(function (err) {
2209
+ handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
2210
+ });
2211
+ return;
2212
+ }
2213
+ if (msg.type === "list-tests") {
2214
+ httpJson("/api/tests").then(function (body) {
2215
+ handleHostMessage({ type: "tests", tests: body.tests || [] });
2216
+ }).catch(function (err) {
2217
+ log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
2218
+ });
2219
+ return;
2220
+ }
2221
+ if (msg.type === "run-test") {
2222
+ httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
2223
+ handleHostMessage(Object.assign({ type: "test-bundle" }, body));
2224
+ }).catch(function (err) {
2225
+ handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
2226
+ });
2227
+ }
2228
+ }
2229
+ function showStatus(text, isError) {
2230
+ if (!statusEl) return;
2231
+ statusEl.textContent = text;
2232
+ statusEl.className = isError ? "error" : "";
2233
+ statusEl.hidden = false;
2234
+ if (isError && logEl) logEl.classList.add("open");
2235
+ }
2236
+ function hideStatus() { if (statusEl) statusEl.hidden = true; }
2237
+ function formatError(err) {
2238
+ var msg = err && err.message ? err.message : String(err);
2239
+ var stack = err && err.stack ? "\\n" + err.stack : "";
2240
+ return msg + stack;
2241
+ }
2242
+ window.addEventListener("message", function (ev) {
2243
+ handleHostMessage(ev.data);
2244
+ });
2245
+ function b64ToBuf(b64) {
2246
+ var bin = atob(b64);
2247
+ var bytes = new Uint8Array(bin.length);
2248
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
2249
+ return bytes.buffer;
2250
+ }
2251
+ function proxyFetch(url, method) {
2252
+ return new Promise(function (resolve, reject) {
2253
+ if (!vscodeApi) {
2254
+ reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
2255
+ return;
2256
+ }
2257
+ var id = nextId();
2258
+ pending[id] = { resolve: resolve, reject: reject };
2259
+ vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
2260
+ }).then(function (msg) {
2261
+ var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
2262
+ return new Response(buf, {
2263
+ status: msg.status || 0,
2264
+ statusText: msg.statusText || "",
2265
+ headers: msg.headers || {}
2266
+ });
2267
+ });
2268
+ }
2269
+ function isHttpUrl(url) {
2270
+ return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
2271
+ }
2272
+ function isPlainHttp(url) {
2273
+ return typeof url === "string" && url.indexOf("http://") === 0;
2274
+ }
2275
+ if (hostKind === "vscode") {
2276
+ window.fetch = function (input, init) {
2277
+ var url = typeof input === "string" ? input : (input && input.url);
2278
+ log("fetch " + url);
2279
+ if (isPlainHttp(url)) {
2280
+ return proxyFetch(url, init && init.method).then(function (res) {
2281
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
2282
+ return res;
2283
+ });
2284
+ }
2285
+ return origFetch(input, init).catch(function (err) {
2286
+ log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
2287
+ if (!isHttpUrl(url)) throw err;
2288
+ return proxyFetch(url, init && init.method).then(function (res) {
2289
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
2290
+ return res;
2291
+ });
2292
+ });
2293
+ };
2294
+ var origOpen = XMLHttpRequest.prototype.open;
2295
+ var origSend = XMLHttpRequest.prototype.send;
2296
+ XMLHttpRequest.prototype.open = function (method, url) {
2297
+ this.__etMethod = method;
2298
+ this.__etUrl = String(url);
2299
+ return origOpen.apply(this, arguments);
2300
+ };
2301
+ XMLHttpRequest.prototype.send = function (body) {
2302
+ var xhr = this;
2303
+ var url = xhr.__etUrl;
2304
+ if (!isPlainHttp(url)) return origSend.call(this, body);
2305
+ log("xhr proxy " + xhr.__etMethod + " " + url);
2306
+ proxyFetch(url, xhr.__etMethod).then(function (res) {
2307
+ return res.arrayBuffer().then(function (buf) {
2308
+ var text = "";
2309
+ try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
2310
+ Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
2311
+ Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
2312
+ Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
2313
+ Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
2314
+ var rt = xhr.responseType;
2315
+ var response = buf;
2316
+ if (rt === "" || rt === "text") response = text;
2317
+ else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
2318
+ Object.defineProperty(xhr, "response", { configurable: true, value: response });
2319
+ Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
2320
+ if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
2321
+ xhr.dispatchEvent(new Event("load"));
2322
+ xhr.dispatchEvent(new Event("loadend"));
2323
+ });
2324
+ }).catch(function (err) {
2325
+ log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
2326
+ if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
2327
+ xhr.dispatchEvent(new Event("error"));
2328
+ xhr.dispatchEvent(new Event("loadend"));
2329
+ });
2330
+ };
2331
+ function patchHttpSrc(proto, prop) {
2332
+ var desc = Object.getOwnPropertyDescriptor(proto, prop);
2333
+ if (!desc || typeof desc.set !== "function") return;
2334
+ Object.defineProperty(proto, prop, {
2335
+ configurable: true,
2336
+ enumerable: desc.enumerable,
2337
+ get: function () { return desc.get.call(this); },
2338
+ set: function (value) {
2339
+ var el = this;
2340
+ var url = String(value);
2341
+ if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
2342
+ log("media proxy " + url);
2343
+ proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
2344
+ desc.set.call(el, URL.createObjectURL(blob));
2345
+ }).catch(function (err) {
2346
+ log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
2347
+ try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
2348
+ });
2349
+ }
2350
+ });
2351
+ }
2352
+ patchHttpSrc(HTMLImageElement.prototype, "src");
2353
+ patchHttpSrc(HTMLMediaElement.prototype, "src");
2354
+ }
2355
+ document.getElementById("btn-debug").addEventListener("click", function () {
2356
+ if (logEl) logEl.classList.toggle("open");
2357
+ });
2358
+ var btnDevtools = document.getElementById("btn-devtools");
2359
+ var btnOutput = document.getElementById("btn-output");
2360
+ if (hostKind === "http") {
2361
+ if (btnDevtools) btnDevtools.hidden = true;
2362
+ if (btnOutput) btnOutput.hidden = true;
2363
+ }
2364
+ if (btnDevtools) btnDevtools.addEventListener("click", function () {
2365
+ postToHost({ type: "open-devtools" });
2366
+ });
2367
+ if (btnOutput) btnOutput.addEventListener("click", function () {
2368
+ postToHost({ type: "show-output" });
2369
+ });
2370
+ function setRunStatus(text) {
2371
+ var el = document.getElementById("run-status");
2372
+ if (el) el.textContent = text;
2373
+ }
2374
+ function setRunBusy(busy) {
2375
+ running = busy;
2376
+ var btn = document.getElementById("btn-run");
2377
+ if (btn) btn.disabled = !!busy || runningTest;
2378
+ syncTestControls();
2379
+ }
2380
+ function syncTestControls() {
2381
+ var panel = document.getElementById("test-panel");
2382
+ if (!panel) return;
2383
+ var disabled = !testsReady || running || runningTest;
2384
+ var nodes = panel.querySelectorAll("button.test-run, input.test-input");
2385
+ for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
2386
+ }
2387
+ function renderTests(tests) {
2388
+ var list = document.getElementById("test-list");
2389
+ if (!list) return;
2390
+ list.textContent = "";
2391
+ if (!tests || !tests.length) {
2392
+ var empty = document.createElement("span");
2393
+ empty.className = "test-empty";
2394
+ empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
2395
+ list.appendChild(empty);
2396
+ return;
2397
+ }
2398
+ var groups = {};
2399
+ var order = [];
2400
+ for (var i = 0; i < tests.length; i++) {
2401
+ var t = tests[i];
2402
+ if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
2403
+ groups[t.file].push(t);
2404
+ }
2405
+ for (var g = 0; g < order.length; g++) {
2406
+ var file = order[g];
2407
+ var heading = document.createElement("span");
2408
+ heading.className = "test-file";
2409
+ heading.textContent = file;
2410
+ list.appendChild(heading);
2411
+ var items = groups[file];
2412
+ for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
2413
+ }
2414
+ syncTestControls();
2415
+ }
2416
+ function makeTestControl(t) {
2417
+ var wrap = document.createElement("span");
2418
+ wrap.className = "test-item";
2419
+ if (t.hasInput) {
2420
+ var input = document.createElement("input");
2421
+ input.type = "text";
2422
+ input.className = "test-input";
2423
+ input.placeholder = t.inputName || "input";
2424
+ var btn = document.createElement("button");
2425
+ btn.type = "button";
2426
+ btn.className = "test-run";
2427
+ btn.textContent = t.name;
2428
+ btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
2429
+ input.addEventListener("keydown", function (ev) {
2430
+ if (ev.key === "Enter") requestRunTest(t.id, input.value);
2431
+ });
2432
+ wrap.appendChild(input);
2433
+ wrap.appendChild(btn);
2434
+ } else {
2435
+ var only = document.createElement("button");
2436
+ only.type = "button";
2437
+ only.className = "test-run";
2438
+ only.textContent = t.name;
2439
+ only.addEventListener("click", function () { requestRunTest(t.id); });
2440
+ wrap.appendChild(only);
2441
+ }
2442
+ return wrap;
2443
+ }
2444
+ function requestRunTest(id, input) {
2445
+ if (!testsReady || running || runningTest) return;
2446
+ if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
2447
+ runningTest = true;
2448
+ setRunBusy(running);
2449
+ setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
2450
+ log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
2451
+ postToHost({ type: "run-test", id: id, input: input });
2452
+ }
2453
+ async function runExportedTest(code, exportName, input, hasInput) {
2454
+ var blob = new Blob([code], { type: "text/javascript" });
2455
+ var url = URL.createObjectURL(blob);
2456
+ try {
2457
+ var mod = await import(url);
2458
+ var fn = mod[exportName];
2459
+ if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
2460
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
2461
+ var ctx = host.getTestContext();
2462
+ var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
2463
+ await Promise.resolve(result);
2464
+ } finally {
2465
+ URL.revokeObjectURL(url);
2466
+ }
2467
+ }
2468
+ function applyRenderPatch() {
2469
+ // 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,
2470
+ // \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
2471
+ var scene = engine && engine.mainScene;
2472
+ if (
2473
+ scene && scene.rootComponent && scene.rootComponent.version &&
2474
+ !scene.postprocessingComponent &&
2475
+ runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
2476
+ ) {
2477
+ 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");
2478
+ scene.registerRenderCallback(function () {
2479
+ scene.renderer.render(scene.sceneObject, scene.camera.main);
2480
+ });
2481
+ }
2482
+ }
2483
+ async function loadHostScene(nextEngine, nextJson) {
2484
+ engine = nextEngine;
2485
+ var sceneManager = engine.getManager(runtime.SceneManager);
2486
+ await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
2487
+ applyRenderPatch();
2488
+ }
2489
+ function readAsset(relPath) {
2490
+ return new Promise(function (resolve, reject) {
2491
+ var id = nextId();
2492
+ pending[id] = { resolve: resolve, reject: reject };
2493
+ postToHost({ type: "read-asset", id: id, path: relPath });
2494
+ });
2495
+ }
2496
+ async function runUserCode(code) {
2497
+ setRunStatus("\u8FD0\u884C\u4E2D\u2026");
2498
+ try {
2499
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
2500
+ await host.run(code);
2501
+ log("Run \u5B8C\u6210");
2502
+ setRunStatus("\u8FD0\u884C\u4E2D");
2503
+ } catch (err) {
2504
+ log("Run \u5931\u8D25 " + formatError(err));
2505
+ setRunStatus("\u5931\u8D25");
2506
+ if (logEl) logEl.classList.add("open");
2507
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
2508
+ } finally {
2509
+ setRunBusy(false);
2510
+ }
2511
+ }
2512
+ document.getElementById("btn-run").addEventListener("click", function () {
2513
+ if (running || runningTest) return;
2514
+ if (!engine) {
2515
+ log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
2516
+ return;
2517
+ }
2518
+ setRunBusy(true);
2519
+ setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
2520
+ log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
2521
+ postToHost({ type: "run" });
2522
+ });
2523
+ document.getElementById("btn-refresh-tests").addEventListener("click", function () {
2524
+ log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
2525
+ postToHost({ type: "list-tests" });
2526
+ });
2527
+ function readEmbeddedTests() {
2528
+ var el = document.getElementById("workspace-tests");
2529
+ if (!el || !el.textContent) return [];
2530
+ try { return JSON.parse(el.textContent); } catch (e) { return []; }
2531
+ }
2532
+ renderTests(readEmbeddedTests());
2533
+ function normalizeHierarchyConfig(vo) {
2534
+ var objs = vo && vo.objs ? vo.objs : [];
2535
+ for (var i = 0; i < objs.length; i++) {
2536
+ var hc = objs[i].hierarchyConfig || {};
2537
+ if (typeof hc.active !== "boolean") {
2538
+ hc.active = hc.inActive === true ? false : hc.visible !== false;
2539
+ }
2540
+ if (typeof hc.lock !== "boolean") hc.lock = false;
2541
+ if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
2542
+ objs[i].hierarchyConfig = hc;
2543
+ }
2544
+ return vo;
2545
+ }
2546
+ function toSceneJson(runtime, raw) {
2547
+ var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
2548
+ if (vo && vo.sceneComponent) {
2549
+ return {
2550
+ id: vo.id || "preview",
2551
+ name: vo.name || "\u573A\u666F\u9884\u89C8",
2552
+ sceneComponent: vo.sceneComponent
2553
+ };
2554
+ }
2555
+ vo = normalizeHierarchyConfig(vo);
2556
+ var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
2557
+ var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
2558
+ return {
2559
+ id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
2560
+ name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
2561
+ sceneComponent: runtime.convertObjToComponentJson(vo)
2562
+ };
2563
+ }
2564
+ try {
2565
+ showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
2566
+ log("runtimeUri=" + __RUNTIME_URI__);
2567
+ log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
2568
+ log("1/4 import twin-runtime / TwinApp host");
2569
+ runtime = await import("@easytwin/runtime");
2570
+ var hostMod = await import(__HOST_URI__);
2571
+ log("2/4 parse scene JSON");
2572
+ var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
2573
+ sceneJson = toSceneJson(runtime, sceneVo);
2574
+ log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
2575
+ log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
2576
+ host = new hostMod.TwinAppPreviewHost({
2577
+ runtime: runtime,
2578
+ containerId: "twin-root",
2579
+ ossUrl: __BASE_OSS_URL__,
2580
+ appId: __APP_ID__,
2581
+ sceneId: sceneJson.id,
2582
+ sceneJson: sceneJson,
2583
+ customComponentDeps: {
2584
+ "@easytwin/runtime": runtime,
2585
+ "@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
2586
+ react: { createElement: function () { return null; }, Fragment: "div" }
2587
+ },
2588
+ loadScene: loadHostScene,
2589
+ readAsset: readAsset,
2590
+ log: log
2591
+ });
2592
+ await host.boot();
2593
+ engine = host.getEngine();
2594
+ log("4/4 loadScene");
2595
+ log("\u5B8C\u6210");
2596
+ hideStatus();
2597
+ testsReady = true;
2598
+ setRunBusy(false);
2599
+ } catch (err) {
2600
+ log("\u5931\u8D25 " + formatError(err));
2601
+ 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);
2602
+ }
2603
+ window.addEventListener("pagehide", function () {
2604
+ if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
2605
+ });
2606
+ `;
2607
+ function originOf(url) {
2608
+ return new URL(url).origin;
2609
+ }
2610
+ function buildPreviewHtml(options) {
2611
+ const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
2612
+ const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
2613
+ const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
2614
+ const appId = options.appId ?? "preview";
2615
+ const hostKind = options.host ?? "vscode";
2616
+ const apiOrigin = originOf(baseUrl);
2617
+ const ossOrigin = originOf(ossUrl);
2618
+ const runtimeOrigin = originOf(runtimeUri);
2619
+ const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
2620
+ 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));
2621
+ const importMap = JSON.stringify({
2622
+ imports: {
2623
+ "@easytwin/runtime": runtimeUri,
2624
+ "@easytwin/apps": appsUri
2625
+ }
2626
+ });
2627
+ const testsJson = JSON.stringify(options.tests ?? []);
2628
+ 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>` : "";
2629
+ return `<!DOCTYPE html>
2630
+ <html lang="zh-CN">
2631
+ <head>
2632
+ <meta charset="UTF-8">
2633
+ <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:;">
2634
+ <title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
2635
+ <style>${PREVIEW_STYLE}</style>
2636
+ <script type="importmap">${importMap}</script>
2637
+ </head>
2638
+ <body>
2639
+ <div id="stage">
2640
+ <div id="twin-root"></div>
2641
+ ${mockBanner}
2642
+ <div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
2643
+ <pre id="debug-log"></pre>
2644
+ </div>
2645
+ <div id="test-panel">
2646
+ <span class="test-label">\u6D4B\u8BD5</span>
2647
+ <div id="test-list"></div>
2648
+ <button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
2649
+ </div>
2650
+ <div id="run-bar">
2651
+ <button type="button" id="btn-run">Run</button>
2652
+ <span id="run-status">\u5C31\u7EEA</span>
2653
+ <span class="spacer"></span>
2654
+ <button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
2655
+ <button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
2656
+ <button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
2657
+ </div>
2658
+ <script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
2659
+ <script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
2660
+ <script type="module">
2661
+ ${renderScript}
2662
+ </script>
2663
+ </body>
2664
+ </html>`;
2665
+ }
1817
2666
  export {
1818
2667
  APPS_DTS,
1819
2668
  APPS_MODULE,
1820
2669
  APPS_TYPES_FILE,
2670
+ APP_ID_HEADER,
1821
2671
  AUTH_HEADER,
1822
2672
  BundleError,
1823
2673
  CODEX_MARKER_BEGIN,
@@ -1843,43 +2693,53 @@ export {
1843
2693
  MOCK_APP_ID,
1844
2694
  MOCK_APP_SECRET,
1845
2695
  MOCK_SCENE_NAME,
1846
- OP_ACCOUNT_ID_HEADER,
1847
- OP_USER_ID_HEADER,
1848
2696
  PORTABLE_RUNTIME_EXPORTS,
1849
2697
  RUNTIME_MODULE,
1850
2698
  SKILL_NAMES,
1851
- SPACE_ID_HEADER,
1852
2699
  TEST_BASE_URL,
1853
- TEST_OP_ACCOUNT_ID,
1854
- TEST_OP_USER_ID,
1855
- TEST_SPACE_ID,
1856
2700
  TSCONFIG_PATHS_HINT,
1857
2701
  TwinApp,
1858
2702
  TwinAppPreviewHost,
1859
2703
  USER_ENTRY,
2704
+ WORKSPACE_CODE_EXTENSIONS,
2705
+ WORKSPACE_IGNORED_DIRS,
2706
+ WORKSPACE_IGNORED_FILES,
1860
2707
  appendGitignore,
1861
2708
  applyWorkspacePull,
1862
2709
  assertUploadable,
1863
2710
  buildMultipartBody,
2711
+ buildPreviewHtml,
1864
2712
  buildRuntimeTypesContent,
2713
+ buildStatusHtml,
2714
+ buildWorkspacePushBody,
1865
2715
  bundleUserCode,
2716
+ bundleWorkspaceModule,
2717
+ bundleWorkspaceTestFile,
1866
2718
  collectFiles,
1867
2719
  configFilePath,
1868
2720
  createAppInstance,
1869
2721
  defaultPullIgnore,
2722
+ defaultWorkspaceIgnore,
1870
2723
  defineApp,
1871
2724
  deriveExampleSceneId,
1872
2725
  detectSkillsStatus,
1873
2726
  directoryDepth,
2727
+ escapeHtml,
2728
+ escapeJsonForScript,
1874
2729
  exampleSceneSummary,
1875
2730
  extractInlineSourceMap,
1876
2731
  extractSceneArray,
2732
+ formatUploadResult,
1877
2733
  formatWorkspacePullPlan,
1878
2734
  formatWorkspacePullResult,
1879
2735
  initConfig,
2736
+ isIgnoredWorkspacePath,
1880
2737
  isMockCredentials,
1881
2738
  isSafeRelPath,
2739
+ isWorkspaceCodeFile,
2740
+ isWorkspaceSpecFile,
1882
2741
  listScenes,
2742
+ listWorkspaceTests,
1883
2743
  loadConfig,
1884
2744
  loadExampleScene,
1885
2745
  normalizeLinkedScenes,
@@ -1889,8 +2749,10 @@ export {
1889
2749
  normalizeWorkspaceConfig,
1890
2750
  normalizeWorkspaceFiles,
1891
2751
  parseConfig,
2752
+ parseExportedTestFunctions,
1892
2753
  parseSceneStructure,
1893
2754
  planWorkspacePull,
2755
+ planWorkspaceUpload,
1894
2756
  portableRuntimeWarning,
1895
2757
  pullScene,
1896
2758
  pullWorkspace,
@@ -1911,7 +2773,7 @@ export {
1911
2773
  validateConfigShape,
1912
2774
  workspacePullHasConflicts,
1913
2775
  workspacePullPendingWrites,
2776
+ workspaceTestId,
1914
2777
  writeConfigFile,
1915
2778
  writeConfigScenes
1916
2779
  };
1917
- //# sourceMappingURL=index.js.map