@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/bin.js CHANGED
@@ -7,14 +7,17 @@ import { readFile } from "fs/promises";
7
7
  import { createInterface } from "readline/promises";
8
8
  import { stdin, stdout } from "process";
9
9
  import { pathToFileURL } from "url";
10
- import path6 from "path";
10
+ import path8 from "path";
11
11
 
12
12
  // src/config.ts
13
13
  import { promises as fs } from "fs";
14
14
  import path from "path";
15
15
  var CONFIG_FILE_NAME = "easytwin.config.json";
16
16
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
17
- var TEST_BASE_URL = "http://saas-twin-test.k8s.dtstack.cn/";
17
+ var TEST_BASE_URL = "http://172.16.125.3:10100/";
18
+ var TEST_OP_ACCOUNT_ID = "25";
19
+ var TEST_OP_USER_ID = "25";
20
+ var TEST_SPACE_ID = "54";
18
21
  var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
19
22
  var MOCK_APP_ID = "test";
20
23
  var MOCK_APP_SECRET = "test";
@@ -59,12 +62,59 @@ function validateConfigShape(value) {
59
62
  if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
60
63
  throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
61
64
  }
65
+ const opAccountId = optionalGatewayId(v.opAccountId);
66
+ const opUserId = optionalGatewayId(v.opUserId);
67
+ const spaceId = optionalGatewayId(v.spaceId);
62
68
  const config = { appId: v.appId, appSecret: v.appSecret };
63
69
  if (v.env === "prod" || v.env === "test") config.env = v.env;
64
70
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
65
71
  if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
72
+ if (opAccountId) config.opAccountId = opAccountId;
73
+ if (opUserId) config.opUserId = opUserId;
74
+ if (spaceId) config.spaceId = spaceId;
75
+ const scenes = parseConfigScenes(v.scenes);
76
+ if (scenes) config.scenes = scenes;
66
77
  return config;
67
78
  }
79
+ function optionalGatewayId(value) {
80
+ if (typeof value === "string") {
81
+ const t = value.trim();
82
+ return t.length > 0 ? t : void 0;
83
+ }
84
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
85
+ if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
86
+ return void 0;
87
+ }
88
+ function parseConfigScenes(value) {
89
+ if (value === void 0) return void 0;
90
+ if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
91
+ return value.map((item, i) => {
92
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
93
+ throw new ConfigError(`scenes[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
94
+ }
95
+ const it = item;
96
+ if (typeof it.id !== "string" || it.id.length === 0) {
97
+ throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 id`);
98
+ }
99
+ if (typeof it.name !== "string" || it.name.length === 0) {
100
+ throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 name`);
101
+ }
102
+ const scene = { id: it.id, name: it.name };
103
+ if (it.linkedSceneId !== void 0) {
104
+ if (typeof it.linkedSceneId !== "string") throw new ConfigError(`scenes[${i}].linkedSceneId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
105
+ if (it.linkedSceneId.length > 0) scene.linkedSceneId = it.linkedSceneId;
106
+ }
107
+ if (it.snapshotUrl !== void 0) {
108
+ if (typeof it.snapshotUrl !== "string") throw new ConfigError(`scenes[${i}].snapshotUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
109
+ if (it.snapshotUrl.length > 0) scene.snapshotUrl = it.snapshotUrl;
110
+ }
111
+ if (it.defaultLoading !== void 0) {
112
+ if (typeof it.defaultLoading !== "boolean") throw new ConfigError(`scenes[${i}].defaultLoading \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
113
+ scene.defaultLoading = it.defaultLoading;
114
+ }
115
+ return scene;
116
+ });
117
+ }
68
118
  async function readConfigFile(cwd) {
69
119
  const file = configFilePath(cwd);
70
120
  let raw;
@@ -84,10 +134,30 @@ function resolveConfig(file, env = process.env) {
84
134
  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");
85
135
  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");
86
136
  const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
87
- return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
137
+ const useTestGateway = easyEnv === "test";
138
+ const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
139
+ const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
140
+ const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
141
+ const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
142
+ if (opAccountId) resolved.opAccountId = opAccountId;
143
+ if (opUserId) resolved.opUserId = opUserId;
144
+ if (spaceId) resolved.spaceId = spaceId;
145
+ return resolved;
88
146
  }
89
147
  async function loadConfig(cwd, env = process.env) {
90
- return resolveConfig(await readConfigFile(cwd), env);
148
+ const file = await readConfigFile(cwd);
149
+ const resolved = resolveConfig(file, env);
150
+ if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
151
+ if (!file.opAccountId || !file.opUserId || !file.spaceId) {
152
+ await writeConfigFile(cwd, {
153
+ ...file,
154
+ opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
155
+ opUserId: file.opUserId ?? TEST_OP_USER_ID,
156
+ spaceId: file.spaceId ?? TEST_SPACE_ID
157
+ });
158
+ }
159
+ }
160
+ return resolved;
91
161
  }
92
162
  async function writeConfigFile(cwd, config) {
93
163
  const file = configFilePath(cwd);
@@ -95,6 +165,13 @@ async function writeConfigFile(cwd, config) {
95
165
  if (config.env) body.env = config.env;
96
166
  if (config.baseUrl) body.baseUrl = config.baseUrl;
97
167
  if (config.ossUrl) body.ossUrl = config.ossUrl;
168
+ const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
169
+ const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
170
+ const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
171
+ if (opAccountId) body.opAccountId = opAccountId;
172
+ if (opUserId) body.opUserId = opUserId;
173
+ if (spaceId) body.spaceId = spaceId;
174
+ if (config.scenes !== void 0) body.scenes = config.scenes;
98
175
  await fs.mkdir(cwd, { recursive: true });
99
176
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
100
177
  return file;
@@ -122,18 +199,31 @@ async function initConfig(input, cwd) {
122
199
  const gitignore = await appendGitignore(cwd);
123
200
  return { configFile, gitignore };
124
201
  }
202
+ async function writeConfigScenes(cwd, scenes) {
203
+ const file = await readConfigFile(cwd);
204
+ await writeConfigFile(cwd, { ...file, scenes });
205
+ }
125
206
 
126
207
  // src/client.ts
127
208
  import http from "http";
128
209
  import https from "https";
129
210
  import { URL as URL2 } from "url";
130
- var AUTH_HEADER = "Authorization";
131
- var BEARER_PREFIX = "Bearer";
132
- var APP_ID_HEADER = "X-Easytwin-App-Id";
211
+ var AUTH_HEADER = "x-app-secret";
212
+ var OP_ACCOUNT_ID_HEADER = "op-account-id";
213
+ var OP_USER_ID_HEADER = "op-user-id";
214
+ var SPACE_ID_HEADER = "space-id";
215
+ function enc(id) {
216
+ return encodeURIComponent(id);
217
+ }
133
218
  var ENDPOINTS = {
134
- scenes: "/api/scenes",
135
- scene: (id) => `/api/scenes/${encodeURIComponent(id)}`,
136
- upload: "/api/upload"
219
+ /** GET 已关联场景列表。 */
220
+ linkedScenes: (applicationId) => `/api/twin/v1/sdk-application-scenes/${enc(applicationId)}/scenes`,
221
+ /** GET 工作区全部代码文件。 */
222
+ workspaceCode: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}`,
223
+ /** GET 工作区文件约束。 */
224
+ workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`,
225
+ /** POST 新建 / PATCH 批量更新 / DELETE 批量删除。 */
226
+ workspaceCodeFiles: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/files`
137
227
  };
138
228
  var EasyTwinApiError = class extends Error {
139
229
  status;
@@ -161,28 +251,50 @@ function parseResponseBody(buffer) {
161
251
  return text;
162
252
  }
163
253
  }
254
+ function isEnvelope(value) {
255
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "success" in value;
256
+ }
257
+ function unwrapBody(parsed, status) {
258
+ if (!isEnvelope(parsed)) return parsed;
259
+ if (parsed.success === false) {
260
+ throw new EasyTwinApiError(status, messageFromBody(parsed) ?? "\u8BF7\u6C42\u5931\u8D25", parsed);
261
+ }
262
+ if (parsed.success === true && "data" in parsed) return parsed.data;
263
+ return parsed;
264
+ }
164
265
  var EasyTwinClient = class {
165
266
  baseUrl;
166
- /** 本地测试模式:true scene/upload 走本地 mock,不发网络请求(见 config.ts)。 */
167
- mock;
267
+ /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
268
+ ossUrl;
269
+ /** 接入凭证 App ID;同时作为场景/代码 API 的 applicationId 路径参数。 */
168
270
  appId;
271
+ /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
272
+ mock;
169
273
  appSecret;
274
+ opAccountId;
275
+ opUserId;
276
+ spaceId;
170
277
  constructor(config) {
171
278
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
279
+ this.ossUrl = config.ossUrl.replace(/\/+$/, "");
172
280
  this.mock = config.mock;
173
281
  this.appId = config.appId;
174
282
  this.appSecret = config.appSecret;
283
+ this.opAccountId = config.opAccountId;
284
+ this.opUserId = config.opUserId;
285
+ this.spaceId = config.spaceId;
175
286
  }
176
287
  /** 全仓唯一认证头注入点。 */
177
288
  authHeaders() {
178
- return {
179
- [AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
180
- [APP_ID_HEADER]: this.appId
181
- };
289
+ const headers = { [AUTH_HEADER]: this.appSecret };
290
+ if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
291
+ if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
292
+ if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
293
+ return headers;
182
294
  }
183
295
  /** JSON 请求(原生 fetch)。 */
184
- async request(path7, options = {}) {
185
- const url = `${this.baseUrl}${path7}`;
296
+ async request(path9, options = {}) {
297
+ const url = `${this.baseUrl}${path9}`;
186
298
  const headers = { ...this.authHeaders(), ...options.headers };
187
299
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
188
300
  headers["Content-Type"] = "application/json";
@@ -204,8 +316,8 @@ var EasyTwinClient = class {
204
316
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
205
317
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
206
318
  */
207
- async upload(path7, options) {
208
- const url = new URL2(`${this.baseUrl}${path7}`);
319
+ async upload(path9, options) {
320
+ const url = new URL2(`${this.baseUrl}${path9}`);
209
321
  const mod = url.protocol === "https:" ? https : http;
210
322
  const headers = {
211
323
  ...this.authHeaders(),
@@ -238,8 +350,8 @@ var EasyTwinClient = class {
238
350
  return this.handleStatus(raw.status, raw.body);
239
351
  }
240
352
  handleStatus(status, body) {
241
- if (status >= 200 && status < 300) return parseResponseBody(body);
242
353
  const parsed = parseResponseBody(body);
354
+ if (status >= 200 && status < 300) return unwrapBody(parsed, status);
243
355
  throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
244
356
  }
245
357
  };
@@ -248,17 +360,89 @@ var EasyTwinClient = class {
248
360
  import { promises as fs2 } from "fs";
249
361
  import path2 from "path";
250
362
  import { fileURLToPath } from "url";
251
- function normalizeSceneList(data) {
252
- const list = Array.isArray(data) ? data : data?.list;
253
- if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
363
+ function asRecord(value) {
364
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
365
+ }
366
+ function asString(value) {
367
+ return typeof value === "string" ? value : "";
368
+ }
369
+ function asBoolean(value) {
370
+ return value === true;
371
+ }
372
+ function extractSceneArray(data) {
373
+ if (Array.isArray(data)) return data;
374
+ const root = asRecord(data);
375
+ if (Array.isArray(root.data)) return root.data;
376
+ if (Array.isArray(root.list)) return root.list;
377
+ if (Array.isArray(root.scenes)) return root.scenes;
378
+ const nested = asRecord(root.data);
379
+ if (Array.isArray(nested.scenes)) return nested.scenes;
380
+ if (Array.isArray(nested.list)) return nested.list;
381
+ throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
382
+ }
383
+ function normalizeLinkedScenes(data) {
384
+ const list = extractSceneArray(data);
254
385
  return list.map((item) => {
255
- const it = item ?? {};
256
- return { id: String(it.id ?? ""), name: String(it.name ?? it.id ?? "") };
386
+ const it = asRecord(item);
387
+ const sceneKey = asString(it.sceneKey);
388
+ const sourceProjectName = asString(it.sourceProjectName);
389
+ const sourceSceneName = asString(it.sourceSceneName);
390
+ return {
391
+ id: asString(it.id),
392
+ sceneKey,
393
+ name: sourceProjectName || sourceSceneName || sceneKey,
394
+ defaultLoading: asBoolean(it.defaultLoading),
395
+ sourceSceneId: asString(it.sourceSceneId),
396
+ sourceProjectId: asString(it.sourceProjectId),
397
+ sourceSceneName,
398
+ sourceProjectName,
399
+ sourceLost: asBoolean(it.sourceLost),
400
+ snapshotUrl: asString(it.snapshotUrl),
401
+ snapshotUpdatedAt: asString(it.snapshotUpdatedAt)
402
+ };
257
403
  });
258
404
  }
259
- function normalizeSceneDetail(data) {
260
- const it = data ?? {};
261
- return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: data };
405
+ function normalizeSceneList(data) {
406
+ return normalizeLinkedScenes(data).map((s) => ({
407
+ id: s.sceneKey,
408
+ name: s.name,
409
+ linkedSceneId: s.id,
410
+ snapshotUrl: s.snapshotUrl,
411
+ defaultLoading: s.defaultLoading
412
+ }));
413
+ }
414
+ function toConfigScenes(scenes) {
415
+ return scenes.map((s) => {
416
+ const item = { id: s.id, name: s.name };
417
+ if (s.linkedSceneId) item.linkedSceneId = s.linkedSceneId;
418
+ if (s.snapshotUrl) item.snapshotUrl = s.snapshotUrl;
419
+ if (s.defaultLoading !== void 0) item.defaultLoading = s.defaultLoading;
420
+ return item;
421
+ });
422
+ }
423
+ function resolveSnapshotUrl(ossUrl, snapshotUrl) {
424
+ if (/^https?:\/\//i.test(snapshotUrl)) return snapshotUrl;
425
+ const base = ossUrl.replace(/\/+$/, "");
426
+ const rel = snapshotUrl.replace(/^\/+/, "");
427
+ return `${base}/${rel}`;
428
+ }
429
+ async function fetchSnapshotJson(ossUrl, snapshotUrl) {
430
+ if (snapshotUrl.length === 0) throw new Error("\u573A\u666F\u5FEB\u7167\u5730\u5740\u4E3A\u7A7A");
431
+ const url = resolveSnapshotUrl(ossUrl, snapshotUrl);
432
+ let res;
433
+ try {
434
+ res = await fetch(url);
435
+ } catch (err) {
436
+ throw new EasyTwinApiError(0, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:${url}:${err instanceof Error ? err.message : String(err)}`);
437
+ }
438
+ if (!res.ok) {
439
+ throw new EasyTwinApiError(res.status, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:HTTP ${res.status} ${url}`);
440
+ }
441
+ try {
442
+ return await res.json();
443
+ } catch {
444
+ throw new Error(`\u573A\u666F\u5FEB\u7167\u4E0D\u662F\u5408\u6CD5 JSON:${url}`);
445
+ }
262
446
  }
263
447
  var EXAMPLE_SCENE_FILE = "scene.example.json";
264
448
  var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
@@ -291,21 +475,30 @@ async function exampleSceneSummary(exampleFile) {
291
475
  return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
292
476
  }
293
477
  async function listScenes(client, options = {}) {
294
- if (client.mock) return [await exampleSceneSummary(options.exampleFile)];
295
- const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
296
- return normalizeSceneList(data);
478
+ const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" }));
479
+ if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
480
+ return scenes;
297
481
  }
298
482
  async function pullScene(client, id, options = {}) {
299
483
  if (client.mock) {
300
- const payload = await loadExampleScene(options.exampleFile);
301
- const sceneId = deriveExampleSceneId(payload);
484
+ const payload2 = await loadExampleScene(options.exampleFile);
485
+ const sceneId = deriveExampleSceneId(payload2);
302
486
  if (sceneId !== id) {
303
487
  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)`);
304
488
  }
305
- return { id: sceneId, name: MOCK_SCENE_NAME, payload };
489
+ return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
306
490
  }
307
- const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
308
- return normalizeSceneDetail(data);
491
+ const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
492
+ const scenes = normalizeLinkedScenes(data);
493
+ const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
494
+ if (!hit) {
495
+ const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
496
+ throw new Error(
497
+ available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
498
+ );
499
+ }
500
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
501
+ return { id: hit.sceneKey, name: hit.name, payload };
309
502
  }
310
503
  async function saveSceneJson(scene, out) {
311
504
  await fs2.mkdir(path2.dirname(out), { recursive: true });
@@ -338,32 +531,73 @@ async function collectFiles(dir, ignore = () => false) {
338
531
  files.sort((a, b) => a.relPath.localeCompare(b.relPath));
339
532
  return files;
340
533
  }
341
- function buildMultipartBody(boundary, fields, file) {
342
- const parts = [];
343
- for (const [key, value] of Object.entries(fields)) {
344
- parts.push(
345
- Buffer.from(`--${boundary}\r
346
- Content-Disposition: form-data; name="${key}"\r
347
- \r
348
- ${value}\r
349
- `, "utf8")
534
+ function asRecord2(value) {
535
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
536
+ }
537
+ function asString2(value) {
538
+ return typeof value === "string" ? value : "";
539
+ }
540
+ function asNumber(value) {
541
+ return typeof value === "number" && Number.isFinite(value) ? value : NaN;
542
+ }
543
+ function normalizePath(relPath) {
544
+ return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
545
+ }
546
+ function normalizeWorkspaceConfig(data) {
547
+ const it = asRecord2(data);
548
+ if (!Array.isArray(it.allowedFileExtensions)) throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
549
+ const maxCodeFiles = asNumber(it.maxCodeFiles);
550
+ const maxCodeDirectoryDepth = asNumber(it.maxCodeDirectoryDepth);
551
+ if (!Number.isFinite(maxCodeFiles) || !Number.isFinite(maxCodeDirectoryDepth)) {
552
+ throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
553
+ }
554
+ return {
555
+ allowedFileExtensions: it.allowedFileExtensions.filter((e) => typeof e === "string"),
556
+ maxCodeFiles,
557
+ maxCodeDirectoryDepth
558
+ };
559
+ }
560
+ function normalizeWorkspaceFiles(data) {
561
+ if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
562
+ return data.map((item) => {
563
+ const it = asRecord2(item);
564
+ return { id: asString2(it.id), filePath: asString2(it.filePath), content: asString2(it.content) };
565
+ });
566
+ }
567
+ function extensionOf(relPath) {
568
+ const base = relPath.split("/").pop() ?? "";
569
+ const i = base.lastIndexOf(".");
570
+ if (i <= 0) return "";
571
+ return base.slice(i).toLowerCase();
572
+ }
573
+ function allowedExtensionSet(list) {
574
+ return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
575
+ }
576
+ function directoryDepth(relPath) {
577
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
578
+ return Math.max(0, parts.length - 1);
579
+ }
580
+ function assertUploadable(files, config) {
581
+ const errors = [];
582
+ if (files.length > config.maxCodeFiles) {
583
+ errors.push(`\u6587\u4EF6\u6570 ${files.length} \u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeFiles}`);
584
+ }
585
+ const tooDeep = files.filter((f) => directoryDepth(f.relPath) > config.maxCodeDirectoryDepth);
586
+ if (tooDeep.length > 0) {
587
+ errors.push(
588
+ `\u76EE\u5F55\u6DF1\u5EA6\u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeDirectoryDepth}:${tooDeep.map((f) => f.relPath).join(", ")}`
350
589
  );
351
590
  }
352
- parts.push(
353
- Buffer.from(
354
- `--${boundary}\r
355
- Content-Disposition: form-data; name="file"; filename="${file.name}"\r
356
- Content-Type: application/octet-stream\r
357
- \r
358
- `,
359
- "utf8"
360
- )
361
- );
362
- parts.push(file.content);
363
- parts.push(Buffer.from(`\r
364
- --${boundary}--\r
365
- `, "utf8"));
366
- return Buffer.concat(parts);
591
+ if (config.allowedFileExtensions.length > 0) {
592
+ const allowed = allowedExtensionSet(config.allowedFileExtensions);
593
+ const bad = files.filter((f) => !allowed.has(extensionOf(f.relPath)));
594
+ if (bad.length > 0) {
595
+ errors.push(
596
+ `\u6269\u5C55\u540D\u4E0D\u5728\u5141\u8BB8\u5217\u8868(${config.allowedFileExtensions.join(", ")}):${bad.map((f) => f.relPath).join(", ")}`
597
+ );
598
+ }
599
+ }
600
+ if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
367
601
  }
368
602
  async function mockUpload(dir, options) {
369
603
  const ignore = options.ignore ?? (() => false);
@@ -379,26 +613,306 @@ async function uploadDirectory(client, dir, options = {}) {
379
613
  const files = await collectFiles(dir, ignore);
380
614
  const total = files.reduce((sum, f) => sum + f.size, 0);
381
615
  options.onProgress?.({ phase: "collect", current: total, total });
382
- let sent = 0;
616
+ const appId = client.appId;
617
+ const wsConfig = normalizeWorkspaceConfig(await client.request(ENDPOINTS.workspaceConfig(appId), { method: "GET" }));
618
+ assertUploadable(files, wsConfig);
619
+ const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
620
+ const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
621
+ const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
622
+ const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
623
+ if (toDelete.length > 0) {
624
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
625
+ method: "DELETE",
626
+ body: JSON.stringify({ ids: toDelete })
627
+ });
628
+ }
629
+ const creates = [];
630
+ const patches = [];
631
+ const unchanged = [];
383
632
  for (const file of files) {
384
- const content = await fs3.readFile(file.absPath);
385
- const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
386
- const body = buildMultipartBody(
387
- boundary,
388
- // TODO(swagger): multipart 字段名/结构待定,当前为占位约定。
389
- { path: file.relPath },
390
- { name: path3.basename(file.relPath), content }
391
- );
392
- await client.upload(ENDPOINTS.upload, { boundary, body });
633
+ const content = await fs3.readFile(file.absPath, "utf8");
634
+ const remoteFile = remoteByPath.get(normalizePath(file.relPath));
635
+ if (!remoteFile) creates.push({ file, content });
636
+ else if (remoteFile.content !== content) patches.push({ file, id: remoteFile.id, content });
637
+ else unchanged.push(file);
638
+ }
639
+ let sent = 0;
640
+ const bump = (file) => {
393
641
  sent += file.size;
394
642
  options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
643
+ };
644
+ if (patches.length > 0) {
645
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
646
+ method: "PATCH",
647
+ body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
648
+ });
649
+ for (const p of patches) bump(p.file);
395
650
  }
396
- return { fileCount: files.length, byteCount: sent };
651
+ for (const c of creates) {
652
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
653
+ method: "POST",
654
+ body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
655
+ });
656
+ bump(c.file);
657
+ }
658
+ for (const u of unchanged) bump(u);
659
+ if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
660
+ return { fileCount: files.length, byteCount: total };
397
661
  }
398
662
 
399
- // src/skills.ts
400
- import { existsSync, promises as fs4 } from "fs";
663
+ // src/workspace.ts
664
+ import { promises as fs4 } from "fs";
665
+ import path5 from "path";
666
+
667
+ // src/assetsPath.ts
401
668
  import path4 from "path";
669
+ function resolveWorkspaceAssetPath(cwd, relPath) {
670
+ const root = path4.resolve(cwd);
671
+ const input = relPath.trim();
672
+ if (!input) throw new Error("assets \u8DEF\u5F84\u4E3A\u7A7A");
673
+ 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}`);
674
+ const resolved = path4.resolve(root, input);
675
+ const rel = path4.relative(root, resolved);
676
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
677
+ throw new Error(`assets \u8DEF\u5F84\u9003\u9038\u5DE5\u4F5C\u533A: ${relPath}`);
678
+ }
679
+ return resolved;
680
+ }
681
+
682
+ // src/workspace.ts
683
+ var DEFAULT_ENTRY_PATH = "src/main.ts";
684
+ var DEFAULT_MAIN_TS = `import { TwinApp, type TwinAppContext } from "@easytwin/apps";
685
+
686
+ export default class App extends TwinApp {
687
+ async init(ctx: TwinAppContext) {
688
+ // engine / container \u5DF2\u6709;scene / camera \u4E3A null
689
+ void ctx;
690
+ }
691
+
692
+ async onSceneLoaded(ctx: TwinAppContext) {
693
+ // \u573A\u666F\u5B57\u6BB5\u6709\u503C;\u5728\u6B64\u52A0\u7269\u4F53 / \u7ED1\u4E8B\u4EF6,\u5E76\u7528 ctx.sceneCleanup \u5BF9\u79F0\u62C6\u9664
694
+ void ctx;
695
+ }
696
+
697
+ onUpdate(ctx: TwinAppContext, delta: number, elapsed: number) {
698
+ void ctx;
699
+ void delta;
700
+ void elapsed;
701
+ }
702
+
703
+ onDispose(ctx: TwinAppContext) {
704
+ void ctx;
705
+ }
706
+ }
707
+ `;
708
+ var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
709
+ ".git",
710
+ "node_modules",
711
+ "dist",
712
+ ".easytwin",
713
+ ".cursor",
714
+ ".claude",
715
+ ".qoder",
716
+ ".vscode"
717
+ ]);
718
+ var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
719
+ var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
720
+ function defaultPullIgnore(relPath) {
721
+ const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
722
+ if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
723
+ const base = parts[parts.length - 1];
724
+ return base !== void 0 && PULL_IGNORED_FILES.has(base);
725
+ }
726
+ function isSafeRelPath(relPath) {
727
+ const n = normalizePath(relPath);
728
+ if (!n) return false;
729
+ if (n.toLowerCase() === "easytwin.config.json") return false;
730
+ if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
731
+ const parts = n.split("/");
732
+ if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
733
+ return true;
734
+ }
735
+ function extensionOf2(relPath) {
736
+ const base = relPath.split("/").pop() ?? "";
737
+ const i = base.lastIndexOf(".");
738
+ if (i <= 0) return "";
739
+ return base.slice(i).toLowerCase();
740
+ }
741
+ function allowedExtensionSet2(list) {
742
+ return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
743
+ }
744
+ function normalizeEol(text) {
745
+ return text.replace(/\r\n/g, "\n");
746
+ }
747
+ function combineIgnore(extra) {
748
+ return (relPath) => defaultPullIgnore(relPath) || (extra?.(relPath) ?? false);
749
+ }
750
+ async function readLocalText(absPath) {
751
+ try {
752
+ return await fs4.readFile(absPath, "utf8");
753
+ } catch (err) {
754
+ const code = err.code;
755
+ if (code === "ENOENT") return void 0;
756
+ throw err;
757
+ }
758
+ }
759
+ async function loadAllowedExtensions(client) {
760
+ if (client.mock) return DEFAULT_PULL_EXTENSIONS;
761
+ try {
762
+ const cfg = normalizeWorkspaceConfig(
763
+ await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
764
+ );
765
+ return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
766
+ } catch {
767
+ return DEFAULT_PULL_EXTENSIONS;
768
+ }
769
+ }
770
+ async function fetchRemoteFiles(client) {
771
+ if (client.mock) return [];
772
+ return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(client.appId), { method: "GET" }));
773
+ }
774
+ async function planWorkspacePull(client, dir, options = {}) {
775
+ const ignore = combineIgnore(options.ignore);
776
+ const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
777
+ const remoteRaw = await fetchRemoteFiles(client);
778
+ const skippedRemotePaths = [];
779
+ const remoteByPath = /* @__PURE__ */ new Map();
780
+ for (const file of remoteRaw) {
781
+ const rel = normalizePath(file.filePath);
782
+ if (!isSafeRelPath(rel) || ignore(rel)) {
783
+ skippedRemotePaths.push(rel || file.filePath);
784
+ continue;
785
+ }
786
+ remoteByPath.set(rel, file.content);
787
+ }
788
+ let localFiles = [];
789
+ try {
790
+ localFiles = await collectFiles(dir, ignore);
791
+ } catch (err) {
792
+ const code = err.code;
793
+ if (code !== "ENOENT") throw err;
794
+ }
795
+ const localByPath = /* @__PURE__ */ new Map();
796
+ for (const file of localFiles) {
797
+ const rel = normalizePath(file.relPath);
798
+ if (!allowed.has(extensionOf2(rel))) continue;
799
+ const content = await readLocalText(file.absPath);
800
+ if (content !== void 0) localByPath.set(rel, content);
801
+ }
802
+ const changes = [];
803
+ const seen = /* @__PURE__ */ new Set();
804
+ for (const [rel, remoteContent] of remoteByPath) {
805
+ seen.add(rel);
806
+ const localContent = localByPath.get(rel);
807
+ if (localContent === void 0) {
808
+ changes.push({ path: rel, kind: "remote-only", remoteContent });
809
+ } else if (normalizeEol(localContent) === normalizeEol(remoteContent)) {
810
+ changes.push({ path: rel, kind: "identical", localContent, remoteContent });
811
+ } else {
812
+ changes.push({ path: rel, kind: "modified", localContent, remoteContent });
813
+ }
814
+ }
815
+ for (const [rel, localContent] of localByPath) {
816
+ if (seen.has(rel)) continue;
817
+ changes.push({ path: rel, kind: "local-only", localContent });
818
+ }
819
+ const remoteEmpty = remoteByPath.size === 0;
820
+ let seededDefaults = false;
821
+ if (remoteEmpty) {
822
+ const localMain = localByPath.get(DEFAULT_ENTRY_PATH);
823
+ if (localMain === void 0) {
824
+ changes.push({ path: DEFAULT_ENTRY_PATH, kind: "seed", remoteContent: DEFAULT_MAIN_TS });
825
+ seededDefaults = true;
826
+ }
827
+ }
828
+ changes.sort((a, b) => a.path.localeCompare(b.path));
829
+ return {
830
+ remoteEmpty,
831
+ seededDefaults,
832
+ mock: client.mock || void 0,
833
+ skippedRemotePaths,
834
+ changes
835
+ };
836
+ }
837
+ async function applyWorkspacePull(dir, plan, options = {}) {
838
+ const written = [];
839
+ const skippedConflicts = [];
840
+ for (const change of plan.changes) {
841
+ if (change.kind === "identical" || change.kind === "local-only") continue;
842
+ if (change.kind === "modified" && !options.force) {
843
+ skippedConflicts.push(change.path);
844
+ continue;
845
+ }
846
+ const content = change.remoteContent ?? "";
847
+ const abs = resolveWorkspaceAssetPath(dir, change.path);
848
+ await fs4.mkdir(path5.dirname(abs), { recursive: true });
849
+ await fs4.writeFile(abs, content, "utf8");
850
+ written.push(change.path);
851
+ }
852
+ return { written, skippedConflicts };
853
+ }
854
+ async function pullWorkspace(client, dir, options = {}) {
855
+ const plan = await planWorkspacePull(client, dir, { ignore: options.ignore });
856
+ if (options.dryRun) {
857
+ const skippedConflicts = plan.changes.filter((c) => c.kind === "modified" && !options.force).map((c) => c.path);
858
+ return { plan, written: [], skippedConflicts, dryRun: true, mock: plan.mock };
859
+ }
860
+ const applied = await applyWorkspacePull(dir, plan, { force: options.force });
861
+ return { plan, ...applied, mock: plan.mock };
862
+ }
863
+ var KIND_LABEL = {
864
+ identical: "same ",
865
+ "remote-only": "create",
866
+ "local-only": "keep ",
867
+ modified: "modify",
868
+ seed: "seed "
869
+ };
870
+ function formatWorkspacePullPlan(plan) {
871
+ const lines = [];
872
+ if (plan.mock) lines.push("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u6309\u8FDC\u7AEF\u4E3A\u7A7A\u5904\u7406");
873
+ const remoteCount = plan.changes.filter((c) => c.kind !== "local-only" && c.kind !== "seed").length;
874
+ if (plan.remoteEmpty) {
875
+ lines.push(
876
+ 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"
877
+ );
878
+ } else {
879
+ lines.push(`\u8FDC\u7AEF ${remoteCount} \u4E2A\u6587\u4EF6`);
880
+ }
881
+ for (const change of plan.changes) {
882
+ let extra = "";
883
+ if (change.kind === "modified") extra = " (\u51B2\u7A81,\u9ED8\u8BA4\u4E0D\u8986\u76D6)";
884
+ if (change.kind === "local-only") extra = " (\u4EC5\u672C\u5730)";
885
+ if (change.kind === "seed") extra = " (\u9ED8\u8BA4 TwinApp)";
886
+ lines.push(` ${KIND_LABEL[change.kind]} ${change.path}${extra}`);
887
+ }
888
+ if (plan.skippedRemotePaths.length > 0) {
889
+ lines.push(`\u8DF3\u8FC7\u975E\u6CD5/\u51ED\u636E\u8DEF\u5F84: ${plan.skippedRemotePaths.join(", ")}`);
890
+ }
891
+ return lines.join("\n");
892
+ }
893
+ function formatWorkspacePullResult(result) {
894
+ const lines = [formatWorkspacePullPlan(result.plan)];
895
+ if (result.dryRun) {
896
+ lines.push("dry-run:\u672A\u5199\u76D8");
897
+ if (result.skippedConflicts.length > 0) {
898
+ lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force)`);
899
+ }
900
+ return lines.join("\n");
901
+ }
902
+ if (result.written.length > 0) {
903
+ lines.push(`\u5DF2\u5199\u5165 ${result.written.length} \u4E2A\u6587\u4EF6: ${result.written.join(", ")}`);
904
+ } else {
905
+ lines.push("\u672A\u5199\u5165\u6587\u4EF6");
906
+ }
907
+ if (result.skippedConflicts.length > 0) {
908
+ lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force): ${result.skippedConflicts.join(", ")}`);
909
+ }
910
+ return lines.join("\n");
911
+ }
912
+
913
+ // src/skills.ts
914
+ import { existsSync, promises as fs5 } from "fs";
915
+ import path6 from "path";
402
916
  import { fileURLToPath as fileURLToPath2 } from "url";
403
917
  var SKILL_NAMES = [
404
918
  "easytwin-render",
@@ -421,7 +935,8 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
421
935
  skipLibCheck: true,
422
936
  noEmit: true,
423
937
  paths: {
424
- "@easytwin/runtime": [".easytwin/types"]
938
+ "@easytwin/runtime": [".easytwin/types"],
939
+ "@easytwin/apps": [".easytwin/types/apps"]
425
940
  }
426
941
  },
427
942
  include: ["src/**/*.ts"]
@@ -433,22 +948,55 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
433
948
  var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
434
949
  "skipLibCheck": true,
435
950
  "paths": {
436
- "@easytwin/runtime": [".easytwin/types"]
951
+ "@easytwin/runtime": [".easytwin/types"],
952
+ "@easytwin/apps": [".easytwin/types/apps"]
437
953
  }`;
438
- var RUN_CONTEXT_DECL = `
439
- /** \u9884\u89C8\u9875 Run \u6309\u94AE\u6CE8\u5165\u7684\u8FD0\u884C\u4E0A\u4E0B\u6587(\u89C1 D12)\u3002 */
440
- export interface EasyTwinRunContext {
954
+ var APPS_TYPES_FILE = "apps.d.ts";
955
+ var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
956
+
957
+ export type TwinAppContext = {
958
+ app: {
959
+ id: string;
960
+ mode: "preview" | "publish";
961
+ };
441
962
  engine: RuntimeEngine;
442
- runtime: typeof import("@easytwin/runtime");
443
- sceneJson: SceneJson;
963
+ container: HTMLElement;
964
+ runtimeScene: RuntimeScene | null;
965
+ scene: RuntimeScene["sceneObject"] | null;
966
+ camera: RuntimeScene["camera"]["main"] | null;
967
+ sceneManager: {
968
+ currentSceneId: string;
969
+ runtime: SceneManager | null;
970
+ loadScene(sceneId: string): Promise<void>;
971
+ };
972
+ logger: {
973
+ log(...args: unknown[]): void;
974
+ info(...args: unknown[]): void;
975
+ warn(...args: unknown[]): void;
976
+ error(...args: unknown[]): void;
977
+ };
978
+ assets: {
979
+ text(path: string): Promise<string>;
980
+ json<T = unknown>(path: string): Promise<T>;
981
+ };
982
+ cleanup(fn: () => void | Promise<void>): void;
983
+ sceneCleanup(fn: () => void | Promise<void>): void;
984
+ };
985
+
986
+ export declare abstract class TwinApp {
987
+ init?(ctx: TwinAppContext): void | Promise<void>;
988
+ onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
989
+ onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
990
+ onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
991
+ onDispose?(ctx: TwinAppContext): void | Promise<void>;
992
+ onError?(ctx: TwinAppContext, error: unknown): boolean | void;
444
993
  }
994
+
995
+ export declare function defineApp<T>(app: T): T;
445
996
  `;
446
997
  function buildRuntimeTypesContent(sourceDts) {
447
- const trimmed = sourceDts.replace(/\s+$/, "");
448
- if (trimmed.includes("export interface EasyTwinRunContext")) return `${trimmed}
998
+ return `${sourceDts.replace(/\s+$/, "")}
449
999
  `;
450
- return `${trimmed}
451
- ${RUN_CONTEXT_DECL}`;
452
1000
  }
453
1001
  function normalizeTargets(target = "all") {
454
1002
  if (target === "all") return ["cursor", "claude", "codex", "qoder"];
@@ -459,69 +1007,69 @@ function resolveSkillsSourceDir() {
459
1007
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
460
1008
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
461
1009
  }
462
- const here = path4.dirname(fileURLToPath2(import.meta.url));
463
- return path4.resolve(here, "..", "skills");
1010
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1011
+ return path6.resolve(here, "..", "skills");
464
1012
  }
465
1013
  function resolveRuntimeTypesSourceFile() {
466
1014
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
467
1015
  throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
468
1016
  }
469
- const here = path4.dirname(fileURLToPath2(import.meta.url));
470
- const fromDist = path4.join(here, "runtime-types", "index.d.ts");
471
- const fromSrc = path4.resolve(here, "lib", "index.d.ts");
1017
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1018
+ const fromDist = path6.join(here, "runtime-types", "index.d.ts");
1019
+ const fromSrc = path6.resolve(here, "lib", "index.d.ts");
472
1020
  if (existsSync(fromDist)) return fromDist;
473
1021
  if (existsSync(fromSrc)) return fromSrc;
474
1022
  throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
475
1023
  }
476
1024
  async function readJson(file) {
477
- return JSON.parse(await fs4.readFile(file, "utf8"));
1025
+ return JSON.parse(await fs5.readFile(file, "utf8"));
478
1026
  }
479
1027
  async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
480
- const marker = path4.join(skillsDir, ".easytwin-source.json");
1028
+ const marker = path6.join(skillsDir, ".easytwin-source.json");
481
1029
  try {
482
1030
  const meta = await readJson(marker);
483
1031
  if (typeof meta.version === "string") return meta.version;
484
1032
  } catch {
485
1033
  }
486
1034
  try {
487
- const pkg = await readJson(path4.resolve(skillsDir, "..", "package.json"));
1035
+ const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
488
1036
  return pkg.version;
489
1037
  } catch {
490
1038
  return "0.0.0";
491
1039
  }
492
1040
  }
493
1041
  async function copyDir(src, dest) {
494
- await fs4.mkdir(dest, { recursive: true });
495
- const entries = await fs4.readdir(src, { withFileTypes: true });
1042
+ await fs5.mkdir(dest, { recursive: true });
1043
+ const entries = await fs5.readdir(src, { withFileTypes: true });
496
1044
  for (const entry of entries) {
497
- const s = path4.join(src, entry.name);
498
- const d = path4.join(dest, entry.name);
1045
+ const s = path6.join(src, entry.name);
1046
+ const d = path6.join(dest, entry.name);
499
1047
  if (entry.isDirectory()) await copyDir(s, d);
500
- else if (entry.isFile()) await fs4.copyFile(s, d);
1048
+ else if (entry.isFile()) await fs5.copyFile(s, d);
501
1049
  }
502
1050
  }
503
1051
  async function dirMatches(src, dest) {
504
1052
  let sourceEntries;
505
1053
  try {
506
- sourceEntries = await fs4.readdir(src);
1054
+ sourceEntries = await fs5.readdir(src);
507
1055
  } catch {
508
1056
  return false;
509
1057
  }
510
1058
  for (const name of sourceEntries) {
511
- const s = path4.join(src, name);
512
- const d = path4.join(dest, name);
513
- const sStat = await fs4.stat(s);
1059
+ const s = path6.join(src, name);
1060
+ const d = path6.join(dest, name);
1061
+ const sStat = await fs5.stat(s);
514
1062
  if (sStat.isDirectory()) {
515
1063
  if (!await dirMatches(s, d)) return false;
516
1064
  } else {
517
1065
  let dStat;
518
1066
  try {
519
- dStat = await fs4.stat(d);
1067
+ dStat = await fs5.stat(d);
520
1068
  } catch {
521
1069
  return false;
522
1070
  }
523
1071
  if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
524
- if (!(await fs4.readFile(s)).equals(await fs4.readFile(d))) return false;
1072
+ if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
525
1073
  }
526
1074
  }
527
1075
  return true;
@@ -532,18 +1080,18 @@ function actionFor(exists, matches) {
532
1080
  }
533
1081
  async function writeMeta(destDir, version) {
534
1082
  const meta = { name: "@easytwin/devkit", version };
535
- await fs4.writeFile(path4.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
1083
+ await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
536
1084
  }
537
1085
  async function syncToDir(target, sourceRoot, targetRoot, version) {
538
1086
  const entries = [];
539
1087
  for (const name of SKILL_NAMES) {
540
- const src = path4.join(sourceRoot, name);
541
- const dest = path4.join(targetRoot, name);
542
- const exists = await fs4.stat(dest).then(() => true).catch(() => false);
1088
+ const src = path6.join(sourceRoot, name);
1089
+ const dest = path6.join(targetRoot, name);
1090
+ const exists = await fs5.stat(dest).then(() => true).catch(() => false);
543
1091
  const matches = await dirMatches(src, dest);
544
1092
  const action = actionFor(exists, matches);
545
1093
  if (action !== "unchanged") {
546
- await fs4.rm(dest, { recursive: true, force: true });
1094
+ await fs5.rm(dest, { recursive: true, force: true });
547
1095
  await copyDir(src, dest);
548
1096
  }
549
1097
  entries.push({ name, action });
@@ -562,19 +1110,19 @@ function buildCodexSegment(version) {
562
1110
  "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
563
1111
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
564
1112
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
565
- "- `easytwin-upload`:\u5168\u91CF\u4E0A\u4F20\u5F00\u53D1\u4EA7\u7269\u76EE\u5F55(\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
1113
+ "- `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",
566
1114
  "",
567
1115
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
568
1116
  CODEX_MARKER_END
569
1117
  ].join("\n");
570
1118
  }
571
1119
  async function syncToCodex(sourceRoot, cwd, version) {
572
- const agentsFile = path4.join(cwd, "AGENTS.md");
1120
+ const agentsFile = path6.join(cwd, "AGENTS.md");
573
1121
  const segment = buildCodexSegment(version);
574
1122
  let content = "";
575
1123
  let exists = true;
576
1124
  try {
577
- content = await fs4.readFile(agentsFile, "utf8");
1125
+ content = await fs5.readFile(agentsFile, "utf8");
578
1126
  } catch {
579
1127
  exists = false;
580
1128
  }
@@ -592,39 +1140,50 @@ async function syncToCodex(sourceRoot, cwd, version) {
592
1140
  next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
593
1141
  }
594
1142
  if (action !== "unchanged") {
595
- await fs4.mkdir(cwd, { recursive: true });
596
- await fs4.writeFile(agentsFile, next, "utf8");
1143
+ await fs5.mkdir(cwd, { recursive: true });
1144
+ await fs5.writeFile(agentsFile, next, "utf8");
597
1145
  }
598
1146
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
599
1147
  }
600
1148
  async function syncRuntimeTypes(cwd, typesSourceFile) {
601
- const destDir = path4.join(cwd, ".easytwin", "types");
602
- const destFile = path4.join(destDir, "index.d.ts");
603
- const content = buildRuntimeTypesContent(await fs4.readFile(typesSourceFile, "utf8"));
604
- let exists = true;
605
- let current = "";
1149
+ const destDir = path6.join(cwd, ".easytwin", "types");
1150
+ const destFile = path6.join(destDir, "index.d.ts");
1151
+ const appsFile = path6.join(destDir, APPS_TYPES_FILE);
1152
+ const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
1153
+ let runtimeCurrent = "";
1154
+ let appsCurrent = "";
1155
+ let runtimeExists = true;
1156
+ let appsExists = true;
606
1157
  try {
607
- current = await fs4.readFile(destFile, "utf8");
1158
+ runtimeCurrent = await fs5.readFile(destFile, "utf8");
608
1159
  } catch {
609
- exists = false;
1160
+ runtimeExists = false;
610
1161
  }
611
- const action = actionFor(exists, current === content);
612
- if (action !== "unchanged") {
613
- await fs4.mkdir(destDir, { recursive: true });
614
- await fs4.writeFile(destFile, content, "utf8");
1162
+ try {
1163
+ appsCurrent = await fs5.readFile(appsFile, "utf8");
1164
+ } catch {
1165
+ appsExists = false;
1166
+ }
1167
+ const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
1168
+ const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
1169
+ const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
1170
+ if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
1171
+ await fs5.mkdir(destDir, { recursive: true });
1172
+ if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
1173
+ if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
615
1174
  }
616
- const tsconfigPath = path4.join(cwd, "tsconfig.json");
1175
+ const tsconfigPath = path6.join(cwd, "tsconfig.json");
617
1176
  let tsconfig;
618
1177
  let pathsHint;
619
1178
  try {
620
- const existing = await fs4.readFile(tsconfigPath, "utf8");
1179
+ const existing = await fs5.readFile(tsconfigPath, "utf8");
621
1180
  if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
622
1181
  else {
623
1182
  tsconfig = "manual-paths";
624
1183
  pathsHint = TSCONFIG_PATHS_HINT;
625
1184
  }
626
1185
  } catch {
627
- await fs4.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1186
+ await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
628
1187
  tsconfig = "created";
629
1188
  }
630
1189
  return { action, tsconfig, pathsHint };
@@ -635,9 +1194,9 @@ async function syncSkills(options) {
635
1194
  const version = options.version ?? await readDevkitVersion(sourceRoot);
636
1195
  const summaries = [];
637
1196
  for (const target of targets) {
638
- if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
639
- else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
640
- else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path4.join(options.cwd, ".qoder", "skills"), version));
1197
+ if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
1198
+ else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
1199
+ else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
641
1200
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
642
1201
  }
643
1202
  const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
@@ -646,11 +1205,33 @@ async function syncSkills(options) {
646
1205
 
647
1206
  // src/bundle.ts
648
1207
  import * as esbuild from "esbuild-wasm";
649
- import { promises as fs5 } from "fs";
650
- import path5 from "path";
1208
+ import { promises as fs6 } from "fs";
1209
+ import path7 from "path";
1210
+
1211
+ // src/apps.ts
1212
+ var PORTABLE_RUNTIME_EXPORTS = [
1213
+ "THREE",
1214
+ "RuntimeEngine",
1215
+ "SceneManager",
1216
+ "LoadSceneMode",
1217
+ "convertObjToComponentJson"
1218
+ ];
1219
+ function portableRuntimeWarning(names) {
1220
+ const extra = [...new Set(names)].filter(
1221
+ (n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
1222
+ );
1223
+ const ns = [...names].includes("*");
1224
+ if (!ns && extra.length === 0) return void 0;
1225
+ const detail = ns ? "namespace import *" : extra.sort().join(", ");
1226
+ 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`;
1227
+ }
1228
+
1229
+ // src/bundle.ts
651
1230
  var USER_ENTRY = "src/main.ts";
652
1231
  var DEFAULT_BUNDLE_OUT = "dist/main.js";
653
1232
  var RUNTIME_MODULE = "@easytwin/runtime";
1233
+ var APPS_MODULE = "@easytwin/apps";
1234
+ var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
654
1235
  var BundleError = class extends Error {
655
1236
  constructor(message) {
656
1237
  super(message);
@@ -658,7 +1239,7 @@ var BundleError = class extends Error {
658
1239
  }
659
1240
  };
660
1241
  function isRelativeOrAbsolute(spec) {
661
- return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path5.isAbsolute(spec);
1242
+ return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
662
1243
  }
663
1244
  function whitelistPlugin() {
664
1245
  return {
@@ -667,11 +1248,11 @@ function whitelistPlugin() {
667
1248
  build2.onResolve({ filter: /.*/ }, (args) => {
668
1249
  if (args.kind === "entry-point") return void 0;
669
1250
  if (isRelativeOrAbsolute(args.path)) return void 0;
670
- if (args.path === RUNTIME_MODULE) return { path: args.path, external: true };
1251
+ if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
671
1252
  return {
672
1253
  errors: [
673
1254
  {
674
- 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`
1255
+ 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`
675
1256
  }
676
1257
  ]
677
1258
  };
@@ -685,14 +1266,29 @@ function formatEsbuildMessages(messages) {
685
1266
  return `${loc}${m.text}`;
686
1267
  }).join("\n");
687
1268
  }
1269
+ function collectBundledRuntimeExports(code) {
1270
+ const names = [];
1271
+ if (/import\s+\*\s+as\s+[\w$]+\s+from\s*["']@easytwin\/runtime["']/.test(code)) names.push("*");
1272
+ const named = /import\s*\{([^}]+)\}\s*from\s*["']@easytwin\/runtime["']/g;
1273
+ for (const match of code.matchAll(named)) {
1274
+ const body = match[1] ?? "";
1275
+ for (const part of body.split(",")) {
1276
+ const token = part.replace(/\btype\b/g, "").trim();
1277
+ if (!token) continue;
1278
+ const id = token.split(/\s+as\s+/)[0]?.trim();
1279
+ if (id) names.push(id);
1280
+ }
1281
+ }
1282
+ return names;
1283
+ }
688
1284
  async function bundleUserCode(options) {
689
- const cwd = path5.resolve(options.cwd);
690
- const entry = path5.join(cwd, USER_ENTRY);
1285
+ const cwd = path7.resolve(options.cwd);
1286
+ const entry = path7.join(cwd, USER_ENTRY);
691
1287
  try {
692
- await fs5.access(entry);
1288
+ await fs6.access(entry);
693
1289
  } catch {
694
1290
  throw new BundleError(
695
- `\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`
1291
+ `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
696
1292
  );
697
1293
  }
698
1294
  let result;
@@ -723,16 +1319,18 @@ async function bundleUserCode(options) {
723
1319
  if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
724
1320
  const code = file.text;
725
1321
  const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
1322
+ const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
1323
+ if (portable) warnings.push(portable);
726
1324
  if (options.outFile) {
727
- const outFile = path5.isAbsolute(options.outFile) ? options.outFile : path5.join(cwd, options.outFile);
728
- await fs5.mkdir(path5.dirname(outFile), { recursive: true });
729
- await fs5.writeFile(outFile, code, "utf8");
1325
+ const outFile = path7.isAbsolute(options.outFile) ? options.outFile : path7.join(cwd, options.outFile);
1326
+ await fs6.mkdir(path7.dirname(outFile), { recursive: true });
1327
+ await fs6.writeFile(outFile, code, "utf8");
730
1328
  }
731
1329
  return { code, warnings };
732
1330
  }
733
1331
  async function typesMissingHint(cwd) {
734
1332
  try {
735
- await fs5.access(path5.join(cwd, ".easytwin", "types", "index.d.ts"));
1333
+ await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
736
1334
  return void 0;
737
1335
  } catch {
738
1336
  return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
@@ -748,7 +1346,7 @@ async function readVersion() {
748
1346
  return "0.0.0";
749
1347
  }
750
1348
  }
751
- var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20\u8D70\u672C\u5730 mock";
1349
+ var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20/\u5DE5\u4F5C\u533A\u62C9\u53D6\u8D70\u672C\u5730 mock";
752
1350
  async function prompt(question) {
753
1351
  const rl = createInterface({ input: stdin, output: stdout });
754
1352
  try {
@@ -783,26 +1381,35 @@ function buildProgram(cwd, version = "0.0.0") {
783
1381
  program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
784
1382
  program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--env <prod|test>", "\u670D\u52A1\u73AF\u5883(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
785
1383
  const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
786
- scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F").action(async () => {
1384
+ scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
787
1385
  const config = await loadConfig(cwd);
788
1386
  if (config.mock) console.log(MOCK_MODE_HINT);
789
1387
  const client = new EasyTwinClient(config);
790
- const scenes = await listScenes(client);
1388
+ const scenes = await listScenes(client, { cwd });
791
1389
  if (scenes.length === 0) {
792
1390
  console.log("(\u65E0\u573A\u666F)");
793
- return;
1391
+ } else {
1392
+ for (const s of scenes) console.log(`${s.id} ${s.name}`);
794
1393
  }
795
- for (const s of scenes) console.log(`${s.id} ${s.name}`);
1394
+ console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
796
1395
  });
797
1396
  scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ./<id>.scene.json)").action(async (id, opts) => {
798
1397
  const config = await loadConfig(cwd);
799
1398
  if (config.mock) console.log(MOCK_MODE_HINT);
800
1399
  const client = new EasyTwinClient(config);
801
1400
  const scene2 = await pullScene(client, id);
802
- const out = opts.out ?? path6.join(cwd, `${id}.scene.json`);
1401
+ const out = opts.out ?? path8.join(cwd, `${id}.scene.json`);
803
1402
  const file = await saveSceneJson(scene2, out);
804
1403
  console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
805
1404
  });
1405
+ program.command("pull").description("\u62C9\u53D6\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5219\u5199\u5165\u9ED8\u8BA4 src/main.ts;\u51B2\u7A81\u9ED8\u8BA4\u4E0D\u8986\u76D6)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").option("--force", "\u7528\u8FDC\u7AEF\u5185\u5BB9\u8986\u76D6\u672C\u5730\u51B2\u7A81\u6587\u4EF6").option("--dry-run", "\u53EA\u6253\u5370 diff,\u4E0D\u5199\u76D8").action(async (dirArg, opts) => {
1406
+ const config = await loadConfig(cwd);
1407
+ if (config.mock) console.log(MOCK_MODE_HINT);
1408
+ const client = new EasyTwinClient(config);
1409
+ const target = dirArg ? path8.resolve(cwd, dirArg) : cwd;
1410
+ const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
1411
+ console.log(formatWorkspacePullResult(result));
1412
+ });
806
1413
  program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
807
1414
  const config = await loadConfig(cwd);
808
1415
  if (config.mock) console.log(MOCK_MODE_HINT);
@@ -816,11 +1423,12 @@ function buildProgram(cwd, version = "0.0.0") {
816
1423
  });
817
1424
  process.stderr.write("\n");
818
1425
  });
819
- program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8\u4F9D\u8D56 @easytwin/runtime)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
1426
+ program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8 @easytwin/runtime \u4E0E @easytwin/apps)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
820
1427
  const hint = await typesMissingHint(cwd);
821
1428
  if (hint) console.warn(hint);
822
- const outFile = path6.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
823
- await bundleUserCode({ cwd, outFile });
1429
+ const outFile = path8.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
1430
+ const result = await bundleUserCode({ cwd, outFile });
1431
+ for (const warning of result.warnings) console.warn(warning);
824
1432
  console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
825
1433
  });
826
1434
  const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");