@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/bin.js CHANGED
@@ -7,7 +7,7 @@ import { readFile } from "fs/promises";
7
7
  import { createInterface } from "readline/promises";
8
8
  import { stdin, stdout } from "process";
9
9
  import { pathToFileURL } from "url";
10
- import path8 from "path";
10
+ import path10 from "path";
11
11
 
12
12
  // src/config.ts
13
13
  import { promises as fs } from "fs";
@@ -15,9 +15,6 @@ import path from "path";
15
15
  var CONFIG_FILE_NAME = "easytwin.config.json";
16
16
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
17
17
  var TEST_BASE_URL = "http://172.16.125.3:10100/";
18
- var TEST_OP_ACCOUNT_ID = "25";
19
- var TEST_OP_USER_ID = "25";
20
- var TEST_SPACE_ID = "54";
21
18
  var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
22
19
  var MOCK_APP_ID = "test";
23
20
  var MOCK_APP_SECRET = "test";
@@ -62,29 +59,14 @@ function validateConfigShape(value) {
62
59
  if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
63
60
  throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
64
61
  }
65
- const opAccountId = optionalGatewayId(v.opAccountId);
66
- const opUserId = optionalGatewayId(v.opUserId);
67
- const spaceId = optionalGatewayId(v.spaceId);
68
62
  const config = { appId: v.appId, appSecret: v.appSecret };
69
63
  if (v.env === "prod" || v.env === "test") config.env = v.env;
70
64
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
71
65
  if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
72
- if (opAccountId) config.opAccountId = opAccountId;
73
- if (opUserId) config.opUserId = opUserId;
74
- if (spaceId) config.spaceId = spaceId;
75
66
  const scenes = parseConfigScenes(v.scenes);
76
67
  if (scenes) config.scenes = scenes;
77
68
  return config;
78
69
  }
79
- function optionalGatewayId(value) {
80
- if (typeof value === "string") {
81
- const t = value.trim();
82
- return t.length > 0 ? t : void 0;
83
- }
84
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
85
- if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
86
- return void 0;
87
- }
88
70
  function parseConfigScenes(value) {
89
71
  if (value === void 0) return void 0;
90
72
  if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
@@ -134,30 +116,11 @@ function resolveConfig(file, env = process.env) {
134
116
  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");
135
117
  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");
136
118
  const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
137
- const useTestGateway = easyEnv === "test";
138
- const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
139
- const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
140
- const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
141
- const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
142
- if (opAccountId) resolved.opAccountId = opAccountId;
143
- if (opUserId) resolved.opUserId = opUserId;
144
- if (spaceId) resolved.spaceId = spaceId;
145
- return resolved;
119
+ return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
146
120
  }
147
121
  async function loadConfig(cwd, env = process.env) {
148
122
  const file = await readConfigFile(cwd);
149
- const resolved = resolveConfig(file, env);
150
- if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
151
- if (!file.opAccountId || !file.opUserId || !file.spaceId) {
152
- await writeConfigFile(cwd, {
153
- ...file,
154
- opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
155
- opUserId: file.opUserId ?? TEST_OP_USER_ID,
156
- spaceId: file.spaceId ?? TEST_SPACE_ID
157
- });
158
- }
159
- }
160
- return resolved;
123
+ return resolveConfig(file, env);
161
124
  }
162
125
  async function writeConfigFile(cwd, config) {
163
126
  const file = configFilePath(cwd);
@@ -165,12 +128,6 @@ async function writeConfigFile(cwd, config) {
165
128
  if (config.env) body.env = config.env;
166
129
  if (config.baseUrl) body.baseUrl = config.baseUrl;
167
130
  if (config.ossUrl) body.ossUrl = config.ossUrl;
168
- const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
169
- const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
170
- const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
171
- if (opAccountId) body.opAccountId = opAccountId;
172
- if (opUserId) body.opUserId = opUserId;
173
- if (spaceId) body.spaceId = spaceId;
174
131
  if (config.scenes !== void 0) body.scenes = config.scenes;
175
132
  await fs.mkdir(cwd, { recursive: true });
176
133
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
@@ -208,22 +165,20 @@ async function writeConfigScenes(cwd, scenes) {
208
165
  import http from "http";
209
166
  import https from "https";
210
167
  import { URL as URL2 } from "url";
168
+ var APP_ID_HEADER = "x-app-id";
211
169
  var AUTH_HEADER = "x-app-secret";
212
- var OP_ACCOUNT_ID_HEADER = "op-account-id";
213
- var OP_USER_ID_HEADER = "op-user-id";
214
- var SPACE_ID_HEADER = "space-id";
215
170
  function enc(id) {
216
171
  return encodeURIComponent(id);
217
172
  }
218
173
  var ENDPOINTS = {
219
- /** GET 已关联场景列表。 */
220
- linkedScenes: (applicationId) => `/api/twin/v1/sdk-application-scenes/${enc(applicationId)}/scenes`,
221
- /** GET 工作区全部代码文件。 */
222
- workspaceCode: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}`,
223
- /** GET 工作区文件约束。 */
224
- workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`,
225
- /** POST 新建 / PATCH 批量更新 / DELETE 批量删除。 */
226
- workspaceCodeFiles: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/files`
174
+ /** POST 已关联场景列表(分享路径,空 body)。 */
175
+ linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
176
+ /** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
177
+ workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
178
+ /** POST 一次推送 create/update/delete(分享路径)。 */
179
+ workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
180
+ /** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
181
+ workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
227
182
  };
228
183
  var EasyTwinApiError = class extends Error {
229
184
  status;
@@ -266,35 +221,25 @@ var EasyTwinClient = class {
266
221
  baseUrl;
267
222
  /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
268
223
  ossUrl;
269
- /** 接入凭证 App ID;同时作为场景/代码 API applicationId 路径参数。 */
224
+ /** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
270
225
  appId;
271
226
  /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
272
227
  mock;
273
228
  appSecret;
274
- opAccountId;
275
- opUserId;
276
- spaceId;
277
229
  constructor(config) {
278
230
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
279
231
  this.ossUrl = config.ossUrl.replace(/\/+$/, "");
280
232
  this.mock = config.mock;
281
233
  this.appId = config.appId;
282
234
  this.appSecret = config.appSecret;
283
- this.opAccountId = config.opAccountId;
284
- this.opUserId = config.opUserId;
285
- this.spaceId = config.spaceId;
286
235
  }
287
236
  /** 全仓唯一认证头注入点。 */
288
237
  authHeaders() {
289
- const headers = { [AUTH_HEADER]: this.appSecret };
290
- if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
291
- if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
292
- if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
293
- return headers;
238
+ return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
294
239
  }
295
240
  /** JSON 请求(原生 fetch)。 */
296
- async request(path9, options = {}) {
297
- const url = `${this.baseUrl}${path9}`;
241
+ async request(path11, options = {}) {
242
+ const url = `${this.baseUrl}${path11}`;
298
243
  const headers = { ...this.authHeaders(), ...options.headers };
299
244
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
300
245
  headers["Content-Type"] = "application/json";
@@ -316,8 +261,8 @@ var EasyTwinClient = class {
316
261
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
317
262
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
318
263
  */
319
- async upload(path9, options) {
320
- const url = new URL2(`${this.baseUrl}${path9}`);
264
+ async upload(path11, options) {
265
+ const url = new URL2(`${this.baseUrl}${path11}`);
321
266
  const mod = url.protocol === "https:" ? https : http;
322
267
  const headers = {
323
268
  ...this.authHeaders(),
@@ -474,8 +419,11 @@ function deriveExampleSceneId(payload) {
474
419
  async function exampleSceneSummary(exampleFile) {
475
420
  return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
476
421
  }
422
+ async function fetchLinkedScenes(client) {
423
+ return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
424
+ }
477
425
  async function listScenes(client, options = {}) {
478
- const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" }));
426
+ const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
479
427
  if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
480
428
  return scenes;
481
429
  }
@@ -488,7 +436,7 @@ async function pullScene(client, id, options = {}) {
488
436
  }
489
437
  return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
490
438
  }
491
- const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
439
+ const data = await fetchLinkedScenes(client);
492
440
  const scenes = normalizeLinkedScenes(data);
493
441
  const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
494
442
  if (!hit) {
@@ -509,7 +457,43 @@ async function saveSceneJson(scene, out) {
509
457
  // src/upload.ts
510
458
  import { promises as fs3 } from "fs";
511
459
  import path3 from "path";
512
- var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".git"]);
460
+ var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
461
+ ".git",
462
+ "node_modules",
463
+ "dist",
464
+ ".easytwin",
465
+ ".cursor",
466
+ ".claude",
467
+ ".qoder",
468
+ ".vscode"
469
+ ]);
470
+ var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
471
+ function isTsconfigFile(base) {
472
+ return /^tsconfig(\..+)?\.json$/i.test(base);
473
+ }
474
+ var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
475
+ function isWorkspaceSpecFile(relPath) {
476
+ const base = normalizePath(relPath).split("/").pop() ?? "";
477
+ return /\.spec\.ts$/i.test(base);
478
+ }
479
+ function isIgnoredWorkspacePath(relPath) {
480
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
481
+ if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
482
+ const base = parts[parts.length - 1];
483
+ if (base === void 0) return false;
484
+ if (WORKSPACE_IGNORED_FILES.has(base)) return true;
485
+ if (isTsconfigFile(base)) return true;
486
+ return base.toLowerCase().endsWith(".scene.json");
487
+ }
488
+ function defaultWorkspaceIgnore(relPath) {
489
+ return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
490
+ }
491
+ function combineUploadIgnore(extra) {
492
+ return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
493
+ }
494
+ async function collectWorkspaceCodeFiles(dir, ignore) {
495
+ return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
496
+ }
513
497
  async function collectFiles(dir, ignore = () => false) {
514
498
  const files = [];
515
499
  async function walk(current, rel) {
@@ -518,7 +502,7 @@ async function collectFiles(dir, ignore = () => false) {
518
502
  const relPath = path3.posix.join(rel, entry.name);
519
503
  if (ignore(relPath)) continue;
520
504
  if (entry.isDirectory()) {
521
- if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
505
+ if (entry.name === ".git") continue;
522
506
  await walk(path3.join(current, entry.name), relPath);
523
507
  } else if (entry.isFile()) {
524
508
  const absPath = path3.join(current, entry.name);
@@ -537,26 +521,9 @@ function asRecord2(value) {
537
521
  function asString2(value) {
538
522
  return typeof value === "string" ? value : "";
539
523
  }
540
- function asNumber(value) {
541
- return typeof value === "number" && Number.isFinite(value) ? value : NaN;
542
- }
543
524
  function normalizePath(relPath) {
544
525
  return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
545
526
  }
546
- function normalizeWorkspaceConfig(data) {
547
- const it = asRecord2(data);
548
- if (!Array.isArray(it.allowedFileExtensions)) throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
549
- const maxCodeFiles = asNumber(it.maxCodeFiles);
550
- const maxCodeDirectoryDepth = asNumber(it.maxCodeDirectoryDepth);
551
- if (!Number.isFinite(maxCodeFiles) || !Number.isFinite(maxCodeDirectoryDepth)) {
552
- throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
553
- }
554
- return {
555
- allowedFileExtensions: it.allowedFileExtensions.filter((e) => typeof e === "string"),
556
- maxCodeFiles,
557
- maxCodeDirectoryDepth
558
- };
559
- }
560
527
  function normalizeWorkspaceFiles(data) {
561
528
  if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
562
529
  return data.map((item) => {
@@ -573,91 +540,98 @@ function extensionOf(relPath) {
573
540
  function allowedExtensionSet(list) {
574
541
  return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
575
542
  }
576
- function directoryDepth(relPath) {
577
- const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
578
- return Math.max(0, parts.length - 1);
543
+ var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
544
+ function isWorkspaceCodeFile(relPath) {
545
+ return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
579
546
  }
580
- function assertUploadable(files, config) {
581
- const errors = [];
582
- if (files.length > config.maxCodeFiles) {
583
- errors.push(`\u6587\u4EF6\u6570 ${files.length} \u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeFiles}`);
584
- }
585
- const tooDeep = files.filter((f) => directoryDepth(f.relPath) > config.maxCodeDirectoryDepth);
586
- if (tooDeep.length > 0) {
587
- errors.push(
588
- `\u76EE\u5F55\u6DF1\u5EA6\u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeDirectoryDepth}:${tooDeep.map((f) => f.relPath).join(", ")}`
589
- );
590
- }
591
- if (config.allowedFileExtensions.length > 0) {
592
- const allowed = allowedExtensionSet(config.allowedFileExtensions);
593
- const bad = files.filter((f) => !allowed.has(extensionOf(f.relPath)));
594
- if (bad.length > 0) {
595
- errors.push(
596
- `\u6269\u5C55\u540D\u4E0D\u5728\u5141\u8BB8\u5217\u8868(${config.allowedFileExtensions.join(", ")}):${bad.map((f) => f.relPath).join(", ")}`
597
- );
547
+ function countsFromPlan(plan) {
548
+ return {
549
+ created: plan.creates.length,
550
+ updated: plan.updates.length,
551
+ deleted: plan.deletes.length,
552
+ unchanged: plan.unchanged.length
553
+ };
554
+ }
555
+ function planWorkspaceUpload(local, remote) {
556
+ const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
557
+ const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
558
+ const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
559
+ const creates = [];
560
+ const updates = [];
561
+ const unchanged = [];
562
+ for (const item of local) {
563
+ const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
564
+ if (!remoteFile) creates.push(item);
565
+ else if (remoteFile.content !== item.content) {
566
+ updates.push({ file: item.file, id: remoteFile.id, content: item.content });
567
+ } else {
568
+ unchanged.push(item.file);
598
569
  }
599
570
  }
600
- if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
571
+ return { creates, updates, deletes, unchanged };
572
+ }
573
+ function formatUploadResult(result) {
574
+ const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
575
+ return `${prefix}\u672C\u5730 ${result.fileCount} \u4E2A\u6587\u4EF6,\u5171 ${result.byteCount} bytes;\u65B0\u589E ${result.created},\u66F4\u65B0 ${result.updated},\u5220\u9664 ${result.deleted},\u672A\u53D8 ${result.unchanged}`;
601
576
  }
602
577
  async function mockUpload(dir, options) {
603
- const ignore = options.ignore ?? (() => false);
604
- const files = await collectFiles(dir, ignore);
578
+ const ignore = combineUploadIgnore(options.ignore);
579
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
605
580
  const total = files.reduce((sum, f) => sum + f.size, 0);
606
581
  options.onProgress?.({ phase: "collect", current: total, total });
607
582
  options.onProgress?.({ phase: "upload", current: total, total });
608
- return { fileCount: files.length, byteCount: total, mock: true };
583
+ return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
584
+ }
585
+ function buildWorkspacePushBody(plan) {
586
+ const body = {};
587
+ if (plan.creates.length > 0) {
588
+ body.create = plan.creates.map((c) => ({
589
+ filePath: normalizePath(c.file.relPath),
590
+ content: c.content
591
+ }));
592
+ }
593
+ if (plan.updates.length > 0) {
594
+ body.update = plan.updates.map((p) => ({
595
+ id: p.id,
596
+ content: p.content,
597
+ filePath: normalizePath(p.file.relPath)
598
+ }));
599
+ }
600
+ if (plan.deletes.length > 0) {
601
+ body.delete = plan.deletes.map((d) => d.id);
602
+ }
603
+ return Object.keys(body).length > 0 ? body : null;
609
604
  }
610
605
  async function uploadDirectory(client, dir, options = {}) {
611
606
  if (client.mock) return mockUpload(dir, options);
612
- const ignore = options.ignore ?? (() => false);
613
- const files = await collectFiles(dir, ignore);
607
+ const ignore = combineUploadIgnore(options.ignore);
608
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
614
609
  const total = files.reduce((sum, f) => sum + f.size, 0);
615
610
  options.onProgress?.({ phase: "collect", current: total, total });
616
- const appId = client.appId;
617
- const wsConfig = normalizeWorkspaceConfig(await client.request(ENDPOINTS.workspaceConfig(appId), { method: "GET" }));
618
- assertUploadable(files, wsConfig);
619
- const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
620
- const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
621
- const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
622
- const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
623
- if (toDelete.length > 0) {
624
- await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
625
- method: "DELETE",
626
- body: JSON.stringify({ ids: toDelete })
627
- });
628
- }
629
- const creates = [];
630
- const patches = [];
631
- const unchanged = [];
611
+ const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
612
+ const local = [];
632
613
  for (const file of files) {
633
- const content = await fs3.readFile(file.absPath, "utf8");
634
- const remoteFile = remoteByPath.get(normalizePath(file.relPath));
635
- if (!remoteFile) creates.push({ file, content });
636
- else if (remoteFile.content !== content) patches.push({ file, id: remoteFile.id, content });
637
- else unchanged.push(file);
614
+ local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
615
+ }
616
+ const plan = planWorkspaceUpload(local, remote);
617
+ const counts = countsFromPlan(plan);
618
+ const pushBody = buildWorkspacePushBody(plan);
619
+ if (pushBody) {
620
+ await client.request(ENDPOINTS.workspaceCodePush(), {
621
+ method: "POST",
622
+ body: JSON.stringify(pushBody)
623
+ });
638
624
  }
639
625
  let sent = 0;
640
626
  const bump = (file) => {
641
627
  sent += file.size;
642
628
  options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
643
629
  };
644
- if (patches.length > 0) {
645
- await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
646
- method: "PATCH",
647
- body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
648
- });
649
- for (const p of patches) bump(p.file);
650
- }
651
- for (const c of creates) {
652
- await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
653
- method: "POST",
654
- body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
655
- });
656
- bump(c.file);
657
- }
658
- for (const u of unchanged) bump(u);
630
+ for (const c of plan.creates) bump(c.file);
631
+ for (const p of plan.updates) bump(p.file);
632
+ for (const u of plan.unchanged) bump(u);
659
633
  if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
660
- return { fileCount: files.length, byteCount: total };
634
+ return { fileCount: files.length, byteCount: total, ...counts };
661
635
  }
662
636
 
663
637
  // src/workspace.ts
@@ -705,42 +679,20 @@ export default class App extends TwinApp {
705
679
  }
706
680
  }
707
681
  `;
708
- var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
709
- ".git",
710
- "node_modules",
711
- "dist",
712
- ".easytwin",
713
- ".cursor",
714
- ".claude",
715
- ".qoder",
716
- ".vscode"
717
- ]);
718
- var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
719
- var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
720
682
  function defaultPullIgnore(relPath) {
721
- const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
722
- if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
723
- const base = parts[parts.length - 1];
724
- return base !== void 0 && PULL_IGNORED_FILES.has(base);
683
+ return defaultWorkspaceIgnore(relPath);
725
684
  }
726
685
  function isSafeRelPath(relPath) {
727
686
  const n = normalizePath(relPath);
728
687
  if (!n) return false;
729
688
  if (n.toLowerCase() === "easytwin.config.json") return false;
689
+ const base = n.split("/").pop() ?? "";
690
+ if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
730
691
  if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
731
692
  const parts = n.split("/");
732
693
  if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
733
694
  return true;
734
695
  }
735
- function extensionOf2(relPath) {
736
- const base = relPath.split("/").pop() ?? "";
737
- const i = base.lastIndexOf(".");
738
- if (i <= 0) return "";
739
- return base.slice(i).toLowerCase();
740
- }
741
- function allowedExtensionSet2(list) {
742
- return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
743
- }
744
696
  function normalizeEol(text) {
745
697
  return text.replace(/\r\n/g, "\n");
746
698
  }
@@ -756,30 +708,18 @@ async function readLocalText(absPath) {
756
708
  throw err;
757
709
  }
758
710
  }
759
- async function loadAllowedExtensions(client) {
760
- if (client.mock) return DEFAULT_PULL_EXTENSIONS;
761
- try {
762
- const cfg = normalizeWorkspaceConfig(
763
- await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
764
- );
765
- return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
766
- } catch {
767
- return DEFAULT_PULL_EXTENSIONS;
768
- }
769
- }
770
711
  async function fetchRemoteFiles(client) {
771
712
  if (client.mock) return [];
772
- return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(client.appId), { method: "GET" }));
713
+ return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
773
714
  }
774
715
  async function planWorkspacePull(client, dir, options = {}) {
775
716
  const ignore = combineIgnore(options.ignore);
776
- const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
777
717
  const remoteRaw = await fetchRemoteFiles(client);
778
718
  const skippedRemotePaths = [];
779
719
  const remoteByPath = /* @__PURE__ */ new Map();
780
720
  for (const file of remoteRaw) {
781
721
  const rel = normalizePath(file.filePath);
782
- if (!isSafeRelPath(rel) || ignore(rel)) {
722
+ if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
783
723
  skippedRemotePaths.push(rel || file.filePath);
784
724
  continue;
785
725
  }
@@ -795,7 +735,7 @@ async function planWorkspacePull(client, dir, options = {}) {
795
735
  const localByPath = /* @__PURE__ */ new Map();
796
736
  for (const file of localFiles) {
797
737
  const rel = normalizePath(file.relPath);
798
- if (!allowed.has(extensionOf2(rel))) continue;
738
+ if (!isWorkspaceCodeFile(rel)) continue;
799
739
  const content = await readLocalText(file.absPath);
800
740
  if (content !== void 0) localByPath.set(rel, content);
801
741
  }
@@ -920,7 +860,8 @@ var SKILL_NAMES = [
920
860
  "easytwin-develop",
921
861
  "easytwin-bootstrap",
922
862
  "easytwin-scene",
923
- "easytwin-upload"
863
+ "easytwin-upload",
864
+ "easytwin-test"
924
865
  ];
925
866
  var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
926
867
  var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
@@ -1111,6 +1052,7 @@ function buildCodexSegment(version) {
1111
1052
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
1112
1053
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
1113
1054
  "- `easytwin-upload`:\u62C9\u53D6/\u4E0A\u4F20\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5199\u9ED8\u8BA4\u5165\u53E3;\u4E0A\u4F20\u5168\u91CF\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
1055
+ "- `easytwin-test`:\u5199\u5DE5\u4F5C\u533A `*.spec.ts`,\u9884\u89C8\u9875\u6309\u94AE/\u8F93\u5165\u6846\u8C03\u7528\u5BFC\u51FA\u51FD\u6570(\u4E0D\u540C\u6B65;CLI `easytwin preview` \u6216\u63D2\u4EF6)\u3002",
1114
1056
  "",
1115
1057
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
1116
1058
  CODEX_MARKER_END
@@ -1281,14 +1223,14 @@ function collectBundledRuntimeExports(code) {
1281
1223
  }
1282
1224
  return names;
1283
1225
  }
1284
- async function bundleUserCode(options) {
1226
+ async function bundleWorkspaceModule(options) {
1285
1227
  const cwd = path7.resolve(options.cwd);
1286
- const entry = path7.join(cwd, USER_ENTRY);
1228
+ const entry = path7.isAbsolute(options.entry) ? options.entry : path7.join(cwd, options.entry);
1287
1229
  try {
1288
1230
  await fs6.access(entry);
1289
1231
  } catch {
1290
1232
  throw new BundleError(
1291
- `\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`
1233
+ options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
1292
1234
  );
1293
1235
  }
1294
1236
  let result;
@@ -1328,6 +1270,16 @@ async function bundleUserCode(options) {
1328
1270
  }
1329
1271
  return { code, warnings };
1330
1272
  }
1273
+ async function bundleUserCode(options) {
1274
+ const cwd = path7.resolve(options.cwd);
1275
+ const entry = path7.join(cwd, USER_ENTRY);
1276
+ return bundleWorkspaceModule({
1277
+ cwd: options.cwd,
1278
+ entry: USER_ENTRY,
1279
+ outFile: options.outFile,
1280
+ missingEntryMessage: `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
1281
+ });
1282
+ }
1331
1283
  async function typesMissingHint(cwd) {
1332
1284
  try {
1333
1285
  await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
@@ -1336,6 +1288,1140 @@ async function typesMissingHint(cwd) {
1336
1288
  return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
1337
1289
  }
1338
1290
  }
1291
+ var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
1292
+ var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1293
+ function decodeVLQValues(str) {
1294
+ const values = [];
1295
+ let i = 0;
1296
+ while (i < str.length) {
1297
+ let result = 0;
1298
+ let shift = 0;
1299
+ let continuation = true;
1300
+ while (continuation) {
1301
+ if (i >= str.length) return values;
1302
+ const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
1303
+ if (digit < 0) return values;
1304
+ continuation = (digit & 32) !== 0;
1305
+ result += (digit & 31) << shift;
1306
+ shift += 5;
1307
+ }
1308
+ values.push(result & 1 ? -(result >> 1) : result >> 1);
1309
+ }
1310
+ return values;
1311
+ }
1312
+ function decodeMappings(map) {
1313
+ const sources = map.sources ?? [];
1314
+ const lines = (map.mappings ?? "").split(";");
1315
+ let sourceIndex = 0;
1316
+ let originalLine = 0;
1317
+ let originalColumn = 0;
1318
+ const decoded = [];
1319
+ for (const line of lines) {
1320
+ let generatedColumn = 0;
1321
+ const segs = [];
1322
+ if (line) {
1323
+ for (const raw of line.split(",")) {
1324
+ if (!raw) continue;
1325
+ const nums = decodeVLQValues(raw);
1326
+ if (nums[0] === void 0) continue;
1327
+ generatedColumn += nums[0];
1328
+ if (nums.length >= 4) {
1329
+ sourceIndex += nums[1] ?? 0;
1330
+ originalLine += nums[2] ?? 0;
1331
+ originalColumn += nums[3] ?? 0;
1332
+ segs.push({
1333
+ generatedColumn,
1334
+ source: sources[sourceIndex] ?? USER_ENTRY,
1335
+ originalLine,
1336
+ originalColumn
1337
+ });
1338
+ }
1339
+ }
1340
+ }
1341
+ decoded.push(segs);
1342
+ }
1343
+ return decoded;
1344
+ }
1345
+ function originalPositionFor(map, line, column) {
1346
+ const decoded = decodeMappings(map);
1347
+ for (let i = line - 1; i >= 0; i--) {
1348
+ const segs = decoded[i];
1349
+ if (!segs || segs.length === 0) continue;
1350
+ const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
1351
+ let best = segs[0];
1352
+ for (const seg of segs) {
1353
+ if (seg.generatedColumn <= col) best = seg;
1354
+ else break;
1355
+ }
1356
+ if (!best) continue;
1357
+ return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
1358
+ }
1359
+ return void 0;
1360
+ }
1361
+ function extractInlineSourceMap(code) {
1362
+ const m = code.match(SOURCEMAP_RE);
1363
+ if (!m?.[1]) return void 0;
1364
+ try {
1365
+ return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
1366
+ } catch {
1367
+ return void 0;
1368
+ }
1369
+ }
1370
+ function remapErrorStack(stack, bundledCode) {
1371
+ const map = extractInlineSourceMap(bundledCode);
1372
+ if (!map?.mappings) return stack;
1373
+ return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
1374
+ const orig = originalPositionFor(map, Number(line), Number(col));
1375
+ if (!orig) return full;
1376
+ return `${orig.source}:${orig.line}:${orig.column}`;
1377
+ });
1378
+ }
1379
+
1380
+ // src/previewServer.ts
1381
+ import { existsSync as existsSync2 } from "fs";
1382
+ import { promises as fs8 } from "fs";
1383
+ import http2 from "http";
1384
+ import path9 from "path";
1385
+ import { fileURLToPath as fileURLToPath3 } from "url";
1386
+
1387
+ // src/previewHtml.ts
1388
+ function escapeJsonForScript(json) {
1389
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1390
+ }
1391
+ var PREVIEW_STYLE = `
1392
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
1393
+ body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
1394
+ #stage { position: relative; flex: 1; min-height: 0; }
1395
+ #twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
1396
+ #twin-root canvas { display: block; }
1397
+ #status {
1398
+ position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
1399
+ padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
1400
+ }
1401
+ #status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
1402
+ /* \u4F5C\u8005\u6837\u5F0F display:flex \u4F1A\u8986\u76D6 UA \u7684 [hidden]{display:none},\u5FC5\u987B\u663E\u5F0F\u58F0\u660E\u624D\u80FD\u9690\u85CF\u52A0\u8F7D\u6587\u6848 */
1403
+ #status[hidden] { display: none; }
1404
+ #mock-banner {
1405
+ position: absolute; top: 0; left: 0; right: 0; z-index: 2;
1406
+ padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
1407
+ border-bottom: 1px solid #f0c36d; pointer-events: none;
1408
+ }
1409
+ #debug-log {
1410
+ display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
1411
+ max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
1412
+ background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
1413
+ white-space: pre-wrap; word-break: break-all; pointer-events: auto;
1414
+ }
1415
+ #debug-log.open { display: block; }
1416
+ #run-bar {
1417
+ flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
1418
+ padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
1419
+ }
1420
+ #run-bar button {
1421
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
1422
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
1423
+ }
1424
+ #run-bar button:disabled { opacity: .55; cursor: default; }
1425
+ #btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
1426
+ #run-status { color: #bbb; min-width: 4em; }
1427
+ #run-bar .spacer { flex: 1; }
1428
+ #test-panel {
1429
+ flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
1430
+ padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
1431
+ max-height: 30%; overflow: auto;
1432
+ }
1433
+ #test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
1434
+ #test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
1435
+ #test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
1436
+ #test-panel .test-empty { color: #777; }
1437
+ #test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
1438
+ #test-panel input.test-input {
1439
+ width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
1440
+ padding: 3px 6px; border-radius: 4px; font: 12px inherit;
1441
+ }
1442
+ #test-panel button {
1443
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
1444
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
1445
+ }
1446
+ #test-panel button:disabled { opacity: .55; cursor: default; }
1447
+ `;
1448
+ var RENDER_SCRIPT = `
1449
+ var statusEl = document.getElementById("status");
1450
+ var logEl = document.getElementById("debug-log");
1451
+ var engine = null;
1452
+ var runtime = null;
1453
+ var sceneJson = null;
1454
+ var host = null;
1455
+ var running = false;
1456
+ var runningTest = false;
1457
+ var testsReady = false;
1458
+ var hostKind = __HOST_KIND__;
1459
+ var vscodeApi = null;
1460
+ if (hostKind === "vscode") {
1461
+ try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
1462
+ }
1463
+ var pending = {};
1464
+ var seq = 0;
1465
+ var origFetch = window.fetch.bind(window);
1466
+ function now() { return new Date().toISOString().slice(11, 23); }
1467
+ function log(line) {
1468
+ var text = "[" + now() + "] " + line;
1469
+ if (logEl) {
1470
+ logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
1471
+ logEl.scrollTop = logEl.scrollHeight;
1472
+ }
1473
+ if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
1474
+ console.log("[EasyTwin]", line);
1475
+ }
1476
+ function nextId() { return String(++seq); }
1477
+ function handleHostMessage(msg) {
1478
+ if (!msg) return;
1479
+ if (msg.type === "proxy-fetch-result") {
1480
+ var p = pending[msg.id];
1481
+ if (!p) return;
1482
+ delete pending[msg.id];
1483
+ if (msg.error) p.reject(new Error(msg.error));
1484
+ else p.resolve(msg);
1485
+ return;
1486
+ }
1487
+ if (msg.type === "bundle-result") {
1488
+ if (!msg.ok) {
1489
+ log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
1490
+ setRunStatus("\u7F16\u8BD1\u5931\u8D25");
1491
+ setRunBusy(false);
1492
+ if (logEl) logEl.classList.add("open");
1493
+ return;
1494
+ }
1495
+ for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
1496
+ runUserCode(msg.code);
1497
+ return;
1498
+ }
1499
+ if (msg.type === "read-asset-result") {
1500
+ var ap = pending[msg.id];
1501
+ if (!ap) return;
1502
+ delete pending[msg.id];
1503
+ if (msg.error) ap.reject(new Error(msg.error));
1504
+ else ap.resolve(msg.text);
1505
+ return;
1506
+ }
1507
+ if (msg.type === "run-error-mapped") {
1508
+ log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
1509
+ return;
1510
+ }
1511
+ if (msg.type === "tests") {
1512
+ renderTests(msg.tests || []);
1513
+ return;
1514
+ }
1515
+ if (msg.type === "test-bundle") {
1516
+ if (!msg.ok) {
1517
+ log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
1518
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
1519
+ runningTest = false;
1520
+ setRunBusy(running);
1521
+ if (logEl) logEl.classList.add("open");
1522
+ return;
1523
+ }
1524
+ for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
1525
+ runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
1526
+ log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
1527
+ setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
1528
+ }).catch(function (err) {
1529
+ log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
1530
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
1531
+ if (logEl) logEl.classList.add("open");
1532
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
1533
+ }).then(function () {
1534
+ runningTest = false;
1535
+ setRunBusy(running);
1536
+ });
1537
+ }
1538
+ }
1539
+ function httpJson(url, init) {
1540
+ return origFetch(url, init).then(function (res) {
1541
+ return res.json().then(function (body) {
1542
+ if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
1543
+ return body;
1544
+ });
1545
+ });
1546
+ }
1547
+ function postToHost(msg) {
1548
+ if (vscodeApi) {
1549
+ vscodeApi.postMessage(msg);
1550
+ return;
1551
+ }
1552
+ if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
1553
+ if (msg.type === "run") {
1554
+ httpJson("/api/bundle", { method: "POST" }).then(function (body) {
1555
+ handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
1556
+ }).catch(function (err) {
1557
+ handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
1558
+ });
1559
+ return;
1560
+ }
1561
+ if (msg.type === "run-error") {
1562
+ httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
1563
+ handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
1564
+ }).catch(function (err) {
1565
+ log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
1566
+ });
1567
+ return;
1568
+ }
1569
+ if (msg.type === "read-asset") {
1570
+ httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
1571
+ handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
1572
+ }).catch(function (err) {
1573
+ handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
1574
+ });
1575
+ return;
1576
+ }
1577
+ if (msg.type === "list-tests") {
1578
+ httpJson("/api/tests").then(function (body) {
1579
+ handleHostMessage({ type: "tests", tests: body.tests || [] });
1580
+ }).catch(function (err) {
1581
+ log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
1582
+ });
1583
+ return;
1584
+ }
1585
+ if (msg.type === "run-test") {
1586
+ httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
1587
+ handleHostMessage(Object.assign({ type: "test-bundle" }, body));
1588
+ }).catch(function (err) {
1589
+ handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
1590
+ });
1591
+ }
1592
+ }
1593
+ function showStatus(text, isError) {
1594
+ if (!statusEl) return;
1595
+ statusEl.textContent = text;
1596
+ statusEl.className = isError ? "error" : "";
1597
+ statusEl.hidden = false;
1598
+ if (isError && logEl) logEl.classList.add("open");
1599
+ }
1600
+ function hideStatus() { if (statusEl) statusEl.hidden = true; }
1601
+ function formatError(err) {
1602
+ var msg = err && err.message ? err.message : String(err);
1603
+ var stack = err && err.stack ? "\\n" + err.stack : "";
1604
+ return msg + stack;
1605
+ }
1606
+ window.addEventListener("message", function (ev) {
1607
+ handleHostMessage(ev.data);
1608
+ });
1609
+ function b64ToBuf(b64) {
1610
+ var bin = atob(b64);
1611
+ var bytes = new Uint8Array(bin.length);
1612
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1613
+ return bytes.buffer;
1614
+ }
1615
+ function proxyFetch(url, method) {
1616
+ return new Promise(function (resolve, reject) {
1617
+ if (!vscodeApi) {
1618
+ reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
1619
+ return;
1620
+ }
1621
+ var id = nextId();
1622
+ pending[id] = { resolve: resolve, reject: reject };
1623
+ vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
1624
+ }).then(function (msg) {
1625
+ var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
1626
+ return new Response(buf, {
1627
+ status: msg.status || 0,
1628
+ statusText: msg.statusText || "",
1629
+ headers: msg.headers || {}
1630
+ });
1631
+ });
1632
+ }
1633
+ function isHttpUrl(url) {
1634
+ return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
1635
+ }
1636
+ function isPlainHttp(url) {
1637
+ return typeof url === "string" && url.indexOf("http://") === 0;
1638
+ }
1639
+ if (hostKind === "vscode") {
1640
+ window.fetch = function (input, init) {
1641
+ var url = typeof input === "string" ? input : (input && input.url);
1642
+ log("fetch " + url);
1643
+ if (isPlainHttp(url)) {
1644
+ return proxyFetch(url, init && init.method).then(function (res) {
1645
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
1646
+ return res;
1647
+ });
1648
+ }
1649
+ return origFetch(input, init).catch(function (err) {
1650
+ log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
1651
+ if (!isHttpUrl(url)) throw err;
1652
+ return proxyFetch(url, init && init.method).then(function (res) {
1653
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
1654
+ return res;
1655
+ });
1656
+ });
1657
+ };
1658
+ var origOpen = XMLHttpRequest.prototype.open;
1659
+ var origSend = XMLHttpRequest.prototype.send;
1660
+ XMLHttpRequest.prototype.open = function (method, url) {
1661
+ this.__etMethod = method;
1662
+ this.__etUrl = String(url);
1663
+ return origOpen.apply(this, arguments);
1664
+ };
1665
+ XMLHttpRequest.prototype.send = function (body) {
1666
+ var xhr = this;
1667
+ var url = xhr.__etUrl;
1668
+ if (!isPlainHttp(url)) return origSend.call(this, body);
1669
+ log("xhr proxy " + xhr.__etMethod + " " + url);
1670
+ proxyFetch(url, xhr.__etMethod).then(function (res) {
1671
+ return res.arrayBuffer().then(function (buf) {
1672
+ var text = "";
1673
+ try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
1674
+ Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
1675
+ Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
1676
+ Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
1677
+ Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
1678
+ var rt = xhr.responseType;
1679
+ var response = buf;
1680
+ if (rt === "" || rt === "text") response = text;
1681
+ else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
1682
+ Object.defineProperty(xhr, "response", { configurable: true, value: response });
1683
+ Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
1684
+ if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
1685
+ xhr.dispatchEvent(new Event("load"));
1686
+ xhr.dispatchEvent(new Event("loadend"));
1687
+ });
1688
+ }).catch(function (err) {
1689
+ log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
1690
+ if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
1691
+ xhr.dispatchEvent(new Event("error"));
1692
+ xhr.dispatchEvent(new Event("loadend"));
1693
+ });
1694
+ };
1695
+ function patchHttpSrc(proto, prop) {
1696
+ var desc = Object.getOwnPropertyDescriptor(proto, prop);
1697
+ if (!desc || typeof desc.set !== "function") return;
1698
+ Object.defineProperty(proto, prop, {
1699
+ configurable: true,
1700
+ enumerable: desc.enumerable,
1701
+ get: function () { return desc.get.call(this); },
1702
+ set: function (value) {
1703
+ var el = this;
1704
+ var url = String(value);
1705
+ if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
1706
+ log("media proxy " + url);
1707
+ proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
1708
+ desc.set.call(el, URL.createObjectURL(blob));
1709
+ }).catch(function (err) {
1710
+ log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
1711
+ try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
1712
+ });
1713
+ }
1714
+ });
1715
+ }
1716
+ patchHttpSrc(HTMLImageElement.prototype, "src");
1717
+ patchHttpSrc(HTMLMediaElement.prototype, "src");
1718
+ }
1719
+ document.getElementById("btn-debug").addEventListener("click", function () {
1720
+ if (logEl) logEl.classList.toggle("open");
1721
+ });
1722
+ var btnDevtools = document.getElementById("btn-devtools");
1723
+ var btnOutput = document.getElementById("btn-output");
1724
+ if (hostKind === "http") {
1725
+ if (btnDevtools) btnDevtools.hidden = true;
1726
+ if (btnOutput) btnOutput.hidden = true;
1727
+ }
1728
+ if (btnDevtools) btnDevtools.addEventListener("click", function () {
1729
+ postToHost({ type: "open-devtools" });
1730
+ });
1731
+ if (btnOutput) btnOutput.addEventListener("click", function () {
1732
+ postToHost({ type: "show-output" });
1733
+ });
1734
+ function setRunStatus(text) {
1735
+ var el = document.getElementById("run-status");
1736
+ if (el) el.textContent = text;
1737
+ }
1738
+ function setRunBusy(busy) {
1739
+ running = busy;
1740
+ var btn = document.getElementById("btn-run");
1741
+ if (btn) btn.disabled = !!busy || runningTest;
1742
+ syncTestControls();
1743
+ }
1744
+ function syncTestControls() {
1745
+ var panel = document.getElementById("test-panel");
1746
+ if (!panel) return;
1747
+ var disabled = !testsReady || running || runningTest;
1748
+ var nodes = panel.querySelectorAll("button.test-run, input.test-input");
1749
+ for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
1750
+ }
1751
+ function renderTests(tests) {
1752
+ var list = document.getElementById("test-list");
1753
+ if (!list) return;
1754
+ list.textContent = "";
1755
+ if (!tests || !tests.length) {
1756
+ var empty = document.createElement("span");
1757
+ empty.className = "test-empty";
1758
+ empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
1759
+ list.appendChild(empty);
1760
+ return;
1761
+ }
1762
+ var groups = {};
1763
+ var order = [];
1764
+ for (var i = 0; i < tests.length; i++) {
1765
+ var t = tests[i];
1766
+ if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
1767
+ groups[t.file].push(t);
1768
+ }
1769
+ for (var g = 0; g < order.length; g++) {
1770
+ var file = order[g];
1771
+ var heading = document.createElement("span");
1772
+ heading.className = "test-file";
1773
+ heading.textContent = file;
1774
+ list.appendChild(heading);
1775
+ var items = groups[file];
1776
+ for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
1777
+ }
1778
+ syncTestControls();
1779
+ }
1780
+ function makeTestControl(t) {
1781
+ var wrap = document.createElement("span");
1782
+ wrap.className = "test-item";
1783
+ if (t.hasInput) {
1784
+ var input = document.createElement("input");
1785
+ input.type = "text";
1786
+ input.className = "test-input";
1787
+ input.placeholder = t.inputName || "input";
1788
+ var btn = document.createElement("button");
1789
+ btn.type = "button";
1790
+ btn.className = "test-run";
1791
+ btn.textContent = t.name;
1792
+ btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
1793
+ input.addEventListener("keydown", function (ev) {
1794
+ if (ev.key === "Enter") requestRunTest(t.id, input.value);
1795
+ });
1796
+ wrap.appendChild(input);
1797
+ wrap.appendChild(btn);
1798
+ } else {
1799
+ var only = document.createElement("button");
1800
+ only.type = "button";
1801
+ only.className = "test-run";
1802
+ only.textContent = t.name;
1803
+ only.addEventListener("click", function () { requestRunTest(t.id); });
1804
+ wrap.appendChild(only);
1805
+ }
1806
+ return wrap;
1807
+ }
1808
+ function requestRunTest(id, input) {
1809
+ if (!testsReady || running || runningTest) return;
1810
+ if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
1811
+ runningTest = true;
1812
+ setRunBusy(running);
1813
+ setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
1814
+ log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
1815
+ postToHost({ type: "run-test", id: id, input: input });
1816
+ }
1817
+ async function runExportedTest(code, exportName, input, hasInput) {
1818
+ var blob = new Blob([code], { type: "text/javascript" });
1819
+ var url = URL.createObjectURL(blob);
1820
+ try {
1821
+ var mod = await import(url);
1822
+ var fn = mod[exportName];
1823
+ if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
1824
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
1825
+ var ctx = host.getTestContext();
1826
+ var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
1827
+ await Promise.resolve(result);
1828
+ } finally {
1829
+ URL.revokeObjectURL(url);
1830
+ }
1831
+ }
1832
+ function applyRenderPatch() {
1833
+ // runtime \u6E32\u67D3\u5206\u652F:\u7248\u672C <= 0.0.31 \u8D70\u666E\u901A/\u540E\u5904\u7406\u6E32\u67D3,\u7248\u672C > 0.0.31 \u8981\u6C42\u5B58\u5728 postprocessingComponent,
1834
+ // \u5426\u5219\u6240\u6709\u5206\u652F\u90FD\u88AB\u8DF3\u8FC7(renderer.info.render.frame \u6052\u4E3A 0)\u5BFC\u81F4\u9ED1\u5C4F\u3002\u6B64\u5904\u6309 runtime \u8BED\u4E49\u515C\u5E95\u3002
1835
+ var scene = engine && engine.mainScene;
1836
+ if (
1837
+ scene && scene.rootComponent && scene.rootComponent.version &&
1838
+ !scene.postprocessingComponent &&
1839
+ runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
1840
+ ) {
1841
+ log("\u8865\u6E32\u67D3:\u573A\u666F\u7248\u672C " + scene.rootComponent.version + " \u65E0\u540E\u5904\u7406\u7EC4\u4EF6,runtime \u9AD8\u7248\u672C\u6E32\u67D3\u5206\u652F\u4E3A\u7A7A,\u6CE8\u518C\u6BCF\u5E27\u6E32\u67D3\u56DE\u8C03");
1842
+ scene.registerRenderCallback(function () {
1843
+ scene.renderer.render(scene.sceneObject, scene.camera.main);
1844
+ });
1845
+ }
1846
+ }
1847
+ async function loadHostScene(nextEngine, nextJson) {
1848
+ engine = nextEngine;
1849
+ var sceneManager = engine.getManager(runtime.SceneManager);
1850
+ await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
1851
+ applyRenderPatch();
1852
+ }
1853
+ function readAsset(relPath) {
1854
+ return new Promise(function (resolve, reject) {
1855
+ var id = nextId();
1856
+ pending[id] = { resolve: resolve, reject: reject };
1857
+ postToHost({ type: "read-asset", id: id, path: relPath });
1858
+ });
1859
+ }
1860
+ async function runUserCode(code) {
1861
+ setRunStatus("\u8FD0\u884C\u4E2D\u2026");
1862
+ try {
1863
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
1864
+ await host.run(code);
1865
+ log("Run \u5B8C\u6210");
1866
+ setRunStatus("\u8FD0\u884C\u4E2D");
1867
+ } catch (err) {
1868
+ log("Run \u5931\u8D25 " + formatError(err));
1869
+ setRunStatus("\u5931\u8D25");
1870
+ if (logEl) logEl.classList.add("open");
1871
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
1872
+ } finally {
1873
+ setRunBusy(false);
1874
+ }
1875
+ }
1876
+ document.getElementById("btn-run").addEventListener("click", function () {
1877
+ if (running || runningTest) return;
1878
+ if (!engine) {
1879
+ log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
1880
+ return;
1881
+ }
1882
+ setRunBusy(true);
1883
+ setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
1884
+ log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
1885
+ postToHost({ type: "run" });
1886
+ });
1887
+ document.getElementById("btn-refresh-tests").addEventListener("click", function () {
1888
+ log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
1889
+ postToHost({ type: "list-tests" });
1890
+ });
1891
+ function readEmbeddedTests() {
1892
+ var el = document.getElementById("workspace-tests");
1893
+ if (!el || !el.textContent) return [];
1894
+ try { return JSON.parse(el.textContent); } catch (e) { return []; }
1895
+ }
1896
+ renderTests(readEmbeddedTests());
1897
+ function normalizeHierarchyConfig(vo) {
1898
+ var objs = vo && vo.objs ? vo.objs : [];
1899
+ for (var i = 0; i < objs.length; i++) {
1900
+ var hc = objs[i].hierarchyConfig || {};
1901
+ if (typeof hc.active !== "boolean") {
1902
+ hc.active = hc.inActive === true ? false : hc.visible !== false;
1903
+ }
1904
+ if (typeof hc.lock !== "boolean") hc.lock = false;
1905
+ if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
1906
+ objs[i].hierarchyConfig = hc;
1907
+ }
1908
+ return vo;
1909
+ }
1910
+ function toSceneJson(runtime, raw) {
1911
+ var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
1912
+ if (vo && vo.sceneComponent) {
1913
+ return {
1914
+ id: vo.id || "preview",
1915
+ name: vo.name || "\u573A\u666F\u9884\u89C8",
1916
+ sceneComponent: vo.sceneComponent
1917
+ };
1918
+ }
1919
+ vo = normalizeHierarchyConfig(vo);
1920
+ var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
1921
+ var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
1922
+ return {
1923
+ id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
1924
+ name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
1925
+ sceneComponent: runtime.convertObjToComponentJson(vo)
1926
+ };
1927
+ }
1928
+ try {
1929
+ showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
1930
+ log("runtimeUri=" + __RUNTIME_URI__);
1931
+ log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
1932
+ log("1/4 import twin-runtime / TwinApp host");
1933
+ runtime = await import("@easytwin/runtime");
1934
+ var hostMod = await import(__HOST_URI__);
1935
+ log("2/4 parse scene JSON");
1936
+ var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
1937
+ sceneJson = toSceneJson(runtime, sceneVo);
1938
+ log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
1939
+ log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
1940
+ host = new hostMod.TwinAppPreviewHost({
1941
+ runtime: runtime,
1942
+ containerId: "twin-root",
1943
+ ossUrl: __BASE_OSS_URL__,
1944
+ appId: __APP_ID__,
1945
+ sceneId: sceneJson.id,
1946
+ sceneJson: sceneJson,
1947
+ customComponentDeps: {
1948
+ "@easytwin/runtime": runtime,
1949
+ "@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
1950
+ react: { createElement: function () { return null; }, Fragment: "div" }
1951
+ },
1952
+ loadScene: loadHostScene,
1953
+ readAsset: readAsset,
1954
+ log: log
1955
+ });
1956
+ await host.boot();
1957
+ engine = host.getEngine();
1958
+ log("4/4 loadScene");
1959
+ log("\u5B8C\u6210");
1960
+ hideStatus();
1961
+ testsReady = true;
1962
+ setRunBusy(false);
1963
+ } catch (err) {
1964
+ log("\u5931\u8D25 " + formatError(err));
1965
+ showStatus("\u573A\u666F\u6E32\u67D3\u5931\u8D25: " + (err && err.message ? err.message : String(err)) + "\\n(\u70B9\u53F3\u4E0B\u89D2\u300C\u8C03\u8BD5\u65E5\u5FD7\u300D\u67E5\u770B\u6B65\u9AA4\u4E0E\u8BF7\u6C42 URL;\u300C\u5F00\u53D1\u8005\u5DE5\u5177\u300D\u6253\u5F00 webview DevTools)", true);
1966
+ }
1967
+ window.addEventListener("pagehide", function () {
1968
+ if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
1969
+ });
1970
+ `;
1971
+ function originOf(url) {
1972
+ return new URL(url).origin;
1973
+ }
1974
+ function buildPreviewHtml(options) {
1975
+ const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
1976
+ const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
1977
+ const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
1978
+ const appId = options.appId ?? "preview";
1979
+ const hostKind = options.host ?? "vscode";
1980
+ const apiOrigin = originOf(baseUrl);
1981
+ const ossOrigin = originOf(ossUrl);
1982
+ const runtimeOrigin = originOf(runtimeUri);
1983
+ const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
1984
+ const renderScript = RENDER_SCRIPT.replaceAll("__RUNTIME_URI__", JSON.stringify(runtimeUri)).replaceAll("__HOST_URI__", JSON.stringify(hostUri)).replaceAll("__BASE_OSS_URL__", JSON.stringify(ossUrl)).replaceAll("__APP_ID__", JSON.stringify(appId)).replaceAll("__HOST_KIND__", JSON.stringify(hostKind));
1985
+ const importMap = JSON.stringify({
1986
+ imports: {
1987
+ "@easytwin/runtime": runtimeUri,
1988
+ "@easytwin/apps": appsUri
1989
+ }
1990
+ });
1991
+ const testsJson = JSON.stringify(options.tests ?? []);
1992
+ const mockBanner = mock ? `<div id="mock-banner"><b>\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F</b>:\u573A\u666F\u6570\u636E\u6765\u81EA\u672C\u5730 <code>scene.example.json</code>,\u672A\u8BF7\u6C42\u573A\u666F\u63A5\u53E3;\u4E09\u7EF4\u9884\u89C8\u8D70\u6253\u5305\u7684 twin runtime,\u7CFB\u7EDF\u5E93\u6309\u5B98\u65B9 OSS \u5728\u7EBF\u52A0\u8F7D\u3002</div>` : "";
1993
+ return `<!DOCTYPE html>
1994
+ <html lang="zh-CN">
1995
+ <head>
1996
+ <meta charset="UTF-8">
1997
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc}; worker-src blob: data: 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc}; child-src blob: data:; img-src ${apiOrigin} ${ossOrigin} ${resourceSrc} https: http: data: blob:; media-src ${apiOrigin} ${ossOrigin} https: http: data: blob:; connect-src 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc} https: http: data: blob:; font-src ${apiOrigin} ${ossOrigin} https: data:;">
1998
+ <title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
1999
+ <style>${PREVIEW_STYLE}</style>
2000
+ <script type="importmap">${importMap}</script>
2001
+ </head>
2002
+ <body>
2003
+ <div id="stage">
2004
+ <div id="twin-root"></div>
2005
+ ${mockBanner}
2006
+ <div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
2007
+ <pre id="debug-log"></pre>
2008
+ </div>
2009
+ <div id="test-panel">
2010
+ <span class="test-label">\u6D4B\u8BD5</span>
2011
+ <div id="test-list"></div>
2012
+ <button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
2013
+ </div>
2014
+ <div id="run-bar">
2015
+ <button type="button" id="btn-run">Run</button>
2016
+ <span id="run-status">\u5C31\u7EEA</span>
2017
+ <span class="spacer"></span>
2018
+ <button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
2019
+ <button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
2020
+ <button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
2021
+ </div>
2022
+ <script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
2023
+ <script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
2024
+ <script type="module">
2025
+ ${renderScript}
2026
+ </script>
2027
+ </body>
2028
+ </html>`;
2029
+ }
2030
+
2031
+ // src/workspaceTests.ts
2032
+ import { promises as fs7 } from "fs";
2033
+ import path8 from "path";
2034
+ var IDENT = "[A-Za-z_$][\\w$]*";
2035
+ var FUNCTION_EXPORT = new RegExp(
2036
+ `export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
2037
+ "g"
2038
+ );
2039
+ var CONST_EXPORT = new RegExp(
2040
+ `export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
2041
+ "g"
2042
+ );
2043
+ function stripTsCommentsAndStringsKeepCode(source) {
2044
+ let out = "";
2045
+ let i = 0;
2046
+ const n = source.length;
2047
+ while (i < n) {
2048
+ const c = source[i];
2049
+ const next = source[i + 1];
2050
+ if (c === "/" && next === "/") {
2051
+ i += 2;
2052
+ while (i < n && source[i] !== "\n") i++;
2053
+ continue;
2054
+ }
2055
+ if (c === "/" && next === "*") {
2056
+ i += 2;
2057
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
2058
+ i = Math.min(n, i + 2);
2059
+ out += " ";
2060
+ continue;
2061
+ }
2062
+ if (c === "'" || c === '"' || c === "`") {
2063
+ const quote = c;
2064
+ out += " ";
2065
+ i++;
2066
+ while (i < n) {
2067
+ const ch = source[i];
2068
+ if (ch === "\\") {
2069
+ i += 2;
2070
+ continue;
2071
+ }
2072
+ if (ch === quote) {
2073
+ i++;
2074
+ break;
2075
+ }
2076
+ i++;
2077
+ }
2078
+ continue;
2079
+ }
2080
+ out += c;
2081
+ i++;
2082
+ }
2083
+ return out;
2084
+ }
2085
+ function matchingClose(src, openIndex, open, close) {
2086
+ let depth = 0;
2087
+ let angle = 0;
2088
+ let brace = 0;
2089
+ for (let i = openIndex; i < src.length; i++) {
2090
+ const c = src[i];
2091
+ if (c === open) depth++;
2092
+ else if (c === close) {
2093
+ depth--;
2094
+ if (depth === 0 && angle <= 0 && brace <= 0) return i;
2095
+ } else if (c === "<") angle++;
2096
+ else if (c === ">" && angle > 0) angle--;
2097
+ else if (c === "{") brace++;
2098
+ else if (c === "}" && brace > 0) brace--;
2099
+ }
2100
+ return -1;
2101
+ }
2102
+ function splitTopLevelParams(list) {
2103
+ const params = [];
2104
+ let current = "";
2105
+ let paren = 0;
2106
+ let angle = 0;
2107
+ let brace = 0;
2108
+ let bracket = 0;
2109
+ for (const c of list) {
2110
+ if (c === "(") paren++;
2111
+ else if (c === ")") paren--;
2112
+ else if (c === "<") angle++;
2113
+ else if (c === ">" && angle > 0) angle--;
2114
+ else if (c === "{") brace++;
2115
+ else if (c === "}" && brace > 0) brace--;
2116
+ else if (c === "[") bracket++;
2117
+ else if (c === "]" && bracket > 0) bracket--;
2118
+ if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
2119
+ if (current.trim()) params.push(current.trim());
2120
+ current = "";
2121
+ continue;
2122
+ }
2123
+ current += c;
2124
+ }
2125
+ if (current.trim()) params.push(current.trim());
2126
+ return params;
2127
+ }
2128
+ function paramName(raw) {
2129
+ let s = raw.trim();
2130
+ if (!s || s === "this") return void 0;
2131
+ if (s.startsWith("{") || s.startsWith("[")) return "input";
2132
+ s = s.replace(/^\.\.\./, "");
2133
+ const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
2134
+ if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
2135
+ return token;
2136
+ }
2137
+ function collectFromPattern(source, pattern) {
2138
+ const found = [];
2139
+ pattern.lastIndex = 0;
2140
+ let match;
2141
+ while (match = pattern.exec(source)) {
2142
+ const name = match[1];
2143
+ if (!name) continue;
2144
+ const openIndex = match.index + match[0].length - 1;
2145
+ const closeIndex = matchingClose(source, openIndex, "(", ")");
2146
+ if (closeIndex < 0) continue;
2147
+ const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
2148
+ const hasInput = params.length >= 2;
2149
+ const second = hasInput ? paramName(params[1] ?? "") : void 0;
2150
+ found.push({
2151
+ id: name,
2152
+ file: "",
2153
+ name,
2154
+ hasInput,
2155
+ inputName: hasInput ? second : void 0
2156
+ });
2157
+ }
2158
+ return found;
2159
+ }
2160
+ function parseExportedTestFunctions(source) {
2161
+ const text = stripTsCommentsAndStringsKeepCode(source);
2162
+ const seen = /* @__PURE__ */ new Set();
2163
+ const out = [];
2164
+ for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
2165
+ if (seen.has(item.name)) continue;
2166
+ seen.add(item.name);
2167
+ out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
2168
+ }
2169
+ return out;
2170
+ }
2171
+ function workspaceTestId(file, name) {
2172
+ return `${normalizePath(file)}:${name}`;
2173
+ }
2174
+ async function listWorkspaceTests(cwd) {
2175
+ const root = path8.resolve(cwd);
2176
+ let files;
2177
+ try {
2178
+ files = await collectFiles(root, isIgnoredWorkspacePath);
2179
+ } catch (err) {
2180
+ const code = err.code;
2181
+ if (code === "ENOENT") return [];
2182
+ throw err;
2183
+ }
2184
+ const tests = [];
2185
+ for (const file of files) {
2186
+ if (!isWorkspaceSpecFile(file.relPath)) continue;
2187
+ const posix = normalizePath(file.relPath);
2188
+ const source = await fs7.readFile(file.absPath, "utf8");
2189
+ for (const fn of parseExportedTestFunctions(source)) {
2190
+ tests.push({
2191
+ id: workspaceTestId(posix, fn.name),
2192
+ file: posix,
2193
+ name: fn.name,
2194
+ hasInput: fn.hasInput,
2195
+ inputName: fn.inputName
2196
+ });
2197
+ }
2198
+ }
2199
+ tests.sort((a, b) => a.id.localeCompare(b.id));
2200
+ return tests;
2201
+ }
2202
+ async function bundleWorkspaceTestFile(options) {
2203
+ const file = normalizePath(options.file);
2204
+ return bundleWorkspaceModule({
2205
+ cwd: options.cwd,
2206
+ entry: file,
2207
+ missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
2208
+ });
2209
+ }
2210
+
2211
+ // src/previewServer.ts
2212
+ var DEFAULT_PREVIEW_PORT = 4173;
2213
+ var DEFAULT_PREVIEW_HOST = "127.0.0.1";
2214
+ var RUNTIME_FILES = /* @__PURE__ */ new Set(["twin-runtime.js", "twin-apps.js", "twin-app-host.js"]);
2215
+ function formatPreviewReadyMessage(info) {
2216
+ const lines = [
2217
+ `EasyTwin \u9884\u89C8: ${info.url}`,
2218
+ `\u573A\u666F: ${info.sceneId}`,
2219
+ "\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u4E0A\u8FF0\u5730\u5740\u3002\u70B9 Run \u8FD0\u884C TwinApp;\u6D4B\u8BD5\u6309\u94AE\u5728 Run \u680F\u4E0A\u65B9\u3002Ctrl+C \u505C\u6B62\u3002"
2220
+ ];
2221
+ if (info.mock) lines.unshift("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u573A\u666F\u6570\u636E\u6765\u81EA\u672C\u5730\u793A\u4F8B,\u672A\u8BF7\u6C42\u573A\u666F\u63A5\u53E3");
2222
+ return lines.join("\n");
2223
+ }
2224
+ function resolvePreviewSceneId(scenes, requested) {
2225
+ const id = requested?.trim() ?? "";
2226
+ if (id.length > 0) return id;
2227
+ const list = scenes ?? [];
2228
+ const preferred = list.find((s) => s.defaultLoading === true) ?? list[0];
2229
+ if (preferred) return preferred.id;
2230
+ throw new Error("\u672A\u6307\u5B9A\u573A\u666F\u3002\u8BF7\u4F20\u5165 Scene Key,\u6216\u5148\u8FD0\u884C easytwin scene list");
2231
+ }
2232
+ function resolvePreviewRuntimeDir() {
2233
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
2234
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u9884\u89C8 runtime:\u63D2\u4EF6/CJS \u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 runtimeDir");
2235
+ }
2236
+ const here = path9.dirname(fileURLToPath3(import.meta.url));
2237
+ const candidates = [
2238
+ path9.join(here, "runtime"),
2239
+ path9.resolve(here, "..", "dist", "runtime"),
2240
+ path9.resolve(here, "runtime")
2241
+ ];
2242
+ for (const dir of candidates) {
2243
+ if (existsSync2(path9.join(dir, "twin-runtime.js"))) return dir;
2244
+ }
2245
+ throw new Error(
2246
+ `\u672A\u627E\u5230\u9884\u89C8 runtime(twin-runtime.js)\u3002\u5DF2\u5C1D\u8BD5:${candidates.join(", ")}\u3002\u8BF7\u5148\u6784\u5EFA @easytwin/devkit\u3002`
2247
+ );
2248
+ }
2249
+ function mimeFor(file) {
2250
+ if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
2251
+ return "application/octet-stream";
2252
+ }
2253
+ function sendJson(res, body, status = 200) {
2254
+ res.statusCode = status;
2255
+ res.setHeader("content-type", "application/json; charset=utf-8");
2256
+ res.setHeader("cache-control", "no-store");
2257
+ res.end(JSON.stringify(body));
2258
+ }
2259
+ function sendText(res, body, status, contentType) {
2260
+ res.statusCode = status;
2261
+ res.setHeader("content-type", contentType);
2262
+ res.end(body);
2263
+ }
2264
+ async function readJsonBody(req) {
2265
+ const chunks = [];
2266
+ for await (const chunk of req) chunks.push(chunk);
2267
+ if (chunks.length === 0) return {};
2268
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
2269
+ if (raw.length === 0) return {};
2270
+ return JSON.parse(raw);
2271
+ }
2272
+ function asRecord3(value) {
2273
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2274
+ }
2275
+ async function startPreviewServer(options) {
2276
+ const cwd = path9.resolve(options.cwd);
2277
+ const config = await loadConfig(cwd);
2278
+ const fileConfig = await readConfigFile(cwd);
2279
+ const runtimeDir = options.runtimeDir ?? resolvePreviewRuntimeDir();
2280
+ if (!existsSync2(path9.join(runtimeDir, "twin-runtime.js"))) {
2281
+ throw new Error(`\u9884\u89C8 runtime \u76EE\u5F55\u7F3A\u5C11 twin-runtime.js:${runtimeDir}`);
2282
+ }
2283
+ let sceneId;
2284
+ try {
2285
+ sceneId = resolvePreviewSceneId(fileConfig.scenes, options.sceneId);
2286
+ } catch (err) {
2287
+ if (config.mock) {
2288
+ sceneId = deriveExampleSceneId(await loadExampleScene());
2289
+ } else {
2290
+ throw err;
2291
+ }
2292
+ }
2293
+ const client = new EasyTwinClient(config);
2294
+ const scene = await pullScene(client, sceneId);
2295
+ const tests = await listWorkspaceTests(cwd);
2296
+ const hostname = options.hostname ?? DEFAULT_PREVIEW_HOST;
2297
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
2298
+ `));
2299
+ let lastBundledCode;
2300
+ const originRef = { url: "" };
2301
+ const html = () => buildPreviewHtml({
2302
+ baseUrl: config.baseUrl,
2303
+ ossUrl: config.ossUrl,
2304
+ sceneJson: JSON.stringify(scene.payload ?? scene, null, 2),
2305
+ mock: config.mock,
2306
+ runtimeUri: `${originRef.url}/runtime/twin-runtime.js`,
2307
+ appsUri: `${originRef.url}/runtime/twin-apps.js`,
2308
+ hostUri: `${originRef.url}/runtime/twin-app-host.js`,
2309
+ appId: config.appId,
2310
+ host: "http",
2311
+ tests
2312
+ });
2313
+ const server = http2.createServer((req, res) => {
2314
+ void handle(req, res);
2315
+ });
2316
+ async function handle(req, res) {
2317
+ try {
2318
+ const url = new URL(req.url ?? "/", originRef.url || `http://${hostname}`);
2319
+ const method = req.method ?? "GET";
2320
+ if (method === "GET" && url.pathname === "/") {
2321
+ sendText(res, html(), 200, "text/html; charset=utf-8");
2322
+ return;
2323
+ }
2324
+ if (method === "GET" && url.pathname.startsWith("/runtime/")) {
2325
+ const name = path9.posix.basename(url.pathname);
2326
+ if (!RUNTIME_FILES.has(name)) {
2327
+ sendText(res, "not found", 404, "text/plain; charset=utf-8");
2328
+ return;
2329
+ }
2330
+ const file = path9.join(runtimeDir, name);
2331
+ const buf = await fs8.readFile(file);
2332
+ res.statusCode = 200;
2333
+ res.setHeader("content-type", mimeFor(name));
2334
+ res.setHeader("cache-control", "no-store");
2335
+ res.end(buf);
2336
+ return;
2337
+ }
2338
+ if (method === "POST" && url.pathname === "/api/bundle") {
2339
+ try {
2340
+ const result = await bundleUserCode({ cwd });
2341
+ lastBundledCode = result.code;
2342
+ for (const warning of result.warnings) log(`[run] \u8B66\u544A ${warning}`);
2343
+ sendJson(res, { ok: true, code: result.code, warnings: result.warnings });
2344
+ } catch (err) {
2345
+ const error = err instanceof Error ? err.message : String(err);
2346
+ log(`[run] \u7F16\u8BD1\u5931\u8D25 ${error}`);
2347
+ sendJson(res, { ok: false, error });
2348
+ }
2349
+ return;
2350
+ }
2351
+ if (method === "GET" && url.pathname === "/api/tests") {
2352
+ sendJson(res, { tests: await listWorkspaceTests(cwd) });
2353
+ return;
2354
+ }
2355
+ if (method === "POST" && url.pathname === "/api/test-bundle") {
2356
+ const body = asRecord3(await readJsonBody(req));
2357
+ const id = typeof body.id === "string" ? body.id : "";
2358
+ const input = typeof body.input === "string" ? body.input : void 0;
2359
+ try {
2360
+ const listed = await listWorkspaceTests(cwd);
2361
+ const item = listed.find((t) => t.id === id);
2362
+ if (!item) throw new Error(`\u672A\u627E\u5230\u6D4B\u8BD5 ${id}`);
2363
+ log(`[test] bundle ${item.file} :: ${item.name}`);
2364
+ const result = await bundleWorkspaceTestFile({ cwd, file: item.file });
2365
+ lastBundledCode = result.code;
2366
+ for (const warning of result.warnings) log(`[test] \u8B66\u544A ${warning}`);
2367
+ sendJson(res, {
2368
+ ok: true,
2369
+ code: result.code,
2370
+ exportName: item.name,
2371
+ input,
2372
+ hasInput: item.hasInput,
2373
+ warnings: result.warnings
2374
+ });
2375
+ } catch (err) {
2376
+ const error = err instanceof Error ? err.message : String(err);
2377
+ log(`[test] \u7F16\u8BD1\u5931\u8D25 ${error}`);
2378
+ sendJson(res, { ok: false, error });
2379
+ }
2380
+ return;
2381
+ }
2382
+ if (method === "GET" && url.pathname === "/api/assets") {
2383
+ const rel = url.searchParams.get("path") ?? "";
2384
+ try {
2385
+ const file = resolveWorkspaceAssetPath(cwd, rel);
2386
+ sendJson(res, { text: await fs8.readFile(file, "utf8") });
2387
+ } catch (err) {
2388
+ const error = err instanceof Error ? err.message : String(err);
2389
+ log(`[assets] FAIL ${rel} ${error}`);
2390
+ sendJson(res, { error });
2391
+ }
2392
+ return;
2393
+ }
2394
+ if (method === "POST" && url.pathname === "/api/remap-error") {
2395
+ const body = asRecord3(await readJsonBody(req));
2396
+ const stack = typeof body.stack === "string" ? body.stack : "";
2397
+ sendJson(res, { stack: lastBundledCode ? remapErrorStack(stack, lastBundledCode) : stack });
2398
+ return;
2399
+ }
2400
+ sendText(res, "not found", 404, "text/plain; charset=utf-8");
2401
+ } catch (err) {
2402
+ const error = err instanceof Error ? err.message : String(err);
2403
+ log(`[preview] ${error}`);
2404
+ if (!res.headersSent) sendJson(res, { error }, 500);
2405
+ }
2406
+ }
2407
+ const port = options.port ?? DEFAULT_PREVIEW_PORT;
2408
+ await new Promise((resolve, reject) => {
2409
+ server.once("error", reject);
2410
+ server.listen(port, hostname, () => resolve());
2411
+ });
2412
+ const address = server.address();
2413
+ const bound = typeof address === "object" && address !== null ? address.port : port;
2414
+ originRef.url = `http://${hostname}:${bound}`;
2415
+ return {
2416
+ url: originRef.url,
2417
+ port: bound,
2418
+ sceneId,
2419
+ mock: config.mock,
2420
+ close: () => new Promise((resolve, reject) => {
2421
+ server.close((err) => err ? reject(err) : resolve());
2422
+ })
2423
+ };
2424
+ }
1339
2425
 
1340
2426
  // src/bin.ts
1341
2427
  async function readVersion() {
@@ -1398,7 +2484,7 @@ function buildProgram(cwd, version = "0.0.0") {
1398
2484
  if (config.mock) console.log(MOCK_MODE_HINT);
1399
2485
  const client = new EasyTwinClient(config);
1400
2486
  const scene2 = await pullScene(client, id);
1401
- const out = opts.out ?? path8.join(cwd, `${id}.scene.json`);
2487
+ const out = opts.out ?? path10.join(cwd, `${id}.scene.json`);
1402
2488
  const file = await saveSceneJson(scene2, out);
1403
2489
  console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
1404
2490
  });
@@ -1406,15 +2492,16 @@ function buildProgram(cwd, version = "0.0.0") {
1406
2492
  const config = await loadConfig(cwd);
1407
2493
  if (config.mock) console.log(MOCK_MODE_HINT);
1408
2494
  const client = new EasyTwinClient(config);
1409
- const target = dirArg ? path8.resolve(cwd, dirArg) : cwd;
2495
+ const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
1410
2496
  const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
1411
2497
  console.log(formatWorkspacePullResult(result));
1412
2498
  });
1413
- program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
2499
+ program.command("upload").description("\u5BF9\u7167\u8FDC\u7AEF\u5168\u91CF\u5BF9\u9F50\u5DE5\u4F5C\u533A(\u8DF3\u8FC7\u63D2\u4EF6\u4EA7\u7269,\u4E0D\u53EF\u9006)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").action(async (dirArg) => {
1414
2500
  const config = await loadConfig(cwd);
1415
2501
  if (config.mock) console.log(MOCK_MODE_HINT);
1416
2502
  const client = new EasyTwinClient(config);
1417
- await uploadDirectory(client, dir, {
2503
+ const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
2504
+ const result = await uploadDirectory(client, target, {
1418
2505
  onProgress: (p) => {
1419
2506
  if (p.phase === "upload") {
1420
2507
  process.stderr.write(`\r\u5DF2\u4E0A\u4F20 ${p.current}/${p.total} bytes${p.file ? ` (${p.file})` : ""}`);
@@ -1422,15 +2509,29 @@ function buildProgram(cwd, version = "0.0.0") {
1422
2509
  }
1423
2510
  });
1424
2511
  process.stderr.write("\n");
2512
+ console.log(formatUploadResult(result));
1425
2513
  });
1426
2514
  program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8 @easytwin/runtime \u4E0E @easytwin/apps)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
1427
2515
  const hint = await typesMissingHint(cwd);
1428
2516
  if (hint) console.warn(hint);
1429
- const outFile = path8.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
2517
+ const outFile = path10.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
1430
2518
  const result = await bundleUserCode({ cwd, outFile });
1431
2519
  for (const warning of result.warnings) console.warn(warning);
1432
2520
  console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
1433
2521
  });
2522
+ program.command("preview").description("\u542F\u52A8\u672C\u5730\u9884\u89C8\u9875(\u4E09\u7EF4\u573A\u666F + Run + \u6D4B\u8BD5\u63A7\u4EF6),\u6253\u5370 URL \u4F9B\u6D4F\u89C8\u5668\u6253\u5F00").argument("[sceneId]", "\u573A\u666F Scene Key(\u7F3A\u7701\u914D\u7F6E scenes \u4E2D defaultLoading \u6216\u9996\u9879)").option("-p, --port <port>", "\u7AEF\u53E3(\u7F3A\u7701 4173,\u53EA\u7ED1 127.0.0.1)", "4173").action(async (sceneId, opts) => {
2523
+ const port = Number.parseInt(opts.port, 10);
2524
+ if (!Number.isFinite(port) || port < 0) throw new Error(`\u65E0\u6548\u7AEF\u53E3:${opts.port}`);
2525
+ const server = await startPreviewServer({ cwd, sceneId, port });
2526
+ console.log(formatPreviewReadyMessage(server));
2527
+ await new Promise((resolve, reject) => {
2528
+ const stop = () => {
2529
+ server.close().then(resolve, reject);
2530
+ };
2531
+ process.once("SIGINT", stop);
2532
+ process.once("SIGTERM", stop);
2533
+ });
2534
+ });
1434
2535
  const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
1435
2536
  skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex/qoder),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|qoder|all", "all").action(async (opts) => {
1436
2537
  const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
@@ -1464,4 +2565,3 @@ export {
1464
2565
  main,
1465
2566
  runInit
1466
2567
  };
1467
- //# sourceMappingURL=bin.js.map