@easytwin/devkit 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,17 @@
2
2
  import { promises as fs } from "fs";
3
3
  import path from "path";
4
4
  var CONFIG_FILE_NAME = "easytwin.config.json";
5
- var DEFAULT_BASE_URL = "https://api.easytwin.example.com";
5
+ var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
6
+ var TEST_BASE_URL = "http://172.16.125.3:10100/";
7
+ var TEST_OP_ACCOUNT_ID = "25";
8
+ var TEST_OP_USER_ID = "25";
9
+ var TEST_SPACE_ID = "54";
10
+ var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
11
+ var MOCK_APP_ID = "test";
12
+ var MOCK_APP_SECRET = "test";
13
+ function isMockCredentials(appId, appSecret) {
14
+ return appId === MOCK_APP_ID && appSecret === MOCK_APP_SECRET;
15
+ }
6
16
  var ConfigError = class extends Error {
7
17
  constructor(message) {
8
18
  super(message);
@@ -35,10 +45,65 @@ function validateConfigShape(value) {
35
45
  if (v.baseUrl !== void 0 && (typeof v.baseUrl !== "string" || v.baseUrl.length === 0)) {
36
46
  throw new ConfigError("baseUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
37
47
  }
48
+ if (v.ossUrl !== void 0 && (typeof v.ossUrl !== "string" || v.ossUrl.length === 0)) {
49
+ throw new ConfigError("ossUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
50
+ }
51
+ if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
52
+ throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
53
+ }
54
+ const opAccountId = optionalGatewayId(v.opAccountId);
55
+ const opUserId = optionalGatewayId(v.opUserId);
56
+ const spaceId = optionalGatewayId(v.spaceId);
38
57
  const config = { appId: v.appId, appSecret: v.appSecret };
58
+ if (v.env === "prod" || v.env === "test") config.env = v.env;
39
59
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
60
+ if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
61
+ if (opAccountId) config.opAccountId = opAccountId;
62
+ if (opUserId) config.opUserId = opUserId;
63
+ if (spaceId) config.spaceId = spaceId;
64
+ const scenes = parseConfigScenes(v.scenes);
65
+ if (scenes) config.scenes = scenes;
40
66
  return config;
41
67
  }
68
+ function optionalGatewayId(value) {
69
+ if (typeof value === "string") {
70
+ const t = value.trim();
71
+ return t.length > 0 ? t : void 0;
72
+ }
73
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
74
+ if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
75
+ return void 0;
76
+ }
77
+ function parseConfigScenes(value) {
78
+ if (value === void 0) return void 0;
79
+ if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
80
+ return value.map((item, i) => {
81
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
82
+ throw new ConfigError(`scenes[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
83
+ }
84
+ const it = item;
85
+ if (typeof it.id !== "string" || it.id.length === 0) {
86
+ throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 id`);
87
+ }
88
+ if (typeof it.name !== "string" || it.name.length === 0) {
89
+ throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 name`);
90
+ }
91
+ const scene = { id: it.id, name: it.name };
92
+ if (it.linkedSceneId !== void 0) {
93
+ if (typeof it.linkedSceneId !== "string") throw new ConfigError(`scenes[${i}].linkedSceneId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
94
+ if (it.linkedSceneId.length > 0) scene.linkedSceneId = it.linkedSceneId;
95
+ }
96
+ if (it.snapshotUrl !== void 0) {
97
+ if (typeof it.snapshotUrl !== "string") throw new ConfigError(`scenes[${i}].snapshotUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
98
+ if (it.snapshotUrl.length > 0) scene.snapshotUrl = it.snapshotUrl;
99
+ }
100
+ if (it.defaultLoading !== void 0) {
101
+ if (typeof it.defaultLoading !== "boolean") throw new ConfigError(`scenes[${i}].defaultLoading \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
102
+ scene.defaultLoading = it.defaultLoading;
103
+ }
104
+ return scene;
105
+ });
106
+ }
42
107
  async function readConfigFile(cwd) {
43
108
  const file = configFilePath(cwd);
44
109
  let raw;
@@ -52,19 +117,50 @@ async function readConfigFile(cwd) {
52
117
  function resolveConfig(file, env = process.env) {
53
118
  const appId = env.EASYTWIN_APP_ID ?? file.appId;
54
119
  const appSecret = env.EASYTWIN_APP_SECRET ?? file.appSecret;
55
- const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? DEFAULT_BASE_URL;
120
+ const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file.env ?? "prod";
121
+ const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? (easyEnv === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL);
122
+ const ossUrl = env.EASYTWIN_OSS_URL ?? file.ossUrl ?? DEFAULT_OSS_URL;
56
123
  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");
57
124
  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");
58
125
  const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
59
- return { appId, appSecret, baseUrl, source };
126
+ const useTestGateway = easyEnv === "test";
127
+ const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
128
+ const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
129
+ const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
130
+ const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
131
+ if (opAccountId) resolved.opAccountId = opAccountId;
132
+ if (opUserId) resolved.opUserId = opUserId;
133
+ if (spaceId) resolved.spaceId = spaceId;
134
+ return resolved;
60
135
  }
61
136
  async function loadConfig(cwd, env = process.env) {
62
- return resolveConfig(await readConfigFile(cwd), env);
137
+ const file = await readConfigFile(cwd);
138
+ const resolved = resolveConfig(file, env);
139
+ if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
140
+ if (!file.opAccountId || !file.opUserId || !file.spaceId) {
141
+ await writeConfigFile(cwd, {
142
+ ...file,
143
+ opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
144
+ opUserId: file.opUserId ?? TEST_OP_USER_ID,
145
+ spaceId: file.spaceId ?? TEST_SPACE_ID
146
+ });
147
+ }
148
+ }
149
+ return resolved;
63
150
  }
64
151
  async function writeConfigFile(cwd, config) {
65
152
  const file = configFilePath(cwd);
66
153
  const body = { appId: config.appId, appSecret: config.appSecret };
154
+ if (config.env) body.env = config.env;
67
155
  if (config.baseUrl) body.baseUrl = config.baseUrl;
156
+ if (config.ossUrl) body.ossUrl = config.ossUrl;
157
+ const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
158
+ const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
159
+ const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
160
+ if (opAccountId) body.opAccountId = opAccountId;
161
+ if (opUserId) body.opUserId = opUserId;
162
+ if (spaceId) body.spaceId = spaceId;
163
+ if (config.scenes !== void 0) body.scenes = config.scenes;
68
164
  await fs.mkdir(cwd, { recursive: true });
69
165
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
70
166
  return file;
@@ -92,18 +188,31 @@ async function initConfig(input, cwd) {
92
188
  const gitignore = await appendGitignore(cwd);
93
189
  return { configFile, gitignore };
94
190
  }
191
+ async function writeConfigScenes(cwd, scenes) {
192
+ const file = await readConfigFile(cwd);
193
+ await writeConfigFile(cwd, { ...file, scenes });
194
+ }
95
195
 
96
196
  // src/client.ts
97
197
  import http from "http";
98
198
  import https from "https";
99
- import { URL } from "url";
100
- var AUTH_HEADER = "Authorization";
101
- var BEARER_PREFIX = "Bearer";
102
- var APP_ID_HEADER = "X-Easytwin-App-Id";
199
+ import { URL as URL2 } from "url";
200
+ var AUTH_HEADER = "x-app-secret";
201
+ var OP_ACCOUNT_ID_HEADER = "op-account-id";
202
+ var OP_USER_ID_HEADER = "op-user-id";
203
+ var SPACE_ID_HEADER = "space-id";
204
+ function enc(id) {
205
+ return encodeURIComponent(id);
206
+ }
103
207
  var ENDPOINTS = {
104
- scenes: "/api/scenes",
105
- scene: (id) => `/api/scenes/${encodeURIComponent(id)}`,
106
- upload: "/api/upload"
208
+ /** GET 已关联场景列表。 */
209
+ linkedScenes: (applicationId) => `/api/twin/v1/sdk-application-scenes/${enc(applicationId)}/scenes`,
210
+ /** GET 工作区全部代码文件。 */
211
+ workspaceCode: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}`,
212
+ /** GET 工作区文件约束。 */
213
+ workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`,
214
+ /** POST 新建 / PATCH 批量更新 / DELETE 批量删除。 */
215
+ workspaceCodeFiles: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/files`
107
216
  };
108
217
  var EasyTwinApiError = class extends Error {
109
218
  status;
@@ -131,25 +240,50 @@ function parseResponseBody(buffer) {
131
240
  return text;
132
241
  }
133
242
  }
243
+ function isEnvelope(value) {
244
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "success" in value;
245
+ }
246
+ function unwrapBody(parsed, status) {
247
+ if (!isEnvelope(parsed)) return parsed;
248
+ if (parsed.success === false) {
249
+ throw new EasyTwinApiError(status, messageFromBody(parsed) ?? "\u8BF7\u6C42\u5931\u8D25", parsed);
250
+ }
251
+ if (parsed.success === true && "data" in parsed) return parsed.data;
252
+ return parsed;
253
+ }
134
254
  var EasyTwinClient = class {
135
255
  baseUrl;
256
+ /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
257
+ ossUrl;
258
+ /** 接入凭证 App ID;同时作为场景/代码 API 的 applicationId 路径参数。 */
136
259
  appId;
260
+ /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
261
+ mock;
137
262
  appSecret;
263
+ opAccountId;
264
+ opUserId;
265
+ spaceId;
138
266
  constructor(config) {
139
267
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
268
+ this.ossUrl = config.ossUrl.replace(/\/+$/, "");
269
+ this.mock = config.mock;
140
270
  this.appId = config.appId;
141
271
  this.appSecret = config.appSecret;
272
+ this.opAccountId = config.opAccountId;
273
+ this.opUserId = config.opUserId;
274
+ this.spaceId = config.spaceId;
142
275
  }
143
276
  /** 全仓唯一认证头注入点。 */
144
277
  authHeaders() {
145
- return {
146
- [AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
147
- [APP_ID_HEADER]: this.appId
148
- };
278
+ const headers = { [AUTH_HEADER]: this.appSecret };
279
+ if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
280
+ if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
281
+ if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
282
+ return headers;
149
283
  }
150
284
  /** JSON 请求(原生 fetch)。 */
151
- async request(path5, options = {}) {
152
- const url = `${this.baseUrl}${path5}`;
285
+ async request(path8, options = {}) {
286
+ const url = `${this.baseUrl}${path8}`;
153
287
  const headers = { ...this.authHeaders(), ...options.headers };
154
288
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
155
289
  headers["Content-Type"] = "application/json";
@@ -171,8 +305,8 @@ var EasyTwinClient = class {
171
305
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
172
306
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
173
307
  */
174
- async upload(path5, options) {
175
- const url = new URL(`${this.baseUrl}${path5}`);
308
+ async upload(path8, options) {
309
+ const url = new URL2(`${this.baseUrl}${path8}`);
176
310
  const mod = url.protocol === "https:" ? https : http;
177
311
  const headers = {
178
312
  ...this.authHeaders(),
@@ -205,8 +339,8 @@ var EasyTwinClient = class {
205
339
  return this.handleStatus(raw.status, raw.body);
206
340
  }
207
341
  handleStatus(status, body) {
208
- if (status >= 200 && status < 300) return parseResponseBody(body);
209
342
  const parsed = parseResponseBody(body);
343
+ if (status >= 200 && status < 300) return unwrapBody(parsed, status);
210
344
  throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
211
345
  }
212
346
  };
@@ -214,31 +348,202 @@ var EasyTwinClient = class {
214
348
  // src/scene.ts
215
349
  import { promises as fs2 } from "fs";
216
350
  import path2 from "path";
217
- function normalizeSceneList(data) {
218
- const list = Array.isArray(data) ? data : data?.list;
219
- if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
351
+ import { fileURLToPath } from "url";
352
+ function asRecord(value) {
353
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
354
+ }
355
+ function asString(value) {
356
+ return typeof value === "string" ? value : "";
357
+ }
358
+ function asBoolean(value) {
359
+ return value === true;
360
+ }
361
+ function extractSceneArray(data) {
362
+ if (Array.isArray(data)) return data;
363
+ const root = asRecord(data);
364
+ if (Array.isArray(root.data)) return root.data;
365
+ if (Array.isArray(root.list)) return root.list;
366
+ if (Array.isArray(root.scenes)) return root.scenes;
367
+ const nested = asRecord(root.data);
368
+ if (Array.isArray(nested.scenes)) return nested.scenes;
369
+ if (Array.isArray(nested.list)) return nested.list;
370
+ throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
371
+ }
372
+ function normalizeLinkedScenes(data) {
373
+ const list = extractSceneArray(data);
220
374
  return list.map((item) => {
221
- const it = item ?? {};
222
- return { id: String(it.id ?? ""), name: String(it.name ?? it.id ?? "") };
375
+ const it = asRecord(item);
376
+ const sceneKey = asString(it.sceneKey);
377
+ const sourceProjectName = asString(it.sourceProjectName);
378
+ const sourceSceneName = asString(it.sourceSceneName);
379
+ return {
380
+ id: asString(it.id),
381
+ sceneKey,
382
+ name: sourceProjectName || sourceSceneName || sceneKey,
383
+ defaultLoading: asBoolean(it.defaultLoading),
384
+ sourceSceneId: asString(it.sourceSceneId),
385
+ sourceProjectId: asString(it.sourceProjectId),
386
+ sourceSceneName,
387
+ sourceProjectName,
388
+ sourceLost: asBoolean(it.sourceLost),
389
+ snapshotUrl: asString(it.snapshotUrl),
390
+ snapshotUpdatedAt: asString(it.snapshotUpdatedAt)
391
+ };
223
392
  });
224
393
  }
225
- function normalizeSceneDetail(data) {
226
- const it = data ?? {};
227
- return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: data };
394
+ function normalizeSceneList(data) {
395
+ return normalizeLinkedScenes(data).map((s) => ({
396
+ id: s.sceneKey,
397
+ name: s.name,
398
+ linkedSceneId: s.id,
399
+ snapshotUrl: s.snapshotUrl,
400
+ defaultLoading: s.defaultLoading
401
+ }));
228
402
  }
229
- async function listScenes(client) {
230
- const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
231
- return normalizeSceneList(data);
403
+ function toConfigScenes(scenes) {
404
+ return scenes.map((s) => {
405
+ const item = { id: s.id, name: s.name };
406
+ if (s.linkedSceneId) item.linkedSceneId = s.linkedSceneId;
407
+ if (s.snapshotUrl) item.snapshotUrl = s.snapshotUrl;
408
+ if (s.defaultLoading !== void 0) item.defaultLoading = s.defaultLoading;
409
+ return item;
410
+ });
232
411
  }
233
- async function pullScene(client, id) {
234
- const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
235
- return normalizeSceneDetail(data);
412
+ function resolveSnapshotUrl(ossUrl, snapshotUrl) {
413
+ if (/^https?:\/\//i.test(snapshotUrl)) return snapshotUrl;
414
+ const base = ossUrl.replace(/\/+$/, "");
415
+ const rel = snapshotUrl.replace(/^\/+/, "");
416
+ return `${base}/${rel}`;
417
+ }
418
+ async function fetchSnapshotJson(ossUrl, snapshotUrl) {
419
+ if (snapshotUrl.length === 0) throw new Error("\u573A\u666F\u5FEB\u7167\u5730\u5740\u4E3A\u7A7A");
420
+ const url = resolveSnapshotUrl(ossUrl, snapshotUrl);
421
+ let res;
422
+ try {
423
+ res = await fetch(url);
424
+ } catch (err) {
425
+ throw new EasyTwinApiError(0, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:${url}:${err instanceof Error ? err.message : String(err)}`);
426
+ }
427
+ if (!res.ok) {
428
+ throw new EasyTwinApiError(res.status, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:HTTP ${res.status} ${url}`);
429
+ }
430
+ try {
431
+ return await res.json();
432
+ } catch {
433
+ throw new Error(`\u573A\u666F\u5FEB\u7167\u4E0D\u662F\u5408\u6CD5 JSON:${url}`);
434
+ }
435
+ }
436
+ var EXAMPLE_SCENE_FILE = "scene.example.json";
437
+ var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
438
+ function resolveExampleScenePath() {
439
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
440
+ throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D ${EXAMPLE_SCENE_FILE}:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 exampleFile`);
441
+ }
442
+ return path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..", EXAMPLE_SCENE_FILE);
443
+ }
444
+ async function loadExampleScene(exampleFile) {
445
+ const file = exampleFile ?? resolveExampleScenePath();
446
+ let raw;
447
+ try {
448
+ raw = await fs2.readFile(file, "utf8");
449
+ } catch {
450
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730\u793A\u4F8B\u573A\u666F ${file}(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F\u9700\u8981 devkit \u5305\u5185\u7684 ${EXAMPLE_SCENE_FILE})`);
451
+ }
452
+ try {
453
+ return JSON.parse(raw);
454
+ } catch {
455
+ throw new Error(`\u672C\u5730\u793A\u4F8B\u573A\u666F\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
456
+ }
457
+ }
458
+ function deriveExampleSceneId(payload) {
459
+ const root = payload ?? {};
460
+ const id = root?.objs?.map((o) => o.sceneId).find((s) => typeof s === "string" && s.length > 0);
461
+ return id ?? "local";
462
+ }
463
+ async function exampleSceneSummary(exampleFile) {
464
+ return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
465
+ }
466
+ async function listScenes(client, options = {}) {
467
+ const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" }));
468
+ if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
469
+ return scenes;
470
+ }
471
+ async function pullScene(client, id, options = {}) {
472
+ if (client.mock) {
473
+ const payload2 = await loadExampleScene(options.exampleFile);
474
+ const sceneId = deriveExampleSceneId(payload2);
475
+ if (sceneId !== id) {
476
+ 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)`);
477
+ }
478
+ return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
479
+ }
480
+ const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
481
+ const scenes = normalizeLinkedScenes(data);
482
+ const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
483
+ if (!hit) {
484
+ const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
485
+ throw new Error(
486
+ available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
487
+ );
488
+ }
489
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
490
+ return { id: hit.sceneKey, name: hit.name, payload };
236
491
  }
237
492
  async function saveSceneJson(scene, out) {
238
493
  await fs2.mkdir(path2.dirname(out), { recursive: true });
239
494
  await fs2.writeFile(out, JSON.stringify(scene.payload ?? scene, null, 2) + "\n", "utf8");
240
495
  return out;
241
496
  }
497
+ function parseSceneStructure(scene) {
498
+ let data = scene;
499
+ if (typeof data === "string") {
500
+ try {
501
+ data = JSON.parse(data);
502
+ } catch {
503
+ return [];
504
+ }
505
+ }
506
+ const root = data ?? {};
507
+ const objs = Array.isArray(root.objs) ? root.objs : [];
508
+ const hierarchy = Array.isArray(root.hierarchy) ? root.hierarchy : [];
509
+ const items = objs.length > 0 ? objs : hierarchy;
510
+ const typeById = /* @__PURE__ */ new Map();
511
+ for (const o of objs) {
512
+ if (typeof o.id === "string" && typeof o.type === "string") typeById.set(o.id, o.type);
513
+ }
514
+ const nodes = /* @__PURE__ */ new Map();
515
+ const order = [];
516
+ for (const item of items) {
517
+ const id = typeof item.id === "string" ? item.id : "";
518
+ if (id.length === 0 || nodes.has(id)) continue;
519
+ const name = typeof item.name === "string" && item.name.length > 0 ? item.name : id;
520
+ nodes.set(id, { id, name, type: typeById.get(id), children: [] });
521
+ order.push(id);
522
+ }
523
+ const childrenOf = /* @__PURE__ */ new Map();
524
+ for (const item of items) {
525
+ const id = typeof item.id === "string" ? item.id : "";
526
+ if (id.length === 0 || !nodes.has(id)) continue;
527
+ const parentId = typeof item.parentObjId === "string" && item.parentObjId.length > 0 ? item.parentObjId : "";
528
+ if (parentId.length === 0 || parentId === id || !nodes.has(parentId)) continue;
529
+ const kids = childrenOf.get(parentId) ?? [];
530
+ kids.push(id);
531
+ childrenOf.set(parentId, kids);
532
+ }
533
+ const isChild = /* @__PURE__ */ new Set();
534
+ for (const kids of childrenOf.values()) for (const k of kids) isChild.add(k);
535
+ const build2 = (id, seen) => {
536
+ const node = nodes.get(id);
537
+ const children = [];
538
+ if (!seen.has(id)) {
539
+ seen.add(id);
540
+ for (const kid of childrenOf.get(id) ?? []) children.push(build2(kid, seen));
541
+ seen.delete(id);
542
+ }
543
+ return { ...node, children };
544
+ };
545
+ return order.filter((id) => !isChild.has(id)).map((id) => build2(id, /* @__PURE__ */ new Set()));
546
+ }
242
547
 
243
548
  // src/upload.ts
244
549
  import { promises as fs3 } from "fs";
@@ -292,32 +597,395 @@ Content-Type: application/octet-stream\r
292
597
  `, "utf8"));
293
598
  return Buffer.concat(parts);
294
599
  }
600
+ function asRecord2(value) {
601
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
602
+ }
603
+ function asString2(value) {
604
+ return typeof value === "string" ? value : "";
605
+ }
606
+ function asNumber(value) {
607
+ return typeof value === "number" && Number.isFinite(value) ? value : NaN;
608
+ }
609
+ function normalizePath(relPath) {
610
+ return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
611
+ }
612
+ function normalizeWorkspaceConfig(data) {
613
+ const it = asRecord2(data);
614
+ if (!Array.isArray(it.allowedFileExtensions)) throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
615
+ const maxCodeFiles = asNumber(it.maxCodeFiles);
616
+ const maxCodeDirectoryDepth = asNumber(it.maxCodeDirectoryDepth);
617
+ if (!Number.isFinite(maxCodeFiles) || !Number.isFinite(maxCodeDirectoryDepth)) {
618
+ throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
619
+ }
620
+ return {
621
+ allowedFileExtensions: it.allowedFileExtensions.filter((e) => typeof e === "string"),
622
+ maxCodeFiles,
623
+ maxCodeDirectoryDepth
624
+ };
625
+ }
626
+ function normalizeWorkspaceFiles(data) {
627
+ if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
628
+ return data.map((item) => {
629
+ const it = asRecord2(item);
630
+ return { id: asString2(it.id), filePath: asString2(it.filePath), content: asString2(it.content) };
631
+ });
632
+ }
633
+ function extensionOf(relPath) {
634
+ const base = relPath.split("/").pop() ?? "";
635
+ const i = base.lastIndexOf(".");
636
+ if (i <= 0) return "";
637
+ return base.slice(i).toLowerCase();
638
+ }
639
+ function allowedExtensionSet(list) {
640
+ return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
641
+ }
642
+ function directoryDepth(relPath) {
643
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
644
+ return Math.max(0, parts.length - 1);
645
+ }
646
+ function assertUploadable(files, config) {
647
+ const errors = [];
648
+ if (files.length > config.maxCodeFiles) {
649
+ errors.push(`\u6587\u4EF6\u6570 ${files.length} \u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeFiles}`);
650
+ }
651
+ const tooDeep = files.filter((f) => directoryDepth(f.relPath) > config.maxCodeDirectoryDepth);
652
+ if (tooDeep.length > 0) {
653
+ errors.push(
654
+ `\u76EE\u5F55\u6DF1\u5EA6\u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeDirectoryDepth}:${tooDeep.map((f) => f.relPath).join(", ")}`
655
+ );
656
+ }
657
+ if (config.allowedFileExtensions.length > 0) {
658
+ const allowed = allowedExtensionSet(config.allowedFileExtensions);
659
+ const bad = files.filter((f) => !allowed.has(extensionOf(f.relPath)));
660
+ if (bad.length > 0) {
661
+ errors.push(
662
+ `\u6269\u5C55\u540D\u4E0D\u5728\u5141\u8BB8\u5217\u8868(${config.allowedFileExtensions.join(", ")}):${bad.map((f) => f.relPath).join(", ")}`
663
+ );
664
+ }
665
+ }
666
+ if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
667
+ }
668
+ async function mockUpload(dir, options) {
669
+ const ignore = options.ignore ?? (() => false);
670
+ const files = await collectFiles(dir, ignore);
671
+ const total = files.reduce((sum, f) => sum + f.size, 0);
672
+ options.onProgress?.({ phase: "collect", current: total, total });
673
+ options.onProgress?.({ phase: "upload", current: total, total });
674
+ return { fileCount: files.length, byteCount: total, mock: true };
675
+ }
295
676
  async function uploadDirectory(client, dir, options = {}) {
677
+ if (client.mock) return mockUpload(dir, options);
296
678
  const ignore = options.ignore ?? (() => false);
297
679
  const files = await collectFiles(dir, ignore);
298
680
  const total = files.reduce((sum, f) => sum + f.size, 0);
299
681
  options.onProgress?.({ phase: "collect", current: total, total });
300
- let sent = 0;
682
+ const appId = client.appId;
683
+ const wsConfig = normalizeWorkspaceConfig(await client.request(ENDPOINTS.workspaceConfig(appId), { method: "GET" }));
684
+ assertUploadable(files, wsConfig);
685
+ const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
686
+ const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
687
+ const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
688
+ const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
689
+ if (toDelete.length > 0) {
690
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
691
+ method: "DELETE",
692
+ body: JSON.stringify({ ids: toDelete })
693
+ });
694
+ }
695
+ const creates = [];
696
+ const patches = [];
697
+ const unchanged = [];
301
698
  for (const file of files) {
302
- const content = await fs3.readFile(file.absPath);
303
- const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
304
- const body = buildMultipartBody(
305
- boundary,
306
- // TODO(swagger): multipart 字段名/结构待定,当前为占位约定。
307
- { path: file.relPath },
308
- { name: path3.basename(file.relPath), content }
309
- );
310
- await client.upload(ENDPOINTS.upload, { boundary, body });
699
+ const content = await fs3.readFile(file.absPath, "utf8");
700
+ const remoteFile = remoteByPath.get(normalizePath(file.relPath));
701
+ if (!remoteFile) creates.push({ file, content });
702
+ else if (remoteFile.content !== content) patches.push({ file, id: remoteFile.id, content });
703
+ else unchanged.push(file);
704
+ }
705
+ let sent = 0;
706
+ const bump = (file) => {
311
707
  sent += file.size;
312
708
  options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
709
+ };
710
+ if (patches.length > 0) {
711
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
712
+ method: "PATCH",
713
+ body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
714
+ });
715
+ for (const p of patches) bump(p.file);
716
+ }
717
+ for (const c of creates) {
718
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
719
+ method: "POST",
720
+ body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
721
+ });
722
+ bump(c.file);
313
723
  }
314
- return { fileCount: files.length, byteCount: sent };
724
+ for (const u of unchanged) bump(u);
725
+ if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
726
+ return { fileCount: files.length, byteCount: total };
315
727
  }
316
728
 
317
- // src/skills.ts
729
+ // src/workspace.ts
318
730
  import { promises as fs4 } from "fs";
731
+ import path5 from "path";
732
+
733
+ // src/assetsPath.ts
319
734
  import path4 from "path";
320
- import { fileURLToPath } from "url";
735
+ function resolveWorkspaceAssetPath(cwd, relPath) {
736
+ const root = path4.resolve(cwd);
737
+ const input = relPath.trim();
738
+ if (!input) throw new Error("assets \u8DEF\u5F84\u4E3A\u7A7A");
739
+ if (path4.isAbsolute(input)) throw new Error(`assets \u53EA\u63A5\u53D7\u5DE5\u4F5C\u533A\u76F8\u5BF9\u8DEF\u5F84,\u62D2\u7EDD\u7EDD\u5BF9\u8DEF\u5F84: ${relPath}`);
740
+ const resolved = path4.resolve(root, input);
741
+ const rel = path4.relative(root, resolved);
742
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
743
+ throw new Error(`assets \u8DEF\u5F84\u9003\u9038\u5DE5\u4F5C\u533A: ${relPath}`);
744
+ }
745
+ return resolved;
746
+ }
747
+
748
+ // src/workspace.ts
749
+ var DEFAULT_ENTRY_PATH = "src/main.ts";
750
+ var DEFAULT_MAIN_TS = `import { TwinApp, type TwinAppContext } from "@easytwin/apps";
751
+
752
+ export default class App extends TwinApp {
753
+ async init(ctx: TwinAppContext) {
754
+ // engine / container \u5DF2\u6709;scene / camera \u4E3A null
755
+ void ctx;
756
+ }
757
+
758
+ async onSceneLoaded(ctx: TwinAppContext) {
759
+ // \u573A\u666F\u5B57\u6BB5\u6709\u503C;\u5728\u6B64\u52A0\u7269\u4F53 / \u7ED1\u4E8B\u4EF6,\u5E76\u7528 ctx.sceneCleanup \u5BF9\u79F0\u62C6\u9664
760
+ void ctx;
761
+ }
762
+
763
+ onUpdate(ctx: TwinAppContext, delta: number, elapsed: number) {
764
+ void ctx;
765
+ void delta;
766
+ void elapsed;
767
+ }
768
+
769
+ onDispose(ctx: TwinAppContext) {
770
+ void ctx;
771
+ }
772
+ }
773
+ `;
774
+ var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
775
+ ".git",
776
+ "node_modules",
777
+ "dist",
778
+ ".easytwin",
779
+ ".cursor",
780
+ ".claude",
781
+ ".qoder",
782
+ ".vscode"
783
+ ]);
784
+ var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
785
+ var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
786
+ function defaultPullIgnore(relPath) {
787
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
788
+ if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
789
+ const base = parts[parts.length - 1];
790
+ return base !== void 0 && PULL_IGNORED_FILES.has(base);
791
+ }
792
+ function isSafeRelPath(relPath) {
793
+ const n = normalizePath(relPath);
794
+ if (!n) return false;
795
+ if (n.toLowerCase() === "easytwin.config.json") return false;
796
+ if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
797
+ const parts = n.split("/");
798
+ if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
799
+ return true;
800
+ }
801
+ function extensionOf2(relPath) {
802
+ const base = relPath.split("/").pop() ?? "";
803
+ const i = base.lastIndexOf(".");
804
+ if (i <= 0) return "";
805
+ return base.slice(i).toLowerCase();
806
+ }
807
+ function allowedExtensionSet2(list) {
808
+ return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
809
+ }
810
+ function normalizeEol(text) {
811
+ return text.replace(/\r\n/g, "\n");
812
+ }
813
+ function combineIgnore(extra) {
814
+ return (relPath) => defaultPullIgnore(relPath) || (extra?.(relPath) ?? false);
815
+ }
816
+ async function readLocalText(absPath) {
817
+ try {
818
+ return await fs4.readFile(absPath, "utf8");
819
+ } catch (err) {
820
+ const code = err.code;
821
+ if (code === "ENOENT") return void 0;
822
+ throw err;
823
+ }
824
+ }
825
+ async function loadAllowedExtensions(client) {
826
+ if (client.mock) return DEFAULT_PULL_EXTENSIONS;
827
+ try {
828
+ const cfg = normalizeWorkspaceConfig(
829
+ await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
830
+ );
831
+ return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
832
+ } catch {
833
+ return DEFAULT_PULL_EXTENSIONS;
834
+ }
835
+ }
836
+ async function fetchRemoteFiles(client) {
837
+ if (client.mock) return [];
838
+ return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(client.appId), { method: "GET" }));
839
+ }
840
+ async function planWorkspacePull(client, dir, options = {}) {
841
+ const ignore = combineIgnore(options.ignore);
842
+ const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
843
+ const remoteRaw = await fetchRemoteFiles(client);
844
+ const skippedRemotePaths = [];
845
+ const remoteByPath = /* @__PURE__ */ new Map();
846
+ for (const file of remoteRaw) {
847
+ const rel = normalizePath(file.filePath);
848
+ if (!isSafeRelPath(rel) || ignore(rel)) {
849
+ skippedRemotePaths.push(rel || file.filePath);
850
+ continue;
851
+ }
852
+ remoteByPath.set(rel, file.content);
853
+ }
854
+ let localFiles = [];
855
+ try {
856
+ localFiles = await collectFiles(dir, ignore);
857
+ } catch (err) {
858
+ const code = err.code;
859
+ if (code !== "ENOENT") throw err;
860
+ }
861
+ const localByPath = /* @__PURE__ */ new Map();
862
+ for (const file of localFiles) {
863
+ const rel = normalizePath(file.relPath);
864
+ if (!allowed.has(extensionOf2(rel))) continue;
865
+ const content = await readLocalText(file.absPath);
866
+ if (content !== void 0) localByPath.set(rel, content);
867
+ }
868
+ const changes = [];
869
+ const seen = /* @__PURE__ */ new Set();
870
+ for (const [rel, remoteContent] of remoteByPath) {
871
+ seen.add(rel);
872
+ const localContent = localByPath.get(rel);
873
+ if (localContent === void 0) {
874
+ changes.push({ path: rel, kind: "remote-only", remoteContent });
875
+ } else if (normalizeEol(localContent) === normalizeEol(remoteContent)) {
876
+ changes.push({ path: rel, kind: "identical", localContent, remoteContent });
877
+ } else {
878
+ changes.push({ path: rel, kind: "modified", localContent, remoteContent });
879
+ }
880
+ }
881
+ for (const [rel, localContent] of localByPath) {
882
+ if (seen.has(rel)) continue;
883
+ changes.push({ path: rel, kind: "local-only", localContent });
884
+ }
885
+ const remoteEmpty = remoteByPath.size === 0;
886
+ let seededDefaults = false;
887
+ if (remoteEmpty) {
888
+ const localMain = localByPath.get(DEFAULT_ENTRY_PATH);
889
+ if (localMain === void 0) {
890
+ changes.push({ path: DEFAULT_ENTRY_PATH, kind: "seed", remoteContent: DEFAULT_MAIN_TS });
891
+ seededDefaults = true;
892
+ }
893
+ }
894
+ changes.sort((a, b) => a.path.localeCompare(b.path));
895
+ return {
896
+ remoteEmpty,
897
+ seededDefaults,
898
+ mock: client.mock || void 0,
899
+ skippedRemotePaths,
900
+ changes
901
+ };
902
+ }
903
+ async function applyWorkspacePull(dir, plan, options = {}) {
904
+ const written = [];
905
+ const skippedConflicts = [];
906
+ for (const change of plan.changes) {
907
+ if (change.kind === "identical" || change.kind === "local-only") continue;
908
+ if (change.kind === "modified" && !options.force) {
909
+ skippedConflicts.push(change.path);
910
+ continue;
911
+ }
912
+ const content = change.remoteContent ?? "";
913
+ const abs = resolveWorkspaceAssetPath(dir, change.path);
914
+ await fs4.mkdir(path5.dirname(abs), { recursive: true });
915
+ await fs4.writeFile(abs, content, "utf8");
916
+ written.push(change.path);
917
+ }
918
+ return { written, skippedConflicts };
919
+ }
920
+ async function pullWorkspace(client, dir, options = {}) {
921
+ const plan = await planWorkspacePull(client, dir, { ignore: options.ignore });
922
+ if (options.dryRun) {
923
+ const skippedConflicts = plan.changes.filter((c) => c.kind === "modified" && !options.force).map((c) => c.path);
924
+ return { plan, written: [], skippedConflicts, dryRun: true, mock: plan.mock };
925
+ }
926
+ const applied = await applyWorkspacePull(dir, plan, { force: options.force });
927
+ return { plan, ...applied, mock: plan.mock };
928
+ }
929
+ var KIND_LABEL = {
930
+ identical: "same ",
931
+ "remote-only": "create",
932
+ "local-only": "keep ",
933
+ modified: "modify",
934
+ seed: "seed "
935
+ };
936
+ function formatWorkspacePullPlan(plan) {
937
+ const lines = [];
938
+ if (plan.mock) lines.push("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u6309\u8FDC\u7AEF\u4E3A\u7A7A\u5904\u7406");
939
+ const remoteCount = plan.changes.filter((c) => c.kind !== "local-only" && c.kind !== "seed").length;
940
+ if (plan.remoteEmpty) {
941
+ lines.push(
942
+ plan.seededDefaults ? `\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u5C06\u5199\u5165\u9ED8\u8BA4 ${DEFAULT_ENTRY_PATH}` : "\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u672C\u5730\u5DF2\u6709\u5165\u53E3,\u672A\u6539\u52A8\u9ED8\u8BA4\u6A21\u677F"
943
+ );
944
+ } else {
945
+ lines.push(`\u8FDC\u7AEF ${remoteCount} \u4E2A\u6587\u4EF6`);
946
+ }
947
+ for (const change of plan.changes) {
948
+ let extra = "";
949
+ if (change.kind === "modified") extra = " (\u51B2\u7A81,\u9ED8\u8BA4\u4E0D\u8986\u76D6)";
950
+ if (change.kind === "local-only") extra = " (\u4EC5\u672C\u5730)";
951
+ if (change.kind === "seed") extra = " (\u9ED8\u8BA4 TwinApp)";
952
+ lines.push(` ${KIND_LABEL[change.kind]} ${change.path}${extra}`);
953
+ }
954
+ if (plan.skippedRemotePaths.length > 0) {
955
+ lines.push(`\u8DF3\u8FC7\u975E\u6CD5/\u51ED\u636E\u8DEF\u5F84: ${plan.skippedRemotePaths.join(", ")}`);
956
+ }
957
+ return lines.join("\n");
958
+ }
959
+ function formatWorkspacePullResult(result) {
960
+ const lines = [formatWorkspacePullPlan(result.plan)];
961
+ if (result.dryRun) {
962
+ lines.push("dry-run:\u672A\u5199\u76D8");
963
+ if (result.skippedConflicts.length > 0) {
964
+ lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force)`);
965
+ }
966
+ return lines.join("\n");
967
+ }
968
+ if (result.written.length > 0) {
969
+ lines.push(`\u5DF2\u5199\u5165 ${result.written.length} \u4E2A\u6587\u4EF6: ${result.written.join(", ")}`);
970
+ } else {
971
+ lines.push("\u672A\u5199\u5165\u6587\u4EF6");
972
+ }
973
+ if (result.skippedConflicts.length > 0) {
974
+ lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force): ${result.skippedConflicts.join(", ")}`);
975
+ }
976
+ return lines.join("\n");
977
+ }
978
+ function workspacePullHasConflicts(plan) {
979
+ return plan.changes.some((c) => c.kind === "modified");
980
+ }
981
+ function workspacePullPendingWrites(plan, force = false) {
982
+ return plan.changes.filter((c) => c.kind === "seed" || c.kind === "remote-only" || force && c.kind === "modified").map((c) => c.path);
983
+ }
984
+
985
+ // src/skills.ts
986
+ import { existsSync, promises as fs5 } from "fs";
987
+ import path6 from "path";
988
+ import { fileURLToPath as fileURLToPath2 } from "url";
321
989
  var SKILL_NAMES = [
322
990
  "easytwin-render",
323
991
  "easytwin-core",
@@ -329,8 +997,82 @@ var SKILL_NAMES = [
329
997
  var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
330
998
  var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
331
999
  var META_FILE_NAME = ".easytwin-meta.json";
1000
+ var EASYTWIN_TYPES_DIR = ".easytwin/types";
1001
+ var MINIMAL_TSCONFIG = `${JSON.stringify(
1002
+ {
1003
+ compilerOptions: {
1004
+ target: "ES2022",
1005
+ module: "ESNext",
1006
+ moduleResolution: "bundler",
1007
+ strict: true,
1008
+ skipLibCheck: true,
1009
+ noEmit: true,
1010
+ paths: {
1011
+ "@easytwin/runtime": [".easytwin/types"],
1012
+ "@easytwin/apps": [".easytwin/types/apps"]
1013
+ }
1014
+ },
1015
+ include: ["src/**/*.ts"]
1016
+ },
1017
+ null,
1018
+ 2
1019
+ )}
1020
+ `;
1021
+ var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
1022
+ "skipLibCheck": true,
1023
+ "paths": {
1024
+ "@easytwin/runtime": [".easytwin/types"],
1025
+ "@easytwin/apps": [".easytwin/types/apps"]
1026
+ }`;
1027
+ var APPS_TYPES_FILE = "apps.d.ts";
1028
+ var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
1029
+
1030
+ export type TwinAppContext = {
1031
+ app: {
1032
+ id: string;
1033
+ mode: "preview" | "publish";
1034
+ };
1035
+ engine: RuntimeEngine;
1036
+ container: HTMLElement;
1037
+ runtimeScene: RuntimeScene | null;
1038
+ scene: RuntimeScene["sceneObject"] | null;
1039
+ camera: RuntimeScene["camera"]["main"] | null;
1040
+ sceneManager: {
1041
+ currentSceneId: string;
1042
+ runtime: SceneManager | null;
1043
+ loadScene(sceneId: string): Promise<void>;
1044
+ };
1045
+ logger: {
1046
+ log(...args: unknown[]): void;
1047
+ info(...args: unknown[]): void;
1048
+ warn(...args: unknown[]): void;
1049
+ error(...args: unknown[]): void;
1050
+ };
1051
+ assets: {
1052
+ text(path: string): Promise<string>;
1053
+ json<T = unknown>(path: string): Promise<T>;
1054
+ };
1055
+ cleanup(fn: () => void | Promise<void>): void;
1056
+ sceneCleanup(fn: () => void | Promise<void>): void;
1057
+ };
1058
+
1059
+ export declare abstract class TwinApp {
1060
+ init?(ctx: TwinAppContext): void | Promise<void>;
1061
+ onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
1062
+ onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
1063
+ onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
1064
+ onDispose?(ctx: TwinAppContext): void | Promise<void>;
1065
+ onError?(ctx: TwinAppContext, error: unknown): boolean | void;
1066
+ }
1067
+
1068
+ export declare function defineApp<T>(app: T): T;
1069
+ `;
1070
+ function buildRuntimeTypesContent(sourceDts) {
1071
+ return `${sourceDts.replace(/\s+$/, "")}
1072
+ `;
1073
+ }
332
1074
  function normalizeTargets(target = "all") {
333
- if (target === "all") return ["cursor", "claude", "codex"];
1075
+ if (target === "all") return ["cursor", "claude", "codex", "qoder"];
334
1076
  if (Array.isArray(target)) return [...new Set(target)];
335
1077
  return [target];
336
1078
  }
@@ -338,58 +1080,69 @@ function resolveSkillsSourceDir() {
338
1080
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
339
1081
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
340
1082
  }
341
- const here = path4.dirname(fileURLToPath(import.meta.url));
342
- return path4.resolve(here, "..", "skills");
1083
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1084
+ return path6.resolve(here, "..", "skills");
1085
+ }
1086
+ function resolveRuntimeTypesSourceFile() {
1087
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1088
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
1089
+ }
1090
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1091
+ const fromDist = path6.join(here, "runtime-types", "index.d.ts");
1092
+ const fromSrc = path6.resolve(here, "lib", "index.d.ts");
1093
+ if (existsSync(fromDist)) return fromDist;
1094
+ if (existsSync(fromSrc)) return fromSrc;
1095
+ throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
343
1096
  }
344
1097
  async function readJson(file) {
345
- return JSON.parse(await fs4.readFile(file, "utf8"));
1098
+ return JSON.parse(await fs5.readFile(file, "utf8"));
346
1099
  }
347
1100
  async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
348
- const marker = path4.join(skillsDir, ".easytwin-source.json");
1101
+ const marker = path6.join(skillsDir, ".easytwin-source.json");
349
1102
  try {
350
1103
  const meta = await readJson(marker);
351
1104
  if (typeof meta.version === "string") return meta.version;
352
1105
  } catch {
353
1106
  }
354
1107
  try {
355
- const pkg = await readJson(path4.resolve(skillsDir, "..", "package.json"));
1108
+ const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
356
1109
  return pkg.version;
357
1110
  } catch {
358
1111
  return "0.0.0";
359
1112
  }
360
1113
  }
361
1114
  async function copyDir(src, dest) {
362
- await fs4.mkdir(dest, { recursive: true });
363
- const entries = await fs4.readdir(src, { withFileTypes: true });
1115
+ await fs5.mkdir(dest, { recursive: true });
1116
+ const entries = await fs5.readdir(src, { withFileTypes: true });
364
1117
  for (const entry of entries) {
365
- const s = path4.join(src, entry.name);
366
- const d = path4.join(dest, entry.name);
1118
+ const s = path6.join(src, entry.name);
1119
+ const d = path6.join(dest, entry.name);
367
1120
  if (entry.isDirectory()) await copyDir(s, d);
368
- else if (entry.isFile()) await fs4.copyFile(s, d);
1121
+ else if (entry.isFile()) await fs5.copyFile(s, d);
369
1122
  }
370
1123
  }
371
1124
  async function dirMatches(src, dest) {
372
1125
  let sourceEntries;
373
1126
  try {
374
- sourceEntries = await fs4.readdir(src);
1127
+ sourceEntries = await fs5.readdir(src);
375
1128
  } catch {
376
1129
  return false;
377
1130
  }
378
1131
  for (const name of sourceEntries) {
379
- const s = path4.join(src, name);
380
- const d = path4.join(dest, name);
381
- const sStat = await fs4.stat(s);
1132
+ const s = path6.join(src, name);
1133
+ const d = path6.join(dest, name);
1134
+ const sStat = await fs5.stat(s);
382
1135
  if (sStat.isDirectory()) {
383
1136
  if (!await dirMatches(s, d)) return false;
384
1137
  } else {
385
1138
  let dStat;
386
1139
  try {
387
- dStat = await fs4.stat(d);
1140
+ dStat = await fs5.stat(d);
388
1141
  } catch {
389
1142
  return false;
390
1143
  }
391
1144
  if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
392
- if (!(await fs4.readFile(s)).equals(await fs4.readFile(d))) return false;
1145
+ if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
393
1146
  }
394
1147
  }
395
1148
  return true;
@@ -400,18 +1153,18 @@ function actionFor(exists, matches) {
400
1153
  }
401
1154
  async function writeMeta(destDir, version) {
402
1155
  const meta = { name: "@easytwin/devkit", version };
403
- await fs4.writeFile(path4.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
1156
+ await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
404
1157
  }
405
1158
  async function syncToDir(target, sourceRoot, targetRoot, version) {
406
1159
  const entries = [];
407
1160
  for (const name of SKILL_NAMES) {
408
- const src = path4.join(sourceRoot, name);
409
- const dest = path4.join(targetRoot, name);
410
- const exists = await fs4.stat(dest).then(() => true).catch(() => false);
1161
+ const src = path6.join(sourceRoot, name);
1162
+ const dest = path6.join(targetRoot, name);
1163
+ const exists = await fs5.stat(dest).then(() => true).catch(() => false);
411
1164
  const matches = await dirMatches(src, dest);
412
1165
  const action = actionFor(exists, matches);
413
1166
  if (action !== "unchanged") {
414
- await fs4.rm(dest, { recursive: true, force: true });
1167
+ await fs5.rm(dest, { recursive: true, force: true });
415
1168
  await copyDir(src, dest);
416
1169
  }
417
1170
  entries.push({ name, action });
@@ -430,19 +1183,19 @@ function buildCodexSegment(version) {
430
1183
  "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
431
1184
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
432
1185
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
433
- "- `easytwin-upload`:\u5168\u91CF\u4E0A\u4F20\u5F00\u53D1\u4EA7\u7269\u76EE\u5F55(\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
1186
+ "- `easytwin-upload`:\u62C9\u53D6/\u4E0A\u4F20\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5199\u9ED8\u8BA4\u5165\u53E3;\u4E0A\u4F20\u5168\u91CF\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
434
1187
  "",
435
1188
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
436
1189
  CODEX_MARKER_END
437
1190
  ].join("\n");
438
1191
  }
439
1192
  async function syncToCodex(sourceRoot, cwd, version) {
440
- const agentsFile = path4.join(cwd, "AGENTS.md");
1193
+ const agentsFile = path6.join(cwd, "AGENTS.md");
441
1194
  const segment = buildCodexSegment(version);
442
1195
  let content = "";
443
1196
  let exists = true;
444
1197
  try {
445
- content = await fs4.readFile(agentsFile, "utf8");
1198
+ content = await fs5.readFile(agentsFile, "utf8");
446
1199
  } catch {
447
1200
  exists = false;
448
1201
  }
@@ -460,36 +1213,87 @@ async function syncToCodex(sourceRoot, cwd, version) {
460
1213
  next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
461
1214
  }
462
1215
  if (action !== "unchanged") {
463
- await fs4.mkdir(cwd, { recursive: true });
464
- await fs4.writeFile(agentsFile, next, "utf8");
1216
+ await fs5.mkdir(cwd, { recursive: true });
1217
+ await fs5.writeFile(agentsFile, next, "utf8");
465
1218
  }
466
1219
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
467
1220
  }
1221
+ async function syncRuntimeTypes(cwd, typesSourceFile) {
1222
+ const destDir = path6.join(cwd, ".easytwin", "types");
1223
+ const destFile = path6.join(destDir, "index.d.ts");
1224
+ const appsFile = path6.join(destDir, APPS_TYPES_FILE);
1225
+ const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
1226
+ let runtimeCurrent = "";
1227
+ let appsCurrent = "";
1228
+ let runtimeExists = true;
1229
+ let appsExists = true;
1230
+ try {
1231
+ runtimeCurrent = await fs5.readFile(destFile, "utf8");
1232
+ } catch {
1233
+ runtimeExists = false;
1234
+ }
1235
+ try {
1236
+ appsCurrent = await fs5.readFile(appsFile, "utf8");
1237
+ } catch {
1238
+ appsExists = false;
1239
+ }
1240
+ const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
1241
+ const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
1242
+ const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
1243
+ if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
1244
+ await fs5.mkdir(destDir, { recursive: true });
1245
+ if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
1246
+ if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
1247
+ }
1248
+ const tsconfigPath = path6.join(cwd, "tsconfig.json");
1249
+ let tsconfig;
1250
+ let pathsHint;
1251
+ try {
1252
+ const existing = await fs5.readFile(tsconfigPath, "utf8");
1253
+ if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
1254
+ else {
1255
+ tsconfig = "manual-paths";
1256
+ pathsHint = TSCONFIG_PATHS_HINT;
1257
+ }
1258
+ } catch {
1259
+ await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1260
+ tsconfig = "created";
1261
+ }
1262
+ return { action, tsconfig, pathsHint };
1263
+ }
468
1264
  async function syncSkills(options) {
469
1265
  const targets = normalizeTargets(options.targets ?? "all");
470
1266
  const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
471
1267
  const version = options.version ?? await readDevkitVersion(sourceRoot);
472
1268
  const summaries = [];
473
1269
  for (const target of targets) {
474
- if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
475
- else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
1270
+ if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
1271
+ else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
1272
+ else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
476
1273
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
477
1274
  }
478
- return summaries;
1275
+ const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
1276
+ return { summaries, types };
479
1277
  }
480
1278
  async function detectSkillsStatus(cwd, sourceDir) {
481
1279
  const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
482
1280
  const version = await readDevkitVersion(sourceRoot);
483
1281
  const targets = [];
484
- for (const target of ["cursor", "claude"]) {
485
- const root = path4.join(cwd, target === "cursor" ? ".cursor/skills" : ".claude/skills");
1282
+ const dirTargets = ["cursor", "claude", "qoder"];
1283
+ const DIR_ROOTS = {
1284
+ cursor: ".cursor/skills",
1285
+ claude: ".claude/skills",
1286
+ qoder: ".qoder/skills"
1287
+ };
1288
+ for (const target of dirTargets) {
1289
+ const root = path6.join(cwd, DIR_ROOTS[target]);
486
1290
  const missing = [];
487
1291
  for (const name of SKILL_NAMES) {
488
- if (!await fs4.stat(path4.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
1292
+ if (!await fs5.stat(path6.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
489
1293
  }
490
1294
  let stale = false;
491
1295
  try {
492
- const meta = await readJson(path4.join(root, META_FILE_NAME));
1296
+ const meta = await readJson(path6.join(root, META_FILE_NAME));
493
1297
  stale = meta.version !== version;
494
1298
  } catch {
495
1299
  stale = missing.length === 0;
@@ -502,50 +1306,612 @@ async function detectSkillsStatus(cwd, sourceDir) {
502
1306
  }
503
1307
  let agentsContent = "";
504
1308
  try {
505
- agentsContent = await fs4.readFile(path4.join(cwd, "AGENTS.md"), "utf8");
1309
+ agentsContent = await fs5.readFile(path6.join(cwd, "AGENTS.md"), "utf8");
506
1310
  } catch {
507
1311
  }
508
1312
  const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
509
1313
  targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
510
- return { cwd, targets };
1314
+ let typesSynced = false;
1315
+ try {
1316
+ const dts = await fs5.readFile(path6.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
1317
+ const appsDts = await fs5.readFile(path6.join(cwd, ".easytwin", "types", APPS_TYPES_FILE), "utf8");
1318
+ typesSynced = dts.includes("registerEngineTickListener") && appsDts.includes("export declare abstract class TwinApp");
1319
+ } catch {
1320
+ typesSynced = false;
1321
+ }
1322
+ return { cwd, targets, typesSynced };
1323
+ }
1324
+
1325
+ // src/bundle.ts
1326
+ import * as esbuild from "esbuild-wasm";
1327
+ import { promises as fs6 } from "fs";
1328
+ import path7 from "path";
1329
+
1330
+ // src/apps.ts
1331
+ var PORTABLE_RUNTIME_EXPORTS = [
1332
+ "THREE",
1333
+ "RuntimeEngine",
1334
+ "SceneManager",
1335
+ "LoadSceneMode",
1336
+ "convertObjToComponentJson"
1337
+ ];
1338
+ var TwinApp = class {
1339
+ };
1340
+ function defineApp(app) {
1341
+ return app;
1342
+ }
1343
+ var LIFECYCLE_NAMES = [
1344
+ "init",
1345
+ "onUpdate",
1346
+ "onBeforeSceneUnload",
1347
+ "onSceneLoaded",
1348
+ "onDispose",
1349
+ "onError"
1350
+ ];
1351
+ function isLifecycleApp(value) {
1352
+ if (typeof value !== "object" || value === null) return false;
1353
+ return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
1354
+ }
1355
+ function createAppInstance(appExport) {
1356
+ if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
1357
+ return new appExport();
1358
+ }
1359
+ if (isLifecycleApp(appExport)) return appExport;
1360
+ throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
1361
+ }
1362
+ function portableRuntimeWarning(names) {
1363
+ const extra = [...new Set(names)].filter(
1364
+ (n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
1365
+ );
1366
+ const ns = [...names].includes("*");
1367
+ if (!ns && extra.length === 0) return void 0;
1368
+ const detail = ns ? "namespace import *" : extra.sort().join(", ");
1369
+ return `\u68C0\u6D4B\u5230 @easytwin/runtime \u5BFC\u51FA ${detail} \u4E0D\u5728\u5728\u7EBF\u7F16\u8BD1\u767D\u540D\u5355(${PORTABLE_RUNTIME_EXPORTS.join(", ")})\u5185\u3002\u672C\u5730\u9884\u89C8\u53EF\u7528,\u4E0A\u4F20\u5230\u5728\u7EBF TwinApp \u53EF\u80FD\u7F16\u4E0D\u8FC7\u3002`;
1370
+ }
1371
+
1372
+ // src/bundle.ts
1373
+ var USER_ENTRY = "src/main.ts";
1374
+ var DEFAULT_BUNDLE_OUT = "dist/main.js";
1375
+ var RUNTIME_MODULE = "@easytwin/runtime";
1376
+ var APPS_MODULE = "@easytwin/apps";
1377
+ var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
1378
+ var BundleError = class extends Error {
1379
+ constructor(message) {
1380
+ super(message);
1381
+ this.name = "BundleError";
1382
+ }
1383
+ };
1384
+ function isRelativeOrAbsolute(spec) {
1385
+ return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
1386
+ }
1387
+ function whitelistPlugin() {
1388
+ return {
1389
+ name: "easytwin-whitelist",
1390
+ setup(build2) {
1391
+ build2.onResolve({ filter: /.*/ }, (args) => {
1392
+ if (args.kind === "entry-point") return void 0;
1393
+ if (isRelativeOrAbsolute(args.path)) return void 0;
1394
+ if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
1395
+ return {
1396
+ errors: [
1397
+ {
1398
+ text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime \u4E0E @easytwin/apps\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
1399
+ }
1400
+ ]
1401
+ };
1402
+ });
1403
+ }
1404
+ };
1405
+ }
1406
+ function formatEsbuildMessages(messages) {
1407
+ return messages.map((m) => {
1408
+ const loc = m.location ? `${m.location.file}:${m.location.line}:${m.location.column}: ` : "";
1409
+ return `${loc}${m.text}`;
1410
+ }).join("\n");
1411
+ }
1412
+ function collectBundledRuntimeExports(code) {
1413
+ const names = [];
1414
+ if (/import\s+\*\s+as\s+[\w$]+\s+from\s*["']@easytwin\/runtime["']/.test(code)) names.push("*");
1415
+ const named = /import\s*\{([^}]+)\}\s*from\s*["']@easytwin\/runtime["']/g;
1416
+ for (const match of code.matchAll(named)) {
1417
+ const body = match[1] ?? "";
1418
+ for (const part of body.split(",")) {
1419
+ const token = part.replace(/\btype\b/g, "").trim();
1420
+ if (!token) continue;
1421
+ const id = token.split(/\s+as\s+/)[0]?.trim();
1422
+ if (id) names.push(id);
1423
+ }
1424
+ }
1425
+ return names;
1426
+ }
1427
+ async function bundleUserCode(options) {
1428
+ const cwd = path7.resolve(options.cwd);
1429
+ const entry = path7.join(cwd, USER_ENTRY);
1430
+ try {
1431
+ await fs6.access(entry);
1432
+ } catch {
1433
+ throw new BundleError(
1434
+ `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
1435
+ );
1436
+ }
1437
+ let result;
1438
+ try {
1439
+ result = await esbuild.build({
1440
+ absWorkingDir: cwd,
1441
+ entryPoints: [entry],
1442
+ bundle: true,
1443
+ write: false,
1444
+ format: "esm",
1445
+ platform: "browser",
1446
+ target: "es2022",
1447
+ sourcemap: "inline",
1448
+ logLevel: "silent",
1449
+ plugins: [whitelistPlugin()]
1450
+ });
1451
+ } catch (err) {
1452
+ const errors = err.errors;
1453
+ if (Array.isArray(errors) && errors.length > 0) {
1454
+ throw new BundleError(formatEsbuildMessages(errors));
1455
+ }
1456
+ throw err instanceof Error ? new BundleError(err.message) : err;
1457
+ }
1458
+ if (result.errors.length > 0) {
1459
+ throw new BundleError(formatEsbuildMessages(result.errors));
1460
+ }
1461
+ const file = result.outputFiles?.[0];
1462
+ if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
1463
+ const code = file.text;
1464
+ const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
1465
+ const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
1466
+ if (portable) warnings.push(portable);
1467
+ if (options.outFile) {
1468
+ const outFile = path7.isAbsolute(options.outFile) ? options.outFile : path7.join(cwd, options.outFile);
1469
+ await fs6.mkdir(path7.dirname(outFile), { recursive: true });
1470
+ await fs6.writeFile(outFile, code, "utf8");
1471
+ }
1472
+ return { code, warnings };
1473
+ }
1474
+ async function typesMissingHint(cwd) {
1475
+ try {
1476
+ await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
1477
+ return void 0;
1478
+ } catch {
1479
+ return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
1480
+ }
1481
+ }
1482
+ var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
1483
+ var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1484
+ function decodeVLQValues(str) {
1485
+ const values = [];
1486
+ let i = 0;
1487
+ while (i < str.length) {
1488
+ let result = 0;
1489
+ let shift = 0;
1490
+ let continuation = true;
1491
+ while (continuation) {
1492
+ if (i >= str.length) return values;
1493
+ const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
1494
+ if (digit < 0) return values;
1495
+ continuation = (digit & 32) !== 0;
1496
+ result += (digit & 31) << shift;
1497
+ shift += 5;
1498
+ }
1499
+ values.push(result & 1 ? -(result >> 1) : result >> 1);
1500
+ }
1501
+ return values;
1502
+ }
1503
+ function decodeMappings(map) {
1504
+ const sources = map.sources ?? [];
1505
+ const lines = (map.mappings ?? "").split(";");
1506
+ let sourceIndex = 0;
1507
+ let originalLine = 0;
1508
+ let originalColumn = 0;
1509
+ const decoded = [];
1510
+ for (const line of lines) {
1511
+ let generatedColumn = 0;
1512
+ const segs = [];
1513
+ if (line) {
1514
+ for (const raw of line.split(",")) {
1515
+ if (!raw) continue;
1516
+ const nums = decodeVLQValues(raw);
1517
+ if (nums[0] === void 0) continue;
1518
+ generatedColumn += nums[0];
1519
+ if (nums.length >= 4) {
1520
+ sourceIndex += nums[1] ?? 0;
1521
+ originalLine += nums[2] ?? 0;
1522
+ originalColumn += nums[3] ?? 0;
1523
+ segs.push({
1524
+ generatedColumn,
1525
+ source: sources[sourceIndex] ?? USER_ENTRY,
1526
+ originalLine,
1527
+ originalColumn
1528
+ });
1529
+ }
1530
+ }
1531
+ }
1532
+ decoded.push(segs);
1533
+ }
1534
+ return decoded;
1535
+ }
1536
+ function originalPositionFor(map, line, column) {
1537
+ const decoded = decodeMappings(map);
1538
+ for (let i = line - 1; i >= 0; i--) {
1539
+ const segs = decoded[i];
1540
+ if (!segs || segs.length === 0) continue;
1541
+ const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
1542
+ let best = segs[0];
1543
+ for (const seg of segs) {
1544
+ if (seg.generatedColumn <= col) best = seg;
1545
+ else break;
1546
+ }
1547
+ if (!best) continue;
1548
+ return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
1549
+ }
1550
+ return void 0;
1551
+ }
1552
+ function extractInlineSourceMap(code) {
1553
+ const m = code.match(SOURCEMAP_RE);
1554
+ if (!m?.[1]) return void 0;
1555
+ try {
1556
+ return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
1557
+ } catch {
1558
+ return void 0;
1559
+ }
1560
+ }
1561
+ function remapErrorStack(stack, bundledCode) {
1562
+ const map = extractInlineSourceMap(bundledCode);
1563
+ if (!map?.mappings) return stack;
1564
+ return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
1565
+ const orig = originalPositionFor(map, Number(line), Number(col));
1566
+ if (!orig) return full;
1567
+ return `${orig.source}:${orig.line}:${orig.column}`;
1568
+ });
511
1569
  }
1570
+
1571
+ // src/twinAppHost.ts
1572
+ var LOCAL_LOAD_SCENE_ERROR = "\u672C\u5730\u9884\u89C8\u5355\u573A\u666F,\u4E0D\u80FD\u8C03\u7528 sceneManager.loadScene";
1573
+ var MISSING_TICK_ERROR = "\u5F53\u524D runtime \u4E0D\u652F\u6301\u5F15\u64CE\u65F6\u949F(\u7F3A\u5C11 registerEngineTickListener)\u3002\u65E0\u6CD5\u8FD0\u884C onUpdate\u3002";
1574
+ function isPromiseLike(value) {
1575
+ return typeof value === "object" && value !== null && "then" in value;
1576
+ }
1577
+ var TwinAppPreviewHost = class {
1578
+ constructor(options) {
1579
+ this.options = options;
1580
+ this.currentSceneId = options.sceneId;
1581
+ this.onEngineTick = (engine) => this.handleTick(engine);
1582
+ }
1583
+ options;
1584
+ app = null;
1585
+ engine = null;
1586
+ disposed = false;
1587
+ /** 用户应用已经成功 onSceneLoaded。 */
1588
+ scenePresented = false;
1589
+ tickAttached = false;
1590
+ sceneLoadTask = null;
1591
+ ctx = null;
1592
+ cleanups = [];
1593
+ sceneCleanups = [];
1594
+ warnedAsyncUpdate = false;
1595
+ userBlobUrl = null;
1596
+ currentSceneId;
1597
+ onEngineTick;
1598
+ getEngine() {
1599
+ return this.engine;
1600
+ }
1601
+ async boot() {
1602
+ const { runtime, containerId, ossUrl, customComponentDeps } = this.options;
1603
+ this.engine = await runtime.RuntimeEngine.create({
1604
+ containerId,
1605
+ baseOSSUrl: ossUrl,
1606
+ customComponentDeps,
1607
+ sceneResources: [],
1608
+ componentRels: [],
1609
+ enableResourcePersistence: false
1610
+ });
1611
+ await this.loadCurrentScene();
1612
+ }
1613
+ async run(code) {
1614
+ if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
1615
+ if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
1616
+ this.assertTickApi();
1617
+ try {
1618
+ await this.teardownApp({ keepEngine: true });
1619
+ const mod = await this.importUser(code);
1620
+ this.app = createAppInstance(mod.default);
1621
+ await this.app.init?.(this.getContext());
1622
+ await this.loadCurrentScene();
1623
+ this.scenePresented = true;
1624
+ await this.app.onSceneLoaded?.(this.getContext());
1625
+ this.attachTick();
1626
+ } catch (error) {
1627
+ await this.teardownApp({ keepEngine: true, ignoreHookErrors: true });
1628
+ throw error;
1629
+ }
1630
+ }
1631
+ async dispose() {
1632
+ if (this.disposed) return;
1633
+ this.disposed = true;
1634
+ await this.teardownApp({ keepEngine: false, ignoreHookErrors: true });
1635
+ }
1636
+ assertTickApi() {
1637
+ const { runtime } = this.options;
1638
+ if (typeof runtime.registerEngineTickListener !== "function" || typeof runtime.unregisterEngineTickListener !== "function") {
1639
+ throw new Error(MISSING_TICK_ERROR);
1640
+ }
1641
+ }
1642
+ async importUser(code) {
1643
+ if (this.options.importUserModule) return this.options.importUserModule(code);
1644
+ if (this.userBlobUrl) {
1645
+ try {
1646
+ URL.revokeObjectURL(this.userBlobUrl);
1647
+ } catch {
1648
+ }
1649
+ this.userBlobUrl = null;
1650
+ }
1651
+ const blob = new Blob([code], { type: "text/javascript" });
1652
+ this.userBlobUrl = URL.createObjectURL(blob);
1653
+ return import(
1654
+ /* @vite-ignore */
1655
+ this.userBlobUrl
1656
+ );
1657
+ }
1658
+ async loadCurrentScene() {
1659
+ if (!this.engine) throw new Error("Engine is not created");
1660
+ if (this.sceneLoadTask) await this.sceneLoadTask;
1661
+ this.sceneLoadTask = this.options.loadScene(this.engine, this.options.sceneJson).finally(() => {
1662
+ this.sceneLoadTask = null;
1663
+ });
1664
+ await this.sceneLoadTask;
1665
+ }
1666
+ handleTick(engine) {
1667
+ if (this.disposed || this.sceneLoadTask || !this.scenePresented) return;
1668
+ try {
1669
+ const result = this.app?.onUpdate?.(
1670
+ this.getContext(),
1671
+ engine.time.deltaTime,
1672
+ engine.time.elapsedTime
1673
+ );
1674
+ if (isPromiseLike(result)) {
1675
+ void Promise.resolve(result).catch((error) => {
1676
+ console.error("async onUpdate rejected:", error);
1677
+ });
1678
+ if (!this.warnedAsyncUpdate) {
1679
+ this.warnedAsyncUpdate = true;
1680
+ console.warn("TwinApp.onUpdate \u5E94\u540C\u6B65\u6267\u884C\uFF1B\u672C\u6B21\u5DF2\u5FFD\u7565\u5176 Promise \u8FD4\u56DE\u503C");
1681
+ }
1682
+ }
1683
+ } catch (error) {
1684
+ this.handleUpdateError(error);
1685
+ }
1686
+ }
1687
+ handleUpdateError(error) {
1688
+ let shouldContinue = false;
1689
+ try {
1690
+ shouldContinue = this.app?.onError?.(this.getContext(), error) === true;
1691
+ } catch (onError) {
1692
+ void this.failRun(onError);
1693
+ return;
1694
+ }
1695
+ if (shouldContinue) {
1696
+ console.error("TwinApp.onUpdate threw:", error);
1697
+ this.options.log?.(`onUpdate \u629B\u9519\u5DF2 continue: ${error instanceof Error ? error.message : String(error)}`);
1698
+ return;
1699
+ }
1700
+ void this.failRun(error);
1701
+ }
1702
+ async failRun(error) {
1703
+ this.options.log?.(`Run \u5931\u8D25 ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1704
+ await this.teardownApp({ keepEngine: true, ignoreHookErrors: true });
1705
+ }
1706
+ attachTick() {
1707
+ if (!this.engine || this.tickAttached) return;
1708
+ this.options.runtime.registerEngineTickListener?.(this.engine, this.onEngineTick);
1709
+ this.tickAttached = true;
1710
+ }
1711
+ detachTick() {
1712
+ if (!this.engine || !this.tickAttached) return;
1713
+ this.options.runtime.unregisterEngineTickListener?.(this.engine, this.onEngineTick);
1714
+ this.tickAttached = false;
1715
+ }
1716
+ async teardownApp(options) {
1717
+ this.detachTick();
1718
+ await this.unloadUserScene({ ignoreErrors: options.ignoreHookErrors });
1719
+ if (this.app && this.engine) {
1720
+ try {
1721
+ await this.app.onDispose?.(this.getContext());
1722
+ } catch (error) {
1723
+ if (!options.ignoreHookErrors) throw error;
1724
+ console.warn("TwinApp.onDispose failed:", error);
1725
+ }
1726
+ }
1727
+ await this.runHooks(this.cleanups);
1728
+ this.cleanups = [];
1729
+ this.app = null;
1730
+ this.ctx = null;
1731
+ this.scenePresented = false;
1732
+ if (!options.keepEngine) {
1733
+ try {
1734
+ this.engine?.destroy();
1735
+ } catch {
1736
+ }
1737
+ this.engine = null;
1738
+ }
1739
+ if (this.userBlobUrl) {
1740
+ try {
1741
+ URL.revokeObjectURL(this.userBlobUrl);
1742
+ } catch {
1743
+ }
1744
+ this.userBlobUrl = null;
1745
+ }
1746
+ }
1747
+ async unloadUserScene(options) {
1748
+ if (!this.scenePresented) return;
1749
+ this.scenePresented = false;
1750
+ try {
1751
+ if (this.engine) await this.app?.onBeforeSceneUnload?.(this.getContext());
1752
+ } catch (error) {
1753
+ if (!options?.ignoreErrors) throw error;
1754
+ console.warn("TwinApp.onBeforeSceneUnload failed:", error);
1755
+ } finally {
1756
+ await this.runHooks(this.sceneCleanups);
1757
+ this.sceneCleanups = [];
1758
+ }
1759
+ }
1760
+ async runHooks(hooks) {
1761
+ for (const fn of [...hooks].reverse()) {
1762
+ try {
1763
+ await fn();
1764
+ } catch (error) {
1765
+ console.warn("TwinApp cleanup failed:", error);
1766
+ }
1767
+ }
1768
+ }
1769
+ getContext() {
1770
+ if (!this.engine) throw new Error("Engine is not created");
1771
+ const runtimeScene = this.scenePresented ? this.engine.mainScene ?? null : null;
1772
+ const scene = runtimeScene?.sceneObject ?? null;
1773
+ const camera = runtimeScene?.camera?.main ?? null;
1774
+ const sceneManagerRuntime = this.engine.getManager(this.options.runtime.SceneManager);
1775
+ if (!this.ctx) {
1776
+ this.ctx = {
1777
+ app: { id: this.options.appId, mode: "preview" },
1778
+ engine: this.engine,
1779
+ container: this.engine.container,
1780
+ runtimeScene,
1781
+ scene,
1782
+ camera,
1783
+ sceneManager: {
1784
+ currentSceneId: this.currentSceneId,
1785
+ runtime: sceneManagerRuntime,
1786
+ loadScene: () => Promise.reject(new Error(LOCAL_LOAD_SCENE_ERROR))
1787
+ },
1788
+ logger: {
1789
+ log: (...args) => console.log(...args),
1790
+ info: (...args) => console.info(...args),
1791
+ warn: (...args) => console.warn(...args),
1792
+ error: (...args) => console.error(...args)
1793
+ },
1794
+ assets: {
1795
+ text: (p) => this.options.readAsset(p),
1796
+ json: async (p) => JSON.parse(await this.options.readAsset(p))
1797
+ },
1798
+ cleanup: (fn) => {
1799
+ this.cleanups.push(fn);
1800
+ },
1801
+ sceneCleanup: (fn) => {
1802
+ this.sceneCleanups.push(fn);
1803
+ }
1804
+ };
1805
+ return this.ctx;
1806
+ }
1807
+ this.ctx.engine = this.engine;
1808
+ this.ctx.container = this.engine.container;
1809
+ this.ctx.runtimeScene = runtimeScene;
1810
+ this.ctx.scene = scene;
1811
+ this.ctx.camera = camera;
1812
+ this.ctx.sceneManager.currentSceneId = this.currentSceneId;
1813
+ this.ctx.sceneManager.runtime = sceneManagerRuntime;
1814
+ return this.ctx;
1815
+ }
1816
+ };
512
1817
  export {
513
- APP_ID_HEADER,
1818
+ APPS_DTS,
1819
+ APPS_MODULE,
1820
+ APPS_TYPES_FILE,
514
1821
  AUTH_HEADER,
515
- BEARER_PREFIX,
1822
+ BundleError,
516
1823
  CODEX_MARKER_BEGIN,
517
1824
  CODEX_MARKER_END,
518
1825
  CONFIG_FILE_NAME,
519
1826
  ConfigError,
520
1827
  DEFAULT_BASE_URL,
1828
+ DEFAULT_BUNDLE_OUT,
1829
+ DEFAULT_ENTRY_PATH,
1830
+ DEFAULT_MAIN_TS,
1831
+ DEFAULT_OSS_URL,
1832
+ EASYTWIN_TYPES_DIR,
521
1833
  ENDPOINTS,
1834
+ EXAMPLE_SCENE_FILE,
522
1835
  EasyTwinApiError,
523
1836
  EasyTwinClient,
524
1837
  GITIGNORE_ENTRY,
525
1838
  GITIGNORE_FILE_NAME,
1839
+ LOCAL_LOAD_SCENE_ERROR,
526
1840
  META_FILE_NAME,
1841
+ MINIMAL_TSCONFIG,
1842
+ MISSING_TICK_ERROR,
1843
+ MOCK_APP_ID,
1844
+ MOCK_APP_SECRET,
1845
+ MOCK_SCENE_NAME,
1846
+ OP_ACCOUNT_ID_HEADER,
1847
+ OP_USER_ID_HEADER,
1848
+ PORTABLE_RUNTIME_EXPORTS,
1849
+ RUNTIME_MODULE,
527
1850
  SKILL_NAMES,
1851
+ SPACE_ID_HEADER,
1852
+ TEST_BASE_URL,
1853
+ TEST_OP_ACCOUNT_ID,
1854
+ TEST_OP_USER_ID,
1855
+ TEST_SPACE_ID,
1856
+ TSCONFIG_PATHS_HINT,
1857
+ TwinApp,
1858
+ TwinAppPreviewHost,
1859
+ USER_ENTRY,
528
1860
  appendGitignore,
1861
+ applyWorkspacePull,
1862
+ assertUploadable,
529
1863
  buildMultipartBody,
1864
+ buildRuntimeTypesContent,
1865
+ bundleUserCode,
530
1866
  collectFiles,
531
1867
  configFilePath,
1868
+ createAppInstance,
1869
+ defaultPullIgnore,
1870
+ defineApp,
1871
+ deriveExampleSceneId,
532
1872
  detectSkillsStatus,
1873
+ directoryDepth,
1874
+ exampleSceneSummary,
1875
+ extractInlineSourceMap,
1876
+ extractSceneArray,
1877
+ formatWorkspacePullPlan,
1878
+ formatWorkspacePullResult,
533
1879
  initConfig,
1880
+ isMockCredentials,
1881
+ isSafeRelPath,
534
1882
  listScenes,
535
1883
  loadConfig,
536
- normalizeSceneDetail,
1884
+ loadExampleScene,
1885
+ normalizeLinkedScenes,
1886
+ normalizePath,
537
1887
  normalizeSceneList,
538
1888
  normalizeTargets,
1889
+ normalizeWorkspaceConfig,
1890
+ normalizeWorkspaceFiles,
539
1891
  parseConfig,
1892
+ parseSceneStructure,
1893
+ planWorkspacePull,
1894
+ portableRuntimeWarning,
540
1895
  pullScene,
1896
+ pullWorkspace,
541
1897
  readConfigFile,
542
1898
  readDevkitVersion,
1899
+ remapErrorStack,
543
1900
  resolveConfig,
1901
+ resolveExampleScenePath,
1902
+ resolveRuntimeTypesSourceFile,
544
1903
  resolveSkillsSourceDir,
1904
+ resolveSnapshotUrl,
1905
+ resolveWorkspaceAssetPath,
545
1906
  saveSceneJson,
546
1907
  syncSkills,
1908
+ toConfigScenes,
1909
+ typesMissingHint,
547
1910
  uploadDirectory,
548
1911
  validateConfigShape,
549
- writeConfigFile
1912
+ workspacePullHasConflicts,
1913
+ workspacePullPendingWrites,
1914
+ writeConfigFile,
1915
+ writeConfigScenes
550
1916
  };
551
1917
  //# sourceMappingURL=index.js.map