@easytwin/devkit 0.1.2 → 0.1.4

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,18 +7,20 @@ 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";
14
14
  import path from "path";
15
+ var EASYTWIN_DIR = ".easytwin";
15
16
  var CONFIG_FILE_NAME = "easytwin.config.json";
17
+ var EASYTWIN_SCENES_DIRNAME = "scenes";
16
18
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
17
19
  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
- var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
20
+ var TEST_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
21
+ var PROD_OSS_URL = "https://dt-easyv-prod.oss-cn-hangzhou.aliyuncs.com/";
22
+ var DEFAULT_OSS_URL = TEST_OSS_URL;
23
+ var RUNTIME_TYPES_RELPATH = "easytwin/system/libs/runtime/types/index.d.ts";
22
24
  var MOCK_APP_ID = "test";
23
25
  var MOCK_APP_SECRET = "test";
24
26
  function isMockCredentials(appId, appSecret) {
@@ -31,8 +33,31 @@ var ConfigError = class extends Error {
31
33
  }
32
34
  };
33
35
  function configFilePath(cwd) {
36
+ return path.join(cwd, EASYTWIN_DIR, CONFIG_FILE_NAME);
37
+ }
38
+ function joinOssPath(ossUrl, relPath) {
39
+ return `${ossUrl.replace(/\/+$/, "")}/${relPath.replace(/^\/+/, "")}`;
40
+ }
41
+ function resolveRuntimeTypesUrl(options = {}) {
42
+ const root = options.ossUrl ?? (options.env === "prod" ? PROD_OSS_URL : TEST_OSS_URL);
43
+ return joinOssPath(root, RUNTIME_TYPES_RELPATH);
44
+ }
45
+ async function resolveRuntimeTypesUrlForCwd(cwd, env = process.env) {
46
+ let file;
47
+ try {
48
+ file = await readConfigFile(cwd);
49
+ } catch {
50
+ }
51
+ const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file?.env;
52
+ const ossUrl = env.EASYTWIN_OSS_URL ?? file?.ossUrl;
53
+ return resolveRuntimeTypesUrl({ env: easyEnv, ossUrl });
54
+ }
55
+ function legacyConfigFilePath(cwd) {
34
56
  return path.join(cwd, CONFIG_FILE_NAME);
35
57
  }
58
+ function defaultSceneOutPath(cwd, id) {
59
+ return path.join(cwd, EASYTWIN_DIR, EASYTWIN_SCENES_DIRNAME, `${path.basename(id)}.scene.json`);
60
+ }
36
61
  function parseConfig(raw) {
37
62
  let parsed;
38
63
  try {
@@ -62,29 +87,14 @@ function validateConfigShape(value) {
62
87
  if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
63
88
  throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
64
89
  }
65
- const opAccountId = optionalGatewayId(v.opAccountId);
66
- const opUserId = optionalGatewayId(v.opUserId);
67
- const spaceId = optionalGatewayId(v.spaceId);
68
90
  const config = { appId: v.appId, appSecret: v.appSecret };
69
91
  if (v.env === "prod" || v.env === "test") config.env = v.env;
70
92
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
71
93
  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
94
  const scenes = parseConfigScenes(v.scenes);
76
95
  if (scenes) config.scenes = scenes;
77
96
  return config;
78
97
  }
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
98
  function parseConfigScenes(value) {
89
99
  if (value === void 0) return void 0;
90
100
  if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
@@ -115,15 +125,39 @@ function parseConfigScenes(value) {
115
125
  return scene;
116
126
  });
117
127
  }
118
- async function readConfigFile(cwd) {
119
- const file = configFilePath(cwd);
120
- let raw;
128
+ async function tryReadText(file) {
121
129
  try {
122
- raw = await fs.readFile(file, "utf8");
130
+ return await fs.readFile(file, "utf8");
123
131
  } catch {
124
- throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
132
+ return void 0;
133
+ }
134
+ }
135
+ async function removeLegacyConfigFile(cwd) {
136
+ try {
137
+ await fs.unlink(legacyConfigFilePath(cwd));
138
+ } catch {
139
+ }
140
+ }
141
+ async function migrateLegacyConfigIfPresent(cwd) {
142
+ const raw = await tryReadText(legacyConfigFilePath(cwd));
143
+ if (raw === void 0) return void 0;
144
+ const config = parseConfig(raw);
145
+ await writeConfigFile(cwd, config);
146
+ await removeLegacyConfigFile(cwd);
147
+ await appendGitignore(cwd);
148
+ return config;
149
+ }
150
+ async function readConfigFile(cwd) {
151
+ const file = configFilePath(cwd);
152
+ const raw = await tryReadText(file);
153
+ if (raw !== void 0) {
154
+ const config = parseConfig(raw);
155
+ await removeLegacyConfigFile(cwd);
156
+ return config;
125
157
  }
126
- return parseConfig(raw);
158
+ const migrated = await migrateLegacyConfigIfPresent(cwd);
159
+ if (migrated) return migrated;
160
+ throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
127
161
  }
128
162
  function resolveConfig(file, env = process.env) {
129
163
  const appId = env.EASYTWIN_APP_ID ?? file.appId;
@@ -134,30 +168,11 @@ function resolveConfig(file, env = process.env) {
134
168
  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
169
  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
170
  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;
171
+ return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
146
172
  }
147
173
  async function loadConfig(cwd, env = process.env) {
148
174
  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;
175
+ return resolveConfig(file, env);
161
176
  }
162
177
  async function writeConfigFile(cwd, config) {
163
178
  const file = configFilePath(cwd);
@@ -165,19 +180,15 @@ async function writeConfigFile(cwd, config) {
165
180
  if (config.env) body.env = config.env;
166
181
  if (config.baseUrl) body.baseUrl = config.baseUrl;
167
182
  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
183
  if (config.scenes !== void 0) body.scenes = config.scenes;
175
- await fs.mkdir(cwd, { recursive: true });
184
+ await fs.mkdir(path.dirname(file), { recursive: true });
176
185
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
186
+ await removeLegacyConfigFile(cwd);
177
187
  return file;
178
188
  }
179
189
  var GITIGNORE_FILE_NAME = ".gitignore";
180
- var GITIGNORE_ENTRY = "easytwin.config.json";
190
+ var GITIGNORE_ENTRY = ".easytwin/easytwin.config.json";
191
+ var GITIGNORE_ENTRIES = [GITIGNORE_ENTRY, ".easytwin/scenes/"];
181
192
  async function appendGitignore(cwd) {
182
193
  const file = path.join(cwd, GITIGNORE_FILE_NAME);
183
194
  let content = "";
@@ -188,9 +199,10 @@ async function appendGitignore(cwd) {
188
199
  created = true;
189
200
  }
190
201
  const lines = content.split(/\r?\n/);
191
- if (lines.includes(GITIGNORE_ENTRY)) return { created, added: false };
202
+ const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
203
+ if (missing.length === 0) return { created, added: false };
192
204
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
193
- await fs.writeFile(file, content + prefix + GITIGNORE_ENTRY + "\n", "utf8");
205
+ await fs.writeFile(file, content + prefix + missing.join("\n") + "\n", "utf8");
194
206
  return { created, added: true };
195
207
  }
196
208
  async function initConfig(input, cwd) {
@@ -208,22 +220,20 @@ async function writeConfigScenes(cwd, scenes) {
208
220
  import http from "http";
209
221
  import https from "https";
210
222
  import { URL as URL2 } from "url";
223
+ var APP_ID_HEADER = "x-app-id";
211
224
  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
225
  function enc(id) {
216
226
  return encodeURIComponent(id);
217
227
  }
218
228
  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`
229
+ /** POST 已关联场景列表(分享路径,空 body)。 */
230
+ linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
231
+ /** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
232
+ workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
233
+ /** POST 一次推送 create/update/delete(分享路径)。 */
234
+ workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
235
+ /** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
236
+ workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
227
237
  };
228
238
  var EasyTwinApiError = class extends Error {
229
239
  status;
@@ -266,35 +276,25 @@ var EasyTwinClient = class {
266
276
  baseUrl;
267
277
  /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
268
278
  ossUrl;
269
- /** 接入凭证 App ID;同时作为场景/代码 API applicationId 路径参数。 */
279
+ /** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
270
280
  appId;
271
281
  /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
272
282
  mock;
273
283
  appSecret;
274
- opAccountId;
275
- opUserId;
276
- spaceId;
277
284
  constructor(config) {
278
285
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
279
286
  this.ossUrl = config.ossUrl.replace(/\/+$/, "");
280
287
  this.mock = config.mock;
281
288
  this.appId = config.appId;
282
289
  this.appSecret = config.appSecret;
283
- this.opAccountId = config.opAccountId;
284
- this.opUserId = config.opUserId;
285
- this.spaceId = config.spaceId;
286
290
  }
287
291
  /** 全仓唯一认证头注入点。 */
288
292
  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;
293
+ return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
294
294
  }
295
295
  /** JSON 请求(原生 fetch)。 */
296
- async request(path9, options = {}) {
297
- const url = `${this.baseUrl}${path9}`;
296
+ async request(path11, options = {}) {
297
+ const url = `${this.baseUrl}${path11}`;
298
298
  const headers = { ...this.authHeaders(), ...options.headers };
299
299
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
300
300
  headers["Content-Type"] = "application/json";
@@ -316,8 +316,8 @@ var EasyTwinClient = class {
316
316
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
317
317
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
318
318
  */
319
- async upload(path9, options) {
320
- const url = new URL2(`${this.baseUrl}${path9}`);
319
+ async upload(path11, options) {
320
+ const url = new URL2(`${this.baseUrl}${path11}`);
321
321
  const mod = url.protocol === "https:" ? https : http;
322
322
  const headers = {
323
323
  ...this.authHeaders(),
@@ -474,42 +474,215 @@ function deriveExampleSceneId(payload) {
474
474
  async function exampleSceneSummary(exampleFile) {
475
475
  return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
476
476
  }
477
+ async function fetchLinkedScenes(client) {
478
+ return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
479
+ }
477
480
  async function listScenes(client, options = {}) {
478
- const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" }));
481
+ const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
479
482
  if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
480
483
  return scenes;
481
484
  }
482
485
  async function pullScene(client, id, options = {}) {
486
+ let scene;
483
487
  if (client.mock) {
484
- const payload2 = await loadExampleScene(options.exampleFile);
485
- const sceneId = deriveExampleSceneId(payload2);
488
+ const payload = await loadExampleScene(options.exampleFile);
489
+ const sceneId = deriveExampleSceneId(payload);
486
490
  if (sceneId !== id) {
487
491
  throw new Error(`\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F:\u793A\u4F8B\u573A\u666F id \u4E3A ${sceneId},\u6536\u5230 ${id}(\u672C\u5730\u4EC5\u63D0\u4F9B ${EXAMPLE_SCENE_FILE} \u8FD9\u4E00\u4E2A\u573A\u666F)`);
488
492
  }
489
- return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
490
- }
491
- const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
492
- const scenes = normalizeLinkedScenes(data);
493
- const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
494
- if (!hit) {
495
- const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
496
- throw new Error(
497
- available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
498
- );
493
+ scene = { id: sceneId, name: MOCK_SCENE_NAME, payload };
494
+ } else {
495
+ const data = await fetchLinkedScenes(client);
496
+ const scenes = normalizeLinkedScenes(data);
497
+ const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
498
+ if (!hit) {
499
+ const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
500
+ throw new Error(
501
+ available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
502
+ );
503
+ }
504
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
505
+ scene = { id: hit.sceneKey, name: hit.name, payload };
499
506
  }
500
- const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
501
- return { id: hit.sceneKey, name: hit.name, payload };
507
+ if (options.cwd) await saveSceneJson(scene, defaultSceneOutPath(options.cwd, scene.id));
508
+ return scene;
502
509
  }
503
510
  async function saveSceneJson(scene, out) {
504
511
  await fs2.mkdir(path2.dirname(out), { recursive: true });
505
512
  await fs2.writeFile(out, JSON.stringify(scene.payload ?? scene, null, 2) + "\n", "utf8");
506
513
  return out;
507
514
  }
515
+ function parseSceneStructure(scene) {
516
+ let data = scene;
517
+ if (typeof data === "string") {
518
+ try {
519
+ data = JSON.parse(data);
520
+ } catch {
521
+ return [];
522
+ }
523
+ }
524
+ const root = data ?? {};
525
+ const objs = Array.isArray(root.objs) ? root.objs : [];
526
+ const hierarchy = Array.isArray(root.hierarchy) ? root.hierarchy : [];
527
+ const items = objs.length > 0 ? objs : hierarchy;
528
+ const typeById = /* @__PURE__ */ new Map();
529
+ for (const o of objs) {
530
+ if (typeof o.id === "string" && typeof o.type === "string") typeById.set(o.id, o.type);
531
+ }
532
+ const nodes = /* @__PURE__ */ new Map();
533
+ const order = [];
534
+ for (const item of items) {
535
+ const id = typeof item.id === "string" ? item.id : "";
536
+ if (id.length === 0 || nodes.has(id)) continue;
537
+ const name = typeof item.name === "string" && item.name.length > 0 ? item.name : id;
538
+ nodes.set(id, { id, name, type: typeById.get(id), children: [] });
539
+ order.push(id);
540
+ }
541
+ const childrenOf = /* @__PURE__ */ new Map();
542
+ for (const item of items) {
543
+ const id = typeof item.id === "string" ? item.id : "";
544
+ if (id.length === 0 || !nodes.has(id)) continue;
545
+ const parentId = typeof item.parentObjId === "string" && item.parentObjId.length > 0 ? item.parentObjId : "";
546
+ if (parentId.length === 0 || parentId === id || !nodes.has(parentId)) continue;
547
+ const kids = childrenOf.get(parentId) ?? [];
548
+ kids.push(id);
549
+ childrenOf.set(parentId, kids);
550
+ }
551
+ const isChild = /* @__PURE__ */ new Set();
552
+ for (const kids of childrenOf.values()) for (const k of kids) isChild.add(k);
553
+ const build2 = (id, seen) => {
554
+ const node = nodes.get(id);
555
+ const children = [];
556
+ if (!seen.has(id)) {
557
+ seen.add(id);
558
+ for (const kid of childrenOf.get(id) ?? []) children.push(build2(kid, seen));
559
+ seen.delete(id);
560
+ }
561
+ return { ...node, children };
562
+ };
563
+ return order.filter((id) => !isChild.has(id)).map((id) => build2(id, /* @__PURE__ */ new Set()));
564
+ }
565
+ var INSPECT_NODE_LIMIT = 200;
566
+ var INSPECT_INDENT = " ";
567
+ function nodeMatchesInspect(node, filter) {
568
+ if (filter.name !== void 0 && filter.name.length > 0) {
569
+ if (!node.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
570
+ }
571
+ if (filter.type !== void 0 && filter.type.length > 0) {
572
+ if (node.type !== filter.type) return false;
573
+ }
574
+ return true;
575
+ }
576
+ function filterSceneTree(roots, filter = {}) {
577
+ const hasName = filter.name !== void 0 && filter.name.length > 0;
578
+ const hasType = filter.type !== void 0 && filter.type.length > 0;
579
+ if (!hasName && !hasType) return roots;
580
+ const walk = (node) => {
581
+ const children = [];
582
+ for (const child of node.children) {
583
+ const kept = walk(child);
584
+ if (kept) children.push(kept);
585
+ }
586
+ if (nodeMatchesInspect(node, filter) || children.length > 0) {
587
+ return { ...node, children };
588
+ }
589
+ return void 0;
590
+ };
591
+ return roots.flatMap((node) => {
592
+ const kept = walk(node);
593
+ return kept ? [kept] : [];
594
+ });
595
+ }
596
+ function countSceneTreeNodes(roots) {
597
+ let n = 0;
598
+ const walk = (nodes) => {
599
+ for (const node of nodes) {
600
+ n += 1;
601
+ walk(node.children);
602
+ }
603
+ };
604
+ walk(roots);
605
+ return n;
606
+ }
607
+ function formatSceneTree(roots, limit = INSPECT_NODE_LIMIT) {
608
+ const total = countSceneTreeNodes(roots);
609
+ if (total === 0) {
610
+ return { text: "(\u65E0\u5BF9\u8C61\u7ED3\u6784)\n", printed: 0, total: 0, truncated: false };
611
+ }
612
+ const lines = [];
613
+ let printed = 0;
614
+ const walk = (nodes, depth) => {
615
+ for (const node of nodes) {
616
+ if (printed >= limit) return;
617
+ lines.push(`${INSPECT_INDENT.repeat(depth)}${node.id} ${node.name} ${node.type ?? ""}`);
618
+ printed += 1;
619
+ walk(node.children, depth + 1);
620
+ }
621
+ };
622
+ walk(roots, 0);
623
+ const truncated = printed < total;
624
+ if (truncated) lines.push(`\u5176\u4F59 ${total - printed} \u4E2A,\u8BF7\u52A0 --name/--type`);
625
+ return { text: `${lines.join("\n")}
626
+ `, printed, total, truncated };
627
+ }
628
+ async function inspectScene(options) {
629
+ const file = defaultSceneOutPath(options.cwd, options.id);
630
+ let raw;
631
+ try {
632
+ raw = await fs2.readFile(file, "utf8");
633
+ } catch {
634
+ throw new Error(`\u672A\u627E\u5230\u672C\u5730\u573A\u666F\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin scene pull ${options.id}\``);
635
+ }
636
+ let payload;
637
+ try {
638
+ payload = JSON.parse(raw);
639
+ } catch {
640
+ throw new Error(`\u573A\u666F\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
641
+ }
642
+ const roots = filterSceneTree(parseSceneStructure(payload), { name: options.name, type: options.type });
643
+ return { file, roots, ...formatSceneTree(roots, options.limit ?? INSPECT_NODE_LIMIT) };
644
+ }
508
645
 
509
646
  // src/upload.ts
510
647
  import { promises as fs3 } from "fs";
511
648
  import path3 from "path";
512
- var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".git"]);
649
+ var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
650
+ ".git",
651
+ "node_modules",
652
+ "dist",
653
+ ".easytwin",
654
+ ".cursor",
655
+ ".claude",
656
+ ".qoder",
657
+ ".vscode"
658
+ ]);
659
+ var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
660
+ function isTsconfigFile(base) {
661
+ return /^tsconfig(\..+)?\.json$/i.test(base);
662
+ }
663
+ var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
664
+ function isWorkspaceSpecFile(relPath) {
665
+ const base = normalizePath(relPath).split("/").pop() ?? "";
666
+ return /\.spec\.ts$/i.test(base);
667
+ }
668
+ function isIgnoredWorkspacePath(relPath) {
669
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
670
+ if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
671
+ const base = parts[parts.length - 1];
672
+ if (base === void 0) return false;
673
+ if (WORKSPACE_IGNORED_FILES.has(base)) return true;
674
+ if (isTsconfigFile(base)) return true;
675
+ return base.toLowerCase().endsWith(".scene.json");
676
+ }
677
+ function defaultWorkspaceIgnore(relPath) {
678
+ return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
679
+ }
680
+ function combineUploadIgnore(extra) {
681
+ return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
682
+ }
683
+ async function collectWorkspaceCodeFiles(dir, ignore) {
684
+ return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
685
+ }
513
686
  async function collectFiles(dir, ignore = () => false) {
514
687
  const files = [];
515
688
  async function walk(current, rel) {
@@ -518,7 +691,7 @@ async function collectFiles(dir, ignore = () => false) {
518
691
  const relPath = path3.posix.join(rel, entry.name);
519
692
  if (ignore(relPath)) continue;
520
693
  if (entry.isDirectory()) {
521
- if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
694
+ if (entry.name === ".git") continue;
522
695
  await walk(path3.join(current, entry.name), relPath);
523
696
  } else if (entry.isFile()) {
524
697
  const absPath = path3.join(current, entry.name);
@@ -537,26 +710,9 @@ function asRecord2(value) {
537
710
  function asString2(value) {
538
711
  return typeof value === "string" ? value : "";
539
712
  }
540
- function asNumber(value) {
541
- return typeof value === "number" && Number.isFinite(value) ? value : NaN;
542
- }
543
713
  function normalizePath(relPath) {
544
714
  return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
545
715
  }
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
716
  function normalizeWorkspaceFiles(data) {
561
717
  if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
562
718
  return data.map((item) => {
@@ -573,91 +729,98 @@ function extensionOf(relPath) {
573
729
  function allowedExtensionSet(list) {
574
730
  return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
575
731
  }
576
- function directoryDepth(relPath) {
577
- const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
578
- return Math.max(0, parts.length - 1);
732
+ var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
733
+ function isWorkspaceCodeFile(relPath) {
734
+ return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
579
735
  }
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
- );
736
+ function countsFromPlan(plan) {
737
+ return {
738
+ created: plan.creates.length,
739
+ updated: plan.updates.length,
740
+ deleted: plan.deletes.length,
741
+ unchanged: plan.unchanged.length
742
+ };
743
+ }
744
+ function planWorkspaceUpload(local, remote) {
745
+ const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
746
+ const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
747
+ const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
748
+ const creates = [];
749
+ const updates = [];
750
+ const unchanged = [];
751
+ for (const item of local) {
752
+ const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
753
+ if (!remoteFile) creates.push(item);
754
+ else if (remoteFile.content !== item.content) {
755
+ updates.push({ file: item.file, id: remoteFile.id, content: item.content });
756
+ } else {
757
+ unchanged.push(item.file);
598
758
  }
599
759
  }
600
- if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
760
+ return { creates, updates, deletes, unchanged };
761
+ }
762
+ function formatUploadResult(result) {
763
+ const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
764
+ 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
765
  }
602
766
  async function mockUpload(dir, options) {
603
- const ignore = options.ignore ?? (() => false);
604
- const files = await collectFiles(dir, ignore);
767
+ const ignore = combineUploadIgnore(options.ignore);
768
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
605
769
  const total = files.reduce((sum, f) => sum + f.size, 0);
606
770
  options.onProgress?.({ phase: "collect", current: total, total });
607
771
  options.onProgress?.({ phase: "upload", current: total, total });
608
- return { fileCount: files.length, byteCount: total, mock: true };
772
+ return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
773
+ }
774
+ function buildWorkspacePushBody(plan) {
775
+ const body = {};
776
+ if (plan.creates.length > 0) {
777
+ body.create = plan.creates.map((c) => ({
778
+ filePath: normalizePath(c.file.relPath),
779
+ content: c.content
780
+ }));
781
+ }
782
+ if (plan.updates.length > 0) {
783
+ body.update = plan.updates.map((p) => ({
784
+ id: p.id,
785
+ content: p.content,
786
+ filePath: normalizePath(p.file.relPath)
787
+ }));
788
+ }
789
+ if (plan.deletes.length > 0) {
790
+ body.delete = plan.deletes.map((d) => d.id);
791
+ }
792
+ return Object.keys(body).length > 0 ? body : null;
609
793
  }
610
794
  async function uploadDirectory(client, dir, options = {}) {
611
795
  if (client.mock) return mockUpload(dir, options);
612
- const ignore = options.ignore ?? (() => false);
613
- const files = await collectFiles(dir, ignore);
796
+ const ignore = combineUploadIgnore(options.ignore);
797
+ const files = await collectWorkspaceCodeFiles(dir, ignore);
614
798
  const total = files.reduce((sum, f) => sum + f.size, 0);
615
799
  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 = [];
800
+ const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
801
+ const local = [];
632
802
  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);
803
+ local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
804
+ }
805
+ const plan = planWorkspaceUpload(local, remote);
806
+ const counts = countsFromPlan(plan);
807
+ const pushBody = buildWorkspacePushBody(plan);
808
+ if (pushBody) {
809
+ await client.request(ENDPOINTS.workspaceCodePush(), {
810
+ method: "POST",
811
+ body: JSON.stringify(pushBody)
812
+ });
638
813
  }
639
814
  let sent = 0;
640
815
  const bump = (file) => {
641
816
  sent += file.size;
642
817
  options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
643
818
  };
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);
819
+ for (const c of plan.creates) bump(c.file);
820
+ for (const p of plan.updates) bump(p.file);
821
+ for (const u of plan.unchanged) bump(u);
659
822
  if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
660
- return { fileCount: files.length, byteCount: total };
823
+ return { fileCount: files.length, byteCount: total, ...counts };
661
824
  }
662
825
 
663
826
  // src/workspace.ts
@@ -705,42 +868,22 @@ export default class App extends TwinApp {
705
868
  }
706
869
  }
707
870
  `;
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
871
  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);
872
+ return defaultWorkspaceIgnore(relPath);
725
873
  }
726
874
  function isSafeRelPath(relPath) {
727
875
  const n = normalizePath(relPath);
728
876
  if (!n) return false;
729
- if (n.toLowerCase() === "easytwin.config.json") return false;
877
+ const lower = n.toLowerCase();
878
+ if (lower === "easytwin.config.json" || lower.endsWith("/easytwin.config.json")) return false;
879
+ if (lower.split("/")[0] === ".easytwin") return false;
880
+ const base = n.split("/").pop() ?? "";
881
+ if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
730
882
  if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
731
883
  const parts = n.split("/");
732
884
  if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
733
885
  return true;
734
886
  }
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
887
  function normalizeEol(text) {
745
888
  return text.replace(/\r\n/g, "\n");
746
889
  }
@@ -756,30 +899,18 @@ async function readLocalText(absPath) {
756
899
  throw err;
757
900
  }
758
901
  }
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
902
  async function fetchRemoteFiles(client) {
771
903
  if (client.mock) return [];
772
- return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(client.appId), { method: "GET" }));
904
+ return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
773
905
  }
774
906
  async function planWorkspacePull(client, dir, options = {}) {
775
907
  const ignore = combineIgnore(options.ignore);
776
- const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
777
908
  const remoteRaw = await fetchRemoteFiles(client);
778
909
  const skippedRemotePaths = [];
779
910
  const remoteByPath = /* @__PURE__ */ new Map();
780
911
  for (const file of remoteRaw) {
781
912
  const rel = normalizePath(file.filePath);
782
- if (!isSafeRelPath(rel) || ignore(rel)) {
913
+ if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
783
914
  skippedRemotePaths.push(rel || file.filePath);
784
915
  continue;
785
916
  }
@@ -795,7 +926,7 @@ async function planWorkspacePull(client, dir, options = {}) {
795
926
  const localByPath = /* @__PURE__ */ new Map();
796
927
  for (const file of localFiles) {
797
928
  const rel = normalizePath(file.relPath);
798
- if (!allowed.has(extensionOf2(rel))) continue;
929
+ if (!isWorkspaceCodeFile(rel)) continue;
799
930
  const content = await readLocalText(file.absPath);
800
931
  if (content !== void 0) localByPath.set(rel, content);
801
932
  }
@@ -920,7 +1051,8 @@ var SKILL_NAMES = [
920
1051
  "easytwin-develop",
921
1052
  "easytwin-bootstrap",
922
1053
  "easytwin-scene",
923
- "easytwin-upload"
1054
+ "easytwin-upload",
1055
+ "easytwin-test"
924
1056
  ];
925
1057
  var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
926
1058
  var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
@@ -998,6 +1130,12 @@ function buildRuntimeTypesContent(sourceDts) {
998
1130
  return `${sourceDts.replace(/\s+$/, "")}
999
1131
  `;
1000
1132
  }
1133
+ var SkillsError = class extends Error {
1134
+ constructor(message) {
1135
+ super(message);
1136
+ this.name = "SkillsError";
1137
+ }
1138
+ };
1001
1139
  function normalizeTargets(target = "all") {
1002
1140
  if (target === "all") return ["cursor", "claude", "codex", "qoder"];
1003
1141
  if (Array.isArray(target)) return [...new Set(target)];
@@ -1010,16 +1148,23 @@ function resolveSkillsSourceDir() {
1010
1148
  const here = path6.dirname(fileURLToPath2(import.meta.url));
1011
1149
  return path6.resolve(here, "..", "skills");
1012
1150
  }
1013
- function resolveRuntimeTypesSourceFile() {
1014
- if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1015
- throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
1151
+ async function fetchRuntimeTypesDts(url, fetchImpl) {
1152
+ let res;
1153
+ try {
1154
+ res = await fetchImpl(url);
1155
+ } catch (err) {
1156
+ throw new SkillsError(
1157
+ `\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:${url} (${err instanceof Error ? err.message : String(err)})`
1158
+ );
1016
1159
  }
1017
- const here = path6.dirname(fileURLToPath2(import.meta.url));
1018
- const fromDist = path6.join(here, "runtime-types", "index.d.ts");
1019
- const fromSrc = path6.resolve(here, "lib", "index.d.ts");
1020
- if (existsSync(fromDist)) return fromDist;
1021
- if (existsSync(fromSrc)) return fromSrc;
1022
- throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
1160
+ if (!res.ok) {
1161
+ throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:HTTP ${res.status} ${url}`);
1162
+ }
1163
+ const text = await res.text();
1164
+ if (text.trim().length === 0) {
1165
+ throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:\u7A7A\u54CD\u5E94 ${url}`);
1166
+ }
1167
+ return text;
1023
1168
  }
1024
1169
  async function readJson(file) {
1025
1170
  return JSON.parse(await fs5.readFile(file, "utf8"));
@@ -1106,11 +1251,12 @@ function buildCodexSegment(version) {
1106
1251
  `EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
1107
1252
  "",
1108
1253
  "- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
1109
- "- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
1110
- "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
1254
+ "- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(`.easytwin/easytwin.config.json`)\u3002",
1255
+ "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3001inspect \u672C\u5730\u5BF9\u8C61\u6811\u3002",
1111
1256
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
1112
1257
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
1113
1258
  "- `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",
1259
+ "- `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
1260
  "",
1115
1261
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
1116
1262
  CODEX_MARKER_END
@@ -1145,11 +1291,11 @@ async function syncToCodex(sourceRoot, cwd, version) {
1145
1291
  }
1146
1292
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
1147
1293
  }
1148
- async function syncRuntimeTypes(cwd, typesSourceFile) {
1294
+ async function syncRuntimeTypes(cwd, sourceDts) {
1149
1295
  const destDir = path6.join(cwd, ".easytwin", "types");
1150
1296
  const destFile = path6.join(destDir, "index.d.ts");
1151
1297
  const appsFile = path6.join(destDir, APPS_TYPES_FILE);
1152
- const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
1298
+ const content = buildRuntimeTypesContent(sourceDts);
1153
1299
  let runtimeCurrent = "";
1154
1300
  let appsCurrent = "";
1155
1301
  let runtimeExists = true;
@@ -1199,7 +1345,11 @@ async function syncSkills(options) {
1199
1345
  else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
1200
1346
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
1201
1347
  }
1202
- const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
1348
+ const typesSource = options.typesSourceFile ? await fs5.readFile(options.typesSourceFile, "utf8") : await fetchRuntimeTypesDts(
1349
+ await resolveRuntimeTypesUrlForCwd(options.cwd, options.env ?? process.env),
1350
+ options.fetch ?? fetch
1351
+ );
1352
+ const types = await syncRuntimeTypes(options.cwd, typesSource);
1203
1353
  return { summaries, types };
1204
1354
  }
1205
1355
 
@@ -1281,14 +1431,14 @@ function collectBundledRuntimeExports(code) {
1281
1431
  }
1282
1432
  return names;
1283
1433
  }
1284
- async function bundleUserCode(options) {
1434
+ async function bundleWorkspaceModule(options) {
1285
1435
  const cwd = path7.resolve(options.cwd);
1286
- const entry = path7.join(cwd, USER_ENTRY);
1436
+ const entry = path7.isAbsolute(options.entry) ? options.entry : path7.join(cwd, options.entry);
1287
1437
  try {
1288
1438
  await fs6.access(entry);
1289
1439
  } catch {
1290
1440
  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`
1441
+ options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
1292
1442
  );
1293
1443
  }
1294
1444
  let result;
@@ -1328,6 +1478,16 @@ async function bundleUserCode(options) {
1328
1478
  }
1329
1479
  return { code, warnings };
1330
1480
  }
1481
+ async function bundleUserCode(options) {
1482
+ const cwd = path7.resolve(options.cwd);
1483
+ const entry = path7.join(cwd, USER_ENTRY);
1484
+ return bundleWorkspaceModule({
1485
+ cwd: options.cwd,
1486
+ entry: USER_ENTRY,
1487
+ outFile: options.outFile,
1488
+ 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`
1489
+ });
1490
+ }
1331
1491
  async function typesMissingHint(cwd) {
1332
1492
  try {
1333
1493
  await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
@@ -1336,6 +1496,1140 @@ async function typesMissingHint(cwd) {
1336
1496
  return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
1337
1497
  }
1338
1498
  }
1499
+ var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
1500
+ var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1501
+ function decodeVLQValues(str) {
1502
+ const values = [];
1503
+ let i = 0;
1504
+ while (i < str.length) {
1505
+ let result = 0;
1506
+ let shift = 0;
1507
+ let continuation = true;
1508
+ while (continuation) {
1509
+ if (i >= str.length) return values;
1510
+ const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
1511
+ if (digit < 0) return values;
1512
+ continuation = (digit & 32) !== 0;
1513
+ result += (digit & 31) << shift;
1514
+ shift += 5;
1515
+ }
1516
+ values.push(result & 1 ? -(result >> 1) : result >> 1);
1517
+ }
1518
+ return values;
1519
+ }
1520
+ function decodeMappings(map) {
1521
+ const sources = map.sources ?? [];
1522
+ const lines = (map.mappings ?? "").split(";");
1523
+ let sourceIndex = 0;
1524
+ let originalLine = 0;
1525
+ let originalColumn = 0;
1526
+ const decoded = [];
1527
+ for (const line of lines) {
1528
+ let generatedColumn = 0;
1529
+ const segs = [];
1530
+ if (line) {
1531
+ for (const raw of line.split(",")) {
1532
+ if (!raw) continue;
1533
+ const nums = decodeVLQValues(raw);
1534
+ if (nums[0] === void 0) continue;
1535
+ generatedColumn += nums[0];
1536
+ if (nums.length >= 4) {
1537
+ sourceIndex += nums[1] ?? 0;
1538
+ originalLine += nums[2] ?? 0;
1539
+ originalColumn += nums[3] ?? 0;
1540
+ segs.push({
1541
+ generatedColumn,
1542
+ source: sources[sourceIndex] ?? USER_ENTRY,
1543
+ originalLine,
1544
+ originalColumn
1545
+ });
1546
+ }
1547
+ }
1548
+ }
1549
+ decoded.push(segs);
1550
+ }
1551
+ return decoded;
1552
+ }
1553
+ function originalPositionFor(map, line, column) {
1554
+ const decoded = decodeMappings(map);
1555
+ for (let i = line - 1; i >= 0; i--) {
1556
+ const segs = decoded[i];
1557
+ if (!segs || segs.length === 0) continue;
1558
+ const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
1559
+ let best = segs[0];
1560
+ for (const seg of segs) {
1561
+ if (seg.generatedColumn <= col) best = seg;
1562
+ else break;
1563
+ }
1564
+ if (!best) continue;
1565
+ return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
1566
+ }
1567
+ return void 0;
1568
+ }
1569
+ function extractInlineSourceMap(code) {
1570
+ const m = code.match(SOURCEMAP_RE);
1571
+ if (!m?.[1]) return void 0;
1572
+ try {
1573
+ return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
1574
+ } catch {
1575
+ return void 0;
1576
+ }
1577
+ }
1578
+ function remapErrorStack(stack, bundledCode) {
1579
+ const map = extractInlineSourceMap(bundledCode);
1580
+ if (!map?.mappings) return stack;
1581
+ return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
1582
+ const orig = originalPositionFor(map, Number(line), Number(col));
1583
+ if (!orig) return full;
1584
+ return `${orig.source}:${orig.line}:${orig.column}`;
1585
+ });
1586
+ }
1587
+
1588
+ // src/previewServer.ts
1589
+ import { existsSync as existsSync2 } from "fs";
1590
+ import { promises as fs8 } from "fs";
1591
+ import http2 from "http";
1592
+ import path9 from "path";
1593
+ import { fileURLToPath as fileURLToPath3 } from "url";
1594
+
1595
+ // src/previewHtml.ts
1596
+ function escapeJsonForScript(json) {
1597
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1598
+ }
1599
+ var PREVIEW_STYLE = `
1600
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
1601
+ body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
1602
+ #stage { position: relative; flex: 1; min-height: 0; }
1603
+ #twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
1604
+ #twin-root canvas { display: block; }
1605
+ #status {
1606
+ position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
1607
+ padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
1608
+ }
1609
+ #status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
1610
+ /* \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 */
1611
+ #status[hidden] { display: none; }
1612
+ #mock-banner {
1613
+ position: absolute; top: 0; left: 0; right: 0; z-index: 2;
1614
+ padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
1615
+ border-bottom: 1px solid #f0c36d; pointer-events: none;
1616
+ }
1617
+ #debug-log {
1618
+ display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
1619
+ max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
1620
+ background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
1621
+ white-space: pre-wrap; word-break: break-all; pointer-events: auto;
1622
+ }
1623
+ #debug-log.open { display: block; }
1624
+ #run-bar {
1625
+ flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
1626
+ padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
1627
+ }
1628
+ #run-bar button {
1629
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
1630
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
1631
+ }
1632
+ #run-bar button:disabled { opacity: .55; cursor: default; }
1633
+ #btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
1634
+ #run-status { color: #bbb; min-width: 4em; }
1635
+ #run-bar .spacer { flex: 1; }
1636
+ #test-panel {
1637
+ flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
1638
+ padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
1639
+ max-height: 30%; overflow: auto;
1640
+ }
1641
+ #test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
1642
+ #test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
1643
+ #test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
1644
+ #test-panel .test-empty { color: #777; }
1645
+ #test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
1646
+ #test-panel input.test-input {
1647
+ width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
1648
+ padding: 3px 6px; border-radius: 4px; font: 12px inherit;
1649
+ }
1650
+ #test-panel button {
1651
+ cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
1652
+ padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
1653
+ }
1654
+ #test-panel button:disabled { opacity: .55; cursor: default; }
1655
+ `;
1656
+ var RENDER_SCRIPT = `
1657
+ var statusEl = document.getElementById("status");
1658
+ var logEl = document.getElementById("debug-log");
1659
+ var engine = null;
1660
+ var runtime = null;
1661
+ var sceneJson = null;
1662
+ var host = null;
1663
+ var running = false;
1664
+ var runningTest = false;
1665
+ var testsReady = false;
1666
+ var hostKind = __HOST_KIND__;
1667
+ var vscodeApi = null;
1668
+ if (hostKind === "vscode") {
1669
+ try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
1670
+ }
1671
+ var pending = {};
1672
+ var seq = 0;
1673
+ var origFetch = window.fetch.bind(window);
1674
+ function now() { return new Date().toISOString().slice(11, 23); }
1675
+ function log(line) {
1676
+ var text = "[" + now() + "] " + line;
1677
+ if (logEl) {
1678
+ logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
1679
+ logEl.scrollTop = logEl.scrollHeight;
1680
+ }
1681
+ if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
1682
+ console.log("[EasyTwin]", line);
1683
+ }
1684
+ function nextId() { return String(++seq); }
1685
+ function handleHostMessage(msg) {
1686
+ if (!msg) return;
1687
+ if (msg.type === "proxy-fetch-result") {
1688
+ var p = pending[msg.id];
1689
+ if (!p) return;
1690
+ delete pending[msg.id];
1691
+ if (msg.error) p.reject(new Error(msg.error));
1692
+ else p.resolve(msg);
1693
+ return;
1694
+ }
1695
+ if (msg.type === "bundle-result") {
1696
+ if (!msg.ok) {
1697
+ log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
1698
+ setRunStatus("\u7F16\u8BD1\u5931\u8D25");
1699
+ setRunBusy(false);
1700
+ if (logEl) logEl.classList.add("open");
1701
+ return;
1702
+ }
1703
+ for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
1704
+ runUserCode(msg.code);
1705
+ return;
1706
+ }
1707
+ if (msg.type === "read-asset-result") {
1708
+ var ap = pending[msg.id];
1709
+ if (!ap) return;
1710
+ delete pending[msg.id];
1711
+ if (msg.error) ap.reject(new Error(msg.error));
1712
+ else ap.resolve(msg.text);
1713
+ return;
1714
+ }
1715
+ if (msg.type === "run-error-mapped") {
1716
+ log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
1717
+ return;
1718
+ }
1719
+ if (msg.type === "tests") {
1720
+ renderTests(msg.tests || []);
1721
+ return;
1722
+ }
1723
+ if (msg.type === "test-bundle") {
1724
+ if (!msg.ok) {
1725
+ log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
1726
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
1727
+ runningTest = false;
1728
+ setRunBusy(running);
1729
+ if (logEl) logEl.classList.add("open");
1730
+ return;
1731
+ }
1732
+ for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
1733
+ runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
1734
+ log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
1735
+ setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
1736
+ }).catch(function (err) {
1737
+ log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
1738
+ setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
1739
+ if (logEl) logEl.classList.add("open");
1740
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
1741
+ }).then(function () {
1742
+ runningTest = false;
1743
+ setRunBusy(running);
1744
+ });
1745
+ }
1746
+ }
1747
+ function httpJson(url, init) {
1748
+ return origFetch(url, init).then(function (res) {
1749
+ return res.json().then(function (body) {
1750
+ if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
1751
+ return body;
1752
+ });
1753
+ });
1754
+ }
1755
+ function postToHost(msg) {
1756
+ if (vscodeApi) {
1757
+ vscodeApi.postMessage(msg);
1758
+ return;
1759
+ }
1760
+ if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
1761
+ if (msg.type === "run") {
1762
+ httpJson("/api/bundle", { method: "POST" }).then(function (body) {
1763
+ handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
1764
+ }).catch(function (err) {
1765
+ handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
1766
+ });
1767
+ return;
1768
+ }
1769
+ if (msg.type === "run-error") {
1770
+ httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
1771
+ handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
1772
+ }).catch(function (err) {
1773
+ log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
1774
+ });
1775
+ return;
1776
+ }
1777
+ if (msg.type === "read-asset") {
1778
+ httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
1779
+ handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
1780
+ }).catch(function (err) {
1781
+ handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
1782
+ });
1783
+ return;
1784
+ }
1785
+ if (msg.type === "list-tests") {
1786
+ httpJson("/api/tests").then(function (body) {
1787
+ handleHostMessage({ type: "tests", tests: body.tests || [] });
1788
+ }).catch(function (err) {
1789
+ log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
1790
+ });
1791
+ return;
1792
+ }
1793
+ if (msg.type === "run-test") {
1794
+ httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
1795
+ handleHostMessage(Object.assign({ type: "test-bundle" }, body));
1796
+ }).catch(function (err) {
1797
+ handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
1798
+ });
1799
+ }
1800
+ }
1801
+ function showStatus(text, isError) {
1802
+ if (!statusEl) return;
1803
+ statusEl.textContent = text;
1804
+ statusEl.className = isError ? "error" : "";
1805
+ statusEl.hidden = false;
1806
+ if (isError && logEl) logEl.classList.add("open");
1807
+ }
1808
+ function hideStatus() { if (statusEl) statusEl.hidden = true; }
1809
+ function formatError(err) {
1810
+ var msg = err && err.message ? err.message : String(err);
1811
+ var stack = err && err.stack ? "\\n" + err.stack : "";
1812
+ return msg + stack;
1813
+ }
1814
+ window.addEventListener("message", function (ev) {
1815
+ handleHostMessage(ev.data);
1816
+ });
1817
+ function b64ToBuf(b64) {
1818
+ var bin = atob(b64);
1819
+ var bytes = new Uint8Array(bin.length);
1820
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1821
+ return bytes.buffer;
1822
+ }
1823
+ function proxyFetch(url, method) {
1824
+ return new Promise(function (resolve, reject) {
1825
+ if (!vscodeApi) {
1826
+ reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
1827
+ return;
1828
+ }
1829
+ var id = nextId();
1830
+ pending[id] = { resolve: resolve, reject: reject };
1831
+ vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
1832
+ }).then(function (msg) {
1833
+ var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
1834
+ return new Response(buf, {
1835
+ status: msg.status || 0,
1836
+ statusText: msg.statusText || "",
1837
+ headers: msg.headers || {}
1838
+ });
1839
+ });
1840
+ }
1841
+ function isHttpUrl(url) {
1842
+ return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
1843
+ }
1844
+ function isPlainHttp(url) {
1845
+ return typeof url === "string" && url.indexOf("http://") === 0;
1846
+ }
1847
+ if (hostKind === "vscode") {
1848
+ window.fetch = function (input, init) {
1849
+ var url = typeof input === "string" ? input : (input && input.url);
1850
+ log("fetch " + url);
1851
+ if (isPlainHttp(url)) {
1852
+ return proxyFetch(url, init && init.method).then(function (res) {
1853
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
1854
+ return res;
1855
+ });
1856
+ }
1857
+ return origFetch(input, init).catch(function (err) {
1858
+ log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
1859
+ if (!isHttpUrl(url)) throw err;
1860
+ return proxyFetch(url, init && init.method).then(function (res) {
1861
+ log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
1862
+ return res;
1863
+ });
1864
+ });
1865
+ };
1866
+ var origOpen = XMLHttpRequest.prototype.open;
1867
+ var origSend = XMLHttpRequest.prototype.send;
1868
+ XMLHttpRequest.prototype.open = function (method, url) {
1869
+ this.__etMethod = method;
1870
+ this.__etUrl = String(url);
1871
+ return origOpen.apply(this, arguments);
1872
+ };
1873
+ XMLHttpRequest.prototype.send = function (body) {
1874
+ var xhr = this;
1875
+ var url = xhr.__etUrl;
1876
+ if (!isPlainHttp(url)) return origSend.call(this, body);
1877
+ log("xhr proxy " + xhr.__etMethod + " " + url);
1878
+ proxyFetch(url, xhr.__etMethod).then(function (res) {
1879
+ return res.arrayBuffer().then(function (buf) {
1880
+ var text = "";
1881
+ try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
1882
+ Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
1883
+ Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
1884
+ Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
1885
+ Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
1886
+ var rt = xhr.responseType;
1887
+ var response = buf;
1888
+ if (rt === "" || rt === "text") response = text;
1889
+ else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
1890
+ Object.defineProperty(xhr, "response", { configurable: true, value: response });
1891
+ Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
1892
+ if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
1893
+ xhr.dispatchEvent(new Event("load"));
1894
+ xhr.dispatchEvent(new Event("loadend"));
1895
+ });
1896
+ }).catch(function (err) {
1897
+ log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
1898
+ if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
1899
+ xhr.dispatchEvent(new Event("error"));
1900
+ xhr.dispatchEvent(new Event("loadend"));
1901
+ });
1902
+ };
1903
+ function patchHttpSrc(proto, prop) {
1904
+ var desc = Object.getOwnPropertyDescriptor(proto, prop);
1905
+ if (!desc || typeof desc.set !== "function") return;
1906
+ Object.defineProperty(proto, prop, {
1907
+ configurable: true,
1908
+ enumerable: desc.enumerable,
1909
+ get: function () { return desc.get.call(this); },
1910
+ set: function (value) {
1911
+ var el = this;
1912
+ var url = String(value);
1913
+ if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
1914
+ log("media proxy " + url);
1915
+ proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
1916
+ desc.set.call(el, URL.createObjectURL(blob));
1917
+ }).catch(function (err) {
1918
+ log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
1919
+ try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
1920
+ });
1921
+ }
1922
+ });
1923
+ }
1924
+ patchHttpSrc(HTMLImageElement.prototype, "src");
1925
+ patchHttpSrc(HTMLMediaElement.prototype, "src");
1926
+ }
1927
+ document.getElementById("btn-debug").addEventListener("click", function () {
1928
+ if (logEl) logEl.classList.toggle("open");
1929
+ });
1930
+ var btnDevtools = document.getElementById("btn-devtools");
1931
+ var btnOutput = document.getElementById("btn-output");
1932
+ if (hostKind === "http") {
1933
+ if (btnDevtools) btnDevtools.hidden = true;
1934
+ if (btnOutput) btnOutput.hidden = true;
1935
+ }
1936
+ if (btnDevtools) btnDevtools.addEventListener("click", function () {
1937
+ postToHost({ type: "open-devtools" });
1938
+ });
1939
+ if (btnOutput) btnOutput.addEventListener("click", function () {
1940
+ postToHost({ type: "show-output" });
1941
+ });
1942
+ function setRunStatus(text) {
1943
+ var el = document.getElementById("run-status");
1944
+ if (el) el.textContent = text;
1945
+ }
1946
+ function setRunBusy(busy) {
1947
+ running = busy;
1948
+ var btn = document.getElementById("btn-run");
1949
+ if (btn) btn.disabled = !!busy || runningTest;
1950
+ syncTestControls();
1951
+ }
1952
+ function syncTestControls() {
1953
+ var panel = document.getElementById("test-panel");
1954
+ if (!panel) return;
1955
+ var disabled = !testsReady || running || runningTest;
1956
+ var nodes = panel.querySelectorAll("button.test-run, input.test-input");
1957
+ for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
1958
+ }
1959
+ function renderTests(tests) {
1960
+ var list = document.getElementById("test-list");
1961
+ if (!list) return;
1962
+ list.textContent = "";
1963
+ if (!tests || !tests.length) {
1964
+ var empty = document.createElement("span");
1965
+ empty.className = "test-empty";
1966
+ empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
1967
+ list.appendChild(empty);
1968
+ return;
1969
+ }
1970
+ var groups = {};
1971
+ var order = [];
1972
+ for (var i = 0; i < tests.length; i++) {
1973
+ var t = tests[i];
1974
+ if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
1975
+ groups[t.file].push(t);
1976
+ }
1977
+ for (var g = 0; g < order.length; g++) {
1978
+ var file = order[g];
1979
+ var heading = document.createElement("span");
1980
+ heading.className = "test-file";
1981
+ heading.textContent = file;
1982
+ list.appendChild(heading);
1983
+ var items = groups[file];
1984
+ for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
1985
+ }
1986
+ syncTestControls();
1987
+ }
1988
+ function makeTestControl(t) {
1989
+ var wrap = document.createElement("span");
1990
+ wrap.className = "test-item";
1991
+ if (t.hasInput) {
1992
+ var input = document.createElement("input");
1993
+ input.type = "text";
1994
+ input.className = "test-input";
1995
+ input.placeholder = t.inputName || "input";
1996
+ var btn = document.createElement("button");
1997
+ btn.type = "button";
1998
+ btn.className = "test-run";
1999
+ btn.textContent = t.name;
2000
+ btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
2001
+ input.addEventListener("keydown", function (ev) {
2002
+ if (ev.key === "Enter") requestRunTest(t.id, input.value);
2003
+ });
2004
+ wrap.appendChild(input);
2005
+ wrap.appendChild(btn);
2006
+ } else {
2007
+ var only = document.createElement("button");
2008
+ only.type = "button";
2009
+ only.className = "test-run";
2010
+ only.textContent = t.name;
2011
+ only.addEventListener("click", function () { requestRunTest(t.id); });
2012
+ wrap.appendChild(only);
2013
+ }
2014
+ return wrap;
2015
+ }
2016
+ function requestRunTest(id, input) {
2017
+ if (!testsReady || running || runningTest) return;
2018
+ if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
2019
+ runningTest = true;
2020
+ setRunBusy(running);
2021
+ setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
2022
+ log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
2023
+ postToHost({ type: "run-test", id: id, input: input });
2024
+ }
2025
+ async function runExportedTest(code, exportName, input, hasInput) {
2026
+ var blob = new Blob([code], { type: "text/javascript" });
2027
+ var url = URL.createObjectURL(blob);
2028
+ try {
2029
+ var mod = await import(url);
2030
+ var fn = mod[exportName];
2031
+ if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
2032
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
2033
+ var ctx = host.getTestContext();
2034
+ var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
2035
+ await Promise.resolve(result);
2036
+ } finally {
2037
+ URL.revokeObjectURL(url);
2038
+ }
2039
+ }
2040
+ function applyRenderPatch() {
2041
+ // 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,
2042
+ // \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
2043
+ var scene = engine && engine.mainScene;
2044
+ if (
2045
+ scene && scene.rootComponent && scene.rootComponent.version &&
2046
+ !scene.postprocessingComponent &&
2047
+ runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
2048
+ ) {
2049
+ 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");
2050
+ scene.registerRenderCallback(function () {
2051
+ scene.renderer.render(scene.sceneObject, scene.camera.main);
2052
+ });
2053
+ }
2054
+ }
2055
+ async function loadHostScene(nextEngine, nextJson) {
2056
+ engine = nextEngine;
2057
+ var sceneManager = engine.getManager(runtime.SceneManager);
2058
+ await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
2059
+ applyRenderPatch();
2060
+ }
2061
+ function readAsset(relPath) {
2062
+ return new Promise(function (resolve, reject) {
2063
+ var id = nextId();
2064
+ pending[id] = { resolve: resolve, reject: reject };
2065
+ postToHost({ type: "read-asset", id: id, path: relPath });
2066
+ });
2067
+ }
2068
+ async function runUserCode(code) {
2069
+ setRunStatus("\u8FD0\u884C\u4E2D\u2026");
2070
+ try {
2071
+ if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
2072
+ await host.run(code);
2073
+ log("Run \u5B8C\u6210");
2074
+ setRunStatus("\u8FD0\u884C\u4E2D");
2075
+ } catch (err) {
2076
+ log("Run \u5931\u8D25 " + formatError(err));
2077
+ setRunStatus("\u5931\u8D25");
2078
+ if (logEl) logEl.classList.add("open");
2079
+ postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
2080
+ } finally {
2081
+ setRunBusy(false);
2082
+ }
2083
+ }
2084
+ document.getElementById("btn-run").addEventListener("click", function () {
2085
+ if (running || runningTest) return;
2086
+ if (!engine) {
2087
+ log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
2088
+ return;
2089
+ }
2090
+ setRunBusy(true);
2091
+ setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
2092
+ log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
2093
+ postToHost({ type: "run" });
2094
+ });
2095
+ document.getElementById("btn-refresh-tests").addEventListener("click", function () {
2096
+ log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
2097
+ postToHost({ type: "list-tests" });
2098
+ });
2099
+ function readEmbeddedTests() {
2100
+ var el = document.getElementById("workspace-tests");
2101
+ if (!el || !el.textContent) return [];
2102
+ try { return JSON.parse(el.textContent); } catch (e) { return []; }
2103
+ }
2104
+ renderTests(readEmbeddedTests());
2105
+ function normalizeHierarchyConfig(vo) {
2106
+ var objs = vo && vo.objs ? vo.objs : [];
2107
+ for (var i = 0; i < objs.length; i++) {
2108
+ var hc = objs[i].hierarchyConfig || {};
2109
+ if (typeof hc.active !== "boolean") {
2110
+ hc.active = hc.inActive === true ? false : hc.visible !== false;
2111
+ }
2112
+ if (typeof hc.lock !== "boolean") hc.lock = false;
2113
+ if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
2114
+ objs[i].hierarchyConfig = hc;
2115
+ }
2116
+ return vo;
2117
+ }
2118
+ function toSceneJson(runtime, raw) {
2119
+ var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
2120
+ if (vo && vo.sceneComponent) {
2121
+ return {
2122
+ id: vo.id || "preview",
2123
+ name: vo.name || "\u573A\u666F\u9884\u89C8",
2124
+ sceneComponent: vo.sceneComponent
2125
+ };
2126
+ }
2127
+ vo = normalizeHierarchyConfig(vo);
2128
+ var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
2129
+ var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
2130
+ return {
2131
+ id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
2132
+ name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
2133
+ sceneComponent: runtime.convertObjToComponentJson(vo)
2134
+ };
2135
+ }
2136
+ try {
2137
+ showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
2138
+ log("runtimeUri=" + __RUNTIME_URI__);
2139
+ log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
2140
+ log("1/4 import twin-runtime / TwinApp host");
2141
+ runtime = await import("@easytwin/runtime");
2142
+ var hostMod = await import(__HOST_URI__);
2143
+ log("2/4 parse scene JSON");
2144
+ var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
2145
+ sceneJson = toSceneJson(runtime, sceneVo);
2146
+ log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
2147
+ log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
2148
+ host = new hostMod.TwinAppPreviewHost({
2149
+ runtime: runtime,
2150
+ containerId: "twin-root",
2151
+ ossUrl: __BASE_OSS_URL__,
2152
+ appId: __APP_ID__,
2153
+ sceneId: sceneJson.id,
2154
+ sceneJson: sceneJson,
2155
+ customComponentDeps: {
2156
+ "@easytwin/runtime": runtime,
2157
+ "@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
2158
+ react: { createElement: function () { return null; }, Fragment: "div" }
2159
+ },
2160
+ loadScene: loadHostScene,
2161
+ readAsset: readAsset,
2162
+ log: log
2163
+ });
2164
+ await host.boot();
2165
+ engine = host.getEngine();
2166
+ log("4/4 loadScene");
2167
+ log("\u5B8C\u6210");
2168
+ hideStatus();
2169
+ testsReady = true;
2170
+ setRunBusy(false);
2171
+ } catch (err) {
2172
+ log("\u5931\u8D25 " + formatError(err));
2173
+ 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);
2174
+ }
2175
+ window.addEventListener("pagehide", function () {
2176
+ if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
2177
+ });
2178
+ `;
2179
+ function originOf(url) {
2180
+ return new URL(url).origin;
2181
+ }
2182
+ function buildPreviewHtml(options) {
2183
+ const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
2184
+ const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
2185
+ const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
2186
+ const appId = options.appId ?? "preview";
2187
+ const hostKind = options.host ?? "vscode";
2188
+ const apiOrigin = originOf(baseUrl);
2189
+ const ossOrigin = originOf(ossUrl);
2190
+ const runtimeOrigin = originOf(runtimeUri);
2191
+ const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
2192
+ 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));
2193
+ const importMap = JSON.stringify({
2194
+ imports: {
2195
+ "@easytwin/runtime": runtimeUri,
2196
+ "@easytwin/apps": appsUri
2197
+ }
2198
+ });
2199
+ const testsJson = JSON.stringify(options.tests ?? []);
2200
+ 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>` : "";
2201
+ return `<!DOCTYPE html>
2202
+ <html lang="zh-CN">
2203
+ <head>
2204
+ <meta charset="UTF-8">
2205
+ <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:;">
2206
+ <title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
2207
+ <style>${PREVIEW_STYLE}</style>
2208
+ <script type="importmap">${importMap}</script>
2209
+ </head>
2210
+ <body>
2211
+ <div id="stage">
2212
+ <div id="twin-root"></div>
2213
+ ${mockBanner}
2214
+ <div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
2215
+ <pre id="debug-log"></pre>
2216
+ </div>
2217
+ <div id="test-panel">
2218
+ <span class="test-label">\u6D4B\u8BD5</span>
2219
+ <div id="test-list"></div>
2220
+ <button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
2221
+ </div>
2222
+ <div id="run-bar">
2223
+ <button type="button" id="btn-run">Run</button>
2224
+ <span id="run-status">\u5C31\u7EEA</span>
2225
+ <span class="spacer"></span>
2226
+ <button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
2227
+ <button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
2228
+ <button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
2229
+ </div>
2230
+ <script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
2231
+ <script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
2232
+ <script type="module">
2233
+ ${renderScript}
2234
+ </script>
2235
+ </body>
2236
+ </html>`;
2237
+ }
2238
+
2239
+ // src/workspaceTests.ts
2240
+ import { promises as fs7 } from "fs";
2241
+ import path8 from "path";
2242
+ var IDENT = "[A-Za-z_$][\\w$]*";
2243
+ var FUNCTION_EXPORT = new RegExp(
2244
+ `export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
2245
+ "g"
2246
+ );
2247
+ var CONST_EXPORT = new RegExp(
2248
+ `export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
2249
+ "g"
2250
+ );
2251
+ function stripTsCommentsAndStringsKeepCode(source) {
2252
+ let out = "";
2253
+ let i = 0;
2254
+ const n = source.length;
2255
+ while (i < n) {
2256
+ const c = source[i];
2257
+ const next = source[i + 1];
2258
+ if (c === "/" && next === "/") {
2259
+ i += 2;
2260
+ while (i < n && source[i] !== "\n") i++;
2261
+ continue;
2262
+ }
2263
+ if (c === "/" && next === "*") {
2264
+ i += 2;
2265
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
2266
+ i = Math.min(n, i + 2);
2267
+ out += " ";
2268
+ continue;
2269
+ }
2270
+ if (c === "'" || c === '"' || c === "`") {
2271
+ const quote = c;
2272
+ out += " ";
2273
+ i++;
2274
+ while (i < n) {
2275
+ const ch = source[i];
2276
+ if (ch === "\\") {
2277
+ i += 2;
2278
+ continue;
2279
+ }
2280
+ if (ch === quote) {
2281
+ i++;
2282
+ break;
2283
+ }
2284
+ i++;
2285
+ }
2286
+ continue;
2287
+ }
2288
+ out += c;
2289
+ i++;
2290
+ }
2291
+ return out;
2292
+ }
2293
+ function matchingClose(src, openIndex, open, close) {
2294
+ let depth = 0;
2295
+ let angle = 0;
2296
+ let brace = 0;
2297
+ for (let i = openIndex; i < src.length; i++) {
2298
+ const c = src[i];
2299
+ if (c === open) depth++;
2300
+ else if (c === close) {
2301
+ depth--;
2302
+ if (depth === 0 && angle <= 0 && brace <= 0) return i;
2303
+ } else if (c === "<") angle++;
2304
+ else if (c === ">" && angle > 0) angle--;
2305
+ else if (c === "{") brace++;
2306
+ else if (c === "}" && brace > 0) brace--;
2307
+ }
2308
+ return -1;
2309
+ }
2310
+ function splitTopLevelParams(list) {
2311
+ const params = [];
2312
+ let current = "";
2313
+ let paren = 0;
2314
+ let angle = 0;
2315
+ let brace = 0;
2316
+ let bracket = 0;
2317
+ for (const c of list) {
2318
+ if (c === "(") paren++;
2319
+ else if (c === ")") paren--;
2320
+ else if (c === "<") angle++;
2321
+ else if (c === ">" && angle > 0) angle--;
2322
+ else if (c === "{") brace++;
2323
+ else if (c === "}" && brace > 0) brace--;
2324
+ else if (c === "[") bracket++;
2325
+ else if (c === "]" && bracket > 0) bracket--;
2326
+ if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
2327
+ if (current.trim()) params.push(current.trim());
2328
+ current = "";
2329
+ continue;
2330
+ }
2331
+ current += c;
2332
+ }
2333
+ if (current.trim()) params.push(current.trim());
2334
+ return params;
2335
+ }
2336
+ function paramName(raw) {
2337
+ let s = raw.trim();
2338
+ if (!s || s === "this") return void 0;
2339
+ if (s.startsWith("{") || s.startsWith("[")) return "input";
2340
+ s = s.replace(/^\.\.\./, "");
2341
+ const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
2342
+ if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
2343
+ return token;
2344
+ }
2345
+ function collectFromPattern(source, pattern) {
2346
+ const found = [];
2347
+ pattern.lastIndex = 0;
2348
+ let match;
2349
+ while (match = pattern.exec(source)) {
2350
+ const name = match[1];
2351
+ if (!name) continue;
2352
+ const openIndex = match.index + match[0].length - 1;
2353
+ const closeIndex = matchingClose(source, openIndex, "(", ")");
2354
+ if (closeIndex < 0) continue;
2355
+ const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
2356
+ const hasInput = params.length >= 2;
2357
+ const second = hasInput ? paramName(params[1] ?? "") : void 0;
2358
+ found.push({
2359
+ id: name,
2360
+ file: "",
2361
+ name,
2362
+ hasInput,
2363
+ inputName: hasInput ? second : void 0
2364
+ });
2365
+ }
2366
+ return found;
2367
+ }
2368
+ function parseExportedTestFunctions(source) {
2369
+ const text = stripTsCommentsAndStringsKeepCode(source);
2370
+ const seen = /* @__PURE__ */ new Set();
2371
+ const out = [];
2372
+ for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
2373
+ if (seen.has(item.name)) continue;
2374
+ seen.add(item.name);
2375
+ out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
2376
+ }
2377
+ return out;
2378
+ }
2379
+ function workspaceTestId(file, name) {
2380
+ return `${normalizePath(file)}:${name}`;
2381
+ }
2382
+ async function listWorkspaceTests(cwd) {
2383
+ const root = path8.resolve(cwd);
2384
+ let files;
2385
+ try {
2386
+ files = await collectFiles(root, isIgnoredWorkspacePath);
2387
+ } catch (err) {
2388
+ const code = err.code;
2389
+ if (code === "ENOENT") return [];
2390
+ throw err;
2391
+ }
2392
+ const tests = [];
2393
+ for (const file of files) {
2394
+ if (!isWorkspaceSpecFile(file.relPath)) continue;
2395
+ const posix = normalizePath(file.relPath);
2396
+ const source = await fs7.readFile(file.absPath, "utf8");
2397
+ for (const fn of parseExportedTestFunctions(source)) {
2398
+ tests.push({
2399
+ id: workspaceTestId(posix, fn.name),
2400
+ file: posix,
2401
+ name: fn.name,
2402
+ hasInput: fn.hasInput,
2403
+ inputName: fn.inputName
2404
+ });
2405
+ }
2406
+ }
2407
+ tests.sort((a, b) => a.id.localeCompare(b.id));
2408
+ return tests;
2409
+ }
2410
+ async function bundleWorkspaceTestFile(options) {
2411
+ const file = normalizePath(options.file);
2412
+ return bundleWorkspaceModule({
2413
+ cwd: options.cwd,
2414
+ entry: file,
2415
+ missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
2416
+ });
2417
+ }
2418
+
2419
+ // src/previewServer.ts
2420
+ var DEFAULT_PREVIEW_PORT = 4173;
2421
+ var DEFAULT_PREVIEW_HOST = "127.0.0.1";
2422
+ var RUNTIME_FILES = /* @__PURE__ */ new Set(["twin-runtime.js", "twin-apps.js", "twin-app-host.js"]);
2423
+ function formatPreviewReadyMessage(info) {
2424
+ const lines = [
2425
+ `EasyTwin \u9884\u89C8: ${info.url}`,
2426
+ `\u573A\u666F: ${info.sceneId}`,
2427
+ "\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"
2428
+ ];
2429
+ 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");
2430
+ return lines.join("\n");
2431
+ }
2432
+ function resolvePreviewSceneId(scenes, requested) {
2433
+ const id = requested?.trim() ?? "";
2434
+ if (id.length > 0) return id;
2435
+ const list = scenes ?? [];
2436
+ const preferred = list.find((s) => s.defaultLoading === true) ?? list[0];
2437
+ if (preferred) return preferred.id;
2438
+ throw new Error("\u672A\u6307\u5B9A\u573A\u666F\u3002\u8BF7\u4F20\u5165 Scene Key,\u6216\u5148\u8FD0\u884C easytwin scene list");
2439
+ }
2440
+ function resolvePreviewRuntimeDir() {
2441
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
2442
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u9884\u89C8 runtime:\u63D2\u4EF6/CJS \u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 runtimeDir");
2443
+ }
2444
+ const here = path9.dirname(fileURLToPath3(import.meta.url));
2445
+ const candidates = [
2446
+ path9.join(here, "runtime"),
2447
+ path9.resolve(here, "..", "dist", "runtime"),
2448
+ path9.resolve(here, "runtime")
2449
+ ];
2450
+ for (const dir of candidates) {
2451
+ if (existsSync2(path9.join(dir, "twin-runtime.js"))) return dir;
2452
+ }
2453
+ throw new Error(
2454
+ `\u672A\u627E\u5230\u9884\u89C8 runtime(twin-runtime.js)\u3002\u5DF2\u5C1D\u8BD5:${candidates.join(", ")}\u3002\u8BF7\u5148\u6784\u5EFA @easytwin/devkit\u3002`
2455
+ );
2456
+ }
2457
+ function mimeFor(file) {
2458
+ if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
2459
+ return "application/octet-stream";
2460
+ }
2461
+ function sendJson(res, body, status = 200) {
2462
+ res.statusCode = status;
2463
+ res.setHeader("content-type", "application/json; charset=utf-8");
2464
+ res.setHeader("cache-control", "no-store");
2465
+ res.end(JSON.stringify(body));
2466
+ }
2467
+ function sendText(res, body, status, contentType) {
2468
+ res.statusCode = status;
2469
+ res.setHeader("content-type", contentType);
2470
+ res.end(body);
2471
+ }
2472
+ async function readJsonBody(req) {
2473
+ const chunks = [];
2474
+ for await (const chunk of req) chunks.push(chunk);
2475
+ if (chunks.length === 0) return {};
2476
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
2477
+ if (raw.length === 0) return {};
2478
+ return JSON.parse(raw);
2479
+ }
2480
+ function asRecord3(value) {
2481
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2482
+ }
2483
+ async function startPreviewServer(options) {
2484
+ const cwd = path9.resolve(options.cwd);
2485
+ const config = await loadConfig(cwd);
2486
+ const fileConfig = await readConfigFile(cwd);
2487
+ const runtimeDir = options.runtimeDir ?? resolvePreviewRuntimeDir();
2488
+ if (!existsSync2(path9.join(runtimeDir, "twin-runtime.js"))) {
2489
+ throw new Error(`\u9884\u89C8 runtime \u76EE\u5F55\u7F3A\u5C11 twin-runtime.js:${runtimeDir}`);
2490
+ }
2491
+ let sceneId;
2492
+ try {
2493
+ sceneId = resolvePreviewSceneId(fileConfig.scenes, options.sceneId);
2494
+ } catch (err) {
2495
+ if (config.mock) {
2496
+ sceneId = deriveExampleSceneId(await loadExampleScene());
2497
+ } else {
2498
+ throw err;
2499
+ }
2500
+ }
2501
+ const client = new EasyTwinClient(config);
2502
+ const scene = await pullScene(client, sceneId, { cwd });
2503
+ const tests = await listWorkspaceTests(cwd);
2504
+ const hostname = options.hostname ?? DEFAULT_PREVIEW_HOST;
2505
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
2506
+ `));
2507
+ let lastBundledCode;
2508
+ const originRef = { url: "" };
2509
+ const html = () => buildPreviewHtml({
2510
+ baseUrl: config.baseUrl,
2511
+ ossUrl: config.ossUrl,
2512
+ sceneJson: JSON.stringify(scene.payload ?? scene, null, 2),
2513
+ mock: config.mock,
2514
+ runtimeUri: `${originRef.url}/runtime/twin-runtime.js`,
2515
+ appsUri: `${originRef.url}/runtime/twin-apps.js`,
2516
+ hostUri: `${originRef.url}/runtime/twin-app-host.js`,
2517
+ appId: config.appId,
2518
+ host: "http",
2519
+ tests
2520
+ });
2521
+ const server = http2.createServer((req, res) => {
2522
+ void handle(req, res);
2523
+ });
2524
+ async function handle(req, res) {
2525
+ try {
2526
+ const url = new URL(req.url ?? "/", originRef.url || `http://${hostname}`);
2527
+ const method = req.method ?? "GET";
2528
+ if (method === "GET" && url.pathname === "/") {
2529
+ sendText(res, html(), 200, "text/html; charset=utf-8");
2530
+ return;
2531
+ }
2532
+ if (method === "GET" && url.pathname.startsWith("/runtime/")) {
2533
+ const name = path9.posix.basename(url.pathname);
2534
+ if (!RUNTIME_FILES.has(name)) {
2535
+ sendText(res, "not found", 404, "text/plain; charset=utf-8");
2536
+ return;
2537
+ }
2538
+ const file = path9.join(runtimeDir, name);
2539
+ const buf = await fs8.readFile(file);
2540
+ res.statusCode = 200;
2541
+ res.setHeader("content-type", mimeFor(name));
2542
+ res.setHeader("cache-control", "no-store");
2543
+ res.end(buf);
2544
+ return;
2545
+ }
2546
+ if (method === "POST" && url.pathname === "/api/bundle") {
2547
+ try {
2548
+ const result = await bundleUserCode({ cwd });
2549
+ lastBundledCode = result.code;
2550
+ for (const warning of result.warnings) log(`[run] \u8B66\u544A ${warning}`);
2551
+ sendJson(res, { ok: true, code: result.code, warnings: result.warnings });
2552
+ } catch (err) {
2553
+ const error = err instanceof Error ? err.message : String(err);
2554
+ log(`[run] \u7F16\u8BD1\u5931\u8D25 ${error}`);
2555
+ sendJson(res, { ok: false, error });
2556
+ }
2557
+ return;
2558
+ }
2559
+ if (method === "GET" && url.pathname === "/api/tests") {
2560
+ sendJson(res, { tests: await listWorkspaceTests(cwd) });
2561
+ return;
2562
+ }
2563
+ if (method === "POST" && url.pathname === "/api/test-bundle") {
2564
+ const body = asRecord3(await readJsonBody(req));
2565
+ const id = typeof body.id === "string" ? body.id : "";
2566
+ const input = typeof body.input === "string" ? body.input : void 0;
2567
+ try {
2568
+ const listed = await listWorkspaceTests(cwd);
2569
+ const item = listed.find((t) => t.id === id);
2570
+ if (!item) throw new Error(`\u672A\u627E\u5230\u6D4B\u8BD5 ${id}`);
2571
+ log(`[test] bundle ${item.file} :: ${item.name}`);
2572
+ const result = await bundleWorkspaceTestFile({ cwd, file: item.file });
2573
+ lastBundledCode = result.code;
2574
+ for (const warning of result.warnings) log(`[test] \u8B66\u544A ${warning}`);
2575
+ sendJson(res, {
2576
+ ok: true,
2577
+ code: result.code,
2578
+ exportName: item.name,
2579
+ input,
2580
+ hasInput: item.hasInput,
2581
+ warnings: result.warnings
2582
+ });
2583
+ } catch (err) {
2584
+ const error = err instanceof Error ? err.message : String(err);
2585
+ log(`[test] \u7F16\u8BD1\u5931\u8D25 ${error}`);
2586
+ sendJson(res, { ok: false, error });
2587
+ }
2588
+ return;
2589
+ }
2590
+ if (method === "GET" && url.pathname === "/api/assets") {
2591
+ const rel = url.searchParams.get("path") ?? "";
2592
+ try {
2593
+ const file = resolveWorkspaceAssetPath(cwd, rel);
2594
+ sendJson(res, { text: await fs8.readFile(file, "utf8") });
2595
+ } catch (err) {
2596
+ const error = err instanceof Error ? err.message : String(err);
2597
+ log(`[assets] FAIL ${rel} ${error}`);
2598
+ sendJson(res, { error });
2599
+ }
2600
+ return;
2601
+ }
2602
+ if (method === "POST" && url.pathname === "/api/remap-error") {
2603
+ const body = asRecord3(await readJsonBody(req));
2604
+ const stack = typeof body.stack === "string" ? body.stack : "";
2605
+ sendJson(res, { stack: lastBundledCode ? remapErrorStack(stack, lastBundledCode) : stack });
2606
+ return;
2607
+ }
2608
+ sendText(res, "not found", 404, "text/plain; charset=utf-8");
2609
+ } catch (err) {
2610
+ const error = err instanceof Error ? err.message : String(err);
2611
+ log(`[preview] ${error}`);
2612
+ if (!res.headersSent) sendJson(res, { error }, 500);
2613
+ }
2614
+ }
2615
+ const port = options.port ?? DEFAULT_PREVIEW_PORT;
2616
+ await new Promise((resolve, reject) => {
2617
+ server.once("error", reject);
2618
+ server.listen(port, hostname, () => resolve());
2619
+ });
2620
+ const address = server.address();
2621
+ const bound = typeof address === "object" && address !== null ? address.port : port;
2622
+ originRef.url = `http://${hostname}:${bound}`;
2623
+ return {
2624
+ url: originRef.url,
2625
+ port: bound,
2626
+ sceneId,
2627
+ mock: config.mock,
2628
+ close: () => new Promise((resolve, reject) => {
2629
+ server.close((err) => err ? reject(err) : resolve());
2630
+ })
2631
+ };
2632
+ }
1339
2633
 
1340
2634
  // src/bin.ts
1341
2635
  async function readVersion() {
@@ -1372,16 +2666,17 @@ async function runInit(cwd, flags) {
1372
2666
  if (baseUrlInput.trim().length > 0) input.baseUrl = baseUrlInput.trim();
1373
2667
  const result = await initConfig(input, cwd);
1374
2668
  console.log(`\u5DF2\u751F\u6210 ${result.configFile}`);
2669
+ const gitignoreHint = GITIGNORE_ENTRIES.join("\u3001");
1375
2670
  console.log(
1376
- result.gitignore.added ? `\u5DF2\u628A ${CONFIG_FILE_NAME} \u8FFD\u52A0\u8FDB .gitignore(\u9632\u6B62\u5BC6\u94A5\u5165\u5E93)` : `.gitignore \u5DF2\u5305\u542B ${CONFIG_FILE_NAME}`
2671
+ result.gitignore.added ? `\u5DF2\u628A ${gitignoreHint} \u8FFD\u52A0\u8FDB .gitignore(\u9632\u6B62\u5BC6\u94A5\u5165\u5E93)` : `.gitignore \u5DF2\u5305\u542B ${gitignoreHint}`
1377
2672
  );
1378
2673
  }
1379
2674
  function buildProgram(cwd, version = "0.0.0") {
1380
2675
  const program = new Command();
1381
2676
  program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
1382
- program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--env <prod|test>", "\u670D\u52A1\u73AF\u5883(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
2677
+ program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 .easytwin/easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--env <prod|test>", "\u670D\u52A1\u73AF\u5883(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
1383
2678
  const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
1384
- scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
2679
+ scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 .easytwin/easytwin.config.json").action(async () => {
1385
2680
  const config = await loadConfig(cwd);
1386
2681
  if (config.mock) console.log(MOCK_MODE_HINT);
1387
2682
  const client = new EasyTwinClient(config);
@@ -1391,30 +2686,35 @@ function buildProgram(cwd, version = "0.0.0") {
1391
2686
  } else {
1392
2687
  for (const s of scenes) console.log(`${s.id} ${s.name}`);
1393
2688
  }
1394
- console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
2689
+ console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${EASYTWIN_DIR}/${CONFIG_FILE_NAME}`);
1395
2690
  });
1396
- scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ./<id>.scene.json)").action(async (id, opts) => {
2691
+ scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u989D\u5916\u8F93\u51FA\u8DEF\u5F84(\u4ECD\u4F1A\u5199\u5165 .easytwin/scenes/<id>.scene.json)").action(async (id, opts) => {
1397
2692
  const config = await loadConfig(cwd);
1398
2693
  if (config.mock) console.log(MOCK_MODE_HINT);
1399
2694
  const client = new EasyTwinClient(config);
1400
- const scene2 = await pullScene(client, id);
1401
- const out = opts.out ?? path8.join(cwd, `${id}.scene.json`);
1402
- const file = await saveSceneJson(scene2, out);
2695
+ const scene2 = await pullScene(client, id, { cwd });
2696
+ const out = opts.out;
2697
+ const file = out ? await saveSceneJson(scene2, out) : defaultSceneOutPath(cwd, scene2.id);
1403
2698
  console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
1404
2699
  });
2700
+ scene.command("inspect <id>").description("\u6253\u5370\u672C\u5730\u573A\u666F\u5BF9\u8C61\u6811(id/name/type),\u4E0D\u8BFB\u5B8C\u6574 JSON").option("--name <substr>", "\u6309\u540D\u79F0\u5B50\u4E32\u8FC7\u6EE4(\u4E0D\u533A\u5206\u5927\u5C0F\u5199)").option("--type <type>", "\u6309\u7EC4\u4EF6 type \u7CBE\u786E\u8FC7\u6EE4").action(async (id, opts) => {
2701
+ const result = await inspectScene({ cwd, id, name: opts.name, type: opts.type });
2702
+ process.stdout.write(result.text);
2703
+ });
1405
2704
  program.command("pull").description("\u62C9\u53D6\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5219\u5199\u5165\u9ED8\u8BA4 src/main.ts;\u51B2\u7A81\u9ED8\u8BA4\u4E0D\u8986\u76D6)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").option("--force", "\u7528\u8FDC\u7AEF\u5185\u5BB9\u8986\u76D6\u672C\u5730\u51B2\u7A81\u6587\u4EF6").option("--dry-run", "\u53EA\u6253\u5370 diff,\u4E0D\u5199\u76D8").action(async (dirArg, opts) => {
1406
2705
  const config = await loadConfig(cwd);
1407
2706
  if (config.mock) console.log(MOCK_MODE_HINT);
1408
2707
  const client = new EasyTwinClient(config);
1409
- const target = dirArg ? path8.resolve(cwd, dirArg) : cwd;
2708
+ const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
1410
2709
  const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
1411
2710
  console.log(formatWorkspacePullResult(result));
1412
2711
  });
1413
- program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
2712
+ 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
2713
  const config = await loadConfig(cwd);
1415
2714
  if (config.mock) console.log(MOCK_MODE_HINT);
1416
2715
  const client = new EasyTwinClient(config);
1417
- await uploadDirectory(client, dir, {
2716
+ const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
2717
+ const result = await uploadDirectory(client, target, {
1418
2718
  onProgress: (p) => {
1419
2719
  if (p.phase === "upload") {
1420
2720
  process.stderr.write(`\r\u5DF2\u4E0A\u4F20 ${p.current}/${p.total} bytes${p.file ? ` (${p.file})` : ""}`);
@@ -1422,15 +2722,29 @@ function buildProgram(cwd, version = "0.0.0") {
1422
2722
  }
1423
2723
  });
1424
2724
  process.stderr.write("\n");
2725
+ console.log(formatUploadResult(result));
1425
2726
  });
1426
2727
  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
2728
  const hint = await typesMissingHint(cwd);
1428
2729
  if (hint) console.warn(hint);
1429
- const outFile = path8.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
2730
+ const outFile = path10.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
1430
2731
  const result = await bundleUserCode({ cwd, outFile });
1431
2732
  for (const warning of result.warnings) console.warn(warning);
1432
2733
  console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
1433
2734
  });
2735
+ 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) => {
2736
+ const port = Number.parseInt(opts.port, 10);
2737
+ if (!Number.isFinite(port) || port < 0) throw new Error(`\u65E0\u6548\u7AEF\u53E3:${opts.port}`);
2738
+ const server = await startPreviewServer({ cwd, sceneId, port });
2739
+ console.log(formatPreviewReadyMessage(server));
2740
+ await new Promise((resolve, reject) => {
2741
+ const stop = () => {
2742
+ server.close().then(resolve, reject);
2743
+ };
2744
+ process.once("SIGINT", stop);
2745
+ process.once("SIGTERM", stop);
2746
+ });
2747
+ });
1434
2748
  const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
1435
2749
  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
2750
  const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
@@ -1464,4 +2778,3 @@ export {
1464
2778
  main,
1465
2779
  runInit
1466
2780
  };
1467
- //# sourceMappingURL=bin.js.map