@easytwin/devkit 0.1.1 → 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
@@ -3,7 +3,10 @@ import { promises as fs } from "fs";
3
3
  import path from "path";
4
4
  var CONFIG_FILE_NAME = "easytwin.config.json";
5
5
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
6
- var TEST_BASE_URL = "http://saas-twin-test.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";
7
10
  var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
8
11
  var MOCK_APP_ID = "test";
9
12
  var MOCK_APP_SECRET = "test";
@@ -48,12 +51,59 @@ function validateConfigShape(value) {
48
51
  if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
49
52
  throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
50
53
  }
54
+ const opAccountId = optionalGatewayId(v.opAccountId);
55
+ const opUserId = optionalGatewayId(v.opUserId);
56
+ const spaceId = optionalGatewayId(v.spaceId);
51
57
  const config = { appId: v.appId, appSecret: v.appSecret };
52
58
  if (v.env === "prod" || v.env === "test") config.env = v.env;
53
59
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
54
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;
55
66
  return config;
56
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
+ }
57
107
  async function readConfigFile(cwd) {
58
108
  const file = configFilePath(cwd);
59
109
  let raw;
@@ -73,10 +123,30 @@ function resolveConfig(file, env = process.env) {
73
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");
74
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");
75
125
  const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
76
- return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), 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;
77
135
  }
78
136
  async function loadConfig(cwd, env = process.env) {
79
- 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;
80
150
  }
81
151
  async function writeConfigFile(cwd, config) {
82
152
  const file = configFilePath(cwd);
@@ -84,6 +154,13 @@ async function writeConfigFile(cwd, config) {
84
154
  if (config.env) body.env = config.env;
85
155
  if (config.baseUrl) body.baseUrl = config.baseUrl;
86
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;
87
164
  await fs.mkdir(cwd, { recursive: true });
88
165
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
89
166
  return file;
@@ -111,18 +188,31 @@ async function initConfig(input, cwd) {
111
188
  const gitignore = await appendGitignore(cwd);
112
189
  return { configFile, gitignore };
113
190
  }
191
+ async function writeConfigScenes(cwd, scenes) {
192
+ const file = await readConfigFile(cwd);
193
+ await writeConfigFile(cwd, { ...file, scenes });
194
+ }
114
195
 
115
196
  // src/client.ts
116
197
  import http from "http";
117
198
  import https from "https";
118
- import { URL } from "url";
119
- var AUTH_HEADER = "Authorization";
120
- var BEARER_PREFIX = "Bearer";
121
- 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
+ }
122
207
  var ENDPOINTS = {
123
- scenes: "/api/scenes",
124
- scene: (id) => `/api/scenes/${encodeURIComponent(id)}`,
125
- 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`
126
216
  };
127
217
  var EasyTwinApiError = class extends Error {
128
218
  status;
@@ -150,28 +240,50 @@ function parseResponseBody(buffer) {
150
240
  return text;
151
241
  }
152
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
+ }
153
254
  var EasyTwinClient = class {
154
255
  baseUrl;
155
- /** 本地测试模式:true scene/upload 走本地 mock,不发网络请求(见 config.ts)。 */
156
- mock;
256
+ /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
257
+ ossUrl;
258
+ /** 接入凭证 App ID;同时作为场景/代码 API 的 applicationId 路径参数。 */
157
259
  appId;
260
+ /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
261
+ mock;
158
262
  appSecret;
263
+ opAccountId;
264
+ opUserId;
265
+ spaceId;
159
266
  constructor(config) {
160
267
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
268
+ this.ossUrl = config.ossUrl.replace(/\/+$/, "");
161
269
  this.mock = config.mock;
162
270
  this.appId = config.appId;
163
271
  this.appSecret = config.appSecret;
272
+ this.opAccountId = config.opAccountId;
273
+ this.opUserId = config.opUserId;
274
+ this.spaceId = config.spaceId;
164
275
  }
165
276
  /** 全仓唯一认证头注入点。 */
166
277
  authHeaders() {
167
- return {
168
- [AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
169
- [APP_ID_HEADER]: this.appId
170
- };
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;
171
283
  }
172
284
  /** JSON 请求(原生 fetch)。 */
173
- async request(path6, options = {}) {
174
- const url = `${this.baseUrl}${path6}`;
285
+ async request(path8, options = {}) {
286
+ const url = `${this.baseUrl}${path8}`;
175
287
  const headers = { ...this.authHeaders(), ...options.headers };
176
288
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
177
289
  headers["Content-Type"] = "application/json";
@@ -193,8 +305,8 @@ var EasyTwinClient = class {
193
305
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
194
306
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
195
307
  */
196
- async upload(path6, options) {
197
- const url = new URL(`${this.baseUrl}${path6}`);
308
+ async upload(path8, options) {
309
+ const url = new URL2(`${this.baseUrl}${path8}`);
198
310
  const mod = url.protocol === "https:" ? https : http;
199
311
  const headers = {
200
312
  ...this.authHeaders(),
@@ -227,8 +339,8 @@ var EasyTwinClient = class {
227
339
  return this.handleStatus(raw.status, raw.body);
228
340
  }
229
341
  handleStatus(status, body) {
230
- if (status >= 200 && status < 300) return parseResponseBody(body);
231
342
  const parsed = parseResponseBody(body);
343
+ if (status >= 200 && status < 300) return unwrapBody(parsed, status);
232
344
  throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
233
345
  }
234
346
  };
@@ -237,17 +349,89 @@ var EasyTwinClient = class {
237
349
  import { promises as fs2 } from "fs";
238
350
  import path2 from "path";
239
351
  import { fileURLToPath } from "url";
240
- function normalizeSceneList(data) {
241
- const list = Array.isArray(data) ? data : data?.list;
242
- if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
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);
243
374
  return list.map((item) => {
244
- const it = item ?? {};
245
- 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
+ };
392
+ });
393
+ }
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
+ }));
402
+ }
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;
246
410
  });
247
411
  }
248
- function normalizeSceneDetail(data) {
249
- const it = data ?? {};
250
- return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: 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
+ }
251
435
  }
252
436
  var EXAMPLE_SCENE_FILE = "scene.example.json";
253
437
  var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
@@ -280,21 +464,30 @@ async function exampleSceneSummary(exampleFile) {
280
464
  return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
281
465
  }
282
466
  async function listScenes(client, options = {}) {
283
- if (client.mock) return [await exampleSceneSummary(options.exampleFile)];
284
- const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
285
- return normalizeSceneList(data);
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;
286
470
  }
287
471
  async function pullScene(client, id, options = {}) {
288
472
  if (client.mock) {
289
- const payload = await loadExampleScene(options.exampleFile);
290
- const sceneId = deriveExampleSceneId(payload);
473
+ const payload2 = await loadExampleScene(options.exampleFile);
474
+ const sceneId = deriveExampleSceneId(payload2);
291
475
  if (sceneId !== id) {
292
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)`);
293
477
  }
294
- return { id: sceneId, name: MOCK_SCENE_NAME, payload };
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
+ );
295
488
  }
296
- const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
297
- return normalizeSceneDetail(data);
489
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
490
+ return { id: hit.sceneKey, name: hit.name, payload };
298
491
  }
299
492
  async function saveSceneJson(scene, out) {
300
493
  await fs2.mkdir(path2.dirname(out), { recursive: true });
@@ -404,6 +597,74 @@ Content-Type: application/octet-stream\r
404
597
  `, "utf8"));
405
598
  return Buffer.concat(parts);
406
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
+ }
407
668
  async function mockUpload(dir, options) {
408
669
  const ignore = options.ignore ?? (() => false);
409
670
  const files = await collectFiles(dir, ignore);
@@ -418,26 +679,312 @@ async function uploadDirectory(client, dir, options = {}) {
418
679
  const files = await collectFiles(dir, ignore);
419
680
  const total = files.reduce((sum, f) => sum + f.size, 0);
420
681
  options.onProgress?.({ phase: "collect", current: total, total });
421
- 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 = [];
422
698
  for (const file of files) {
423
- const content = await fs3.readFile(file.absPath);
424
- const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
425
- const body = buildMultipartBody(
426
- boundary,
427
- // TODO(swagger): multipart 字段名/结构待定,当前为占位约定。
428
- { path: file.relPath },
429
- { name: path3.basename(file.relPath), content }
430
- );
431
- 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) => {
432
707
  sent += file.size;
433
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);
434
723
  }
435
- 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 };
436
727
  }
437
728
 
438
- // src/skills.ts
439
- import { existsSync, promises as fs4 } from "fs";
729
+ // src/workspace.ts
730
+ import { promises as fs4 } from "fs";
731
+ import path5 from "path";
732
+
733
+ // src/assetsPath.ts
440
734
  import path4 from "path";
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";
441
988
  import { fileURLToPath as fileURLToPath2 } from "url";
442
989
  var SKILL_NAMES = [
443
990
  "easytwin-render",
@@ -461,7 +1008,8 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
461
1008
  skipLibCheck: true,
462
1009
  noEmit: true,
463
1010
  paths: {
464
- "@easytwin/runtime": [".easytwin/types"]
1011
+ "@easytwin/runtime": [".easytwin/types"],
1012
+ "@easytwin/apps": [".easytwin/types/apps"]
465
1013
  }
466
1014
  },
467
1015
  include: ["src/**/*.ts"]
@@ -473,22 +1021,55 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
473
1021
  var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
474
1022
  "skipLibCheck": true,
475
1023
  "paths": {
476
- "@easytwin/runtime": [".easytwin/types"]
1024
+ "@easytwin/runtime": [".easytwin/types"],
1025
+ "@easytwin/apps": [".easytwin/types/apps"]
477
1026
  }`;
478
- var RUN_CONTEXT_DECL = `
479
- /** \u9884\u89C8\u9875 Run \u6309\u94AE\u6CE8\u5165\u7684\u8FD0\u884C\u4E0A\u4E0B\u6587(\u89C1 D12)\u3002 */
480
- export interface EasyTwinRunContext {
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
+ };
481
1035
  engine: RuntimeEngine;
482
- runtime: typeof import("@easytwin/runtime");
483
- sceneJson: SceneJson;
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;
484
1066
  }
1067
+
1068
+ export declare function defineApp<T>(app: T): T;
485
1069
  `;
486
1070
  function buildRuntimeTypesContent(sourceDts) {
487
- const trimmed = sourceDts.replace(/\s+$/, "");
488
- if (trimmed.includes("export interface EasyTwinRunContext")) return `${trimmed}
1071
+ return `${sourceDts.replace(/\s+$/, "")}
489
1072
  `;
490
- return `${trimmed}
491
- ${RUN_CONTEXT_DECL}`;
492
1073
  }
493
1074
  function normalizeTargets(target = "all") {
494
1075
  if (target === "all") return ["cursor", "claude", "codex", "qoder"];
@@ -499,69 +1080,69 @@ function resolveSkillsSourceDir() {
499
1080
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
500
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");
501
1082
  }
502
- const here = path4.dirname(fileURLToPath2(import.meta.url));
503
- return path4.resolve(here, "..", "skills");
1083
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1084
+ return path6.resolve(here, "..", "skills");
504
1085
  }
505
1086
  function resolveRuntimeTypesSourceFile() {
506
1087
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
507
1088
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
508
1089
  }
509
- const here = path4.dirname(fileURLToPath2(import.meta.url));
510
- const fromDist = path4.join(here, "runtime-types", "index.d.ts");
511
- const fromSrc = path4.resolve(here, "lib", "index.d.ts");
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");
512
1093
  if (existsSync(fromDist)) return fromDist;
513
1094
  if (existsSync(fromSrc)) return fromSrc;
514
1095
  throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
515
1096
  }
516
1097
  async function readJson(file) {
517
- return JSON.parse(await fs4.readFile(file, "utf8"));
1098
+ return JSON.parse(await fs5.readFile(file, "utf8"));
518
1099
  }
519
1100
  async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
520
- const marker = path4.join(skillsDir, ".easytwin-source.json");
1101
+ const marker = path6.join(skillsDir, ".easytwin-source.json");
521
1102
  try {
522
1103
  const meta = await readJson(marker);
523
1104
  if (typeof meta.version === "string") return meta.version;
524
1105
  } catch {
525
1106
  }
526
1107
  try {
527
- const pkg = await readJson(path4.resolve(skillsDir, "..", "package.json"));
1108
+ const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
528
1109
  return pkg.version;
529
1110
  } catch {
530
1111
  return "0.0.0";
531
1112
  }
532
1113
  }
533
1114
  async function copyDir(src, dest) {
534
- await fs4.mkdir(dest, { recursive: true });
535
- const entries = await fs4.readdir(src, { withFileTypes: true });
1115
+ await fs5.mkdir(dest, { recursive: true });
1116
+ const entries = await fs5.readdir(src, { withFileTypes: true });
536
1117
  for (const entry of entries) {
537
- const s = path4.join(src, entry.name);
538
- const d = path4.join(dest, entry.name);
1118
+ const s = path6.join(src, entry.name);
1119
+ const d = path6.join(dest, entry.name);
539
1120
  if (entry.isDirectory()) await copyDir(s, d);
540
- else if (entry.isFile()) await fs4.copyFile(s, d);
1121
+ else if (entry.isFile()) await fs5.copyFile(s, d);
541
1122
  }
542
1123
  }
543
1124
  async function dirMatches(src, dest) {
544
1125
  let sourceEntries;
545
1126
  try {
546
- sourceEntries = await fs4.readdir(src);
1127
+ sourceEntries = await fs5.readdir(src);
547
1128
  } catch {
548
1129
  return false;
549
1130
  }
550
1131
  for (const name of sourceEntries) {
551
- const s = path4.join(src, name);
552
- const d = path4.join(dest, name);
553
- 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);
554
1135
  if (sStat.isDirectory()) {
555
1136
  if (!await dirMatches(s, d)) return false;
556
1137
  } else {
557
1138
  let dStat;
558
1139
  try {
559
- dStat = await fs4.stat(d);
1140
+ dStat = await fs5.stat(d);
560
1141
  } catch {
561
1142
  return false;
562
1143
  }
563
1144
  if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
564
- 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;
565
1146
  }
566
1147
  }
567
1148
  return true;
@@ -572,18 +1153,18 @@ function actionFor(exists, matches) {
572
1153
  }
573
1154
  async function writeMeta(destDir, version) {
574
1155
  const meta = { name: "@easytwin/devkit", version };
575
- 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");
576
1157
  }
577
1158
  async function syncToDir(target, sourceRoot, targetRoot, version) {
578
1159
  const entries = [];
579
1160
  for (const name of SKILL_NAMES) {
580
- const src = path4.join(sourceRoot, name);
581
- const dest = path4.join(targetRoot, name);
582
- 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);
583
1164
  const matches = await dirMatches(src, dest);
584
1165
  const action = actionFor(exists, matches);
585
1166
  if (action !== "unchanged") {
586
- await fs4.rm(dest, { recursive: true, force: true });
1167
+ await fs5.rm(dest, { recursive: true, force: true });
587
1168
  await copyDir(src, dest);
588
1169
  }
589
1170
  entries.push({ name, action });
@@ -602,19 +1183,19 @@ function buildCodexSegment(version) {
602
1183
  "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
603
1184
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
604
1185
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
605
- "- `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",
606
1187
  "",
607
1188
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
608
1189
  CODEX_MARKER_END
609
1190
  ].join("\n");
610
1191
  }
611
1192
  async function syncToCodex(sourceRoot, cwd, version) {
612
- const agentsFile = path4.join(cwd, "AGENTS.md");
1193
+ const agentsFile = path6.join(cwd, "AGENTS.md");
613
1194
  const segment = buildCodexSegment(version);
614
1195
  let content = "";
615
1196
  let exists = true;
616
1197
  try {
617
- content = await fs4.readFile(agentsFile, "utf8");
1198
+ content = await fs5.readFile(agentsFile, "utf8");
618
1199
  } catch {
619
1200
  exists = false;
620
1201
  }
@@ -632,39 +1213,50 @@ async function syncToCodex(sourceRoot, cwd, version) {
632
1213
  next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
633
1214
  }
634
1215
  if (action !== "unchanged") {
635
- await fs4.mkdir(cwd, { recursive: true });
636
- await fs4.writeFile(agentsFile, next, "utf8");
1216
+ await fs5.mkdir(cwd, { recursive: true });
1217
+ await fs5.writeFile(agentsFile, next, "utf8");
637
1218
  }
638
1219
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
639
1220
  }
640
1221
  async function syncRuntimeTypes(cwd, typesSourceFile) {
641
- const destDir = path4.join(cwd, ".easytwin", "types");
642
- const destFile = path4.join(destDir, "index.d.ts");
643
- const content = buildRuntimeTypesContent(await fs4.readFile(typesSourceFile, "utf8"));
644
- let exists = true;
645
- let current = "";
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;
646
1230
  try {
647
- current = await fs4.readFile(destFile, "utf8");
1231
+ runtimeCurrent = await fs5.readFile(destFile, "utf8");
648
1232
  } catch {
649
- exists = false;
1233
+ runtimeExists = false;
650
1234
  }
651
- const action = actionFor(exists, current === content);
652
- if (action !== "unchanged") {
653
- await fs4.mkdir(destDir, { recursive: true });
654
- await fs4.writeFile(destFile, content, "utf8");
1235
+ try {
1236
+ appsCurrent = await fs5.readFile(appsFile, "utf8");
1237
+ } catch {
1238
+ appsExists = false;
655
1239
  }
656
- const tsconfigPath = path4.join(cwd, "tsconfig.json");
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");
657
1249
  let tsconfig;
658
1250
  let pathsHint;
659
1251
  try {
660
- const existing = await fs4.readFile(tsconfigPath, "utf8");
1252
+ const existing = await fs5.readFile(tsconfigPath, "utf8");
661
1253
  if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
662
1254
  else {
663
1255
  tsconfig = "manual-paths";
664
1256
  pathsHint = TSCONFIG_PATHS_HINT;
665
1257
  }
666
1258
  } catch {
667
- await fs4.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1259
+ await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
668
1260
  tsconfig = "created";
669
1261
  }
670
1262
  return { action, tsconfig, pathsHint };
@@ -675,9 +1267,9 @@ async function syncSkills(options) {
675
1267
  const version = options.version ?? await readDevkitVersion(sourceRoot);
676
1268
  const summaries = [];
677
1269
  for (const target of targets) {
678
- if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
679
- else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
680
- else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path4.join(options.cwd, ".qoder", "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));
681
1273
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
682
1274
  }
683
1275
  const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
@@ -694,14 +1286,14 @@ async function detectSkillsStatus(cwd, sourceDir) {
694
1286
  qoder: ".qoder/skills"
695
1287
  };
696
1288
  for (const target of dirTargets) {
697
- const root = path4.join(cwd, DIR_ROOTS[target]);
1289
+ const root = path6.join(cwd, DIR_ROOTS[target]);
698
1290
  const missing = [];
699
1291
  for (const name of SKILL_NAMES) {
700
- 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);
701
1293
  }
702
1294
  let stale = false;
703
1295
  try {
704
- const meta = await readJson(path4.join(root, META_FILE_NAME));
1296
+ const meta = await readJson(path6.join(root, META_FILE_NAME));
705
1297
  stale = meta.version !== version;
706
1298
  } catch {
707
1299
  stale = missing.length === 0;
@@ -714,15 +1306,16 @@ async function detectSkillsStatus(cwd, sourceDir) {
714
1306
  }
715
1307
  let agentsContent = "";
716
1308
  try {
717
- agentsContent = await fs4.readFile(path4.join(cwd, "AGENTS.md"), "utf8");
1309
+ agentsContent = await fs5.readFile(path6.join(cwd, "AGENTS.md"), "utf8");
718
1310
  } catch {
719
1311
  }
720
1312
  const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
721
1313
  targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
722
1314
  let typesSynced = false;
723
1315
  try {
724
- const dts = await fs4.readFile(path4.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
725
- typesSynced = dts.includes("export interface EasyTwinRunContext");
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");
726
1319
  } catch {
727
1320
  typesSynced = false;
728
1321
  }
@@ -731,11 +1324,57 @@ async function detectSkillsStatus(cwd, sourceDir) {
731
1324
 
732
1325
  // src/bundle.ts
733
1326
  import * as esbuild from "esbuild-wasm";
734
- import { promises as fs5 } from "fs";
735
- import path5 from "path";
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
736
1373
  var USER_ENTRY = "src/main.ts";
737
1374
  var DEFAULT_BUNDLE_OUT = "dist/main.js";
738
1375
  var RUNTIME_MODULE = "@easytwin/runtime";
1376
+ var APPS_MODULE = "@easytwin/apps";
1377
+ var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
739
1378
  var BundleError = class extends Error {
740
1379
  constructor(message) {
741
1380
  super(message);
@@ -743,7 +1382,7 @@ var BundleError = class extends Error {
743
1382
  }
744
1383
  };
745
1384
  function isRelativeOrAbsolute(spec) {
746
- return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path5.isAbsolute(spec);
1385
+ return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
747
1386
  }
748
1387
  function whitelistPlugin() {
749
1388
  return {
@@ -752,11 +1391,11 @@ function whitelistPlugin() {
752
1391
  build2.onResolve({ filter: /.*/ }, (args) => {
753
1392
  if (args.kind === "entry-point") return void 0;
754
1393
  if (isRelativeOrAbsolute(args.path)) return void 0;
755
- if (args.path === RUNTIME_MODULE) return { path: args.path, external: true };
1394
+ if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
756
1395
  return {
757
1396
  errors: [
758
1397
  {
759
- text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
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`
760
1399
  }
761
1400
  ]
762
1401
  };
@@ -770,14 +1409,29 @@ function formatEsbuildMessages(messages) {
770
1409
  return `${loc}${m.text}`;
771
1410
  }).join("\n");
772
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
+ }
773
1427
  async function bundleUserCode(options) {
774
- const cwd = path5.resolve(options.cwd);
775
- const entry = path5.join(cwd, USER_ENTRY);
1428
+ const cwd = path7.resolve(options.cwd);
1429
+ const entry = path7.join(cwd, USER_ENTRY);
776
1430
  try {
777
- await fs5.access(entry);
1431
+ await fs6.access(entry);
778
1432
  } catch {
779
1433
  throw new BundleError(
780
- `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default async function main(ctx)\`\u3002`
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`
781
1435
  );
782
1436
  }
783
1437
  let result;
@@ -808,16 +1462,18 @@ async function bundleUserCode(options) {
808
1462
  if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
809
1463
  const code = file.text;
810
1464
  const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
1465
+ const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
1466
+ if (portable) warnings.push(portable);
811
1467
  if (options.outFile) {
812
- const outFile = path5.isAbsolute(options.outFile) ? options.outFile : path5.join(cwd, options.outFile);
813
- await fs5.mkdir(path5.dirname(outFile), { recursive: true });
814
- await fs5.writeFile(outFile, code, "utf8");
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");
815
1471
  }
816
1472
  return { code, warnings };
817
1473
  }
818
1474
  async function typesMissingHint(cwd) {
819
1475
  try {
820
- await fs5.access(path5.join(cwd, ".easytwin", "types", "index.d.ts"));
1476
+ await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
821
1477
  return void 0;
822
1478
  } catch {
823
1479
  return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
@@ -911,10 +1567,258 @@ function remapErrorStack(stack, bundledCode) {
911
1567
  return `${orig.source}:${orig.line}:${orig.column}`;
912
1568
  });
913
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
+ };
914
1817
  export {
915
- APP_ID_HEADER,
1818
+ APPS_DTS,
1819
+ APPS_MODULE,
1820
+ APPS_TYPES_FILE,
916
1821
  AUTH_HEADER,
917
- BEARER_PREFIX,
918
1822
  BundleError,
919
1823
  CODEX_MARKER_BEGIN,
920
1824
  CODEX_MARKER_END,
@@ -922,6 +1826,8 @@ export {
922
1826
  ConfigError,
923
1827
  DEFAULT_BASE_URL,
924
1828
  DEFAULT_BUNDLE_OUT,
1829
+ DEFAULT_ENTRY_PATH,
1830
+ DEFAULT_MAIN_TS,
925
1831
  DEFAULT_OSS_URL,
926
1832
  EASYTWIN_TYPES_DIR,
927
1833
  ENDPOINTS,
@@ -930,37 +1836,64 @@ export {
930
1836
  EasyTwinClient,
931
1837
  GITIGNORE_ENTRY,
932
1838
  GITIGNORE_FILE_NAME,
1839
+ LOCAL_LOAD_SCENE_ERROR,
933
1840
  META_FILE_NAME,
934
1841
  MINIMAL_TSCONFIG,
1842
+ MISSING_TICK_ERROR,
935
1843
  MOCK_APP_ID,
936
1844
  MOCK_APP_SECRET,
937
1845
  MOCK_SCENE_NAME,
1846
+ OP_ACCOUNT_ID_HEADER,
1847
+ OP_USER_ID_HEADER,
1848
+ PORTABLE_RUNTIME_EXPORTS,
938
1849
  RUNTIME_MODULE,
939
1850
  SKILL_NAMES,
1851
+ SPACE_ID_HEADER,
940
1852
  TEST_BASE_URL,
1853
+ TEST_OP_ACCOUNT_ID,
1854
+ TEST_OP_USER_ID,
1855
+ TEST_SPACE_ID,
941
1856
  TSCONFIG_PATHS_HINT,
1857
+ TwinApp,
1858
+ TwinAppPreviewHost,
942
1859
  USER_ENTRY,
943
1860
  appendGitignore,
1861
+ applyWorkspacePull,
1862
+ assertUploadable,
944
1863
  buildMultipartBody,
945
1864
  buildRuntimeTypesContent,
946
1865
  bundleUserCode,
947
1866
  collectFiles,
948
1867
  configFilePath,
1868
+ createAppInstance,
1869
+ defaultPullIgnore,
1870
+ defineApp,
949
1871
  deriveExampleSceneId,
950
1872
  detectSkillsStatus,
1873
+ directoryDepth,
951
1874
  exampleSceneSummary,
952
1875
  extractInlineSourceMap,
1876
+ extractSceneArray,
1877
+ formatWorkspacePullPlan,
1878
+ formatWorkspacePullResult,
953
1879
  initConfig,
954
1880
  isMockCredentials,
1881
+ isSafeRelPath,
955
1882
  listScenes,
956
1883
  loadConfig,
957
1884
  loadExampleScene,
958
- normalizeSceneDetail,
1885
+ normalizeLinkedScenes,
1886
+ normalizePath,
959
1887
  normalizeSceneList,
960
1888
  normalizeTargets,
1889
+ normalizeWorkspaceConfig,
1890
+ normalizeWorkspaceFiles,
961
1891
  parseConfig,
962
1892
  parseSceneStructure,
1893
+ planWorkspacePull,
1894
+ portableRuntimeWarning,
963
1895
  pullScene,
1896
+ pullWorkspace,
964
1897
  readConfigFile,
965
1898
  readDevkitVersion,
966
1899
  remapErrorStack,
@@ -968,11 +1901,17 @@ export {
968
1901
  resolveExampleScenePath,
969
1902
  resolveRuntimeTypesSourceFile,
970
1903
  resolveSkillsSourceDir,
1904
+ resolveSnapshotUrl,
1905
+ resolveWorkspaceAssetPath,
971
1906
  saveSceneJson,
972
1907
  syncSkills,
1908
+ toConfigScenes,
973
1909
  typesMissingHint,
974
1910
  uploadDirectory,
975
1911
  validateConfigShape,
976
- writeConfigFile
1912
+ workspacePullHasConflicts,
1913
+ workspacePullPendingWrites,
1914
+ writeConfigFile,
1915
+ writeConfigScenes
977
1916
  };
978
1917
  //# sourceMappingURL=index.js.map